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:
parent
8888a4722c
commit
0ba9e59f82
1 changed files with 183 additions and 0 deletions
183
docs/superpowers/specs/2026-08-30-wfsales-pos-design.md
Normal file
183
docs/superpowers/specs/2026-08-30-wfsales-pos-design.md
Normal 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.
|
||||
Loading…
Reference in a new issue