// apps/hq/test/functional-d26.test.ts — D26: GST summary, document search, bulk ops. import { describe, it, expect } from 'vitest' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createClient, bulkUpdateClients, getClient } from '../src/repos-clients' import { createModule, setPrice, assignModule, getClientModule } from '../src/repos-modules' import { createDraft, issueDocument, createCreditNote, listDocuments, getDocument } from '../src/repos-documents' import { gstSummary } from '../src/repos-reports' import { listProjectMilestones, bulkSetMilestone, setMilestone } from '../src/repos-milestones' async function world() { const db = openDb(':memory:'); await seedIfEmpty(db) // company state 32, GST18 const c = await createClient(db, 'u1', { name: 'Karapuzha SCB', stateCode: '32' }) const m = await createModule(db, 'u1', { code: 'CORE', name: 'Core', allowedKinds: ['yearly'] }) await setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-01-01' }) return { db, c, m } } const mkInvoice = async (db: any, c: any, m: any, date: string) => { const inv = await issueDocument(db, 'u1', (await createDraft(db, 'u1', { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }], })).id) await db.run(`UPDATE document SET doc_date=? WHERE id=?`, date, inv.id) return inv } describe('GST summary (D26, money locked)', () => { it('sums invoices per month; credit notes subtract; CGST+SGST split exact', async () => { const { db, c, m } = await world() await mkInvoice(db, c, m, '2026-07-05') // ₹10,000 + 18% = 11,800; CGST 900 SGST 900 const inv2 = await mkInvoice(db, c, m, '2026-07-20') await mkInvoice(db, c, m, '2026-08-01') // A full, ISSUED credit note against inv2 (same month) reverses one invoice's tax. const cnDraft = await createCreditNote(db, 'u1', inv2.id) await issueDocument(db, 'u1', cnDraft.id) // a CN counts for GST only once numbered await db.run(`UPDATE document SET doc_date='2026-07-25' WHERE id=?`, cnDraft.id) void getDocument const { rows, total } = await gstSummary(db, { from: '2026-07-01', to: '2026-08-31' }) const jul = rows.find((r) => r.period === '2026-07')! expect(jul).toMatchObject({ invoiceCount: 2, creditNoteCount: 1 }) // 2 invoices − 1 credit note = net 1 invoice's worth in July. expect(jul.taxablePaise).toBe(10_000_00) expect(jul.cgstPaise).toBe(900_00) expect(jul.sgstPaise).toBe(900_00) expect(jul.taxPaise).toBe(1_800_00) expect(jul.totalPaise).toBe(11_800_00) const aug = rows.find((r) => r.period === '2026-08')! expect(aug.totalPaise).toBe(11_800_00) expect(total.totalPaise).toBe(23_600_00) // net July (1) + Aug (1) expect(total.taxPaise).toBe(3_600_00) }) }) describe('document search (D26)', () => { it('finds documents by number and by client name, case-insensitive', async () => { const { db, c, m } = await world() const inv = await mkInvoice(db, c, m, '2026-07-05') expect((await listDocuments(db, { q: inv.docNo!.toLowerCase() })).total).toBe(1) expect((await listDocuments(db, { q: 'KARAPUZHA' })).total).toBe(1) expect((await listDocuments(db, { q: 'nomatch' })).total).toBe(0) expect((await listDocuments(db, {})).total).toBe(1) // no q = all }) }) describe('bulk operations (D26)', () => { it('bulk-updates client fields in one call, each audited', async () => { const { db } = await world() const a = await createClient(db, 'u1', { name: 'A', stateCode: '32' }) const b = await createClient(db, 'u1', { name: 'B', stateCode: '32' }) const out = await bulkUpdateClients(db, 'u1', [a.id, b.id], { district: 'Ernakulam' }) expect(out.updated).toBe(2) expect((await getClient(db, a.id))!.district).toBe('Ernakulam') expect((await getClient(db, b.id))!.district).toBe('Ernakulam') }) it('bulk-ticks a milestone across projects', async () => { const { db, c, m } = await world() const p = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) const out = await bulkSetMilestone(db, 'u1', [p.id], 'installed', true, '2026-07-10') expect(out.updated).toBe(1) const ms = await listProjectMilestones(db, p.id) expect(ms.find((x) => x.key === 'installed')).toMatchObject({ done: true, doneOn: '2026-07-10' }) }) // Pre-ship audit FIX F: bulkSetMilestone used a bare UPDATE, so it never drove // client_module.status (a bulk go_live left status at 'quoted') and never cleared a stale // payment/document link on untick. It now routes through setMilestone — the same path a // single tick uses. it('bulk-tick drives client_module.status the same as a single tick (FIX F)', async () => { const { db, c, m } = await world() const p = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) const out = await bulkSetMilestone(db, 'u1', [p.id], 'go_live', true, '2026-07-10') expect(out.updated).toBe(1) const cm = await getClientModule(db, p.id) expect(cm!.status).toBe('live') }) it('bulk-untick clears a stale payment/document link, same as a single untick (FIX F)', async () => { const { db, c, m } = await world() const p = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) const paymentId = 'pay-bulk-1' await db.run( `INSERT INTO payment (id, client_id, received_on, mode, amount_paise, created_by, created_at) VALUES (?, ?, '2026-07-01', 'bank', 500000, 'u1', '2026-07-01T00:00:00.000Z')`, paymentId, c.id, ) // Single-tick with a linked payment (as the UI does)... await setMilestone(db, 'u1', p.id, 'advance_paid', true, '2026-07-01', paymentId) let ms = await listProjectMilestones(db, p.id) expect(ms.find((x) => x.key === 'advance_paid')!.paymentId).toBe(paymentId) // ...then a bulk untick must clear that link, not leave it stale. const out = await bulkSetMilestone(db, 'u1', [p.id], 'advance_paid', false) expect(out.updated).toBe(1) ms = await listProjectMilestones(db, p.id) const advancePaid = ms.find((x) => x.key === 'advance_paid')! expect(advancePaid.done).toBe(false) expect(advancePaid.doneOn).toBeNull() expect(advancePaid.paymentId).toBeNull() }) it('bulk ops only touch projects that actually have the milestone key, others are silently skipped', async () => { const { db, c, m } = await world() const p = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) const out = await bulkSetMilestone(db, 'u1', [p.id, 'not-a-real-client-module-id'], 'go_live', true, '2026-07-10') expect(out.updated).toBe(1) // only the real one counted }) })