diff --git a/routes/admin.js b/routes/admin.js index 3bc56a4..126b3da 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { getDb, rollbackMigration } = require('../db/database'); const { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays, setDebugLogging } = require('../services/bankSyncConfigService.cts'); -const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker'); +const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts'); const { isEnvKeyActive, keyFingerprint } = require('../services/encryptionService.cts'); const { hashPassword } = require('../services/authService'); const { logAudit } = require('../services/auditService.cts'); @@ -18,7 +18,7 @@ const { getScheduleStatus, runScheduledBackupNow, saveSettings: saveBackupScheduleSettings, -} = require('../services/backupScheduler'); +} = require('../services/backupScheduler.cts'); const { getCleanupStatus, runAllCleanup, diff --git a/routes/calendar.js b/routes/calendar.js index 0bb1089..586942f 100644 --- a/routes/calendar.js +++ b/routes/calendar.js @@ -3,7 +3,7 @@ const { standardizeError } = require('../middleware/errorFormatter'); const router = express.Router(); const { getDb } = require('../db/database'); const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts'); -const { getUserSettings } = require('../services/userSettings'); +const { getUserSettings } = require('../services/userSettings.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); const { feedUrlForToken, diff --git a/routes/dataSources.js b/routes/dataSources.js index b04137e..c636b6a 100644 --- a/routes/dataSources.js +++ b/routes/dataSources.js @@ -5,7 +5,7 @@ const { getDb } = require('../db/database'); const { standardizeError } = require('../middleware/errorFormatter'); const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService.cts'); const { connectSimplefin, syncDataSource, backfillDataSource, disconnectDataSource } = require('../services/bankSyncService'); -const { sanitizeErrorMessage } = require('../services/simplefinService'); +const { sanitizeErrorMessage } = require('../services/simplefinService.cts'); const { getBankSyncConfig } = require('../services/bankSyncConfigService.cts'); const { syncLimiter } = require('../middleware/rateLimiter'); diff --git a/routes/import.js b/routes/import.js index 16d8025..17404f4 100644 --- a/routes/import.js +++ b/routes/import.js @@ -19,7 +19,7 @@ const { const { previewOfxTransactions, commitOfxTransactions, -} = require('../services/ofxImportService'); +} = require('../services/ofxImportService.cts'); function dataImportEnabled() { return String(process.env.DATA_IMPORT_ENABLED ?? 'true').toLowerCase() !== 'false'; diff --git a/routes/settings.js b/routes/settings.js index 5fdbadd..f476dac 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); -const { getUserSettings, setUserSettings } = require('../services/userSettings'); +const { getUserSettings, setUserSettings } = require('../services/userSettings.cts'); // GET /api/settings — returns only user-facing app preferences router.get('/', (req, res) => { diff --git a/routes/status.js b/routes/status.js index 3ba3933..1857260 100644 --- a/routes/status.js +++ b/routes/status.js @@ -5,9 +5,9 @@ const os = require('os'); const { getDb, getSetting } = require('../db/database'); const { getStatusRuntime, recordError } = require('../services/statusRuntime.cts'); const { listBackups } = require('../services/backupService'); -const { getScheduleStatus } = require('../services/backupScheduler'); +const { getScheduleStatus } = require('../services/backupScheduler.cts'); const { checkForUpdates } = require('../services/updateCheckService.cts'); -const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker'); +const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts'); const { getBankSyncConfig } = require('../services/bankSyncConfigService.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); const { localDateString } = require('../utils/dates.mts'); diff --git a/routes/summary.js b/routes/summary.js index 2f38b2e..6040784 100644 --- a/routes/summary.js +++ b/routes/summary.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { getDb } = require('../db/database'); const { getCycleRange, resolveDueDate } = require('../services/statusService.cts'); -const { getUserSettings } = require('../services/userSettings'); +const { getUserSettings } = require('../services/userSettings.cts'); const { accountingActiveSql } = require('../services/paymentAccountingService.cts'); const { toCents, fromCents } = require('../utils/money.mts'); diff --git a/routes/user.js b/routes/user.js index 5253653..b0e6305 100644 --- a/routes/user.js +++ b/routes/user.js @@ -5,7 +5,7 @@ const router = express.Router(); const { getDb } = require('../db/database'); const { seedDemoData } = require('../scripts/seedDemoData'); const { demoDataLimiter } = require('../middleware/rateLimiter'); -const { eraseUserData } = require('../services/userDataService'); +const { eraseUserData } = require('../services/userDataService.cts'); const { standardizeError } = require('../middleware/errorFormatter'); // GET /api/user/seeded-status — returns whether the current user has any seeded data diff --git a/server.js b/server.js index 116930c..afe8bcb 100644 --- a/server.js +++ b/server.js @@ -299,14 +299,14 @@ async function main() { // Start SimpleFIN auto-sync worker (no-op when bank sync is disabled) try { - require('./services/bankSyncWorker').start(); + require('./services/bankSyncWorker.cts').start(); } catch (err) { console.error('[bankSync] Failed to start auto-sync worker:', err.message); } // Start scheduled database backups only when enabled in Admin settings. try { - const backupScheduler = require('./services/backupScheduler'); + const backupScheduler = require('./services/backupScheduler.cts'); if (backupScheduler.getScheduleStatus().enabled) { backupScheduler.start(); } diff --git a/services/backupScheduler.js b/services/backupScheduler.cts similarity index 78% rename from services/backupScheduler.js rename to services/backupScheduler.cts index bfb93c4..62825fb 100644 --- a/services/backupScheduler.js +++ b/services/backupScheduler.cts @@ -1,40 +1,50 @@ +// import required so Node type-strips this .cts (it has no other import). +import type { Db } from '../types/db'; + const cron = require('node-cron'); const { getSetting, setSetting } = require('../db/database'); const { applyScheduledRetention, createBackup } = require('./backupService'); -let task = null; -let nextRunAt = null; +interface ScheduleSettings { + enabled: boolean; + frequency: string; + time: string; + retention_count: number; +} + +let task: any = null; +let nextRunAt: string | null = null; let running = false; -function parseBool(value) { +function parseBool(value: unknown): boolean { return value === true || value === 'true'; } -function validateScheduleSettings(input = {}) { +function validateScheduleSettings(input: any = {}): ScheduleSettings { const enabled = parseBool(input.enabled); const frequency = input.frequency || 'daily'; const time = input.time || '02:00'; const retentionCount = parseInt(input.retention_count ?? '2', 10); if (!['daily', 'weekly'].includes(frequency)) { - const err = new Error('frequency must be daily or weekly'); + const err = new Error('frequency must be daily or weekly') as any; err.status = 400; throw err; } if (!/^\d{2}:\d{2}$/.test(time)) { - const err = new Error('time must use HH:MM format'); + const err = new Error('time must use HH:MM format') as any; err.status = 400; throw err; } const [hour, minute] = time.split(':').map(Number); if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { - const err = new Error('time must be a valid 24-hour time'); + const err = new Error('time must be a valid 24-hour time') as any; err.status = 400; throw err; } if (!Number.isInteger(retentionCount) || retentionCount < 1 || retentionCount > 365) { - const err = new Error('retention_count must be between 1 and 365'); + const err = new Error('retention_count must be between 1 and 365') as any; err.status = 400; throw err; } @@ -42,7 +52,7 @@ function validateScheduleSettings(input = {}) { return { enabled, frequency, time, retention_count: retentionCount }; } -function readSettings() { +function readSettings(): ScheduleSettings { return validateScheduleSettings({ enabled: getSetting('backup_schedule_enabled') === 'true', frequency: getSetting('backup_schedule_frequency') || 'daily', @@ -51,15 +61,15 @@ function readSettings() { }); } -function getLastRunAt() { +function getLastRunAt(): string | null { return getSetting('backup_schedule_last_run_at') || null; } -function getLastError() { +function getLastError(): string | null { return getSetting('backup_schedule_last_error') || null; } -function computeNextRun(settings = readSettings(), from = new Date()) { +function computeNextRun(settings: ScheduleSettings = readSettings(), from: Date = new Date()): string | null { if (!settings.enabled) return null; const [hour, minute] = settings.time.split(':').map(Number); const next = new Date(from); @@ -74,16 +84,16 @@ function computeNextRun(settings = readSettings(), from = new Date()) { return next.toISOString(); } -function cronExpression(settings) { +function cronExpression(settings: ScheduleSettings): string { const [hour, minute] = settings.time.split(':').map(Number); return settings.frequency === 'weekly' ? `${minute} ${hour} * * 0` : `${minute} ${hour} * * *`; } -async function runScheduledBackupNow() { +async function runScheduledBackupNow(): Promise { if (running) { - const err = new Error('Scheduled backup is already running'); + const err = new Error('Scheduled backup is already running') as any; err.status = 409; throw err; } @@ -97,7 +107,7 @@ async function runScheduledBackupNow() { setSetting('backup_schedule_last_error', ''); nextRunAt = computeNextRun(settings); return backup; - } catch (err) { + } catch (err: any) { setSetting('backup_schedule_last_error', err.message || 'Scheduled backup failed'); throw err; } finally { @@ -105,7 +115,7 @@ async function runScheduledBackupNow() { } } -function stop() { +function stop(): void { if (task) task.stop(); task = null; } @@ -118,7 +128,7 @@ function reloadSchedule() { if (!settings.enabled) return getScheduleStatus(); task = cron.schedule(cronExpression(settings), () => { - runScheduledBackupNow().catch(err => { + runScheduledBackupNow().catch((err: any) => { console.error('[backupScheduler] Scheduled backup failed:', err.message); }); }); @@ -126,7 +136,7 @@ function reloadSchedule() { return getScheduleStatus(); } -function saveSettings(input) { +function saveSettings(input: any) { const settings = validateScheduleSettings(input); setSetting('backup_schedule_enabled', settings.enabled ? 'true' : 'false'); setSetting('backup_schedule_frequency', settings.frequency); diff --git a/services/bankSyncService.js b/services/bankSyncService.js index 13e97af..c6c00b6 100644 --- a/services/bankSyncService.js +++ b/services/bankSyncService.js @@ -7,14 +7,14 @@ const { normalizeAccount, normalizeTransaction, sanitizeErrorMessage, -} = require('./simplefinService'); +} = require('./simplefinService.cts'); const { getBankSyncConfig, SYNC_DAYS_EFFECTIVE, SYNC_DAYS_DEFAULT } = require('./bankSyncConfigService.cts'); const { decorateDataSource } = require('./transactionService.cts'); const { applyMerchantRules } = require('./billMerchantRuleService.cts'); const { applySpendingCategoryRules } = require('./spendingService.cts'); const { applyMerchantStoreMatches } = require('./merchantStoreMatchService.cts'); const { autoMatchForUser } = require('./matchSuggestionService.cts'); -const { getUserSettings } = require('./userSettings'); +const { getUserSettings } = require('./userSettings.cts'); function sinceEpochDays(days) { return Math.floor((Date.now() - days * 86400 * 1000) / 1000); diff --git a/services/bankSyncWorker.js b/services/bankSyncWorker.cts similarity index 89% rename from services/bankSyncWorker.js rename to services/bankSyncWorker.cts index ec3e88b..3891df0 100644 --- a/services/bankSyncWorker.js +++ b/services/bankSyncWorker.cts @@ -1,5 +1,7 @@ 'use strict'; +import type { Db } from '../types/db'; + const { getDb } = require('../db/database'); const { getBankSyncConfig } = require('./bankSyncConfigService.cts'); const { syncDataSource } = require('./bankSyncService'); @@ -9,27 +11,27 @@ const MIN_SYNC_AGE_MS = 60 * 60 * 1000; // 1 hour // Pause between each source to avoid hammering SimpleFIN const STAGGER_DELAY_MS = 3000; -let timer = null; +let timer: any = null; let running = false; -let lastRunAt = null; -let nextRunAt = null; +let lastRunAt: string | null = null; +let nextRunAt: string | null = null; -function intervalMs() { +function intervalMs(): number { const { sync_interval_hours } = getBankSyncConfig(); return Math.round(sync_interval_hours * 3600000); } -function needsSync(source) { +function needsSync(source: any): boolean { if (!source.last_sync_at) return true; const age = Date.now() - new Date(source.last_sync_at).getTime(); return age >= MIN_SYNC_AGE_MS; } -function sleep(ms) { +function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } -async function runCycle() { +async function runCycle(): Promise { if (running) return; const config = getBankSyncConfig(); @@ -40,7 +42,7 @@ async function runCycle() { const debug = config.debug_logging; - let db, sources; + let db: Db, sources: any[]; try { db = getDb(); sources = db.prepare(` @@ -48,7 +50,7 @@ async function runCycle() { WHERE type = 'provider_sync' AND provider = 'simplefin' ORDER BY last_sync_at ASC `).all(); - } catch (err) { + } catch (err: any) { console.error('[bankSync] Worker failed to load sources:', err.message); return; } @@ -84,7 +86,7 @@ async function runCycle() { result.pendingCleared ? `${result.pendingCleared} pending cleared` : null, ].filter(Boolean).join(', '); console.log(`[bankSync] Source #${source.id}: OK — ${result.accountsUpserted} account(s), ${result.transactionsNew} new, ${result.transactionsSkip} skipped${extra ? `, ${extra}` : ''}${result.errlist ? ` [partial: ${result.errlist}]` : ''}`); - } catch (err) { + } catch (err: any) { failed++; console.error(`[bankSync] Source #${source.id}: FAILED — ${err.message}`); } @@ -97,7 +99,7 @@ async function runCycle() { running = false; } -function scheduleNext() { +function scheduleNext(): void { const ms = intervalMs(); nextRunAt = new Date(Date.now() + ms).toISOString(); @@ -114,13 +116,13 @@ function scheduleNext() { if (timer.unref) timer.unref(); } -function start() { +function start(): void { if (timer) return; scheduleNext(); console.log(`[bankSync] Auto-sync worker started (interval: ${getBankSyncConfig().sync_interval_hours}h)`); } -function stop() { +function stop(): void { clearTimeout(timer); timer = null; } diff --git a/services/billMerchantRuleService.cts b/services/billMerchantRuleService.cts index 6eb4343..2112dfc 100644 --- a/services/billMerchantRuleService.cts +++ b/services/billMerchantRuleService.cts @@ -3,7 +3,7 @@ import type { Db } from '../types/db'; const { normalizeMerchant } = require('./subscriptionService'); -const { getUserSettings } = require('./userSettings'); +const { getUserSettings } = require('./userSettings.cts'); const { applyBankPaymentAsSourceOfTruth } = require('./paymentAccountingService.cts'); const { localDateString } = require('../utils/dates.mts') as typeof import('../utils/dates.mts'); const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts'); diff --git a/services/driftService.cts b/services/driftService.cts index 19b936e..8fe501a 100644 --- a/services/driftService.cts +++ b/services/driftService.cts @@ -5,7 +5,7 @@ import type { Dollars } from '../utils/money.mts'; const { getDb } = require('../db/database'); const { getCycleRange } = require('./statusService.cts'); const { accountingActiveSql } = require('./paymentAccountingService.cts'); -const { getUserSettings } = require('./userSettings'); +const { getUserSettings } = require('./userSettings.cts'); const { localDateString } = require('../utils/dates.mts') as typeof import('../utils/dates.mts'); const { roundMoney, fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts'); diff --git a/services/ofxImportService.js b/services/ofxImportService.cts similarity index 88% rename from services/ofxImportService.js rename to services/ofxImportService.cts index 0a3c04b..a8943cf 100644 --- a/services/ofxImportService.js +++ b/services/ofxImportService.cts @@ -9,6 +9,8 @@ // path as the CSV importer (shared primitives from csvTransactionImportService), // so dedupe scope, the import_sessions table, and import_history are identical. +import type { Db } from '../types/db'; + const { getDb } = require('../db/database'); const { decorateTransaction, ensureManualDataSource } = require('./transactionService.cts'); const { @@ -21,10 +23,12 @@ const { parseCents, } = require('./csvTransactionImportService'); +type Row = Record; + const MAX_TX = 25000; -function importError(status, message, code, details = []) { - const err = new Error(message); +function importError(status: number, message: string, code: string, details: any[] = []): Error { + const err = new Error(message) as any; err.status = status; err.code = code; err.details = details; @@ -32,13 +36,13 @@ function importError(status, message, code, details = []) { } // Read the first value of in `block`, up to the next '<' or line end. -function tagValue(block, tag) { +function tagValue(block: string, tag: string): string { const m = new RegExp(`<${tag}>([^<\\r\\n]*)`, 'i').exec(block); return m ? m[1].trim() : ''; } // OFX date: YYYYMMDD[HHMMSS[.XXX]][ tz ] → 'YYYY-MM-DD' (posted date; tz dropped). -function ofxDate(value) { +function ofxDate(value: unknown): string | null { const m = /^(\d{4})(\d{2})(\d{2})/.exec(String(value || '').trim()); if (!m) return null; const [, y, mo, d] = m; @@ -48,10 +52,10 @@ function ofxDate(value) { return `${y}-${mo}-${d}`; } -function decodeEntities(s) { +function decodeEntities(s: unknown): string { return String(s || '') .replace(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>') - .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))) + .replace(/&#(\d+);/g, (_: string, n: string) => String.fromCharCode(Number(n))) .trim(); } @@ -59,7 +63,7 @@ function decodeEntities(s) { * Parse an OFX/QFX buffer into normalized transactions (same shape the CSV path * produces). Throws importError on a file with no parsable transactions. */ -function parseOfx(buffer) { +function parseOfx(buffer: any): Row[] { const text = Buffer.isBuffer(buffer) ? buffer.toString('utf8') : String(buffer || ''); if (!//i.test(text) && !//i.test(text)) { throw importError(400, 'This does not look like an OFX/QFX file.', 'OFX_INVALID'); @@ -74,7 +78,7 @@ function parseOfx(buffer) { || text.match(/[\s\S]*?(?=|<\/BANKTRANLIST>|<\/OFX>)/gi) || []; - const transactions = []; + const transactions: Row[] = []; for (const raw of blocks) { if (transactions.length >= MAX_TX) break; const postedDate = ofxDate(tagValue(raw, 'DTPOSTED')); @@ -111,7 +115,7 @@ function parseOfx(buffer) { return transactions; } -function getOrCreateOfxDataSource(db, userId) { +function getOrCreateOfxDataSource(db: Db, userId: number): Row { ensureManualDataSource(db, userId); const existing = db.prepare(` SELECT * FROM data_sources @@ -126,8 +130,8 @@ function getOrCreateOfxDataSource(db, userId) { return db.prepare('SELECT * FROM data_sources WHERE id = ? AND user_id = ?').get(result.lastInsertRowid, userId); } -function previewOfxTransactions(userId, buffer, options = {}) { - const db = getDb(); +function previewOfxTransactions(userId: number, buffer: any, options: any = {}) { + const db: Db = getDb(); pruneExpiredSessions(db); const transactions = parseOfx(buffer); const sessionId = saveImportSession(db, userId, { @@ -143,8 +147,8 @@ function previewOfxTransactions(userId, buffer, options = {}) { }; } -function commitOfxTransactions(userId, importSessionId, options = {}) { - const db = getDb(); +function commitOfxTransactions(userId: number, importSessionId: any, options: any = {}) { + const db: Db = getDb(); const session = loadImportSession(db, userId, importSessionId); if (session.kind !== 'ofx_transactions') { throw importError(400, 'Import session is not an OFX/QFX preview.', 'OFX_SESSION_INVALID'); @@ -152,7 +156,7 @@ function commitOfxTransactions(userId, importSessionId, options = {}) { const dataSource = getOrCreateOfxDataSource(db, userId); const counts = { imported: 0, skipped: 0, failed: 0 }; - const details = []; + const details: any[] = []; const insert = db.prepare(` INSERT INTO transactions (user_id, data_source_id, account_id, provider_transaction_id, source_type, @@ -165,7 +169,7 @@ function commitOfxTransactions(userId, importSessionId, options = {}) { ); const run = db.transaction(() => { - (session.transactions || []).forEach((tx, index) => { + (session.transactions || []).forEach((tx: Row, index: number) => { try { if (existing.get(userId, dataSource.id, tx.provider_transaction_id)) { counts.skipped++; @@ -187,7 +191,7 @@ function commitOfxTransactions(userId, importSessionId, options = {}) { source_type: 'file_import', account_id: account?.id ?? null, match_status: 'unmatched', ignored: 0, }), }); - } catch (err) { + } catch (err: any) { counts.failed++; details.push({ row: index + 1, result: 'failed', message: err.message }); } diff --git a/services/simplefinService.js b/services/simplefinService.cts similarity index 88% rename from services/simplefinService.js rename to services/simplefinService.cts index 93eed1c..b3efb97 100644 --- a/services/simplefinService.js +++ b/services/simplefinService.cts @@ -1,5 +1,7 @@ 'use strict'; +// import required so Node type-strips this .cts (it has no other import). +import type { Db } from '../types/db'; // SimpleFIN consumer client. // @@ -12,21 +14,23 @@ // - Tokens and Access URLs are never logged. // - Error messages are sanitized before being returned to callers. -function sanitizeErrorMessage(msg) { +type Row = Record; + +function sanitizeErrorMessage(msg: unknown): string { if (typeof msg !== 'string') return 'Provider error'; // Strip embedded HTTP Basic Auth credentials (https://user:pass@host) return msg.replace(/https?:\/\/[^@\s]+:[^@\s]+@/gi, 'https://[credentials]@'); } -function sanitizeError(err) { +function sanitizeError(err: any): Error { const msg = sanitizeErrorMessage(err?.message || String(err || 'Unknown error')); - const e = new Error(msg); + const e = new Error(msg) as any; e.code = err?.code || 'SIMPLEFIN_ERROR'; e.status = err?.status; return e; } -function sanitizeRawData(obj) { +function sanitizeRawData(obj: any): string | null { if (!obj || typeof obj !== 'object') return null; const safe = JSON.parse(JSON.stringify(obj)); // Remove org sfin-url which may embed auth @@ -38,7 +42,7 @@ function sanitizeRawData(obj) { } } -async function claimSetupToken(setupToken) { +async function claimSetupToken(setupToken: unknown): Promise { if (!setupToken || typeof setupToken !== 'string') { throw new Error('setupToken is required'); } @@ -46,7 +50,7 @@ async function claimSetupToken(setupToken) { const token = setupToken.trim(); // Base64-decode to get the claim URL; handle both raw base64 and data-URLs - let claimUrl; + let claimUrl = token; try { const decoded = Buffer.from(token, 'base64').toString('utf8').trim(); claimUrl = decoded.startsWith('http') ? decoded : token; @@ -63,7 +67,7 @@ async function claimSetupToken(setupToken) { throw new Error('Setup token must use a secure HTTPS URL'); } - let accessUrl; + let accessUrl = ''; try { const res = await fetch(claimUrl, { method: 'POST', signal: AbortSignal.timeout(30000) }); if (res.status === 403) { @@ -87,8 +91,8 @@ async function claimSetupToken(setupToken) { const FETCH_RETRY_ATTEMPTS = 3; const FETCH_RETRY_DELAYS = [1000, 2000]; // ms between attempts 1→2 and 2→3 -async function fetchAccountsAndTransactions(accessUrl, sinceEpoch) { - let url; +async function fetchAccountsAndTransactions(accessUrl: string, sinceEpoch: number): Promise { + let url: URL; try { url = new URL(accessUrl); } catch { @@ -100,11 +104,11 @@ async function fetchAccountsAndTransactions(accessUrl, sinceEpoch) { // pending=1 asks SimpleFIN to include not-yet-settled transactions (excluded by default). const endpoint = `${baseUrl}/accounts?start-date=${Math.floor(sinceEpoch)}&pending=1&version=2`; - let lastErr; + let lastErr: any; for (let attempt = 0; attempt < FETCH_RETRY_ATTEMPTS; attempt++) { if (attempt > 0) await new Promise(r => setTimeout(r, FETCH_RETRY_DELAYS[attempt - 1])); - let res; + let res: Response; try { res = await fetch(endpoint, { headers: { 'Authorization': `Basic ${basicAuth}` }, @@ -126,7 +130,7 @@ async function fetchAccountsAndTransactions(accessUrl, sinceEpoch) { throw sanitizeError(err); } - let data; + let data: any; try { data = await res.json(); } catch (err) { @@ -136,7 +140,7 @@ async function fetchAccountsAndTransactions(accessUrl, sinceEpoch) { // Surface any connection-level errors from the errlist so callers can log them if (Array.isArray(data.errlist) && data.errlist.length > 0) { const msgs = data.errlist - .map(e => sanitizeErrorMessage(e.message || e.msg || e.code || 'Unknown error')) + .map((e: any) => sanitizeErrorMessage(e.message || e.msg || e.code || 'Unknown error')) .join('; '); data._errlistSummary = msgs; } @@ -146,7 +150,7 @@ async function fetchAccountsAndTransactions(accessUrl, sinceEpoch) { throw sanitizeError(lastErr); } -function normalizeAccount(rawAccount, dataSourceId, userId) { +function normalizeAccount(rawAccount: any, dataSourceId: number, userId: number): Row { const balance = rawAccount.balance != null ? Math.round(parseFloat(rawAccount.balance) * 100) : null; const availableBalance = rawAccount['available-balance'] != null ? Math.round(parseFloat(rawAccount['available-balance']) * 100) : null; @@ -167,7 +171,7 @@ function normalizeAccount(rawAccount, dataSourceId, userId) { // accountCurrency: currency string from the parent account (e.g. "USD", "EUR"). // accountId: raw SimpleFIN account id — used in the stable dedup key so the key // survives disconnect/reconnect (data_source_id is intentionally omitted). -function normalizeTransaction(rawTx, localAccountId, dataSourceId, userId, accountId, accountCurrency) { +function normalizeTransaction(rawTx: any, localAccountId: any, dataSourceId: number, userId: number, accountId: any, accountCurrency: any): Row { const amount = Math.round(parseFloat(rawTx.amount) * 100); // Pending transactions report posted = 0 (or omit it) until they settle. const isPending = rawTx.pending === true || rawTx.pending === 1; diff --git a/services/trackerService.js b/services/trackerService.js index ca948fd..f1adabd 100644 --- a/services/trackerService.js +++ b/services/trackerService.js @@ -2,7 +2,7 @@ const { getDb } = require('../db/database'); const { buildTrackerRow, getCycleRange, resolveDueDate, isPaidStatus } = require('./statusService.cts'); -const { getUserSettings } = require('./userSettings'); +const { getUserSettings } = require('./userSettings.cts'); const { computeBalanceDelta, applyBalanceDelta } = require('./billsService'); const { computeAmountSuggestionsBatch } = require('./amountSuggestionService.cts'); const { accountingActiveSql } = require('./paymentAccountingService.cts'); diff --git a/services/userDataService.js b/services/userDataService.cts similarity index 90% rename from services/userDataService.js rename to services/userDataService.cts index 2683dcb..78ebb16 100644 --- a/services/userDataService.js +++ b/services/userDataService.cts @@ -1,5 +1,7 @@ 'use strict'; +import type { Db } from '../types/db'; + const { ensureUserDefaultCategories } = require('../db/database'); // Independent user-owned tables (each has a user_id) wiped wholesale. @@ -16,12 +18,10 @@ const USER_TABLES = [ * preferences. Runs in one transaction (child → parent order so it holds with * foreign_keys ON), then re-seeds default categories so the app stays usable and * writes an audit row to import_history. - * - * @returns {{ erased: number, counts: Record }} */ -function eraseUserData(db, userId) { - const counts = {}; - const del = (label, sql) => { counts[label] = db.prepare(sql).run(userId).changes; }; +function eraseUserData(db: Db, userId: number): { erased: number; counts: Record } { + const counts: Record = {}; + const del = (label: string, sql: string) => { counts[label] = db.prepare(sql).run(userId).changes; }; const run = db.transaction(() => { // Bill-children (no user_id of their own) — remove before bills. diff --git a/services/userSettings.js b/services/userSettings.cts similarity index 87% rename from services/userSettings.js rename to services/userSettings.cts index 39adee7..701f9d1 100644 --- a/services/userSettings.js +++ b/services/userSettings.cts @@ -1,5 +1,7 @@ 'use strict'; +import type { Db } from '../types/db'; + const { getDb, getSetting } = require('../db/database'); const USER_SETTING_KEYS = [ @@ -33,7 +35,7 @@ const USER_SETTING_KEYS = [ 'spending_group_categories', ]; -const USER_SETTING_DEFAULTS = { +const USER_SETTING_DEFAULTS: Record = { week_start: '0', default_landing_page: 'tracker', due_soon_days: '3', @@ -55,16 +57,16 @@ const USER_SETTING_DEFAULTS = { spending_group_categories: 'false', }; -function defaultUserSettings() { - const defaults = {}; +function defaultUserSettings(): Record { + const defaults: Record = {}; for (const key of USER_SETTING_KEYS) { defaults[key] = USER_SETTING_DEFAULTS[key] ?? getSetting(key); } return defaults; } -function getUserSettings(userId) { - const db = getDb(); +function getUserSettings(userId: number): Record { + const db: Db = getDb(); const settings = defaultUserSettings(); const rows = db.prepare(` SELECT key, value @@ -79,8 +81,8 @@ function getUserSettings(userId) { return settings; } -function setUserSettings(userId, data) { - const db = getDb(); +function setUserSettings(userId: number, data: Record): Record { + const db: Db = getDb(); const upsert = db.prepare(` INSERT INTO user_settings (user_id, key, value, updated_at) VALUES (?, ?, ?, datetime('now')) diff --git a/tests/backupAndCleanup.test.js b/tests/backupAndCleanup.test.js index 379397d..8fead82 100644 --- a/tests/backupAndCleanup.test.js +++ b/tests/backupAndCleanup.test.js @@ -30,7 +30,7 @@ const { validateScheduleSettings, computeNextRun, runScheduledBackupNow, -} = require('../services/backupScheduler'); +} = require('../services/backupScheduler.cts'); const { pruneOrphanedBackupPartials, pruneStaleExportFiles, diff --git a/tests/eraseUserData.test.js b/tests/eraseUserData.test.js index b0e1ea5..2bf3091 100644 --- a/tests/eraseUserData.test.js +++ b/tests/eraseUserData.test.js @@ -12,7 +12,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-erase-${process.pid}.sqlite` process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); -const { eraseUserData } = require('../services/userDataService'); +const { eraseUserData } = require('../services/userDataService.cts'); function makeUser(db, name) { return db.prepare("INSERT INTO users (username, password_hash, role, active) VALUES (?, 'hash', 'user', 1)").run(name).lastInsertRowid; diff --git a/tests/ofxImportService.test.js b/tests/ofxImportService.test.js index 04ff8d6..92d1361 100644 --- a/tests/ofxImportService.test.js +++ b/tests/ofxImportService.test.js @@ -12,7 +12,7 @@ const dbPath = path.join(os.tmpdir(), `bill-tracker-ofx-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); -const { parseOfx, previewOfxTransactions, commitOfxTransactions } = require('../services/ofxImportService'); +const { parseOfx, previewOfxTransactions, commitOfxTransactions } = require('../services/ofxImportService.cts'); // OFX 1.x SGML — container tags closed, leaf tags unclosed. const OFX_SGML = `OFXHEADER:100