// apps/hq/test/sms-rate-card.test.ts — D23: SMS volume-band pricing, money locked to the paisa. import { describe, it, expect } from 'vitest' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createClient } from '../src/repos-clients' import { createModule } from '../src/repos-modules' import { resolveUsageRate, setUsageRateCard, listUsageRateCards } from '../src/repos-rate-card' import { createDraft, issueDocument, getDocument, prepareDraft } from '../src/repos-documents' // The founder's real card: ₹0.40 / 0.37 / 0.34 / 0.30 per SMS = 40/37/34/30 paise. const BANDS = [ { minQty: 50_000, ratePaise: 40 }, { minQty: 100_000, ratePaise: 37 }, { minQty: 200_000, ratePaise: 34 }, { minQty: 300_000, ratePaise: 30 }, ] 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 sms = await createModule(db, 'u1', { code: 'SMS', name: 'Bulk SMS', allowedKinds: ['usage'] }) await setUsageRateCard(db, 'u1', sms.id, '2026-04-01', BANDS) return { db, c, sms } } describe('SMS volume-band resolution (D23)', () => { it('picks the band by count and enforces the minimum order', async () => { const { db, sms } = await world() const rate = async (n: number) => resolveUsageRate(db, sms.id, n, '2026-07-16') expect(await rate(50_000)).toEqual({ kind: 'rate', ratePaise: 40 }) expect(await rate(75_000)).toEqual({ kind: 'rate', ratePaise: 40 }) // in [50k,100k) expect(await rate(100_000)).toEqual({ kind: 'rate', ratePaise: 37 }) expect(await rate(150_000)).toEqual({ kind: 'rate', ratePaise: 37 }) expect(await rate(200_000)).toEqual({ kind: 'rate', ratePaise: 34 }) expect(await rate(300_000)).toEqual({ kind: 'rate', ratePaise: 30 }) expect(await rate(500_000)).toEqual({ kind: 'rate', ratePaise: 30 }) // >= top band expect(await rate(49_999)).toEqual({ kind: 'below_min', minQty: 50_000 }) // A module with no card is not usage-tiered. const other = await createModule(db, 'u1', { code: 'X', name: 'X' }) expect(await resolveUsageRate(db, other.id, 100_000, '2026-07-16')).toBeNull() }) it('resolves by business date — an older card stays in force before the new one', async () => { const { db, sms } = await world() await setUsageRateCard(db, 'u1', sms.id, '2027-04-01', [{ minQty: 50_000, ratePaise: 45 }]) expect(await resolveUsageRate(db, sms.id, 50_000, '2026-12-31')).toEqual({ kind: 'rate', ratePaise: 40 }) expect(await resolveUsageRate(db, sms.id, 50_000, '2027-05-01')).toEqual({ kind: 'rate', ratePaise: 45 }) }) }) describe('SMS billing through the engine (money locked)', () => { const cases = [ { count: 50_000, taxable: 20_000_00, total: 23_600_00 }, { count: 100_000, taxable: 37_000_00, total: 43_660_00 }, { count: 200_000, taxable: 68_000_00, total: 80_240_00 }, { count: 300_000, taxable: 90_000_00, total: 1_06_200_00 }, ] for (const tc of cases) { it(`${tc.count.toLocaleString('en-IN')} SMS → ₹${tc.taxable / 100} + 18% GST = ₹${tc.total / 100}`, async () => { const { db, c, sms } = await world() const draft = await createDraft(db, 'u1', { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: sms.id, qty: tc.count, kind: 'usage' }], }) const inv = await issueDocument(db, 'u1', draft.id) const full = (await getDocument(db, inv.id))! expect(full.taxablePaise).toBe(tc.taxable) expect(full.payablePaise).toBe(tc.total) // Intra-state (client 32 = company 32): CGST + SGST split, IGST zero. expect(full.cgstPaise + full.sgstPaise).toBe(tc.total - tc.taxable) expect(full.igstPaise).toBe(0) }) } it('a save below the minimum order is refused; preview warns instead of throwing', async () => { const { db, c, sms } = await world() await expect(createDraft(db, 'u1', { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: sms.id, qty: 10_000, kind: 'usage' }], })).rejects.toThrow(/minimum order is 50,000/) const prev = await prepareDraft(db, { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: sms.id, qty: 10_000, kind: 'usage' }], }, { permissive: true }) expect(prev.warnings.some((w) => /minimum/.test(w))).toBe(true) expect(prev.totals.payablePaise).toBe(0) }) it('an explicit per-line unit price overrides the card (special deal)', async () => { const { db, c, sms } = await world() const draft = await createDraft(db, 'u1', { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: sms.id, qty: 100_000, kind: 'usage', unitPricePaise: 35 }], // ₹0.35 special }) const inv = await issueDocument(db, 'u1', draft.id) expect((await getDocument(db, inv.id))!.taxablePaise).toBe(35_000_00) // 100k × 0.35, not the 0.37 band }) it('lists dated cards newest first', async () => { const { db, sms } = await world() const cards = await listUsageRateCards(db, sms.id) expect(cards).toHaveLength(1) expect(cards[0]!.bands).toHaveLength(4) expect(cards[0]!.effectiveFrom).toBe('2026-04-01') }) })