06 — POS & 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
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.
| Step | Implementation |
|---|---|
| Configure | Admin - 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. |
| API | GET /api/pos/settings - returns device.posModules, device.fuelSite (sanitized or null), catalogs, location. |
| UI | ModernPOS.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.
| Key | Meaning |
|---|---|
lineKind | fuel |
fuelPumpId, fuelPumpLabel | Selected dispenser |
fuelPumpHoses | Optional |
fuelGradeCode, fuelGradeLabel | Optional grade |
fuelTankId, fuelProductSku | Optional 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
| Mode | Component | Purpose |
|---|---|---|
| Modern | ModernPOS.jsx | Full-featured (default); optional PosModuleHost vertical panels |
| Standard | StandardPOS.jsx | Classic grid |
| Mobile | MobilePOS.jsx | Touch-optimized |
| Kiosk | Kiosk.jsx | Self-checkout |
| Bar | BarDisplay.jsx | Bar display |
| Kitchen | KitchenDisplay.jsx | KDS |
POS Flow
- Platform login (session required for
/api/pos/*) - Register login — email + POS PIN; staff stored in
localStorage(frontend/src/lib/posLocalPersistence.js) - Register open —
POST /api/pos/register/openwithdeviceKey,openingFloatCents, andstaffIdwhen using PIN - Cart — Create, add items, discounts/tips (Modern POS: gated by
canTransactOnRegister) - Tender — Cash, card, crypto, gift card, loyalty
- 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.
| Method | Path | Purpose |
|---|---|---|
| POST | /api/pos/register/authenticate | Validate email + POS PIN → staff JSON |
| POST | /api/pos/register/open | Open shift on device |
| GET | /api/pos/register/status | Query device_key, optional staff_id / staffId |
| POST | /api/pos/register/close | Close shift |
| GET | /api/pos/register/open-shifts | Admin: open shifts |
| GET | /api/pos/register/shift-history | Closed shifts |
| POST | /api/pos/register/admin-close | Manager closes another device shift |
| POST | /api/pos/register/admin-force-signout | Manager 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
| Method | Path | Purpose |
|---|---|---|
| POST | /api/pos/carts | Create cart |
| POST | /api/pos/carts/:id/items | Add items |
| POST | /api/pos/carts/:id/tender/cash | Cash tender |
| POST | /api/pos/carts/:id/tender/crypto | Crypto tender |
| POST | /api/pos/carts/:id/finalize | Finalize → Invoice |
| GET | /api/pos/catalog | Catalog |
| GET | /api/pos/settings | Device 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.
Chapter detail: 14 — Frontend; trace: Appendix: Data Flows.