// apps/hq/test/documents-page.test.ts — Phase 2 (go-live cluster WS-D): // listDocuments pagination with an honest total (rule 8) + GET /documents pass-through. import express from 'express' import { describe, it, expect, afterAll } from 'vitest' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createStaff } from '../src/auth' import { createClient } from '../src/repos-clients' import { createModule, setPrice } from '../src/repos-modules' import { createDraft, issueDocument, listDocuments, markStatus } from '../src/repos-documents' import { clientLedger } from '../src/repos-payments' import { apiRouter } from '../src/api' const KEY = '11'.repeat(32) const fakePdf = async () => Buffer.from('%PDF-fake') async function world() { const db = openDb(':memory:'); await seedIfEmpty(db) const m = await createModule(db, 'u1', { code: 'CORE', name: 'Core Banking' }) await setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 9_000_00, effectiveFrom: '2026-01-01' }) const mkClient = (name: string) => createClient(db, 'u1', { name, stateCode: '32' }) const mkDraft = (clientId: string, docType: 'QUOTATION' | 'PROFORMA' | 'INVOICE' = 'QUOTATION') => createDraft(db, 'u1', { docType, clientId, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }) return { db, m, mkClient, mkDraft } } describe('listDocuments — pagination (rule 8)', () => { it('returns { documents, total, page, pageSize } with defaults page=1 pageSize=50, newest first', async () => { const { db, mkClient, mkDraft } = await world() const c = await mkClient('Acme Bank') const ids: string[] = [] for (let i = 0; i < 55; i++) ids.push((await mkDraft(c.id)).id) const p1 = await listDocuments(db) expect(p1.total).toBe(55) expect(p1.page).toBe(1) expect(p1.pageSize).toBe(50) expect(p1.documents).toHaveLength(50) // newest first — the last-created draft leads the list expect(p1.documents[0]!.id).toBe(ids[54]) const p2 = await listDocuments(db, { page: 2 }) expect(p2.total).toBe(55) // total is stable across pages expect(p2.documents).toHaveLength(5) // no overlap between pages const seen = new Set(p1.documents.map((d) => d.id)) expect(p2.documents.some((d) => seen.has(d.id))).toBe(false) }) it('joins the client name onto every row (no N+1 for the list view)', async () => { const { db, mkClient, mkDraft } = await world() const a = await mkClient('Acme Bank'); const b = await mkClient('Beta Coop') await mkDraft(a.id); await mkDraft(b.id) const page = await listDocuments(db) const names = new Map(page.documents.map((d) => [d.clientId, d.clientName])) expect(names.get(a.id)).toBe('Acme Bank') expect(names.get(b.id)).toBe('Beta Coop') }) it('type/status/clientId filters compose, with an honest filtered total', async () => { const { db, mkClient, mkDraft } = await world() const a = await mkClient('Acme Bank'); const b = await mkClient('Beta Coop') await mkDraft(a.id, 'QUOTATION') const invA = await mkDraft(a.id, 'INVOICE') await issueDocument(db, 'u1', invA.id) await markStatus(db, 'u1', invA.id, 'sent') await mkDraft(b.id, 'INVOICE') // stays draft const filtered = await listDocuments(db, { type: 'INVOICE', clientId: a.id, status: 'sent', pageSize: 1 }) expect(filtered.total).toBe(1) expect(filtered.documents.map((d) => d.id)).toEqual([invA.id]) const draftInvoices = await listDocuments(db, { type: 'INVOICE', status: 'draft' }) expect(draftInvoices.total).toBe(1) expect(draftInvoices.documents[0]!.clientId).toBe(b.id) }) it('clamps pageSize to 200 and floors page/pageSize at 1', async () => { const { db, mkClient, mkDraft } = await world() const c = await mkClient('Acme Bank') for (let i = 0; i < 3; i++) await mkDraft(c.id) expect((await listDocuments(db, { pageSize: 9999 })).pageSize).toBe(200) expect((await listDocuments(db, { pageSize: 0 })).pageSize).toBe(1) expect((await listDocuments(db, { page: 0 })).page).toBe(1) }) it('clientLedger still surfaces EVERY document — the page cap never truncates the ledger', async () => { const { db, mkClient, mkDraft } = await world() const c = await mkClient('Acme Bank') for (let i = 0; i < 55; i++) await mkDraft(c.id) const ledger = await clientLedger(db, c.id) expect(ledger.documents).toHaveLength(55) }) }) // ---------- GET /documents pass-through ---------- async function appWith() { const { db, mkClient, mkDraft } = await world() await createStaff(db, { email: 'staff@test.in', displayName: 'Staff', role: 'staff', password: 'staff-password' }) const app = express(); app.use(express.json()); app.locals['db'] = db const nullFetch = (async () => new Response('{}', { status: 500 })) as typeof fetch app.use('/api', apiRouter(db, { f: nullFetch, clientId: 'cid', clientSecret: 'sec', keyHex: KEY }, fakePdf)) const server = app.listen(0) const baseUrl = `http://localhost:${(server.address() as { port: number }).port}/api` return { db, server, baseUrl, mkClient, mkDraft } } async function login(baseUrl: string) { return (await (await fetch(`${baseUrl}/auth/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'staff@test.in', password: 'staff-password' }), })).json() as { token: string }).token } interface ListJson { ok: boolean; total: number; page: number; pageSize: number documents: { id: string; docType: string; clientName: string }[] } describe('GET /documents — paginated route', () => { const servers: { close: () => void }[] = [] afterAll(() => { for (const s of servers) s.close() }) it('passes page/pageSize through and reports the honest total + clientName rows', async () => { const ctx = await appWith(); servers.push(ctx.server) const c = await ctx.mkClient('Acme Bank') for (let i = 0; i < 5; i++) await ctx.mkDraft(c.id) const token = await login(ctx.baseUrl) const res = await fetch(`${ctx.baseUrl}/documents?page=2&pageSize=2`, { headers: { authorization: `Bearer ${token}` }, }) expect(res.status).toBe(200) const json = await res.json() as ListJson expect(json).toMatchObject({ ok: true, total: 5, page: 2, pageSize: 2 }) expect(json.documents).toHaveLength(2) expect(json.documents[0]!.clientName).toBe('Acme Bank') }) it('composes type + status filters with pagination', async () => { const ctx = await appWith(); servers.push(ctx.server) const c = await ctx.mkClient('Acme Bank') await ctx.mkDraft(c.id, 'QUOTATION') await ctx.mkDraft(c.id, 'INVOICE') await ctx.mkDraft(c.id, 'INVOICE') const token = await login(ctx.baseUrl) const res = await fetch(`${ctx.baseUrl}/documents?type=INVOICE&status=draft&page=1&pageSize=1`, { headers: { authorization: `Bearer ${token}` }, }) const json = await res.json() as ListJson expect(json.total).toBe(2) // both draft invoices counted, one on the page expect(json.documents).toHaveLength(1) expect(json.documents[0]!.docType).toBe('INVOICE') }) })