import { describe, it, expect } from 'vitest' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createClient } from '../src/repos-clients' import { createModule, setPrice, assignModule } from '../src/repos-modules' import { createDraft, issueDocument } from '../src/repos-documents' import { recordPayment } from '../src/repos-payments' import { upsertAwsUsage } from '../src/repos-aws' import { duesAging, moduleRevenue, clientProfitability } from '../src/repos-reports' async function issuedInvoice(db: any, clientId: string, moduleId: string, docDate: string) { const inv = await issueDocument(db, 'u1', (await createDraft(db, 'u1', { docType: 'INVOICE', clientId, lines: [{ moduleId, qty: 1, kind: 'yearly' }], })).id) // Rewind to a legacy (pre-due-date) invoice: aging must anchor on doc_date, // so the issue-stamped due_date is cleared along with the rewind. await db.run(`UPDATE document SET doc_date=?, due_date=NULL WHERE id=?`, docDate, inv.id) return inv } async function setup() { const db = openDb(':memory:'); await seedIfEmpty(db) // company.state_code=32, GST18 const c = await createClient(db, 'u1', { name: 'Acme', code: 'ACME', stateCode: '32' }) const pos = await createModule(db, 'u1', { code: 'POS', name: 'POS', allowedKinds: ['yearly'] }) await setPrice(db, 'u1', { moduleId: pos.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-01-01' }) await assignModule(db, 'u1', { clientId: c.id, moduleId: pos.id, kind: 'yearly' }) return { db, c, pos } } describe('duesAging', () => { it('buckets each client outstanding by invoice age', async () => { const { db, c, pos } = await setup() await issuedInvoice(db, c.id, pos.id, '2026-07-01') // ~9 days old on 2026-07-10 → 0-30 await issuedInvoice(db, c.id, pos.id, '2026-05-20') // ~51 days → 31-60 await issuedInvoice(db, c.id, pos.id, '2026-01-01') // >90 days → 90+ const rows = await duesAging(db, '2026-07-10') expect(rows).toHaveLength(1) const r = rows[0]! expect(r.clientId).toBe(c.id) expect(r.b0_30).toBe(11_800_00) // 10,000 + 18% GST expect(r.b31_60).toBe(11_800_00) expect(r.b90p).toBe(11_800_00) expect(r.totalPaise).toBe(35_400_00) }) it('drops fully-settled invoices', async () => { const { db, c, pos } = await setup() const inv = await issuedInvoice(db, c.id, pos.id, '2026-07-01') await recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-05', mode: 'bank', amountPaise: 11_800_00, allocations: [{ documentId: inv.id, amountPaise: 11_800_00 }] }) expect(await duesAging(db, '2026-07-10')).toHaveLength(0) }) }) describe('moduleRevenue', () => { it('reports billed and settled per module, settlement split pro-rata', async () => { const { db, c, pos } = await setup() const inv = await issuedInvoice(db, c.id, pos.id, '2026-07-01') await recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-05', mode: 'bank', amountPaise: 5_900_00, allocations: [{ documentId: inv.id, amountPaise: 5_900_00 }] }) const rows = await moduleRevenue(db) expect(rows).toHaveLength(1) expect(rows[0]!.moduleCode).toBe('POS') expect(rows[0]!.billedPaise).toBe(11_800_00) expect(rows[0]!.settledPaise).toBe(5_900_00) }) it('honours a date range', async () => { const { db, c, pos } = await setup() await issuedInvoice(db, c.id, pos.id, '2026-04-01') await issuedInvoice(db, c.id, pos.id, '2026-07-01') expect((await moduleRevenue(db, { from: '2026-07-01', to: '2026-07-31' }))[0]!.billedPaise).toBe(11_800_00) }) }) describe('clientProfitability', () => { it('reports billed, settled, aws cost and margin per client', async () => { const { db, c, pos } = await setup() const inv = await issuedInvoice(db, c.id, pos.id, '2026-07-01') await recordPayment(db, 'u1', { clientId: c.id, receivedOn: '2026-07-05', mode: 'bank', amountPaise: 11_800_00, allocations: [{ documentId: inv.id, amountPaise: 11_800_00 }] }) await upsertAwsUsage(db, 'u1', { clientId: c.id, month: '2026-07', costPaise: 1_000_00 }) const rows = await clientProfitability(db, { from: '2026-07-01', to: '2026-07-31' }) expect(rows).toHaveLength(1) expect(rows[0]!).toMatchObject({ billedPaise: 11_800_00, settledPaise: 11_800_00, awsCostPaise: 1_000_00, marginPaise: 10_800_00, }) }) })