From bbf691ad4295d94d97b07654468d2594378fd5be Mon Sep 17 00:00:00 2001 From: Lan Sovinc Date: Tue, 11 Mar 2025 14:21:40 +0100 Subject: [PATCH 1/8] updated prompt and added optional date to parsing --- src/middlewares/drafts.js | 3 ++- src/middlewares/openai.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/middlewares/drafts.js b/src/middlewares/drafts.js index 72dbe9c..46e46d0 100644 --- a/src/middlewares/drafts.js +++ b/src/middlewares/drafts.js @@ -25,12 +25,13 @@ const parseSmsWithAI = async (sms) => { const smsAI = await openai.createExpense(sms) return { description: smsAI.description, - date: Date.now(), + date: new Date(smsAI.date) || Date.now(), amount: parseInt(smsAI.amount), category1: smsAI.category1, category2: smsAI.category2 }; } catch (e) { + console.log(e) throw new Error("AI SMS parsing failed.") } }; diff --git a/src/middlewares/openai.js b/src/middlewares/openai.js index a19f16c..f7d7b19 100644 --- a/src/middlewares/openai.js +++ b/src/middlewares/openai.js @@ -16,7 +16,8 @@ const createExpense = async (sms) => { role: "system", content: //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: date (change from DD.MM.YYYY to MM.DD.YYYY), amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message. For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." - "You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message (use just the provided category names, do not alter category names). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." + //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message (use just the provided category names, do not alter category names). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." + "You are a helpful finance tracking assistant. You process and categorize received SMS expense data sent in JSON format. Prepare a json response object containing the following: amount (the amount will be in Euros but always convert it to integer cents value), and description (maximum of 5 words, based on what you receive). If the data contains the date (which will always be in European DD-MM-YYYY format), include it in the property 'date' so that the backend can correctly parse the value with `new Date(date)`, otherwise don't create the json property for date at all. Then categorize the transaction by choosing the right category1 value and corresponding category2 value based on the json in the next message (use just the provided category names exactly, do not alter category names, keep the capitalization the same as in the provided json). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true. Example 1: {'description': 'some purchase', 'date': '2023-12-13', 'amount:' 190, 'category1': 'Draft', 'category2': 'Draft'}, example 2: {'description': 'Mercator supermarket', 'amount:' 190, 'category1': 'Foods & Drinks', 'category2': 'Groceries'}." }, { role: "system", From e91ddb5a848ddac24e8952989c05076bcaf944a8 Mon Sep 17 00:00:00 2001 From: Lan Sovinc Date: Tue, 11 Mar 2025 14:26:40 +0100 Subject: [PATCH 2/8] updated prompt and added optional date to parsing 2 --- src/middlewares/openai.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/openai.js b/src/middlewares/openai.js index f7d7b19..3d9c3ff 100644 --- a/src/middlewares/openai.js +++ b/src/middlewares/openai.js @@ -17,7 +17,7 @@ const createExpense = async (sms) => { content: //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: date (change from DD.MM.YYYY to MM.DD.YYYY), amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message. For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message (use just the provided category names, do not alter category names). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." - "You are a helpful finance tracking assistant. You process and categorize received SMS expense data sent in JSON format. Prepare a json response object containing the following: amount (the amount will be in Euros but always convert it to integer cents value), and description (maximum of 5 words, based on what you receive). If the data contains the date (which will always be in European DD-MM-YYYY format), include it in the property 'date' so that the backend can correctly parse the value with `new Date(date)`, otherwise don't create the json property for date at all. Then categorize the transaction by choosing the right category1 value and corresponding category2 value based on the json in the next message (use just the provided category names exactly, do not alter category names, keep the capitalization the same as in the provided json). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true. Example 1: {'description': 'some purchase', 'date': '2023-12-13', 'amount:' 190, 'category1': 'Draft', 'category2': 'Draft'}, example 2: {'description': 'Mercator supermarket', 'amount:' 190, 'category1': 'Foods & Drinks', 'category2': 'Groceries'}." + "You are a helpful finance tracking assistant. You process and categorize received SMS expense data sent in JSON format. Prepare a json response object containing the following: amount (the amount will be in Euros but always convert it to integer cents value), and description (maximum of 5 words, based on what you receive, ideally the name of the store/service). If the data contains the date (which will always be in European DD-MM-YYYY format), include it in the property 'date' so that the backend can correctly parse the value with `new Date(date)`, otherwise don't create the json property for date at all. Then categorize the transaction by choosing the right category1 value and corresponding category2 value based on the json in the next message (use just the provided category names exactly, do not alter category names, keep the capitalization the same as in the provided json). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true. Example 1: {'description': 'some purchase', 'date': '2023-12-13', 'amount:' 190, 'category1': 'Draft', 'category2': 'Draft'}, example 2: {'description': 'Mercator supermarket', 'amount:' 190, 'category1': 'Foods & Drinks', 'category2': 'Groceries'}." }, { role: "system", From 4991b374176f26d16b3c994d263178ec78489bcb Mon Sep 17 00:00:00 2001 From: Lan Sovinc Date: Tue, 11 Mar 2025 14:32:46 +0100 Subject: [PATCH 3/8] updated prompt and added optional date to parsing 3 --- src/middlewares/openai.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/middlewares/openai.js b/src/middlewares/openai.js index 3d9c3ff..36590ab 100644 --- a/src/middlewares/openai.js +++ b/src/middlewares/openai.js @@ -25,6 +25,7 @@ const createExpense = async (sms) => { }, {role: "user", content: sms}, ], + response_format: "json_object", }); return JSON.parse(completion.choices[0].message.content); From 47fdcdc0087d6f2a7e1b7a035e16230ddb08665c Mon Sep 17 00:00:00 2001 From: Lan Sovinc Date: Tue, 11 Mar 2025 15:07:22 +0100 Subject: [PATCH 4/8] define zod structure for the output and fix date --- package-lock.json | 16 +++++++++++++++- package.json | 3 ++- src/middlewares/drafts.js | 3 ++- src/middlewares/openai.js | 14 ++++++++++++-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58962e4..560fdf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "nodemon": "^2.0.4", "openai": "^4.86.2", "passport": "^0.4.1", - "passport-local": "^1.0.0" + "passport-local": "^1.0.0", + "zod": "^3.24.2" }, "devDependencies": { "@babel/core": "^7.23.9", @@ -8200,6 +8201,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -14173,6 +14182,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==" } } } diff --git a/package.json b/package.json index 61288fc..e678d2b 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "nodemon": "^2.0.4", "openai": "^4.86.2", "passport": "^0.4.1", - "passport-local": "^1.0.0" + "passport-local": "^1.0.0", + "zod": "^3.24.2" }, "devDependencies": { "@babel/core": "^7.23.9", diff --git a/src/middlewares/drafts.js b/src/middlewares/drafts.js index 46e46d0..848e5b3 100644 --- a/src/middlewares/drafts.js +++ b/src/middlewares/drafts.js @@ -23,9 +23,10 @@ const parseSmsManually = (sms, isNlb) => { const parseSmsWithAI = async (sms) => { try { const smsAI = await openai.createExpense(sms) + const parsedDate = (smsAI.date && (new Date(smsAI.date).toString() !== "Invalid Date")) ? new Date(smsAI.date) : Date.now() return { description: smsAI.description, - date: new Date(smsAI.date) || Date.now(), + date: parsedDate, amount: parseInt(smsAI.amount), category1: smsAI.category1, category2: smsAI.category2 diff --git a/src/middlewares/openai.js b/src/middlewares/openai.js index 36590ab..bdde1b0 100644 --- a/src/middlewares/openai.js +++ b/src/middlewares/openai.js @@ -1,5 +1,7 @@ import OpenAI from "openai"; import {categoriesExpenses} from "../models/expenses.js"; +import { z } from "zod"; +import {zodResponseFormat} from "openai/helpers/zod"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY || 'api-key', @@ -8,6 +10,14 @@ const openai = new OpenAI({ const createExpense = async (sms) => { + const ExpenseEntry = z.object({ + description: z.string(), + date: z.string().optional(), + amount: z.number().int(), + category1: z.string(), + category2: z.string(), + }); + const completion = await openai.chat.completions.create({ //model: "gpt-4-1106-preview", model: "gpt-4o-mini", @@ -17,7 +27,7 @@ const createExpense = async (sms) => { content: //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: date (change from DD.MM.YYYY to MM.DD.YYYY), amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message. For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message (use just the provided category names, do not alter category names). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." - "You are a helpful finance tracking assistant. You process and categorize received SMS expense data sent in JSON format. Prepare a json response object containing the following: amount (the amount will be in Euros but always convert it to integer cents value), and description (maximum of 5 words, based on what you receive, ideally the name of the store/service). If the data contains the date (which will always be in European DD-MM-YYYY format), include it in the property 'date' so that the backend can correctly parse the value with `new Date(date)`, otherwise don't create the json property for date at all. Then categorize the transaction by choosing the right category1 value and corresponding category2 value based on the json in the next message (use just the provided category names exactly, do not alter category names, keep the capitalization the same as in the provided json). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true. Example 1: {'description': 'some purchase', 'date': '2023-12-13', 'amount:' 190, 'category1': 'Draft', 'category2': 'Draft'}, example 2: {'description': 'Mercator supermarket', 'amount:' 190, 'category1': 'Foods & Drinks', 'category2': 'Groceries'}." + "You are a helpful finance tracking assistant. You process and categorize received SMS expense data sent in JSON format. Prepare a json response object containing the following: amount (the amount will be in Euros but always convert it to integer cents value), and description (maximum of 5 words, based on what you receive, ideally the name of the store/service). If the data contains the date (which will always be in European DD-MM-YYYY format), include it in the property 'date' so that the backend can correctly parse the value with `new Date(date)`, otherwise completely omit the date property from your response. Then categorize the transaction by choosing the right category1 value and corresponding category2 value based on the json in the next message (use just the provided category names exactly, do not alter category names, keep the capitalization the same as in the provided json). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." }, { role: "system", @@ -25,7 +35,7 @@ const createExpense = async (sms) => { }, {role: "user", content: sms}, ], - response_format: "json_object", + response_format: zodResponseFormat(ExpenseEntry, "expense") }); return JSON.parse(completion.choices[0].message.content); From 7dd3c8be4cf97a27ab87ac3cf3f599a3f784895c Mon Sep 17 00:00:00 2001 From: Lan Sovinc Date: Tue, 11 Mar 2025 15:17:28 +0100 Subject: [PATCH 5/8] fix prompt --- src/middlewares/openai.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares/openai.js b/src/middlewares/openai.js index bdde1b0..a3811b3 100644 --- a/src/middlewares/openai.js +++ b/src/middlewares/openai.js @@ -27,7 +27,7 @@ const createExpense = async (sms) => { content: //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: date (change from DD.MM.YYYY to MM.DD.YYYY), amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message. For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message (use just the provided category names, do not alter category names). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." - "You are a helpful finance tracking assistant. You process and categorize received SMS expense data sent in JSON format. Prepare a json response object containing the following: amount (the amount will be in Euros but always convert it to integer cents value), and description (maximum of 5 words, based on what you receive, ideally the name of the store/service). If the data contains the date (which will always be in European DD-MM-YYYY format), include it in the property 'date' so that the backend can correctly parse the value with `new Date(date)`, otherwise completely omit the date property from your response. Then categorize the transaction by choosing the right category1 value and corresponding category2 value based on the json in the next message (use just the provided category names exactly, do not alter category names, keep the capitalization the same as in the provided json). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." + "You are a helpful finance tracking assistant. You process and categorize received SMS expense data sent in JSON format. Prepare a json response object containing the following: amount (the amount will be in Euros but always convert it to integer cents value), and description (maximum of 5 words, based on what you receive, ideally the name of the store/service). If the data contains the date (which will always be in European DD.MM.YYYY format, so carefully rewrite it as YYYY-MM-DD), include it in the property 'date' in format YYYY-MM-DD, otherwise completely omit the date property from your response. Then categorize the transaction by choosing the right category1 value and corresponding category2 value based on the json in the next message (use just the provided category names exactly, do not alter category names, keep the capitalization the same as in the provided json). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." }, { role: "system", From 3daf2bd6765f9144bb940e1b81d2ea54ac057e20 Mon Sep 17 00:00:00 2001 From: Lan Sovinc Date: Tue, 11 Mar 2025 22:42:13 +0100 Subject: [PATCH 6/8] adjust description length in prompt and zod --- src/middlewares/openai.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middlewares/openai.js b/src/middlewares/openai.js index a3811b3..531d7ba 100644 --- a/src/middlewares/openai.js +++ b/src/middlewares/openai.js @@ -11,7 +11,7 @@ const openai = new OpenAI({ const createExpense = async (sms) => { const ExpenseEntry = z.object({ - description: z.string(), + description: z.string().max(32), date: z.string().optional(), amount: z.number().int(), category1: z.string(), @@ -27,7 +27,7 @@ const createExpense = async (sms) => { content: //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: date (change from DD.MM.YYYY to MM.DD.YYYY), amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message. For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." //"You process and categorize received SMS transaction data sent in JSON format. Prepare a json response object containing the following: amount (in cents), and description (maximum of 5 words). Then categorize the transaction by choosing the right category1 and corresponding category2 based on the json in the next message (use just the provided category names, do not alter category names). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." - "You are a helpful finance tracking assistant. You process and categorize received SMS expense data sent in JSON format. Prepare a json response object containing the following: amount (the amount will be in Euros but always convert it to integer cents value), and description (maximum of 5 words, based on what you receive, ideally the name of the store/service). If the data contains the date (which will always be in European DD.MM.YYYY format, so carefully rewrite it as YYYY-MM-DD), include it in the property 'date' in format YYYY-MM-DD, otherwise completely omit the date property from your response. Then categorize the transaction by choosing the right category1 value and corresponding category2 value based on the json in the next message (use just the provided category names exactly, do not alter category names, keep the capitalization the same as in the provided json). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." + "You are a helpful finance tracking assistant. You process and categorize received SMS expense data sent in JSON format. Prepare a json response object containing the following: amount (the amount will be in Euros but always convert it to integer cents value), and description (up to 32 characters, based on what you receive, ideally the name of the store/service). If the data contains the date (which will always be in European DD.MM.YYYY format, so carefully rewrite it as YYYY-MM-DD), include it in the property 'date' in format YYYY-MM-DD, otherwise completely omit the date property from your response. Then categorize the transaction by choosing the right category1 value and corresponding category2 value based on the json in the next message (use just the provided category names exactly, do not alter category names, keep the capitalization the same as in the provided json). For ambiguous cases, set 'Draft' for both 'category1' and 'category2'. If you encounter an error, return a json object with the error property equal to true." }, { role: "system", From ee8cd87b933f610439e27c2c294820d4d47e31a2 Mon Sep 17 00:00:00 2001 From: Lan Sovinc Date: Sat, 29 Mar 2025 19:40:59 +0100 Subject: [PATCH 7/8] remove description length zod --- src/middlewares/drafts.js | 2 +- src/middlewares/openai.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middlewares/drafts.js b/src/middlewares/drafts.js index 848e5b3..ae1f951 100644 --- a/src/middlewares/drafts.js +++ b/src/middlewares/drafts.js @@ -25,7 +25,7 @@ const parseSmsWithAI = async (sms) => { const smsAI = await openai.createExpense(sms) const parsedDate = (smsAI.date && (new Date(smsAI.date).toString() !== "Invalid Date")) ? new Date(smsAI.date) : Date.now() return { - description: smsAI.description, + description: smsAI.description?.slice(0, 31), date: parsedDate, amount: parseInt(smsAI.amount), category1: smsAI.category1, diff --git a/src/middlewares/openai.js b/src/middlewares/openai.js index 531d7ba..2888e9e 100644 --- a/src/middlewares/openai.js +++ b/src/middlewares/openai.js @@ -11,7 +11,7 @@ const openai = new OpenAI({ const createExpense = async (sms) => { const ExpenseEntry = z.object({ - description: z.string().max(32), + description: z.string(), date: z.string().optional(), amount: z.number().int(), category1: z.string(), From 1843e8e75d291e87ff5e2ef50501e933672c81f8 Mon Sep 17 00:00:00 2001 From: Lan Sovinc Date: Sun, 10 Aug 2025 01:05:02 +0200 Subject: [PATCH 8/8] add category2 sums to expense breakdown --- src/controllers/expenses.js | 47 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/controllers/expenses.js b/src/controllers/expenses.js index 84590cd..d7f586e 100644 --- a/src/controllers/expenses.js +++ b/src/controllers/expenses.js @@ -12,8 +12,8 @@ const findAllExpensesByAccountID = async (req, res) => { // Fetch expenses with specified conditions const expenses = await Expense - .find({accountID: req.params.aid}, "-file") - .sort({date: -1, _id: -1}) + .find({ accountID: req.params.aid }, "-file") + .sort({ date: -1, _id: -1 }) .skip(expensesPerPage * page) .limit(expensesPerPage); @@ -86,7 +86,7 @@ const findExpenseFileByID = async (req, res) => { // Create an expense const create = async (req, res) => { - const {userID, accountID, category1, category2, description, date, amount, file} = req.body; + const { userID, accountID, category1, category2, description, date, amount, file } = req.body; // Check expense description length if (description?.length > 32) { @@ -150,13 +150,13 @@ const remove = async (req, res) => { const expense = await Expense.findByIdAndDelete(req.params.id); if (!expense) { - return res.status(404).send({message: "No expense with selected ID!"}); + return res.status(404).send({ message: "No expense with selected ID!" }); } // Update account balance await updateAccountBalances.updateAllAccountBalances(expense.accountID, expense.amount, "+"); - res.send({message: "Expense deleted!"}); + res.send({ message: "Expense deleted!" }); } catch (error) { res.status(500).send({ @@ -171,7 +171,7 @@ const remove = async (req, res) => { // Update expense with requested ID const update = async (req, res) => { - const {accountID, category1, category2, description, date, amount, file} = req.body; + const { accountID, category1, category2, description, date, amount, file } = req.body; // Check expense description length if (description?.length > 32) { @@ -189,12 +189,12 @@ const update = async (req, res) => { let editedExpense = { // Add properties to the object - ...(category1 && {category1: category1}), - ...(category2 && {category2: category2}), - ...(accountID && {accountID: accountID}), - ...(description && {description: description}), - ...(date && {date: date}), - ...(amount && {amount: amount}), + ...(category1 && { category1: category1 }), + ...(category2 && { category2: category2 }), + ...(accountID && { accountID: accountID }), + ...(description && { description: description }), + ...(date && { date: date }), + ...(amount && { amount: amount }), }; if (file) { @@ -208,7 +208,7 @@ const update = async (req, res) => { try { // Fetch the old expense and edit it - const expense = await Expense.findByIdAndUpdate(req.params.id, {$set: editedExpense}); + const expense = await Expense.findByIdAndUpdate(req.params.id, { $set: editedExpense }); if (!expense) { return res.status(404).send({ @@ -236,7 +236,7 @@ const update = async (req, res) => { await updateAccountBalances.updateAllAccountBalances(expense.accountID, difference, operation); } - res.send({message: "Expense updated!"}); + res.send({ message: "Expense updated!" }); } catch (error) { res.status(500).send({ @@ -271,9 +271,22 @@ const expensesBreakdown = async (req, res) => { }; try { - const breakdown = await Expense.aggregate() - .match(filterObject) - .group({"_id": "$category1", "sum": {$sum: "$amount"}}); + const pipeline = [ + { $match: filterObject }, + { $group: { _id: { c1: "$category1", c2: "$category2" }, sum: { $sum: "$amount" } } }, + { $sort: { "_id.c1": 1, sum: -1 } }, + { + $group: { + _id: "$_id.c1", + total: { $sum: "$sum" }, + category2: { $push: { category2: "$_id.c2", sum: "$sum" } } + } + }, + { $project: { _id: 0, category1: "$_id", total: 1, category2: 1 } }, + { $sort: { total: -1 } } + ]; + + const breakdown = await Expense.aggregate(pipeline).option({ allowDiskUse: true }); res.status(200).json(breakdown);