// apps/hq/test/pg-engine.test.ts — D19 P4: the same repos, on REAL Postgres. // Opt-in: set HQ_PG_TEST_URL (e.g. postgres://hq:hq_dev@localhost:5432/hq_test). // The target database is WIPED each run (drop schema public cascade) — point it // at a dedicated test database, never at hq/production. import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { openPgDb } from '../src/db-pg' import type { DB } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createStaff, login } from '../src/auth' import { createClient, listClients, setClientDbPassword, updateClient } from '../src/repos-clients' import { createModule, setPrice } from '../src/repos-modules' import { createDraft, getDocument, issueDocument } from '../src/repos-documents' import { outstandingPaise, recordPayment } from '../src/repos-payments' import { upsertReminder } from '../src/repos-reminders' import { startServer } from '../src/server' const URL_ = process.env['HQ_PG_TEST_URL'] ?? '' describe.skipIf(URL_ === '')('Postgres engine (D19) — core flows on the real thing', () => { let db: DB beforeAll(async () => { // Fresh schema every run: migrations must build the world from zero. const wipe = await openPgDb(URL_) // may apply migrations to a stale schema first await wipe.exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`) await wipe.close() db = await openPgDb(URL_) await seedIfEmpty(db) await createStaff(db, { email: 'pg@test.in', displayName: 'PG', role: 'owner', password: 'pg-password-1' }) }, 30_000) afterAll(async () => { await db.close() }) it('is the postgres engine with migrations recorded', async () => { expect(db.engine).toBe('postgres') const migs = await db.all<{ id: string }>(`SELECT id FROM schema_migrations ORDER BY id`) expect(migs.map((m) => m.id)).toContain('001-init') }) it('walks quotation-less invoice → issue (series + due date) → payment → paid, in integer paise', async () => { const staffId = (await db.get<{ id: string }>(`SELECT id FROM staff_user WHERE email='pg@test.in'`))!.id const c = await createClient(db, staffId, { name: 'PG Bank', stateCode: '32' }) const m = await createModule(db, staffId, { code: 'CBS', name: 'Core Banking' }) await setPrice(db, staffId, { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-01-01' }) const draft = await createDraft(db, staffId, { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }], }) const inv = await issueDocument(db, staffId, draft.id) expect(inv.docNo).toMatch(/^INV\//) // series numbering works on pg expect(typeof inv.payablePaise).toBe('number') // bigint arrives as a JS number expect(inv.payablePaise).toBe(11_800_00) // 10,000 + 18% GST, to the paisa expect(inv.dueDate).not.toBeNull() // D18 terms stamp fires on pg too await recordPayment(db, staffId, { clientId: c.id, receivedOn: inv.docDate, mode: 'bank', amountPaise: 11_800_00, allocations: [{ documentId: inv.id, amountPaise: 11_800_00 }], }) expect(await outstandingPaise(db, inv.id)).toBe(0) expect((await getDocument(db, inv.id))!.status).toBe('paid') }) it('reminder upsert is exactly-once via ON CONFLICT', async () => { const c = (await listClients(db))[0]! const first = await upsertReminder(db, { ruleKind: 'follow_up', subjectId: 'pg-x', duePeriod: '2026-07', clientId: c.id }) const second = await upsertReminder(db, { ruleKind: 'follow_up', subjectId: 'pg-x', duePeriod: '2026-07', clientId: c.id }) expect(first.created).toBe(true) expect(second.created).toBe(false) expect(second.id).toBe(first.id) }) it('rolls back atomically, with savepoint-nested inner transactions', async () => { // Outer txn commits; a caught inner failure rolls back only its savepoint. await db.transaction(async () => { await db.run(`INSERT INTO setting (key, value) VALUES ('pg.outer', '1') ON CONFLICT (key) DO NOTHING`) await db.transaction(async () => { await db.run(`INSERT INTO setting (key, value) VALUES ('pg.inner', '1') ON CONFLICT (key) DO NOTHING`) throw new Error('inner boom') }).catch(() => undefined) }) expect(await db.get(`SELECT value FROM setting WHERE key='pg.outer'`)).toBeDefined() expect(await db.get(`SELECT value FROM setting WHERE key='pg.inner'`)).toBeUndefined() // A failing write mid-transaction leaves nothing behind (PATCH-style mixed write). const staffId = (await db.get<{ id: string }>(`SELECT id FROM staff_user WHERE email='pg@test.in'`))!.id const c = (await listClients(db))[0]! await expect(db.transaction(async () => { await setClientDbPassword(db, staffId, c.id, 'secret-pw', '11'.repeat(32)) await updateClient(db, staffId, c.id, { gstin: 'NOT-A-GSTIN' }) // throws })).rejects.toThrow(/GSTIN/i) const row = (await db.get<{ db_password_enc: string | null }>( `SELECT db_password_enc FROM client WHERE id=?`, c.id, ))! expect(row.db_password_enc).toBeNull() // the password write rolled back with it }) it('boots the HTTP server on DATABASE_URL and serves real requests', async () => { const prev = process.env['DATABASE_URL'] process.env['DATABASE_URL'] = URL_ const server = await startServer(0) try { const port = (server.address() as { port: number }).port const base = `http://localhost:${port}/api` const health = await (await fetch(`${base}/health`)).json() as { ok: boolean } expect(health.ok).toBe(true) const auth = await (await fetch(`${base}/auth/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'pg@test.in', password: 'pg-password-1' }), })).json() as { ok: boolean; token: string } expect(auth.ok).toBe(true) const clients = await (await fetch(`${base}/clients`, { headers: { authorization: `Bearer ${auth.token}` }, })).json() as { ok: boolean; clients: unknown[] } expect(clients.ok).toBe(true) expect(clients.clients.length).toBeGreaterThanOrEqual(1) } finally { server.close() if (prev === undefined) delete process.env['DATABASE_URL'] else process.env['DATABASE_URL'] = prev } }, 30_000) })