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.
sims-hq/apps/hq/test/money-guards.test.ts

58 lines
2.7 KiB
TypeScript

// apps/hq/test/money-guards.test.ts — D24 (red-team): money-integrity guards.
import { describe, it, expect } from 'vitest'
import { openDb } from '../src/db'
import { seedIfEmpty } from '../src/seed'
import { createClient } from '../src/repos-clients'
import { createModule, setPrice } from '../src/repos-modules'
import { createDraft, issueDocument, createCreditNote, markStatus, getDocument } from '../src/repos-documents'
import { recordPayment } from '../src/repos-payments'
async function world() {
const db = openDb(':memory:'); await seedIfEmpty(db)
const c = await createClient(db, 'u1', { name: 'Bank', 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' })
const inv = await issueDocument(db, 'u1', (await createDraft(db, 'u1', {
docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }],
})).id)
return { db, c, m, inv }
}
describe('credit-note cap (D24)', () => {
it('a full-value CN succeeds; a second CN on the same invoice is refused (would exceed it)', async () => {
const { db, inv } = await world()
const cn1 = await createCreditNote(db, 'u1', inv.id) // full value
expect(cn1.payablePaise).toBe(inv.payablePaise)
await expect(createCreditNote(db, 'u1', inv.id)).rejects.toThrow(/exceeds the invoice/i)
})
})
describe('markStatus type guard (D24)', () => {
it('an issued invoice cannot be marked accepted/lost', async () => {
const { db, inv } = await world()
await expect(markStatus(db, 'u1', inv.id, 'lost')).rejects.toThrow(/Only quotations/i)
await expect(markStatus(db, 'u1', inv.id, 'accepted')).rejects.toThrow(/Only quotations/i)
// The invoice keeps its real status — still visible to overdue chasing.
expect((await getDocument(db, inv.id))!.status).not.toBe('lost')
})
})
describe('integer quantity guard (D24)', () => {
it('a fractional / non-integer line quantity is rejected at draft', async () => {
const { db, c, m } = await world()
await expect(createDraft(db, 'u1', {
docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: m.id, qty: 1.5, kind: 'yearly' }],
})).rejects.toThrow(/whole number/i)
})
})
describe('TDS bound (D24)', () => {
it('TDS greater than the cash received is refused', async () => {
const { db, c, inv } = await world()
await expect(recordPayment(db, 'u1', {
clientId: c.id, receivedOn: '2026-07-10', mode: 'bank', amountPaise: 1_000_00, tdsPaise: 2_000_00,
allocations: [{ documentId: inv.id, amountPaise: 3_000_00 }],
})).rejects.toThrow(/TDS cannot exceed/i)
})
})