Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 30 additions & 17 deletions src/controllers/expenses.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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({
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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);

Expand Down
6 changes: 4 additions & 2 deletions src/middlewares/drafts.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ 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: Date.now(),
description: smsAI.description?.slice(0, 31),
date: parsedDate,
amount: parseInt(smsAI.amount),
category1: smsAI.category1,
category2: smsAI.category2
};
} catch (e) {
console.log(e)
throw new Error("AI SMS parsing failed.")
}
};
Expand Down
14 changes: 13 additions & 1 deletion src/middlewares/openai.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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",
Expand All @@ -16,14 +26,16 @@ 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 (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",
content: JSON.stringify(categoriesExpenses)
},
{role: "user", content: sms},
],
response_format: zodResponseFormat(ExpenseEntry, "expense")
});

return JSON.parse(completion.choices[0].message.content);
Expand Down
Loading