# Appendix B — Complete POS Flow (Step-by-Step)

**Developer-level trace of Create Cart → Add Items → Tender → Finalize**

---

## Prerequisites

1. **Platform login** — User must have valid session (`POST /api/auth/login` or `POST /api/auth/oauth/login`)
2. **Tenant resolution** — `req.tenant.orgId`, `req.tenant.slug` set by tenantResolver
3. **Optional:** Register login (`POST /api/pos/register/authenticate`) — email + PIN for staff
4. **Optional:** Device context — deviceKey, locationId for kiosk/mobile

### Optional: Modern POS device settings and vertical modules

- **Route:** `GET /api/pos/settings` (same `posRouter` mount as carts; session + tenant).
- **Returns:** Device row (catalog assignments, location) and `device.posModules` parsed from `Devices.metadata` (`backend/utils/posModules.js`). Only whitelisted module IDs are returned; today `fuel` enables the forecourt UI via `PosModuleHost` / `FuelModulePanel` in `ModernPOS.jsx`.
- **Admin:** Configure metadata on the POS device at `/admin/devices/manager` (not Business Hub). See HTML chapters **06** and **13**.

### Register and shift before cart (Modern POS)

**Mount:** `backend/server.js` mounts `posRegisterRouter` at `/api/pos` with **`requireSessionAuth`** and **`tenantResolver`** (same path prefix as `posRouter`; register routes win where defined). So every register endpoint assumes a **platform session**; PIN auth does not replace that — it attributes the cashier.

**Two layers of identity**

1. **Platform session** — `req.user` from `requireSessionAuth`; org/tenant from `tenantResolver`.
2. **POS PIN session (browser)** — After `POST /api/pos/register/authenticate`, the client stores staff JSON in **`localStorage`** via `frontend/src/lib/posLocalPersistence.js` (keys like `pos_pin_session_v1:{tenantScope}:{deviceKey}`). Active cart id may live under `pos_active_cart_v1:*`. Keys are tenant-scoped (`pawspos_tenant_slug` / org); the module can scan keys when slug loads late.

**APIs** (`backend/routes/pos-register.routes.js`, all under `/api/pos`)

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/register/authenticate` | Email + POS PIN → `{ staff }` (validates only; no new session cookie) |
| POST | `/register/open` | Open shift: body `deviceKey`, `openingFloatCents`, optional **`staffId`** (PIN staff drives `register_shifts.user_id` / `staff_id` when present) |
| GET | `/register/status` | Query: `device_key` or `deviceKey`; optional `staff_id` or `staffId` (from PIN). Response: `isOpen`, `isOpenForCurrentUser`, `shift` with `staffMemberId` / `staff` |
| POST | `/register/close` | Close current user’s shift on device |
| GET | `/register/open-shifts` | Admin: open shifts |
| GET | `/register/shift-history` | Closed-shift history |
| POST | `/register/admin-close` | Manager closes another device’s open shift |
| POST | `/register/admin-force-signout` | Manager forces cashier sign-out |

**Backend behavior (status)**

- Prefer `register_shifts` row with `org_id` + `device_key` + `status = 'open'`.
- If none, **fallback** match by **`device_key` only** (logs a warning) so a session/org mismatch does not hide an open shift.
- Staff display for the shift uses **`loadStaffInfoForOpenShift`**: resolve by `staff_id` on the row first, then **`Staff` link column** vs `user_id` — do not assume `Staff.id = shift.user_id`.

**Frontend** (`frontend/src/pages/pos/ModernPOS.jsx`)

- Status URL includes **`device_key`** and, when present, **`staff_id`** from the PIN session.
- **`canTransactOnRegister`** — true if register is open **for the session user** *or* open shift’s staff matches the PIN staff (`shift.staffMemberId` / `shift.staff.id`).
- **`applyRegisterStatusFromPayload`** — opening-float UI when server confirms no open shift; if PIN + saved cart exist but first status looks “closed”, **retries** status after a short delay (tenant timing).
- **`blockIfRegisterNotReadyForSale`** should gate sales using **`canTransactOnRegister`** (not only `registerOpenForCurrentUser`).
- Admin: **Open registers** and shift history under Admin → Operations.

**DB:** `register_shifts` (tenant main); schema ensured by `ensureRegisterShiftsSchema` in the route file.

---

## Step 1: Create Cart

**Route:** `POST /api/pos/carts`

**Middleware:** requireSessionAuth, tenantResolver, optionalDeviceAuth

**Handler:** `backend/routes/pos.routes.js` (lines ~522–566)

**Request body:**
```json
{
  "currency": "USD",
  "taxRateBps": 0,
  "discountCents": 0,
  "tipCents": 0,
  "locationId": null,
  "metadata": {}
}
```

**Flow:**
1. Validate `orgId`, `tenantSlug` from `req.tenant`
2. Call `posQueries.createCart(tenantSlug, { orgId, locationId, staffId, currency, taxRateBps, discountCents, tipCents, items: [], status: 'open', metadata })`
3. `posQueries` uses `getTenantConnection(tenantSlug, 'main')` → tenant main DB
4. Insert into `pos_carts` table
5. `transactionTrackingService.trackCartEvent('cart_created', cart, {...})`
6. Return `{ ok: true, cart, totals }`

**DB tables:** `pos_carts` (tenant main)

---

## Step 2: Add Items

**Route:** `POST /api/pos/carts/:id/items`

**Handler:** `backend/routes/pos.routes.js` (lines ~850–1042)

**Request body:**
```json
{
  "items": [
    {
      "productId": 123,
      "name": "Coffee",
      "priceCents": 350,
      "qty": 1,
      "sku": "COFFEE-001"
    }
  ]
}
```

**Flow:**
1. Get cart by ID
2. For each item: resolve product by `productId` or `sku` or `name` via `posQueries.findProduct()` or `getProductById()`
3. Aggregate quantities (same productId/sku merged)
4. Update cart `items` JSON in `pos_carts`
5. If crypto tender: create/update `crypto_invoices`
6. `transactionTrackingService.trackCartEvent('item_added', ...)`
7. Return updated cart + totals

**DB tables:** `pos_carts`, `InventoryItems` (lookup), `crypto_invoices` (if crypto)

---

## Step 3: Tender (Payment)

### Cash
**Route:** `POST /api/pos/carts/:id/tender/cash`  
**Handler:** pos.routes.js ~1358  
**DB:** `pos_carts` (payments JSON), `Payments` table

### Card
**Route:** `POST /api/pos/carts/:id/tender/card`  
**Handler:** pos.routes.js ~1472  
**Flow:** Stripe/Coinbase integration; payment intent; mark as succeeded

### Gift Card
**Route:** `POST /api/pos/carts/:id/tender/giftcard`  
**Handler:** pos.routes.js ~1532  
**DB:** `pos_carts`, `gift_cards` (balance update)

### Loyalty
**Route:** `POST /api/pos/carts/:id/tender/loyalty`  
**Handler:** pos.routes.js ~1698  
**DB:** `pos_carts`, `loyalty_accounts`

### Crypto
**Route:** `POST /api/pos/carts/:id/tender/crypto`  
**Handler:** pos.routes.js ~1846  
**Flow:** Create crypto invoice; wait for wallet notify webhook; or poll

---

## Step 4: Finalize

**Route:** `POST /api/pos/carts/:id/finalize`

**Handler:** `backend/routes/pos.routes.js` (lines ~2634–3120+)

**Flow (exact order):**
1. Get cart; ensure status allows finalize
2. Reconcile crypto invoice if present
3. Ensure total tendered >= due
4. Mark pending non-crypto payments as succeeded
5. Update cart status to `paid`
6. Insert `Payments` rows for each tender
7. **KDS:** `ensureKitchenTicketFromCart()` — create kitchen ticket, send to KDS
8. **Inventory:** `buildInventoryConsumption()` → `decrementRecipeIngredients()` → `commitInventoryConsumption()` — deduct stock for preparables
9. Auto-86ing (out-of-stock items)
10. `posAnalyticsService.processTransaction()` — analytics
11. `CustomerAnalyticsService.processSale()` — customer analytics
12. `transactionTrackingService.trackCartEvent('cart_finalized', ...)`
13. **Core Invoicing:** `CoreInvoicingService.createPOSSaleInvoice({ tenantSlug, orgId, tenantId, cartItems, customerId, staffId, paymentMethod })`
14. Loyalty earn (if applicable)
15. Audit log
16. Return `{ ok: true, cart, invoice, totals }`

**DB tables:** `pos_carts`, `Payments`, `kitchen_tickets`, `InventoryMoves`, `recipes`, `InventoryItems`, analytics tables, `Invoices`, `InvoiceLineItems`, `AuditEvents`

---

## CoreInvoicingService.createPOSSaleInvoice

**Service:** `backend/services/coreInvoicing.service.js`

**Inputs:** tenantSlug, orgId, tenantId, cartItems, customerId, staffId, paymentMethod

**Actions:**
1. Get tenant connection (main DB)
2. Insert `Invoices` row (status: paid)
3. Insert `InvoiceLineItems` for each cart item
4. Link payments to invoice
5. Return `{ success: true, invoice }`

**DB tables:** `Invoices`, `InvoiceLineItems` (tenant main)

---

## Key Files

- `backend/routes/pos.routes.js` — All POS routes
- `backend/routes/pos-register.routes.js` — Register authenticate, open, close
- `backend/utils/posTenantQueries.js` — Cart CRUD, product lookup
- `backend/services/coreInvoicing.service.js` — Invoice creation
- `backend/services/transactionTracking.service.js` — Cart event tracking
- `backend/services/posAnalytics.service.js` — Analytics
- `backend/services/customerAnalytics.service.js` — Customer analytics
