import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { describe, it, expect } from 'vitest' import { openDb } from '../src/db' import { writeAudit, listAudit } from '../src/audit' import { seedIfEmpty } from '../src/seed' import { createClient } from '../src/repos-clients' import { createModule, assignModule } from '../src/repos-modules' describe('hq db', () => { it('creates every HQ-1 table', async () => { const db = openDb(':memory:') const names = (await db.all<{ name: string }>(`SELECT name FROM sqlite_master WHERE type='table'`)).map(r => r.name) for (const t of ['staff_user','session','client','module','module_price_book','client_module', 'tax_class','doc_series','document','payment','payment_allocation','document_event', 'email_account','email_log','setting','audit_log','stg_client','stg_invoice']) expect(names, `missing table ${t}`).toContain(t) }) it('audit writes and lists newest-first', async () => { const db = openDb(':memory:') await writeAudit(db, 'u1', 'create', 'client', 'c1', undefined, { name: 'Acme' }) await writeAudit(db, 'u1', 'update', 'client', 'c1', { name: 'Acme' }, { name: 'Acme Ltd' }) const rows = await listAudit(db) expect(rows).toHaveLength(2) expect(rows[0]!.action).toBe('update') expect(JSON.parse(rows[0]!.after_json!)).toEqual({ name: 'Acme Ltd' }) }) // FIX 4 (regression): the APEX importer inserted client_module rows WITHOUT created_at, so // every imported project with zero ticks was permanently NULL-aged. migrate() must backfill // it from installed_on where that's known — using a real (non-:memory:) DB reopened twice, // since the backfill runs as part of migrate() on every open, not just column-creation. describe('client_module.created_at backfill (FIX 4)', () => { it('backfills created_at from installed_on for a legacy row missing it', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-db-backfill-')) try { let db = openDb(dir) await seedIfEmpty(db) const client = await createClient(db, 'u1', { name: 'Legacy Client', stateCode: '32' }) const mod = await createModule(db, 'u1', { code: 'LEGACYBF', name: 'Legacy Module' }) const cm = await assignModule(db, 'u1', { clientId: client.id, moduleId: mod.id, kind: 'yearly' }) // Simulate the actual APEX-importer bug: installed_on known, created_at never stamped. await db.run(`UPDATE client_module SET created_at=NULL, installed_on=? WHERE id=?`, '2025-11-03', cm.id) await db.close() db = openDb(dir) // reopening re-runs migrate() — the backfill must heal it unprompted const row = await db.get<{ created_at: string | null; installed_on: string | null }>( `SELECT created_at, installed_on FROM client_module WHERE id=?`, cm.id, ) expect(row!.created_at).toBe('2025-11-03') await db.close() } finally { fs.rmSync(dir, { recursive: true, force: true }) } }) it('leaves created_at NULL when installed_on is also unknown — never fabricates a date', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-db-backfill-')) try { let db = openDb(dir) await seedIfEmpty(db) const client = await createClient(db, 'u1', { name: 'Legacy Client 2', stateCode: '32' }) const mod = await createModule(db, 'u1', { code: 'LEGACYBF2', name: 'Legacy Module 2' }) const cm = await assignModule(db, 'u1', { clientId: client.id, moduleId: mod.id, kind: 'yearly' }) await db.run(`UPDATE client_module SET created_at=NULL WHERE id=?`, cm.id) // installed_on stays NULL too await db.close() db = openDb(dir) const row = await db.get<{ created_at: string | null }>( `SELECT created_at FROM client_module WHERE id=?`, cm.id, ) expect(row!.created_at).toBeNull() await db.close() } finally { fs.rmSync(dir, { recursive: true, force: true }) } }) }) })