- 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
195 lines
6.0 KiB
JavaScript
195 lines
6.0 KiB
JavaScript
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;
|