06 — POS & Invoicing

POS flow, cart, tender, finalize, invoicing

CEO Summary

POS is the primary revenue capture point. Every sale flows: cart → items → tender → finalize → invoice. Invoicing is the backbone of all money movement. Business value: Direct revenue. Key metrics: Transaction volume, average ticket, tender mix. If this breaks: No sales can be processed.

POS Sale Flow

sequenceDiagram participant User participant ModernPOS participant API participant POSRoutes participant CoreInvoicing participant DB User->>ModernPOS: Click Finalize ModernPOS->>API: POST /api/pos/carts/:id/finalize API->>POSRoutes: requireSessionAuth, tenantResolver POSRoutes->>CoreInvoicing: createPOSSaleInvoice CoreInvoicing->>DB: INSERT Invoices, InvoiceLineItems DB-->>CoreInvoicing: OK CoreInvoicing-->>POSRoutes: invoice POSRoutes-->>ModernPOS: { ok, invoice } ModernPOS-->>User: Receipt, cart cleared

Modern POS vertical modules (forecourt / fuel)

Modern POS can load optional UI modules based on the device record, not the cart. Module IDs come from Devices.metadata as posModules or legacy pos_modules (array or comma-separated string). The backend only exposes whitelisted IDs from backend/utils/posModules.js — currently fuel for forecourt / pump-style workflows.

StepImplementation
ConfigureAdmin - Device management - Device registry (/admin/devices/manager): edit a POS device, enable Forecourt / fuel module (posModules: ["fuel"]), then fill Forecourt site config (FMS) (pumps, grades, authorization mode, integration, POS panel options) stored as metadata.fuelSite. JSON shape: 13 - Bridge & Devices.
APIGET /api/pos/settings - returns device.posModules, device.fuelSite (sanitized or null), catalogs, location.
UIModernPOS.jsx - PosModuleHost - FuelModulePanel: pump chips and full pump select, optional grade, preview amount; add line requires at least one pump in fuelSite.

Fuel cart line metadata

Fuel lines use POST /api/pos/carts/:id/items with line metadata so FMS and reporting know which pump and grade apply.

KeyMeaning
lineKindfuel
fuelPumpId, fuelPumpLabelSelected dispenser
fuelPumpHosesOptional
fuelGradeCode, fuelGradeLabelOptional grade
fuelTankId, fuelProductSkuOptional from grade row

Discovery: Admin control panel search (/admin) keywords include fuel/forecourt to find Device registry; Business Center does not edit this (16 — Business Center). Live pump I/O will use the site bridge; amounts may stay preview until integrated.

POS Modes

ModeComponentPurpose
ModernModernPOS.jsxFull-featured (default); optional PosModuleHost vertical panels
StandardStandardPOS.jsxClassic grid
MobileMobilePOS.jsxTouch-optimized
KioskKiosk.jsxSelf-checkout
BarBarDisplay.jsxBar display
KitchenKitchenDisplay.jsxKDS

POS Flow

  1. Platform login (session required for /api/pos/*)
  2. Register login — email + POS PIN; staff stored in localStorage (frontend/src/lib/posLocalPersistence.js)
  3. Register open — POST /api/pos/register/open with deviceKey, openingFloatCents, and staffId when using PIN
  4. Cart — Create, add items, discounts/tips (Modern POS: gated by canTransactOnRegister)
  5. Tender — Cash, card, crypto, gift card, loyalty
  6. Finalize → Invoice

Register / shift APIs

Mounted at /api/pos with requireSessionAuth + tenantResolver (backend/routes/pos-register.routes.js). Status prefers org_id + device_key; falls back to open shift by device_key only if needed. Deep dive: Appendix B (markdown), Data flows.

MethodPathPurpose
POST/api/pos/register/authenticateValidate email + POS PIN → staff JSON
POST/api/pos/register/openOpen shift on device
GET/api/pos/register/statusQuery device_key, optional staff_id / staffId
POST/api/pos/register/closeClose shift
GET/api/pos/register/open-shiftsAdmin: open shifts
GET/api/pos/register/shift-historyClosed shifts
POST/api/pos/register/admin-closeManager closes another device shift
POST/api/pos/register/admin-force-signoutManager force sign-out

Kitchen tickets (KDS)

When a cart is finalized (or sent manually with POST /api/pos/carts/:id/send-to-kitchen), preparable lines can create rows in kitchen_tickets for the kitchen display. Station assignment uses product metadata, optional recipes, and tenant category → station rules configured in admin. See 09 — KDS & Kitchen.

Barcode Scan & Platform Catalog

ModernPOS (frontend/src/pages/pos/ModernPOS.jsx): after resolving SKU/barcode against the loaded POS catalog, if there is no match the client calls GET /api/platform-catalog/lookup?barcode=.... When the platform has metadata, a synthetic line item is added (default $0.00 unless attributes.suggestedRetailCents / msrpCents is set on the master catalog row). User sees a short toast to verify price. See Home — Platform catalog and 02 — Databases.

Key APIs

MethodPathPurpose
POST/api/pos/cartsCreate cart
POST/api/pos/carts/:id/itemsAdd items
POST/api/pos/carts/:id/tender/cashCash tender
POST/api/pos/carts/:id/tender/cryptoCrypto tender
POST/api/pos/carts/:id/finalizeFinalize → Invoice
GET/api/pos/catalogCatalog
GET/api/pos/settingsDevice config: location, catalogs, device.posModules, device.fuelSite (forecourt JSON from metadata, sanitized)

Invoicing

Service: coreInvoicing.service.js — createPOSSaleInvoice

Model: invoice.model.js — orgId, invoiceNumber, lines (JSON), paymentMethod, status

Statuses: draft, pending, paid, partially_paid, cancelled, refunded

Offline Support & idempotent replay

Mutating cart calls go through frontend/src/pos/api.js (posApi) → tryOnlineThenQueue.js: one generated mutation envelope per logical action so the same Idempotency-Key is used for the live request and for any queued replay. queue.js persists jobs in browser IndexedDB pawspos_offline (legacy ppos_offline_v1 is migrated automatically). public/sw.js shares that DB name for background sync. Surfaces: ModernPOS, StandardPOS, Kiosk, Storefront.

sequenceDiagram participant User participant POS as POS UI posApi participant Try as tryOnlineThenQueue participant APIS as Express api pos participant IDB as IndexedDB pawspos_offline User->>POS: Add item / tender / finalize POS->>Try: Same mutation meta + headers alt Online success Try->>APIS: POST carts + Idempotency-Key + X-PPO-* APIS-->>Try: 2xx + body Try-->>POS: Response else Network or 5xx Try->>APIS: POST fails Try->>IDB: enqueue job + headers + mutation Try-->>POS: queued Note over IDB,APIS: Replay uses mergeHeadersForReplay IDB->>APIS: Same Idempotency-Key end

Chapter detail: 14 — Frontend; trace: Appendix: Data Flows.