Standards-unification batch 2. All inline 4xx bodies and try/catch-500
wrappers converted to thrown ApiError factories; validation helpers
(parseYearMonth, getAutopaySuggestionContext, rejectTransactionLinkedPayment)
now throw. Client-visible codes preserved (TRANSACTION_PAYMENT_LOCKED,
NOT_AUTO_MATCH, NO_TRANSACTION, RECLASSIFY_ONLY_SYNC, DUPLICATE_ERROR,
NO_DEBTS). Kills five more err.message-on-500 leaks in subscriptions.cts
(search/decline/catalog/descriptor wrappers).
Two deliberate exceptions, documented inline:
- payments POST / keeps the hand-rolled DUPLICATE_SUSPECTED 409 (carries the
'existing' payment payload the client's add-anyway flow reads)
- subscriptions /recommendations/create keeps 400-parity pass-through of
service validation messages via an ApiError wrap
Test harnesses (paymentsRoute, transactionMatchService) mirror the terminal
handler for thrown errors. Server suite 252/252.
Co-Authored-By: Claude Fable 5 <[email protected]>
Every routes/*.js → .cts, with handler signatures typed against the shared
http Req/Res/Next types. Routes carry @ts-nocheck for now — they're thin glue
over the (fully typed) services, so full route-level type-checking is deferred;
the handler-param typing is a head-start. server.js + test requires updated to
the explicit .cts paths.
Behavior-preserving. Verified: check:server 0, typecheck:server 0, suite 226/226,
real boot → /api/health + /api/about 200 (all routes mount).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The deliberate manual POST /payments had no dedupe guard (double-submit
made a second payment), but silently deduping it like the auto paths
would lose a legitimately-identical same-day payment — a money bug. So
the server returns 409 DUPLICATE_SUSPECTED (existing payment attached),
and a shared client helper (client/lib/paymentActions.ts, used by the
tracker inline cell, partial-payment dialog, and bill modal) turns it
into an 'add anyway?' toast that resends with allow_duplicate. Automated
one-click paths keep deduping silently (a repeat there is a retry).
Dropped a now-unused api import in PaymentLedgerDialog (Track E sweep).
Server suite 188 green; typecheck/lint/build clean; probe 17/17.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Quick/bulk-pay were already atomic (X1); the audit found the row write +
applyBalanceDelta split across two un-transactional statements on manual
POST /payments, autopay-confirm, DELETE (unpay), restore, and PUT (edit,
where the bill-balance write and the payment UPDATE weren't atomic). A
failure between the two could leave a payment without its balance
adjustment (or a balance reversed without the row deleted). Each is now a
single db.transaction(). Behavior-preserving.
tests/paymentsRoute.test.js pins the create→delete→restore→edit balance
round-trip + double-restore no-op. Full suite 186 green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>