import { Router, Response } from 'express'; import Expense from '../models/Expense'; import { AuthRequest } from '../middleware/authenticate'; const router = Router(); router.get('/', async (req: AuthRequest, res: Response) => { try { const { page = 1, limit = 20, category } = req.query; const filter: any = { userId: req.userId }; if (category) filter.category = category; const expenses = await Expense.find(filter) .sort({ date: -1 }) .limit(Number(limit)) .skip((Number(page) - 1) * Number(limit)); const total = await Expense.countDocuments(filter); res.json({ expenses, total, page: Number(page), limit: Number(limit) }); } catch (err) { res.status(500).json({ message: 'Failed to fetch expenses', error: err }); } }); router.post('/', async (req: AuthRequest, res: Response) => { try { const expense = new Expense({ ...req.body, userId: req.userId }); await expense.save(); res.status(201).json(expense); } catch (err: any) { // Handle duplicate key error (MongoDB error code 11000) if (err.code === 11000) { const duplicateField = Object.keys(err.keyPattern || {})[0]; res.status(400).json({ message: `Expense with this ${duplicateField || 'reference'} already exists (duplicate prevented)`, isDuplicate: true, error: err }); } else if (err.name === 'ValidationError') { res.status(400).json({ message: 'Validation failed', error: err.message }); } else { res.status(400).json({ message: 'Failed to create expense', error: err }); } } }); router.get('/:id', async (req: AuthRequest, res: Response) => { try { const expense = await Expense.findOne({ _id: req.params.id, userId: req.userId }); if (!expense) return res.status(404).json({ message: 'Expense not found' }); res.json(expense); } catch (err) { res.status(500).json({ message: 'Failed to fetch expense', error: err }); } }); router.put('/:id', async (req: AuthRequest, res: Response) => { try { const expense = await Expense.findOneAndUpdate( { _id: req.params.id, userId: req.userId }, req.body, { new: true, runValidators: true } ); if (!expense) return res.status(404).json({ message: 'Expense not found' }); res.json(expense); } catch (err) { res.status(400).json({ message: 'Failed to update expense', error: err }); } }); // Clean up duplicate Australia Post expenses router.post('/cleanup-duplicates', async (req: AuthRequest, res: Response) => { try { // Find all Australia Post expenses for this user const expenses = await Expense.find({ userId: req.userId, vendor: 'Australia Post' }).sort({ dateCreated: 1 }); // Sort by creation date, keep earliest // Group by tracking number (reference field) const groupedByTracking = new Map(); expenses.forEach(expense => { if (expense.reference) { if (!groupedByTracking.has(expense.reference)) { groupedByTracking.set(expense.reference, []); } groupedByTracking.get(expense.reference).push(expense); } }); // Find duplicates and delete all but the first (earliest) one let deletedCount = 0; const duplicateGroups = []; for (const [trackingNumber, expenseGroup] of groupedByTracking) { if (expenseGroup.length > 1) { // Keep the first (earliest created) and delete the rest const toKeep = expenseGroup[0]; const toDelete = expenseGroup.slice(1); duplicateGroups.push({ trackingNumber, total: expenseGroup.length, keeping: { id: toKeep._id, date: toKeep.date, amount: toKeep.amount, created: toKeep.dateCreated }, deleting: toDelete.map((exp: any) => ({ id: exp._id, date: exp.date, amount: exp.amount, created: exp.dateCreated })) }); // Delete the duplicates await Expense.deleteMany({ _id: { $in: toDelete.map((exp: any) => exp._id) }, userId: req.userId }); deletedCount += toDelete.length; } } res.json({ message: `Cleaned up ${deletedCount} duplicate Australia Post expenses`, deletedCount, duplicateGroups: duplicateGroups.length, details: duplicateGroups }); } catch (err: any) { res.status(500).json({ message: 'Failed to cleanup duplicates', error: err }); } }); router.delete('/:id', async (req: AuthRequest, res: Response) => { try { const expense = await Expense.findOneAndDelete({ _id: req.params.id, userId: req.userId }); if (!expense) return res.status(404).json({ message: 'Expense not found' }); res.json({ message: 'Expense deleted' }); } catch (err) { res.status(500).json({ message: 'Failed to delete expense', error: err }); } }); export default router;