From 68dc394b58efc11cff4bccf43cc41b4c7fdee2fd Mon Sep 17 00:00:00 2001 From: Lan Sovinc Date: Sun, 10 Aug 2025 17:39:51 +0200 Subject: [PATCH] add daily expense sums endpoint and optimize indexes for queries --- src/controllers/expenses.js | 48 ++++++++++++++++++++++++++++++++++++- src/models/expenses.js | 3 +++ src/models/incomes.js | 3 +++ src/routes/expenses.js | 3 +++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/controllers/expenses.js b/src/controllers/expenses.js index d7f586e..a76d4f0 100644 --- a/src/controllers/expenses.js +++ b/src/controllers/expenses.js @@ -297,6 +297,51 @@ const expensesBreakdown = async (req, res) => { } }; +// Return daily expense sums within a date range +const expensesDaily = async (req, res) => { + if (!req.query.startDate) { + return res.status(400).json({ + message: "Start date must be present!" + }); + } + + if (!req.query.endDate) { + return res.status(400).json({ + message: "End date must be present!" + }); + } + + const filterObject = { + accountID: req.params.aid, + date: { + $gte: new Date(req.query.startDate), + $lte: new Date(req.query.endDate) + } + }; + + try { + const pipeline = [ + { $match: filterObject }, + { + $group: { + _id: { $dateToString: { format: "%Y-%m-%d", date: "$date" } }, + total: { $sum: "$amount" } + } + }, + { $project: { _id: 0, date: "$_id", total: 1 } }, + { $sort: { date: 1 } } + ]; + + const daily = await Expense.aggregate(pipeline).option({ allowDiskUse: true }); + + res.status(200).json(daily); + } catch (error) { + res.status(500).send({ + message: error.message || "An error occurred while fetching daily expenses!" + }); + } +}; + export default { findAllExpensesByAccountID, findExpenseByID, @@ -304,5 +349,6 @@ export default { create, remove, update, - expensesBreakdown + expensesBreakdown, + expensesDaily, } \ No newline at end of file diff --git a/src/models/expenses.js b/src/models/expenses.js index 55f7adc..b28917b 100644 --- a/src/models/expenses.js +++ b/src/models/expenses.js @@ -58,6 +58,9 @@ let expense = new Schema({ timestamps: true }); +// Compound index to optimize account-scoped date queries and sorted pagination +expense.index({ accountID: 1, date: -1, _id: -1 }); + // validate category1 with array of allowed categories1 function validateCategory1(category) { diff --git a/src/models/incomes.js b/src/models/incomes.js index a253e55..bca15ca 100644 --- a/src/models/incomes.js +++ b/src/models/incomes.js @@ -30,6 +30,9 @@ const income = new Schema({ timestamps: true }); +// Compound index to optimize account-scoped date queries and sorted pagination (latest first) +income.index({ accountID: 1, date: -1, _id: -1 }); + // Validate category1 with array of allowed categories1 function validateCategory1(category) { diff --git a/src/routes/expenses.js b/src/routes/expenses.js index 8832343..99409b1 100644 --- a/src/routes/expenses.js +++ b/src/routes/expenses.js @@ -42,6 +42,9 @@ router.put("/:id", [authJWT.verifyTokenWhitelist, authJWT.verifyTokenExpense, dr // Return expense breakdown by primary categories router.get("/breakdown/:aid", [authJWT.verifyTokenWhitelist, authJWT.verifyTokenAccount], expensesController.expensesBreakdown); +// Return daily expense sums in date range +router.get("/daily/:aid", [authJWT.verifyTokenWhitelist, authJWT.verifyTokenAccount], expensesController.expensesDaily); + // Create sms expense draft router.post("/sms", [authJWT.verifyTokenWhitelist, drafts.createExpenseSMS], expensesController.create);