018328a877
No card processor / POS terminal on any site yet. Offering "Card" would let an operator record a card payment that never cleared a terminal, corrupting the till reconciliation — a fraud/error surface on an operator-adversary system. Add apps/web/src/lib/features.ts → CARD_PAYMENTS_ENABLED=false, gating both tender pickers (BoothPayModal, SubscriptionManager). With card off there's nothing to choose, so the tender row is suppressed and payment defaults to cash. UI-only gate: the Tender type, payment events, shift accounting, and reports still understand `card`, so historical card events and a future re-enable stay coherent. Verified via Playwright: an unpaid-ticket modal shows Total + "Pay + open barrier" with no tender/cash/card row. Wiki: new concepts/card-payments.md records the current cash-only state, the PCI-scope-out-of-app constraint, the future-POS device requirements, and the re-enable path (flip the flag once a bank-certified P2PE terminal is provisioned). Linked from index, parking-session, open-questions #3. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
77 lines
4.6 KiB
Markdown
77 lines
4.6 KiB
Markdown
---
|
|
type: concept
|
|
tags: [parking, payment, pci, p2pe, pos, threat-model]
|
|
sources: [parking-system-architecture]
|
|
updated: 2026-07-01
|
|
status: open
|
|
---
|
|
|
|
# Card payments (P2PE POS) — disabled until a terminal is on-site
|
|
|
|
The app **models** a `card` tender end-to-end, but as of **2026-07-01 there is no card processor /
|
|
POS terminal on any site**, so the card option is **disabled in the UI**. This page records why,
|
|
what a future POS needs, and how to re-enable — so the gap isn't rediscovered as "why can't I take a
|
|
card?".
|
|
|
|
## Current state — CASH ONLY (2026-07-01)
|
|
|
|
- The booth pay/exit modal and the subscription-sale form show **cash only**. The tender picker is
|
|
**suppressed entirely** when there's nothing to choose (payment silently defaults to `cash`).
|
|
- Nothing about the data model changed: `Tender = "cash" | "card"` still exists ([[technology-stack|
|
|
shared]] `packages/shared`), the server/`payment` events, [[shift]] accounting, and
|
|
[[reporting-analytics|reports]] all still understand `card` — this is **only a UI gate**, so any
|
|
historical `card` events (or a future re-enable) stay coherent.
|
|
- Flag: **`apps/web/src/lib/features.ts` → `CARD_PAYMENTS_ENABLED = false`**. Both tender pickers
|
|
(`BoothPayModal.tsx`, `SubscriptionManager.tsx`) render card only when it is `true`.
|
|
|
|
**Why disable rather than leave it?** Offering "Card" with no terminal lets an operator record a card
|
|
payment that **never actually cleared** — money that isn't in the drawer and didn't hit the bank —
|
|
which silently corrupts the [[shift|till reconciliation]] and the [[reconciliation|financial audit]].
|
|
On a system whose adversary is the [[threat-model|booth operator]], a tender the site can't fulfil is
|
|
a fraud/error surface, not a convenience. Cash-only is the honest state until hardware exists.
|
|
|
|
## The constraint a POS must satisfy — PCI scope stays OUT of the app
|
|
|
|
This is a **standing architectural rule** ([[bom]], [[open-questions]] #3, [[parking-session]]):
|
|
card capture goes through a **standalone, bank-certified P2PE (point-to-point encryption) terminal** —
|
|
the application **must never see card data (PAN, track, CVV)**. The app only records that a payment's
|
|
`tender` was `card`; the terminal does the capture, encryption, and settlement against the acquiring
|
|
bank. This keeps the whole appliance **out of PCI-DSS scope**, which is a hard requirement (a booth PC
|
|
in PCI scope is a non-starter).
|
|
|
|
The terminal model is **dictated by the acquiring bank** (not our choice) — verify local
|
|
availability (Albania/EU) when the bank is chosen. See [[bom]] "Payment".
|
|
|
|
## Future POS device — what has to be configured (open)
|
|
|
|
When a terminal is procured, this is the outline (details TBD — flag on [[open-questions]] #3):
|
|
|
|
1. **Hardware**: a bank-certified standalone P2PE terminal beside the booth PC + the cash drawer.
|
|
2. **Integration boundary**: decide how the app learns a card sale succeeded WITHOUT touching card
|
|
data — options range from *manual* (operator runs the card on the terminal, then confirms in the
|
|
app → a `card` `payment` event) to a *terminal-integration* (the app requests an amount, the
|
|
terminal returns an approved/declined result over a local link). The manual path keeps PCI scope
|
|
trivially out; an integration must preserve the same boundary (no PAN ever reaches the app).
|
|
3. **Device model**: if integrated, the terminal becomes a [[device-registry|device adapter]] behind
|
|
an interface (like reader/printer/relay) — a `payment-terminal` capability — so a hardware swap is
|
|
a new adapter, nothing else. A *manual* terminal needs no adapter (it's off-system; the app just
|
|
records the tender).
|
|
4. **Reconciliation**: card takings must reconcile against the **terminal's/bank's** settlement
|
|
report, separately from the cash drawer (card money never enters the drawer). [[shift]] Z-reports
|
|
already split cash vs card totals — wire the card side to the terminal batch.
|
|
|
|
## Re-enabling
|
|
|
|
1. Provision + configure the terminal (per above).
|
|
2. Flip `CARD_PAYMENTS_ENABLED = true` in `apps/web/src/lib/features.ts`. The tender pickers reappear.
|
|
3. If integrated, add the `payment-terminal` adapter + wire the approved-result → `card` `payment`
|
|
event. If manual, no code beyond the flag.
|
|
4. Update this page (→ `status: settled`) and [[open-questions]] #3.
|
|
|
|
## Relates
|
|
|
|
- [[bom]] — the payment subsystem line (certified P2PE terminal + cash drawer, PCI-out-of-scope).
|
|
- [[open-questions]] #3 — payment subsystem (manned booth P2PE vs unmanned pay station).
|
|
- [[parking-session]] / [[shift]] — where `tender` is recorded and reconciled.
|
|
- [[threat-model]] — why a tender the site can't fulfil is a fraud surface.
|