Files
budget-app/server/src/routes/bills.js
Christian Hood 11476086cd Docs: backfill JSDoc, utility docs, and CLAUDE.md API/schema sections
- Add JSDoc to paychecks.js helpers: buildVirtualPaychecks, generatePaychecks, fetchPaychecksForMonth
- Add JSDoc to financing.js helpers: remainingPeriods, calcPaymentAmount, enrichPlan
- Add JSDoc to validateBillFields (bills.js) and getAllConfig (config.js)
- Add JSDoc to ThemeProvider and useTheme in ThemeContext.jsx
- Add Database Schema reference table to CLAUDE.md
- Add complete API Endpoints reference section to CLAUDE.md covering all routes

Nightshift-Task: docs-backfill
Nightshift-Ref: https://github.com/marcus/nightshift
2026-03-20 02:54:45 -04:00

195 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (131).
* @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;