2026-05-03 19:51:57 -05:00
const express = require ( 'express' ) ;
const router = express . Router ( ) ;
const { getDb , ensureUserDefaultCategories } = require ( '../db/database' ) ;
2026-05-11 12:12:31 -05:00
const { VALID _VISIBILITY , getValidCycleTypes , parseDueDay , parseInterestRate , validateCycleDay , validateBillData } = require ( '../services/billsService' ) ;
2026-05-09 13:03:36 -05:00
const { standardizeError } = require ( '../middleware/errorFormatter' ) ;
2026-05-03 19:51:57 -05:00
// ── GET /api/bills ────────────────────────────────────────────────────────────
router . get ( '/' , ( req , res ) => {
const db = getDb ( ) ;
ensureUserDefaultCategories ( req . user . id ) ;
const includeInactive = req . query . inactive === 'true' ;
const bills = db . prepare ( `
SELECT b . * , c . name AS category _name ,
CASE WHEN EXISTS (
SELECT 1 FROM bill _history _ranges WHERE bill _id = b . id
) THEN 1 ELSE 0 END AS has _history _ranges
FROM bills b
LEFT JOIN categories c ON b . category _id = c . id
WHERE b . user _id = ?
$ { includeInactive ? '' : 'AND b.active = 1' }
ORDER BY b . due _day ASC , b . name ASC
` ).all(req.user.id);
res . json ( bills ) ;
} ) ;
// ── GET /api/bills/:id/monthly-state?year=&month= ─────────────────────────────
router . get ( '/:id/monthly-state' , ( req , res ) => {
const db = getDb ( ) ;
const billId = parseInt ( req . params . id , 10 ) ;
if ( ! db . prepare ( 'SELECT id FROM bills WHERE id = ? AND user_id = ?' ) . get ( billId , 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 year = parseInt ( req . query . year , 10 ) ;
const month = parseInt ( req . query . month , 10 ) ;
if ( isNaN ( year ) || year < 2000 || year > 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 ( isNaN ( month ) || month < 1 || month > 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
const mbs = db . prepare (
'SELECT actual_amount, notes, is_skipped FROM monthly_bill_state WHERE bill_id=? AND year=? AND month=?'
) . get ( billId , year , month ) ;
res . json ( {
bill _id : billId ,
year ,
month ,
actual _amount : mbs ? . actual _amount ? ? null ,
notes : mbs ? . notes ? ? null ,
is _skipped : ! ! ( mbs ? . is _skipped ) ,
} ) ;
} ) ;
// ── PUT /api/bills/:id/monthly-state ──────────────────────────────────────────
router . put ( '/:id/monthly-state' , ( req , res ) => {
const db = getDb ( ) ;
const billId = parseInt ( req . params . id , 10 ) ;
if ( ! db . prepare ( 'SELECT id FROM bills WHERE id = ? AND user_id = ?' ) . get ( billId , 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 { year , month , actual _amount , notes , is _skipped } = req . body ;
const y = parseInt ( year , 10 ) ;
const m = parseInt ( month , 10 ) ;
if ( isNaN ( 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 ( isNaN ( 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
if ( actual _amount !== undefined && actual _amount !== null ) {
const amt = parseFloat ( actual _amount ) ;
if ( isNaN ( amt ) || amt < 0 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'actual_amount must be a non-negative number or null' , 'VALIDATION_ERROR' , 'actual_amount' ) ) ;
2026-05-03 19:51:57 -05:00
}
const amt = actual _amount !== undefined ? ( actual _amount === null ? null : parseFloat ( actual _amount ) ) : null ;
const noteVal = notes !== undefined ? ( notes || null ) : null ;
const skipVal = is _skipped !== undefined ? ( is _skipped ? 1 : 0 ) : 0 ;
db . prepare ( `
INSERT INTO monthly _bill _state ( bill _id , year , month , actual _amount , notes , is _skipped , updated _at )
VALUES ( ? , ? , ? , ? , ? , ? , datetime ( 'now' ) )
ON CONFLICT ( bill _id , year , month ) DO UPDATE SET
actual _amount = excluded . actual _amount ,
notes = excluded . notes ,
is _skipped = excluded . is _skipped ,
updated _at = datetime ( 'now' )
` ).run(billId, y, m, amt, noteVal, skipVal);
const saved = db . prepare (
'SELECT * FROM monthly_bill_state WHERE bill_id=? AND year=? AND month=?'
) . get ( billId , y , m ) ;
res . json ( {
bill _id : saved . bill _id ,
year : saved . year ,
month : saved . month ,
actual _amount : saved . actual _amount ,
notes : saved . notes ,
is _skipped : ! ! saved . is _skipped ,
created _at : saved . created _at ,
updated _at : saved . updated _at ,
} ) ;
} ) ;
// ── GET /api/bills/:id ────────────────────────────────────────────────────────
router . get ( '/:id' , ( req , res ) => {
const db = getDb ( ) ;
const bill = db . prepare ( `
SELECT b . * , c . name AS category _name ,
CASE WHEN EXISTS (
SELECT 1 FROM bill _history _ranges WHERE bill _id = b . id
) THEN 1 ELSE 0 END AS has _history _ranges
FROM bills b
LEFT JOIN categories c ON b . category _id = c . id
WHERE b . id = ? AND b . user _id = ?
` ).get(req.params.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
res . json ( bill ) ;
} ) ;
// ── POST /api/bills ───────────────────────────────────────────────────────────
router . post ( '/' , ( req , res ) => {
const db = getDb ( ) ;
const {
name , category _id , due _day , override _due _date , expected _amount , interest _rate ,
billing _cycle , autopay _enabled , autodraft _status , website , username ,
2026-05-10 00:39:11 -05:00
account _info , has _2fa , notes , history _visibility , cycle _type , cycle _day ,
2026-05-03 19:51:57 -05:00
} = req . body ;
2026-05-11 12:12:31 -05:00
// Validate and normalize bill data
const validation = validateBillData ( req . body ) ;
if ( validation . errors . length > 0 ) {
const firstError = validation . errors [ 0 ] ;
return res . status ( 400 ) . json ( standardizeError ( firstError . message , 'VALIDATION_ERROR' , firstError . field ) ) ;
2026-05-03 19:51:57 -05:00
}
2026-05-11 12:12:31 -05:00
const { normalized } = validation ;
2026-05-03 19:51:57 -05:00
2026-05-11 12:12:31 -05:00
// Validate category_id exists for this user
if ( normalized . category _id && ! db . prepare ( 'SELECT id FROM categories WHERE id = ? AND user_id = ?' ) . get ( normalized . category _id , req . user . id ) ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'category_id is invalid for this user' , 'VALIDATION_ERROR' , 'category_id' ) ) ;
2026-05-03 19:51:57 -05:00
}
const result = db . prepare ( `
INSERT INTO bills
( user _id , name , category _id , due _day , override _due _date , bucket , expected _amount ,
interest _rate , billing _cycle , autopay _enabled , autodraft _status , website , username ,
2026-05-10 00:39:11 -05:00
account _info , has _2fa , notes , history _visibility , active , cycle _type , cycle _day )
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , 1 , ? , ? )
2026-05-03 19:51:57 -05:00
` ).run(
req . user . id ,
2026-05-11 12:12:31 -05:00
normalized . name ,
normalized . category _id ,
normalized . due _day ,
normalized . override _due _date ,
normalized . bucket ,
normalized . expected _amount ,
normalized . interest _rate ,
normalized . billing _cycle ,
normalized . autopay _enabled ,
normalized . autodraft _status ,
normalized . website ,
normalized . username ,
normalized . account _info ,
normalized . has _2fa ,
normalized . notes ,
normalized . history _visibility ,
normalized . cycle _type ,
normalized . cycle _day ,
2026-05-03 19:51:57 -05:00
) ;
const created = db . prepare ( 'SELECT * FROM bills WHERE id = ?' ) . get ( result . lastInsertRowid ) ;
res . status ( 201 ) . json ( created ) ;
} ) ;
// ── PUT /api/bills/:id ────────────────────────────────────────────────────────
router . put ( '/:id' , ( req , res ) => {
const db = getDb ( ) ;
const existing = db . prepare ( 'SELECT * FROM bills WHERE id = ? AND 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 ( 'Bill not found' , 'NOT_FOUND' , 'bill_id' ) ) ;
2026-05-03 19:51:57 -05:00
2026-05-11 12:12:31 -05:00
// Validate and normalize bill data
const validation = validateBillData ( req . body , existing ) ;
if ( validation . errors . length > 0 ) {
const firstError = validation . errors [ 0 ] ;
return res . status ( 400 ) . json ( standardizeError ( firstError . message , 'VALIDATION_ERROR' , firstError . field ) ) ;
2026-05-03 19:51:57 -05:00
}
2026-05-11 12:12:31 -05:00
const { normalized } = validation ;
2026-05-03 19:51:57 -05:00
2026-05-11 12:12:31 -05:00
// Validate category_id exists for this user if changed
if ( normalized . category _id && ! db . prepare ( 'SELECT id FROM categories WHERE id = ? AND user_id = ?' ) . get ( normalized . category _id , req . user . id ) ) {
return res . status ( 400 ) . json ( standardizeError ( 'category_id is invalid for this user' , 'VALIDATION_ERROR' , 'category_id' ) ) ;
2026-05-10 00:39:11 -05:00
}
2026-05-03 19:51:57 -05:00
db . prepare ( `
UPDATE bills SET
name = ? , category _id = ? , due _day = ? , override _due _date = ? , bucket = ? ,
expected _amount = ? , interest _rate = ? , billing _cycle = ? , autopay _enabled = ? , autodraft _status = ? ,
website = ? , username = ? , account _info = ? , has _2fa = ? , notes = ? , active = ? ,
2026-05-10 00:39:11 -05:00
history _visibility = ? , cycle _type = ? , cycle _day = ? ,
2026-05-03 19:51:57 -05:00
updated _at = datetime ( 'now' )
WHERE id = ? AND user _id = ?
` ).run(
2026-05-11 12:12:31 -05:00
normalized . name ,
normalized . category _id ,
normalized . due _day ,
normalized . override _due _date ,
normalized . bucket ,
normalized . expected _amount ,
normalized . interest _rate ,
normalized . billing _cycle ,
normalized . autopay _enabled ,
normalized . autodraft _status ,
normalized . website ,
normalized . username ,
normalized . account _info ,
normalized . has _2fa ,
normalized . notes ,
normalized . active ,
normalized . history _visibility ,
normalized . cycle _type ,
normalized . cycle _day ,
2026-05-03 19:51:57 -05:00
req . params . id ,
req . user . id ,
) ;
const updated = db . prepare ( 'SELECT * FROM bills WHERE id = ? AND user_id = ?' ) . get ( req . params . id , req . user . id ) ;
res . json ( updated ) ;
} ) ;
// ── DELETE /api/bills/:id — destructive hard-delete ───────────────────────────
// Permanently removes the bill and all associated data (payments, monthly state,
// history ranges). Inactivation (PUT with active:0) is the safer alternative.
// WARNING: this action is irreversible.
router . delete ( '/:id' , ( req , res ) => {
const db = getDb ( ) ;
const bill = db . prepare ( 'SELECT id, name FROM bills WHERE id = ? AND user_id = ?' ) . get ( req . params . 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
// ON DELETE CASCADE in the schema removes payments, monthly_bill_state, and
// bill_history_ranges automatically. Verify foreign_keys pragma is ON.
db . prepare ( 'DELETE FROM bills WHERE id = ? AND user_id = ?' ) . run ( req . params . id , req . user . id ) ;
res . json ( {
success : true ,
deleted _bill _id : bill . id ,
deleted _bill _name : bill . name ,
warning : 'Bill and all associated payments, monthly state, and history ranges were permanently deleted.' ,
} ) ;
} ) ;
// ── GET /api/bills/:id/payments?page=1&limit=20 ───────────────────────────────
router . get ( '/:id/payments' , ( req , res ) => {
const db = getDb ( ) ;
const bill = db . prepare ( 'SELECT id, name FROM bills WHERE id = ? AND user_id = ?' ) . get ( req . params . 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 limit = Math . min ( parseInt ( req . query . limit || '20' , 10 ) , 100 ) ;
const page = Math . max ( parseInt ( req . query . page || '1' , 10 ) , 1 ) ;
const offset = ( page - 1 ) * limit ;
const total = db . prepare (
'SELECT COUNT(*) AS n FROM payments WHERE bill_id = ? AND deleted_at IS NULL'
) . get ( req . params . id ) . n ;
const items = db . prepare (
'SELECT * FROM payments WHERE bill_id = ? AND deleted_at IS NULL ORDER BY paid_date DESC LIMIT ? OFFSET ?'
) . all ( req . params . id , limit , offset ) ;
res . json ( {
bill _id : parseInt ( req . params . id , 10 ) ,
bill _name : bill . name ,
total ,
page ,
limit ,
pages : Math . ceil ( total / limit ) ,
payments : items ,
} ) ;
} ) ;
2026-05-09 13:03:36 -05:00
// ── POST /api/bills/:id/toggle-paid — toggle Paid/Unpaid status ──────────────
router . post ( '/:id/toggle-paid' , ( req , res ) => {
const db = getDb ( ) ;
const billId = parseInt ( req . params . id , 10 ) ;
2026-05-10 15:25:47 -05:00
// Get bill - always scope to the requesting user
2026-05-11 11:56:49 -05:00
const bill = db . prepare ( 'SELECT id, expected_amount, user_id, due_day FROM bills WHERE id = ? AND user_id = ?' ) . get ( billId , 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-11 11:56:49 -05:00
// Scope to year/month if provided
const year = req . body . year !== undefined ? parseInt ( req . body . year , 10 ) : null ;
const month = req . body . month !== undefined ? parseInt ( req . body . month , 10 ) : null ;
let currentPayment ;
if ( year !== null && month !== null ) {
currentPayment = db . prepare (
'SELECT * FROM payments WHERE bill_id = ? AND deleted_at IS NULL AND strftime(\'%Y\', paid_date) = ? AND strftime(\'%m\', paid_date) = ? ORDER BY paid_date DESC LIMIT 1'
) . get ( billId , String ( year ) , String ( month ) . padStart ( 2 , '0' ) ) ;
} else {
currentPayment = db . prepare (
'SELECT * FROM payments WHERE bill_id = ? AND deleted_at IS NULL ORDER BY paid_date DESC LIMIT 1'
) . get ( billId ) ;
}
2026-05-09 13:03:36 -05:00
// If paid (has payment), remove it → Unpaid
if ( currentPayment ) {
db . prepare ( "UPDATE payments SET deleted_at = datetime('now') WHERE id = ?" ) . run ( currentPayment . id ) ;
res . json ( {
success : true ,
isPaid : false ,
action : 'removed_payment' ,
paymentId : currentPayment . id ,
} ) ;
return ;
}
// If unpaid, create payment → Paid
// Use expected_amount if no amount provided
const amount = req . body . amount !== undefined ? parseFloat ( req . body . amount ) : bill . expected _amount ;
2026-05-11 11:56:49 -05:00
// Determine paid_date
let paidDate = req . body . paid _date ;
if ( ! paidDate && year !== null && month !== null ) {
// Calculate paid_date from bill's due_day clamped to the month's days
const daysInMonth = new Date ( year , month , 0 ) . getDate ( ) ;
const day = Math . min ( Math . max ( Number ( bill . due _day ) , 1 ) , daysInMonth ) ;
paidDate = ` ${ year } - ${ String ( month ) . padStart ( 2 , '0' ) } - ${ String ( day ) . padStart ( 2 , '0' ) } ` ;
} else if ( ! paidDate ) {
paidDate = new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
}
2026-05-09 13:03:36 -05:00
const method = req . body . method || null ;
const notes = req . body . notes || null ;
if ( isNaN ( amount ) || amount <= 0 ) {
return res . status ( 400 ) . json ( standardizeError ( 'amount must be a positive number' , 'VALIDATION_ERROR' , 'amount' ) ) ;
}
const result = db . prepare (
'INSERT INTO payments (bill_id, amount, paid_date, method, notes) VALUES (?, ?, ?, ?, ?)'
) . run ( billId , amount , paidDate , method , notes ) ;
res . status ( 201 ) . json ( {
success : true ,
isPaid : true ,
action : 'created_payment' ,
payment : db . prepare ( 'SELECT * FROM payments WHERE id = ?' ) . get ( result . lastInsertRowid ) ,
} ) ;
} ) ;
2026-05-03 19:51:57 -05:00
// ── GET /api/bills/:id/history-ranges ────────────────────────────────────────
router . get ( '/:id/history-ranges' , ( req , res ) => {
const db = getDb ( ) ;
if ( ! db . prepare ( 'SELECT id FROM bills WHERE id = ? AND user_id = ?' ) . get ( req . params . 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 ranges = db . prepare (
'SELECT * FROM bill_history_ranges WHERE bill_id = ? ORDER BY start_year ASC, start_month ASC'
) . all ( req . params . id ) ;
const bill = db . prepare ( 'SELECT history_visibility FROM bills WHERE id = ?' ) . get ( req . params . id ) ;
res . json ( { bill _id : parseInt ( req . params . id , 10 ) , history _visibility : bill . history _visibility , ranges } ) ;
} ) ;
// ── POST /api/bills/:id/history-ranges ───────────────────────────────────────
router . post ( '/:id/history-ranges' , ( req , res ) => {
const db = getDb ( ) ;
if ( ! db . prepare ( 'SELECT id FROM bills WHERE id = ? AND user_id = ?' ) . get ( req . params . 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 { start _year , start _month , end _year , end _month , label } = req . body ;
const sy = parseInt ( start _year , 10 ) ;
const sm = parseInt ( start _month , 10 ) ;
if ( isNaN ( sy ) || sy < 2000 || sy > 2100 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'start_year must be between 2000 and 2100' , 'VALIDATION_ERROR' , 'start_year' ) ) ;
2026-05-03 19:51:57 -05:00
if ( isNaN ( sm ) || sm < 1 || sm > 12 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'start_month must be between 1 and 12' , 'VALIDATION_ERROR' , 'start_month' ) ) ;
2026-05-03 19:51:57 -05:00
let ey = null , em = null ;
if ( end _year != null ) {
ey = parseInt ( end _year , 10 ) ;
if ( isNaN ( ey ) || ey < 2000 || ey > 2100 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'end_year must be between 2000 and 2100' , 'VALIDATION_ERROR' , 'end_year' ) ) ;
2026-05-03 19:51:57 -05:00
}
if ( end _month != null ) {
em = parseInt ( end _month , 10 ) ;
if ( isNaN ( em ) || em < 1 || em > 12 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'end_month must be between 1 and 12' , 'VALIDATION_ERROR' , 'end_month' ) ) ;
2026-05-03 19:51:57 -05:00
}
if ( ( ey == null ) !== ( em == null ) ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'end_year and end_month must both be provided or both omitted' , 'VALIDATION_ERROR' , 'end_year' ) ) ;
2026-05-03 19:51:57 -05:00
}
if ( ey != null ) {
const startVal = sy * 12 + sm ;
const endVal = ey * 12 + em ;
if ( endVal < startVal )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'end date must be on or after start date' , 'VALIDATION_ERROR' , 'end_year' ) ) ;
2026-05-03 19:51:57 -05:00
}
const result = db . prepare ( `
INSERT INTO bill _history _ranges ( bill _id , start _year , start _month , end _year , end _month , label )
VALUES ( ? , ? , ? , ? , ? , ? )
` ).run(req.params.id, sy, sm, ey, em, label || null);
const created = db . prepare ( 'SELECT * FROM bill_history_ranges WHERE id = ?' ) . get ( result . lastInsertRowid ) ;
res . status ( 201 ) . json ( created ) ;
} ) ;
// ── PUT /api/bills/:id/history-ranges/:rangeId ───────────────────────────────
router . put ( '/:id/history-ranges/:rangeId' , ( req , res ) => {
const db = getDb ( ) ;
if ( ! db . prepare ( 'SELECT id FROM bills WHERE id = ? AND user_id = ?' ) . get ( req . params . 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 range = db . prepare ( 'SELECT * FROM bill_history_ranges WHERE id = ? AND bill_id = ?' )
. get ( req . params . rangeId , req . params . id ) ;
2026-05-09 13:03:36 -05:00
if ( ! range ) return res . status ( 404 ) . json ( standardizeError ( 'History range not found' , 'NOT_FOUND' , 'rangeId' ) ) ;
2026-05-03 19:51:57 -05:00
const { start _year , start _month , end _year , end _month , label } = req . body ;
const sy = start _year != null ? parseInt ( start _year , 10 ) : range . start _year ;
const sm = start _month != null ? parseInt ( start _month , 10 ) : range . start _month ;
if ( isNaN ( sy ) || sy < 2000 || sy > 2100 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'start_year must be between 2000 and 2100' , 'VALIDATION_ERROR' , 'start_year' ) ) ;
2026-05-03 19:51:57 -05:00
if ( isNaN ( sm ) || sm < 1 || sm > 12 )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'start_month must be between 1 and 12' , 'VALIDATION_ERROR' , 'start_month' ) ) ;
2026-05-03 19:51:57 -05:00
let ey = range . end _year ;
let em = range . end _month ;
if ( end _year !== undefined ) ey = end _year != null ? parseInt ( end _year , 10 ) : null ;
if ( end _month !== undefined ) em = end _month != null ? parseInt ( end _month , 10 ) : null ;
if ( ey != null && ( isNaN ( ey ) || ey < 2000 || ey > 2100 ) )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'end_year must be between 2000 and 2100' , 'VALIDATION_ERROR' , 'end_year' ) ) ;
2026-05-03 19:51:57 -05:00
if ( em != null && ( isNaN ( em ) || em < 1 || em > 12 ) )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'end_month must be between 1 and 12' , 'VALIDATION_ERROR' , 'end_month' ) ) ;
2026-05-03 19:51:57 -05:00
if ( ( ey == null ) !== ( em == null ) )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'end_year and end_month must both be provided or both omitted' , 'VALIDATION_ERROR' , 'end_year' ) ) ;
2026-05-03 19:51:57 -05:00
if ( ey != null && ( ey * 12 + em ) < ( sy * 12 + sm ) )
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'end date must be on or after start date' , 'VALIDATION_ERROR' , 'end_year' ) ) ;
2026-05-03 19:51:57 -05:00
db . prepare ( `
UPDATE bill _history _ranges
SET start _year = ? , start _month = ? , end _year = ? , end _month = ? , label = ? ,
updated _at = datetime ( 'now' )
WHERE id = ? AND bill _id = ?
` ).run(sy, sm, ey, em, label !== undefined ? (label || null) : range.label, req.params.rangeId, req.params.id);
const updated = db . prepare ( 'SELECT * FROM bill_history_ranges WHERE id = ?' ) . get ( req . params . rangeId ) ;
res . json ( updated ) ;
} ) ;
// ── DELETE /api/bills/:id/history-ranges/:rangeId ────────────────────────────
router . delete ( '/:id/history-ranges/:rangeId' , ( req , res ) => {
const db = getDb ( ) ;
if ( ! db . prepare ( 'SELECT id FROM bills WHERE id = ? AND user_id = ?' ) . get ( req . params . 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 range = db . prepare ( 'SELECT id FROM bill_history_ranges WHERE id = ? AND bill_id = ?' )
. get ( req . params . rangeId , req . params . id ) ;
2026-05-09 13:03:36 -05:00
if ( ! range ) return res . status ( 404 ) . json ( standardizeError ( 'History range not found' , 'NOT_FOUND' , 'rangeId' ) ) ;
2026-05-03 19:51:57 -05:00
db . prepare ( 'DELETE FROM bill_history_ranges WHERE id = ? AND bill_id = ?' )
. run ( req . params . rangeId , req . params . id ) ;
res . json ( { success : true } ) ;
} ) ;
module . exports = router ;