2026-07-05 13:03:32 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
// Phase 1 — spendingService budgets were untested. Pins the dollars↔cents
|
|
|
|
|
// round-trip and the set/update/clear semantics of setSpendingBudget /
|
|
|
|
|
// getSpendingBudgets (getSpendingSummary's gating is separately reconciled
|
|
|
|
|
// against the Tracker in tests/reconciliation.test.js).
|
|
|
|
|
const test = require('node:test');
|
|
|
|
|
const assert = require('node:assert/strict');
|
|
|
|
|
const os = require('node:os');
|
|
|
|
|
const path = require('node:path');
|
|
|
|
|
const fs = require('node:fs');
|
|
|
|
|
|
|
|
|
|
const dbPath = path.join(os.tmpdir(), `bill-tracker-spendsvc-${process.pid}.sqlite`);
|
|
|
|
|
process.env.DB_PATH = dbPath;
|
|
|
|
|
|
refactor(server): migrate middleware, workers, and db layer to TypeScript (.cts)
- middleware/ (5): requireAuth, securityHeaders, errorFormatter, csrf,
rateLimiter — fully typed against the shared http Req/Res/Next types.
- workers/dailyWorker — fully typed.
- db/ (4): database, subscriptionCatalogSeed, migrations/versionedMigrations,
migrations/legacyReconcileMigrations — large dynamic schema/migration infra,
converted with @ts-nocheck (typing deferred). All requires updated to .cts.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 11:34:08 -05:00
|
|
|
const { getDb, closeDb } = require('../db/database.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 <[email protected]>
2026-07-06 10:35:29 -05:00
|
|
|
const { setSpendingBudget, getSpendingBudgets } = require('../services/spendingService.cts');
|
2026-07-05 13:03:32 -05:00
|
|
|
|
|
|
|
|
let db, userId, catId;
|
2026-07-06 14:23:53 -05:00
|
|
|
const rawCents = () =>
|
|
|
|
|
db
|
|
|
|
|
.prepare(
|
|
|
|
|
'SELECT amount FROM spending_budgets WHERE user_id=? AND category_id=? AND year=2026 AND month=7',
|
|
|
|
|
)
|
|
|
|
|
.get(userId, catId)?.amount;
|
2026-07-05 13:03:32 -05:00
|
|
|
|
|
|
|
|
test.before(() => {
|
|
|
|
|
db = getDb();
|
2026-07-06 14:23:53 -05:00
|
|
|
userId = db
|
|
|
|
|
.prepare(
|
|
|
|
|
"INSERT INTO users (username, password_hash, role, active) VALUES ('spend-user','x','user',1)",
|
|
|
|
|
)
|
|
|
|
|
.run().lastInsertRowid;
|
|
|
|
|
catId = db
|
|
|
|
|
.prepare("INSERT INTO categories (user_id, name, spending_enabled) VALUES (?, 'Groceries', 1)")
|
|
|
|
|
.run(userId).lastInsertRowid;
|
2026-07-05 13:03:32 -05:00
|
|
|
});
|
|
|
|
|
test.after(() => {
|
|
|
|
|
closeDb();
|
2026-07-06 14:23:53 -05:00
|
|
|
for (const s of ['', '-wal', '-shm']) {
|
|
|
|
|
try {
|
|
|
|
|
fs.rmSync(dbPath + s);
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
2026-07-05 13:03:32 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('setSpendingBudget stores cents and getSpendingBudgets returns dollars', () => {
|
|
|
|
|
setSpendingBudget(db, userId, catId, 2026, 7, 200);
|
|
|
|
|
assert.equal(rawCents(), 20000, 'stored as integer cents');
|
|
|
|
|
const budgets = getSpendingBudgets(db, userId, 2026, 7);
|
2026-07-06 14:23:53 -05:00
|
|
|
assert.deepEqual(
|
|
|
|
|
budgets,
|
|
|
|
|
[{ category_id: catId, amount: 200, category_name: 'Groceries' }],
|
|
|
|
|
'returned in dollars',
|
|
|
|
|
);
|
2026-07-05 13:03:32 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('setting the same category again updates in place (ON CONFLICT), no duplicate row', () => {
|
2026-07-06 14:23:53 -05:00
|
|
|
setSpendingBudget(db, userId, catId, 2026, 7, 350.5);
|
2026-07-05 13:03:32 -05:00
|
|
|
assert.equal(rawCents(), 35050);
|
|
|
|
|
const budgets = getSpendingBudgets(db, userId, 2026, 7);
|
|
|
|
|
assert.equal(budgets.length, 1, 'still one budget row for the category/month');
|
|
|
|
|
assert.equal(budgets[0].amount, 350.5);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('setting null clears the budget', () => {
|
|
|
|
|
setSpendingBudget(db, userId, catId, 2026, 7, null);
|
|
|
|
|
assert.equal(rawCents(), undefined, 'row deleted');
|
|
|
|
|
assert.deepEqual(getSpendingBudgets(db, userId, 2026, 7), []);
|
|
|
|
|
});
|