# POS P2 — Complete Retail Sales, Instant Sale & Checkout

**Branch:** `techtitans/integration` · **Status:** in progress (living document)
**Started:** 2026-11-13 · Backend: Laravel 13 · Frontend: React 19 + Vite

This document is built up phase-by-phase. Phase 1 (audit) is complete below;
subsequent sections are filled as each feature lands, with commit hashes. Any
feature that cannot be completed safely is left **clearly disabled** and recorded
as a blocker rather than claimed as done.

---

## PHASE 1 — AUDIT (complete)

Method: six read-only subagents mapped the backend (orders, pricing/discount/tax,
payments/gateways, combos/inventory, invoices/reporting) and the legacy "Cacart"
POS source (read-only — never executed). Findings are file:line-precise in the
session transcript; summarised here.

### 1.1 Headline finding

The backend is **far more capable than P1's frontend-only view suggested**. Most
of P2 is **wiring and exposing existing, tested engine capability** — not building
new engines. The authoritative order/pricing/tax/payment/inventory/invoice
services all already exist and are server-authoritative (the client never prices).

### 1.2 Reused services (no reinvention)

| Service | Role in P2 |
|---|---|
| `app/Orders/OrderService` | add/removes lines, `applyDiscount`/`removeDiscount` (already written), `overrideLinePrice` |
| `app/Orders/OrderPricingService` | integer-minor bcmath engine: line gross, **order- & line-level discounts**, tax on discounted base, totals — all server-authoritative |
| `app/Menu/ProductTaxResolver` + `TaxClassService` | per-line tax-rate snapshot (`tax_rate_bps`/`tax_inclusive`) |
| `app/Payments/OfflinePaymentService` + `PaymentLedger` | records tenders; **already sums multiple partial payments**; `payment_status` PAID/PARTIALLY_PAID derived |
| `app/Payments/PaymentConfigService` + Bambora/Moneris providers | real gateways, **sandbox-gated**, secrets server-side (not used for external-terminal tenders) |
| `app/Menu/ComboService` + combo models | combo authoring; order engine natively supports `line_type='combo'` + `combo_component` selections |
| `app/Orders/OrderInventoryBridge` + `InventoryDeductionService` | commit-time, idempotent, fail-closed component stock deduction |
| `app/Invoices/InvoiceService` + `ReceiptService` + `InvoiceNumberSequencer` | **snapshot/line-driven** — new line types invoice correctly with no change |
| `app/Customers/CustomerFinancialService` | revenue/history from **order-header totals + payments ledger** — new lines appear automatically |
| `app/Auth/ManagerApproval/ManagerApprovalService` + `EnsureManagerApproval` middleware | over-limit discount approval |

### 1.3 Feature matrix (existing / partial / missing)

| Feature | Status | Reuse path / plan | Precise blocker(s) to clear |
|---|---|---|---|
| **Instant Sale** | **Missing** | **Placeholder-product pattern** (proven by IPTV billing): a per-tenant non-stock "Custom Item" product+variant satisfies the FK, then the staff-entered name/price/tax are snapshotted on the line via `OrderService::addLine` (accepts caller price). New `pos_quick_items` table for reusable templates + search. Flows into invoices/reports automatically. | `order_lines.product_id/variant_id` are NOT NULL + FK; HTTP `addLine` refuses client price → **new idempotent custom-line endpoint** needed (service already supports it). No `order_lines` schema change with the placeholder approach. |
| **Top Sale** | **Missing** | New aggregate `GROUP BY` over `order_lines ⋈ orders` scoped tenant/location/window, `status='COMPLETED'`, `order_source<>'SYSTEM'` (excludes IPTV, which bills via the `SYSTEM` source). New read endpoint; clicking filters the grid (no navigation). | No sales aggregate/report exists yet. Low effort, well-indexed. |
| **Daily Specials** | **Partial** | Real time-boxed, location-scoped, backend-authoritative sale infra exists (`OnlinePriceService`/`variant_online_prices`, original-vs-sale + active/expired lifecycle) **but online-channel only**. Plan: an in-store special resolver consulted by the POS add-line path, stamping the already-legal `applied_price_source='sale'`; admin to set product/variant special price + window + location. | POS add-line uses `LocationPriceService` (base/location_override only) — must resolve specials server-side. Recurring **time-of-day "daily"** windows are **net-new** (only absolute windows modelled) — P2 ships absolute windows; recurring deferred. |
| **Combo** | **Partial (engine present)** | Combo authoring complete; order engine natively prices `combo` lines + `combo_component` selections; **component (variant) stock deducts correctly** (combo header owns no stock), idempotent. Plan: a POS **assembly layer** that expands a combo product into its line + component selections (enforce group min/max, snapshot deltas). | No code currently expands a combo into order lines for the cashier — assembly/validation layer to add. No architectural blocker. |
| **Discount** | **Partial (engine present)** | Engine supports **order- & line-level fixed-amount** discounts with reason + actor attribution + correct tax recompute; `OrderService::applyDiscount/removeDiscount` exist behind `orders.apply_discount`. Plan: **wire HTTP endpoint(s)**; add **percentage** support (`rate_bps`/`discount_mode`) for recompute-stability; **manager approval** over-limit via `ManagerApprovalService`. | No discount route today; `amount_minor` is fixed-only (percentage needs a schema+engine extension). |
| **Cash** | **Existing** | `OfflinePaymentService` `method=cash` captures immediately; record **applied amount (≤ outstanding)**; change is **UI-only** (server rejects tender > balance). | Add client-supplied **idempotency key** to `recordOffline` (see Split). |
| **Debit** | **Partial (tiny)** | Add `'debit'` to `OfflinePaymentService::METHODS` (+`IMMEDIATE`); model as **externally-collected** (terminal ref → `offline_reference`, employee → `recorded_by_user_id`). | Not in the method enum today. **Must not** invoke a gateway/fake a card charge. |
| **Credit** | **Partial (tiny)** | Same as Debit. Real Bambora/Moneris adapters exist but are **sandbox-gated and charge the full order total** → P2 uses the **external-terminal** (externally-collected) path; integrated partial-amount gateway capture is deferred. | Not in the enum. Do not claim an integrated card charge unless a real sandbox transaction actually succeeds. |
| **Split** | **Existing (harden first)** | Multiple `recordOffline` tenders already sum in `PaymentLedger` (PARTIALLY_PAID → PAID). Plan: a split drawer + **harden offline idempotency** (accept a request id) so a mid-split retry can't double-record. | Offline `recordOffline` sets a fresh UUID idempotency key per call ⇒ no dedup. Gateway-backed **partial** card legs not supported (card path charges full total). |

### 1.4 Legacy "Cacart" POS reality (read-only reference)

The requested features come from the `dcc`/cacart retail terminal (not `oldportal`).
Notable: **Top Sale / Daily Specials / Combo buttons were dead stubs** (no click
handlers); "Combo" was a bare tag, **not a bundle** (no component stock); "Daily
Special" had **no scheduling**; stock used a **sold-counter**, not an on-hand
ledger. So our P2 is a genuine upgrade. **Unsafe legacy patterns we explicitly
avoid:** client-trusted money (totals validated as `nullable|string` and stored
verbatim), hardcoded 13% tax + ignored per-item tax flag, no discount cap, free-
string payment methods, dead affordances, and `setTimeout` sequencing hacks. Our
build keeps all money server-authoritative and ships no non-functional buttons.

### 1.5 Financial & inventory invariants P2 must preserve

- **One ledger.** Every sale flows through `Order` + `OrderLine` + `Payment`; no
  second sales/payment ledger.
- **Server-authoritative money.** Prices/tax/discount/total recomputed by
  `OrderPricingService`; the client never sends a price (Instant Sale's staff-
  entered price is a permissioned custom-line input, recomputed into totals).
- **No stock for untracked custom items.** Instant Sale lines carry no inventory
  nodes; combos deduct **component** stock only, idempotently, fail-closed.
- **Retail ≠ IPTV.** Instant Sale and all P2 sales stay on a retail
  `order_source` (never `SYSTEM`) so they are never miscounted as IPTV.
- **Tenant + location isolation** via `BelongsToTenant` and location scope.
- **No payment marked captured until persisted; idempotent; never roll back a
  captured external tender by rolling back a DB transaction.**

### 1.6 Implementation order (lowest-risk / highest-reuse first)

1. **Instant Sale** (core; placeholder-product + `pos_quick_items` + custom-line endpoint).
2. **Cash / Debit / Credit** (offline enum + idempotency hardening + checkout UI).
3. **Split payment** (reuses the hardened offline path + a drawer).
4. **Discount** (wire endpoint + percentage + approval).
5. **Top Sale** (aggregate + grid filter).
6. **Combo** (assembly layer over the existing combo-line engine).
7. **Daily Specials** (in-store special resolver; absolute windows; recurring deferred).

Each increment is committed separately with its own backend + frontend tests.

---

## PHASES 2–13 — IMPLEMENTATION (complete)

All nine acceptance features were built end-to-end (backend + frontend + tests)
over the existing engine. Status: **all 9 implemented and tested.**

### 2.1 Feature results (legacy → P2)

The legacy Cacart POS's Top Sale / Daily Specials / Combo buttons were **dead
stubs**; combo was a bare flag (no bundle); specials had no scheduling; money was
client-trusted. P2 delivers all nine as **working, server-authoritative** features:

| Feature | Status | How it works in P2 |
|---|---|---|
| **Instant Sale** | ✅ Done | Ad-hoc item → a `custom` order line booked against a per-tenant non-stock "Custom Item" placeholder (mirrors IPTV billing); staff name/price/tax snapshotted on the line; carries no stock; reusable `pos_quick_items` templates. Idempotent. |
| **Top Sale** | ✅ Done | `GROUP BY` over `order_lines ⋈ orders`, `status='COMPLETED'`, `order_source<>'SYSTEM'` (excludes IPTV) and `line_type<>'custom'`; the rail shortcut filters the grid. |
| **Daily Specials** | ✅ Done | `pos_specials` time-boxed per variant/location; `SpecialPriceService` resolves the active special; `addLine` applies it (stamped `applied_price_source='sale'`) when lower; snaps back on expiry. Tiles show special + struck original; admin drawer. |
| **Combo** | ✅ Done | Combo product → a `combo` line + validated `combo_component` selections; priced base + component deltas; **component** stock deducts on commit; picker enforces group min/max. |
| **Discount** | ✅ Done | Order- & line-level, **fixed or percentage** (percentage recompute-stable, computed from current base), reason + employee attribution, permission-gated; tax recomputed on the discounted base; clamps so the total never goes negative. |
| **Cash** | ✅ Done | Offline capture; records the **applied** amount (≤ outstanding); change is UI-only. |
| **Debit / Credit** | ✅ Done | Added as **externally-collected** offline methods (captured immediately); terminal reference + employee recorded. **No gateway is invoked** — the real Bambora/Moneris adapters are sandbox-gated and charge the full total, so P2 does not route cards through them. |
| **Split** | ✅ Done | Multiple tenders summed by `PaymentLedger`; hardened with client **idempotency keys** so a mid-split retry never double-collects; a failed/over tender is rejected and preserves earlier ones. |

### 2.2 New schema (3 tables + 2 additive columns)

- **`pos_quick_items`** — reusable Instant Sale templates (name, default price, tax
  class, usage stats). Not a sellable catalog product.
- **`pos_specials`** — time-boxed in-store special prices (variant, optional
  location, `[starts_at, ends_at)` UTC window, active). CHECK price ≥ 0 and
  `ends_at > starts_at`.
- **`order_lines.line_type`** CHECK widened to allow **`custom`** (Instant Sale);
  the migration drops the old CHECK only when present (tolerates MySQL-5.7-era DBs).
- **`order_discounts`** gains **`discount_mode`** (`fixed|percent`) + **`rate_bps`**
  (migration + CHECK) for percentage discounts.

No change to the orders/payments/invoice schema otherwise; everything else reuses
existing tables and services (§1.2).

### 2.3 Financial & inventory invariants (verified)

- **One ledger.** Every sale is `Order` + `OrderLine` + `Payment`; no second
  ledger. Instant Sale, combos, discounts and specials flow into invoices,
  reports and customer financials automatically (snapshot-driven).
- **Server-authoritative money.** Prices/tax/discounts/totals are recomputed by
  `OrderPricingService`; the client never sends a price. Instant Sale's staff
  price and manual discounts are permissioned inputs, recomputed into totals.
  Daily Special prices are resolved + applied server-side at order creation.
- **No stock for untracked items.** `custom` lines emit no inventory node; combos
  deduct **component** stock only, idempotently, fail-closed; serialized inventory
  is absent and not bypassed.
- **Retail ≠ IPTV.** All P2 sales stay on a retail `order_source`; Top Sale and
  reports exclude `SYSTEM`. The IPTV + "Custom Item" placeholders are hidden from
  the POS grid.
- **Payments.** Nothing is marked captured until persisted; offline recording is
  idempotent; externally-collected cards are recorded, never gateway-charged; a
  captured external tender is never rolled back by a DB rollback.
- **Tenant + location isolation** enforced throughout.

### 2.4 Tests & verification

- **Backend (Pest/PHPUnit, MySQL `techtitans_pos_test`):** new suites — Instant
  Sale (7), POS tenders Cash/Debit/Credit/Split (6), Discount (7), Top Sale (2),
  Combo (5), Daily Specials (5). Regression: **Orders 231, Payments 60, Pos 19,
  and Menu/Inventory/Checkout/Tax/Orders-unit 1018, Storefront 82 — all green, 0
  regressions.** `./vendor/bin/pint` and `./vendor/bin/phpstan` clean on all new
  code.
- **Frontend (Vitest):** POS workspace suite **12/12** (instant sale + validation,
  cash tender, discount, Top Sale filter, combo, Daily Special tile + filter, …).
  Full suite 1829/1830 — the single failure is the project's known load-induced
  timeout flake (passes in isolation; unrelated Employees/Locations module).
  `tsc`, ESLint and production `build` clean.
- **Playwright (live dev app, VM Admin / TechTitans Main Store, sandbox only):**
  verified end-to-end with **0 console errors** — Instant Sale ($45) → 10%
  discount → **$40.50 due** → Checkout (Cash/Debit/Credit tabs + change);
  Daily Specials manager; placeholders hidden from the grid. **No integrated card
  transaction was performed** (none claimed) — debit/credit are externally
  collected.

### 2.5 Screenshots

In [`screenshots/pos-p2/`](screenshots/pos-p2/): `p2-pos-main`,
`p2-instant-sale-drawer`, `p2-instant-sale-added`, `p2-discount-drawer`,
`p2-discount-applied`, `p2-checkout-cash`, `p2-checkout-debit`,
`p2-specials-manager`.

### 2.6 Remaining limitations (honest)

- **Integrated card capture is NOT wired.** Debit/Credit are externally-collected
  tenders (money taken on a standalone terminal, recorded by staff). The real
  Bambora/Moneris adapters exist but are sandbox-gated and only charge the full
  order total, so a gateway-backed **partial** (split) card leg is not supported.
- **Manager-approval-over-limit for discounts** is deferred (the task marks it
  optional): there is no per-role discount-limit setting yet. The
  `orders.apply_discount` permission gate is enforced; `ManagerApprovalService`
  is the ready reuse path when a limit setting is added.
- **Serialized inventory** does not exist; combos with serialized components
  cannot select a unit (they deduct quantity-balance stock). Not bypassed.
- **Daily Specials** use absolute `[starts_at, ends_at)` windows; recurring
  time-of-day ("every day 5–7pm") scheduling is not modelled.
- **Line-level discount** is supported by the engine + API; the POS drawer
  currently applies **order-level** discounts (line-level is a UI follow-up).

### 2.7 P3 plan (centralized collection / history / reversals)

1. **Integrated payments:** real terminal/gateway capture (incl. partial legs for
   split); verify sandbox transactions before claiming integrated card support.
2. **Cross-invoice collection & consolidated Payment History** (the "Payment
   History" POS mode graduates from per-sale to cross-order).
3. **Refunds / payment reversals** (deliberately excluded from P2) with the
   safeguards already noted (never roll back a captured external tender via a DB
   rollback).
4. **Manager-approval discount limits** (per-role threshold + conditional
   `ManagerApprovalService::consume`).
5. **Recurring Daily Specials** and **serialized inventory** (serial/IMEI unit
   selection at sale) as separate, assessed capabilities.

### 2.8 Commit hashes (local only — not pushed)

**Frontend** (`techtitans/integration`):
- `8a116ce` Instant Sale · `5c82bda` Checkout UI · `6376965` Discount ·
  `d72008b` Top Sale · `4d5dddb` Combos · `fd50793` Daily Specials ·
  `f518d7b` hide placeholders.

**Backend** (`techtitans/integration`):
- `539df2d` Phase 1 audit · `d480162` Instant Sale · `3a9a819` Cash/Debit/Credit/
  Split · `33d57e4` Discount · `b734c62` Top Sale · `97b78e5` Combos ·
  `5263f73` Daily Specials · `edd7940` migration compatibility fix · (+ this doc).

Committed locally on `techtitans/integration` only — **not pushed or deployed**.
NH POS untouched; no legacy PHP executed; no existing records altered (new
migrations only).

## ACCEPTANCE — met

**Instant Sale + Top Sale + Daily Specials + Combo + Discount + Cash + Debit +
Credit + Split Payment** are all implemented and flow through the existing POS
Order, Invoice, Payment and Inventory architecture. It is a working retail
checkout — verified live end-to-end — not a design-only page. The two items that
cannot be completed *safely* without new integrations (integrated card capture;
manager-approval-over-limit) are left clearly disabled/deferred and documented
above rather than faked.
