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 = require ( 'express' ) . Router ( ) ;
const { getDb } = require ( '../db/database' ) ;
const LIVE = 'deleted_at IS NULL' ; // filter for non-deleted payments
// GET /api/payments?bill_id=&year=&month=
router . get ( '/' , ( req , res ) => {
const db = getDb ( ) ;
const { bill _id , year , month } = req . query ;
// Validate year/month when provided
if ( ( year || month ) && ! ( year && month ) ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'Both year and month are required when filtering by date' , 'VALIDATION_ERROR' , 'year' ) ) ;
2026-05-03 19:51:57 -05:00
}
let y , m ;
if ( year && month ) {
y = parseInt ( year , 10 ) ;
m = parseInt ( month , 10 ) ;
if ( ! Number . isInteger ( y ) || y < 2000 || y > 2100 ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'year must be a 4-digit integer between 2000 and 2100' , 'VALIDATION_ERROR' , 'year' ) ) ;
2026-05-03 19:51:57 -05:00
}
if ( ! Number . isInteger ( m ) || m < 1 || m > 12 ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'month must be an integer between 1 and 12' , 'VALIDATION_ERROR' , 'month' ) ) ;
2026-05-03 19:51:57 -05:00
}
}
let query = ` SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p. ${ LIVE } AND b.user_id = ? ` ;
const params = [ req . user . id ] ;
if ( bill _id ) { query += ' AND p.bill_id = ?' ; params . push ( parseInt ( bill _id , 10 ) ) ; }
if ( y && m ) {
const yStr = String ( y ) ;
const mStr = String ( m ) . padStart ( 2 , '0' ) ;
const daysInMonth = new Date ( y , m , 0 ) . getDate ( ) ;
const endDay = String ( daysInMonth ) . padStart ( 2 , '0' ) ;
query += ' AND p.paid_date BETWEEN ? AND ?' ;
params . push ( ` ${ yStr } - ${ mStr } -01 ` , ` ${ yStr } - ${ mStr } - ${ endDay } ` ) ;
}
query += ' ORDER BY p.paid_date DESC' ;
res . json ( db . prepare ( query ) . all ( ... params ) ) ;
} ) ;
// GET /api/payments/:id
router . get ( '/:id' , ( req , res ) => {
const db = getDb ( ) ;
const payment = db . prepare ( ` SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p. ${ LIVE } AND b.user_id = ? ` ) . get ( req . params . id , req . user . id ) ;
2026-05-09 13:03:36 -05:00
if ( ! payment ) return res . status ( 404 ) . json ( standardizeError ( 'Payment not found' , 'NOT_FOUND' , 'id' ) ) ;
2026-05-03 19:51:57 -05:00
res . json ( payment ) ;
} ) ;
// POST /api/payments — create single payment
router . post ( '/' , ( req , res ) => {
const db = getDb ( ) ;
const { bill _id , amount , paid _date , method , notes } = req . body ;
if ( ! bill _id || amount == null || ! paid _date )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'bill_id, amount, and paid_date are required' , 'VALIDATION_ERROR' , 'bill_id' ) ) ;
2026-05-03 19:51:57 -05:00
const parsedAmount = parseFloat ( amount ) ;
if ( isNaN ( parsedAmount ) || parsedAmount <= 0 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'amount must be a positive number' , 'VALIDATION_ERROR' , 'amount' ) ) ;
2026-05-03 19:51:57 -05:00
if ( ! db . prepare ( 'SELECT id FROM bills WHERE id = ? AND user_id = ?' ) . get ( bill _id , req . user . id ) )
2026-05-09 13:03:36 -05:00
return res . status ( 404 ) . json ( standardizeError ( 'Bill not found' , 'NOT_FOUND' , 'bill_id' ) ) ;
2026-05-03 19:51:57 -05:00
const result = db . prepare (
'INSERT INTO payments (bill_id, amount, paid_date, method, notes) VALUES (?, ?, ?, ?, ?)'
) . run ( bill _id , parsedAmount , paid _date , method || null , notes || null ) ;
res . status ( 201 ) . json ( db . prepare ( 'SELECT * FROM payments WHERE id = ?' ) . get ( result . lastInsertRowid ) ) ;
} ) ;
// POST /api/payments/quick — pay a bill (expected amount, today)
router . post ( '/quick' , ( req , res ) => {
const db = getDb ( ) ;
const { bill _id , amount , paid _date , method , notes } = req . body ;
2026-05-09 13:03:36 -05:00
if ( ! bill _id ) return res . status ( 400 ) . json ( standardizeError ( 'bill_id is required' , 'VALIDATION_ERROR' , 'bill_id' ) ) ;
2026-05-03 19:51:57 -05:00
const bill = db . prepare ( 'SELECT * FROM bills WHERE id = ? AND user_id = ?' ) . get ( bill _id , req . user . id ) ;
2026-05-09 13:03:36 -05:00
if ( ! bill ) return res . status ( 404 ) . json ( standardizeError ( 'Bill not found' , 'NOT_FOUND' , 'bill_id' ) ) ;
2026-05-03 19:51:57 -05:00
const payAmount = amount != null ? parseFloat ( amount ) : bill . expected _amount ;
if ( isNaN ( payAmount ) || payAmount <= 0 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'amount must be a positive number' , 'VALIDATION_ERROR' , 'amount' ) ) ;
2026-05-03 19:51:57 -05:00
const payDate = paid _date || new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
const result = db . prepare (
'INSERT INTO payments (bill_id, amount, paid_date, method, notes) VALUES (?, ?, ?, ?, ?)'
) . run ( bill _id , payAmount , payDate , method || null , notes || null ) ;
if ( bill . autopay _enabled ) {
db . prepare ( "UPDATE bills SET autodraft_status='confirmed', updated_at=datetime('now') WHERE id=?" ) . run ( bill _id ) ;
}
res . status ( 201 ) . json ( db . prepare ( 'SELECT * FROM payments WHERE id = ?' ) . get ( result . lastInsertRowid ) ) ;
} ) ;
// POST /api/payments/bulk — record multiple payments in one request
router . post ( '/bulk' , ( req , res ) => {
const db = getDb ( ) ;
const items = req . body ;
if ( ! Array . isArray ( items ) || items . length === 0 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'Body must be a non-empty array of payments' , 'VALIDATION_ERROR' , 'body' ) ) ;
2026-05-03 19:51:57 -05:00
const insert = db . prepare (
'INSERT INTO payments (bill_id, amount, paid_date, method, notes) VALUES (?, ?, ?, ?, ?)'
) ;
const created = [ ] ;
const errors = [ ] ;
const runBulk = db . transaction ( ( ) => {
for ( const item of items ) {
const { bill _id , amount , paid _date , method , notes } = item ;
if ( ! bill _id || amount == null || ! paid _date ) {
2026-05-09 13:03:36 -05:00
errors . push ( { item , error : standardizeError ( 'bill_id, amount, and paid_date are required' , 'VALIDATION_ERROR' , 'bill_id' ) } ) ;
2026-05-03 19:51:57 -05:00
continue ;
}
const parsedAmt = parseFloat ( amount ) ;
if ( isNaN ( parsedAmt ) || parsedAmt <= 0 ) {
2026-05-09 13:03:36 -05:00
errors . push ( { item , error : standardizeError ( 'amount must be a positive number' , 'VALIDATION_ERROR' , 'amount' ) } ) ;
2026-05-03 19:51:57 -05:00
continue ;
}
if ( ! db . prepare ( 'SELECT id FROM bills WHERE id = ? AND user_id = ?' ) . get ( bill _id , req . user . id ) ) {
errors . push ( { item , error : ` Bill ${ bill _id } not found ` } ) ;
continue ;
}
const r = insert . run ( bill _id , parsedAmt , paid _date , method || null , notes || null ) ;
created . push ( db . prepare ( 'SELECT * FROM payments WHERE id = ?' ) . get ( r . lastInsertRowid ) ) ;
}
} ) ;
runBulk ( ) ;
res . status ( 201 ) . json ( { created , errors } ) ;
} ) ;
// PUT /api/payments/:id
router . put ( '/:id' , ( req , res ) => {
const db = getDb ( ) ;
const existing = db . prepare ( ` SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p. ${ LIVE } AND b.user_id = ? ` ) . get ( req . params . id , req . user . id ) ;
2026-05-09 13:03:36 -05:00
if ( ! existing ) return res . status ( 404 ) . json ( standardizeError ( 'Payment not found' , 'NOT_FOUND' , 'id' ) ) ;
2026-05-03 19:51:57 -05:00
const { amount , paid _date , method , notes } = req . body ;
db . prepare ( `
UPDATE payments SET
amount = ? , paid _date = ? , method = ? , notes = ? ,
updated _at = datetime ( 'now' )
WHERE id = ?
` ).run(
amount != null ? parseFloat ( amount ) : existing . amount ,
paid _date ? ? existing . paid _date ,
method !== undefined ? ( method || null ) : existing . method ,
notes !== undefined ? ( notes || null ) : existing . notes ,
req . params . id ,
) ;
res . json ( db . prepare ( 'SELECT * FROM payments WHERE id = ?' ) . get ( req . params . id ) ) ;
} ) ;
// DELETE /api/payments/:id — soft delete (sets deleted_at)
router . delete ( '/:id' , ( req , res ) => {
const db = getDb ( ) ;
const payment = db . prepare ( ` SELECT p.id FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p. ${ LIVE } AND b.user_id = ? ` ) . get ( req . params . id , req . user . id ) ;
2026-05-09 13:03:36 -05:00
if ( ! payment ) return res . status ( 404 ) . json ( standardizeError ( 'Payment not found' , 'NOT_FOUND' , 'id' ) ) ;
2026-05-03 19:51:57 -05:00
db . prepare ( "UPDATE payments SET deleted_at = datetime('now') WHERE id = ?" ) . run ( req . params . id ) ;
res . json ( { success : true } ) ;
} ) ;
// POST /api/payments/:id/restore — undo soft delete
router . post ( '/:id/restore' , ( req , res ) => {
const db = getDb ( ) ;
const payment = db . prepare ( 'SELECT p.id FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.deleted_at IS NOT NULL AND b.user_id = ?' ) . get ( req . params . id , req . user . id ) ;
2026-05-09 13:03:36 -05:00
if ( ! payment ) return res . status ( 404 ) . json ( standardizeError ( 'Deleted payment not found' , 'NOT_FOUND' , 'id' ) ) ;
2026-05-03 19:51:57 -05:00
db . prepare ( 'UPDATE payments SET deleted_at = NULL WHERE id = ?' ) . run ( req . params . id ) ;
res . json ( db . prepare ( 'SELECT * FROM payments WHERE id = ?' ) . get ( req . params . id ) ) ;
} ) ;
module . exports = router ;