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 ;
const { getDb , closeDb } = require ( '../db/database' ) ;
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 ;
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 ;
test . before ( ( ) => {
db = getDb ( ) ;
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 ;
} ) ;
test . after ( ( ) => {
closeDb ( ) ;
for ( const s of [ '' , '-wal' , '-shm' ] ) { try { fs . rmSync ( dbPath + s ) ; } catch { } }
} ) ;
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 ) ;
assert . deepEqual ( budgets , [ { category _id : catId , amount : 200 , category _name : 'Groceries' } ] , 'returned in dollars' ) ;
} ) ;
test ( 'setting the same category again updates in place (ON CONFLICT), no duplicate row' , ( ) => {
setSpendingBudget ( db , userId , catId , 2026 , 7 , 350.50 ) ;
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 ) , [ ] ) ;
} ) ;