2026-06-04 04:31:25 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
import type { Db } from '../types/db';
|
|
|
|
|
|
2026-07-06 11:22:44 -05:00
|
|
|
const { normalizeMerchant } = require('./subscriptionService.cts');
|
2026-07-06 16:05:18 -05:00
|
|
|
const { log } = require('../utils/logger.cts');
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
const { localDateString } = require('../utils/dates.mts') as typeof import('../utils/dates.mts');
|
|
|
|
|
const { toCents, fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts');
|
2026-06-04 04:31:25 -05:00
|
|
|
|
|
|
|
|
// Spending = unmatched outflows (amount < 0) that haven't been ignored.
|
|
|
|
|
// Bill-matched transactions are excluded so there's no double-counting.
|
|
|
|
|
const SPENDING_WHERE = `
|
|
|
|
|
t.amount < 0
|
|
|
|
|
AND t.ignored = 0
|
|
|
|
|
AND t.match_status != 'matched'
|
|
|
|
|
AND t.user_id = ?
|
|
|
|
|
`;
|
|
|
|
|
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
interface SpendingCatRow {
|
|
|
|
|
category_id: number | string | null;
|
|
|
|
|
category_name: string;
|
|
|
|
|
group_id: number | null;
|
|
|
|
|
group_name: string | null;
|
|
|
|
|
amount: number;
|
|
|
|
|
tx_count: number;
|
|
|
|
|
budget: number | null;
|
|
|
|
|
avg_3mo: number | null;
|
|
|
|
|
pct_of_total?: number;
|
|
|
|
|
}
|
2026-07-06 14:23:53 -05:00
|
|
|
interface SpendingTxOptions {
|
|
|
|
|
categoryId?: number | null;
|
|
|
|
|
uncategorizedOnly?: boolean;
|
|
|
|
|
page?: number;
|
|
|
|
|
limit?: number;
|
|
|
|
|
}
|
|
|
|
|
interface IncomeOptions {
|
|
|
|
|
page?: number;
|
|
|
|
|
limit?: number;
|
|
|
|
|
includeIgnored?: boolean;
|
|
|
|
|
}
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
|
|
|
|
|
function monthRange(year: number, month: number): { start: string; end: string } {
|
2026-06-04 04:31:25 -05:00
|
|
|
const start = `${year}-${String(month).padStart(2, '0')}-01`;
|
2026-07-06 14:23:53 -05:00
|
|
|
const end = localDateString(new Date(year, month, 0)); // last day
|
2026-06-04 04:31:25 -05:00
|
|
|
return { start, end };
|
|
|
|
|
}
|
|
|
|
|
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
function shiftMonth(year: number, month: number, delta: number): { year: number; month: number } {
|
2026-07-06 14:23:53 -05:00
|
|
|
let m = month + delta,
|
|
|
|
|
y = year;
|
|
|
|
|
while (m < 1) {
|
|
|
|
|
m += 12;
|
|
|
|
|
y--;
|
|
|
|
|
}
|
|
|
|
|
while (m > 12) {
|
|
|
|
|
m -= 12;
|
|
|
|
|
y++;
|
|
|
|
|
}
|
2026-06-14 19:21:34 -05:00
|
|
|
return { year: y, month: m };
|
|
|
|
|
}
|
|
|
|
|
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
function cents(raw: unknown): number {
|
2026-06-04 04:31:25 -05:00
|
|
|
return Math.abs(Number(raw)) / 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
function getSpendingSummary(db: Db, userId: number, year: number, month: number) {
|
2026-06-04 04:31:25 -05:00
|
|
|
const { start, end } = monthRange(year, month);
|
2026-06-14 19:21:34 -05:00
|
|
|
const dateRangeParams = [start, end, start + 'T00:00:00', end + 'T23:59:59'];
|
2026-07-06 14:23:53 -05:00
|
|
|
const DATE_RANGE_SQL =
|
|
|
|
|
'(t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))';
|
2026-06-04 04:31:25 -05:00
|
|
|
|
2026-06-14 19:21:34 -05:00
|
|
|
// Every spending-enabled category, even with $0 activity this month (YNAB-style: budgeted
|
|
|
|
|
// categories are always visible, not just ones with transactions).
|
2026-07-06 14:23:53 -05:00
|
|
|
const categoryRows = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
SELECT
|
|
|
|
|
c.id AS category_id,
|
|
|
|
|
c.name AS category_name,
|
2026-06-14 19:21:34 -05:00
|
|
|
c.group_id AS group_id,
|
|
|
|
|
cg.name AS group_name,
|
|
|
|
|
COALESCE(SUM(ABS(t.amount)), 0) AS total_cents,
|
|
|
|
|
COUNT(t.id) AS tx_count
|
|
|
|
|
FROM categories c
|
|
|
|
|
LEFT JOIN category_groups cg ON cg.id = c.group_id
|
|
|
|
|
LEFT JOIN transactions t ON t.spending_category_id = c.id
|
|
|
|
|
AND ${SPENDING_WHERE} AND ${DATE_RANGE_SQL}
|
|
|
|
|
WHERE c.user_id = ? AND c.deleted_at IS NULL AND c.spending_enabled = 1
|
2026-06-04 04:31:25 -05:00
|
|
|
GROUP BY c.id
|
2026-06-14 19:21:34 -05:00
|
|
|
ORDER BY CASE WHEN c.sort_order IS NULL THEN 1 ELSE 0 END, c.sort_order ASC, c.name COLLATE NOCASE ASC
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(userId, ...dateRangeParams, userId);
|
2026-06-14 19:21:34 -05:00
|
|
|
|
|
|
|
|
// Uncategorized outflows
|
2026-07-06 14:23:53 -05:00
|
|
|
const uncatRow = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-14 19:21:34 -05:00
|
|
|
SELECT COALESCE(SUM(ABS(t.amount)), 0) AS total_cents, COUNT(t.id) AS tx_count
|
|
|
|
|
FROM transactions t
|
|
|
|
|
WHERE ${SPENDING_WHERE} AND t.spending_category_id IS NULL AND ${DATE_RANGE_SQL}
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(userId, ...dateRangeParams);
|
2026-06-14 19:21:34 -05:00
|
|
|
|
|
|
|
|
// Outflows categorized to a category that isn't (or is no longer) spending-enabled —
|
|
|
|
|
// bundled into a single "Other categories" row so totals stay accurate without polluting
|
|
|
|
|
// the spending-enabled category list.
|
2026-07-06 14:23:53 -05:00
|
|
|
const otherRow = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-14 19:21:34 -05:00
|
|
|
SELECT COALESCE(SUM(ABS(t.amount)), 0) AS total_cents, COUNT(t.id) AS tx_count
|
|
|
|
|
FROM transactions t
|
|
|
|
|
LEFT JOIN categories c ON c.id = t.spending_category_id
|
|
|
|
|
WHERE ${SPENDING_WHERE} AND ${DATE_RANGE_SQL}
|
|
|
|
|
AND t.spending_category_id IS NOT NULL
|
|
|
|
|
AND (c.id IS NULL OR c.deleted_at IS NOT NULL OR c.spending_enabled = 0)
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(userId, ...dateRangeParams);
|
2026-06-04 04:31:25 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const budgets = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
SELECT category_id, amount FROM spending_budgets
|
|
|
|
|
WHERE user_id = ? AND year = ? AND month = ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(userId, year, month);
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
const budgetMap = new Map(budgets.map((b: any) => [b.category_id, b.amount]));
|
2026-06-04 04:31:25 -05:00
|
|
|
|
2026-06-14 19:21:34 -05:00
|
|
|
// 3-month spending average per category (the 3 calendar months before the requested one)
|
|
|
|
|
const prev1 = monthRange(shiftMonth(year, month, -1).year, shiftMonth(year, month, -1).month);
|
|
|
|
|
const prev3 = monthRange(shiftMonth(year, month, -3).year, shiftMonth(year, month, -3).month);
|
2026-07-06 14:23:53 -05:00
|
|
|
const avgRows = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-14 19:21:34 -05:00
|
|
|
SELECT t.spending_category_id AS category_id, COALESCE(SUM(ABS(t.amount)), 0) AS total_cents
|
|
|
|
|
FROM transactions t
|
|
|
|
|
WHERE ${SPENDING_WHERE} AND t.spending_category_id IS NOT NULL
|
|
|
|
|
AND (t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))
|
|
|
|
|
GROUP BY t.spending_category_id
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(userId, prev3.start, prev1.end, prev3.start + 'T00:00:00', prev1.end + 'T23:59:59');
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
const avgMap = new Map(avgRows.map((r: any) => [r.category_id, r.total_cents / 3]));
|
2026-06-14 19:21:34 -05:00
|
|
|
|
2026-06-04 04:31:25 -05:00
|
|
|
let totalCents = 0;
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
const byCategory: SpendingCatRow[] = categoryRows.map((r: any) => {
|
2026-06-04 04:31:25 -05:00
|
|
|
totalCents += r.total_cents;
|
|
|
|
|
return {
|
2026-07-06 14:23:53 -05:00
|
|
|
category_id: r.category_id,
|
2026-06-14 19:21:34 -05:00
|
|
|
category_name: r.category_name,
|
2026-07-06 14:23:53 -05:00
|
|
|
group_id: r.group_id ?? null,
|
|
|
|
|
group_name: r.group_name ?? null,
|
|
|
|
|
amount: r.total_cents / 100,
|
|
|
|
|
tx_count: r.tx_count,
|
|
|
|
|
budget: fromCents(budgetMap.get(r.category_id) ?? null),
|
|
|
|
|
avg_3mo: fromCents(avgMap.get(r.category_id) ?? 0),
|
2026-06-04 04:31:25 -05:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-14 19:21:34 -05:00
|
|
|
if (uncatRow.tx_count > 0) {
|
|
|
|
|
totalCents += uncatRow.total_cents;
|
|
|
|
|
byCategory.push({
|
2026-07-06 14:23:53 -05:00
|
|
|
category_id: null,
|
|
|
|
|
category_name: '(Uncategorized)',
|
|
|
|
|
group_id: null,
|
|
|
|
|
group_name: null,
|
|
|
|
|
amount: uncatRow.total_cents / 100,
|
|
|
|
|
tx_count: uncatRow.tx_count,
|
|
|
|
|
budget: null,
|
|
|
|
|
avg_3mo: null,
|
2026-06-14 19:21:34 -05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (otherRow.tx_count > 0) {
|
|
|
|
|
totalCents += otherRow.total_cents;
|
|
|
|
|
byCategory.push({
|
2026-07-06 14:23:53 -05:00
|
|
|
category_id: 'other',
|
|
|
|
|
category_name: 'Other categories',
|
|
|
|
|
group_id: null,
|
|
|
|
|
group_name: null,
|
|
|
|
|
amount: otherRow.total_cents / 100,
|
|
|
|
|
tx_count: otherRow.tx_count,
|
|
|
|
|
budget: null,
|
|
|
|
|
avg_3mo: null,
|
2026-06-14 19:21:34 -05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 04:31:25 -05:00
|
|
|
// Attach pct_of_total
|
2026-07-06 14:23:53 -05:00
|
|
|
byCategory.forEach((c) => {
|
|
|
|
|
c.pct_of_total = totalCents > 0 ? Math.round((c.amount / (totalCents / 100)) * 100) / 100 : 0;
|
2026-06-04 04:31:25 -05:00
|
|
|
});
|
|
|
|
|
|
2026-06-14 19:21:34 -05:00
|
|
|
const uncategorized_amount = uncatRow.total_cents / 100;
|
2026-07-06 14:23:53 -05:00
|
|
|
const uncategorized_count = uncatRow.tx_count;
|
2026-06-04 04:31:25 -05:00
|
|
|
|
|
|
|
|
// Income (positive unmatched transactions this month)
|
2026-07-06 14:23:53 -05:00
|
|
|
const incomeRow = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
SELECT COALESCE(SUM(t.amount), 0) AS total FROM transactions t
|
|
|
|
|
WHERE t.user_id = ? AND t.ignored = 0 AND t.amount > 0
|
|
|
|
|
AND t.match_status != 'matched'
|
|
|
|
|
AND (t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(userId, start, end, start + 'T00:00:00', end + 'T23:59:59');
|
2026-06-04 04:31:25 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const categoryGroups = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-14 19:21:34 -05:00
|
|
|
SELECT id, name, sort_order FROM category_groups
|
|
|
|
|
WHERE user_id = ? ORDER BY sort_order ASC, name COLLATE NOCASE ASC
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(userId);
|
2026-06-14 19:21:34 -05:00
|
|
|
|
2026-06-04 04:31:25 -05:00
|
|
|
return {
|
2026-07-06 14:23:53 -05:00
|
|
|
year,
|
|
|
|
|
month,
|
|
|
|
|
total_spending: totalCents / 100,
|
2026-06-04 04:31:25 -05:00
|
|
|
uncategorized_amount,
|
|
|
|
|
uncategorized_count,
|
2026-07-06 14:23:53 -05:00
|
|
|
by_category: byCategory,
|
|
|
|
|
category_groups: categoryGroups,
|
|
|
|
|
income: (incomeRow?.total ?? 0) / 100,
|
2026-06-04 04:31:25 -05:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Transactions ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
function getSpendingTransactions(
|
|
|
|
|
db: Db,
|
|
|
|
|
userId: number,
|
|
|
|
|
year: number,
|
|
|
|
|
month: number,
|
|
|
|
|
{
|
|
|
|
|
categoryId = undefined,
|
|
|
|
|
uncategorizedOnly = false,
|
|
|
|
|
page = 1,
|
|
|
|
|
limit = 50,
|
|
|
|
|
}: SpendingTxOptions = {},
|
|
|
|
|
) {
|
2026-06-04 04:31:25 -05:00
|
|
|
const { start, end } = monthRange(year, month);
|
|
|
|
|
const offset = (Math.max(1, page) - 1) * limit;
|
|
|
|
|
|
|
|
|
|
let filter = '';
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
const params: any[] = [userId, start, end, start + 'T00:00:00', end + 'T23:59:59'];
|
2026-06-04 04:31:25 -05:00
|
|
|
|
|
|
|
|
if (uncategorizedOnly) {
|
|
|
|
|
filter = 'AND t.spending_category_id IS NULL';
|
|
|
|
|
} else if (categoryId !== undefined) {
|
|
|
|
|
if (categoryId === null) {
|
|
|
|
|
filter = 'AND t.spending_category_id IS NULL';
|
|
|
|
|
} else {
|
|
|
|
|
filter = 'AND t.spending_category_id = ?';
|
|
|
|
|
params.push(categoryId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const rows = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
SELECT
|
|
|
|
|
t.id, t.amount, t.payee, t.description, t.memo,
|
|
|
|
|
t.posted_date, t.transacted_at, t.spending_category_id,
|
|
|
|
|
c.name AS category_name
|
|
|
|
|
FROM transactions t
|
|
|
|
|
LEFT JOIN categories c ON c.id = t.spending_category_id AND c.deleted_at IS NULL
|
|
|
|
|
WHERE ${SPENDING_WHERE} ${filter}
|
|
|
|
|
AND (t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))
|
|
|
|
|
ORDER BY COALESCE(t.posted_date, DATE(t.transacted_at)) DESC, t.id DESC
|
|
|
|
|
LIMIT ? OFFSET ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(...params, limit, offset);
|
2026-06-04 04:31:25 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const total = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
SELECT COUNT(*) AS n FROM transactions t
|
|
|
|
|
WHERE ${SPENDING_WHERE} ${filter}
|
|
|
|
|
AND (t.posted_date BETWEEN ? AND ? OR (t.posted_date IS NULL AND t.transacted_at BETWEEN ? AND ?))
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(...params).n;
|
2026-06-04 04:31:25 -05:00
|
|
|
|
|
|
|
|
return {
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
transactions: rows.map((r: any) => ({
|
2026-07-06 14:23:53 -05:00
|
|
|
id: r.id,
|
|
|
|
|
amount: cents(r.amount),
|
|
|
|
|
payee: r.payee || r.description || r.memo || '(Unknown)',
|
|
|
|
|
date: r.posted_date || (r.transacted_at ? String(r.transacted_at).slice(0, 10) : null),
|
|
|
|
|
spending_category_id: r.spending_category_id,
|
2026-06-04 04:31:25 -05:00
|
|
|
spending_category_name: r.category_name ?? null,
|
|
|
|
|
})),
|
|
|
|
|
total,
|
|
|
|
|
page,
|
|
|
|
|
pages: Math.ceil(total / limit),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Categorize ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
function categorizeTransaction(
|
|
|
|
|
db: Db,
|
|
|
|
|
userId: number,
|
|
|
|
|
txId: number,
|
|
|
|
|
categoryId: number | null,
|
|
|
|
|
saveMerchantRule = false,
|
|
|
|
|
): void {
|
|
|
|
|
const tx = db
|
|
|
|
|
.prepare('SELECT * FROM transactions WHERE id = ? AND user_id = ?')
|
|
|
|
|
.get(txId, userId);
|
2026-06-04 04:31:25 -05:00
|
|
|
if (!tx) throw Object.assign(new Error('Transaction not found'), { status: 404 });
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare(
|
|
|
|
|
"UPDATE transactions SET spending_category_id=?, updated_at=datetime('now') WHERE id=? AND user_id=?",
|
|
|
|
|
).run(categoryId ?? null, txId, userId);
|
2026-06-04 04:31:25 -05:00
|
|
|
|
|
|
|
|
if (saveMerchantRule && categoryId) {
|
|
|
|
|
const merchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
|
|
|
|
|
if (merchant && merchant.length >= 3) {
|
|
|
|
|
try {
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
INSERT INTO spending_category_rules (user_id, category_id, merchant)
|
|
|
|
|
VALUES (?, ?, ?)
|
|
|
|
|
ON CONFLICT(user_id, merchant) DO UPDATE SET category_id=excluded.category_id
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
).run(userId, categoryId, merchant);
|
2026-06-04 20:01:51 -05:00
|
|
|
// Auto-mark this category as spending-enabled
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare(
|
|
|
|
|
"UPDATE categories SET spending_enabled=1, updated_at=datetime('now') WHERE id=? AND user_id=?",
|
|
|
|
|
).run(categoryId, userId);
|
2026-06-04 04:31:25 -05:00
|
|
|
applySpendingCategoryRules(db, userId, merchant);
|
2026-07-06 14:23:53 -05:00
|
|
|
} catch {
|
|
|
|
|
/* safe to ignore */
|
|
|
|
|
}
|
2026-06-04 04:31:25 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Auto-categorization ──────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
function applySpendingCategoryRules(
|
|
|
|
|
db: Db,
|
|
|
|
|
userId: number,
|
|
|
|
|
onlyMerchant: string | null = null,
|
|
|
|
|
): number {
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
let rules: any[];
|
2026-06-04 04:31:25 -05:00
|
|
|
try {
|
|
|
|
|
const q = onlyMerchant
|
2026-07-06 14:23:53 -05:00
|
|
|
? db
|
|
|
|
|
.prepare(
|
|
|
|
|
'SELECT merchant, category_id FROM spending_category_rules WHERE user_id=? AND merchant=?',
|
|
|
|
|
)
|
2026-06-04 04:31:25 -05:00
|
|
|
.all(userId, onlyMerchant)
|
2026-07-06 14:23:53 -05:00
|
|
|
: db
|
|
|
|
|
.prepare('SELECT merchant, category_id FROM spending_category_rules WHERE user_id=?')
|
2026-06-04 04:31:25 -05:00
|
|
|
.all(userId);
|
|
|
|
|
rules = q;
|
2026-07-06 14:23:53 -05:00
|
|
|
} catch {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2026-06-04 04:31:25 -05:00
|
|
|
if (!rules.length) return 0;
|
|
|
|
|
|
|
|
|
|
let applied = 0;
|
2026-07-06 14:23:53 -05:00
|
|
|
const update = db.prepare(
|
|
|
|
|
"UPDATE transactions SET spending_category_id=?, updated_at=datetime('now') WHERE id=? AND user_id=?",
|
|
|
|
|
);
|
2026-06-04 04:31:25 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const txRows = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
SELECT id, payee, description, memo FROM transactions
|
|
|
|
|
WHERE user_id=? AND amount<0 AND ignored=0 AND match_status!='matched' AND spending_category_id IS NULL
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(userId);
|
2026-06-04 04:31:25 -05:00
|
|
|
|
|
|
|
|
db.transaction(() => {
|
|
|
|
|
for (const tx of txRows) {
|
|
|
|
|
const merchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
|
|
|
|
|
if (!merchant) continue;
|
2026-07-06 14:23:53 -05:00
|
|
|
const rule = rules.find((r) => merchantMatches(merchant, r.merchant));
|
|
|
|
|
if (rule) {
|
|
|
|
|
update.run(rule.category_id, tx.id, userId);
|
|
|
|
|
applied++;
|
|
|
|
|
}
|
2026-06-04 04:31:25 -05:00
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
return applied;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
function merchantMatches(txMerchant: string, ruleMerchant: string): boolean {
|
2026-06-04 04:31:25 -05:00
|
|
|
if (!txMerchant || !ruleMerchant) return false;
|
|
|
|
|
if (txMerchant === ruleMerchant) return true;
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
2026-07-06 14:23:53 -05:00
|
|
|
const wb = (s: string) => new RegExp(`(^|\\s)${esc(s)}(\\s|$)`);
|
2026-06-04 04:31:25 -05:00
|
|
|
return wb(ruleMerchant).test(txMerchant) || wb(txMerchant).test(ruleMerchant);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Budgets ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
function getSpendingBudgets(db: Db, userId: number, year: number, month: number) {
|
2026-07-06 14:23:53 -05:00
|
|
|
const rows = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
SELECT sb.category_id, sb.amount, c.name AS category_name
|
|
|
|
|
FROM spending_budgets sb
|
|
|
|
|
JOIN categories c ON c.id = sb.category_id AND c.deleted_at IS NULL
|
|
|
|
|
WHERE sb.user_id=? AND sb.year=? AND sb.month=?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(userId, year, month);
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
return rows.map((r: any) => ({ ...r, amount: fromCents(r.amount) }));
|
2026-06-04 04:31:25 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
function setSpendingBudget(
|
|
|
|
|
db: Db,
|
|
|
|
|
userId: number,
|
|
|
|
|
categoryId: number,
|
|
|
|
|
year: number,
|
|
|
|
|
month: number,
|
|
|
|
|
amount: number | string | null,
|
|
|
|
|
): void {
|
2026-06-04 04:31:25 -05:00
|
|
|
if (amount === null || amount === undefined) {
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare(
|
|
|
|
|
'DELETE FROM spending_budgets WHERE user_id=? AND category_id=? AND year=? AND month=?',
|
|
|
|
|
).run(userId, categoryId, year, month);
|
2026-06-04 04:31:25 -05:00
|
|
|
} else {
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
INSERT INTO spending_budgets (user_id, category_id, year, month, amount, updated_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
|
|
|
ON CONFLICT(user_id, category_id, year, month) DO UPDATE SET
|
|
|
|
|
amount=excluded.amount, updated_at=datetime('now')
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
).run(userId, categoryId, year, month, toCents(amount));
|
2026-06-04 04:31:25 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Category rules ───────────────────────────────────────────────────────────
|
|
|
|
|
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
function getSpendingCategoryRules(db: Db, userId: number) {
|
2026-07-06 14:23:53 -05:00
|
|
|
return db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
SELECT r.id, r.merchant, r.category_id, c.name AS category_name
|
|
|
|
|
FROM spending_category_rules r
|
|
|
|
|
JOIN categories c ON c.id = r.category_id AND c.deleted_at IS NULL
|
|
|
|
|
WHERE r.user_id=?
|
|
|
|
|
ORDER BY r.merchant ASC
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(userId);
|
2026-06-04 04:31:25 -05:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
function addSpendingCategoryRule(
|
|
|
|
|
db: Db,
|
|
|
|
|
userId: number,
|
|
|
|
|
categoryId: number,
|
|
|
|
|
merchant: string,
|
|
|
|
|
): void {
|
2026-06-04 04:31:25 -05:00
|
|
|
const normalized = normalizeMerchant(merchant);
|
2026-07-06 14:23:53 -05:00
|
|
|
if (!normalized || normalized.length < 2)
|
|
|
|
|
throw Object.assign(new Error('Merchant name too short'), { status: 400 });
|
|
|
|
|
db.prepare(
|
|
|
|
|
`
|
2026-06-04 04:31:25 -05:00
|
|
|
INSERT INTO spending_category_rules (user_id, category_id, merchant)
|
|
|
|
|
VALUES (?, ?, ?)
|
|
|
|
|
ON CONFLICT(user_id, merchant) DO UPDATE SET category_id=excluded.category_id
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
).run(userId, categoryId, normalized);
|
2026-06-04 20:01:51 -05:00
|
|
|
// Auto-mark this category as spending-enabled so it shows in the picker
|
|
|
|
|
try {
|
2026-07-06 14:23:53 -05:00
|
|
|
db.prepare(
|
|
|
|
|
"UPDATE categories SET spending_enabled=1, updated_at=datetime('now') WHERE id=? AND user_id=?",
|
|
|
|
|
).run(categoryId, userId);
|
|
|
|
|
} catch {
|
|
|
|
|
/* non-critical */
|
|
|
|
|
}
|
2026-06-04 04:31:25 -05:00
|
|
|
applySpendingCategoryRules(db, userId, normalized);
|
|
|
|
|
}
|
|
|
|
|
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
function deleteSpendingCategoryRule(db: Db, userId: number, ruleId: number): void {
|
2026-06-04 04:31:25 -05:00
|
|
|
db.prepare('DELETE FROM spending_category_rules WHERE id=? AND user_id=?').run(ruleId, userId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 19:53:38 -05:00
|
|
|
// ── Income ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
function getIncomeTransactions(
|
|
|
|
|
db: Db,
|
|
|
|
|
userId: number,
|
|
|
|
|
year: number,
|
|
|
|
|
month: number,
|
|
|
|
|
{ page = 1, limit = 50, includeIgnored = false }: IncomeOptions = {},
|
|
|
|
|
) {
|
2026-06-04 19:53:38 -05:00
|
|
|
const { start, end } = monthRange(year, month);
|
|
|
|
|
const offset = (Math.max(1, page) - 1) * limit;
|
2026-06-04 21:19:25 -05:00
|
|
|
const ignoredFilter = includeIgnored ? '' : 'AND ignored = 0';
|
2026-06-04 19:53:38 -05:00
|
|
|
|
|
|
|
|
try {
|
2026-07-06 14:23:53 -05:00
|
|
|
const rows = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 21:19:25 -05:00
|
|
|
SELECT id, amount, payee, description, memo, posted_date, transacted_at, ignored
|
2026-06-04 19:53:38 -05:00
|
|
|
FROM transactions
|
2026-06-04 21:19:25 -05:00
|
|
|
WHERE user_id = ? AND amount > 0 ${ignoredFilter} AND match_status != 'matched'
|
2026-06-04 19:53:38 -05:00
|
|
|
AND (posted_date BETWEEN ? AND ? OR (posted_date IS NULL AND transacted_at BETWEEN ? AND ?))
|
2026-06-04 21:19:25 -05:00
|
|
|
ORDER BY ignored ASC, COALESCE(posted_date, DATE(transacted_at)) DESC, id DESC
|
2026-06-04 19:53:38 -05:00
|
|
|
LIMIT ? OFFSET ?
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.all(userId, start, end, start + 'T00:00:00', end + 'T23:59:59', limit, offset);
|
2026-06-04 19:53:38 -05:00
|
|
|
|
2026-07-06 14:23:53 -05:00
|
|
|
const total = db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-06-04 19:53:38 -05:00
|
|
|
SELECT COUNT(*) AS n FROM transactions
|
2026-06-04 21:19:25 -05:00
|
|
|
WHERE user_id = ? AND amount > 0 ${ignoredFilter} AND match_status != 'matched'
|
2026-06-04 19:53:38 -05:00
|
|
|
AND (posted_date BETWEEN ? AND ? OR (posted_date IS NULL AND transacted_at BETWEEN ? AND ?))
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.get(userId, start, end, start + 'T00:00:00', end + 'T23:59:59').n;
|
2026-06-04 19:53:38 -05:00
|
|
|
|
|
|
|
|
return {
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
transactions: rows.map((r: any) => ({
|
2026-07-06 14:23:53 -05:00
|
|
|
id: r.id,
|
|
|
|
|
amount: Math.abs(Number(r.amount)) / 100,
|
|
|
|
|
payee: r.payee || r.description || r.memo || '(Unknown)',
|
|
|
|
|
date: r.posted_date || (r.transacted_at ? String(r.transacted_at).slice(0, 10) : null),
|
2026-06-04 21:19:25 -05:00
|
|
|
ignored: r.ignored === 1,
|
2026-06-04 19:53:38 -05:00
|
|
|
})),
|
|
|
|
|
total,
|
|
|
|
|
page,
|
|
|
|
|
pages: Math.ceil(total / limit),
|
|
|
|
|
};
|
refactor(server): migrate 10 services to TypeScript (.cts)
Continues the incremental server TS migration (after utils/*.mts and
paymentValidation.cts): converts 10 services to .cts (CommonJS TypeScript,
run natively via Node type-stripping — no build step), type-checked by
tsconfig.server.json.
Migrated: totpService, amountSuggestionService, encryptionService,
paymentAccountingService, statusService, aprService, driftService,
analyticsService, spendingService, billMerchantRuleService.
- types/db.d.ts: shared minimal better-sqlite3 Db/Statement types (the lib
ships none), reused across the migrated modules.
- Every require of a migrated service updated to the explicit .cts extension
(extensionless require does not resolve .cts) across routes / services /
db migrations / server.js / workers / tests — require-only changes.
- package.json: check:server find pattern now includes *.cts (it silently
skipped .cts before, so paymentValidation.cts had no node --check gate).
Behavior-preserving (types only). Verified: typecheck:server 0,
check:server 0, full server suite 226/226, real boot → /api/health 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:35:29 -05:00
|
|
|
} catch (err: any) {
|
2026-07-06 16:05:18 -05:00
|
|
|
log.error('[getIncomeTransactions]', err.message);
|
2026-06-04 19:53:38 -05:00
|
|
|
return { transactions: [], total: 0, page: 1, pages: 1 };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 04:31:25 -05:00
|
|
|
module.exports = {
|
|
|
|
|
getSpendingSummary,
|
|
|
|
|
getSpendingTransactions,
|
|
|
|
|
categorizeTransaction,
|
|
|
|
|
applySpendingCategoryRules,
|
|
|
|
|
getSpendingBudgets,
|
|
|
|
|
setSpendingBudget,
|
|
|
|
|
getSpendingCategoryRules,
|
|
|
|
|
addSpendingCategoryRule,
|
|
|
|
|
deleteSpendingCategoryRule,
|
2026-06-04 19:53:38 -05:00
|
|
|
getIncomeTransactions,
|
2026-06-04 04:31:25 -05:00
|
|
|
};
|