There is one source of truth (stock_ledger) and one denormalised cache (stock_items.current_qty). Every stock movement — buying from a supplier, booking a surplus, fulfilling a customer order, ringing up a sale at the till — writes a ledger row first, then mirrors the qty into the cache. The POS app never touches the cache directly; it goes through three authenticated API endpoints.
Two PocketBase collections carry the entire stock system. The ledger is append-only and signed; the cache is denormalised for fast reads.
The catalogue. One row per SKU. Carries all the pricing, supplier linkage, and the running quantity the UI reads.
The audit trail. Every stock movement — in or out — is a row here with a signed quantity and a reason. The cache is rebuildable from this table alone.
Three entry points. All three end in stock_ledger.reason ∈ {stock_order, purchase_surplus, adjustment}.
Warehouse marks the PO received (stock_purchases.status = "received"). Accountant then hits "Book into stock" on the invoicing page.
Partial lines supported — each line carries its own quantity_received accumulator.
When the supplier delivers more than the customer ordered (e.g. MOQ), the driver records qty_collected on the order item. Operator splits the difference into stock.
Match by SKU → by supplier + normalised description. Operator can override.
Operator counts the shelf, finds a discrepancy vs. the cache, and posts a signed correction.
Compensation tagging kicks in if the cache write fails after the ledger row lands.
Three exit points. All three write stock_ledger.reason = "customer_fulfillment" with a negative quantity (or "adjustment" for write-offs).
Operator on /orders/new ticks "fulfil from stock" for a line. Same route used on /orders/[id]/process to redirect an existing line.
`stock_booked_from` back-link written on the order_item for audit + drill-down.
External WFSALES POS app (Flutter, separate repo) calls a Bearer POS_API_KEY-protected endpoint, commits a sale, and decrements stock in one transaction.
Idempotent on client_ref — offline till retries don't double-decrement.
Damaged in the warehouse, expired, lost, given to a sample kit. Same /stock/[id]/adjust route as Path C, just a negative qty.
One route handles both directions — the sign of the qty is the difference.
The WFSALES POS is a separate app (Flutter) that runs on the office till. It only ever talks to WFDASH through three authenticated endpoints under /api/v1/pos/*. The auth is a device-scoped bearer token (POS_API_KEY) — distinct from the agent's key, because the till only needs catalog + checkout + history.
Three pricing fields on a stock row — cost_price, selling_price, markup. Each is set by a different trigger.
When the accountant books a stock purchase, the line's cost_price overwrites the stock row's cost_price and appends an immutable entry to cost_history.
Operator edits the shelf price or markup % directly on the stock detail page. If cost changes, a zero-qty ledger row is written as an audit record (reason: adjustment, ref: manual).
External agent (Hermes) keeps the catalogue in sync with supplier price lists — e.g. when a price list PDF re-baselines, it PATCHes the selling_price field through the agent API.
Green = stock-in. Orange = stock-out. Purple = POS surfaces. Everything terminates in the same ledger.
flowchart TB
classDef in fill:#e8f1ed,stroke:#2f6f5e,color:#14171e
classDef out fill:#faeede,stroke:#c97b3f,color:#14171e
classDef pos fill:#ede9f8,stroke:#5a4ec9,color:#14171e
classDef cache fill:#ffffff,stroke:#7c7a72,color:#14171e,stroke-dasharray: 4 3
classDef truth fill:#f4f1ea,stroke:#14171e,color:#14171e
subgraph SOURCES["Stock-in sources"]
direction TB
SP["stock_purchase
(accountant books)"]:::in
SU["customer order
(supplier over-delivers)"]:::in
ADJ_IN["manual adjustment
(stocktake correction)"]:::in
end
subgraph OUTS["Stock-out destinations"]
direction TB
CO["customer order
(fulfil from stock)"]:::out
PSALE["POS sale
(WFSALES till)"]:::pos
ADJ_OUT["manual write-off
(damage / loss)"]:::out
end
LEDGER[("stock_ledger
(append-only, signed qty)")]:::truth
CACHE[("stock_items.current_qty
(denormalised cache)")]:::cache
POS_APP["WFSALES POS app
(Flutter, separate repo)"]:::pos
POS_APP -- GET /api/v1/pos/catalog --> CACHE
POS_APP -- POST /api/v1/pos/checkout --> PSALE
SP -- "bookStockFromInvoice
reason: stock_order" --> LEDGER
SU -- "bookSurplus
reason: purchase_surplus" --> LEDGER
ADJ_IN -- "stock_items.adjust
reason: adjustment" --> LEDGER
CO -- "stock/fulfill
reason: customer_fulfillment" --> LEDGER
PSALE -- "checkoutSale
reason: customer_fulfillment
ref: pos_sale" --> LEDGER
ADJ_OUT -- "stock_items.adjust
reason: adjustment" --> LEDGER
LEDGER -. "update current_qty" .-> CACHE
WFDASH_UI["WFDASH /stock, /stock-orders,
/orders/[id] (operator UI)"]
WFDASH_UI -- "reads" --> CACHE
WFDASH_UI -- "drills into" --> LEDGER