// apps/hq/test/documents.test.ts import { describe, it, expect } from 'vitest' import { openDb } from '../src/db' import { createClient } from '../src/repos-clients' import { createModule, setPrice } from '../src/repos-modules' import { createDraft, issueDocument, convertDocument, createCreditNote, cancelDocument } from '../src/repos-documents' function setup() { const db = openDb(':memory:') db.prepare(`INSERT INTO setting (key, value) VALUES ('company.state_code','32')`).run() db.prepare(`INSERT INTO tax_class (class_code, rate_pct_bp, effective_from) VALUES ('GST18', 1800, '2017-07-01')`).run() const c = createClient(db, 'u1', { name: 'Acme', stateCode: '32' }) const m = createModule(db, 'u1', { code: 'POS', name: 'POS Billing' }) setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-04-01' }) return { db, c, m } } describe('documents', () => { it('quotation: ₹10,000 + 18% intra-state = CGST 900 + SGST 900, payable ₹11,800', () => { const { db, c, m } = setup() const d = createDraft(db, 'u1', { docType: 'QUOTATION', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }) expect(d.taxablePaise).toBe(10_000_00) expect(d.cgstPaise).toBe(900_00) expect(d.sgstPaise).toBe(900_00) expect(d.igstPaise).toBe(0) expect(d.payablePaise).toBe(11_800_00) expect(d.status).toBe('draft') expect(d.docNo).toBeNull() }) it('inter-state client gets IGST', () => { const { db, m } = setup() const db2 = db const kar = createClient(db2, 'u1', { name: 'BLR Co', stateCode: '29' }) const d = createDraft(db2, 'u1', { docType: 'INVOICE', clientId: kar.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }) expect(d.igstPaise).toBe(1_800_00) expect(d.cgstPaise).toBe(0) }) it('issue assigns a series number; convert QT→INV carries lines and links back', () => { const { db, c, m } = setup() const qt = createDraft(db, 'u1', { docType: 'QUOTATION', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }) const issued = issueDocument(db, 'u1', qt.id) expect(issued.docNo).toMatch(/^QT\/\d{2}-\d{2}-\d{4}$/) const inv = convertDocument(db, 'u1', qt.id, 'INVOICE') expect(inv.refDocId).toBe(qt.id) expect(inv.payablePaise).toBe(qt.payablePaise) }) it('line contents default to the module quote content, and can be overridden per line', () => { const { db, c } = setup() const sms = createModule(db, 'u1', { code: 'SMS', name: 'SMS Gateway', quoteContent: ['Bulk SMS gateway', 'DLT registration'], }) setPrice(db, 'u1', { moduleId: sms.id, kind: 'yearly', pricePaise: 5_000_00, effectiveFrom: '2026-04-01' }) const d = createDraft(db, 'u1', { docType: 'QUOTATION', clientId: c.id, lines: [{ moduleId: sms.id, qty: 1, kind: 'yearly' }] }) expect(d.payload.lineContents).toEqual([['Bulk SMS gateway', 'DLT registration']]) const d2 = createDraft(db, 'u1', { docType: 'QUOTATION', clientId: c.id, lines: [{ moduleId: sms.id, qty: 1, kind: 'yearly', contentLines: ['Custom scope only'] }] }) expect(d2.payload.lineContents).toEqual([['Custom scope only']]) }) it('a module without quote content contributes an empty content array', () => { const { db, c, m } = setup() const d = createDraft(db, 'u1', { docType: 'QUOTATION', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }) expect(d.payload.lineContents).toEqual([[]]) }) it('a pack (non-standard edition) prices from that edition and prints the pack name on the line', () => { const { db, c, m } = setup() setPrice(db, 'u1', { moduleId: m.id, edition: 'SMS-50K', kind: 'yearly', pricePaise: 5_000_00, effectiveFrom: '2026-04-01' }) const d = createDraft(db, 'u1', { docType: 'QUOTATION', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly', edition: 'SMS-50K' }] }) expect(d.taxablePaise).toBe(5_000_00) expect(d.payload.lines[0]!.name).toContain('— SMS-50K') const std = createDraft(db, 'u1', { docType: 'QUOTATION', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }) expect(std.payload.lines[0]!.name).not.toContain('—') }) it('credit note defaults to full value against the invoice; cancel keeps the number', () => { const { db, c, m } = setup() const inv = issueDocument(db, 'u1', createDraft(db, 'u1', { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }).id) const cn = createCreditNote(db, 'u1', inv.id) expect(cn.docType).toBe('CREDIT_NOTE') expect(cn.payablePaise).toBe(inv.payablePaise) const cancelled = cancelDocument(db, 'u1', inv.id) expect(cancelled.status).toBe('cancelled') expect(cancelled.docNo).toBe(inv.docNo) // number consumed, never reused }) })