feat(sms): bulk SMS purchase proforma + document source tagging (renewal vs top-up)
Threads an optional `source` through DraftInput/insertDocRow (default 'hq', preserving current behaviour when omitted), tags module renewals as 'module_renewal', and adds generateBulkSmsPurchaseQuote — an ad-hoc bulk SMS credit top-up priced via the existing usage rate card, tagged 'bulk_sms_topup' so the client-detail document list can distinguish it from a subscription renewal. New route: POST /client-modules/:id/bulk-sms-quote. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/client-detail-redesign
parent
fdf31b4dbd
commit
195d821bef
@ -0,0 +1,74 @@
|
||||
// apps/hq/test/bulk-sms-purchase.test.ts — Task 7: ad-hoc bulk-SMS top-up documents,
|
||||
// tagged distinctly from subscription renewals via document.source.
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { openDb } from '../src/db'
|
||||
import { seedIfEmpty } from '../src/seed'
|
||||
import { createClient } from '../src/repos-clients'
|
||||
import { createModule, assignModule, setPrice, updateClientModule } from '../src/repos-modules'
|
||||
import { setUsageRateCard } from '../src/repos-rate-card'
|
||||
import { generateModuleRenewalQuote, generateBulkSmsPurchaseQuote } from '../src/repos-renewals'
|
||||
import { createDraft, getDocument } 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)
|
||||
const cm = await assignModule(db, 'u1', { clientId: c.id, moduleId: sms.id, kind: 'usage' })
|
||||
return { db, c, sms, cm }
|
||||
}
|
||||
|
||||
describe('generateBulkSmsPurchaseQuote', () => {
|
||||
it('raises a proforma tagged bulk_sms_topup for the given SMS quantity', async () => {
|
||||
const { db, cm } = await world()
|
||||
const doc = await generateBulkSmsPurchaseQuote(db, 'u1', cm.id, 100_000)
|
||||
expect(doc.docType).toBe('PROFORMA')
|
||||
expect(doc.source).toBe('bulk_sms_topup')
|
||||
expect(doc.payload.lines[0]!.qty).toBe(100_000)
|
||||
expect(doc.payablePaise).toBeGreaterThan(0)
|
||||
// Persisted correctly — a re-fetch from the DB carries the same tag.
|
||||
const full = (await getDocument(db, doc.id))!
|
||||
expect(full.source).toBe('bulk_sms_topup')
|
||||
})
|
||||
|
||||
it('rejects a non-positive or fractional quantity', async () => {
|
||||
const { db, cm } = await world()
|
||||
await expect(generateBulkSmsPurchaseQuote(db, 'u1', cm.id, 0)).rejects.toThrow()
|
||||
await expect(generateBulkSmsPurchaseQuote(db, 'u1', cm.id, -5)).rejects.toThrow()
|
||||
await expect(generateBulkSmsPurchaseQuote(db, 'u1', cm.id, 1.5)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('rejects an unknown client module', async () => {
|
||||
const { db } = await world()
|
||||
await expect(generateBulkSmsPurchaseQuote(db, 'u1', 'nope', 100_000)).rejects.toThrow(/not found/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('document.source tagging', () => {
|
||||
it('tags a renewal proforma module_renewal, and a manual draft keeps the default', async () => {
|
||||
const { db, c } = await world()
|
||||
// A separate yearly-priced module for the renewal path (usage modules enforce a
|
||||
// minimum-order qty that a qty:1 renewal line would fail).
|
||||
const yearly = await createModule(db, 'u1', { code: 'CORE', name: 'Core App', allowedKinds: ['yearly'] })
|
||||
await setPrice(db, 'u1', { moduleId: yearly.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-01-01' })
|
||||
const cm2 = await assignModule(db, 'u1', { clientId: c.id, moduleId: yearly.id, kind: 'yearly' })
|
||||
await updateClientModule(db, 'u1', cm2.id, { nextRenewal: '2026-08-15' })
|
||||
const renewal = await generateModuleRenewalQuote(db, 'u1', cm2.id)
|
||||
expect(renewal.source).toBe('module_renewal')
|
||||
|
||||
const manual = await createDraft(db, 'u1', {
|
||||
docType: 'PROFORMA', clientId: c.id,
|
||||
lines: [{ moduleId: yearly.id, qty: 1, kind: 'yearly' }],
|
||||
})
|
||||
expect(manual.source).toBe('hq') // default preserved when source is omitted
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue