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/docs/09-UX-FLOWS-AND-MENUS.md

61 KiB

09 — Pages, Menus & Flows (UX Blueprint)

This is the screen-level blueprint for all four surfaces — POS Counter (Electron), Back Office (web), Owner App (React Native), and Customer Display — plus the cross-cutting systems they share. It applies the locked decisions everywhere: local-first POS, keyboard-first flows, config-driven content (labels, menus, thresholds, keymaps, and messages are DB rows per 07-DB-AND-CONFIG — nothing below implies a frontend release to change), immutable documents, and edition gating via plan_feature. Keystroke convention: a scan = 0 keystrokes (wedge scanner sends code + Enter); every key press = 1; lane budget 40 items/min means any per-item UI interaction beyond zero keystrokes in the common path is a defect.


1. POS Counter

1.1 Billing screen — zones

┌──────────────────────────────────────────────────────────────────────────────┐
│ HEADER  SiMS · MegaMart T.Nagar · Counter 2 · Ramesh (Cashier) · Shift #14   │
│         Bill: (assigned on close) · Token 07        Tue 08 Jul 2026  6:42 PM │
├──────────────────────────────────────────────┬───────────────────────────────┤
│ ┌──────────────────────────────────────────┐ │ CUSTOMER STRIP  (F6)          │
│ │  SCAN / SEARCH ▌  (48px, always focused) │ │ 98400 12345 · Lakshmi         │
│ └──────────────────────────────────────────┘ │ Khata ₹1,240 dr · 320 pts     │
│ LINES GRID (virtualized, 36px rows)          ├───────────────────────────────┤
│ #  Item                Qty  MRP  Rate  Amt   │ TOTALS  Items 12 · Qty 14.750 │
│ 1  Aashirvaad Atta 5kg  1   285  270   270   │ Gross 1,412.00 · Disc 42.00  │
│ 2  Tomato (loose)     1.250  —   32/kg  40   │ GST (breakup ▾) +61.20        │
│    ▶ last-added row highlighted              │ ┌───────────────────────────┐ │
│                                              │ │  PAYABLE  ₹1,431  (64pt)  │ │
│                                              │ └───────────────────────────┘ │
│ QUICK-KEYS ROW (Ctrl+1…Ctrl+0, touch tiles)  │ You saved ₹57 vs MRP          │
│ [Tomato][Onion][Potato][Bag ₹5] …            │ [F12 Cash] [F11 Pay] [F7 Hold]│
├──────────────────────────────────────────────┴───────────────────────────────┤
│ STATUS BAR  ● Synced 2m ago · Printer OK · Scale 0.000kg · Held: 2 ·         │
│             Drawer ₹8,140 · Keymap: Classic · v2.3.1                          │
└──────────────────────────────────────────────────────────────────────────────┘
Zone Notes
Scan/search box The sacred line. Focus always returns here after any action, overlay, or error — enforced by a focus sentinel; no dialog, toast, or sync event may steal it.
Lines grid (~62%) Columns: #, name (+ regional secondary name, HSN small, batch/expiry chip when tracked), qty, unit, MRP, rate, disc, GST%, amount. Last-added row highlighted, auto-scrolled; ↑/↓ selects for edits. Repeat scans merge into one line (default ON; auto OFF for batch-tracked items).
Totals panel (~38%) Mirrors the customer display so both parties read the same numbers. PAYABLE huge (cashier speaks it aloud); GST breakup collapsible — compliance lives on the print. "You saved ₹X vs MRP" is the trust line.
Customer strip Optional per bill: phone, name, khata balance chip, loyalty points. Sits by totals because khata/loyalty affect payment. F6 focuses.
Quick-keys row 10 configurable tiles (produce PLUs, carry bag), Ctrl+1…Ctrl+0 and touch. Per-counter DB config; hidden if unconfigured (typical Lite).
Status bar Sync dot, printer, live scale weight, held-bills badge, drawer cash (role-visible), keymap preset, version. Everything informs, nothing interrupts.
Header Store, counter, cashier, shift, clock. Bill number reads "(assigned on close)" — the per-counter GST series number is allocated only at payment commit, so held/abandoned carts never create series gaps. Held carts carry a Token number instead.

Touch: totals buttons and quick-key tiles ≥ 48 px; touch never adds steps to the keyboard flow.

1.2 The smart scan box — four grammars, no modes

Input Behavior
Digits + Enter (or scan) Barcode/item-code lookup → line added. Weighing-scale EAN-13 (2029 prefix) parsed per counter profile (PLU digits + embedded weight/price, configurable divisor).
Letters Live fuzzy/phonetic search < 150 ms (see §5.1); ↑/↓ + Enter picks; Esc clears.
n* prefix then scan/code Quantity multiplier: 3* + scan = qty 3 on one line.
Enter on empty box No-op (double-Enter guard).

1.3 Keymap — "Next" preset (DB profile; per preset / counter / user override)

Key Action Gate
Enter / Esc Confirm · Cancel/back one level (Esc never destroys lines)
↑ ↓ PgUp PgDn Line/list navigation
* Qty-multiplier prefix
F1 Keymap cheat-sheet overlay (printable card)
F2 / F3 Edit qty / price override on selected line F3: supervisor beyond ±X% band
F4 / F5 Line / bill discount (5 = ₹, 5% = percent) Supervisor above X%
F6 Customer strip: attach by phone
F7 / F8 Hold bill / resume tray
F9 Returns/exchange mode Setting: open vs supervisor
F10 Bill history & reprint (day-scoped)
F11 Payment overlay (all tenders, split)
F12 / Numpad + Cash close — the one key
Del / Ctrl+Del Remove line (inline confirm) / cancel cart (reason) Supervisor per setting
Ctrl+P Reprint last bill Copies > N → supervisor
Ctrl+F / Ctrl+M Open drawer no-sale / cash in-out Supervisor (drawer always audit-logged; paid-out above ₹X)
Ctrl+1…Ctrl+0 Quick-key tiles
Ctrl+L / Alt+S / Ctrl+, Lock counter · salesman tag (Standard+) · settings Settings: supervisor/manager

Numpad-only right hand is deliberate: scanner in left hand, right hand never leaves the numpad (digits, *, + = cash close, Enter). A second shipped preset, Classic keys, mirrors SiMS Classic's Oracle Forms bindings key-for-key — migrating cashiers keep 100% muscle memory on day one. Rebinds are per-action with conflict detection; changes sync as counter-scope settings.

1.4 Billing flows (keystrokes excl. scans and free-text typing)

Normal cash sale (the 90% path): scan items (0) → state total → F12: series number assigned, bill committed (immutable document + stock movements + outbox row in one SQLite transaction), drawer kicks, print spools async, customer display thanks, scan box refocused. 1 keystroke. With change: type tendered digits first, then F12 → change flashes 4 s on both displays. 5 keystrokes.

Weighed produce: (A) integrated scale, Pro — quick-key tile + Enter on live stable weight = 2; (B) manual weight, all editions day 1 — tile + .750 Enter ≈ 6; (C) label-scale barcode (2x EAN-13) — 0, the correct answer for high-volume supermarket produce.

Edits: re-scan merges qty (0) · 3*+scan (2) · F2 qty (3) · F3 price (5, +PIN out of band) · F4/F5 discount (45, +PIN over threshold) · Del remove (23). All overrides write before/after values to the append-only audit log with cashier + approver identity.

Unknown barcode (errors never trap): no match → non-blocking inline panel under the scan box: "Unknown code… [Enter] Quick-add · [Esc] Skip" — further scans queue behind it. Enter → 3-field micro-form (Name, Price/MRP, GST picklist defaulting to store slab) → item saved as draft, line added, flagged for back-office completion. 3 keystrokes + typing; skip = 1.

Customer attach: F6 → 10-digit phone (live match after 4 digits) → Enter. ≤12. No match → Enter again creates the customer phone-only. Attached customer ⇒ WhatsApp e-bill offered at close (auto/ask/off setting).

Hold/resume: F7 parks the cart under its Token instantly (1); held carts are journaled — they survive restarts and power cuts, and have no invoice number. F8 → tray → Enter (23). Day-end forces the tray empty.

Payment overlay (F11) — for everything that isn't exact cash. Payable on top; tender legs left; amount field prefilled with remainder; method hotkeys 1 Cash 2 UPI 3 Card 4 Khata 5 Other. Grammar: (optional amount) + method key adds a leg; Enter on zero remainder commits + prints; Esc abandons, cart untouched.

Tender Flow Keys
Cash w/ change via overlay F11 → 20001 → Enter 7
UPI dynamic QR (Standard+, online) F11 → 2 → amount-embedded QR on customer display → webhook auto-confirms → auto-close 2
UPI offline Static VPA QR; cashier sights success screen → Enter (payment flagged manually_confirmed) 3
Card F11 → 3 → amount pushed to terminal → Enter on approval 3
Khata F11 → 4 → requires attached customer; shows new balance vs credit limit → Enter; over limit → supervisor PIN 3 (+PIN)
Split ₹500 cash + UPI F11 → 50012 → QR remainder 6

Bill-close latency budget: commit + print spool < 1 s; the lane never waits on the printer.

Cancel/void (bills are immutable — a "void" is a new document): Ctrl+Del cancels an open cart with reason picklist (+supervisor PIN if it has lines; abandoned carts logged with line snapshot for shrink analytics). Voiding a closed bill (same day, pre-filing): F10 → find → V → reason → supervisor PIN → a cancellation document is appended referencing the bill; the bill stays in the series marked CANCELLED (series stays consecutive — GST-safe), stock reversed by counter-movements, payments reversed. Registered IRN → IRN-cancel queued within the 24 h window; outside it the flow refuses and routes to Returns (credit note).

Returns/exchange (F9): find the bill by scanning the receipt QR (0), bill no, or phone lookup → original lines listed with returnable qty netted → Space toggles, F2 partial → grid enters RETURN mode (red accent, watermark, negative lines; batch items return to their original batch). Exchange = keep scanning positive lines in the same document. Settle net via F11 (cash refund / khata credit); committed as a credit note in its own per-counter series referencing the original. No-receipt return: supervisor PIN, current-rate floor, audit-flagged. Simple 1-line refund ≈ 6 keystrokes.

Reprint: Ctrl+P = last bill (1). Older: F10 → find → P; W resends the WhatsApp e-bill. Beyond N copies → supervisor; every reprint audit-logged and printed "DUPLICATE".

1.5 Offline-degraded state — almost nothing changes

Offline is the architecture, not a mode. Scanning, search, lines, totals, print, drawer, cash/card/khata/hold/returns/reprint: identical. What differs: status dot goes grey ("Offline — 34 queued", count ticks — that's the whole ceremony); UPI falls back to static VPA QR + manual confirm; WhatsApp e-bills queue; e-Invoice bills print with "IRN pending" and register on reconnect; customer/khata lookups use the local replica (limit checked against last-synced balance, flagged; multi-counter stores keep a live LAN view via Store Hub); offline loyalty redemption capped per setting. No banners, no modals, no focus theft. Reconnect drains the outbox silently.

1.6 Shift & day flows

Lite skips the ceremony — login straight to billing, day-end report auto-generates. Standard+:

  • Login: cashier tiles + 46 digit PIN, < 5 s cold start; 5 wrong PINs → 60 s lockout + audit event. Ctrl+L locks mid-day (held carts and open bill preserved).
  • Shift open: denomination grid (₹500/200/100/50/20/10/coins, Tab advances, live total ≈ 25 keys); variance vs previous shift's declared float; over tolerance → supervisor + reason. Quick-open setting for small stores: single declared amount, ≈ 6 keys. Emits immutable shift_opened.
  • Cash in/out (Ctrl+M): direction → amount → reason picklist (float top-up, safe drop, petty expense, owner drawing) → drawer kicks, voucher slip prints. Above ₹X → supervisor. ≈ 610 keys.
  • Shift close: blind count by default (expected hidden until submitted) → variance with reason + supervisor PIN outside tolerance → held-cart tray must be empty → declare leave-behind float → X-report prints (bills, ABV, tenders, discounts, voids, returns, cash movements, variance, names). Synced; owner app gets the summary.
  • Day-end: one Enter → Z-report consolidating all counters (via Store Hub): GST summary by rate, tenders vs counted cash, top items, voids/returns, khata sales, pending-sync count. Prints + queues to owner digest; triggers the cloud backup snapshot. Fully offline-capable (report syncs later, with a pending-sync warning line if the outbox isn't empty).

1.7 Supervisor overlay — the one sanctioned interruption (always user-initiated)

Gated actions (all thresholds are DB settings per tenant/store): price override beyond band, discounts above X%, cancel-with-lines/void, no-receipt return, khata over limit, drawer no-sale, reprint > N, paid-out > ₹X, shift variance, offline loyalty above cap, tax-exempt sale, settings/keymap entry, training-mode toggle (manager).

Flow: compact overlay atop the visible bill — action description, before → after values, reason picklist (list is DB config), PIN field. Supervisor authorizes by user-code + PIN, badge barcode/card swipe (0 keys — the fast path), or (Phase 3, online) remote approve pushed to the owner app — overlay shows "awaiting approval", cashier can Esc and hold the bill so the queue keeps moving. Approve → executes; audit row (action, cashier, approver, counter, bill ref, before/after, timestamp) syncs to the back-office audit browser and owner feed. Deny/30 s timeout → clean cancel, focus → scan box. 3 wrong PINs → 60 s counter lockout + owner alert. Supervisor authority is itself a role permission from the DB matrix.

1.8 POS settings (Ctrl+, — supervisor gated; never required mid-bill)

Counter-scope DB settings (port/device bindings stay local-only): Printers (profiles, per-document routing, test print/drawer kick, template preview — templates edited in back office); Scale (port + protocol presets shipped as data, live readout, label-barcode scheme editor with test-parse); Customer display (monitor, orientation, playlist preview, language); Sync status (pending ops by type, per-item retry, force-sync, Store Hub state, one-click "send diagnostics" bundle — no PII); Keymap (preset picker Next/Classic/Custom, rebind grid, quick-key tile editor, print cheat card); Hardware wizard (first-run guided detection, re-runnable); About/License (edition, locked features as discoverable upsells, update channel).

1.9 Customer display (Pro; second monitor, same Electron app, local)

State Content
Idle/promo DB-driven playlist: offers, house ads, WhatsApp e-bill opt-in card.
Billing Last 56 lines large (customer's script, MRP struck-through when rate < MRP — kills above-MRP disputes), running total huge, "You saved ₹X", updates inside the 150 ms scan budget.
QR payment Full-screen dynamic QR with amount ("Scan to pay ₹1,431") → green PAID on webhook; offline: static VPA QR + amount.
Change due / Thank-you "Change ₹569" 4 s · total, saved, loyalty earned, "bill sent on WhatsApp", catalog message per language, 5 s.
Return mode Refund lines + net, red accent — transparency during returns.

The display never shows sync/printer/internal errors.

1.10 Keystroke scoreboard (published, telemetry-verified)

Cash exact 1 · cash w/ change 5 · UPI 2 (offline 3) · card 3 · khata 3 · split 6 · scale produce 2 (manual ~6, label 0) · qty 0/2/3 · price/discount 45 · quick-add 3+typing · customer attach ≤12 · hold 1 / resume 23 · cancel cart 23+PIN · void 510+PIN · 1-line return ~6 · reprint 1 · login 58 · shift open/close ~2535 · day-end 1. Telemetry: per-bill timing, scan-to-line histogram, keystrokes-per-bill, overlay dwell — the "fastest counter in India" number we publish.


2. Back-Office IA & Menu Tree

2.1 Design stance (defaults, not options)

Table-first workbench: every module opens on a virtualized, keyboard-navigable grid; details are drawers by default, full pages on demand. Two levels of navigation, never three — Module → Page; anything deeper is a tab or filter. Hide by role, lock by edition: a user never sees a page their role can't use; owners/managers see edition-locked pages as discoverable upsells. The whole menu is data: menu_item(code, module_code, route, icon, sort, min_edition, label_key) + menu_item_role — support reorders/relabels/regates with zero frontend release.

2.2 Menu tree — eleven modules, ordered by daily usage [L/S/P/E] = min edition

  • 🏠 Dashboard — Overview L (today's sales/payments live; alert cards deep-linking to the fixing page) · Approvals & Tasks S (discount/void approvals, adjustment approvals, POS quick-adds needing completion, failed IRNs).
  • 🧾 Sales — Bills L · Returns & Credit Notes L · Shifts & Day-End L (recon features S).
  • 📥 Purchases — Purchase Inbox P (flagship queue, unread badge, first item) · Purchase Invoices L · Orders & GRNs P · Purchase Returns / Debit Notes L · Suppliers L · GSTR-2B Reconciliation P. (Label printing stays under Catalog — purchases hand off to it.)
  • 📦 Inventory — Stock on Hand L · Stock Movements L (the append-only ledger behind every number — stock is derived, never typed) · Adjustments L (reason-coded, approval workflow) · Stock Takes P · Transfers E (in-transit state, e-Way hook) · Expiry & Ageing P (action lists, not charts) · Reorder S (velocity suggestions → one-click draft PO).
  • 🏷️ Catalog — Items L · Categories & Brands L · Price Lists S (effective-dated, store overrides) · Schemes & Offers S · Label Printing S.
  • 👥 Customers — Customers L (phone-keyed) · Khata & Collections L (WhatsApp reminders + UPI collect links, single or bulk) · Loyalty P · Campaigns P.
  • 📒 Accounting — Ledgers L · Vouchers S · Cash & Bank Books S · Day Book P · Financial Statements P · Tally Export S · Year Close & Locks S.
  • 🏛️ GST & Compliance — GST Returns S (GSTR-1/3B prep, JSON export, filing status) · e-Invoice Queue P · e-Way Bills P · HSN & Tax Rates L (effective-dated — the GST-2.0-proof page).
  • 📊 Reports — Sales L · Stock & Valuation L · Party Outstanding L · GST Registers L (the CA hand-off pack) · Staff & Counters S · Analytics P · Scheduled Reports S.
  • 🏢 Head Office E — Store Comparison · Central Pricing · Central Purchasing (Ph 4) · Consolidated GST (multi-GSTIN) · Outlet P&L (Ph 4).
  • ⚙️ Admin — Users & Roles S · Stores & Counters L (per-counter series, print profiles, Store Hub role) · Settings L (tenant→store→counter→user hierarchy) · Templates L (visual editor + preview) · Integrations L (WhatsApp, UPI, GSP, email-in address) · Subscription & Plan L · Audit Log S · Sync & Devices L · Backups L · Data Import L (Excel/CSV + Classic migration tool with verification report) · API Access E.

Not in the tenant menu: plan/price-book editing, the global message catalog, staged rollouts, and cross-tenant telemetry live in the internal SiMS Control Panel — same engine, vendor tenant scope. Never expose vendor-level config in customer IA.

2.3 Navigation model

  • Left sidebar (240 px, collapsible to icon rail): accordion modules, active one auto-expands. Top bar: scope switcher (store selector — "All stores" for Enterprise owners — plus a date-range chip every page inherits), Ctrl+K, notifications bell (fed by Approvals & Tasks), sync dot, user menu (per-user language). Breadcrumb Module / Page / Record; every record has a stable, copyable URL.
  • Ctrl+K command palette — the second navigation system. Four ranked groups: Records (bill no, item, party, GSTIN — same fuzzy/phonetic engine as POS), Pages, Actions ("Export GSTR-1 June", "Send khata reminders"), Recents (last 15 records per user, server-side, cross-device). Prefix operators: # bill/doc no · @ party · > action · ? help/settings. The palette respects role visibility and edition gating — locked actions show a lock glyph and open the upsell sheet; role-hidden items never appear.
  • Keyboard: g + module key (g s Sales, g p Purchases, g i Inventory, g c Catalog, g a Accounting, g g GST, g r Reports, g x Admin); / focuses the grid filter; n = page's primary create; e = export. Keymaps are DB data, same mechanism as POS.

2.4 Visibility & gating rules

Rule Behavior
Role lacks access Page absent from sidebar, palette, routes (deep link → 403 with catalog message).
Read-only role Page visible; create/edit/post disabled with tooltip; export still works.
Below edition, viewer owner/manager Lock badge; click → upsell sheet: 30-sec demo, one-paragraph pitch, "Included in Pro — Upgrade" upgrading in place.
Below edition, viewer staff Hidden — upsells pitch buyers, not cashiers.
Cashier Zero back-office pages; login rejected with "use the counter app". Cashiers exist only in POS.
Module empties after filtering Module header disappears too — except Purchases, GST, and Reports keep their locked flagship pages (Purchase Inbox, e-Invoice, Analytics) visible to owners: the top three upgrade drivers.

Default role → module map (the Admin permission matrix refines to page level with None/Read/Full cells; Auditor is read-everything by definition; Owner is Full at their edition): Supervisor — Dashboard, Sales, Inventory (no Transfers), Customers, Catalog (read), sales/stock Reports. Purchaser — Purchases, Catalog, Inventory (Stock/Reorder/Expiry), stock/purchase Reports. Accountant — Accounting, GST, Sales/Purchases (read), Khata, GST/party Reports. Net effect: the Lite solo owner sees a 7-module ~20-page app; a Pro accountant sees a compliance cockpit; the Enterprise owner sees all eleven — same engine, same routes, different rows in menu_item_role + plan_feature.

2.5 Anatomy of the pages that matter most

  • Catalog → Items (biggest grid): virtualized columns (name, code, barcodes, category, HSN, GST, MRP, cost, margin %, stock, status incl. incomplete — POS quick-adds land here); saved-filter tabs; side-peek preview on row focus; bulk actions (effective-dated category/GST/price edits, labels, merge duplicates); empty state = Import from Excel / Migrate from Classic / Add first item — replaced by the "N counter quick-adds need details" worklist when applicable.
  • Item editor: sections — Identity (multi-language names, HSN with rate auto-fill), Barcodes (many, incl. 2x PLU config on Pro), Units & Packs conversion matrix, Pricing (per-price-list effective-dated rows), Batch & Expiry toggle (Pro), Reorder. Right rail: live stock by store, last 5 purchases/sales, audit drawer. Only name + GST rate hard-required — everything else completable later. Effective-dated fields edit via a "Change from…" timeline dialog, never in-place.
  • Sales → Bills: grid with payment-mode chips, e-bill and IRN status; side-peek renders the bill exactly as printed (same template engine) with the banner "Bills are permanent records — corrections create a credit note." Actions: Reprint, Send e-Bill, Create Return, Generate e-Way. Per-counter sync-lag indicator so "no data" and "not synced yet" are never confused.
  • Inventory → Stock on Hand: group-by item/category/store/batch; valuation (FIFO/weighted avg per tenant setting); row expansion shows the last 30 movements inline — "why is this number what it is" is a first-class answer. Filters include negative!, zero, below-reorder, dead > 90 days, expiry windows.
  • GST → GST Returns: period board Draft → Reviewed → Exported → Filed (ARN stored); exceptions worklist first (missing GSTIN, rate mismatches, missing HSN, unregistered IRNs) — the return can't reach Reviewed with open exceptions (owner-PIN override, audit-logged). Prepare locks a snapshot; fixes deep-link and re-snapshot deltas. Cross-link to GSTR-2B Recon for the inward side.
  • Admin → Users & Roles: Users tab (surfaces allowed: POS/back office/owner app) + Roles tab (permission matrix + sensitive-action gates: max discount %, void rights, override band, adjustment threshold, PIN policy). Deactivate, never delete; at least one owner must remain; matrix edits show "affects N users" and land in the audit log.

2.6 Cross-page conventions (the engine's grammar)

Convention Behavior
List → detail = side-peek drawer; Shift+↵ = full page with own URL; browser back restores grid scroll/filter state.
Saved filters Named views as tabs above the grid; personal by default, shareable tenant-wide; support can seed per-vertical presets as DB rows.
Export everywhere e → Excel/CSV/PDF honoring filters/columns/sort; > 10k rows async with bell notification; exports audit-logged.
Audit drawer Every record: chronological who/what/when with field-level before → after diffs from audit_log; same component everywhere.
Effective-dated edits Timeline dialog: past (grayed, immutable) → current → "new value from [date]"; scheduled changes show as a chip; deleting a scheduled change is allowed, rewriting history is not.
Immutable documents Posted docs render read-only with "Permanent record — correct via Credit Note / Debit Note / Reversal"; the correction pre-fills a linked document; both cross-link forever.
Import on every master Typed template → upload → staged preview with row-level errors → partial commit; error rows re-downloadable. Same component serves Classic-migration verification.
Empty states teach One sentence (from message_catalog, localized) + primary CTA + import path. Never a blank table.
Sync honesty Store-originated data shows a freshness chip ("Store 2: synced 2 min ago"); stale > 15 min turns amber and links to Sync & Devices.
Money & scope Tabular figures, Indian grouping from the one shared formatter; pack context ("2 box + 3 pc"); top-bar store + date scope follows the user across pages.

3. Purchases & Purchase Inbox

All three entry paths — manual, PO/GRN, Inbox — converge on one posting pipeline producing the same immutable purchase_invoice document.

3.1 Placement — POS "Back Room" mode

The Electron POS gets a supervisor-gated Back Room mode (separate from the sacred billing screen) running the same React screens as the web back office, but against local SQLite, syncing up. Rationale: Lite shops have one machine and shaky internet; goods receiving happens where the scanner and label printer are; purchase documents flow up cleanly and can never conflict. Zero new frontend surface.

Function POS Back Room (offline) Back office web Owner app
Manual entry, GRN, purchase returns, label printing ✔ default home (scanner + label printer here) ✔ identical screens (label queue mgmt only)
PO create/approve approve/deny
Purchase Inbox review — (AI + raw docs live in cloud) ✔ only badge + camera capture
Suppliers read-only ledger lookup ✔ full outstanding summary
GSTR-2B recon ✔ only

Inbox postings sync the document down to the assigned store, which applies stock-in movements locally — stock stays derived from local events.

3.2 Manual purchase entry — the speed flow (MVP, Lite+)

Creed mirrors the counter: nothing interrupts line entry; all prompts (price changes, labels, mismatches) batch to post time; focus returns to the scan box. Layout: header strip (supplier · invoice no · date · Local/Interstate auto from GSTIN state · terms); lines grid Item | Batch | Expiry | Qty | Free | Unit | Cost | Disc% | Tax% | MRP | Margin% | Sell | Amount — which columns the cursor stops in is a per-store "column profile" setting (a kirana stops at Qty+Cost; a pharmacy adds Batch/Expiry); right rail item card (photo, stock, last 3 costs from this supplier with dates, live margin %); footer tax breakup + "Bill total (as printed)" field — the single most effective error catcher in purchase entry.

Flow: Ctrl+P new → supplier by 3-char fuzzy match → invoice no (inline duplicate check on supplier + no + FY) → date defaults today → scan (line pre-fills last cost, HSN tax, current MRP) → qty → cost (deviation beyond a % setting shows an inline chip ↑ ₹2.10 vs 04-Jun, never a dialog) → Enter blows through skipped columns; new MRP silently queues a price decision → Ctrl+↵ close → totals-check overlay: type the printed grand total; delta beyond tolerance (default ₹0.99) shows per-line hints and an inline "other charges" line → Post → one consolidated price-change dialog (suggested sell per the item's pricing rule, checkboxes default ON, audit-logged) → label prompt ("Print labels for 5 new/changed items?", quantities default to received qty, jobs to the Label Print Queue).

Budget: ~5 keys/line, ~24 keys overhead → a 12-line bill in ≈ 85 keystrokes, under 2.5 minutes. Publish this like scan-to-line < 150 ms. Unknown barcode → inline quick-create flagged needs_completion, exactly like POS quick-add. Unit conversion from the pack master (CASE(24) → PCS), cost per stocking unit live. Below-floor margin renders amber, never blocks. Posting = one local transaction: immutable document, batch-wise stock-in movements, supplier ledger credit, price-change audit entries, label jobs. Corrections are debit notes or reversal + re-entry — never edits.

3.3 PO → GRN → Invoice (Pro, Phase 3)

Direct entry stays the default; PO mode is a store setting (purchases.mode = direct | po_optional | po_required). PO: Draft → Approved (threshold-gated; owner-app approve/deny) → Sent (PDF via email/WhatsApp from template) → Partially Received → Received → Closed | Short-closed | Cancelled, lines carrying ordered/received/invoiced qtys; reorder suggestions can prefill from sales velocity. GRN (POS Back Room, scanner-first): pick PO → lines prefill remaining qty → scan to verify (scan-vs-typed mismatch highlighted) → over-receipt beyond tolerance % → supervisor PIN → batch/expiry per flagged line, damaged-on-arrival to wastage → post: stock-in immediately (goods are sellable the moment they hit the shelf — never make the counter wait for accounts) + "GRN pending invoice" accrual at PO cost; the label prompt here is the natural supermarket labeling moment. Invoice matches one or more GRNs (or one partially) and does only the financial part — ledger credit, cost true-up via weighted-avg adjustment movement, tax capture; no double stock-in. Inbox drafts auto-propose open GRNs for the supplier; variances beyond tolerance route to approval.

3.4 Purchase Inbox (Pro, Phase 3 — the flagship)

North star: ≥ 70% of purchase lines via Inbox (Phase-3 exit ≥ 50%). The pitch: forward the bill, confirm, done.

Intake: auto-provisioned email address bills-{tenant}@in.sims.app (++{store} aliases for routing; printed on our POs so suppliers self-onboard; sender learned per supplier) · drag-drop/bulk upload with auto-split of stacked-scan PDFs · owner-app camera (fast-follow) · WhatsApp forward (fast-follow) · e-Invoice JSON/QR parsed deterministically — no AI, 100% confidence; scanning the IRN QR with the Back Room scanner also creates a doc.

Pipeline: SHA-256 dedupe → object storage (Mumbai) → classify (invoice / CDN / statement / junk — non-invoices to an "Other" tab) → parse (deterministic for e-invoice JSON and text-layer PDFs; Claude vision for photos) yielding header + lines, each field with confidence + source bounding box → supplier match (GSTIN exact → auto; fuzzy + learned sender → suggested) → item match in three tiers: A alias-memory hit via supplier_item_alias (mapped once, remembered forever, green) · B barcode or HSN+fuzzy suggestion (amber, one-key confirm) · C unmatched (red) → validations (duplicate by supplier+no+FY plus fuzzy same-amount-same-week, totals recompute, line tax vs our dated rate, CGST/SGST vs IGST vs state, MRP/cost jumps) → state Ready (all green) or Needs Review. inbox.autopost_trusted_suppliers (default off) auto-posts clean docs from whitelisted suppliers — the endgame for FMCG distributors. States are an append-only log: Received → Parsing → Needs Review | Ready → Posted | Discarded | Duplicate.

Review screen (the demo moment): three zones — queue rail (J/K next/prev, source badge, age, confidence %), center source viewer (zoom/rotate/thumbnails), right draft form (the same grid component as manual entry). Clicking any field highlights its source region in the PDF and vice versa — trust is built by making verification effortless. Confidence tinting: green ≥ 95 auto, amber 7094 ( accepts, Tab edits), red < 70. Header verdict math: Printed ₹12,480.50 · Computed ₹12,480.50 ✓. Tax mismatch banner offers three actions: use bill rate for this purchase (ITC follows the bill; default), update item master (dated edit, never silent), flag to accountant. Red-line mapping keeps the supplier's raw text visible with type-ahead or pre-filled Create-item; differing units get a one-time "CTN = how many PCS? [24]" stored on the alias. Microcopy under every mapping: "Will be remembered for Vikram Distributors." That sentence is the product promise. Ctrl+↵ posts → syncs down → store applies movements; price-change and label prompts fire on the store machine. Target: a 30-line bill confirmed in under 60 seconds; no bulk-post by design — every posting is a human confirmation. Header stat: "This month: 84% of purchase lines via Inbox."

Exceptions (never a dead end): unknown supplier → GSTIN public lookup pre-fills create, or link-to-existing fuzzy list; unknown items → per-line map-or-create (bulk "create all N" gated to purchaser role); exact duplicates can never post, fuzzy ones show side-by-side compare with reason-coded override; total mismatch → ranked causes (add-charges one-click, tax-inclusive basis toggle, jump-to-worst-cell), post blocked until resolved or supervisor-overridden; unreadable scans → request re-upload (polite catalog template) or manual entry pre-filled with whatever parsed; multi-invoice files auto-split; statements file under Other and feed ledger reconciliation later.

3.5 Suppliers

List: name, GSTIN, outstanding, overdue, last purchase, MTD volume. Detail tabs: Overview (ageing buckets, terms vs actual days-to-pay, 12-month trend, top items) L/1 · Ledger (running balance, drill into any document, Record-payment with oldest-first allocation, WhatsApp statement) L/1 · Price history (item × date cost grid, trend flags — the data behind the last-cost chips) S/2 · Pending POs/GRNs (accrual exposure totaled, one-click remind) P/3 · Items & aliases (the matching memory, editable, CSV-seedable for big distributors) P/3 · Documents (all Inbox docs) P/3 · Settings (GSTIN, terms/limit, tax basis, recognized senders, trusted-auto-post, bank details) L/1. Back Room gets read-only ledger lookup — the supplier's rep at the back door gets his balance offline.

3.6 GSTR-2B reconciliation (Pro, Phase 3)

Pick period → pull via GSP (or upload portal JSON — fallback always available) → summary tiles (ITC per 2B · per books · Matched % · At-risk ₹) → work buckets top-down; period locks when 3B files. Buckets: Exact (auto, collapsed) · Fuzzy (side-by-side, confirms — confirmations train the per-supplier invoice-no normalizer) · Value mismatch (accept-with-note / raise DN-CN / flag supplier) · In 2B, not in books — the killer feature: "Create draft in Purchase Inbox" from the 2B row (header pre-filled, lines pending until the paper bill surfaces); nothing missed stays missed · In books, not in 2B — ITC-at-risk list with WhatsApp/email nudge to the supplier, ITC-hold flag surfacing in 3B prep, auto-recheck next period. All actions audit-logged; recon state append-only per period.

3.7 Purchase returns / debit notes (MVP, Lite+)

Immutable DNs in their own per-store series (ST1-DN-00042), referencing the original where linked. Flow: pick supplier → pick original invoice (scan any item to filter) or unlinked mode → return qty per line with mandatory DB-configured reason codes (damaged/expired/short-supplied/rate difference/scheme not passed/quality), batch defaulting to received batch → rate-difference-only type: amounts without quantities — no stock movement, pure ledger + GST document → tax auto-reverses at the original invoice's rates (rates are dated config; the original document is the truth) → post (above-₹X → supervisor PIN): stock-out movements, ledger debit → print/PDF/WhatsApp travels with the goods; e-Way queue picks it up over threshold. Settlement nets against the next payment allocation or a replacement GRN referencing the DN.

3.8 Config & metrics

Everything tunable is setting rows (purchases.mode/column_profile/cost_warn_pct/margin_floor_pct/margin_basis/total_tolerance/over_receipt_pct, inbox.autopost_trusted_suppliers, dn.approval_threshold, gstr2b.amount_tolerance) plus supplier_item_alias, inbox_document/extraction, price_change_event, label_job tables. Instrument from day one: keys/seconds per manual line (≤ 5 / ≤ 8 s), % lines via Inbox, % docs auto-Ready, median review time (< 60 s), alias hit rate (→ 95% per supplier by the third bill), duplicate and total-mismatch catches, 2B matched-% and ITC-at-risk per tenant.


4. Owner App & Onboarding

4.1 Owner app (React Native, Android-first; Phase 3)

Stance: online-first, cache-always — every screen renders instantly from a local SQLite snapshot, then refreshes; a permanent freshness ribbon ("As of 12:41 PM · Store synced 3 min ago") because "live" is really "as-synced"; never a spinner over a cached screen. One write action: approve/deny — everything else is read; new alert types, digest sections, and report tiles are DB config, not app releases. Language per user setting, all text from message_catalog (Hindi + English at launch). Auth: phone + OTP, then a 4-digit app PIN (doubles as approval PIN) with biometric; max 3 devices, revocable from Admin. On Lite, only Today tiles and the Day-End Digest render; Alerts/Approvals/Reports show as locked cards with one-tap upgrade.

Pages (four bottom tabs + header store switcher/profile): Today — net sales vs same weekday last week, bills count + bills/hour sparkline, payments donut (Cash/UPI/Card/Khata), ABV, returns, khata given, per-counter open/closed status, sync freshness chips, Day-2 checklist card, top 3 alerts; 60 s auto-refresh. Store switcher — sheet with per-store sales + freshness dots (green ≤ 5 min / amber ≤ 30 / red > 30 during open hours), "All stores" rollup first; single-store tenants never see it. Alerts feed — 30 days cached, day-grouped, filter chips (Stock/Expiry/Khata/Sync/Discounts/System); every alert deep-links; long-press mutes the type (user-scope setting); dismiss state syncs later. Approvals — pending cards (type, store+counter, cashier, bill detail, live countdown to POS timeout, Approve/Deny behind PIN) + 7-day history showing who decided; approve/deny never queue offline — a stale approval is worse than none; offline shows "counter will use supervisor PIN". Reports Lite — deliberately five, no builder: sales trend, top-20 items, category performance, khata outstanding (tap → server-side WhatsApp reminder), month-to-date GST snapshot; tile definitions are DB rows, so a sixth report is config. Day-End Digest — rendered digest per store + date, last 14 cached (the guaranteed-offline artifact), back-swipe browses days. Settings — language, per-event Push/Digest/Off, quiet hours, PIN, devices, plan (read + upgrade CTA), support routed to the dealer first via tenant config.

Flow — remote approval (the one write): the invariant rules it — billing never waits on network, so remote approval is a fast path layered on the always-available supervisor-PIN gate, never a replacement. Threshold trips → POS overlay offers Supervisor PIN (always) and "Ask owner" (only when online with a live owner push token) → approval_request outbox doc → high-priority FCM, body resolved server-side per device language → owner taps, PIN, decides → decision pushes down. Round-trip target < 10 s; POS timeout 45 s (counter setting); on timeout/offline/deny the overlay falls back to PIN or cancel, and the cashier can hold the bill and serve the next customer. Late decisions are discarded and logged. Every override — however resolved — still emits a post-facto owner alert: remote approval is convenience; the audit trail is the guarantee. Offline owner (train, basement): every tab renders cached with a grey ribbon, zero blank screens; reconnect refreshes silently.

4.2 Notification strategy

The owner's phone is sacred: default posture is digest; events earn a push; one coalesced push beats twelve. Three priority classes assigned per event in a notification_rule table (event → class, coalesce window, quiet-hours override, channel — team-editable, no release): P0 interrupt now (bypasses quiet hours) · P1 push, batched, respects quiet hours (default 22:0007:00) · P2 digest/feed only. All texts resolve server-side from message_catalog keyed (code, channel, lang) — push is one line, WhatsApp is a structured template. Channels: FCM primary; WhatsApp for the day-end digest to app-less owners; SMS only for license-expiry final notice; no email. Per-user overrides to Push/Digest/Off — except P0 sync-down and license expiry, reducible only to Digest.

Defaults: approval requests P0 (uncoalesced) · store sync-silent > 30 min during open hours P0 (one per incident + recovery push) · license expiry P0 T-7/3/1 (+SMS at T-1 if push undelivered) · day-end digest P1 at close or 21:30 · cash variance > ₹100 P1 · big discount/void P1 coalesced in 15-min batches ("3 overrides at Store 1") · khata limit crossed P1 (max 1/customer/day) · khata ageing, low stock (coalesced counts), near-expiry (30/15/7-day horizons), record-day delight, app updates, backup status P2 (backup failed 2 days running escalates to P1).

The Day-End Digest is the product's daily heartbeat: one push per store (+ chain rollup) — net sales & bills vs last same weekday → payments split → cash variance per counter → khata given/collected → top 5 items → alerts summary → one Day-2-checklist nudge. Each section is a DB-configured digest block, reorderable without release. Ships in Phase 2 as WhatsApp-only (needs no app); the Phase 3 app is mostly a rendering client.

4.3 Onboarding wizard (Phase 2; back office /setup, resumable)

Target: first real bill within 15 minutes, measured. Every completed step writes real tenant config immediately (no final submit — abandonment leaves a usable half-configured tenant); every skip is a stored decision that creates its Day-2 checklist item; per-step 30-second Hindi/English videos make both self-serve and dealer-assisted modes work without us on a call (the Phase-2 exit criterion). No step can dead-end: GSTIN API down → manual entry + auto-retry verify; printer undetected → skip; import errors → quarantine.

# Step Highlights Skippable
0 Account & plan Phone + OTP → tenant; plan picker rendered from plan/price_book rows; default 14-day full-Pro trial; language sets tenant + user default Plan deferrable via trial
1 GSTIN → business GSP auto-fetch of legal name/address/state/registration type; "No GSTIN" path = unregistered mode (Bill of Supply) — the mini-shop reality; adding GSTIN later upgrades the invoice format automatically Yes
2 Store & counters Store 1 pre-filled from GSTIN; each counter auto-assigns a GST-legal series prefix (ST1C1-, editable) in doc_series Counters addable later
3 Tax & pricing defaults Three pre-answered confirmations: MRP tax-inclusive ON, round to nearest rupee, B2C thermal default; composition preset; GST rate master preloaded system-wide — never owner-entered Yes
4 Items (a) Excel upload with synonym-dictionary column auto-mapping ("MRP/Rate/Price", "विवरण"), 20-row preview, good rows import in background, bad rows quarantined — never block on row 4,812; (b) business-type template catalogs (kirana, pharmacy, apparel… — DB data, dealer-extendable); (c) skip — POS quick-add covers day one Yes — the most important skip
5 Staff Cashiers with POS PINs; owner is default supervisor until staff added Yes
6 Install & pair POS Download link SMS'd to the counter PC; POS first-run takes a 6-digit pairing code (15-min TTL) → binds machine, pulls license, starts master sync; import progress visible Pairing deferrable
7 Printer setup (on POS) Auto-detect ESC/POS, paper width, test print (₹ + Hindi line verifies code page), drawer-kick test Yes — "No printer / WhatsApp e-bills only" is a first-class choice
8 First test bill Guided overlay on the real billing screen; TRAINING watermark, separate doc_type=TRAINING series — never pollutes the GST series; the dealer's proof-of-done No — the "aha"
9 Go live Confirm series starts → Start Billing flips training off; confetti once; owner-app link SMS'd No

Progress lives in onboarding_step(tenant, step_code, status, completed_by, at) rows, feeding the checklist and the dealer hand-off record; every step is reopenable from Settings forever.

4.4 Day-2 checklist

One data-driven "Finish setting up" surface: checklist_item(code, title_msg_code, deep_link, completion_probe, edition_min, priority, snooze_days) — items added/retired without release; completion is detected by server-side probes, never self-reported (UPI item completes the moment the setting row exists). Surfaces: back-office dashboard card (progress ring + next 3, deep-linked), owner-app Today card, one idle POS status-bar chip ("Setup 70%" — never on the billing path), and exactly one line in the day-end digest. No pushes for checklist items, ever. Default items in priority order: complete/verify GSTIN → fix quarantined import rows → items missing HSN/GST rate (blocks clean GSTR-1 — chase early) → MRP/cost on template items (auto-completes via purchases) → UPI ID for dynamic QR → WhatsApp e-bills → top-5 suppliers → reorder levels (auto-suggest from 2 weeks of sales) → khata opening balances → staff + discount threshold → shop logo → owner-app install (auto-completes on first login) → barcode labels → store timings (drives sync-health alerts and digest timing) → backup/sync green (surfaces only if red). Done (auto) / Snooze 7 days / Dismiss forever; the card retires at 100% or 60 days and moves to Settings → Setup.

4.5 Dealer machinery (Phase 2 minimum; portal is Phase 4)

dealer(code, name, phone, region, white_label_profile) rows; every tenant carries dealer_code from signup — attribution for margins from day one, before any dealer UI exists. Dealer-assisted signup: dealer enters code + owner's phone; the owner receives the OTP and reads it out (consent stays with the owner; a consent SMS names the dealer). The dealer's login gets a scoped installer role: wizard steps 18, settings, imports, printer — no sales data, ledgers, or reports — auto-expiring 7 days after go-live, owner-re-grantable. The wizard's resumability is the dealer workflow: pre-provision steps 15 from the office the evening before, then on-site pairing → printer → training bill with the actual cashier at the keyboard → go-live in under 10 minutes. USB offline installer (shop bandwidth reality; pairing needs one moment of connectivity, initial payload masters-only). Classic customers: the migration tool replaces step 4 and marks steps 15 done from imported data. On go-live a hand-off summary generated from onboarding_step rows goes to the owner by WhatsApp — disputes settled by data. The training series doubles as the post-go-live staff-training tool; support.channel routes the owner's Support button to the dealer first. Deferred to the portal: commission statements, fleet view, white-label UI — the attribution rows above make them pure additions.


5. Cross-Cutting UX Systems

One meta-rule governs everything here: nothing on the billing path may steal focus, block on network, or require reading a paragraph. Each system is one pure-TS package (@sims/search-core, ui-states, i18n, print-queue, gates, error-catalog) consumed by all four surfaces; behavior ships in git once, content is tuned in the DB forever.

5.1 Search — one engine, two profiles

The POS scan box (local SQLite, items only, results painted < 100 ms — headroom inside the 150 ms scan-to-line budget) and the back-office Ctrl+K omnibox (Postgres pg_trgm + the same key functions ported, < 400 ms p95, items/parties/bills/actions/settings with #/@/> prefixes) share one normalizer, phonetic function, and ranking — a cashier promoted to back-office work never relearns search.

Match tiers (short-circuit): 1 barcode exact (≥ 8 digits or wedge suffix; 2x scale barcodes parsed before search) → 2 item-code exact (Classic codes carry over) → 3 token prefix on every word of every name/alias → 4 phonetic key → 5 fuzzy (edit-distance ≤ 2), running only when tiers 14 return < 8 results. Rank within tier by 90-day sales velocity, then shorter name — popularity re-ranking makes 100k SKUs feel small.

Hinglish, three mechanisms, all DB data: multi-script names auto-transliterated at save (चावल → chaval); a shared Indic phonetic key applied to index and query (aa→a, ee→i, w→v, ph→f, chh→ch, sh→s… so typed chawal and indexed chaval both collapse to caval; dhaniya/dhania/daniya collide correctly); and item_alias(item_id, alias_text, source) fed by imports, Purchase Inbox matching memory, and a one-key POS gesture — when a cashier's search fails but they find the item another way, POS offers "remember chawal for this item? [Enter]". The catalog teaches itself the shop's vocabulary; a Hindi commodity-term seed pack ships as rows.

Performance at 100k SKUs: FTS5 with 23-char prefixes + a phonetic search_key table; top-5,000-by-velocity hot set in memory at startup (< 300 ms, inside the 5 s cold start) painting in < 20 ms with full-index merge behind it; no debounce (debounce = felt lag) — each keystroke cancels the in-flight query on a worker thread; results capped 50, fuzzy candidates bounded ≤ 500. p50/p95 latency is telemetry with regression alerts — we publish the number.

5.2 State & feedback vocabulary

Five tokens, one meaning each, everywhere: ok green (done/synced/paid) · busy amber (queued/pending) · alert red (a human must act now) · idle grey (offline/disabled — calm) · info blue. Offline is grey, never red — offline is the architecture, not a failure; red spent on "meh" kills the vocabulary. Every state renders icon + color + text/number via shared components that physically cannot render color alone. Back office uses skeletons (never a bare spinner > 300 ms); the POS billing path has no loading states at all. One document-status chip set spans POS lines, the bill browser, and the owner app: Draft · Held · Closed · Print queued/Printed · e-bill sent · IRN pending/registered · Sync pending/Synced · Cancelled.

The sync chip (canonical spec, same component on all surfaces): Synced (grey-green check) · Syncing (amber + ↑ 12) · Offline (grey slash + quiet count) · Attention (red triangle — sync errors retry won't fix). State changes animate the chip only — no toast, no sound, no focus change. Clicking it (or Ctrl+, → Sync) opens a slide-over: last sync, pending ops by type, per-item errors with codes and action buttons, [Sync now], [Copy diagnostics]; Esc returns focus to the scan box.

The Focus Doctrine: on the billing screen, no system-initiated toast, dialog, popover, or focus change — not sync, not updates, not license warnings. Ambient information goes to a non-focusable notice rail above the status bar (icon + short text + key hint chips, max 3, overflow collapses to a count). The only sanctioned overlays are user-initiated by keystroke, and Esc always returns to the scan box. Back office may toast and dialog normally — it tolerates interruption; the counter does not.

5.3 i18n & bilingual printing

The full message_catalog (keyed by code, per language, per channel ui|print|whatsapp|sms, ICU MessageFormat) syncs into POS SQLite like any master — language never depends on network; per-user language applies instantly at login. Fallback: user → store default → English → (dev builds) the raw key, so gaps surface before rollout; a missing-strings report + machine-draft/human-confirm workflow makes language N a data task. Rendering rules: bundle Noto Sans + Devanagari (+ launch scripts) in app and print renderer — never trust shop-PC fonts; ASCII digits always, in every language, wherever money/qty appears; one shared formatINR() and FY-aware date formatter; layouts CI-tested against a +40% pseudo-locale (Hindi runs ~25% longer); key hints (F8) never localize.

Printing language policy lives in the template, not code: GSTIN/HSN/tax summary/legal footer always English (stable for auditors); header and thank-you in the store print language; item names per {{item.name:print}} policy (English-only default for thermal, bilingual default for A4/A5). ESC/POS text mode can't render Indic scripts — non-Latin receipts rasterize in bit-image mode; if raster pushes close-to-print past 1 s, POS suggests English thermal + bilingual A4. Fast by default, pretty on request.

5.4 Print system

print_profile(counter, purpose ∈ receipt|invoice_a4|label|report, printer, paper, template, copies, drawer_kick…)routing is a rule, not a question: B2C → thermal, B2B-with-GSTIN → A4, labels → label printer; the cashier never picks a printer mid-bill (Ctrl+P on a closed bill offers the override list). Profiles are settings rows editable from POS and back office — support fixes a store's printing remotely; the back-office template editor can queue a test page to any counter via sync. The iron rule: the bill is legal the moment it commits to SQLite; printing is a consequence, never a condition. Drawer-kick fires on payment confirm, not print success — cash must open even with a jammed printer. Failed prints stay Queued (billing is already on the next customer), retry every 15 s, auto-drain oldest-first on recovery (prompt-first is a config alternative); a job queued > 60 s with a customer phone offers the WhatsApp e-bill from the notice rail. Reprints: same-day from F10 ungated; every copy prints "DUPLICATE" and is audit-logged; older/> 2 copies → supervisor. Silent printing always — the OS dialog exists only in Settings → Advanced.

5.5 Permissions UX — two gates, two looks

Edition gates lock (and sell); role gates hide — except in-flow actions, which challenge. Locked features render normally + lock glyph + edition badge (<GateBadge> reading license-token flags); activation opens the upsell panel (screenshot/15 s demo, all DB content marketing can tune) — CTA by role: owner → in-place upgrade, staff → "Ask owner" request. Staff hitting paywalls become the sales channel. Never gate mid-bill: edition-gated POS features simply don't bind keys on the billing screen; the lock lives in Settings/menu. Role gates: navigation hidden (the cashier's world contains no Accounting menu — a smaller world trains in 10 minutes), but in-flow sensitive actions stay visible and raise the supervisor overlay (§1.7) — hiding them breaks flow and teaches nothing.

5.6 Accessibility & the training reality

Design target: a cashier who reads numbers fluently, reads text slowly, may be new to computers, and must be productive in 10 minutes. Defaults: distinct audio for scan-OK / error / attention (cashiers scan without looking — sound is the primary feedback channel); totals the largest element on screen, tabular numerals, WCAG AA high-contrast dense theme with dark mode for night shifts; every actionable button shows its key (भुगतान F11), with a printed keyboard sticker sheet in the dealer install kit — the F-row is the UI for a non-reader; touch targets ≥ 48 px, quick-keys as image + name + code (recognition beats reading); counter-scope Normal/Large font setting. Back office is semantic HTML + ARIA, keyboard-complete; POS v1's accessibility investment is keyboard + audio, full screen-reader support honestly deferred.

Practice mode (Phase 2): a Practice button on the login screen, no role needed. Separate SQLite DB seeded from a read-only copy of the store's real item master (muscle memory forms on the real catalog) + fake customers/khata, reset nightly. Unmistakable: diagonal PRACTICE / अभ्यास watermark, a reserved purple top bar, PRACTICE banner on the customer display. Contained: TRAIN-#### series (the same mechanism as onboarding step 8's training bills — never touches the GST series), prints blocked by default, no transactional sync ever leaves, drawer-kick disabled. A guided first run coaches a 5-bill script (scan, qty, unknown-barcode recovery, hold/resume, cash close, UPI, one return) with completion recorded per user ("trained ✓" in Admin); script content is catalog rows, dealer-localizable. Practice mode doubles as the dealer's in-shop demo and the upsell demo surface.

5.7 Errors a human can act on

Every error, every surface: [icon] what happened (one plain sentence) · what to do (imperative, one or two steps) · action buttons that perform the fix · a small code (E-2101) — the support-call handshake replacing five minutes of description. Codes: E-1xxx billing · 2xxx print/hardware · 3xxx sync · 4xxx payment/UPI · 5xxx GST · 6xxx auth/license · 7xxx master data · 9xxx internal. error_catalog(code, severity, title_key, body_key, actions, support_note) — texts translated and support-editable without release; support_note is the internal playbook line; code frequencies feed the tickets-per-store metric.

Presentation: billing-path errors are inline only (attached to the line or beside the scan box — never modal); ambient failures go to the notice rail + chips; blocking is reserved for exactly three conditions — shift not open, local DB integrity failure, license hard-expiry after the grace path (grace itself is a notice; never strand a shop with a queue mid-day). No raw exceptions ever: anything unmapped renders as E-9001 ("Your bills are safe. Restart; if it repeats, call support with this code") and auto-files a crash report. Destructive confirmations exist only off the billing path and name the object — never "Are you sure?". Day-one catalog examples: E-1102 barcode not found ([Enter] quick-add · [Esc] skip) · E-1310 batch expired ([Pick batch] · [Supervisor]) · E-1405 over credit limit ([Take payment] · [Supervisor]) · E-2101 printer not responding ("Bill is saved… prints automatically when it's back" · [Retry] · [Send e-bill]) · E-4203 UPI unconfirmed ([Mark paid] · [Wait] · [Cash instead]) · E-3302 cloud rejected changes ("Nothing is lost" · [Open sync panel]) · E-5110 IRN pending offline (notice only) · E-6201 subscription expiring ([Remind owner]).

Phase mapping

Search (POS profile), state vocabulary, print system, and errors are MVP-critical — they are the counter. i18n ships its mechanism in MVP (from first commit) with Hindi content in Phase 2. The supervisor PIN overlay is MVP; practice mode and the onboarding wizard + WhatsApp digest + Day-2 checklist are Phase 2; owner app, push notifications, remote approvals, Purchase Inbox, PO/GRN, and GSTR-2B recon are Phase 3. Every metric named in this document — keystrokes per flow, scan-to-line, wizard-to-first-bill, Inbox line share, approval round-trip, push opt-out rate — is instrumented from day one: we publish what we measure.