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/pos/test/offline.test.ts

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)
})
})