2026-05-03 19:51:57 -05:00
const express = require ( 'express' ) ;
2026-05-09 13:03:36 -05:00
const { standardizeError } = require ( '../middleware/errorFormatter' ) ;
2026-05-03 19:51:57 -05:00
const router = express . Router ( ) ;
const { getDb , ensureUserDefaultCategories } = require ( '../db/database' ) ;
// GET /api/categories
router . get ( '/' , ( req , res ) => {
const db = getDb ( ) ;
ensureUserDefaultCategories ( req . user . id ) ;
2026-05-04 16:38:03 -05:00
const categories = db . prepare ( `
SELECT id , user _id , name , created _at , updated _at
FROM categories
WHERE user _id = ?
2026-05-16 10:34:32 -05:00
AND deleted _at IS NULL
2026-05-04 16:38:03 -05:00
ORDER BY name COLLATE NOCASE ASC
` ).all(req.user.id);
const billsByCategory = db . prepare ( `
SELECT
b . id ,
b . category _id ,
b . name ,
b . active ,
b . expected _amount ,
b . due _day ,
COUNT ( p . id ) AS payment _count ,
COALESCE ( SUM ( p . amount ) , 0 ) AS total _paid ,
MAX ( p . paid _date ) AS last _paid _date
FROM bills b
LEFT JOIN payments p
ON p . bill _id = b . id
AND p . deleted _at IS NULL
WHERE b . user _id = ?
AND b . category _id = ?
2026-05-16 10:34:32 -05:00
AND b . deleted _at IS NULL
2026-05-04 16:38:03 -05:00
GROUP BY b . id
ORDER BY b . active DESC , b . due _day ASC , b . name COLLATE NOCASE ASC
` );
const shaped = categories . map ( category => {
const bills = billsByCategory . all ( req . user . id , category . id ) . map ( bill => ( {
... bill ,
active : ! ! bill . active ,
payment _count : Number ( bill . payment _count || 0 ) ,
total _paid : Number ( bill . total _paid || 0 ) ,
last _paid _date : bill . last _paid _date || null ,
} ) ) ;
const activeBillCount = bills . filter ( bill => bill . active ) . length ;
const inactiveBillCount = bills . length - activeBillCount ;
const paymentCount = bills . reduce ( ( sum , bill ) => sum + bill . payment _count , 0 ) ;
return {
... category ,
bill _count : activeBillCount ,
active _bill _count : activeBillCount ,
inactive _bill _count : inactiveBillCount ,
payment _count : paymentCount ,
bill _names : bills . map ( bill => bill . name ) ,
bills ,
} ;
} ) ;
res . json ( shaped ) ;
2026-05-03 19:51:57 -05:00
} ) ;
// POST /api/categories
router . post ( '/' , ( req , res ) => {
const db = getDb ( ) ;
const { name } = req . body ;
2026-05-09 13:03:36 -05:00
if ( ! name ) return res . status ( 400 ) . json ( standardizeError ( 'name is required' , 'VALIDATION_ERROR' , 'name' ) ) ;
2026-05-03 19:51:57 -05:00
try {
const result = db . prepare ( 'INSERT INTO categories (user_id, name) VALUES (?, ?)' ) . run ( req . user . id , name . trim ( ) ) ;
const created = db . prepare ( 'SELECT * FROM categories WHERE id = ?' ) . get ( result . lastInsertRowid ) ;
res . status ( 201 ) . json ( created ) ;
} catch ( e ) {
if ( e . message . includes ( 'UNIQUE' ) ) {
2026-05-09 13:03:36 -05:00
return res . status ( 409 ) . json ( standardizeError ( 'Category already exists' , 'CONFLICT' , 'name' ) ) ;
2026-05-03 19:51:57 -05:00
}
throw e ;
}
} ) ;
// PUT /api/categories/:id
router . put ( '/:id' , ( req , res ) => {
const db = getDb ( ) ;
const { name } = req . body ;
2026-05-09 13:03:36 -05:00
if ( ! name ) return res . status ( 400 ) . json ( standardizeError ( 'name is required' , 'VALIDATION_ERROR' , 'name' ) ) ;
2026-05-03 19:51:57 -05:00
2026-05-16 10:34:32 -05:00
const cat = db . prepare ( 'SELECT id FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NULL' ) . get ( req . params . id , req . user . id ) ;
2026-05-09 13:03:36 -05:00
if ( ! cat ) return res . status ( 404 ) . json ( standardizeError ( 'Category not found' , 'NOT_FOUND' , 'id' ) ) ;
2026-05-03 19:51:57 -05:00
try {
db . prepare ( "UPDATE categories SET name = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?" )
. run ( name . trim ( ) , req . params . id , req . user . id ) ;
2026-05-16 10:34:32 -05:00
res . json ( db . prepare ( 'SELECT * FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NULL' ) . get ( req . params . id , req . user . id ) ) ;
2026-05-03 19:51:57 -05:00
} catch ( e ) {
if ( e . message . includes ( 'UNIQUE' ) ) {
2026-05-09 13:03:36 -05:00
return res . status ( 409 ) . json ( standardizeError ( 'Category already exists' , 'CONFLICT' , 'name' ) ) ;
2026-05-03 19:51:57 -05:00
}
throw e ;
}
} ) ;
// DELETE /api/categories/:id
router . delete ( '/:id' , ( req , res ) => {
const db = getDb ( ) ;
2026-05-16 10:34:32 -05:00
const cat = db . prepare ( 'SELECT id, name FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NULL' ) . get ( req . params . id , req . user . id ) ;
2026-05-09 13:03:36 -05:00
if ( ! cat ) return res . status ( 404 ) . json ( standardizeError ( 'Category not found' , 'NOT_FOUND' , 'id' ) ) ;
2026-05-04 20:12:57 -05:00
2026-05-16 10:34:32 -05:00
const deleted = db . prepare ( "UPDATE categories SET deleted_at = datetime('now'), updated_at = datetime('now') WHERE id = ? AND user_id = ? AND deleted_at IS NULL" )
. run ( req . params . id , req . user . id ) ;
2026-05-04 20:12:57 -05:00
2026-05-16 10:34:32 -05:00
res . json ( {
success : true ,
deleted : deleted . changes ,
deleted _category _id : cat . id ,
deleted _category _name : cat . name ,
recoverable _until _days : 30 ,
2026-05-04 20:12:57 -05:00
} ) ;
2026-05-16 10:34:32 -05:00
} ) ;
// POST /api/categories/:id/restore — undo category soft delete
router . post ( '/:id/restore' , ( req , res ) => {
const db = getDb ( ) ;
const cat = db . prepare ( 'SELECT id, name FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NOT NULL' ) . get ( req . params . id , req . user . id ) ;
if ( ! cat ) return res . status ( 404 ) . json ( standardizeError ( 'Deleted category not found' , 'NOT_FOUND' , 'id' ) ) ;
2026-05-04 20:12:57 -05:00
2026-05-16 10:34:32 -05:00
db . prepare ( "UPDATE categories SET deleted_at = NULL, updated_at = datetime('now') WHERE id = ? AND user_id = ?" )
. run ( req . params . id , req . user . id ) ;
res . json ( db . prepare ( 'SELECT * FROM categories WHERE id = ? AND user_id = ?' ) . get ( req . params . id , req . user . id ) ) ;
2026-05-03 19:51:57 -05:00
} ) ;
module . exports = router ;