From 6c915a62c8fe33c0b2b90c26df0345ee6ac53c86 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 11 Jul 2026 06:57:41 -0500 Subject: [PATCH] =?UTF-8?q?refactor(types):=20type-check=20batch=202=20?= =?UTF-8?q?=E2=80=94=207=20medium=20routes=20+=202=20real=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- routes/admin.cts | 33 ++++++++++++------------ routes/authOidc.cts | 9 +++---- routes/calendar.cts | 33 +++++++++++------------- routes/export.cts | 39 +++++++++++++++-------------- routes/monthly-starting-amounts.cts | 11 ++++---- routes/payments.cts | 13 +++++----- routes/subscriptions.cts | 11 ++++---- types/http.d.ts | 6 +++++ 8 files changed, 76 insertions(+), 79 deletions(-) diff --git a/routes/admin.cts b/routes/admin.cts index 04fb7e9..76dc391 100644 --- a/routes/admin.cts +++ b/routes/admin.cts @@ -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', diff --git a/routes/authOidc.cts b/routes/authOidc.cts index 3f4738f..86be9d6 100644 --- a/routes/authOidc.cts +++ b/routes/authOidc.cts @@ -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'; diff --git a/routes/calendar.cts b/routes/calendar.cts index 5795d1a..8a3de31 100644 --- a/routes/calendar.cts +++ b/routes/calendar.cts @@ -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, }, }); }); diff --git a/routes/export.cts b/routes/export.cts index a520e90..fcbbed0 100644 --- a/routes/export.cts +++ b/routes/export.cts @@ -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); }; diff --git a/routes/monthly-starting-amounts.cts b/routes/monthly-starting-amounts.cts index 193cfa6..b1abf99 100644 --- a/routes/monthly-starting-amounts.cts +++ b/routes/monthly-starting-amounts.cts @@ -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); diff --git a/routes/payments.cts b/routes/payments.cts index 3261c4a..141a0e9 100644 --- a/routes/payments.cts +++ b/routes/payments.cts @@ -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) { diff --git a/routes/subscriptions.cts b/routes/subscriptions.cts index ec4c491..21c8ac4 100644 --- a/routes/subscriptions.cts +++ b/routes/subscriptions.cts @@ -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(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, diff --git a/types/http.d.ts b/types/http.d.ts index e8d4f6c..4b7709e 100644 --- a/types/http.d.ts +++ b/types/http.d.ts @@ -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, + cb?: (err?: any) => void, + ): void; sendFile(path: string, options?: any, cb?: (err?: any) => void): void; attachment(filename?: string): Res; locals: any;