const express = require('express'); const router = express.Router(); const { pool } = require('../db'); /** * Validate the request body for bill create/update operations. * * Checks that all required fields are present and within acceptable ranges. * Amount is optional when `variable_amount` is true (defaults to 0 on save). * * @param {object} body - Request body. * @param {string} body.name - Bill name (non-empty). * @param {number|string} [body.amount] - Bill amount; required when variable_amount is falsy. * @param {number|string} body.due_day - Day of month (1–31). * @param {number|string} body.assigned_paycheck - Which paycheck: 1 or 2. * @param {boolean} [body.variable_amount] - Whether the bill amount varies each month. * @returns {string|null} Validation error message, or null when valid. */ function validateBillFields(body) { const { name, amount, due_day, assigned_paycheck, variable_amount } = body; if (!name || name.toString().trim() === '') { return 'name is required'; } // Amount is optional for variable bills (defaults to 0) if (!variable_amount && (amount === undefined || amount === null || amount === '')) { return 'amount is required'; } if (amount !== undefined && amount !== null && amount !== '' && isNaN(Number(amount))) { return 'amount must be a number'; } if (due_day === undefined || due_day === null || due_day === '') { return 'due_day is required'; } const dueDayInt = parseInt(due_day, 10); if (isNaN(dueDayInt) || dueDayInt < 1 || dueDayInt > 31) { return 'due_day must be an integer between 1 and 31'; } if (assigned_paycheck === undefined || assigned_paycheck === null || assigned_paycheck === '') { return 'assigned_paycheck is required'; } const pc = parseInt(assigned_paycheck, 10); if (pc !== 1 && pc !== 2) { return 'assigned_paycheck must be 1 or 2'; } return null; } // GET /api/bills — list all bills router.get('/bills', async (req, res) => { try { const result = await pool.query( 'SELECT * FROM bills ORDER BY assigned_paycheck, name' ); res.json(result.rows); } catch (err) { console.error('GET /api/bills error:', err); res.status(500).json({ error: 'Failed to fetch bills' }); } }); // POST /api/bills — create a bill router.post('/bills', async (req, res) => { const validationError = validateBillFields(req.body); if (validationError) { return res.status(400).json({ error: validationError }); } const { name, amount, due_day, assigned_paycheck, category = 'General', active = true, variable_amount = false, } = req.body; try { const result = await pool.query( `INSERT INTO bills (name, amount, due_day, assigned_paycheck, category, active, variable_amount) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, [ name.toString().trim(), Number(amount) || 0, parseInt(due_day, 10), parseInt(assigned_paycheck, 10), category || 'General', active !== undefined ? active : true, Boolean(variable_amount), ] ); res.status(201).json(result.rows[0]); } catch (err) { console.error('POST /api/bills error:', err); res.status(500).json({ error: 'Failed to create bill' }); } }); // GET /api/bills/:id — get single bill router.get('/bills/:id', async (req, res) => { try { const result = await pool.query('SELECT * FROM bills WHERE id = $1', [req.params.id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Bill not found' }); } res.json(result.rows[0]); } catch (err) { console.error('GET /api/bills/:id error:', err); res.status(500).json({ error: 'Failed to fetch bill' }); } }); // PUT /api/bills/:id — update bill router.put('/bills/:id', async (req, res) => { const validationError = validateBillFields(req.body); if (validationError) { return res.status(400).json({ error: validationError }); } const { name, amount, due_day, assigned_paycheck, category = 'General', active = true, variable_amount = false, } = req.body; try { const result = await pool.query( `UPDATE bills SET name = $1, amount = $2, due_day = $3, assigned_paycheck = $4, category = $5, active = $6, variable_amount = $7 WHERE id = $8 RETURNING *`, [ name.toString().trim(), Number(amount) || 0, parseInt(due_day, 10), parseInt(assigned_paycheck, 10), category || 'General', active !== undefined ? active : true, Boolean(variable_amount), req.params.id, ] ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Bill not found' }); } res.json(result.rows[0]); } catch (err) { console.error('PUT /api/bills/:id error:', err); res.status(500).json({ error: 'Failed to update bill' }); } }); // DELETE /api/bills/:id — hard delete router.delete('/bills/:id', async (req, res) => { try { const result = await pool.query( 'DELETE FROM bills WHERE id = $1 RETURNING id', [req.params.id] ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Bill not found' }); } res.json({ deleted: true, id: result.rows[0].id }); } catch (err) { console.error('DELETE /api/bills/:id error:', err); res.status(500).json({ error: 'Failed to delete bill' }); } }); // PATCH /api/bills/:id/toggle — toggle active field router.patch('/bills/:id/toggle', async (req, res) => { try { const result = await pool.query( 'UPDATE bills SET active = NOT active WHERE id = $1 RETURNING *', [req.params.id] ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Bill not found' }); } res.json(result.rows[0]); } catch (err) { console.error('PATCH /api/bills/:id/toggle error:', err); res.status(500).json({ error: 'Failed to toggle bill' }); } }); module.exports = router; module.exports.validateBillFields = validateBillFields;