Stock (PRD 5.2): a branch's stock of a product is now the sum of its
StockMovement rows. Every movement names the document that caused it and
(type, refId) is unique, so replaying a sale, retrying a transfer or a
second M-Pesa callback never moves the same stock twice.
- Cash sales and confirmed M-Pesa payments deduct stock; purchases of a
catalogue product add it.
- POST /api/stock/transfers moves stock between two branches (owner: any
two; manager: out of their own) and refuses to send more than the
sending branch holds. Double submits are applied once.
- POST /api/stock/adjustments records opening stock, breakage or a
physical count (the ledger is corrected to the counted number).
- GET /api/stock returns every branch x product with LOW/OUT flags
(per-product lowStockThreshold, default 10); GET /api/stock/movements
is the history.
Sync (PRD 5.3/5.5): POST /api/transactions/cash now returns a result per
sale - saved, duplicate or rejected - instead of silently skipping
replays. The old client never learned a replayed sale had synced, so
rows piled up until the batch passed the 100-sale limit and every flush
failed with 400. Also:
- A sale can hold several products (one Transaction per line, grouped by
saleId); the old single-product payload is still accepted.
- One bad sale is rejected on its own instead of failing the batch.
- A queued sale is only accepted from the cashier who recorded it, and
stays with the branch where it happened.
- A product deactivated while offline is still recorded at its price; an
invalid phone is dropped rather than losing the sale; the phone is now
optional for cash sales.
- The price the device showed is kept and a difference is flagged
(priceMismatch) instead of being silently overwritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The admin pages redirected cashiers away, but the API itself let them read
profit, expenses and purchases and record expenses and purchases. Branch
managers could also change product prices for every branch. Every gated
route now goes through authorize(), which answers 401 when signed out and
403 for the wrong role.
- Owners and managers only: profit report, expenses, purchases, users,
morning dispatch and evening count. Owner only: create and edit products,
rename branches.
- PATCH /api/users/:id deactivates or reactivates staff (managers: their own
cashiers only) and revokes the user's device sessions. PATCH
/api/branches/:id renames a branch.
- Login and token refresh refuse inactive or SUSPENDED businesses.
- Public signup refuses PLATFORM_ADMIN_EMAILS addresses, so an unregistered
operator email can no longer be claimed.
- M-Pesa sits behind FEATURE_MPESA (off by default, out of scope for the
pilot): its routes return 404 and the Daraja variables are optional.
Turning it on in production requires the callback IP allowlist.
- "Today" and the default month now follow Africa/Nairobi instead of the
server's UTC clock, so the business day no longer starts at 03:00 EAT.
- Malformed ids in query strings and bodies return 400 instead of a 500,
the inventory routes check the product belongs to the tenant, and the
reconciliation lookups are tenant-scoped.
- Branch-scoped staff see their branch's override price from GET /api/products.
- Adds a vitest suite against an in-memory MongoDB (npm test).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>