From 0cc1484faecac2d1dc8d374b1f07c631174e7a18 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 20:07:08 +0300 Subject: [PATCH 01/14] chore: move lib folder in server root --- server/{src => }/lib/prisma.js | 0 server/package.json | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) rename server/{src => }/lib/prisma.js (100%) diff --git a/server/src/lib/prisma.js b/server/lib/prisma.js similarity index 100% rename from server/src/lib/prisma.js rename to server/lib/prisma.js diff --git a/server/package.json b/server/package.json index 8fe6e2f..d79bff0 100644 --- a/server/package.json +++ b/server/package.json @@ -27,8 +27,7 @@ "express": "^4.18.2", "helmet": "^7.1.0", "morgan": "^1.10.0", - "pg": "^8.11.3", - "@prisma/client": "^5.18.0" + "pg": "^8.11.3" }, "devDependencies": { "nodemon": "^3.0.2", From 7af62267a92cdf8d28aed5fdb9b4426682c0c92b Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 20:10:30 +0300 Subject: [PATCH 02/14] chore(prisma): remove custom generator output and drop icon_emoji attribute from IncomeCategory model --- server/prisma/schema.prisma | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 1c871fb..7f9dd94 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -6,7 +6,6 @@ generator client { provider = "prisma-client-js" - output = "../generated/prisma" } datasource db { @@ -49,7 +48,6 @@ model IncomeCategory { category_id Int @id @default(autoincrement()) category_name String @db.VarChar(50) icon_url String? @db.VarChar(500) - icon_emoji String? @db.VarChar(10) is_custom Boolean @default(false) user_id Int? user User? @relation(fields: [user_id], references: [user_id], onDelete: Cascade) From b0aff8aa0d00847044bcb029f31706f68934c11f Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 20:17:44 +0300 Subject: [PATCH 03/14] feat(client): add income models --- client/src/types/Income.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 client/src/types/Income.ts diff --git a/client/src/types/Income.ts b/client/src/types/Income.ts new file mode 100644 index 0000000..14aeec4 --- /dev/null +++ b/client/src/types/Income.ts @@ -0,0 +1,37 @@ +export interface Income { + income_id: number; + amount: number; + date: string; + source: string; + description?: string; + creation_date: string; + user_id: number; + category_id: number; + category?: IncomeCategory; +} + +export interface IncomeCategory { + category_id: number; + category_name: string; + icon_url?: string; + is_custom: boolean; + user_id?: number; + created_at: string; + updated_at: string; +} + +export interface CreateIncomeRequest { + amount: number; + date: string; + source: string; + description?: string; + category_id: number; +} + +export interface UpdateIncomeRequest { + amount?: number; + date?: string; + source?: string; + description?: string; + category_id?: number; +} \ No newline at end of file From 08dcacc0b462ab75211a7e95d80fe476bf754b11 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 20:26:40 +0300 Subject: [PATCH 04/14] feat(server): implement income controller with full CRUD operations --- server/controllers/income.controller.js | 191 ++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 server/controllers/income.controller.js diff --git a/server/controllers/income.controller.js b/server/controllers/income.controller.js new file mode 100644 index 0000000..c4c769a --- /dev/null +++ b/server/controllers/income.controller.js @@ -0,0 +1,191 @@ +import { prisma } from "../lib/prisma"; + +//GET ALL INCOMES +export const getIncomes = async (req, res) => { + try { + const { start, end } = req.query; + const userId = req.user.user_id; + + let whereClause = { user_id: userId }; + + if (start || end) { + whereClause.date = {}; + if (start) whereClause.date.gte = new Date(start); + if (end) whereClause.date.lte = new Date(end); + } + + const incomes = await prisma.income.findMany({ + where: whereClause, + include: { + category: true + }, + orderBy: { + date: 'desc' + } + }); + + res.json(incomes); + } catch (error) { + console.error('Error fetching incomes:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +//GET BY ID +export const getIncomeById = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.user_id; + + const income = await prisma.income.findFirst({ + where: { + income_id: parseInt(id), + user_id: userId + }, + include: { + category: true + } + }); + + if (!income) { + return res.status(404).json({ error: 'Income not found' }); + } + + res.json(income); + } catch (error) { + console.error('Error fetching income:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +//POST +export const createIncome = async (req, res) => { + try { + const { amount, date, source, description, category_id } = req.body; + const userId = req.user.user_id; + + if (!amount || !date || !source || !category_id) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + const category = await prisma.incomeCategory.findFirst({ + where: { + category_id: parseInt(category_id), + OR: [ + { user_id: userId }, + { user_id: null } + ] + } + }); + + if (!category) { + return res.status(400).json({ error: 'Invalid category' }); + } + + const income = await prisma.income.create({ + data: { + amount: parseFloat(amount), + date: new Date(date), + source, + description, + user_id: userId, + category_id: parseInt(category_id) + }, + include: { + category: true + } + }); + + res.status(201).json(income); + } catch (error) { + console.error('Error creating income:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +//PUT - PATCH +export const updateIncome = async (req, res) => { + try { + const { id } = req.params; + const { amount, date, source, description, category_id } = req.body; + const userId = req.user.user_id; + + const existingIncome = await prisma.income.findFirst({ + where: { + income_id: parseInt(id), + user_id: userId + } + }); + + if (!existingIncome) { + return res.status(404).json({ error: 'Income not found' }); + } + + if (category_id) { + const category = await prisma.incomeCategory.findFirst({ + where: { + category_id: parseInt(category_id), + OR: [ + { user_id: userId }, + { user_id: null } + ] + } + }); + + if (!category) { + return res.status(400).json({ error: 'Invalid category' }); + } + } + + const income = await prisma.income.update({ + where: { + income_id: parseInt(id) + }, + data: { + ...(amount && { amount: parseFloat(amount) }), + ...(date && { date: new Date(date) }), + ...(source && { source }), + ...(description !== undefined && { description }), + ...(category_id && { category_id: parseInt(category_id) }) + }, + include: { + category: true + } + }); + + res.json(income); + } catch (error) { + console.error('Error updating income:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +//DELETE +export const deleteIncome = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.user_id; + + const income = await prisma.income.findFirst({ + where: { + income_id: parseInt(id), + user_id: userId + } + }); + + if (!income) { + return res.status(404).json({ error: 'Income not found' }); + } + + await prisma.income.delete({ + where: { + income_id: parseInt(id) + } + }); + + res.status(204).send(); + } catch (error) { + console.error('Error deleting income:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; \ No newline at end of file From 3567d5e7a57c5694a3ce98b8790a4e665165d615 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 20:30:18 +0300 Subject: [PATCH 05/14] feat(server): implement income category controller --- .../controllers/incomeCategory.controller.js | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 server/controllers/incomeCategory.controller.js diff --git a/server/controllers/incomeCategory.controller.js b/server/controllers/incomeCategory.controller.js new file mode 100644 index 0000000..d0835cd --- /dev/null +++ b/server/controllers/incomeCategory.controller.js @@ -0,0 +1,160 @@ +import { prisma } from "../lib/prisma"; + +//GET ALL INCOME CATEGORIES +export const getIncomeCategories = async (req, res) => { + try { + const userId = req.user.user_id; + + const categories = await prisma.incomeCategory.findMany({ + where: { + OR: [ + { user_id: null }, + { user_id: userId } + ] + }, + orderBy: [ + { is_custom: 'asc' }, //system categories are displayed first + { category_name: 'asc' } + ] + }); + + res.json(categories); + } catch (error) { + console.error('Error fetching income categories:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +//POST +export const createIncomeCategory = async (req, res) => { + try { + const { category_name, icon_url, icon_emoji } = req.body; + const userId = req.user.user_id; + + if (!category_name) { + return res.status(400).json({ error: 'Category name is required' }); + } + + const existingCategory = await prisma.incomeCategory.findFirst({ + where: { + category_name, + user_id: userId + } + }); + + if (existingCategory) { + return res.status(400).json({ error: 'Category already exists' }); + } + + const category = await prisma.incomeCategory.create({ + data: { + category_name, + icon_url, + icon_emoji, + is_custom: true, + user_id: userId + } + }); + + res.status(201).json(category); + } catch (error) { + console.error('Error creating income category:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +//PUT - PATCH +export const updateIncomeCategory = async (req, res) => { + try { + const { id } = req.params; + const { category_name, icon_url, icon_emoji } = req.body; + const userId = req.user.user_id; + + if (!category_name) { + return res.status(400).json({ error: 'Category name is required' }); + } + + const existingCategory = await prisma.incomeCategory.findFirst({ + where: { + category_id: parseInt(id), + user_id: userId + } + }); + + if (!existingCategory) { + return res.status(404).json({ error: 'Category not found' }); + } + + const duplicateCategory = await prisma.incomeCategory.findFirst({ + where: { + category_name, + user_id: userId, + NOT: { + category_id: parseInt(id) + } + } + }); + + if (duplicateCategory) { + return res.status(400).json({ error: 'Category name already exists' }); + } + + const category = await prisma.incomeCategory.update({ + where: { + category_id: parseInt(id) + }, + data: { + category_name, + icon_url, + icon_emoji + } + }); + + res.json(category); + } catch (error) { + console.error('Error updating income category:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +//DELETE +export const deleteIncomeCategory = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.user_id; + + const category = await prisma.incomeCategory.findFirst({ + where: { + category_id: parseInt(id), + user_id: userId + } + }); + + if (!category) { + return res.status(404).json({ error: 'Category not found' }); + } + + const incomesUsingCategory = await prisma.income.count({ + where: { + category_id: parseInt(id) + } + }); + + if (incomesUsingCategory > 0) { + return res.status(400).json({ + error: 'Cannot delete category that is being used by incomes' + }); + } + + await prisma.incomeCategory.delete({ + where: { + category_id: parseInt(id) + } + }); + + res.status(204).send(); + } catch (error) { + console.error('Error deleting income category:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; \ No newline at end of file From 8acfe4d305454af4ba65ed2f8210cb9dcc880325 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 20:36:46 +0300 Subject: [PATCH 06/14] feat(server): asyncHandler utility --- server/controllers/income.controller.js | 132 +++++++++--------- .../controllers/incomeCategory.controller.js | 121 ++++++++-------- server/{lib => db}/prisma.js | 0 server/utils/asyncHandler.js | 3 + 4 files changed, 125 insertions(+), 131 deletions(-) rename server/{lib => db}/prisma.js (100%) create mode 100644 server/utils/asyncHandler.js diff --git a/server/controllers/income.controller.js b/server/controllers/income.controller.js index c4c769a..0d893c9 100644 --- a/server/controllers/income.controller.js +++ b/server/controllers/income.controller.js @@ -1,33 +1,33 @@ -import { prisma } from "../lib/prisma"; +import { prisma } from "../db/prisma"; //GET ALL INCOMES export const getIncomes = async (req, res) => { try { const { start, end } = req.query; const userId = req.user.user_id; - + let whereClause = { user_id: userId }; - + if (start || end) { whereClause.date = {}; if (start) whereClause.date.gte = new Date(start); if (end) whereClause.date.lte = new Date(end); } - + const incomes = await prisma.income.findMany({ where: whereClause, include: { - category: true + category: true, }, orderBy: { - date: 'desc' - } + date: "desc", + }, }); - + res.json(incomes); } catch (error) { - console.error('Error fetching incomes:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error fetching incomes:", error); + res.status(500).json({ error: "Internal server error" }); } }; @@ -36,25 +36,25 @@ export const getIncomeById = async (req, res) => { try { const { id } = req.params; const userId = req.user.user_id; - + const income = await prisma.income.findFirst({ where: { income_id: parseInt(id), - user_id: userId + user_id: userId, }, include: { - category: true - } + category: true, + }, }); - + if (!income) { - return res.status(404).json({ error: 'Income not found' }); + return res.status(404).json({ error: "Income not found" }); } - + res.json(income); } catch (error) { - console.error('Error fetching income:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error fetching income:", error); + res.status(500).json({ error: "Internal server error" }); } }; @@ -63,25 +63,22 @@ export const createIncome = async (req, res) => { try { const { amount, date, source, description, category_id } = req.body; const userId = req.user.user_id; - + if (!amount || !date || !source || !category_id) { - return res.status(400).json({ error: 'Missing required fields' }); + return res.status(400).json({ error: "Missing required fields" }); } - + const category = await prisma.incomeCategory.findFirst({ where: { category_id: parseInt(category_id), - OR: [ - { user_id: userId }, - { user_id: null } - ] - } + OR: [{ user_id: userId }, { user_id: null }], + }, }); - + if (!category) { - return res.status(400).json({ error: 'Invalid category' }); + return res.status(400).json({ error: "Invalid category" }); } - + const income = await prisma.income.create({ data: { amount: parseFloat(amount), @@ -89,17 +86,17 @@ export const createIncome = async (req, res) => { source, description, user_id: userId, - category_id: parseInt(category_id) + category_id: parseInt(category_id), }, include: { - category: true - } + category: true, + }, }); - + res.status(201).json(income); } catch (error) { - console.error('Error creating income:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error creating income:", error); + res.status(500).json({ error: "Internal server error" }); } }; @@ -109,54 +106,51 @@ export const updateIncome = async (req, res) => { const { id } = req.params; const { amount, date, source, description, category_id } = req.body; const userId = req.user.user_id; - + const existingIncome = await prisma.income.findFirst({ where: { income_id: parseInt(id), - user_id: userId - } + user_id: userId, + }, }); - + if (!existingIncome) { - return res.status(404).json({ error: 'Income not found' }); + return res.status(404).json({ error: "Income not found" }); } - + if (category_id) { const category = await prisma.incomeCategory.findFirst({ where: { category_id: parseInt(category_id), - OR: [ - { user_id: userId }, - { user_id: null } - ] - } + OR: [{ user_id: userId }, { user_id: null }], + }, }); - + if (!category) { - return res.status(400).json({ error: 'Invalid category' }); + return res.status(400).json({ error: "Invalid category" }); } } - + const income = await prisma.income.update({ where: { - income_id: parseInt(id) + income_id: parseInt(id), }, data: { ...(amount && { amount: parseFloat(amount) }), ...(date && { date: new Date(date) }), ...(source && { source }), ...(description !== undefined && { description }), - ...(category_id && { category_id: parseInt(category_id) }) + ...(category_id && { category_id: parseInt(category_id) }), }, include: { - category: true - } + category: true, + }, }); - + res.json(income); } catch (error) { - console.error('Error updating income:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error updating income:", error); + res.status(500).json({ error: "Internal server error" }); } }; @@ -165,27 +159,27 @@ export const deleteIncome = async (req, res) => { try { const { id } = req.params; const userId = req.user.user_id; - + const income = await prisma.income.findFirst({ where: { income_id: parseInt(id), - user_id: userId - } + user_id: userId, + }, }); - + if (!income) { - return res.status(404).json({ error: 'Income not found' }); + return res.status(404).json({ error: "Income not found" }); } - + await prisma.income.delete({ where: { - income_id: parseInt(id) - } + income_id: parseInt(id), + }, }); - + res.status(204).send(); } catch (error) { - console.error('Error deleting income:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error deleting income:", error); + res.status(500).json({ error: "Internal server error" }); } -}; \ No newline at end of file +}; diff --git a/server/controllers/incomeCategory.controller.js b/server/controllers/incomeCategory.controller.js index d0835cd..d87b553 100644 --- a/server/controllers/incomeCategory.controller.js +++ b/server/controllers/incomeCategory.controller.js @@ -1,27 +1,24 @@ -import { prisma } from "../lib/prisma"; +import { prisma } from "../db/prisma"; //GET ALL INCOME CATEGORIES export const getIncomeCategories = async (req, res) => { try { const userId = req.user.user_id; - + const categories = await prisma.incomeCategory.findMany({ where: { - OR: [ - { user_id: null }, - { user_id: userId } - ] + OR: [{ user_id: null }, { user_id: userId }], }, orderBy: [ - { is_custom: 'asc' }, //system categories are displayed first - { category_name: 'asc' } - ] + { is_custom: "asc" }, //system categories are displayed first + { category_name: "asc" }, + ], }); - + res.json(categories); } catch (error) { - console.error('Error fetching income categories:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error fetching income categories:", error); + res.status(500).json({ error: "Internal server error" }); } }; @@ -30,36 +27,36 @@ export const createIncomeCategory = async (req, res) => { try { const { category_name, icon_url, icon_emoji } = req.body; const userId = req.user.user_id; - + if (!category_name) { - return res.status(400).json({ error: 'Category name is required' }); + return res.status(400).json({ error: "Category name is required" }); } - + const existingCategory = await prisma.incomeCategory.findFirst({ where: { category_name, - user_id: userId - } + user_id: userId, + }, }); - + if (existingCategory) { - return res.status(400).json({ error: 'Category already exists' }); + return res.status(400).json({ error: "Category already exists" }); } - + const category = await prisma.incomeCategory.create({ data: { category_name, icon_url, icon_emoji, is_custom: true, - user_id: userId - } + user_id: userId, + }, }); - + res.status(201).json(category); } catch (error) { - console.error('Error creating income category:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error creating income category:", error); + res.status(500).json({ error: "Internal server error" }); } }; @@ -69,20 +66,20 @@ export const updateIncomeCategory = async (req, res) => { const { id } = req.params; const { category_name, icon_url, icon_emoji } = req.body; const userId = req.user.user_id; - + if (!category_name) { - return res.status(400).json({ error: 'Category name is required' }); + return res.status(400).json({ error: "Category name is required" }); } - + const existingCategory = await prisma.incomeCategory.findFirst({ where: { category_id: parseInt(id), - user_id: userId - } + user_id: userId, + }, }); - + if (!existingCategory) { - return res.status(404).json({ error: 'Category not found' }); + return res.status(404).json({ error: "Category not found" }); } const duplicateCategory = await prisma.incomeCategory.findFirst({ @@ -90,30 +87,30 @@ export const updateIncomeCategory = async (req, res) => { category_name, user_id: userId, NOT: { - category_id: parseInt(id) - } - } + category_id: parseInt(id), + }, + }, }); - + if (duplicateCategory) { - return res.status(400).json({ error: 'Category name already exists' }); + return res.status(400).json({ error: "Category name already exists" }); } - + const category = await prisma.incomeCategory.update({ where: { - category_id: parseInt(id) + category_id: parseInt(id), }, data: { category_name, icon_url, - icon_emoji - } + icon_emoji, + }, }); - + res.json(category); } catch (error) { - console.error('Error updating income category:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error updating income category:", error); + res.status(500).json({ error: "Internal server error" }); } }; @@ -122,39 +119,39 @@ export const deleteIncomeCategory = async (req, res) => { try { const { id } = req.params; const userId = req.user.user_id; - + const category = await prisma.incomeCategory.findFirst({ where: { category_id: parseInt(id), - user_id: userId - } + user_id: userId, + }, }); - + if (!category) { - return res.status(404).json({ error: 'Category not found' }); + return res.status(404).json({ error: "Category not found" }); } - + const incomesUsingCategory = await prisma.income.count({ where: { - category_id: parseInt(id) - } + category_id: parseInt(id), + }, }); - + if (incomesUsingCategory > 0) { - return res.status(400).json({ - error: 'Cannot delete category that is being used by incomes' + return res.status(400).json({ + error: "Cannot delete category that is being used by incomes", }); } - + await prisma.incomeCategory.delete({ where: { - category_id: parseInt(id) - } + category_id: parseInt(id), + }, }); - + res.status(204).send(); } catch (error) { - console.error('Error deleting income category:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error deleting income category:", error); + res.status(500).json({ error: "Internal server error" }); } -}; \ No newline at end of file +}; diff --git a/server/lib/prisma.js b/server/db/prisma.js similarity index 100% rename from server/lib/prisma.js rename to server/db/prisma.js diff --git a/server/utils/asyncHandler.js b/server/utils/asyncHandler.js new file mode 100644 index 0000000..bb1b35f --- /dev/null +++ b/server/utils/asyncHandler.js @@ -0,0 +1,3 @@ +export const asyncHandler = (fn) => (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; From 39282033b6e8295414f40bbf945cd3831a730db0 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 20:47:56 +0300 Subject: [PATCH 07/14] refactor: separate controller and service logic for cleaner architecture --- server/controllers/income.controller.js | 185 ------------------ .../controllers/income/income.controller.js | 86 ++++++++ .../income/incomeCategory.controller.js | 67 +++++++ .../controllers/incomeCategory.controller.js | 157 --------------- server/services/income/income.service.js | 170 ++++++++++++++++ 5 files changed, 323 insertions(+), 342 deletions(-) delete mode 100644 server/controllers/income.controller.js create mode 100644 server/controllers/income/income.controller.js create mode 100644 server/controllers/income/incomeCategory.controller.js delete mode 100644 server/controllers/incomeCategory.controller.js create mode 100644 server/services/income/income.service.js diff --git a/server/controllers/income.controller.js b/server/controllers/income.controller.js deleted file mode 100644 index 0d893c9..0000000 --- a/server/controllers/income.controller.js +++ /dev/null @@ -1,185 +0,0 @@ -import { prisma } from "../db/prisma"; - -//GET ALL INCOMES -export const getIncomes = async (req, res) => { - try { - const { start, end } = req.query; - const userId = req.user.user_id; - - let whereClause = { user_id: userId }; - - if (start || end) { - whereClause.date = {}; - if (start) whereClause.date.gte = new Date(start); - if (end) whereClause.date.lte = new Date(end); - } - - const incomes = await prisma.income.findMany({ - where: whereClause, - include: { - category: true, - }, - orderBy: { - date: "desc", - }, - }); - - res.json(incomes); - } catch (error) { - console.error("Error fetching incomes:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; - -//GET BY ID -export const getIncomeById = async (req, res) => { - try { - const { id } = req.params; - const userId = req.user.user_id; - - const income = await prisma.income.findFirst({ - where: { - income_id: parseInt(id), - user_id: userId, - }, - include: { - category: true, - }, - }); - - if (!income) { - return res.status(404).json({ error: "Income not found" }); - } - - res.json(income); - } catch (error) { - console.error("Error fetching income:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; - -//POST -export const createIncome = async (req, res) => { - try { - const { amount, date, source, description, category_id } = req.body; - const userId = req.user.user_id; - - if (!amount || !date || !source || !category_id) { - return res.status(400).json({ error: "Missing required fields" }); - } - - const category = await prisma.incomeCategory.findFirst({ - where: { - category_id: parseInt(category_id), - OR: [{ user_id: userId }, { user_id: null }], - }, - }); - - if (!category) { - return res.status(400).json({ error: "Invalid category" }); - } - - const income = await prisma.income.create({ - data: { - amount: parseFloat(amount), - date: new Date(date), - source, - description, - user_id: userId, - category_id: parseInt(category_id), - }, - include: { - category: true, - }, - }); - - res.status(201).json(income); - } catch (error) { - console.error("Error creating income:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; - -//PUT - PATCH -export const updateIncome = async (req, res) => { - try { - const { id } = req.params; - const { amount, date, source, description, category_id } = req.body; - const userId = req.user.user_id; - - const existingIncome = await prisma.income.findFirst({ - where: { - income_id: parseInt(id), - user_id: userId, - }, - }); - - if (!existingIncome) { - return res.status(404).json({ error: "Income not found" }); - } - - if (category_id) { - const category = await prisma.incomeCategory.findFirst({ - where: { - category_id: parseInt(category_id), - OR: [{ user_id: userId }, { user_id: null }], - }, - }); - - if (!category) { - return res.status(400).json({ error: "Invalid category" }); - } - } - - const income = await prisma.income.update({ - where: { - income_id: parseInt(id), - }, - data: { - ...(amount && { amount: parseFloat(amount) }), - ...(date && { date: new Date(date) }), - ...(source && { source }), - ...(description !== undefined && { description }), - ...(category_id && { category_id: parseInt(category_id) }), - }, - include: { - category: true, - }, - }); - - res.json(income); - } catch (error) { - console.error("Error updating income:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; - -//DELETE -export const deleteIncome = async (req, res) => { - try { - const { id } = req.params; - const userId = req.user.user_id; - - const income = await prisma.income.findFirst({ - where: { - income_id: parseInt(id), - user_id: userId, - }, - }); - - if (!income) { - return res.status(404).json({ error: "Income not found" }); - } - - await prisma.income.delete({ - where: { - income_id: parseInt(id), - }, - }); - - res.status(204).send(); - } catch (error) { - console.error("Error deleting income:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; diff --git a/server/controllers/income/income.controller.js b/server/controllers/income/income.controller.js new file mode 100644 index 0000000..3a0346f --- /dev/null +++ b/server/controllers/income/income.controller.js @@ -0,0 +1,86 @@ +import { incomeService } from "../../services/income/income.service"; +import { asyncHandler } from "../../utils/asyncHandler"; + +const createIncome = asyncHandler(async (req, res) => { + const { amount, date, source, description, category_id } = req.body; + + if (!amount || !date || !source || !category_id) { + return res.status(400).json({ error: "Missing required fields" }); + } + + const income = await incomeService.createIncome(req.user.user_id, { + amount: parseFloat(amount), + date: new Date(date), + source, + description, + category: { connect: { category_id: parseInt(category_id) } }, + }); + + res.status(201).json(income); +}); + +const getIncomes = asyncHandler(async (req, res) => { + const { start, end } = req.query; + const filters = {}; + + if (start) filters.start = new Date(start); + if (end) filters.end = new Date(end); + + const incomes = await incomeService.getIncomes(req.user.user_id, filters); + res.json(incomes); +}); + +const getIncome = asyncHandler(async (req, res) => { + const { id } = req.params; + const income = await incomeService.getIncomeById(id, req.user.user_id); + + if (!income) { + return res.status(404).json({ error: "Income not found" }); + } + + res.json(income); +}); + +const updateIncome = asyncHandler(async (req, res) => { + const { id } = req.params; + const updates = req.body; + + if (updates.date) updates.date = new Date(updates.date); + if (updates.amount) updates.amount = parseFloat(updates.amount); + if (updates.category_id) { + updates.category = { + connect: { category_id: parseInt(updates.category_id) }, + }; + delete updates.category_id; + } + + try { + const income = await incomeService.updateIncome( + id, + req.user.user_id, + updates + ); + res.json(income); + } catch (error) { + res.status(404).json({ error: error.message }); + } +}); + +const deleteIncome = asyncHandler(async (req, res) => { + const { id } = req.params; + + try { + await incomeService.deleteIncome(id, req.user.user_id); + res.status(204).send(); + } catch (error) { + res.status(404).json({ error: error.message }); + } +}); + +export default { + createIncome, + getIncomes, + getIncome, + updateIncome, + deleteIncome, +}; diff --git a/server/controllers/income/incomeCategory.controller.js b/server/controllers/income/incomeCategory.controller.js new file mode 100644 index 0000000..0d40659 --- /dev/null +++ b/server/controllers/income/incomeCategory.controller.js @@ -0,0 +1,67 @@ +import { incomeService } from "../../services/income/income.service"; +import { asyncHandler } from "../../utils/asyncHandler"; + +const getIncomeCategories = asyncHandler(async (req, res) => { + const categories = await incomeService.getIncomeCategories(req.user.user_id); + res.json(categories); +}); + +const createIncomeCategory = asyncHandler(async (req, res) => { + const { category_name, icon_url, icon_emoji } = req.body; + + if (!category_name) { + return res.status(400).json({ error: "Category name is required" }); + } + + try { + const category = await incomeService.createIncomeCategory( + req.user.user_id, + { + category_name, + icon_url, + icon_emoji, + } + ); + res.status(201).json(category); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +const updateIncomeCategory = asyncHandler(async (req, res) => { + const { id } = req.params; + const { category_name, icon_url, icon_emoji } = req.body; + + if (!category_name) { + return res.status(400).json({ error: "Category name is required" }); + } + + try { + const category = await incomeService.updateIncomeCategory( + id, + req.user.user_id, + { category_name, icon_url, icon_emoji } + ); + res.json(category); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +const deleteIncomeCategory = asyncHandler(async (req, res) => { + const { id } = req.params; + + try { + await incomeService.deleteIncomeCategory(id, req.user.user_id); + res.status(204).send(); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +export default { + getIncomeCategories, + createIncomeCategory, + updateIncomeCategory, + deleteIncomeCategory, +}; diff --git a/server/controllers/incomeCategory.controller.js b/server/controllers/incomeCategory.controller.js deleted file mode 100644 index d87b553..0000000 --- a/server/controllers/incomeCategory.controller.js +++ /dev/null @@ -1,157 +0,0 @@ -import { prisma } from "../db/prisma"; - -//GET ALL INCOME CATEGORIES -export const getIncomeCategories = async (req, res) => { - try { - const userId = req.user.user_id; - - const categories = await prisma.incomeCategory.findMany({ - where: { - OR: [{ user_id: null }, { user_id: userId }], - }, - orderBy: [ - { is_custom: "asc" }, //system categories are displayed first - { category_name: "asc" }, - ], - }); - - res.json(categories); - } catch (error) { - console.error("Error fetching income categories:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; - -//POST -export const createIncomeCategory = async (req, res) => { - try { - const { category_name, icon_url, icon_emoji } = req.body; - const userId = req.user.user_id; - - if (!category_name) { - return res.status(400).json({ error: "Category name is required" }); - } - - const existingCategory = await prisma.incomeCategory.findFirst({ - where: { - category_name, - user_id: userId, - }, - }); - - if (existingCategory) { - return res.status(400).json({ error: "Category already exists" }); - } - - const category = await prisma.incomeCategory.create({ - data: { - category_name, - icon_url, - icon_emoji, - is_custom: true, - user_id: userId, - }, - }); - - res.status(201).json(category); - } catch (error) { - console.error("Error creating income category:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; - -//PUT - PATCH -export const updateIncomeCategory = async (req, res) => { - try { - const { id } = req.params; - const { category_name, icon_url, icon_emoji } = req.body; - const userId = req.user.user_id; - - if (!category_name) { - return res.status(400).json({ error: "Category name is required" }); - } - - const existingCategory = await prisma.incomeCategory.findFirst({ - where: { - category_id: parseInt(id), - user_id: userId, - }, - }); - - if (!existingCategory) { - return res.status(404).json({ error: "Category not found" }); - } - - const duplicateCategory = await prisma.incomeCategory.findFirst({ - where: { - category_name, - user_id: userId, - NOT: { - category_id: parseInt(id), - }, - }, - }); - - if (duplicateCategory) { - return res.status(400).json({ error: "Category name already exists" }); - } - - const category = await prisma.incomeCategory.update({ - where: { - category_id: parseInt(id), - }, - data: { - category_name, - icon_url, - icon_emoji, - }, - }); - - res.json(category); - } catch (error) { - console.error("Error updating income category:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; - -//DELETE -export const deleteIncomeCategory = async (req, res) => { - try { - const { id } = req.params; - const userId = req.user.user_id; - - const category = await prisma.incomeCategory.findFirst({ - where: { - category_id: parseInt(id), - user_id: userId, - }, - }); - - if (!category) { - return res.status(404).json({ error: "Category not found" }); - } - - const incomesUsingCategory = await prisma.income.count({ - where: { - category_id: parseInt(id), - }, - }); - - if (incomesUsingCategory > 0) { - return res.status(400).json({ - error: "Cannot delete category that is being used by incomes", - }); - } - - await prisma.incomeCategory.delete({ - where: { - category_id: parseInt(id), - }, - }); - - res.status(204).send(); - } catch (error) { - console.error("Error deleting income category:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; diff --git a/server/services/income/income.service.js b/server/services/income/income.service.js new file mode 100644 index 0000000..df197f0 --- /dev/null +++ b/server/services/income/income.service.js @@ -0,0 +1,170 @@ +import { prisma } from "../../db/prisma"; + +//---------------INCOME---------------// + +//POST +const createIncome = async (userId, data) => { + return await prisma.income.create({ + data: { + ...data, + user: { connect: { user_id: userId } }, + }, + include: { category: true }, + }); +}; + +//GET ALL (with start and end date query params) +const getIncomes = async (userId, filters = {}) => { + const where = { user_id: userId }; + + if (filters.start || filters.end) { + where.date = {}; + if (filters.start) where.date.gte = new Date(filters.start); + if (filters.end) where.date.lte = new Date(filters.end); + } + + return await prisma.income.findMany({ + where, + include: { category: true }, + orderBy: { date: "desc" }, + }); +}; + +//GET BY ID +const getIncomeById = async (id, userId) => { + return await prisma.income.findFirst({ + where: { income_id: parseInt(id), user_id: userId }, + include: { category: true }, + }); +}; + +//UPDATE +const updateIncome = async (id, userId, data) => { + const income = await prisma.income.findFirst({ + where: { income_id: parseInt(id), user_id: userId }, + }); + + if (!income) { + throw new Error("Income not found or not authorized"); + } + + return await prisma.income.update({ + where: { income_id: parseInt(id) }, + data, + include: { category: true }, + }); +}; + +//DELETE +const deleteIncome = async (id, userId) => { + const income = await prisma.income.findFirst({ + where: { income_id: parseInt(id), user_id: userId }, + }); + + if (!income) { + throw new Error("Income not found or not authorized"); + } + + await prisma.income.delete({ + where: { income_id: parseInt(id) }, + }); +}; + +//---------------INCOME CATEGORIES---------------// + +//GET ALL +const getIncomeCategories = async (userId) => { + return await prisma.incomeCategory.findMany({ + where: { + OR: [{ user_id: null }, { user_id: userId }], + }, + orderBy: [{ is_custom: "asc" }, { category_name: "asc" }], + }); +}; + +//POST +const createIncomeCategory = async (userId, data) => { + const existingCategory = await prisma.incomeCategory.findFirst({ + where: { + category_name: data.category_name, + user_id: userId, + }, + }); + + if (existingCategory) { + throw new Error("Category already exists"); + } + + return await prisma.incomeCategory.create({ + data: { + ...data, + is_custom: true, + user: { connect: { user_id: userId } }, + }, + }); +}; + +//UPDATE +const updateIncomeCategory = async (id, userId, data) => { + const category = await prisma.incomeCategory.findFirst({ + where: { category_id: parseInt(id), user_id: userId }, + }); + + if (!category) { + throw new Error("Category not found or not authorized"); + } + + if (data.category_name) { + const duplicateCategory = await prisma.incomeCategory.findFirst({ + where: { + category_name: data.category_name, + user_id: userId, + NOT: { category_id: parseInt(id) }, + }, + }); + + if (duplicateCategory) { + throw new Error("Category name already exists"); + } + } + + return await prisma.incomeCategory.update({ + where: { category_id: parseInt(id) }, + data, + }); +}; + +//DELETE +const deleteIncomeCategory = async (id, userId) => { + const category = await prisma.incomeCategory.findFirst({ + where: { category_id: parseInt(id), user_id: userId }, + }); + + if (!category) { + throw new Error("Category not found or not authorized"); + } + + const incomesUsingCategory = await prisma.income.count({ + where: { category_id: parseInt(id) }, + }); + + if (incomesUsingCategory > 0) { + throw new Error("Cannot delete category that is being used by incomes"); + } + + await prisma.incomeCategory.delete({ + where: { category_id: parseInt(id) }, + }); +}; + +export default { + createIncome, + getIncomes, + getIncomeById, + updateIncome, + deleteIncome, + getIncomeCategories, + createIncomeCategory, + updateIncomeCategory, + deleteIncomeCategory, +}; From ad7d22d51818babb832ea8832cd5aceb8e98c748 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 20:53:54 +0300 Subject: [PATCH 08/14] refactor: unify income and income-category controllers --- .../{income => }/income.controller.js | 66 ++++++++++++++++++ .../income/incomeCategory.controller.js | 67 ------------------- .../services/{income => }/income.service.js | 2 +- 3 files changed, 67 insertions(+), 68 deletions(-) rename server/controllers/{income => }/income.controller.js (54%) delete mode 100644 server/controllers/income/incomeCategory.controller.js rename server/services/{income => }/income.service.js (94%) diff --git a/server/controllers/income/income.controller.js b/server/controllers/income.controller.js similarity index 54% rename from server/controllers/income/income.controller.js rename to server/controllers/income.controller.js index 3a0346f..ab144fa 100644 --- a/server/controllers/income/income.controller.js +++ b/server/controllers/income.controller.js @@ -1,6 +1,8 @@ import { incomeService } from "../../services/income/income.service"; import { asyncHandler } from "../../utils/asyncHandler"; +//---------------INCOME---------------// + const createIncome = asyncHandler(async (req, res) => { const { amount, date, source, description, category_id } = req.body; @@ -77,10 +79,74 @@ const deleteIncome = asyncHandler(async (req, res) => { } }); +//---------------INCOME CATEGORIES---------------// + +const getIncomeCategories = asyncHandler(async (req, res) => { + const categories = await incomeService.getIncomeCategories(req.user.user_id); + res.json(categories); +}); + +const createIncomeCategory = asyncHandler(async (req, res) => { + const { category_name, icon_url, icon_emoji } = req.body; + + if (!category_name) { + return res.status(400).json({ error: "Category name is required" }); + } + + try { + const category = await incomeService.createIncomeCategory( + req.user.user_id, + { + category_name, + icon_url, + icon_emoji, + } + ); + res.status(201).json(category); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +const updateIncomeCategory = asyncHandler(async (req, res) => { + const { id } = req.params; + const { category_name, icon_url, icon_emoji } = req.body; + + if (!category_name) { + return res.status(400).json({ error: "Category name is required" }); + } + + try { + const category = await incomeService.updateIncomeCategory( + id, + req.user.user_id, + { category_name, icon_url, icon_emoji } + ); + res.json(category); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +const deleteIncomeCategory = asyncHandler(async (req, res) => { + const { id } = req.params; + + try { + await incomeService.deleteIncomeCategory(id, req.user.user_id); + res.status(204).send(); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + export default { createIncome, getIncomes, getIncome, updateIncome, deleteIncome, + getIncomeCategories, + createIncomeCategory, + updateIncomeCategory, + deleteIncomeCategory, }; diff --git a/server/controllers/income/incomeCategory.controller.js b/server/controllers/income/incomeCategory.controller.js deleted file mode 100644 index 0d40659..0000000 --- a/server/controllers/income/incomeCategory.controller.js +++ /dev/null @@ -1,67 +0,0 @@ -import { incomeService } from "../../services/income/income.service"; -import { asyncHandler } from "../../utils/asyncHandler"; - -const getIncomeCategories = asyncHandler(async (req, res) => { - const categories = await incomeService.getIncomeCategories(req.user.user_id); - res.json(categories); -}); - -const createIncomeCategory = asyncHandler(async (req, res) => { - const { category_name, icon_url, icon_emoji } = req.body; - - if (!category_name) { - return res.status(400).json({ error: "Category name is required" }); - } - - try { - const category = await incomeService.createIncomeCategory( - req.user.user_id, - { - category_name, - icon_url, - icon_emoji, - } - ); - res.status(201).json(category); - } catch (error) { - res.status(400).json({ error: error.message }); - } -}); - -const updateIncomeCategory = asyncHandler(async (req, res) => { - const { id } = req.params; - const { category_name, icon_url, icon_emoji } = req.body; - - if (!category_name) { - return res.status(400).json({ error: "Category name is required" }); - } - - try { - const category = await incomeService.updateIncomeCategory( - id, - req.user.user_id, - { category_name, icon_url, icon_emoji } - ); - res.json(category); - } catch (error) { - res.status(400).json({ error: error.message }); - } -}); - -const deleteIncomeCategory = asyncHandler(async (req, res) => { - const { id } = req.params; - - try { - await incomeService.deleteIncomeCategory(id, req.user.user_id); - res.status(204).send(); - } catch (error) { - res.status(400).json({ error: error.message }); - } -}); - -export default { - getIncomeCategories, - createIncomeCategory, - updateIncomeCategory, - deleteIncomeCategory, -}; diff --git a/server/services/income/income.service.js b/server/services/income.service.js similarity index 94% rename from server/services/income/income.service.js rename to server/services/income.service.js index df197f0..f1a5a65 100644 --- a/server/services/income/income.service.js +++ b/server/services/income.service.js @@ -70,7 +70,7 @@ const deleteIncome = async (id, userId) => { }); }; -//---------------INCOME CATEGORIES---------------// +//---------------INCOME CATEGORY---------------// //GET ALL const getIncomeCategories = async (userId) => { From 867f846c413cbf16cf687ed76df975eef47a1e0c Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 20:59:45 +0300 Subject: [PATCH 09/14] feat: income routes --- server/routes/income.route.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 server/routes/income.route.js diff --git a/server/routes/income.route.js b/server/routes/income.route.js new file mode 100644 index 0000000..a9c7a13 --- /dev/null +++ b/server/routes/income.route.js @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { createIncome, getIncomes, getIncome, updateIncome, deleteIncome, getIncomeCategories, createIncomeCategory, updateIncomeCategory, deleteIncomeCategory } from '../controllers/incomeController'; + +const router = Router(); + +//INCOME +router.get('/', getIncomes); +router.get('/:id', getIncome); +router.post('/', createIncome); +router.put('/:id', updateIncome); +router.delete('/:id', deleteIncome); + +//INCOME CATEGORY +router.get('/income-categories/list', getIncomeCategories); +router.post('/income-categories', createIncomeCategory); +router.put('/income-categories/:id', updateIncomeCategory); +router.delete('/income-categories/:id', deleteIncomeCategory); + +export default router; \ No newline at end of file From 87d9b4c76f72e4f59482febd452d95883fdb577d Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 21:04:17 +0300 Subject: [PATCH 10/14] feat: GET /custom-categories endpoint to fetch categories created by the user --- server/controllers/income.controller.js | 5 +++++ server/routes/income.route.js | 5 +++-- server/services/income.service.js | 10 ++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/server/controllers/income.controller.js b/server/controllers/income.controller.js index ab144fa..6c0357e 100644 --- a/server/controllers/income.controller.js +++ b/server/controllers/income.controller.js @@ -86,6 +86,11 @@ const getIncomeCategories = asyncHandler(async (req, res) => { res.json(categories); }); +const getIncomeCategoriesByUser = asyncHandler(async (req, res) => { + const categories = await incomeService.getIncomeCategoriesByUser(req.user.user_id); + res.json(categories); +}); + const createIncomeCategory = asyncHandler(async (req, res) => { const { category_name, icon_url, icon_emoji } = req.body; diff --git a/server/routes/income.route.js b/server/routes/income.route.js index a9c7a13..bb6395a 100644 --- a/server/routes/income.route.js +++ b/server/routes/income.route.js @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { createIncome, getIncomes, getIncome, updateIncome, deleteIncome, getIncomeCategories, createIncomeCategory, updateIncomeCategory, deleteIncomeCategory } from '../controllers/incomeController'; +import { createIncome, getIncomes, getIncome, updateIncome, deleteIncome, getIncomeCategories, getIncomeCategoriesByUser,createIncomeCategory, updateIncomeCategory, deleteIncomeCategory } from '../controllers/incomeController'; const router = Router(); @@ -11,7 +11,8 @@ router.put('/:id', updateIncome); router.delete('/:id', deleteIncome); //INCOME CATEGORY -router.get('/income-categories/list', getIncomeCategories); +router.get('/income-categories', getIncomeCategories); +router.get('/custom-income-categories', getIncomeCategoriesByUser) router.post('/income-categories', createIncomeCategory); router.put('/income-categories/:id', updateIncomeCategory); router.delete('/income-categories/:id', deleteIncomeCategory); diff --git a/server/services/income.service.js b/server/services/income.service.js index f1a5a65..e3b759a 100644 --- a/server/services/income.service.js +++ b/server/services/income.service.js @@ -82,6 +82,16 @@ const getIncomeCategories = async (userId) => { }); }; +//GET ALL BY USER +const getIncomeCategoriesByUser = async (userId) => { + return await prisma.incomeCategory.findMany({ + where: { + user_id: userId, + }, + orderBy: [{ is_custom: "asc" }, { category_name: "asc" }], + }); +}; + //POST const createIncomeCategory = async (userId, data) => { const existingCategory = await prisma.incomeCategory.findFirst({ From c094fd01c079eb6fab9b7bbc84d8e906ff265ab3 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 21:27:03 +0300 Subject: [PATCH 11/14] fix: correct income controller exports --- server/controllers/income.controller.js | 36 +++++++++---------------- server/routes/income.route.js | 12 ++++----- server/server.js | 4 +++ server/services/income.service.js | 2 +- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/server/controllers/income.controller.js b/server/controllers/income.controller.js index 6c0357e..a2becc9 100644 --- a/server/controllers/income.controller.js +++ b/server/controllers/income.controller.js @@ -1,9 +1,9 @@ -import { incomeService } from "../../services/income/income.service"; -import { asyncHandler } from "../../utils/asyncHandler"; +import incomeService from "../services/income.service.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; //---------------INCOME---------------// -const createIncome = asyncHandler(async (req, res) => { +export const createIncome = asyncHandler(async (req, res) => { const { amount, date, source, description, category_id } = req.body; if (!amount || !date || !source || !category_id) { @@ -21,7 +21,7 @@ const createIncome = asyncHandler(async (req, res) => { res.status(201).json(income); }); -const getIncomes = asyncHandler(async (req, res) => { +export const getIncomes = asyncHandler(async (req, res) => { const { start, end } = req.query; const filters = {}; @@ -32,7 +32,7 @@ const getIncomes = asyncHandler(async (req, res) => { res.json(incomes); }); -const getIncome = asyncHandler(async (req, res) => { +export const getIncome = asyncHandler(async (req, res) => { const { id } = req.params; const income = await incomeService.getIncomeById(id, req.user.user_id); @@ -43,7 +43,7 @@ const getIncome = asyncHandler(async (req, res) => { res.json(income); }); -const updateIncome = asyncHandler(async (req, res) => { +export const updateIncome = asyncHandler(async (req, res) => { const { id } = req.params; const updates = req.body; @@ -68,7 +68,7 @@ const updateIncome = asyncHandler(async (req, res) => { } }); -const deleteIncome = asyncHandler(async (req, res) => { +export const deleteIncome = asyncHandler(async (req, res) => { const { id } = req.params; try { @@ -81,17 +81,17 @@ const deleteIncome = asyncHandler(async (req, res) => { //---------------INCOME CATEGORIES---------------// -const getIncomeCategories = asyncHandler(async (req, res) => { +export const getIncomeCategories = asyncHandler(async (req, res) => { const categories = await incomeService.getIncomeCategories(req.user.user_id); res.json(categories); }); -const getIncomeCategoriesByUser = asyncHandler(async (req, res) => { +export const getIncomeCategoriesByUser = asyncHandler(async (req, res) => { const categories = await incomeService.getIncomeCategoriesByUser(req.user.user_id); res.json(categories); }); -const createIncomeCategory = asyncHandler(async (req, res) => { +export const createIncomeCategory = asyncHandler(async (req, res) => { const { category_name, icon_url, icon_emoji } = req.body; if (!category_name) { @@ -113,7 +113,7 @@ const createIncomeCategory = asyncHandler(async (req, res) => { } }); -const updateIncomeCategory = asyncHandler(async (req, res) => { +export const updateIncomeCategory = asyncHandler(async (req, res) => { const { id } = req.params; const { category_name, icon_url, icon_emoji } = req.body; @@ -133,7 +133,7 @@ const updateIncomeCategory = asyncHandler(async (req, res) => { } }); -const deleteIncomeCategory = asyncHandler(async (req, res) => { +export const deleteIncomeCategory = asyncHandler(async (req, res) => { const { id } = req.params; try { @@ -144,14 +144,4 @@ const deleteIncomeCategory = asyncHandler(async (req, res) => { } }); -export default { - createIncome, - getIncomes, - getIncome, - updateIncome, - deleteIncome, - getIncomeCategories, - createIncomeCategory, - updateIncomeCategory, - deleteIncomeCategory, -}; + diff --git a/server/routes/income.route.js b/server/routes/income.route.js index bb6395a..65c5d3c 100644 --- a/server/routes/income.route.js +++ b/server/routes/income.route.js @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { createIncome, getIncomes, getIncome, updateIncome, deleteIncome, getIncomeCategories, getIncomeCategoriesByUser,createIncomeCategory, updateIncomeCategory, deleteIncomeCategory } from '../controllers/incomeController'; +import { createIncome, getIncomes, getIncome, updateIncome, deleteIncome, getIncomeCategories, getIncomeCategoriesByUser,createIncomeCategory, updateIncomeCategory, deleteIncomeCategory } from '../controllers/income.controller.js'; const router = Router(); @@ -11,10 +11,10 @@ router.put('/:id', updateIncome); router.delete('/:id', deleteIncome); //INCOME CATEGORY -router.get('/income-categories', getIncomeCategories); -router.get('/custom-income-categories', getIncomeCategoriesByUser) -router.post('/income-categories', createIncomeCategory); -router.put('/income-categories/:id', updateIncomeCategory); -router.delete('/income-categories/:id', deleteIncomeCategory); +router.get('/categories', getIncomeCategories); +router.get('/custom-categories', getIncomeCategoriesByUser) +router.post('/categories', createIncomeCategory); +router.put('/categories/:id', updateIncomeCategory); +router.delete('/categories/:id', deleteIncomeCategory); export default router; \ No newline at end of file diff --git a/server/server.js b/server/server.js index dd6bced..89927e1 100644 --- a/server/server.js +++ b/server/server.js @@ -2,6 +2,7 @@ import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import { PrismaClient } from '@prisma/client'; +import incomeRoutes from './routes/income.route.js'; dotenv.config(); @@ -11,6 +12,9 @@ const PORT = process.env.PORT || 8080; app.use(cors()); app.use(express.json()); +//Routes +app.use('/api/incomes', incomeRoutes); + // Initialize a single Prisma client instance const prisma = new PrismaClient(); diff --git a/server/services/income.service.js b/server/services/income.service.js index e3b759a..8efc7b2 100644 --- a/server/services/income.service.js +++ b/server/services/income.service.js @@ -1,4 +1,4 @@ -import { prisma } from "../../db/prisma"; +import { prisma } from "../db/prisma.js"; //---------------INCOME---------------// From 34ce5bb6f61afc747835de8d71957db03cba0937 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 21:30:51 +0300 Subject: [PATCH 12/14] chore: add temporary middleware to mock user (id: 4) for testing --- server/server.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/server.js b/server/server.js index 89927e1..476fb04 100644 --- a/server/server.js +++ b/server/server.js @@ -12,6 +12,12 @@ const PORT = process.env.PORT || 8080; app.use(cors()); app.use(express.json()); +//temporary middleware for testing +app.use((req, res, next) => { + req.user = { user_id: 4 }; // fake user, but you have to create a fake user with id 4 as well in your local database + next(); +}); + //Routes app.use('/api/incomes', incomeRoutes); From 01853eea02b1f6f83c6cf1575d5ed99e972a87d4 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 21:46:35 +0300 Subject: [PATCH 13/14] fix(server): handle missing date field in income creation --- server/controllers/income.controller.js | 2 +- server/routes/income.route.js | 14 ++++++++------ server/services/income.service.js | 1 + 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/server/controllers/income.controller.js b/server/controllers/income.controller.js index a2becc9..f6a3213 100644 --- a/server/controllers/income.controller.js +++ b/server/controllers/income.controller.js @@ -6,7 +6,7 @@ import { asyncHandler } from "../utils/asyncHandler.js"; export const createIncome = asyncHandler(async (req, res) => { const { amount, date, source, description, category_id } = req.body; - if (!amount || !date || !source || !category_id) { + if (!amount || !source || !category_id) { return res.status(400).json({ error: "Missing required fields" }); } diff --git a/server/routes/income.route.js b/server/routes/income.route.js index 65c5d3c..bf73523 100644 --- a/server/routes/income.route.js +++ b/server/routes/income.route.js @@ -3,6 +3,13 @@ import { createIncome, getIncomes, getIncome, updateIncome, deleteIncome, getInc const router = Router(); +//INCOME CATEGORY +router.get('/categories', getIncomeCategories); +router.get('/custom-categories', getIncomeCategoriesByUser) +router.post('/categories', createIncomeCategory); +router.put('/categories/:id', updateIncomeCategory); +router.delete('/categories/:id', deleteIncomeCategory); + //INCOME router.get('/', getIncomes); router.get('/:id', getIncome); @@ -10,11 +17,6 @@ router.post('/', createIncome); router.put('/:id', updateIncome); router.delete('/:id', deleteIncome); -//INCOME CATEGORY -router.get('/categories', getIncomeCategories); -router.get('/custom-categories', getIncomeCategoriesByUser) -router.post('/categories', createIncomeCategory); -router.put('/categories/:id', updateIncomeCategory); -router.delete('/categories/:id', deleteIncomeCategory); + export default router; \ No newline at end of file diff --git a/server/services/income.service.js b/server/services/income.service.js index 8efc7b2..10c959e 100644 --- a/server/services/income.service.js +++ b/server/services/income.service.js @@ -175,6 +175,7 @@ export default { deleteIncome, getIncomeCategories, createIncomeCategory, + getIncomeCategoriesByUser, updateIncomeCategory, deleteIncomeCategory, }; From b2634856f0c4e6876f0157007c02e09da055df44 Mon Sep 17 00:00:00 2001 From: colombefioren Date: Sat, 23 Aug 2025 21:51:35 +0300 Subject: [PATCH 14/14] fix(server): resolve invalid date type error --- server/controllers/income.controller.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/server/controllers/income.controller.js b/server/controllers/income.controller.js index f6a3213..2b8dac4 100644 --- a/server/controllers/income.controller.js +++ b/server/controllers/income.controller.js @@ -10,14 +10,21 @@ export const createIncome = asyncHandler(async (req, res) => { return res.status(400).json({ error: "Missing required fields" }); } - const income = await incomeService.createIncome(req.user.user_id, { + const incomeData = { amount: parseFloat(amount), - date: new Date(date), source, description, category: { connect: { category_id: parseInt(category_id) } }, - }); + }; + if (date) { + const parsedDate = new Date(date); + if (!isNaN(parsedDate.getTime())) { + incomeData.date = parsedDate; + } + } + + const income = await incomeService.createIncome(req.user.user_id, incomeData); res.status(201).json(income); }); @@ -87,7 +94,9 @@ export const getIncomeCategories = asyncHandler(async (req, res) => { }); export const getIncomeCategoriesByUser = asyncHandler(async (req, res) => { - const categories = await incomeService.getIncomeCategoriesByUser(req.user.user_id); + const categories = await incomeService.getIncomeCategoriesByUser( + req.user.user_id + ); res.json(categories); }); @@ -143,5 +152,3 @@ export const deleteIncomeCategory = asyncHandler(async (req, res) => { res.status(400).json({ error: error.message }); } }); - -