You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
75 lines
3.6 KiB
TypeScript
75 lines
3.6 KiB
TypeScript
// 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
|
|
})
|
|
})
|