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.
89 lines
3.6 KiB
TypeScript
89 lines
3.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { BillTotals } from '@sims/domain'
|
|
import type { LineInput } from '@sims/billing-engine'
|
|
import { buildQueuedBill, nextProvisionalToken, toOfflineBillBody, type QueuedBill } from '../src/offline'
|
|
|
|
const TOTALS: BillTotals = {
|
|
grossPaise: 14_500, discountPaise: 0, taxablePaise: 12_288,
|
|
cgstPaise: 1_106, sgstPaise: 1_106, igstPaise: 0, cessPaise: 0,
|
|
roundOffPaise: 0, payablePaise: 14_500, savingsVsMrpPaise: 500,
|
|
}
|
|
const LINES: LineInput[] = [{
|
|
itemId: 'i-103', name: 'Surf Excel 1kg', hsn: '3402', qty: 1,
|
|
unitCode: 'PCS', unitPricePaise: 14_500, priceIncludesTax: true, taxClassCode: 'GST18',
|
|
}]
|
|
|
|
describe('S4 offline queue — provisional tokens', () => {
|
|
it('formats OFF-n from the sequence', () => {
|
|
expect(nextProvisionalToken(1)).toBe('OFF-1')
|
|
expect(nextProvisionalToken(42)).toBe('OFF-42')
|
|
})
|
|
})
|
|
|
|
describe('S4 offline queue — buildQueuedBill', () => {
|
|
const base = {
|
|
seq: 2, storeId: 's1', counterId: 'c2', cashierId: 'u1', shiftId: 'sh1',
|
|
businessDate: '2026-07-10', lines: LINES,
|
|
payments: [{ mode: 'CASH', amountPaise: 14_500 }],
|
|
totals: TOTALS, now: 1_700_000_000_000,
|
|
}
|
|
|
|
it('assigns a client uuidv7 id, a matching provisional token, and an ISO createdAt', () => {
|
|
const q = buildQueuedBill(base)
|
|
expect(q.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
expect(q.provisionalToken).toBe('OFF-2')
|
|
expect(q.seq).toBe(2)
|
|
expect(q.createdAt).toBe(new Date(1_700_000_000_000).toISOString())
|
|
expect(q.totals.payablePaise).toBe(14_500)
|
|
})
|
|
|
|
it('omits optional customer / override fields when absent', () => {
|
|
const q = buildQueuedBill(base)
|
|
expect('customerId' in q).toBe(false)
|
|
expect('customerName' in q).toBe(false)
|
|
expect('buyerGstin' in q).toBe(false)
|
|
expect('overrides' in q).toBe(false)
|
|
})
|
|
|
|
it('keeps only the customer id and non-empty overrides — never customer PII (M11)', () => {
|
|
const q = buildQueuedBill({
|
|
...base,
|
|
customerId: 'p1',
|
|
overrides: [{ itemId: 'i-103', kind: 'price', before: 15_000, after: 14_500, approvalId: 'a1' }],
|
|
})
|
|
expect(q.customerId).toBe('p1')
|
|
// M11: the queued record must NOT carry the buyer's name or GSTIN — the server
|
|
// re-resolves both from customerId at drain, so no PII lands on the shared counter PC.
|
|
expect('customerName' in q).toBe(false)
|
|
expect('buyerGstin' in q).toBe(false)
|
|
expect(q.overrides).toHaveLength(1)
|
|
})
|
|
|
|
it('drops an empty overrides array rather than persisting it', () => {
|
|
const q = buildQueuedBill({ ...base, overrides: [] })
|
|
expect('overrides' in q).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('S4 offline queue — toOfflineBillBody', () => {
|
|
it('maps to the drain payload: keeps the client id, sets clientPayablePaise, drops display-only fields', () => {
|
|
const q: QueuedBill = buildQueuedBill({
|
|
seq: 1, storeId: 's1', counterId: 'c2', cashierId: 'u1', shiftId: 'sh1',
|
|
customerId: 'p1',
|
|
businessDate: '2026-07-10', lines: LINES,
|
|
payments: [{ mode: 'CASH', amountPaise: 14_500 }], totals: TOTALS,
|
|
})
|
|
const body = toOfflineBillBody(q)
|
|
expect(body.id).toBe(q.id)
|
|
expect(body.clientPayablePaise).toBe(14_500)
|
|
expect(body.customerId).toBe('p1')
|
|
// SEC fix: cashier/tenant come from the server session, never the body
|
|
expect('cashierId' in body).toBe(false)
|
|
// M11: no customer PII is stored offline, so none can travel in the drain body
|
|
expect('customerName' in body).toBe(false)
|
|
expect('buyerGstin' in body).toBe(false)
|
|
expect('totals' in body).toBe(false)
|
|
expect('provisionalToken' in body).toBe(false)
|
|
})
|
|
})
|