WFDASH-WEBAPP/docs/superpowers/specs/2026-08-30-wfsales-pos-design.md
Dave 0ba9e59f82 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>
2026-08-30 23:50:56 +02:00

9.6 KiB

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.