import { describe, expect, it } from 'vitest' import { openDb, type DB } from '../src/db' import { seedIfEmpty } from '../src/seed' import { commitBill } from '../src/repos' /** * M5 (red-team): commitBill's store/counter lookup is tenant-scoped — `WHERE id=? AND tenant_id=?` * for both — and an unknown-for-tenant store or counter is rejected 400 (E-1110) BEFORE any money * logic, which also removes the old undefined-deref 500. (E-1110, not the task's suggested E-1109, * because E-1109 is already the "payments do not sum to payable" code — reusing it would be * ambiguous.) The /bills and /bills/offline routes both funnel through commitBill, so both paths * are covered. Hermetic in-memory DB; Carry Bag (108) has master sale 500, no MRP, GST18. */ function fresh(): DB { const db = openDb(':memory:') seedIfEmpty(db) return db } const carryBag = (db: DB): string => (db.prepare("SELECT id FROM item WHERE tenant_id='t1' AND code='108'").get() as { id: string }).id const billCount = (db: DB): number => (db.prepare('SELECT COUNT(*) n FROM bill').get() as { n: number }).n const base = (db: DB): Omit[1], 'storeId'> => ({ tenantId: 't1', counterId: 'c2', cashierId: 'u1', shiftId: 'sh1', businessDate: '2026-07-10', lines: [{ itemId: carryBag(db), name: 'Carry Bag', hsn: '3923', qty: 1, unitCode: 'PCS', unitPricePaise: 500, priceIncludesTax: true, taxClassCode: 'GST18' }], payments: [{ mode: 'CASH', amountPaise: 500 }], clientPayablePaise: 500, }) const thrown = (fn: () => unknown): (Error & { status?: number; code?: string }) | undefined => { try { fn(); return undefined } catch (e) { return e as Error & { status?: number; code?: string } } } describe('M5 — commitBill store/counter are tenant-scoped', () => { it('a nonexistent storeId is rejected 400 (E-1110) — no 500, nothing committed', () => { const db = fresh() const err = thrown(() => commitBill(db, { ...base(db), storeId: 'does-not-exist' })) expect(err).toBeDefined() expect(err!.status).toBe(400) expect(err!.code).toBe('E-1110') expect(billCount(db)).toBe(0) }) it('a nonexistent counterId is rejected 400 (E-1110)', () => { const db = fresh() const err = thrown(() => commitBill(db, { ...base(db), storeId: 's1', counterId: 'nope' })) expect(err!.status).toBe(400) expect(err!.code).toBe('E-1110') expect(billCount(db)).toBe(0) }) it('a store/counter belonging to ANOTHER tenant is rejected (true cross-tenant scoping)', () => { const db = fresh() db.prepare("INSERT INTO tenant (id, code, name, gst_scheme, fy_start_month, created_at) VALUES ('t2','T2','Other','regular',4,?)").run(new Date().toISOString()) db.prepare("INSERT INTO store (id, tenant_id, code, name, state_code) VALUES ('s2','t2','ST9','Other Store','29')").run() db.prepare("INSERT INTO counter (id, tenant_id, store_id, code, name) VALUES ('cc2','t2','s2','C1','C')").run() // commit as tenant t1 but point at t2's store/counter → must reject, never bill against t2's store const err = thrown(() => commitBill(db, { ...base(db), storeId: 's2', counterId: 'cc2' })) expect(err!.code).toBe('E-1110') expect(billCount(db)).toBe(0) }) it('the honest same-tenant store/counter still commits (4-digit-FY doc no)', () => { const db = fresh() const out = commitBill(db, { ...base(db), storeId: 's1' }) expect(out.docNo).toMatch(/^ST1C22026-/) expect(out.docNo.length).toBeLessThanOrEqual(16) expect(billCount(db)).toBe(1) }) })