From 6115572db949a990d96a30041dd4fb81ef3a67e4 Mon Sep 17 00:00:00 2001 From: Whipander Date: Wed, 27 Aug 2025 19:06:03 +0300 Subject: [PATCH 1/4] feat(api): add expense service --- server/services/expense.service.js | 164 +++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 server/services/expense.service.js diff --git a/server/services/expense.service.js b/server/services/expense.service.js new file mode 100644 index 0000000..dd149a4 --- /dev/null +++ b/server/services/expense.service.js @@ -0,0 +1,164 @@ +import { Prisma } from '@prisma/client'; +import { prisma } from "../db/prisma.js"; + +//Post + +export const createExpense = async (userId,expenseData) => { + try { + const expense = await prisma.expense.create({ + data: { + amount: expenseData.amount, + description: expenseData.description, + type: expenseData.type || 'ONE_TIME', + receipt_upload: expenseData.receipt_upload, + expense_date: expenseData.expense_date ? new Date(expenseData.expense_date) : null, + start_date: expenseData.start_date ? new Date(expenseData.start_date) : null, + end_date: expenseData.end_date ? new Date(expenseData.end_date) : null, + user_id: expenseData.user_id, + category_id: expenseData.category_id, + user: {connect:{ user_id: userId}} + }, + include: { + category: true, + }, + }); + return { success: true, data: expense }; + } catch (error) { + return {success: false, error: error} + } +}; + +//Get by id + +export const getExpenseById = async (expenseId, userId) => { + try { + const expense = await prisma.expense.findFirst({ + where: { + expense_id: parseInt(expenseId), + user_id: userId, + }, + include: { + category: true, + }, + }); + + if (!expense) { + return { success: false, error: 'Expense not found' }; + } + + return { success: true, data: expense }; + } catch (error) { + return { success: false, error: error }; + } +}; + +//Get all + +export const getAllExpenses = async (userId, filters = {}) => { + try { + const { startDate, endDate, categoryId, type } = filters; + + const where = { + user_id: parseInt(userId) + }; + + if (startDate || endDate) { + where.expense_date = {}; + if (startDate) where.expense_date.gte = new Date(startDate); + if (endDate) where.expense_date.lte = new Date(endDate); + } + + if (categoryId) where.category_id = parseInt(categoryId); + if (type) where.type = type; + + const expenses = await prisma.expense.findMany({ + where, + include: { + category: true, + }, + orderBy: { + expense_date: 'desc', + }, + }); + + return { success: true, data: expenses }; + } catch (error) { + return { success: false, error: error}; + } +}; + +//Update expense + +export const updateExpense = async (expenseId, userId, updateData) => { + try { + // First verify the expense exists and belongs to the user + const existingExpense = await prisma.expense.findFirst({ + where: { + expense_id: parseInt(expenseId), + user_id: parseInt(userId), + }, + }); + + if (!existingExpense) { + return { success: false, error: 'Expense not found or access denied' }; + } + + // Prepare the data to update + const dataToUpdate = {}; + const allowedFields = ['amount', 'description', 'type', 'receipt_upload', 'expense_date', 'start_date', 'end_date', 'category_id']; + + Object.keys(updateData).forEach(key => { + if (allowedFields.includes(key) && updateData[key] !== undefined) { + // Handle date fields + if (key.endsWith('_date') && updateData[key]) { + dataToUpdate[key] = new Date(updateData[key]); + } else { + dataToUpdate[key] = updateData[key]; + } + } + }); + + const updatedExpense = await prisma.expense.update({ + where: { + expense_id: parseInt(expenseId), + }, + data: dataToUpdate, + include: { + category: true, + }, + }); + + return { success: true, data: updatedExpense }; + } catch (error) { + return { success: false, error: error }; + } +}; + +//Delete + +export const deleteExpense = async (expenseId, userId) => { + try { + // First verify the expense exists and belongs to the user + const existingExpense = await prisma.expense.findFirst({ + where: { + expense_id: parseInt(expenseId), + user_id: parseInt(userId), + }, + }); + + if (!existingExpense) { + return { success: false, error: 'Expense not found or access denied' }; + } + + const deletedExpense = await prisma.expense.delete({ + where: { + expense_id: parseInt(expenseId), + }, + }); + + return { success: true, data: deletedExpense}; + } catch (error) { + return { success: false, error: error}; + } +}; + From 68f29757a3ce1f0303ab05ee3ca264a7157085bc Mon Sep 17 00:00:00 2001 From: Whipander Date: Wed, 27 Aug 2025 19:49:43 +0300 Subject: [PATCH 2/4] feat(api): add expense controller, expense route and expense validator --- server/controllers/expense.controller.js | 193 +++++++++++++++++++++++ server/routes/expense.route.js | 74 +++++++++ server/services/expense.service.js | 39 +++-- server/validators/expense.validator.js | 143 +++++++++++++++++ 4 files changed, 439 insertions(+), 10 deletions(-) create mode 100644 server/controllers/expense.controller.js create mode 100644 server/routes/expense.route.js create mode 100644 server/validators/expense.validator.js diff --git a/server/controllers/expense.controller.js b/server/controllers/expense.controller.js new file mode 100644 index 0000000..ea6beb6 --- /dev/null +++ b/server/controllers/expense.controller.js @@ -0,0 +1,193 @@ +import { validationResult } from 'express-validator'; +import { + createExpense, + getExpenseById, + getAllExpenses, + updateExpense, + deleteExpense +} from '../services/expense.service.js'; + +// @desc Create a new expense +// @route POST /api/expenses +// @access Private +export const createExpenseController = async (req, res) => { + // Validate request body + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const userId = req.user.user_id; + const expenseData = { + ...req.body, + user_id: userId + }; + + const result = await createExpense(userId,expenseData); + + if (!result.success) { + return res.status(400).json({ + success: false, + error: result.error + }); + } + + return res.status(201).json({ + success: true, + data: result.data + }); + } catch (error) { + console.error('Error creating expense:', error); + return res.status(500).json({ + success: false, + error: 'Server error while creating expense' + }); + } +}; + +// @desc Get a single expense by ID +// @route GET /api/expenses/:id +// @access Private +export const getExpenseController = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.user_id; + + const result = await getExpenseById(id, userId); + + if (!result.success) { + return res.status(404).json({ + success: false, + error: result.error + }); + } + + return res.status(200).json({ + success: true, + data: result.data + }); + } catch (error) { + console.error('Error fetching expense:', error); + return res.status(500).json({ + success: false, + error: 'Server error while fetching expense' + }); + } +}; + +// @desc Get all expenses for a user +// @route GET /api/expenses +// @access Private +export const getAllExpensesController = async (req, res) => { + try { + const userId = req.user.user_id; + const { startDate, endDate, categoryId, type } = req.query; + + const result = await getAllExpenses(userId, { + startDate, + endDate, + categoryId, + type + }); + + if (!result.success) { + return res.status(400).json({ + success: false, + error: result.error + }); + } + + return res.status(200).json({ + success: true, + data: result.data, + count: result.data.length + }); + } catch (error) { + console.error('Error fetching expenses:', error); + return res.status(500).json({ + success: false, + error: 'Server error while fetching expenses' + }); + } +}; + +// @desc Update an expense +// @route PUT /api/expenses/:id +// @access Private +export const updateExpenseController = async (req, res) => { + // Validate request body + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { id } = req.params; + const userId = req.user.user_id; + const updateData = req.body; + + // Don't allow updating user_id + if (updateData.user_id) { + delete updateData.user_id; + } + + const result = await updateExpense(id, userId, updateData); + + if (!result.success) { + const statusCode = result.error.includes('not found') ? 404 : 400; + return res.status(statusCode).json({ + success: false, + error: result.error + }); + } + + return res.status(200).json({ + success: true, + data: result.data + }); + } catch (error) { + console.error('Error updating expense:', error); + return res.status(500).json({ + success: false, + error: 'Server error while updating expense' + }); + } +}; + +// @desc Delete an expense +// @route DELETE /api/expenses/:id +// @access Private +export const deleteExpenseController = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.user_id; + + const result = await deleteExpense(id, userId); + + if (!result.success) { + const statusCode = result.error.includes('not found') ? 404 : 400; + return res.status(statusCode).json({ + success: false, + error: result.error + }); + } + + return res.status(200).json({ + success: true, + data: { id } + }); + } catch (error) { + console.error('Error deleting expense:', error); + return res.status(500).json({ + success: false, + error: 'Server error while deleting expense' + }); + } +}; diff --git a/server/routes/expense.route.js b/server/routes/expense.route.js new file mode 100644 index 0000000..a6c56c6 --- /dev/null +++ b/server/routes/expense.route.js @@ -0,0 +1,74 @@ +import express from 'express'; +import { requireAuth } from '../middleware/auth.middleware.js'; +import { + createExpenseController, + getExpenseController, + getAllExpensesController, + updateExpenseController, + deleteExpenseController +} from '../controllers/expense.controller.js'; +import { + createExpenseValidator, + updateExpenseValidator, + getExpenseValidator, + deleteExpenseValidator, + listExpensesValidator +} from '../validators/expense.validator.js'; +import { validate } from '../middleware/validation.middleware.js'; + +const router = express.Router(); + +// Apply authentication middleware to all routes +router.use(requireAuth); + +// @route POST /api/expenses +// @desc Create a new expense +// @access Private +router.post( + '/', + createExpenseValidator, + validate, + createExpenseController +); + +// @route GET /api/expenses/:id +// @desc Get a single expense by ID +// @access Private +router.get( + '/:id', + getExpenseValidator, + validate, + getExpenseController +); + +// @route GET /api/expenses +// @desc Get all expenses for the authenticated user +// @access Private +router.get( + '/', + listExpensesValidator, + validate, + getAllExpensesController +); + +// @route PUT /api/expenses/:id +// @desc Update an expense +// @access Private +router.put( + '/:id', + updateExpenseValidator, + validate, + updateExpenseController +); + +// @route DELETE /api/expenses/:id +// @desc Delete an expense +// @access Private +router.delete( + '/:id', + deleteExpenseValidator, + validate, + deleteExpenseController +); + +export default router; diff --git a/server/services/expense.service.js b/server/services/expense.service.js index dd149a4..d102142 100644 --- a/server/services/expense.service.js +++ b/server/services/expense.service.js @@ -16,7 +16,7 @@ export const createExpense = async (userId,expenseData) => { end_date: expenseData.end_date ? new Date(expenseData.end_date) : null, user_id: expenseData.user_id, category_id: expenseData.category_id, - user: {connect:{ user_id: userId}} + user: { connect: { user_id: userId } }, }, include: { category: true, @@ -24,18 +24,26 @@ export const createExpense = async (userId,expenseData) => { }); return { success: true, data: expense }; } catch (error) { - return {success: false, error: error} - } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === 'P2002') { + return { success: false, error: 'Expense already exists' }; + } + if (error.code === 'P2003') { + return { success: false, error: 'Invalid user_id or category_id' }; + } + } + return { success: false, error: error.message}; + } }; -//Get by id +//Get expense by id export const getExpenseById = async (expenseId, userId) => { try { const expense = await prisma.expense.findFirst({ where: { expense_id: parseInt(expenseId), - user_id: userId, + user_id: parseInt(userId), }, include: { category: true, @@ -48,7 +56,7 @@ export const getExpenseById = async (expenseId, userId) => { return { success: true, data: expense }; } catch (error) { - return { success: false, error: error }; + return { success: false, error: error.message }; } }; @@ -59,7 +67,7 @@ export const getAllExpenses = async (userId, filters = {}) => { const { startDate, endDate, categoryId, type } = filters; const where = { - user_id: parseInt(userId) + user_id: parseInt(userId), }; if (startDate || endDate) { @@ -83,7 +91,7 @@ export const getAllExpenses = async (userId, filters = {}) => { return { success: true, data: expenses }; } catch (error) { - return { success: false, error: error}; + return { success: false, error: error.message }; } }; @@ -130,7 +138,15 @@ export const updateExpense = async (expenseId, userId, updateData) => { return { success: true, data: updatedExpense }; } catch (error) { - return { success: false, error: error }; + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === 'P2025') { + return { success: false, error: 'Expense not found' }; + } + if (error.code === 'P2003') { + return { success: false, error: 'Invalid category_id' }; + } + } + return { success: false, error: error.message }; } }; @@ -158,7 +174,10 @@ export const deleteExpense = async (expenseId, userId) => { return { success: true, data: deletedExpense}; } catch (error) { - return { success: false, error: error}; + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') { + return { success: false, error: 'Expense not found' }; + } + return { success: false, error: error.message }; } }; diff --git a/server/validators/expense.validator.js b/server/validators/expense.validator.js new file mode 100644 index 0000000..3f64f6c --- /dev/null +++ b/server/validators/expense.validator.js @@ -0,0 +1,143 @@ +import { body, param, query } from 'express-validator'; + +export const createExpenseValidator = [ + body('amount') + .isFloat({ gt: 0 }) + .withMessage('Amount must be a positive number') + .notEmpty() + .withMessage('Amount is required'), + + body('description') + .optional() + .isString() + .withMessage('Description must be a string') + .isLength({ max: 2000 }) + .withMessage('Description must be less than 2000 characters'), + + body('type') + .optional() + .isIn(['ONE_TIME', 'RECURRING']) + .withMessage('Type must be either ONE_TIME or RECURRING'), + + body('receipt_upload') + .optional() + .isString() + .withMessage('Receipt upload must be a string') + .isURL() + .withMessage('Receipt upload must be a valid URL'), + + body('expense_date') + .optional() + .isISO8601() + .withMessage('Expense date must be a valid date') + .toDate(), + + body('start_date') + .optional() + .isISO8601() + .withMessage('Start date must be a valid date') + .toDate(), + + body('end_date') + .optional() + .isISO8601() + .withMessage('End date must be a valid date') + .toDate(), + + body('category_id') + .isInt({ min: 1 }) + .withMessage('Category ID must be a positive integer') + .notEmpty() + .withMessage('Category ID is required'), +]; + +export const updateExpenseValidator = [ + param('id') + .isInt({ min: 1 }) + .withMessage('Expense ID must be a positive integer') + .toInt(), + + body('amount') + .optional() + .isFloat({ gt: 0 }) + .withMessage('Amount must be a positive number'), + + body('description') + .optional() + .isString() + .withMessage('Description must be a string') + .isLength({ max: 2000 }) + .withMessage('Description must be less than 2000 characters'), + + body('type') + .optional() + .isIn(['ONE_TIME', 'RECURRING']) + .withMessage('Type must be either ONE_TIME or RECURRING'), + + body('receipt_upload') + .optional() + .isString() + .withMessage('Receipt upload must be a string') + .isURL() + .withMessage('Receipt upload must be a valid URL'), + + body('expense_date') + .optional() + .isISO8601() + .withMessage('Expense date must be a valid date') + .toDate(), + + body('start_date') + .optional() + .isISO8601() + .withMessage('Start date must be a valid date') + .toDate(), + + body('end_date') + .optional() + .isISO8601() + .withMessage('End date must be a valid date') + .toDate(), + + body('category_id') + .optional() + .isInt({ min: 1 }) + .withMessage('Category ID must be a positive integer') +]; + +export const getExpenseValidator = [ + param('id') + .isInt({ min: 1 }) + .withMessage('Expense ID must be a positive integer') + .toInt() +]; + +export const deleteExpenseValidator = [ + param('id') + .isInt({ min: 1 }) + .withMessage('Expense ID must be a positive integer') + .toInt() +]; + +export const listExpensesValidator = [ + query('startDate') + .optional() + .isISO8601() + .withMessage('Start date must be a valid date'), + + query('endDate') + .optional() + .isISO8601() + .withMessage('End date must be a valid date'), + + query('categoryId') + .optional() + .isInt({ min: 1 }) + .withMessage('Category ID must be a positive integer') + .toInt(), + + query('type') + .optional() + .isIn(['ONE_TIME', 'RECURRING']) + .withMessage('Type must be either ONE_TIME or RECURRING') +]; From 25f80b5ebf0591bd00d846067ad36f09e6df4758 Mon Sep 17 00:00:00 2001 From: Whipander Date: Wed, 27 Aug 2025 20:10:42 +0300 Subject: [PATCH 3/4] fix: add expense route to the main server file --- server/server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/server.js b/server/server.js index 67dd472..3e2993e 100644 --- a/server/server.js +++ b/server/server.js @@ -7,7 +7,7 @@ import { PrismaClient } from '@prisma/client'; import incomeRoutes from './routes/income.route.js'; import authRoutes from './routes/auth.route.js'; import categoryRoutes from './routes/category.route.js'; - +import expenseRoutes from './routes/expense.route.js'; dotenv.config(); const app = express(); @@ -24,6 +24,7 @@ app.use(cookieParser()); app.use('/api/auth', authRoutes); app.use('/api/incomes', requireAuth, incomeRoutes); app.use('/api/categories', categoryRoutes); +app.use('/api/expenses', expenseRoutes) // Initialize a single Prisma client instance const prisma = new PrismaClient(); From 15b6ef395d9b3016969af7acc034a8c5eda66758 Mon Sep 17 00:00:00 2001 From: Whipander Date: Fri, 29 Aug 2025 13:13:22 +0300 Subject: [PATCH 4/4] fix(api): add express-validator dependency and fix minor bug --- server/package-lock.json | 29 +++++ server/package.json | 7 +- server/routes/expense.route.js | 6 - server/services/expense.service.js | 70 +++++++----- server/validators/expense.validator.js | 152 ++++++++++++------------- 5 files changed, 151 insertions(+), 113 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 2525759..76ba302 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-validator": "^7.2.1", "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", @@ -830,6 +831,28 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-validator/node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -1345,6 +1368,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", diff --git a/server/package.json b/server/package.json index c5b9cdc..0c653ee 100644 --- a/server/package.json +++ b/server/package.json @@ -22,15 +22,16 @@ "license": "ISC", "dependencies": { "@prisma/client": "^6.14.0", + "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-validator": "^7.2.1", "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "pg": "^8.11.3", - "bcrypt": "^5.1.1", - "jsonwebtoken": "^9.0.2", - "cookie-parser": "^1.4.6", "validator": "^13.11.0" }, "devDependencies": { diff --git a/server/routes/expense.route.js b/server/routes/expense.route.js index a6c56c6..427cfc6 100644 --- a/server/routes/expense.route.js +++ b/server/routes/expense.route.js @@ -14,7 +14,6 @@ import { deleteExpenseValidator, listExpensesValidator } from '../validators/expense.validator.js'; -import { validate } from '../middleware/validation.middleware.js'; const router = express.Router(); @@ -27,7 +26,6 @@ router.use(requireAuth); router.post( '/', createExpenseValidator, - validate, createExpenseController ); @@ -37,7 +35,6 @@ router.post( router.get( '/:id', getExpenseValidator, - validate, getExpenseController ); @@ -47,7 +44,6 @@ router.get( router.get( '/', listExpensesValidator, - validate, getAllExpensesController ); @@ -57,7 +53,6 @@ router.get( router.put( '/:id', updateExpenseValidator, - validate, updateExpenseController ); @@ -67,7 +62,6 @@ router.put( router.delete( '/:id', deleteExpenseValidator, - validate, deleteExpenseController ); diff --git a/server/services/expense.service.js b/server/services/expense.service.js index d102142..783c4cc 100644 --- a/server/services/expense.service.js +++ b/server/services/expense.service.js @@ -1,22 +1,25 @@ -import { Prisma } from '@prisma/client'; +import { Prisma } from "@prisma/client"; import { prisma } from "../db/prisma.js"; //Post -export const createExpense = async (userId,expenseData) => { +export const createExpense = async (userId, expenseData) => { try { const expense = await prisma.expense.create({ data: { amount: expenseData.amount, description: expenseData.description, - type: expenseData.type || 'ONE_TIME', + type: expenseData.type || "ONE_TIME", receipt_upload: expenseData.receipt_upload, - expense_date: expenseData.expense_date ? new Date(expenseData.expense_date) : null, - start_date: expenseData.start_date ? new Date(expenseData.start_date) : null, + expense_date: expenseData.expense_date + ? new Date(expenseData.expense_date) + : null, + start_date: expenseData.start_date + ? new Date(expenseData.start_date) + : null, end_date: expenseData.end_date ? new Date(expenseData.end_date) : null, user_id: expenseData.user_id, category_id: expenseData.category_id, - user: { connect: { user_id: userId } }, }, include: { category: true, @@ -25,14 +28,14 @@ export const createExpense = async (userId,expenseData) => { return { success: true, data: expense }; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - if (error.code === 'P2002') { - return { success: false, error: 'Expense already exists' }; + if (error.code === "P2002") { + return { success: false, error: "Expense already exists" }; } - if (error.code === 'P2003') { - return { success: false, error: 'Invalid user_id or category_id' }; + if (error.code === "P2003") { + return { success: false, error: "Invalid user_id or category_id" }; } } - return { success: false, error: error.message}; + return { success: false, error: error.message }; } }; @@ -51,7 +54,7 @@ export const getExpenseById = async (expenseId, userId) => { }); if (!expense) { - return { success: false, error: 'Expense not found' }; + return { success: false, error: "Expense not found" }; } return { success: true, data: expense }; @@ -65,7 +68,7 @@ export const getExpenseById = async (expenseId, userId) => { export const getAllExpenses = async (userId, filters = {}) => { try { const { startDate, endDate, categoryId, type } = filters; - + const where = { user_id: parseInt(userId), }; @@ -85,7 +88,7 @@ export const getAllExpenses = async (userId, filters = {}) => { category: true, }, orderBy: { - expense_date: 'desc', + expense_date: "desc", }, }); @@ -108,17 +111,26 @@ export const updateExpense = async (expenseId, userId, updateData) => { }); if (!existingExpense) { - return { success: false, error: 'Expense not found or access denied' }; + return { success: false, error: "Expense not found or access denied" }; } // Prepare the data to update const dataToUpdate = {}; - const allowedFields = ['amount', 'description', 'type', 'receipt_upload', 'expense_date', 'start_date', 'end_date', 'category_id']; - - Object.keys(updateData).forEach(key => { + const allowedFields = [ + "amount", + "description", + "type", + "receipt_upload", + "expense_date", + "start_date", + "end_date", + "category_id", + ]; + + Object.keys(updateData).forEach((key) => { if (allowedFields.includes(key) && updateData[key] !== undefined) { // Handle date fields - if (key.endsWith('_date') && updateData[key]) { + if (key.endsWith("_date") && updateData[key]) { dataToUpdate[key] = new Date(updateData[key]); } else { dataToUpdate[key] = updateData[key]; @@ -139,11 +151,11 @@ export const updateExpense = async (expenseId, userId, updateData) => { return { success: true, data: updatedExpense }; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { - if (error.code === 'P2025') { - return { success: false, error: 'Expense not found' }; + if (error.code === "P2025") { + return { success: false, error: "Expense not found" }; } - if (error.code === 'P2003') { - return { success: false, error: 'Invalid category_id' }; + if (error.code === "P2003") { + return { success: false, error: "Invalid category_id" }; } } return { success: false, error: error.message }; @@ -163,7 +175,7 @@ export const deleteExpense = async (expenseId, userId) => { }); if (!existingExpense) { - return { success: false, error: 'Expense not found or access denied' }; + return { success: false, error: "Expense not found or access denied" }; } const deletedExpense = await prisma.expense.delete({ @@ -172,12 +184,14 @@ export const deleteExpense = async (expenseId, userId) => { }, }); - return { success: true, data: deletedExpense}; + return { success: true, data: deletedExpense }; } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') { - return { success: false, error: 'Expense not found' }; + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === "P2025" + ) { + return { success: false, error: "Expense not found" }; } return { success: false, error: error.message }; } }; - diff --git a/server/validators/expense.validator.js b/server/validators/expense.validator.js index 3f64f6c..29119de 100644 --- a/server/validators/expense.validator.js +++ b/server/validators/expense.validator.js @@ -1,143 +1,143 @@ -import { body, param, query } from 'express-validator'; +import { body, param, query } from "express-validator"; export const createExpenseValidator = [ - body('amount') + body("amount") .isFloat({ gt: 0 }) - .withMessage('Amount must be a positive number') + .withMessage("Amount must be a positive number") .notEmpty() - .withMessage('Amount is required'), - - body('description') + .withMessage("Amount is required"), + + body("description") .optional() .isString() - .withMessage('Description must be a string') + .withMessage("Description must be a string") .isLength({ max: 2000 }) - .withMessage('Description must be less than 2000 characters'), - - body('type') + .withMessage("Description must be less than 2000 characters"), + + body("type") .optional() - .isIn(['ONE_TIME', 'RECURRING']) - .withMessage('Type must be either ONE_TIME or RECURRING'), - - body('receipt_upload') + .isIn(["ONE_TIME", "RECURRING"]) + .withMessage("Type must be either ONE_TIME or RECURRING"), + + body("receipt_upload") .optional() .isString() - .withMessage('Receipt upload must be a string') + .withMessage("Receipt upload must be a string") .isURL() - .withMessage('Receipt upload must be a valid URL'), - - body('expense_date') + .withMessage("Receipt upload must be a valid URL"), + + body("expense_date") .optional() .isISO8601() - .withMessage('Expense date must be a valid date') + .withMessage("Expense date must be a valid date") .toDate(), - - body('start_date') + + body("start_date") .optional() .isISO8601() - .withMessage('Start date must be a valid date') + .withMessage("Start date must be a valid date") .toDate(), - - body('end_date') + + body("end_date") .optional() .isISO8601() - .withMessage('End date must be a valid date') + .withMessage("End date must be a valid date") .toDate(), - - body('category_id') + + body("category_id") .isInt({ min: 1 }) - .withMessage('Category ID must be a positive integer') + .withMessage("Category ID must be a positive integer") .notEmpty() - .withMessage('Category ID is required'), + .withMessage("Category ID is required"), ]; export const updateExpenseValidator = [ - param('id') + param("id") .isInt({ min: 1 }) - .withMessage('Expense ID must be a positive integer') + .withMessage("Expense ID must be a positive integer") .toInt(), - - body('amount') + + body("amount") .optional() .isFloat({ gt: 0 }) - .withMessage('Amount must be a positive number'), - - body('description') + .withMessage("Amount must be a positive number"), + + body("description") .optional() .isString() - .withMessage('Description must be a string') + .withMessage("Description must be a string") .isLength({ max: 2000 }) - .withMessage('Description must be less than 2000 characters'), - - body('type') + .withMessage("Description must be less than 2000 characters"), + + body("type") .optional() - .isIn(['ONE_TIME', 'RECURRING']) - .withMessage('Type must be either ONE_TIME or RECURRING'), - - body('receipt_upload') + .isIn(["ONE_TIME", "RECURRING"]) + .withMessage("Type must be either ONE_TIME or RECURRING"), + + body("receipt_upload") .optional() .isString() - .withMessage('Receipt upload must be a string') + .withMessage("Receipt upload must be a string") .isURL() - .withMessage('Receipt upload must be a valid URL'), - - body('expense_date') + .withMessage("Receipt upload must be a valid URL"), + + body("expense_date") .optional() .isISO8601() - .withMessage('Expense date must be a valid date') + .withMessage("Expense date must be a valid date") .toDate(), - - body('start_date') + + body("start_date") .optional() .isISO8601() - .withMessage('Start date must be a valid date') + .withMessage("Start date must be a valid date") .toDate(), - - body('end_date') + + body("end_date") .optional() .isISO8601() - .withMessage('End date must be a valid date') + .withMessage("End date must be a valid date") .toDate(), - - body('category_id') + + body("category_id") .optional() .isInt({ min: 1 }) - .withMessage('Category ID must be a positive integer') + .withMessage("Category ID must be a positive integer"), ]; export const getExpenseValidator = [ - param('id') + param("id") .isInt({ min: 1 }) - .withMessage('Expense ID must be a positive integer') - .toInt() + .withMessage("Expense ID must be a positive integer") + .toInt(), ]; export const deleteExpenseValidator = [ - param('id') + param("id") .isInt({ min: 1 }) - .withMessage('Expense ID must be a positive integer') - .toInt() + .withMessage("Expense ID must be a positive integer") + .toInt(), ]; export const listExpensesValidator = [ - query('startDate') + query("startDate") .optional() .isISO8601() - .withMessage('Start date must be a valid date'), - - query('endDate') + .withMessage("Start date must be a valid date"), + + query("endDate") .optional() .isISO8601() - .withMessage('End date must be a valid date'), - - query('categoryId') + .withMessage("End date must be a valid date"), + + query("categoryId") .optional() .isInt({ min: 1 }) - .withMessage('Category ID must be a positive integer') + .withMessage("Category ID must be a positive integer") .toInt(), - - query('type') + + query("type") .optional() - .isIn(['ONE_TIME', 'RECURRING']) - .withMessage('Type must be either ONE_TIME or RECURRING') + .isIn(["ONE_TIME", "RECURRING"]) + .withMessage("Type must be either ONE_TIME or RECURRING"), ];