// S7 load verification (docs/17 §S7, R25) — scripted load against ONE store-server. // // Fires 2,500 bill commits distributed across 10 counters at a concurrency of 10 (one // in-flight request per counter, realistic for a single store-server), then a burst // phase (100 bills at concurrency 50) to find the knee. Measures wall-clock, throughput, // and p50/p95/p99/max latency, then runs six integrity checks directly against the load // DB with better-sqlite3 and prints a pass/fail table. // // Plain node (node 22, global fetch). The only imports are better-sqlite3 and esbuild's // absence — no engine bundle is needed because every billable seed item is priced // tax-inclusive with no discount, so the payable is exactly roundToRupee(Σ qty·price) // (billing-engine compute.ts: priceIncludesTax ⇒ lineTotal = net = round(price·qty); // payable = round(Σ lineTotal / 100)·100 — independent of the tax rate). The server // re-verifies every bill to the paisa with the real shared engine (R5) and rejects any // mismatch, so a wrong client payable would surface immediately as failed commits. import Database from 'better-sqlite3' import os from 'node:os' import path from 'node:path' import process from 'node:process' const BASE = process.env.BASE ?? 'http://localhost:5281' const DATA_DIR = process.env.SIMS_DATA_DIR if (!DATA_DIR) { console.error('Set SIMS_DATA_DIR to the load target data dir'); process.exit(1) } const DB_PATH = path.join(DATA_DIR, 'sims.db') const TENANT = 't1' const STORE = 's1' const BUSINESS_DATE = '2026-07-10' const N_COUNTERS = 10 const TOTAL_BILLS = 2500 const BURST_BILLS = 100 const BURST_CONC = 50 // The 4 seeded users (sessions are per-token, so reusing them across counters is fine). const USERS = [ { id: 'u1', pin: '4728' }, { id: 'u2', pin: '8265' }, { id: 'u3', pin: '9174' }, { id: 'u4', pin: '3591' }, ] const COUNTERS = Array.from({ length: N_COUNTERS }, (_, i) => ({ id: `c${i + 1}`, code: `C${i + 1}`, })) const rng = (n) => Math.floor(Math.random() * n) const pct = (arr, p) => { if (arr.length === 0) return 0 const s = [...arr].sort((a, b) => a - b) const idx = Math.min(s.length - 1, Math.ceil((p / 100) * s.length) - 1) return s[Math.max(0, idx)] } const round2 = (n) => Math.round(n * 100) / 100 // ---- setup: ensure 10 real counters (seed makes only C1/C2) ---- function ensureCounters() { const db = new Database(DB_PATH) db.pragma('busy_timeout = 5000') const ins = db.prepare( `INSERT OR IGNORE INTO counter (id, tenant_id, store_id, code, name) VALUES (?, ?, ?, ?, ?)`, ) const tx = db.transaction(() => { for (const c of COUNTERS) ins.run(c.id, TENANT, STORE, c.code, `Counter ${c.code.slice(1)}`) }) tx() const n = db.prepare(`SELECT COUNT(*) n FROM counter WHERE tenant_id=? AND store_id=?`).get(TENANT, STORE).n db.close() return n } async function login(user) { const r = await fetch(`${BASE}/api/auth/pin`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: user.id, pin: user.pin }), }) if (!r.ok) throw new Error(`login ${user.id} failed: ${r.status} ${await r.text()}`) return (await r.json()).token } async function loadCatalog(token) { const r = await fetch(`${BASE}/api/items?all=1`, { headers: { Authorization: `Bearer ${token}` } }) if (!r.ok) throw new Error(`items failed: ${r.status}`) const items = await r.json() // Skip batch-tracked items (they need a valid batchId); everything else is // tax-inclusive, so the payable is trivially roundToRupee(Σ qty·price). return items .filter((it) => !it.batchTracked && it.priceIncludesTax) .map((it) => ({ itemId: it.id, name: it.name, hsn: it.hsn, unitCode: it.unit.code, unitPricePaise: it.salePricePaise, priceIncludesTax: true, taxClassCode: it.taxClassCode, })) } // Build a random 1–5 line bill from distinct catalog items, integer qty 1–5. function buildBill(catalog, counterId) { const nLines = 1 + rng(5) const pool = [...catalog] const lines = [] for (let i = 0; i < nLines && pool.length > 0; i++) { const idx = rng(pool.length) const it = pool.splice(idx, 1)[0] const qty = 1 + rng(5) lines.push({ ...it, qty }) } const total = lines.reduce((a, l) => a + l.unitPricePaise * l.qty, 0) const payable = Math.round(total / 100) * 100 // roundToRupee (ctx.roundToRupee=true) return { body: { storeId: STORE, counterId, shiftId: 'load-sh1', businessDate: BUSINESS_DATE, lines, payments: [{ mode: 'CASH', amountPaise: payable }], clientPayablePaise: payable, }, lines, } } async function postBill(token, bill, tally) { const t0 = performance.now() let status = 0, errText = '' try { const r = await fetch(`${BASE}/api/bills`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify(bill.body), }) status = r.status if (!r.ok) errText = await r.text() else { await r.json() for (const l of bill.lines) tally.items[l.itemId] = (tally.items[l.itemId] ?? 0) + l.qty tally.lines += bill.lines.length } } catch (e) { errText = String(e) } const dt = performance.now() - t0 return { ok: status >= 200 && status < 300, status, errText, dt } } // ---- steady phase: one worker per counter, 250 sequential bills each ---- async function steadyPhase(tokenByCounter, catalog, tally) { const perCounter = Math.floor(TOTAL_BILLS / N_COUNTERS) const latencies = [] let success = 0 const errors = {} const t0 = performance.now() await Promise.all(COUNTERS.map(async (counter) => { const token = tokenByCounter[counter.id] for (let i = 0; i < perCounter; i++) { const bill = buildBill(catalog, counter.id) const res = await postBill(token, bill, tally) latencies.push(res.dt) if (res.ok) success++ else errors[res.status] = (errors[res.status] ?? 0) + 1, (tally.firstErr ??= res.errText) } })) const wallMs = performance.now() - t0 return { attempted: perCounter * N_COUNTERS, success, errors, latencies, wallMs } } // ---- burst phase: 100 bills, concurrency 50 (find the knee) ---- async function burstPhase(tokens, catalog, tally) { const latencies = [] let success = 0 const errors = {} let next = 0 const t0 = performance.now() async function worker() { while (true) { const i = next++ if (i >= BURST_BILLS) return const counter = COUNTERS[i % N_COUNTERS] const token = tokens[i % tokens.length] const bill = buildBill(catalog, counter.id) const res = await postBill(token, bill, tally) latencies.push(res.dt) if (res.ok) success++ else errors[res.status] = (errors[res.status] ?? 0) + 1 } } await Promise.all(Array.from({ length: BURST_CONC }, worker)) const wallMs = performance.now() - t0 return { attempted: BURST_BILLS, success, errors, latencies, wallMs } } // ---- integrity checks against the load DB ---- function integrity(tally, expectedBills, expectedLines) { const db = new Database(DB_PATH, { readonly: true }) db.pragma('busy_timeout = 5000') const checks = [] const add = (name, pass, detail) => checks.push({ name, pass, detail }) // (a) bill count == successful commits const billCount = db.prepare(`SELECT COUNT(*) n FROM bill`).get().n add('(a) bill rows == successful commits', billCount === expectedBills, `bills=${billCount}, successes=${expectedBills}`) // (b) every counter's SALE series is gap-free and duplicate-free (1..n) const bills = db.prepare(`SELECT counter_id, doc_no FROM bill WHERE doc_type='SALE'`).all() const byCounter = {} for (const b of bills) { const seq = parseInt(b.doc_no.slice(b.doc_no.lastIndexOf('-') + 1), 10) ;(byCounter[b.counter_id] ??= []).push(seq) } let seriesOk = true const seriesDetail = [] for (const [cid, seqs] of Object.entries(byCounter)) { seqs.sort((a, b) => a - b) const dup = new Set(seqs).size !== seqs.length const gap = seqs.some((s, i) => s !== i + 1) if (dup || gap) seriesOk = false seriesDetail.push(`${cid}:1..${seqs.length}${dup ? ' DUP' : ''}${gap ? ' GAP' : ''}`) } add('(b) per-counter series gap-free & dup-free', seriesOk, seriesDetail.join(' ')) // (c) SUM(movements per item) == opening − billed qty; and one SALE movement per line let stockOk = true const stockDetail = [] for (const [itemId, billedQty] of Object.entries(tally.items)) { const opening = db.prepare(`SELECT COALESCE(SUM(qty_delta),0) s FROM stock_movement WHERE item_id=? AND reason='OPENING'`).get(itemId).s const final = db.prepare(`SELECT COALESCE(SUM(qty_delta),0) s FROM stock_movement WHERE item_id=?`).get(itemId).s const saleQty = -db.prepare(`SELECT COALESCE(SUM(qty_delta),0) s FROM stock_movement WHERE item_id=? AND reason='SALE'`).get(itemId).s const ok = final === opening - billedQty && saleQty === billedQty if (!ok) { stockOk = false; stockDetail.push(`${itemId} open=${opening} billed=${billedQty} final=${final} saleMv=${saleQty}`) } } const saleMoves = db.prepare(`SELECT COUNT(*) n FROM stock_movement WHERE reason='SALE'`).get().n if (saleMoves !== expectedLines) { stockOk = false; stockDetail.push(`SALE movements ${saleMoves} != lines ${expectedLines}`) } add('(c) stock derived == opening − billed (per item)', stockOk, stockOk ? `${Object.keys(tally.items).length} items reconciled; ${saleMoves} SALE movements == ${expectedLines} lines` : stockDetail.join(' | ')) // (d) every bill's payments sum == payable const payRows = db.prepare(`SELECT payable_paise, payload FROM bill`).all() let payMismatch = 0 for (const row of payRows) { const payments = JSON.parse(row.payload).payments ?? [] const sum = payments.reduce((a, p) => a + p.amountPaise, 0) if (sum !== row.payable_paise) payMismatch++ } add('(d) payments sum == payable (every bill)', payMismatch === 0, `${payRows.length} bills checked, ${payMismatch} mismatch`) // (e) audit BILL_COMMIT count == bill count const auditCommits = db.prepare(`SELECT COUNT(*) n FROM audit_log WHERE action='BILL_COMMIT'`).get().n add('(e) audit BILL_COMMIT == bill count', auditCommits === billCount, `BILL_COMMIT=${auditCommits}, bills=${billCount}`) // (f) outbox rows == bill count const outbox = db.prepare(`SELECT COUNT(*) n FROM outbox`).get().n add('(f) outbox rows == bill count', outbox === billCount, `outbox=${outbox}, bills=${billCount}`) db.close() return checks } async function main() { console.log('=== S7 LOAD VERIFICATION ===') const cpu = os.cpus() console.log(`env: node ${process.version} | ${cpu.length}× ${cpu[0].model.trim()} | ${round2(os.totalmem() / 1024 ** 3)} GB RAM | ${os.platform()} ${os.release()}`) console.log(`target: ${BASE} db: ${DB_PATH}`) const counterCount = ensureCounters() console.log(`counters ready: ${counterCount}`) const tokens = [] for (const u of USERS) tokens.push(await login(u)) console.log(`logged in ${tokens.length} users`) const tokenByCounter = {} COUNTERS.forEach((c, i) => { tokenByCounter[c.id] = tokens[i % tokens.length] }) const catalog = await loadCatalog(tokens[0]) console.log(`billable catalog: ${catalog.length} items (batch-tracked skipped)`) const tally = { items: {}, lines: 0, firstErr: undefined } console.log(`\n--- STEADY: ${TOTAL_BILLS} bills across ${N_COUNTERS} counters, concurrency ${N_COUNTERS} ---`) const steady = await steadyPhase(tokenByCounter, catalog, tally) const sThr = steady.success / (steady.wallMs / 1000) console.log(`attempted=${steady.attempted} success=${steady.success} errors=${JSON.stringify(steady.errors)}`) console.log(`wall=${round2(steady.wallMs / 1000)}s throughput=${round2(sThr)} bills/s`) console.log(`latency ms: p50=${round2(pct(steady.latencies, 50))} p95=${round2(pct(steady.latencies, 95))} p99=${round2(pct(steady.latencies, 99))} max=${round2(Math.max(...steady.latencies))}`) if (steady.firstErr) console.log(`first error: ${steady.firstErr}`) if (tally.firstErr) console.log(`sample error body: ${tally.firstErr}`) console.log(`\n--- BURST: ${BURST_BILLS} bills, concurrency ${BURST_CONC} ---`) const burst = await burstPhase(tokens, catalog, tally) const bThr = burst.success / (burst.wallMs / 1000) console.log(`attempted=${burst.attempted} success=${burst.success} errors=${JSON.stringify(burst.errors)}`) console.log(`wall=${round2(burst.wallMs / 1000)}s throughput=${round2(bThr)} bills/s`) console.log(`latency ms: p50=${round2(pct(burst.latencies, 50))} p95=${round2(pct(burst.latencies, 95))} p99=${round2(pct(burst.latencies, 99))} max=${round2(Math.max(...burst.latencies))}`) const totalSuccess = steady.success + burst.success console.log(`\n--- INTEGRITY (load DB) ---`) const checks = integrity(tally, totalSuccess, tally.lines) for (const c of checks) console.log(`${c.pass ? 'PASS' : 'FAIL'} ${c.name} [${c.detail}]`) const allPass = checks.every((c) => c.pass) console.log(`\nINTEGRITY: ${allPass ? 'ALL PASS' : 'FAILURES PRESENT'}`) // Machine-readable summary for transcription into STATUS.md. const summary = { env: { node: process.version, cpus: cpu.length, cpuModel: cpu[0].model.trim(), ramGB: round2(os.totalmem() / 1024 ** 3), platform: `${os.platform()} ${os.release()}` }, steady: { attempted: steady.attempted, success: steady.success, errors: steady.errors, wallS: round2(steady.wallMs / 1000), throughput: round2(sThr), p50: round2(pct(steady.latencies, 50)), p95: round2(pct(steady.latencies, 95)), p99: round2(pct(steady.latencies, 99)), max: round2(Math.max(...steady.latencies)) }, burst: { attempted: burst.attempted, success: burst.success, errors: burst.errors, wallS: round2(burst.wallMs / 1000), throughput: round2(bThr), p50: round2(pct(burst.latencies, 50)), p95: round2(pct(burst.latencies, 95)), p99: round2(pct(burst.latencies, 99)), max: round2(Math.max(...burst.latencies)) }, totalSuccess, totalLines: tally.lines, checks, allPass, } console.log(`\n===SUMMARY_JSON===\n${JSON.stringify(summary)}\n===END_SUMMARY_JSON===`) process.exit(allPass && totalSuccess === TOTAL_BILLS + BURST_BILLS ? 0 : 1) } main().catch((e) => { console.error(e); process.exit(2) })