# 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 37 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. ## Still open - **Removing a branch.** Branches can be renamed but not removed. Someone needs to decide what happens to a removed branch's stock and history. - **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. - **Cash-basis profit.** The profit report counts all purchases in the period as cost of goods. - **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:** invites and email verification, empty-bottle and deposit tracking, home-screen icons for the PWA.