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