docs(pos): add design spec for WFSALES POS app

Scopes hardening the existing POS mockup (superuser creds removed,
catalog moved to stock_items, checkout wired to stock_ledger) as a
route inside WFDASH-WEBAPP rather than a separate service.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-08-30 23:50:56 +02:00
parent 8888a4722c
commit 0ba9e59f82

View file

@ -0,0 +1,183 @@
# WFSALES POS — Design
## Problem
An existing POS mockup (`pos.html`, `receipt.html`, `history.html`, `cashup.html`,
`pos-app.js`, service worker — built outside this repo, in Open Design) already
implements catalog browsing, cart, cash/card payment, an offline sale queue,
and Bluetooth thermal-printer receipt printing. It is not safe or correctly
wired to go live:
1. `pb-config.js` hardcodes a PocketBase **superuser** email/password in plain
text inside a static, client-shipped JS file. Anyone who can view the page
source gets full admin access to the entire PocketBase instance — every
collection, not just stock.
2. The catalog reads from a `pos` collection that is a stale, disconnected
duplicate of `price_list`. Confirming a sale never decrements
`stock_items.current_qty` or writes to `stock_ledger` — stock levels don't
move when a sale happens, which is the core requirement.
3. The "account sale" path posts to a `delivery_note_items` collection that
does not exist in the PocketBase schema, so it silently fails today.
Separately (confirmed by codebase research, out of scope for this project):
stock-quantity writes are already fragmented across 4 independent call sites
in the main app (`stock-surplus.ts`, `stock-out.ts`, the `/api/v1/stock/fulfill`
route, and the MCP `stock_items.adjust` handler), and delivery notes in the
main app never touch `stock_items` at all. This project does not attempt to
unify those — it adds one more clean, self-contained stock-write path scoped
to POS checkout only.
## Goals
- Counter staff can browse live stock, build a cart, and take a cash or card
payment on a device running this app.
- Confirming a sale correctly and atomically-as-possible decrements
`stock_items.current_qty` and records a `stock_ledger` entry, so stock
shown in the POS and in the main WFDASH app stays accurate.
- No PocketBase credentials of any kind are ever shipped to the client.
- Existing UX (catalog/cart/payment screens, offline queue, Bluetooth thermal
printing) is preserved and reused, not rewritten.
- Account (pay-later) sales can print a delivery-note-style thermal slip
(with or without prices) referencing the customer/company, without creating
a new order/delivery-note record in the main workflow.
## Non-goals
- Unifying the 4 existing divergent stock-adjustment code paths in the main
app.
- Making delivery notes in the main WFDASH app decrement stock.
- Per-cashier login/PIN or staff attribution — this is a shared till.
- Real payment gateway integration — card payments are recorded as "paid via
the physical card machine, no change due"; the POS does not process cards
itself.
## Architecture
**Frontend** stays the existing static PWA, relocated into
`public/pos/` in WFDASH-WEBAPP and served at `/pos/*`:
- `index.html`, `pos.html`, `receipt.html`, `history.html`, `cashup.html`,
`pos-app.js`, `theme.js`, `sw.js`.
- No React/Next.js page rewrite. The offline-first behavior (IndexedDB cart
and sale queue, service worker asset caching, Web Bluetooth thermal
printing) already works client-side and has no SSR dependency.
- `pb-config.js` is replaced with a small config module holding only: the
base URL of this app's own `/api/v1/pos/*` routes, and a `POS_API_KEY`
value (set once at install time, stored in `localStorage`, never a
PocketBase credential of any kind).
**Backend** follows this repo's existing service-layer convention
(`AGENTS.md`: new features → `src/lib/server/<feature>-service.ts` + thin
`src/app/api/v1/<feature>/<op>/route.ts`):
- `src/lib/server/pos-service.ts` — business logic: list catalog, checkout
(create sale + sale items + stock ledger/qty writes), list sales for
history/cashup.
- `src/app/api/v1/pos/catalog/route.ts` (GET) — active `stock_items`.
- `src/app/api/v1/pos/checkout/route.ts` (POST) — confirm a sale.
- `src/app/api/v1/pos/sales/route.ts` (GET) — sales for history/cashup, with
a date-range filter.
- All three routes require a `POS_API_KEY` header, checked server-side
against an env var, following the same shape as the existing
`agent-auth.ts` / `AGENT_API_KEY` pattern. On success, routes use
`pocketbase-admin.ts` for the actual PocketBase access — real credentials
never leave the server.
## Data model
New PocketBase collections (created by the user in PB admin — this repo
never runs schema migrations against the live instance):
- **`pos_sales`**: `sale_number` (text), `payment_method` (select:
`cash`, `card`), `cash_given` (number, nullable), `change_given` (number),
`subtotal` (number), `vat` (number), `total` (number), `customer`
(relation → `customers`, optional), `synced_at` (date).
- **`pos_sale_items`**: `sale` (relation → `pos_sales`), `stock_item`
(relation → `stock_items`), `sku` (text), `description` (text), `qty`
(number), `unit_cost` (number), `unit_price` (number).
Existing collections reused as-is:
- `stock_items` — catalog source and quantity target (`current_qty`).
- `stock_ledger` — one row per sale line on checkout. Needs one new
`reason` select value, e.g. `pos_sale` (per `AGENTS.md`: select values are
added in PB admin first, by the user, before this code path can use them).
- `customers` — for account-sale customer lookup (existing `/pos` search
already queries this).
The `pos` collection (stale price_list duplicate) is no longer read by the
app after this change. It is left in place (not deleted) — deleting it is a
separate decision for the user to make later.
## Data flow
1. **Catalog load**: POS calls `GET /api/v1/pos/catalog`, which returns
active `stock_items` records (id, sku, description, category, unit,
`current_qty`, `selling_price`, `cost_price`). Cached client-side in
IndexedDB exactly as the existing mockup does today, for offline browsing.
2. **Checkout**: POS posts `{ items: [{stock_item, qty, unit_price, ...}],
payment_method, cash_given, customer? }` to `POST /api/v1/pos/checkout`.
`pos-service.ts`:
- Re-reads each `stock_item` server-side and rejects the request if
`current_qty` for a line is insufficient (no negative-stock sales).
- Creates one `pos_sales` record and its `pos_sale_items` rows.
- For each line: creates a `stock_ledger` row (`reason: pos_sale`,
negative `change_qty`) and updates `stock_items.current_qty`. This is
one clean, self-contained helper — not shared with or refactored from
the 4 existing divergent stock-write call sites in the main app.
- Returns the created sale (for the receipt screen) or an error.
3. **Offline behavior**: unchanged from the existing mockup — if the
checkout call fails (offline, network error, server error), the sale is
queued locally (IndexedDB, falling back to `localStorage`) and the UI
still proceeds to the receipt screen immediately (never blocks on
network). The existing `flushQueue()` retry-on-reconnect logic is kept.
Stock is only decremented once a queued sale successfully lands on the
server, so a sale that never syncs never affects stock, and a sale is
never double-decremented.
4. **Account sales / delivery-note printing**: no `orders` or
`delivery_notes` record is created by the POS. `receipt.html` gets a
second print template (thermal, via the existing Web Bluetooth flow) that
renders a delivery-note-style slip pulling the company/customer name from
the `customer` relation on the sale, with a toggle to include or omit
prices. This is purely a print format on top of the `pos_sales` record
already created — not a new workflow record in the main app.
5. **History / Cashup**: `history.html` and `cashup.html` switch from
querying `orders`/`order_items` (today, unfiltered — showing all orders,
not just POS sales) to querying `pos_sales`/`pos_sale_items` via
`GET /api/v1/pos/sales?from=&to=`.
## Error handling
- Checkout validates stock availability server-side before writing anything;
insufficient stock returns a 4xx with enough detail for the UI to show
which line is short.
- If the `pos_sales`/`pos_sale_items` write succeeds but a `stock_ledger`/
`stock_items` write fails partway through a multi-line sale, the response
indicates partial failure (sale exists, some lines may not have decremented
stock) so it can be reconciled manually — the existing pattern in
`stock-out.ts`/`stock-booking-service.ts` for compensation-needed errors is
followed for consistency, without adopting their code directly.
- All `/api/v1/pos/*` routes reject requests missing or mismatching
`POS_API_KEY` with 401, before touching PocketBase.
## Testing
- `pos-service.test.ts`: checkout creates correct `pos_sales`/
`pos_sale_items`/`stock_ledger` rows and decrements `current_qty`
correctly; rejects checkout when stock is insufficient; catalog list only
returns active items.
- Route tests for `catalog`, `checkout`, `sales`: 401 without a valid
`POS_API_KEY`, happy-path shape, following this repo's existing
`route.test.ts` conventions (see `orders/backfill-duplicate-checks/route.test.ts`
for the pattern).
- Manual verification of the frontend (catalog browse, cart, cash/card
checkout, receipt + thermal print, offline queue/flush, history, cashup)
against the new endpoints, since the frontend itself is plain JS/HTML with
no existing test harness in this repo.
## Open items for the user (PocketBase admin, manual)
- Create `pos_sales` and `pos_sale_items` collections with the fields above
and appropriate API rules (no public access; only the server's superuser
credentials write to them).
- Add `pos_sale` as a valid `reason` select value on `stock_ledger`.
- Set a `POS_API_KEY` value as an env var for the WFDASH-WEBAPP deployment,
and configure the same value into the POS device's local config at
install time.