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';
const express = require('express');
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)
// Returns a validated positive integer from req.params.id, or null.
function parseUserId(params) {
function parseUserId(params: any) {
const n = parseInt(params.id, 10);
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
// 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.
function sendError(res, err) {
function sendError(res: Res, err: any) {
const status = err.status || 500;
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 {
const backup = await createBackup();
res.status(201).json(backup);
} catch (err) {
} catch (err: any) {
sendError(res, err);
}
});
@ -88,7 +87,7 @@ router.post('/backups', backupOperationLimiter, async (req: Req, res: Res) => {
router.get('/backups', backupOperationLimiter, (req: Req, res: Res) => {
try {
res.json({ backups: listBackups() });
} catch (err) {
} catch (err: any) {
sendError(res, err);
}
});
@ -97,7 +96,7 @@ router.get('/backups', backupOperationLimiter, (req: Req, res: Res) => {
router.get('/backups/settings', backupOperationLimiter, (req: Req, res: Res) => {
try {
res.json(getScheduleStatus());
} catch (err) {
} catch (err: any) {
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) => {
try {
res.json(saveBackupScheduleSettings(req.body));
} catch (err) {
} catch (err: any) {
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) => {
try {
res.status(201).json(await runScheduledBackupNow());
} catch (err) {
} catch (err: any) {
sendError(res, err);
}
});
@ -137,7 +136,7 @@ router.post(
expectedChecksum: expectedChecksum ? String(expectedChecksum).trim() : undefined,
});
res.status(201).json(backup);
} catch (err) {
} catch (err: any) {
sendError(res, err);
}
},
@ -159,7 +158,7 @@ router.get('/backups/:id/download', (req: Req, res: Res) => {
if (err && !res.headersSent) sendError(res, err);
},
);
} catch (err) {
} catch (err: any) {
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) => {
try {
res.json(await restoreBackup(req.params.id));
} catch (err) {
} catch (err: any) {
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) => {
try {
res.json(deleteBackup(req.params.id));
} catch (err) {
} catch (err: any) {
sendError(res, err);
}
});
@ -428,7 +427,7 @@ router.delete('/users/:id', (req: Req, res: Res) => {
router.get('/cleanup', (req: Req, res: Res) => {
try {
res.json(getCleanupStatus());
} catch (err) {
} catch (err: any) {
sendError(res, err);
}
});
@ -444,7 +443,7 @@ router.get('/cleanup', (req: Req, res: Res) => {
router.put('/cleanup', (req: Req, res: Res) => {
try {
res.json(applyCleanupSettings(req.body));
} catch (err) {
} catch (err: any) {
sendError(res, err);
}
});
@ -455,7 +454,7 @@ router.post('/cleanup/run', backupOperationLimiter, async (req: Req, res: Res) =
try {
const result = await runAllCleanup();
res.json(result);
} catch (err) {
} catch (err: any) {
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) => {
try {
res.json(applyAuthModeSettings(req.body || {}));
} catch (err) {
} catch (err: any) {
res
.status(err.status || 500)
.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'),
});
res.json({ success: true, ...result });
} catch (err) {
} catch (err: any) {
logAudit({
user_id: req.user.id,
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';
('use strict');
@ -47,11 +46,11 @@ router.get('/login', async (req: Req, res: Res) => {
const state = createLoginState(redirectTo);
const authUrl = await buildAuthorizationUrl(config, state);
res.redirect(authUrl);
} catch (err) {
} catch (err: any) {
const msg = err.message || String(err);
log.error('[oidc] Login initiation error:', msg || '(no message)');
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));
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);
res.cookie(COOKIE_NAME, session.sessionId, cookieOpts(req));
res.redirect(savedState.redirect_to || '/');
} catch (err) {
} catch (err: any) {
// Log message only — never log tokens, codes, or ID token contents
const msg = err.message || String(err);
log.error('[oidc] Callback error:', msg || '(no message)');
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));
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';
const express = require('express');
const { ValidationError } = require('../utils/apiError.cts');
@ -18,21 +17,16 @@ const {
const { localDateString } = require('../utils/dates.mts');
const { roundMoney, sumMoney, fromCents } = require('../utils/money.mts');
function clampDay(year, month, day) {
const daysInMonth = new Date(year, month, 0).getDate();
return Math.min(Math.max(parseInt(day || 1, 10), 1), daysInMonth);
}
function toDateString(year, month, day) {
function toDateString(year: number, month: number, day: number) {
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 {
date: toDateString(year, month, day),
day,
bills_due: [],
payments: [],
bills_due: [] as any[],
payments: [] as any[],
status_summary: {
due_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) {
return {
active: false,
@ -189,7 +183,7 @@ router.get('/', (req: Req, res: Res) => {
}
const calendarBills = bills
.map((bill) => {
.map((bill: any) => {
const billRange = getCycleRange(year, month, bill);
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;
}
const activeBills = calendarBills.filter((bill) => !bill.is_skipped);
const expectedTotal = sumMoney(activeBills, (bill) => bill.effective_amount || 0);
const paidTotal = sumMoney(activeBills, (bill) => bill.paid_amount || 0);
const activeBills = calendarBills.filter((bill: any) => !bill.is_skipped);
const expectedTotal = sumMoney(activeBills, (bill: any) => bill.effective_amount || 0);
const paidTotal = sumMoney(activeBills, (bill: any) => bill.paid_amount || 0);
const remainingTotal = Math.max(0, roundMoney(expectedTotal - paidTotal));
const paidPercent =
expectedTotal > 0 ? Math.min(100, Math.round((paidTotal / expectedTotal) * 100)) : 0;
@ -261,10 +255,11 @@ router.get('/', (req: Req, res: Res) => {
remaining_total: remainingTotal,
paid_percent: paidPercent,
bill_count: activeBills.length,
paid_count: activeBills.filter((bill) => bill.is_paid).length,
skipped_count: calendarBills.filter((bill) => bill.is_skipped).length,
missed_count: activeBills.filter((bill) => bill.status === 'late' || bill.status === 'missed')
.length,
paid_count: activeBills.filter((bill: any) => bill.is_paid).length,
skipped_count: calendarBills.filter((bill: any) => bill.is_skipped).length,
missed_count: activeBills.filter(
(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';
const express = require('express');
const { ValidationError } = require('../utils/apiError.cts');
@ -16,7 +15,7 @@ router.get('/', (req: Req, res: Res) => {
const db = getDb();
const format = (req.query.format || 'csv').toLowerCase();
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.
let where, whereParams, label;
@ -71,14 +70,14 @@ router.get('/', (req: Req, res: Res) => {
if (format === 'csv') {
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 '';
const s = String(v);
return /[,"\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
};
const body = rows
.map((r) => {
.map((r: any) => {
const paidMonth = parseInt(r.paid_date.slice(5, 7), 10);
const paidYear = parseInt(r.paid_date.slice(0, 4), 10);
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
const enriched = rows.map((r) => {
const enriched = rows.map((r: any) => {
const paidMonth = parseInt(r.paid_date.slice(5, 7), 10);
const paidYear = parseInt(r.paid_date.slice(0, 4), 10);
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 });
});
function getUserExportData(userId) {
function getUserExportData(userId: any) {
const db = getDb();
const categories = db
.prepare(
@ -136,7 +135,7 @@ function getUserExportData(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
.prepare(
`
@ -150,7 +149,7 @@ function getUserExportData(userId) {
`,
)
.all(userId)
.map((p) => ({ ...p, amount: fromCents(p.amount) }));
.map((p: any) => ({ ...p, amount: fromCents(p.amount) }));
const monthlyState = db
.prepare(
`
@ -162,7 +161,7 @@ function getUserExportData(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
.prepare(
`
@ -173,7 +172,7 @@ function getUserExportData(userId) {
`,
)
.all(userId)
.map((r) => ({
.map((r: any) => ({
...r,
first_amount: fromCents(r.first_amount),
fifteenth_amount: fromCents(r.fifteenth_amount),
@ -190,13 +189,15 @@ function getUserExportData(userId) {
)
.all(userId);
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
.filter((p) => p.notes)
.map((p) => ({ type: 'payment', payment_id: p.id, bill_id: p.bill_id, notes: p.notes })),
.filter((p: any) => p.notes)
.map((p: any) => ({ type: 'payment', payment_id: p.id, bill_id: p.bill_id, notes: p.notes })),
...monthlyState
.filter((m) => m.notes)
.map((m) => ({
.filter((m: any) => m.notes)
.map((m: any) => ({
type: 'monthly_state',
monthly_state_id: m.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
// so the export→import round-trip can be regression-tested (exportImportRoundTrip).
function buildUserDbExportFile(userId) {
function buildUserDbExportFile(userId: any) {
const data = getUserExportData(userId);
const file = path.join(os.tmpdir(), `bill-tracker-user-${userId}-${Date.now()}.sqlite`);
const out = new Database(file);
@ -291,14 +292,14 @@ function buildUserDbExportFile(userId) {
`);
const meta = out.prepare('INSERT INTO export_metadata (key, value) VALUES (?, ?)');
meta.run('metadata_json', JSON.stringify(data.metadata));
const insertRows = (table, rows) => {
const insertRows = (table: any, rows: any) => {
if (!rows.length) return;
const cols = Object.keys(rows[0]);
const stmt = out.prepare(
`INSERT INTO ${table} (${cols.join(',')}) VALUES (${cols.map(() => '?').join(',')})`,
);
const tx = out.transaction((items) =>
items.forEach((row) => stmt.run(cols.map((c) => row[c]))),
const tx = out.transaction((items: any) =>
items.forEach((row: any) => stmt.run(cols.map((c: any) => row[c]))),
);
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';
const express = require('express');
const router = express.Router();
@ -8,7 +7,7 @@ const { accountingActiveSql } = require('../services/paymentAccountingService.ct
const { toCents, fromCents } = require('../utils/money.mts');
const { ValidationError } = require('../utils/apiError.cts');
function parseYearMonth(source) {
function parseYearMonth(source: any) {
const now = new Date();
const year = parseInt(source.year || now.getFullYear(), 10);
const month = parseInt(source.month || now.getMonth() + 1, 10);
@ -23,12 +22,12 @@ function parseYearMonth(source) {
return { year, month };
}
function money(value) {
function money(value: any) {
const n = Number(value);
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
.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);
// 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 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';
const express = require('express');
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 TRANSACTION_MATCH_SOURCE = 'transaction_match';
function isTransactionLinkedPayment(payment) {
function isTransactionLinkedPayment(payment: any) {
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 month = parseInt(body.month, 10);
if (!Number.isInteger(year) || year < 2000 || year > 2100) {
@ -46,7 +45,7 @@ function parseYearMonth(body) {
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
.prepare(
`
@ -543,9 +542,9 @@ router.post('/bulk', (req: Req, res: Res) => {
AND p.${SQL_NOT_DELETED}`,
);
const created = [];
const skipped = [];
const errors = [];
const created: any[] = [];
const skipped: any[] = [];
const errors: any[] = [];
const runBulk = db.transaction(() => {
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';
const express = require('express');
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 rawIds = Array.isArray(req.body?.transaction_ids) ? req.body.transaction_ids : [];
const txIds = rawIds
.map((id) => parseInt(id, 10))
.filter((n) => Number.isInteger(n) && n > 0)
.map((id: any) => parseInt(id, 10))
.filter((n: any) => Number.isInteger(n) && n > 0)
.slice(0, 50);
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 || [] },
});
res.status(201).json(created);
} catch (err) {
} catch (err: any) {
// Service validation errors are user-facing at 4xx (parity with the old
// behavior: no-status service throws surfaced at 400, never as masked 5xx).
throw new ApiError(
@ -222,7 +221,7 @@ router.get('/catalog', (req: Req, res: Res) => {
)
.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
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 });
}
const catalog = catalogEntries.map((entry) => {
const catalog = catalogEntries.map((entry: any) => {
const bill = billByCatalogId.get(entry.id) ?? null;
return {
...entry,

6
types/http.d.ts vendored
View File

@ -39,6 +39,12 @@ export interface Res {
type(t: string): Res;
redirect(url: string): 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;
attachment(filename?: string): Res;
locals: any;