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>