Files
ERP/docs/PILOT_READINESS.md
denis_ireri f865837b27 Address the 11 October test report findings
The initial test report (run before the pilot-readiness changes) raised
seven findings and a retest plan. This closes the ones still open.

- F-01: a business on its free trial may run 2 active branches whatever
  its tier, so an owner can try transfers and per-branch stock. Only
  active branches count towards any limit.
- F-06: owners can rename branches and deactivate/reactivate them from
  the Branches page. A branch can only be deactivated once it holds no
  stock and no active staff; its history stays in every report, and it
  leaves every picker.
- F-04: the Products page lists deactivated products under a toggle with
  Reactivate. Deactivating warns about stock still on hand, which stays
  on the Stock page so it can be transferred or counted out.
- F-05: products carry a unit (L, ml, kg, g, pcs, tray, pack) instead of
  everything being litres.
- CAT-12: product prices must be above zero and names are unique per
  business, ignoring case.
- F-03: a reversed report period is refused by the page and the API, and
  the page labels which period the cards show. Report dates are Nairobi
  business days.
- F-07: cost of goods sold is units sold x average buying price. Stock
  bought in the period is reported separately, with a cash surplus line,
  so unsold stock no longer shows as a loss.
- STA-04: staff and new branch managers can be invited with a one-time,
  7-day link; the person sets their own password. Managers can issue a
  new link for their own cashiers.
- OFF-02 / POS-02: the till shows each product's stock and deducts every
  sale immediately, online or offline; it warns before selling more than
  the recorded stock, and has a Clear sale button.
- npm run backfill:stock turns purchases recorded before the stock ledger
  into stock, so existing tenants (and the report's ZERRO fixtures) start
  from what they bought.

tests/retest-plan.test.ts runs the report's completion and retest plan
and checks every balance and total it predicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:29:21 +03:00

8.5 KiB

Pilot readiness changes

This branch (fix/pilot-readiness) closes the gaps found when the MVP was reviewed against PRD.md and REVIEW.md. This page records what changed, the decisions taken along the way, and what is still open.

What changed

Tenant isolation and roles (PRD 5.1)

  • Every role-limited API route goes through authorize() in src/lib/server/auth.ts: 401 when signed out, 403 for the wrong role. Cashiers can no longer read profit, expenses or purchases, or record expenses and purchases. Only the owner creates or edits products and prices.
  • PATCH /api/users/:id deactivates and reactivates staff (managers: only their own cashiers). Deactivating or moving a user revokes their device sessions.
  • PATCH /api/branches/:id renames a branch (owner only).
  • Login and token refresh refuse an inactive or SUSPENDED business.
  • Public signup refuses any address in PLATFORM_ADMIN_EMAILS.
  • Ids in query strings and bodies are validated (400, not a 500), inventory routes check the product belongs to the tenant, and reconciliation lookups are tenant-scoped.

Stock ledger and transfers (PRD 5.2)

  • StockMovement is the ledger: a branch's stock of a product is the sum of its movements. Each movement names its cause, and (type, refId) is unique, so a replayed sale or retried transfer never moves stock twice.
  • Sales and confirmed M-Pesa payments deduct stock; purchases of a catalogue product add it.
  • POST /api/stock/transfers moves stock between branches. POST /api/stock/adjustments records opening stock, breakage and physical counts. GET /api/stock returns stock per branch with LOW/OUT flags (per-product lowStockThreshold, default 10).
  • The admin Stock page (/admin/stock) covers all of this, plus the existing morning dispatch and evening count. The dashboard shows stock on hand per branch.

Offline POS and sync (PRD 5.3, 5.5)

  • POST /api/transactions/cash answers each sale with saved, duplicate or rejected. Previously a replayed sale was skipped silently, the device never marked it synced, and once more than 100 rows piled up every sync failed.
  • The device queue (src/lib/offlineQueue.ts) is keyed by cashier and branch, sends only the signed-in cashier's sales, chunks to 50 per request, and stops retrying rejected sales (they are shown to the cashier instead).
  • A service worker (public/sw.js) caches the app shell, and the last session and product list are kept on the phone, so the terminal opens and sells with no network.
  • Sales can contain several products, the customer phone is optional for cash, and the terminal shows a printable receipt.

Other

  • "Today" and "this month" follow Africa/Nairobi, not the server's UTC clock (src/lib/server/businessDay.ts).
  • M-Pesa is behind FEATURE_MPESA (off by default; out of scope for the pilot per PRD section 4).

Decisions taken

Decision Why
Offline sales are always accepted, even if they take stock below zero. The sale already happened. Refusing it would lose real cash. Negative stock is shown on the Stock page and fixed with a physical count.
Transfers are refused when the sending branch lacks the stock. Transfers are made online by a manager, so they can be checked.
The server price is authoritative; a different price shown on the device is kept (clientUnitPrice) and flagged (priceMismatch). Trusting the device's price would let a cashier under-charge. Overwriting it silently would hide a cash discrepancy.
A queued sale stays with the branch where it was recorded, even if the cashier has since moved. That is where the stock left and the cash was taken.
A product deactivated while a phone was offline is still recorded at its price. Same reason as the first row: the sale happened.

Rolling out to an existing database

  • No migration is needed: new fields have defaults, and Mongoose creates the new indexes on start-up.
  • Existing branches start with no stock on record. Before the pilot, each branch should record an opening count on the Stock page.
  • Cashiers' phones upgrade their offline queue automatically; any unsynced sales from the old version are sent on the next sync.

Testing

  • npm test runs 50 API tests against an in-memory MongoDB, covering role enforcement, tenant isolation for sales, stock and transfers, suspension, the sync protocol (including a 120-sale backlog replayed twice), and the business day.
  • The offline flow was also checked end to end in headless Chrome: sign in, stop the server, reload the terminal, record two sales, restart the server, confirm they sync once and deduct stock.

Follow-ups from the 11 October 2026 test report

The initial test report (run on the build before these changes) raised seven findings and a retest plan. All of them are addressed on fix/test-report-followups:

Finding What changed
F-01 Trial allows one branch A business on its free trial may run 2 active branches whatever its tier (TRIAL_MAX_BRANCHES in src/lib/tiers.ts). After the trial the tier's limit applies to adding branches.
F-02 No current stock Done in the previous change (stock ledger). Purchases recorded before the ledger existed are added by npm run backfill:stock.
F-03 Reversed report dates Refused in the page and by the API (400). The page labels which period the cards show. Report dates are Nairobi business days.
F-04 No way back for deactivated products Products page lists deactivated products under a toggle, with Reactivate. Deactivating warns about stock still on hand; that stock stays on the Stock page and can be transferred or counted out.
F-05 Eggs shown as litres Products carry a unit (L, ml, kg, g, pcs, tray, pack), chosen when the product is created and shown everywhere.
F-06 No branch or staff removal Staff: deactivate/reactivate (previous change). Branches: rename and deactivate/reactivate on the Branches page. A branch can only be deactivated once it has no stock and no active staff; its history stays in reports.
F-07 Purchases shown as cost of goods Cost of goods sold is now units sold x the product's average buying price. Purchases in the period are shown separately as "Stock purchased", with a "Cash surplus" line.
STA-04 No invites Staff and new branch managers can be invited: the owner or manager gets a one-time link (7 days) to send, and the person sets their own password. A new link can be made from Staff.
CAT-12 Zero price, duplicates Prices must be above zero; product names are unique per business, ignoring case.
OFF-02 Local stock deduction The till shows each product's stock and deducts every sale immediately, online or offline.
POS-02 / INV-05 Oversell, cancel The till warns before selling more than the recorded stock and asks to confirm (it does not block, because the sale may already have happened). A "Clear sale" button empties the cart.

tests/retest-plan.test.ts runs the report's completion and retest plan (online sale, transfer, two offline sales replayed twice, a Branch B sale) and checks every balance and total it predicts.

Before re-running the test report's plan on ZERRO

  1. Deploy, then run npm run backfill:stock once so the existing test purchases count as stock. Run it before anyone records an opening count, or those units would be counted twice.
  2. ZERRO is trialling, so it can now add Branch B without a plan change.
  3. Create the manager and cashier logins with invite links from Staff.

Still open

  • Setting branch prices. Branch price overrides are applied everywhere, but there is no screen or API to set them yet.
  • Global email and phone uniqueness. One person cannot work for two businesses, and signup reveals whether an address is already registered.
  • Isolation by convention. Tenant filters are still written per route. The tests cover the sales, stock, transfer and access paths, not every route.
  • Concurrent transfers. Two transfers submitted at the same moment could together send more than the branch holds.
  • Cost of goods is an average. It uses each product's average buying price across the business, not first-in-first-out per branch. Units sold with no purchase on record are counted and flagged, but have no cost.
  • M-Pesa, if turned on. Only one product per payment. A prompt that times out still needs a manual check before taking cash.
  • Not modelled yet: email verification at signup, sending invites by email (links are shared by hand, e.g. WhatsApp), empty-bottle and deposit tracking, home-screen icons for the PWA.