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>
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:
pb-config.jshardcodes 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.- The catalog reads from a
poscollection that is a stale, disconnected duplicate ofprice_list. Confirming a sale never decrementsstock_items.current_qtyor writes tostock_ledger— stock levels don't move when a sale happens, which is the core requirement. - The "account sale" path posts to a
delivery_note_itemscollection 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_qtyand records astock_ledgerentry, 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.jsis replaced with a small config module holding only: the base URL of this app's own/api/v1/pos/*routes, and aPOS_API_KEYvalue (set once at install time, stored inlocalStorage, 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) — activestock_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_KEYheader, checked server-side against an env var, following the same shape as the existingagent-auth.ts/AGENT_API_KEYpattern. On success, routes usepocketbase-admin.tsfor 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 newreasonselect value, e.g.pos_sale(perAGENTS.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/possearch 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
- Catalog load: POS calls
GET /api/v1/pos/catalog, which returns activestock_itemsrecords (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. - Checkout: POS posts
{ items: [{stock_item, qty, unit_price, ...}], payment_method, cash_given, customer? }toPOST /api/v1/pos/checkout.pos-service.ts:- Re-reads each
stock_itemserver-side and rejects the request ifcurrent_qtyfor a line is insufficient (no negative-stock sales). - Creates one
pos_salesrecord and itspos_sale_itemsrows. - For each line: creates a
stock_ledgerrow (reason: pos_sale, negativechange_qty) and updatesstock_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.
- Re-reads each
- 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 existingflushQueue()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. - Account sales / delivery-note printing: no
ordersordelivery_notesrecord is created by the POS.receipt.htmlgets a second print template (thermal, via the existing Web Bluetooth flow) that renders a delivery-note-style slip pulling the company/customer name from thecustomerrelation on the sale, with a toggle to include or omit prices. This is purely a print format on top of thepos_salesrecord already created — not a new workflow record in the main app. - History / Cashup:
history.htmlandcashup.htmlswitch from queryingorders/order_items(today, unfiltered — showing all orders, not just POS sales) to queryingpos_sales/pos_sale_itemsviaGET /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_itemswrite succeeds but astock_ledger/stock_itemswrite 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 instock-out.ts/stock-booking-service.tsfor compensation-needed errors is followed for consistency, without adopting their code directly. - All
/api/v1/pos/*routes reject requests missing or mismatchingPOS_API_KEYwith 401, before touching PocketBase.
Testing
pos-service.test.ts: checkout creates correctpos_sales/pos_sale_items/stock_ledgerrows and decrementscurrent_qtycorrectly; rejects checkout when stock is insufficient; catalog list only returns active items.- Route tests for
catalog,checkout,sales: 401 without a validPOS_API_KEY, happy-path shape, following this repo's existingroute.test.tsconventions (seeorders/backfill-duplicate-checks/route.test.tsfor 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_salesandpos_sale_itemscollections with the fields above and appropriate API rules (no public access; only the server's superuser credentials write to them). - Add
pos_saleas a validreasonselect value onstock_ledger. - Set a
POS_API_KEYvalue as an env var for the WFDASH-WEBAPP deployment, and configure the same value into the POS device's local config at install time.