const Expense = require('../models/Expense'); const { log } = require('../utils/activityLogger'); // GET /api/expenses exports.getExpenses = async (req, res) => { const { from, to, category, page = 1, limit = 50 } = req.query; const filter = {}; if (category) filter.category = category; if (from || to) { filter.date = {}; if (from) filter.date.$gte = new Date(from); if (to) filter.date.$lte = new Date(new Date(to).setHours(23, 59, 59, 999)); } const [expenses, total] = await Promise.all([ Expense.find(filter) .populate('createdBy', 'name username') .sort({ date: -1 }) .skip((page - 1) * limit) .limit(Number(limit)), Expense.countDocuments(filter), ]); res.json({ expenses, total, page: Number(page), pages: Math.ceil(total / limit) }); }; // POST /api/expenses exports.createExpense = async (req, res) => { const expense = await Expense.create({ ...req.body, createdBy: req.user._id }); log(req.user, 'ADD_EXPENSE', { entity: 'Expense', entityId: expense._id, detail: `${expense.title} Rs.${expense.amount}` }); res.status(201).json(expense); }; // POST /api/expenses/bulk — create multiple expenses in one go exports.createBulkExpenses = async (req, res) => { const { expenses, date } = req.body; if (!Array.isArray(expenses) || expenses.length === 0) { return res.status(400).json({ message: 'Array of expenses is required' }); } const defaultDate = date ? new Date(date) : new Date(); const docsToInsert = expenses .filter((e) => e.title && e.title.trim() && !isNaN(Number(e.amount)) && Number(e.amount) >= 0) .map((e) => ({ title: e.title.trim(), category: e.category || 'Misc', amount: Number(e.amount), date: e.date ? new Date(e.date) : defaultDate, note: e.note || '', recurring: Boolean(e.recurring), frequency: e.recurring ? e.frequency || 'monthly' : null, createdBy: req.user._id, })); if (docsToInsert.length === 0) { return res.status(400).json({ message: 'No valid expense entries provided' }); } const created = await Expense.insertMany(docsToInsert); const totalAmount = created.reduce((s, e) => s + e.amount, 0); log(req.user, 'BULK_EXPENSE', { entity: 'Expense', detail: `Added ${created.length} expenses totalling Rs.${totalAmount}`, }); res.status(201).json({ count: created.length, expenses: created }); }; // PUT /api/expenses/:id exports.updateExpense = async (req, res) => { const expense = await Expense.findById(req.params.id); if (!expense) return res.status(404).json({ message: 'Expense not found' }); // Build human-readable diff for audit trail const changedFields = Object.keys(req.body) .filter((k) => String(expense[k]) !== String(req.body[k])) .map((k) => `${k}: "${expense[k]}" → "${req.body[k]}"`) .join(', '); Object.assign(expense, req.body); expense.editLog.push({ user: req.user._id, changes: changedFields || 'no change' }); await expense.save(); log(req.user, 'EDIT_EXPENSE', { entity: 'Expense', entityId: expense._id, detail: changedFields }); res.json(expense); }; // DELETE /api/expenses/:id exports.deleteExpense = async (req, res) => { const expense = await Expense.findByIdAndDelete(req.params.id); if (!expense) return res.status(404).json({ message: 'Expense not found' }); log(req.user, 'DELETE_EXPENSE', { entity: 'Expense', entityId: expense._id, detail: expense.title }); res.json({ message: 'Expense deleted' }); }; // POST /api/expenses/:id/duplicate — copy to next month (recurring helper) exports.duplicateExpense = async (req, res) => { const source = await Expense.findById(req.params.id); if (!source) return res.status(404).json({ message: 'Expense not found' }); const newDate = new Date(source.date); newDate.setMonth(newDate.getMonth() + 1); const copy = await Expense.create({ title: source.title, category: source.category, amount: source.amount, date: newDate, note: source.note, recurring: source.recurring, frequency: source.frequency, createdBy: req.user._id, }); res.status(201).json(copy); };