Implement full Square payment review fixes + frontend polish

Implement every finding from the deep payment review (P0-P2, minors,
nitpicks), then close the post-implementation re-review items, then
align card-form typography and roll out the Square trust badge.

Backend - Square API alignment:
- tip_settings.allow_tipping nested under device_options (was top-level:
  terminal tips were silently lost in prod)
- CreateCardOnFile now accepts customerID and sends card.customer_id;
  saved-card (ccof:) charges forward square_customer_id as CustomerID
- New SquareClient methods GetPayment, CreateCustomer, CancelCheckout
- SCA verification_token accepted + forwarded in all charge paths
- ExpMonth/ExpYear -> *int; URL-path id validation; CancelCheckout
  NOT_FOUND-only no-op (dropped unverified NOOP); exported ErrorCode/
  ErrorDetail helpers; mock rejects raw PANs, RList locks, redacts
  emails, ForceRefundPending hook

Backend - money safety:
- sweepManualPendingSquareRefunds reconciles rows WITH square_refund_id
  instead of stranding them forever
- SweepStalePendingPayments reconciles at Square before failing (tri-state:
  leave pending on transport error, rescue completed, fail definitively)
- GetCheckoutStatus cancellation-recheck; terminal CANCELED resolution;
  SweepStaleTerminalCheckouts covers terminal_checkouts table
- till gift-card clawback on definitive failure incl. retry path +
  INSUFFICIENT_FUNDS/ADDRESS_VERIFICATION_FAILURE/TRANSACTION_LIMIT
- cross-user saved-card collision fixed (UNIQUE(user_id,square_card_id))
- customer provisioning (lazy, save-only); one-off/guest mint no customer
- discount preview/apply unified in discounts.go (global-milestone visible
  in preview, N+1 eliminated, redemption counter preserved on failures)
- webhook event_id dedup; refund loop dedup; stale comment fixes
- test-isolation t.Cleanup on committed sweep tests

Frontend:
- SCA tokenizeWithVerification across all charge flows (amount as
  major-units decimal), 5-min token-expiry re-tokenize, verification_token
  in request bodies
- PaymentModal synchronous double-click + zero/negative-amount guards
- till online-card UI wired to /api/admin/till/sale
- policyPopover generalised; new /privacy-policy route; consent checkbox
  copy + Square privacy link
- Square card iframe styled to app typography (Inter 14px, oklch tokens);
  mock form md:text-sm parity
- 'Secure payment powered by Square' badge on all 8 card-payment flows

Schema/docs: terminal_checkouts + square_customer_id + per-user card
constraint in init-script.sql; README migrations; P14 plan + backlog +
Technical Manual updated.

Includes 39 modified/new test files; full backend suite (25 pkgs),
-race on payments+square, and frontend build are green.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent fb21538532
commit 54a5b1024e
45 changed files with 6815 additions and 937 deletions
@@ -35,7 +35,7 @@ These are things that work fine in dev (with mocks) but need real implementation
| P10 | **No automated database backups** | M (1d) | Infrastructure | PostgreSQL volume is persistent in Docker but no `pg_dump` cron, no point-in-time recovery. | Standard production DB setup task. |
| P12 | **Square sandbox smoke test (pre-go-live gate)** | S-M (1d, once credentials available) | E2E | **BLOCKED — no real Square credentials available.** Must exercise the real API path end-to-end: new-card tokenization → payment → saved card → refund → reconcile, against Square's sandbox. Also verifies the M-8 open question (is `card.customer_id` enforced as Required?). | The dev mock cannot exercise Square's real wire contract (key-length limits, `device_options`, refund statuses, error codes). This is the sole remaining item before the production flip. See `plans/p11-square-web-payments-sdk.md` Remaining Items. |
| P13 | **Reconcile deterministically-keyed saved-card charges** | S (2-3h) | Backend | **Deferred — deliberate trade-off (N-OBS-1).** The admin "Charge Saved Card" idempotency key `bookingID-sc-type-amount-cardID` dedups two *identical* repeat charges on one booking. Not UI-reachable today (PaymentModal always sends the current `totalDue`, which changes after each charge). | Revisit if the admin flow ever gains a "charge exact amount twice" path — the key would then need a client nonce or attempt counter. Tracked from the final payment review. |
| P14 | **Square customer provisioning & consent** | S-M (1-2d) | Backend + Frontend + Docs | **PLANNED — gated on P12.** The final payment review flagged that Square's docs mark `card.customer_id` as Required on `POST /v2/cards` and on `ccof:` charges. If enforced, saved-card flows 400 in production. Plan: lazily provision Square customers only when a user saves a card, persist `square_customer_id`, charge one-off new-card payments directly with the `cnon:` nonce (no card/customer minted for non-savers or guests), update the privacy policy (Square as processor) and the card-save checkbox copy. **No standalone consent checkbox is required** — the existing card-save checkbox covers it. Includes a policy pop-over on the consent checkbox linking to `/privacy-policy` (mirrors the existing `/cancellation-policy` pop-over pattern — generalise `policyPopover.svelte`, new `/privacy-policy` route). Privacy Policy §2.2 + Terms §3.2 already drafted into the placeholders (Aug 2026). See `plans/p14-square-customer-provisioning-consent.md`. | The app currently mints a Square card-on-file for *every* new-card payment (even when not saving); if customer provisioning becomes mandatory, that would create customer profiles for all payers including guests. Data-minimisation design avoids this. Must sandbox-test first (P12): Square may not actually enforce `customer_id` (M-8/R2 open question). |
| P14 | **Square customer provisioning & consent** | S-M (1-2d) | Backend + Frontend + Docs | **IMPLEMENTED (Aug 2026)** — lazy customer provisioning on card-save, `square_customer_id` persisted + forwarded to Square as `card.customer_id`/CreatePayment `CustomerID`, one-off/guest no-customer, `/privacy-policy` route + consent pop-over, SCA verificationDetails wired across all charge flows. **Remaining:** P12 sandbox verification that Square enforces `customer_id`, and final privacy-policy copy review (route ships DRAFT-bannered). See `plans/p14-square-customer-provisioning-consent.md`. | Closed out of the deep post-implementation review (Aug 2026). |
---
+2 -2
View File
@@ -116,7 +116,7 @@ The DAV service has a completely separate database connection from the rest of t
| `internal/validators` | ID validation (12-char hex format), cursor parsing |
| `internal/dav` | SabreDAV CardDAV integration (build tags: `service_dev.go` / `service_prod.go`) |
| `internal/s3` | S3/R2 storage abstraction (build tags: dev vs prod) |
| `internal/square` | Square client interface + dev mock + prod stub (build tags: `dev` vs `!dev`) |
| `internal/square` | Square client interface + dev mock + real HTTP prod client (build tags: `dev` vs `!dev`) |
| `internal/zxcvbnjs` | Bundles @zxcvbn-ts/core via goja (ExecJS-style). Exact parity with frontend password scoring. Go binary embeds the 1.7MB IIFE JS bundle. |
---
@@ -1276,7 +1276,7 @@ Files with this pattern: `bookings.go` (4 handlers), `custom_services.go`, `user
| `handlers/auth` | Authentication (login, register, refresh, verification) |
| `handlers/bookings` | User booking flow, guest bookings, reservations, edit requests, approval, time-blockers, exceptional hours, cancellation, cross-user isolation |
| `handlers/payments` | Square payments (terminal, online, refunds, tips, saved cards, gift cards) |
| `internal/square` | Square client interface, dev mock, prod stub |
| `internal/square` | Square client interface, dev mock, real HTTP prod client |
| `handlers/admin` | Admin bookings, today view, users, services, GetBookingsByCreatedRange |
| `handlers/scheduling` | Working hours, exceptional groups, available hours, time blockers, gift card expiry cleanup, idle account cleanup |
| `handlers/services` | Service eligibility (age + patch test filtering) |
@@ -1,10 +1,12 @@
# P14 — Square Customer Provisioning & Consent
**Status:** 📋 PLANNED — not started. Gated on P12 (sandbox credentials) to confirm whether Square *enforces* `card.customer_id` as Required.
**Status:** PARTIALLY IMPLEMENTED (backend + frontend code landed Aug 2026) — remaining verification gated on P12 (sandbox credentials).
**Owner:** Implementation agent (payment integration round)
**Estimated effort:** S-M (1-2 days backend/frontend + privacy policy copy)
**Backlog reference:** `Future Work - Gap Backlog.md` item P14 (added alongside this plan)
> **Implementation status (Aug 2026):** The code is DONE: `square_customer_id` is provisioned lazily on card-save, stored on `user_saved_cards`, and now forwarded to Square as `card.customer_id` (CreateCard) and `CustomerID` (ccof: CreatePayment). One-off/guest payments mint no customer. The `/privacy-policy` route + consent pop-over shipped. **What remains:** sandbox verification that Square enforces `customer_id` (P12 gate) and the final privacy-policy copy review (still DRAFT-bannered).
---
## Executive Summary