refactor(types): type-check batch 2 — 7 medium routes + 2 real fixes

Dropped @ts-nocheck from admin/payments/monthly-starting-amounts/
subscriptions/authOidc/calendar/export (~113 errors). Mostly mechanical
(catch bindings, params, never[] arrays), but the checker earned its keep:

- types/http.d.ts: added the 4-arg res.download(path, name, options, cb)
  overload — the v0.42.0 root-option download fix (admin backup + user-db
  export) didn't type-check against the old 3-arg-only signature.
- routes/calendar.cts: deleted dead clampDay() (zero callers; was also a
  standing lint warning).

@ts-nocheck server count: 21 -> 14. Server suite 252/252.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-11 06:57:41 -05:00
parent 345a159288
commit 6c915a62c8
8 changed files with 76 additions and 79 deletions

View File

@ -1,4 +1,3 @@
// @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { log } = require('../utils/logger.cts'); const { log } = require('../utils/logger.cts');
@ -44,7 +43,7 @@ const { backupOperationLimiter } = require('../middleware/rateLimiter.cts');
// All routes mounted at /api/admin (requireAuth + requireAdmin applied at server level) // All routes mounted at /api/admin (requireAuth + requireAdmin applied at server level)
// Returns a validated positive integer from req.params.id, or null. // Returns a validated positive integer from req.params.id, or null.
function parseUserId(params) { function parseUserId(params: any) {
const n = parseInt(params.id, 10); const n = parseInt(params.id, 10);
return Number.isInteger(n) && n > 0 ? n : null; return Number.isInteger(n) && n > 0 ? n : null;
} }
@ -52,7 +51,7 @@ function parseUserId(params) {
// Backup-ops error path. Kept res-based (not throw-pattern) deliberately: the // Backup-ops error path. Kept res-based (not throw-pattern) deliberately: the
// download route can fail mid-stream after headers are sent, where a throw // download route can fail mid-stream after headers are sent, where a throw
// can't produce a response. Masks 500s; 4xx service messages pass through. // can't produce a response. Masks 500s; 4xx service messages pass through.
function sendError(res, err) { function sendError(res: Res, err: any) {
const status = err.status || 500; const status = err.status || 500;
res.status(status).json({ error: status === 500 ? 'Backup operation failed' : err.message }); res.status(status).json({ error: status === 500 ? 'Backup operation failed' : err.message });
} }
@ -79,7 +78,7 @@ router.post('/backups', backupOperationLimiter, async (req: Req, res: Res) => {
try { try {
const backup = await createBackup(); const backup = await createBackup();
res.status(201).json(backup); res.status(201).json(backup);
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -88,7 +87,7 @@ router.post('/backups', backupOperationLimiter, async (req: Req, res: Res) => {
router.get('/backups', backupOperationLimiter, (req: Req, res: Res) => { router.get('/backups', backupOperationLimiter, (req: Req, res: Res) => {
try { try {
res.json({ backups: listBackups() }); res.json({ backups: listBackups() });
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -97,7 +96,7 @@ router.get('/backups', backupOperationLimiter, (req: Req, res: Res) => {
router.get('/backups/settings', backupOperationLimiter, (req: Req, res: Res) => { router.get('/backups/settings', backupOperationLimiter, (req: Req, res: Res) => {
try { try {
res.json(getScheduleStatus()); res.json(getScheduleStatus());
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -106,7 +105,7 @@ router.get('/backups/settings', backupOperationLimiter, (req: Req, res: Res) =>
router.put('/backups/settings', backupOperationLimiter, (req: Req, res: Res) => { router.put('/backups/settings', backupOperationLimiter, (req: Req, res: Res) => {
try { try {
res.json(saveBackupScheduleSettings(req.body)); res.json(saveBackupScheduleSettings(req.body));
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -115,7 +114,7 @@ router.put('/backups/settings', backupOperationLimiter, (req: Req, res: Res) =>
router.post('/backups/run-scheduled-now', backupOperationLimiter, async (req: Req, res: Res) => { router.post('/backups/run-scheduled-now', backupOperationLimiter, async (req: Req, res: Res) => {
try { try {
res.status(201).json(await runScheduledBackupNow()); res.status(201).json(await runScheduledBackupNow());
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -137,7 +136,7 @@ router.post(
expectedChecksum: expectedChecksum ? String(expectedChecksum).trim() : undefined, expectedChecksum: expectedChecksum ? String(expectedChecksum).trim() : undefined,
}); });
res.status(201).json(backup); res.status(201).json(backup);
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}, },
@ -159,7 +158,7 @@ router.get('/backups/:id/download', (req: Req, res: Res) => {
if (err && !res.headersSent) sendError(res, err); if (err && !res.headersSent) sendError(res, err);
}, },
); );
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -168,7 +167,7 @@ router.get('/backups/:id/download', (req: Req, res: Res) => {
router.post('/backups/:id/restore', backupOperationLimiter, async (req: Req, res: Res) => { router.post('/backups/:id/restore', backupOperationLimiter, async (req: Req, res: Res) => {
try { try {
res.json(await restoreBackup(req.params.id)); res.json(await restoreBackup(req.params.id));
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -177,7 +176,7 @@ router.post('/backups/:id/restore', backupOperationLimiter, async (req: Req, res
router.delete('/backups/:id', backupOperationLimiter, (req: Req, res: Res) => { router.delete('/backups/:id', backupOperationLimiter, (req: Req, res: Res) => {
try { try {
res.json(deleteBackup(req.params.id)); res.json(deleteBackup(req.params.id));
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -428,7 +427,7 @@ router.delete('/users/:id', (req: Req, res: Res) => {
router.get('/cleanup', (req: Req, res: Res) => { router.get('/cleanup', (req: Req, res: Res) => {
try { try {
res.json(getCleanupStatus()); res.json(getCleanupStatus());
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -444,7 +443,7 @@ router.get('/cleanup', (req: Req, res: Res) => {
router.put('/cleanup', (req: Req, res: Res) => { router.put('/cleanup', (req: Req, res: Res) => {
try { try {
res.json(applyCleanupSettings(req.body)); res.json(applyCleanupSettings(req.body));
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -455,7 +454,7 @@ router.post('/cleanup/run', backupOperationLimiter, async (req: Req, res: Res) =
try { try {
const result = await runAllCleanup(); const result = await runAllCleanup();
res.json(result); res.json(result);
} catch (err) { } catch (err: any) {
sendError(res, err); sendError(res, err);
} }
}); });
@ -489,7 +488,7 @@ router.post('/auth-mode/oidc-test', async (req: Req, res: Res) => {
router.put('/auth-mode', (req: Req, res: Res) => { router.put('/auth-mode', (req: Req, res: Res) => {
try { try {
res.json(applyAuthModeSettings(req.body || {})); res.json(applyAuthModeSettings(req.body || {}));
} catch (err) { } catch (err: any) {
res res
.status(err.status || 500) .status(err.status || 500)
.json({ error: err.status ? err.message : 'Failed to update authentication settings' }); .json({ error: err.status ? err.message : 'Failed to update authentication settings' });
@ -538,7 +537,7 @@ router.post('/migrations/rollback', async (req: Req, res: Res) => {
user_agent: req.get('user-agent'), user_agent: req.get('user-agent'),
}); });
res.json({ success: true, ...result }); res.json({ success: true, ...result });
} catch (err) { } catch (err: any) {
logAudit({ logAudit({
user_id: req.user.id, user_id: req.user.id,
action: 'migration.rollback.failure', action: 'migration.rollback.failure',

View File

@ -1,4 +1,3 @@
// @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'; import type { Req, Res, Next } from '../types/http';
('use strict'); ('use strict');
@ -47,11 +46,11 @@ router.get('/login', async (req: Req, res: Res) => {
const state = createLoginState(redirectTo); const state = createLoginState(redirectTo);
const authUrl = await buildAuthorizationUrl(config, state); const authUrl = await buildAuthorizationUrl(config, state);
res.redirect(authUrl); res.redirect(authUrl);
} catch (err) { } catch (err: any) {
const msg = err.message || String(err); const msg = err.message || String(err);
log.error('[oidc] Login initiation error:', msg || '(no message)'); log.error('[oidc] Login initiation error:', msg || '(no message)');
if (err.errors) { if (err.errors) {
err.errors.forEach((e, i) => log.error(` [${i}]`, e.message || String(e))); err.errors.forEach((e: any, i: number) => log.error(` [${i}]`, e.message || String(e)));
} }
if (err.cause) log.error(' cause:', err.cause.message || String(err.cause)); if (err.cause) log.error(' cause:', err.cause.message || String(err.cause));
res.status(502).json({ error: 'Failed to reach the identity provider. Please try again.' }); res.status(502).json({ error: 'Failed to reach the identity provider. Please try again.' });
@ -111,12 +110,12 @@ router.get('/callback', async (req: Req, res: Res) => {
recordLogin(user.id, req.ip, req.get('user-agent'), session.sessionId); recordLogin(user.id, req.ip, req.get('user-agent'), session.sessionId);
res.cookie(COOKIE_NAME, session.sessionId, cookieOpts(req)); res.cookie(COOKIE_NAME, session.sessionId, cookieOpts(req));
res.redirect(savedState.redirect_to || '/'); res.redirect(savedState.redirect_to || '/');
} catch (err) { } catch (err: any) {
// Log message only — never log tokens, codes, or ID token contents // Log message only — never log tokens, codes, or ID token contents
const msg = err.message || String(err); const msg = err.message || String(err);
log.error('[oidc] Callback error:', msg || '(no message)'); log.error('[oidc] Callback error:', msg || '(no message)');
if (err.errors) { if (err.errors) {
err.errors.forEach((e, i) => log.error(` [${i}]`, e.message || String(e))); err.errors.forEach((e: any, i: number) => log.error(` [${i}]`, e.message || String(e)));
} }
if (err.cause) log.error(' cause:', err.cause.message || String(err.cause)); if (err.cause) log.error(' cause:', err.cause.message || String(err.cause));
const errCode = err.status === 403 ? 'access_denied' : 'authentication_failed'; const errCode = err.status === 403 ? 'access_denied' : 'authentication_failed';

View File

@ -1,4 +1,3 @@
// @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { ValidationError } = require('../utils/apiError.cts'); const { ValidationError } = require('../utils/apiError.cts');
@ -18,21 +17,16 @@ const {
const { localDateString } = require('../utils/dates.mts'); const { localDateString } = require('../utils/dates.mts');
const { roundMoney, sumMoney, fromCents } = require('../utils/money.mts'); const { roundMoney, sumMoney, fromCents } = require('../utils/money.mts');
function clampDay(year, month, day) { function toDateString(year: number, month: number, day: number) {
const daysInMonth = new Date(year, month, 0).getDate();
return Math.min(Math.max(parseInt(day || 1, 10), 1), daysInMonth);
}
function toDateString(year, month, day) {
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
} }
function emptyDay(year, month, day) { function emptyDay(year: number, month: number, day: number) {
return { return {
date: toDateString(year, month, day), date: toDateString(year, month, day),
day, day,
bills_due: [], bills_due: [] as any[],
payments: [], payments: [] as any[],
status_summary: { status_summary: {
due_count: 0, due_count: 0,
paid_count: 0, paid_count: 0,
@ -44,7 +38,7 @@ function emptyDay(year, month, day) {
}; };
} }
function tokenPayload(req, tokenRow) { function tokenPayload(req: Req, tokenRow: any) {
if (!tokenRow) { if (!tokenRow) {
return { return {
active: false, active: false,
@ -189,7 +183,7 @@ router.get('/', (req: Req, res: Res) => {
} }
const calendarBills = bills const calendarBills = bills
.map((bill) => { .map((bill: any) => {
const billRange = getCycleRange(year, month, bill); const billRange = getCycleRange(year, month, bill);
if (!billRange) return null; if (!billRange) return null;
@ -237,9 +231,9 @@ router.get('/', (req: Req, res: Res) => {
if (!bill.is_skipped) day.status_summary.total_due += bill.effective_amount || 0; if (!bill.is_skipped) day.status_summary.total_due += bill.effective_amount || 0;
} }
const activeBills = calendarBills.filter((bill) => !bill.is_skipped); const activeBills = calendarBills.filter((bill: any) => !bill.is_skipped);
const expectedTotal = sumMoney(activeBills, (bill) => bill.effective_amount || 0); const expectedTotal = sumMoney(activeBills, (bill: any) => bill.effective_amount || 0);
const paidTotal = sumMoney(activeBills, (bill) => bill.paid_amount || 0); const paidTotal = sumMoney(activeBills, (bill: any) => bill.paid_amount || 0);
const remainingTotal = Math.max(0, roundMoney(expectedTotal - paidTotal)); const remainingTotal = Math.max(0, roundMoney(expectedTotal - paidTotal));
const paidPercent = const paidPercent =
expectedTotal > 0 ? Math.min(100, Math.round((paidTotal / expectedTotal) * 100)) : 0; expectedTotal > 0 ? Math.min(100, Math.round((paidTotal / expectedTotal) * 100)) : 0;
@ -261,10 +255,11 @@ router.get('/', (req: Req, res: Res) => {
remaining_total: remainingTotal, remaining_total: remainingTotal,
paid_percent: paidPercent, paid_percent: paidPercent,
bill_count: activeBills.length, bill_count: activeBills.length,
paid_count: activeBills.filter((bill) => bill.is_paid).length, paid_count: activeBills.filter((bill: any) => bill.is_paid).length,
skipped_count: calendarBills.filter((bill) => bill.is_skipped).length, skipped_count: calendarBills.filter((bill: any) => bill.is_skipped).length,
missed_count: activeBills.filter((bill) => bill.status === 'late' || bill.status === 'missed') missed_count: activeBills.filter(
.length, (bill: any) => bill.status === 'late' || bill.status === 'missed',
).length,
}, },
}); });
}); });

View File

@ -1,4 +1,3 @@
// @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { ValidationError } = require('../utils/apiError.cts'); const { ValidationError } = require('../utils/apiError.cts');
@ -16,7 +15,7 @@ router.get('/', (req: Req, res: Res) => {
const db = getDb(); const db = getDb();
const format = (req.query.format || 'csv').toLowerCase(); const format = (req.query.format || 'csv').toLowerCase();
const { from, to } = req.query; const { from, to } = req.query;
const isDate = (s) => typeof s === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(s); const isDate = (s: any) => typeof s === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(s);
// Filter by an explicit date range (both bounds required) or by a single year. // Filter by an explicit date range (both bounds required) or by a single year.
let where, whereParams, label; let where, whereParams, label;
@ -71,14 +70,14 @@ router.get('/', (req: Req, res: Res) => {
if (format === 'csv') { if (format === 'csv') {
const header = 'Date,Bill,Category,Expected,Paid,Method,Notes,Actual Amount,Monthly Notes\n'; const header = 'Date,Bill,Category,Expected,Paid,Method,Notes,Actual Amount,Monthly Notes\n';
const escCsv = (v) => { const escCsv = (v: any) => {
if (v == null) return ''; if (v == null) return '';
const s = String(v); const s = String(v);
return /[,"\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; return /[,"\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}; };
const body = rows const body = rows
.map((r) => { .map((r: any) => {
const paidMonth = parseInt(r.paid_date.slice(5, 7), 10); const paidMonth = parseInt(r.paid_date.slice(5, 7), 10);
const paidYear = parseInt(r.paid_date.slice(0, 4), 10); const paidYear = parseInt(r.paid_date.slice(0, 4), 10);
const mbs = mbsStmt.get(r.bill_id, paidYear, paidMonth); const mbs = mbsStmt.get(r.bill_id, paidYear, paidMonth);
@ -102,7 +101,7 @@ router.get('/', (req: Req, res: Res) => {
} }
// Fallback: JSON — enrich each row with monthly_bill_state overrides // Fallback: JSON — enrich each row with monthly_bill_state overrides
const enriched = rows.map((r) => { const enriched = rows.map((r: any) => {
const paidMonth = parseInt(r.paid_date.slice(5, 7), 10); const paidMonth = parseInt(r.paid_date.slice(5, 7), 10);
const paidYear = parseInt(r.paid_date.slice(0, 4), 10); const paidYear = parseInt(r.paid_date.slice(0, 4), 10);
const mbs = mbsStmt.get(r.bill_id, paidYear, paidMonth); const mbs = mbsStmt.get(r.bill_id, paidYear, paidMonth);
@ -117,7 +116,7 @@ router.get('/', (req: Req, res: Res) => {
res.json({ range: label, count: enriched.length, payments: enriched }); res.json({ range: label, count: enriched.length, payments: enriched });
}); });
function getUserExportData(userId) { function getUserExportData(userId: any) {
const db = getDb(); const db = getDb();
const categories = db const categories = db
.prepare( .prepare(
@ -136,7 +135,7 @@ function getUserExportData(userId) {
`, `,
) )
.all(userId) .all(userId)
.map((b) => ({ ...b, expected_amount: fromCents(b.expected_amount) })); .map((b: any) => ({ ...b, expected_amount: fromCents(b.expected_amount) }));
const payments = db const payments = db
.prepare( .prepare(
` `
@ -150,7 +149,7 @@ function getUserExportData(userId) {
`, `,
) )
.all(userId) .all(userId)
.map((p) => ({ ...p, amount: fromCents(p.amount) })); .map((p: any) => ({ ...p, amount: fromCents(p.amount) }));
const monthlyState = db const monthlyState = db
.prepare( .prepare(
` `
@ -162,7 +161,7 @@ function getUserExportData(userId) {
`, `,
) )
.all(userId) .all(userId)
.map((m) => ({ ...m, actual_amount: fromCents(m.actual_amount) })); .map((m: any) => ({ ...m, actual_amount: fromCents(m.actual_amount) }));
const monthlyStartingAmounts = db const monthlyStartingAmounts = db
.prepare( .prepare(
` `
@ -173,7 +172,7 @@ function getUserExportData(userId) {
`, `,
) )
.all(userId) .all(userId)
.map((r) => ({ .map((r: any) => ({
...r, ...r,
first_amount: fromCents(r.first_amount), first_amount: fromCents(r.first_amount),
fifteenth_amount: fromCents(r.fifteenth_amount), fifteenth_amount: fromCents(r.fifteenth_amount),
@ -190,13 +189,15 @@ function getUserExportData(userId) {
) )
.all(userId); .all(userId);
const notes = [ const notes = [
...bills.filter((b) => b.notes).map((b) => ({ type: 'bill', bill_id: b.id, notes: b.notes })), ...bills
.filter((b: any) => b.notes)
.map((b: any) => ({ type: 'bill', bill_id: b.id, notes: b.notes })),
...payments ...payments
.filter((p) => p.notes) .filter((p: any) => p.notes)
.map((p) => ({ type: 'payment', payment_id: p.id, bill_id: p.bill_id, notes: p.notes })), .map((p: any) => ({ type: 'payment', payment_id: p.id, bill_id: p.bill_id, notes: p.notes })),
...monthlyState ...monthlyState
.filter((m) => m.notes) .filter((m: any) => m.notes)
.map((m) => ({ .map((m: any) => ({
type: 'monthly_state', type: 'monthly_state',
monthly_state_id: m.id, monthly_state_id: m.id,
bill_id: m.bill_id, bill_id: m.bill_id,
@ -274,7 +275,7 @@ router.get('/user-excel', (req: Req, res: Res) => {
// Build the SQLite user-DB export to a temp file and return its path. Extracted // Build the SQLite user-DB export to a temp file and return its path. Extracted
// so the export→import round-trip can be regression-tested (exportImportRoundTrip). // so the export→import round-trip can be regression-tested (exportImportRoundTrip).
function buildUserDbExportFile(userId) { function buildUserDbExportFile(userId: any) {
const data = getUserExportData(userId); const data = getUserExportData(userId);
const file = path.join(os.tmpdir(), `bill-tracker-user-${userId}-${Date.now()}.sqlite`); const file = path.join(os.tmpdir(), `bill-tracker-user-${userId}-${Date.now()}.sqlite`);
const out = new Database(file); const out = new Database(file);
@ -291,14 +292,14 @@ function buildUserDbExportFile(userId) {
`); `);
const meta = out.prepare('INSERT INTO export_metadata (key, value) VALUES (?, ?)'); const meta = out.prepare('INSERT INTO export_metadata (key, value) VALUES (?, ?)');
meta.run('metadata_json', JSON.stringify(data.metadata)); meta.run('metadata_json', JSON.stringify(data.metadata));
const insertRows = (table, rows) => { const insertRows = (table: any, rows: any) => {
if (!rows.length) return; if (!rows.length) return;
const cols = Object.keys(rows[0]); const cols = Object.keys(rows[0]);
const stmt = out.prepare( const stmt = out.prepare(
`INSERT INTO ${table} (${cols.join(',')}) VALUES (${cols.map(() => '?').join(',')})`, `INSERT INTO ${table} (${cols.join(',')}) VALUES (${cols.map(() => '?').join(',')})`,
); );
const tx = out.transaction((items) => const tx = out.transaction((items: any) =>
items.forEach((row) => stmt.run(cols.map((c) => row[c]))), items.forEach((row: any) => stmt.run(cols.map((c: any) => row[c]))),
); );
tx(rows); tx(rows);
}; };

View File

@ -1,4 +1,3 @@
// @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
@ -8,7 +7,7 @@ const { accountingActiveSql } = require('../services/paymentAccountingService.ct
const { toCents, fromCents } = require('../utils/money.mts'); const { toCents, fromCents } = require('../utils/money.mts');
const { ValidationError } = require('../utils/apiError.cts'); const { ValidationError } = require('../utils/apiError.cts');
function parseYearMonth(source) { function parseYearMonth(source: any) {
const now = new Date(); const now = new Date();
const year = parseInt(source.year || now.getFullYear(), 10); const year = parseInt(source.year || now.getFullYear(), 10);
const month = parseInt(source.month || now.getMonth() + 1, 10); const month = parseInt(source.month || now.getMonth() + 1, 10);
@ -23,12 +22,12 @@ function parseYearMonth(source) {
return { year, month }; return { year, month };
} }
function money(value) { function money(value: any) {
const n = Number(value); const n = Number(value);
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0; return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
} }
function getStartingAmounts(db, userId, year, month) { function getStartingAmounts(db: any, userId: any, year: any, month: any) {
const row = db const row = db
.prepare( .prepare(
` `
@ -46,7 +45,7 @@ function getStartingAmounts(db, userId, year, month) {
}; };
} }
function calculatePaidDeductions(db, userId, year, month) { function calculatePaidDeductions(db: any, userId: any, year: any, month: any) {
const { start, end } = getCycleRange(year, month); const { start, end } = getCycleRange(year, month);
// Paid from first bucket: bills with due_day 1-14 // Paid from first bucket: bills with due_day 1-14
@ -119,7 +118,7 @@ function calculatePaidDeductions(db, userId, year, month) {
}; };
} }
function buildStartingAmountsResponse(db, userId, year, month) { function buildStartingAmountsResponse(db: any, userId: any, year: any, month: any) {
const amounts = getStartingAmounts(db, userId, year, month); const amounts = getStartingAmounts(db, userId, year, month);
const paid = calculatePaidDeductions(db, userId, year, month); const paid = calculatePaidDeductions(db, userId, year, month);

View File

@ -1,4 +1,3 @@
// @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { ValidationError, NotFoundError, ConflictError } = require('../utils/apiError.cts'); const { ValidationError, NotFoundError, ConflictError } = require('../utils/apiError.cts');
@ -22,7 +21,7 @@ const { fromCents } = require('../utils/money.mts');
const SQL_NOT_DELETED = 'deleted_at IS NULL'; const SQL_NOT_DELETED = 'deleted_at IS NULL';
const TRANSACTION_MATCH_SOURCE = 'transaction_match'; const TRANSACTION_MATCH_SOURCE = 'transaction_match';
function isTransactionLinkedPayment(payment) { function isTransactionLinkedPayment(payment: any) {
return payment?.payment_source === TRANSACTION_MATCH_SOURCE || payment?.transaction_id != null; return payment?.payment_source === TRANSACTION_MATCH_SOURCE || payment?.transaction_id != null;
} }
@ -34,7 +33,7 @@ function rejectTransactionLinkedPayment() {
); );
} }
function parseYearMonth(body) { function parseYearMonth(body: any) {
const year = parseInt(body.year, 10); const year = parseInt(body.year, 10);
const month = parseInt(body.month, 10); const month = parseInt(body.month, 10);
if (!Number.isInteger(year) || year < 2000 || year > 2100) { if (!Number.isInteger(year) || year < 2000 || year > 2100) {
@ -46,7 +45,7 @@ function parseYearMonth(body) {
return { year, month }; return { year, month };
} }
function getAutopaySuggestionContext(db, userId, billId, year, month) { function getAutopaySuggestionContext(db: any, userId: any, billId: any, year: any, month: any) {
const bill = db const bill = db
.prepare( .prepare(
` `
@ -543,9 +542,9 @@ router.post('/bulk', (req: Req, res: Res) => {
AND p.${SQL_NOT_DELETED}`, AND p.${SQL_NOT_DELETED}`,
); );
const created = []; const created: any[] = [];
const skipped = []; const skipped: any[] = [];
const errors = []; const errors: any[] = [];
const runBulk = db.transaction(() => { const runBulk = db.transaction(() => {
for (const item of payments) { for (const item of payments) {

View File

@ -1,4 +1,3 @@
// @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'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
@ -69,8 +68,8 @@ router.post('/recommendations/match-bill', (req: Req, res: Res) => {
const billId = parseInt(req.body?.bill_id, 10); const billId = parseInt(req.body?.bill_id, 10);
const rawIds = Array.isArray(req.body?.transaction_ids) ? req.body.transaction_ids : []; const rawIds = Array.isArray(req.body?.transaction_ids) ? req.body.transaction_ids : [];
const txIds = rawIds const txIds = rawIds
.map((id) => parseInt(id, 10)) .map((id: any) => parseInt(id, 10))
.filter((n) => Number.isInteger(n) && n > 0) .filter((n: any) => Number.isInteger(n) && n > 0)
.slice(0, 50); .slice(0, 50);
if (!Number.isInteger(billId) || billId < 1) { if (!Number.isInteger(billId) || billId < 1) {
@ -178,7 +177,7 @@ router.post('/recommendations/create', (req: Req, res: Res) => {
metadata: { transaction_ids: req.body?.transaction_ids || [] }, metadata: { transaction_ids: req.body?.transaction_ids || [] },
}); });
res.status(201).json(created); res.status(201).json(created);
} catch (err) { } catch (err: any) {
// Service validation errors are user-facing at 4xx (parity with the old // Service validation errors are user-facing at 4xx (parity with the old
// behavior: no-status service throws surfaced at 400, never as masked 5xx). // behavior: no-status service throws surfaced at 400, never as masked 5xx).
throw new ApiError( throw new ApiError(
@ -222,7 +221,7 @@ router.get('/catalog', (req: Req, res: Res) => {
) )
.all(req.user.id); .all(req.user.id);
const billByCatalogId = new Map(matchedBills.map((b) => [b.catalog_id, b])); const billByCatalogId = new Map<any, any>(matchedBills.map((b: any) => [b.catalog_id, b]));
// User's custom descriptors // User's custom descriptors
let userDescriptors = []; let userDescriptors = [];
@ -240,7 +239,7 @@ router.get('/catalog', (req: Req, res: Res) => {
userDescsByCatalogId.get(d.catalog_id).push({ id: d.id, descriptor: d.descriptor }); userDescsByCatalogId.get(d.catalog_id).push({ id: d.id, descriptor: d.descriptor });
} }
const catalog = catalogEntries.map((entry) => { const catalog = catalogEntries.map((entry: any) => {
const bill = billByCatalogId.get(entry.id) ?? null; const bill = billByCatalogId.get(entry.id) ?? null;
return { return {
...entry, ...entry,

6
types/http.d.ts vendored
View File

@ -39,6 +39,12 @@ export interface Res {
type(t: string): Res; type(t: string): Res;
redirect(url: string): void; redirect(url: string): void;
download(path: string, filename?: string, cb?: (err?: any) => void): void; download(path: string, filename?: string, cb?: (err?: any) => void): void;
download(
path: string,
filename: string,
options: Record<string, any>,
cb?: (err?: any) => void,
): void;
sendFile(path: string, options?: any, cb?: (err?: any) => void): void; sendFile(path: string, options?: any, cb?: (err?: any) => void): void;
attachment(filename?: string): Res; attachment(filename?: string): Res;
locals: any; locals: any;