refactor(routes): monthly-starting-amounts → throw pattern; doc exemplar

5 ad-hoc {error} validations → throw ValidationError(msg, field) (adds
proper code + field). CODE_STANDARDS references routes/categories.cts as
the canonical error-pattern example and notes converted/unconverted routes
emit identical output (terminal handler normalizes both). 236/236.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-06 15:03:52 -05:00
parent 3b016d4876
commit a75ff1a466
2 changed files with 13 additions and 12 deletions

View File

@ -38,7 +38,10 @@ rolled out. Update this doc when a standard changes.
(`ValidationError`, `NotFoundError`, `ConflictError`, …) and let the single terminal error
handler format them — do not hand-roll `res.status().json({ error })`. Express 5 forwards
rejected async handlers automatically, so route bodies should not wrap everything in
`try/catch`. Clients read `message`/`code` + status.
`try/catch`. Clients read `message`/`code` + status. **Canonical example:**
`routes/categories.cts`. (The terminal handler already normalizes any legacy
`standardizeError`/`{ error }` body to the same shape, so converted and not-yet-converted
routes emit identical responses — convert opportunistically.)
- **Success envelope (target):** reads return the bare resource/list; mutations return
`{ success: true, … }`.
- **Validation (target):** validate inputs with the shared validators (throwing

View File

@ -6,6 +6,7 @@ const { getDb } = require('../db/database.cts');
const { getCycleRange } = require('../services/statusService.cts');
const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
const { toCents, fromCents } = require('../utils/money.mts');
const { ValidationError } = require('../utils/apiError.cts');
function parseYearMonth(source) {
const now = new Date();
@ -147,7 +148,7 @@ function buildStartingAmountsResponse(db, userId, year, month) {
router.get('/', (req: Req, res: Res) => {
const parsed = parseYearMonth(req.query);
if (parsed.error) return res.status(400).json({ error: parsed.error });
if (parsed.error) throw ValidationError(parsed.error, 'month');
const db = getDb();
res.json(buildStartingAmountsResponse(db, req.user.id, parsed.year, parsed.month));
@ -155,27 +156,24 @@ router.get('/', (req: Req, res: Res) => {
router.put('/', (req: Req, res: Res) => {
const parsed = parseYearMonth(req.body || {});
if (parsed.error) return res.status(400).json({ error: parsed.error });
if (parsed.error) throw ValidationError(parsed.error, 'month');
const firstAmount = Number(req.body?.first_amount);
if (!Number.isFinite(firstAmount) || firstAmount < 0 || firstAmount > 1000000000) {
return res
.status(400)
.json({ error: 'first_amount must be a number between 0 and 1000000000' });
throw ValidationError('first_amount must be a number between 0 and 1000000000', 'first_amount');
}
const fifteenthAmount = Number(req.body?.fifteenth_amount);
if (!Number.isFinite(fifteenthAmount) || fifteenthAmount < 0 || fifteenthAmount > 1000000000) {
return res
.status(400)
.json({ error: 'fifteenth_amount must be a number between 0 and 1000000000' });
throw ValidationError(
'fifteenth_amount must be a number between 0 and 1000000000',
'fifteenth_amount',
);
}
const otherAmount = Number(req.body?.other_amount);
if (!Number.isFinite(otherAmount) || otherAmount < 0 || otherAmount > 1000000000) {
return res
.status(400)
.json({ error: 'other_amount must be a number between 0 and 1000000000' });
throw ValidationError('other_amount must be a number between 0 and 1000000000', 'other_amount');
}
const db = getDb();