File size: 4,151 Bytes
1e480b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ec62259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1e480b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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);
};