import { describe, expect, it } from 'vitest' import { openDb, type DB } from '../src/db' import { seedIfEmpty } from '../src/seed' import { commitBill, createOverrideApproval } from '../src/repos' /** * SEC-2 / H4: supervisor-override approvals are server-minted, single-use tokens BOUND to * the exact override they authorise. The client sends an `approvalId`, never an approver * identity — commitBill re-detects the deviation from the item master, redeems the token * only if item/kind/before/after match, and reads the approver from its own row. These * tests drive commitBill against a hermetic in-memory DB (seed users: u1 Ramesh/cashier, * u2 Suresh/supervisor). Surf Excel (103) has master sale 14500, MRP 15000. */ function fresh(): DB { const db = openDb(':memory:') seedIfEmpty(db) return db } const surfId = (db: DB): string => (db.prepare("SELECT id FROM item WHERE tenant_id='t1' AND code='103'").get() as { id: string }).id /** A bill that sells Surf Excel at 12000 (a deviation from the 14500 master), overridden. */ const billWith = (db: DB, approvalId: string): Parameters[1] => { const itemId = surfId(db) return { tenantId: 't1', storeId: 's1', counterId: 'c2', cashierId: 'u1', shiftId: 'sh1', businessDate: '2026-07-10', lines: [{ itemId, name: 'Surf Excel 1kg', hsn: '3402', qty: 1, unitCode: 'PCS', unitPricePaise: 12_000, priceIncludesTax: true, taxClassCode: 'GST18' }], payments: [{ mode: 'CASH', amountPaise: 12_000 }], clientPayablePaise: 12_000, overrides: [{ itemId, kind: 'price', before: 14_500, after: 12_000, approvalId }], } } const priceCtx = (db: DB) => ({ itemId: surfId(db), kind: 'price' as const, beforePaise: 14_500, afterPaise: 12_000 }) const billCount = (db: DB): number => (db.prepare('SELECT COUNT(*) n FROM bill').get() as { n: number }).n describe('SEC-2 / H4 bound override approval tokens', () => { it('rejects a commit whose override carries a fabricated approvalId — nothing is committed', () => { const db = fresh() expect(() => commitBill(db, billWith(db, '00000000-0000-7000-8000-000000000000'))).toThrow(/approval/i) expect(billCount(db)).toBe(0) }) it('accepts a genuine bound token, audits the SERVER-side approver, and rejects a replay', () => { const db = fresh() const approvalId = createOverrideApproval(db, 't1', 'u2', priceCtx(db)) // Suresh (supervisor) const out = commitBill(db, billWith(db, approvalId)) expect(out.docNo).toMatch(/^ST1C22026-/) // 4-digit-FY prefix (M9) const audit = db.prepare("SELECT after_json FROM audit_log WHERE action='PRICE_OVERRIDE'").get() as { after_json: string } const after = JSON.parse(audit.after_json) as { approvedBy: string; approvedByUserId: string } expect(after.approvedByUserId).toBe('u2') // read from the server's own row, not the client expect(after.approvedBy).toBe('Suresh') // token is single-use: a replay is rejected and no second bill is written expect(() => commitBill(db, billWith(db, approvalId))).toThrow(/already used/i) expect(billCount(db)).toBe(1) }) it('rejects a token minted for a non-approver role (a cashier)', () => { const db = fresh() const cashierToken = createOverrideApproval(db, 't1', 'u1', priceCtx(db)) // u1 is a cashier expect(() => commitBill(db, billWith(db, cashierToken))).toThrow(/not authorised/i) expect(billCount(db)).toBe(0) }) it('rejects an override with no approvalId at all (deviation unauthorised)', () => { const db = fresh() const itemId = surfId(db) const input = billWith(db, 'placeholder') input.overrides = [{ itemId, kind: 'price', before: 14_500, after: 12_000 }] expect(() => commitBill(db, input)).toThrow(/supervisor approval|E-1302/i) expect(billCount(db)).toBe(0) }) it('H4: a token minted for item A cannot be redirected to item B', () => { const db = fresh() // Mint a genuine supervisor token bound to a DIFFERENT item (105 Maggi 14000→10000)... const maggiId = (db.prepare("SELECT id FROM item WHERE tenant_id='t1' AND code='105'").get() as { id: string }).id const strayToken = createOverrideApproval(db, 't1', 'u2', { itemId: maggiId, kind: 'price', beforePaise: 14_000, afterPaise: 10_000 }) // ...and try to spend it on the Surf Excel deviation. The bound item mismatches → reject. expect(() => commitBill(db, billWith(db, strayToken))).toThrow(/does not match|not recognised|approval/i) expect(billCount(db)).toBe(0) }) })