diff --git a/routes/calendar.cts b/routes/calendar.cts index 5cab533..5795d1a 100644 --- a/routes/calendar.cts +++ b/routes/calendar.cts @@ -1,7 +1,7 @@ // @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). import type { Req, Res, Next } from '../types/http'; const express = require('express'); -const { standardizeError } = require('../middleware/errorFormatter.cts'); +const { ValidationError } = require('../utils/apiError.cts'); const router = express.Router(); const { getDb } = require('../db/database.cts'); const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts'); @@ -104,22 +104,10 @@ router.get('/', (req: Req, res: Res) => { const month = parseInt(req.query.month || now.getMonth() + 1, 10); if (isNaN(year) || year < 2000 || year > 2100) { - return res - .status(400) - .json( - standardizeError( - 'year must be a 4-digit integer between 2000 and 2100', - 'VALIDATION_ERROR', - 'year', - ), - ); + throw ValidationError('year must be a 4-digit integer between 2000 and 2100', 'year'); } if (isNaN(month) || month < 1 || month > 12) { - return res - .status(400) - .json( - standardizeError('month must be an integer between 1 and 12', 'VALIDATION_ERROR', 'month'), - ); + throw ValidationError('month must be an integer between 1 and 12', 'month'); } const today = localDateString(now); diff --git a/routes/payments.cts b/routes/payments.cts index df45285..a40069f 100644 --- a/routes/payments.cts +++ b/routes/payments.cts @@ -1,7 +1,6 @@ // @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services). import type { Req, Res, Next } from '../types/http'; const express = require('express'); -const { standardizeError } = require('../middleware/errorFormatter.cts'); const { ApiError, ValidationError, @@ -269,12 +268,14 @@ router.post('/', (req: Req, res: Res) => { ) .get(bill.id, payment.paid_date, payment.amount); if (existingDuplicate) { + // Deliberate hand-rolled body (documented exception): carries the + // `existing` payment the client's "add anyway?" flow renders — + // formatError has no extras channel. Shape matches the standard envelope. return res.status(409).json({ - ...standardizeError( - 'A matching payment already exists for this bill, date, and amount', - 'DUPLICATE_SUSPECTED', - 'amount', - ), + error: 'DUPLICATE_SUSPECTED', + message: 'A matching payment already exists for this bill, date, and amount', + code: 'DUPLICATE_SUSPECTED', + field: 'amount', existing: serializePayment(existingDuplicate), }); } diff --git a/routes/user.cts b/routes/user.cts index a346160..04f5aac 100644 --- a/routes/user.cts +++ b/routes/user.cts @@ -8,7 +8,16 @@ const { getDb } = require('../db/database.cts'); const { seedDemoData } = require('../scripts/seedDemoData.cts'); const { demoDataLimiter } = require('../middleware/rateLimiter.cts'); const { eraseUserData, clearSeededDemoData } = require('../services/userDataService.cts'); -const { standardizeError } = require('../middleware/errorFormatter.cts'); +const { ApiError, ValidationError } = require('../utils/apiError.cts'); + +// 4xx service errors keep their message/code on the wire; anything else is +// rethrown raw so the terminal handler masks + logs it as a 5xx. +function toUserOpError(err, fallbackCode) { + if (err.status && err.status < 500) { + return new ApiError(err.code || fallbackCode, err.message, err.status); + } + return err; +} // GET /api/user/seeded-status — whether the current user has demo data, with counts router.get('/seeded-status', (req: Req, res: Res) => { @@ -39,14 +48,7 @@ router.get('/seeded-status', (req: Req, res: Res) => { seededTransactions, }); } catch (err) { - res - .status(err.status || 500) - .json( - standardizeError( - err.message || 'Seeded status check failed', - err.code || 'SEEDED_STATUS_ERROR', - ), - ); + throw toUserOpError(err, 'SEEDED_STATUS_ERROR'); } }); @@ -66,14 +68,7 @@ router.post('/clear-demo-data', demoDataLimiter, (req: Req, res: Res) => { counts: result.counts, }); } catch (err) { - res - .status(err.status || 500) - .json( - standardizeError( - err.message || 'Clear demo data operation failed', - err.code || 'CLEAR_DEMO_ERROR', - ), - ); + throw toUserOpError(err, 'CLEAR_DEMO_ERROR'); } }); @@ -102,9 +97,7 @@ router.post('/seed-demo-data', demoDataLimiter, (req: Req, res: Res) => { counts: result.counts, }); } catch (err) { - res - .status(err.status || 500) - .json(standardizeError(err.message || 'Seed operation failed', err.code || 'SEED_ERROR')); + throw toUserOpError(err, 'SEED_ERROR'); } }); @@ -116,17 +109,13 @@ router.post('/erase-data', demoDataLimiter, (req: Req, res: Res) => { .trim() .toUpperCase(); if (confirm !== 'ERASE') { - return res - .status(400) - .json(standardizeError('Type ERASE to confirm.', 'ERASE_CONFIRM_REQUIRED', 'confirm')); + throw ValidationError('Type ERASE to confirm.', 'confirm', 'ERASE_CONFIRM_REQUIRED'); } try { const result = eraseUserData(getDb(), req.user.id); res.json({ success: true, ...result }); } catch (err) { - res - .status(err.status || 500) - .json(standardizeError(err.message || 'Failed to erase data', err.code || 'ERASE_ERROR')); + throw toUserOpError(err, 'ERASE_ERROR'); } });