diff --git a/obsidian/Crussell/Future Work - Gap Backlog.md b/obsidian/Crussell/Future Work - Gap Backlog.md index fad19fa..ab2a112 100644 --- a/obsidian/Crussell/Future Work - Gap Backlog.md +++ b/obsidian/Crussell/Future Work - Gap Backlog.md @@ -35,6 +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). | --- diff --git a/obsidian/Crussell/Privacy Policy.md b/obsidian/Crussell/Privacy Policy.md index cd33ff8..bf11e65 100644 --- a/obsidian/Crussell/Privacy Policy.md +++ b/obsidian/Crussell/Privacy Policy.md @@ -1,7 +1,7 @@ # Privacy Policy -**Last Updated:** June 2026 -**Status:** DRAFT — Local development (not yet in production) +**Last Updated:** August 2026 +**Status:** DRAFT — Local development (not yet in production). **§2.2 (Saved Cards & Square) is drafted from the P14 plan; the placeholder sections below still need to be made 'real' before go-live.** --- @@ -39,9 +39,23 @@ Email: help@crussell.invalid - Gift card codes and balances - Account balances - Payment transaction records (processed via Square, not stored by us) +- Saved-card references (tokenised, stored with our payment provider Square — see §2.3) - Dormant balance records (Account ID only, no PII) -### 2.2 Special Category Data (Health Data) +### 2.2 Saved Cards & Payment Provider (Square) + +When you choose to **save a card for next time**, we store a tokenised reference to your card with our payment processor, **Square** (a data processor), rather than on our own systems. + +- **What Square stores:** a tokenised reference to your card (never your full card number or CVV), plus the name and email address we already hold on your account, grouped into a Square customer profile. +- **Lawful basis:** UK GDPR Article 6(1)(b) — necessary for the performance of the contract (you asked to save your card for future payments). +- **Why:** so you can pay for future bookings, tips, or gift-card purchases without re-entering your card details. +- **One-off payments:** if you do not tick "save this card", **no card is stored and no Square customer profile is created** for you — your card is used only for that single payment. +- **Retention & removal:** the reference remains stored until you delete the card from your account (Account → Saved Cards) or your account is deleted. You can remove a saved card at any time. +- **Square's privacy policy:** [Square Privacy Policy](https://squareup.com/gb/en/legal/privacy-no-account) applies to data Square holds on our behalf. + +We never store full card numbers, card security codes (CVV), or card expiry data on our own systems at any point. + +### 2.3 Special Category Data (Health Data) We collect health-related information with your **explicit consent**: - Allergy records @@ -64,6 +78,7 @@ We collect health-related information with your **explicit consent**: | **Inactive accounts (no balance)** | 2 years idle | GDPR storage limitation | | **Inactive accounts (with balance)** | 5 years idle | Scottish prescriptive period | | **Financial records** | 7 years | HMRC requirement | +| **Saved-card references (Square)** | Until user deletes card or account is deleted (Square-side) | Contract performance (Art 6(1)(b)); card-network card-on-file rules | | **Allergy/health records** | 7 years | Insurance requirement | | **Dormant balances** | Indefinite (Account ID only) | Recovery mechanism | | **Marketing preferences** | Until withdrawn | Consent | @@ -78,6 +93,8 @@ We collect health-related information with your **explicit consent**: 5. Financial records retained 7 years (HMRC) then aggregated. 6. Allergy records retained 7 years (insurance) then deleted. +**Saved cards:** Deleting your account also removes your saved-card references from our system and disables the corresponding card tokens at Square (see §2.2). Card transaction records for payments already made are retained per the HMRC schedule above. + **Inactive account deletion (automatic):** 1. Warning emails sent at 18/23 months (no balance) or 4/59 months (with balance). 2. If no activity, account deleted as above. diff --git a/obsidian/Crussell/Terms & Conditions - Overall App.md b/obsidian/Crussell/Terms & Conditions - Overall App.md index b47c1b1..57b02d9 100644 --- a/obsidian/Crussell/Terms & Conditions - Overall App.md +++ b/obsidian/Crussell/Terms & Conditions - Overall App.md @@ -104,6 +104,7 @@ All warning emails include your Account ID for future balance recovery. - Card payments processed securely via Square. - We do not store full card details. - Refunds processed to original payment method within 5-10 business days. +- **Saving a card for next time** stores a tokenised reference with our payment provider, Square (see the checkbox in the payment flow and our [[Privacy Policy]] §2.2). You can remove saved cards any time from your account. Cards are only stored when you explicitly tick "save this card". ### 3.3 Split Payments - You may split payment across multiple methods (e.g., gift card + cash). diff --git a/obsidian/Crussell/plans/p14-square-customer-provisioning-consent.md b/obsidian/Crussell/plans/p14-square-customer-provisioning-consent.md new file mode 100644 index 0000000..278ee1e --- /dev/null +++ b/obsidian/Crussell/plans/p14-square-customer-provisioning-consent.md @@ -0,0 +1,182 @@ +# P14 — Square Customer Provisioning & Consent + +**Status:** 📋 PLANNED — not started. Gated on P12 (sandbox credentials) to confirm whether Square *enforces* `card.customer_id` as Required. +**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) + +--- + +## Executive Summary + +The final payment review (Aug 2026) flagged a P0: Square's current API docs mark **`customer_id` as Required** on (a) `POST /v2/cards` (the `card` object) and (b) `CreatePayment` **when `source_id` is a Cards-API card-on-file (`ccof:`)**. The app deliberately sends **no `customer_id`** (no Square customer provisioning — linkage uses `reference_id`; see commit 80bad01 and P11 plan Remaining Item R2 / M-8). + +This plan covers **what to do if `customer_id` is enforced**: provision Square customers, persist the id, and — the question this plan answers — **what consent/agreement/checkbox layer is required**. Answer: **no new standalone "Square customer" consent checkbox**; instead three transparency items (privacy policy disclosure, explicit card-save checkbox copy, data-minimisation by not minting customers for one-off payers). + +--- + +## Background + +- The app creates a Square card-on-file for **every new-card payment** (even `save_card=false`), via `CreateCardOnFile` before the charge (`handlers.go` `CreateBookingPayment`/`CreateTipPayment`; `square_http_client.go` `createCardOnFileHTTP` → `POST /v2/cards`). The card is intentionally left as a Square-side orphan when not saved (retry idempotency via deterministic sha256 key). +- If `POST /v2/cards` enforces `customer_id`, **every new-card payer — including guests — would get a Square customer profile** (name + email from the users/guest record) even when they never opted to save anything. That is the central data-protection problem this plan designs around. +- A Square Customer profile is not a consumer-facing account: it is a merchant-side grouping container holding name, email, and tokenized card references (no PANs — PCI-safe). + +--- + +## Consent & Legal Analysis (UK GDPR / DPA 2018 + card network rules) + +### What is NOT required + +| Item | Verdict | Reasoning | +|---|---|---| +| Standalone "create a Square customer profile" checkbox | **NOT required** | The profile is created as a *consequence* of the user's card-save action. The existing card-save checkbox is the consent point; the profile is bookkeeping that rides the same consent. | +| New user agreement / Terms page | **NOT required** | The existing account Terms already govern the account; saved-card storage is disclosed via the privacy policy + checkbox copy below. | +| Consent checkbox for *processing* name/email at Square | **NOT required** | Lawful basis is Article 6(1)(b) — necessary for performance of the contract (the user chose card-on-file for future payments). Consent is not the basis, so no checkbox. | + +### What IS required (transparency layer, not checkboxes) + +1. **Privacy Policy update** (`obsidian/Crussell/Privacy Policy.md`): + - Currently §2.1 states only "Payment transaction records (processed via Square, not stored by us)" — add: when a user saves a card, Square stores the card-on-file reference **and** a customer profile containing the user's name + email (Square is a data processor). + - State the lawful basis for card storage (contract performance / Article 6(1)(b)) and link to Square's privacy policy. + - State retention: card persists until the user deletes it (DeletePaymentMethod) or Square's card-expiry policy applies. +2. **Explicit card-save checkbox copy** (`CardSelection.svelte:146`): + - Change `Save this card for next time` → `Save this card securely with our payment provider (Square) for next time`. + - Ensures informed consent per card-network card-on-file rules and Square's own requirements. Not pre-checked (already the case). +3. **Data minimisation** (design change, see below): do **not** create a Square customer (or card-on-file) for one-off payers or guests who don't save. Charge the `cnon:` nonce directly. + +> ⚠️ Legal note: this is a UK card-on-file assessment consistent with ICO guidance and Visa/Mastercard card-on-file rules, not formal legal advice. Sanity-check the privacy policy wording with a professional before go-live. + +--- + +## Design Decisions + +### D1 — Provision Square customers ONLY when the user saves a card +- Create the Square customer lazily, at the moment `save_card=true`, via Square's `POST /v2/customers` (name + email from the local user record). +- Persist the returned `customer_id` on `user_saved_cards` (new column) and reuse it for subsequent card saves by the same user. +- One-off payments (`save_card=false`): **do not create a card-on-file at all** — call `CreatePayment` with the `cnon:` nonce as `source_id` directly (Square supports nonce-as-source without a card or customer). This reverses the current create-card-first pattern for non-save flows. + +### D2 — Guests never get a customer profile +- Guest bookings have no account and cannot save cards (`canSaveCards` requires `verified_email`, so guests are excluded already). Ensure the backend also refuses to mint a customer for guest users — guest new-card charges must go straight to Square as one-off nonce charges. + +### D3 — Retry idempotency without a card-on-file +- The current create-card-first pattern exists so a pending-record retry can re-mint the same card via the deterministic key. For one-off nonce charges, verify the retry path still dedups at Square on the **idempotency key** alone (it should — Square dedups `CreatePayment` by key; a failed first attempt that never charged is replayed safely). If the retry needs a fresh nonce because the old one is consumed, the frontend must re-tokenize on retry (it already caches the nonce per attempt — confirm the cache is cleared when the nonce is marked used). + +### D4 — Existing controls stay +- Delete-card flow (`DeletePaymentMethod` → Square `POST /v2/cards/{id}/disable`) remains the "view/manage/delete" control card networks require. No change. + +--- + +## Policy Pop-over for the Consent Checkbox (mirroring `/cancellation-policy`) + +The codebase already has a reusable policy pop-over pattern used in 8 places — **the plan reuses it for the card-save consent** so the checkbox links to the relevant policy inline, exactly like the cancellation-policy links today. + +### Existing pattern (ground truth — verified) +- **`frontend/src/lib/components/ui/policyPopover.svelte`** — a generic pop-over component: + - Takes a `trigger` snippet (any label/button the caller renders). + - Renders a small pop-over with two actions: **Open** (new tab → `href`) and **Download PDF** (`href?format=pdf`). + - Closes on click-outside (`[data-policy-popover]`) and Escape; `target="_blank" rel="noopener noreferrer external"`. + - **Currently hardcodes** `href="/cancellation-policy"` and the fallback label "cancellation policy". +- **`frontend/src/routes/cancellation-policy/+page.svelte`** — the policy page itself: + - Plain HTML sections (no auth), `Last updated` date in a header line. + - `?format=pdf` → strips the param from the URL, hides a print notice, triggers `window.print()` → user saves as PDF. + - Print CSS via `@media print` (hides nav + `.no-print`). +- Used in: `BookingModal`, `RescheduleModal`, `EditRequestModal` (×2), `BookingFlow` (×2), `UserBookingModal`, `UserPaymentModal`, account page Policies block. + +### Step 6a — Generalise `policyPopover.svelte` +Make the component take optional `label` (fallback text) and `href` props, defaulting to the current `/cancellation-policy` + "cancellation policy" so the 8 existing call sites keep working unchanged: +```svelte +const { trigger, label = 'cancellation policy', href = '/cancellation-policy' } = $props(); +``` +- `href` used for both the **Open** link and the **Download PDF** action (`${href}?format=pdf`). +- No behavioural change for existing callers (defaults preserve current output). + +### Step 6b — Add the pop-over to the card-save consent +In **`CardSelection.svelte`** next to the consent checkbox (`:135-148`), wrap the label's "Save this card securely with our payment provider (Square)" text (or the label span) so a small **"Privacy Policy"** link opens the pop-over: +```svelte + +``` +- **Only rendered when the checkbox is shown** (`canSaveCards && squareCardReady`), so the link appears exactly at the point of consent — the same "at the moment of data collection" placement UK GDPR Art 13 expects. +- Same component in any other flow that renders a save-card checkbox (UserBookingModal tip modal if it uses `CardSelection`, etc.) — **single source of truth: reuse `CardSelection`** rather than duplicating labels. + +### Step 6c — Create `/privacy-policy` route (the 'real' policy) +- **Gate**: the pop-over ships only after the Privacy Policy placeholder is made 'real' (cleaned up + owner-approved; see Docs step 8). Until then, keep the current behaviour (no privacy pop-over / no link) or point the link at the placeholder page marked DRAFT — **decision: ship the route with the DRAFT content but label it "DRAFT — for review"**, so the consent flow is fully walkable end-to-end in dev while the copy is still being finalised. +- New route **`frontend/src/routes/privacy-policy/+page.svelte`**, copying the `/cancellation-policy` structure: + - `` title "Privacy Policy", `@media print` CSS, `?format=pdf` handling, `Last updated` line. + - Content from `obsidian/Crussell/Privacy Policy.md` (incl. the new §2.2 Saved Cards & Square). + - Footer link back to `/contact` (match the cancellation page). +- **Terms route** (optional, same pattern): if the Terms placeholder is also made real, add `/terms` + a pop-over near account sign-up. **Out of scope for P14 unless the owner asks** — the consent checkbox only needs the Privacy Policy link. + +### Step 6d — Account page "Policies" block +Add a second `PolicyPopover` (Privacy Policy) next to the existing cancellation-policy button in `account/+page.svelte:2356-2378` so users can reach the privacy policy from their account, matching the current presentation. + +--- + +## Implementation Steps (when unblocked by P12) + +### Backend +1. **Schema**: add `square_customer_id TEXT` to `user_saved_cards` (nullable) + `init-script.sql`; document the manual `ALTER TABLE` for existing deployments (README migration section). +2. **Square client** (`types.go` + `square_http_client.go`): + - Add `CreateCustomer(ctx, name, email) (*CustomerResult, error)` and `CustomerID` on `CardOnFile`. + - `createCardOnFileHTTP`: when `req.CustomerID != ""`, send `card.customer_id` (only for save flows). + - `createPaymentHTTP`: when `source_id` is `ccof:` (saved card), include `customer_id` from the local saved-card row. +3. **Handlers** (`handlers.go` `CreateBookingPayment`/`CreateTipPayment`, `service.go` `CreatePaymentMethodFromToken`, till `online_square`): + - Restructure to **charge the nonce directly** when `save_card=false` (skip card mint). + - When `save_card=true`: ensure customer exists (lazy create + cache per user), pass `customer_id`, persist on the saved-card row. +4. **Tests**: update mock (`square_dev.go`) + unit tests to cover: nonce-direct charge, customer creation on save, customer reuse, guest one-off (no customer), and the existing `_NonExact`/retry regressions. + +### Frontend +5. **Checkbox copy** (`CardSelection.svelte:146`): explicit Square wording (above). +6. **Policy pop-over on the consent checkbox** (mirror the existing `/cancellation-policy` pattern — see below). +7. **Retry nonce handling**: verify `SquareCardInput`/flow caches the nonce per attempt and re-tokenizes after a consumed nonce (confirm current behaviour; fix if it re-sends a used nonce). +8. **Account page**: display a small "cards are stored securely with our payment provider, Square" caption near the saved-cards section (optional but recommended for transparency). + +### Docs +8. **Privacy Policy** (`obsidian/Crussell/Privacy Policy.md`): **already drafted in the placeholder** — §2.2 "Saved Cards & Payment Provider (Square)" now contains the processor disclosure, lawful basis (Art 6(1)(b)), one-off-payment statement, retention, and Square privacy-policy link; Financial Data list + retention table + deletion process updated to match. The rest of the document remains a DRAFT placeholder and must be made 'real' (owner-approved copy, live date) before the `/privacy-policy` route ships without its DRAFT banner. +9. **Terms & Conditions** (`Terms & Conditions - Overall App.md`): §3.2 Payment Processing now cross-references the saved-card consent + Privacy Policy §2.2. (Placeholder — same 'make real' gate as above.) +10. **README / Technical Manual**: update the payments section to reflect customer provisioning + nonce-direct one-off charges. + +--- + +## Policy Pop-over Status (already-in-plan vs done) + +| Item | Status | +|---|---| +| Privacy Policy §2.2 content | ✅ Drafted into placeholder (Aug 2026) | +| Terms §3.2 cross-ref | ✅ Added to placeholder | +| `policyPopover.svelte` generalisation (`label`/`href` props) | ⏳ In plan (Step 6a) — not started | +| Pop-over on consent checkbox in `CardSelection.svelte` | ⏳ In plan (Step 6b) — not started | +| `/privacy-policy` route (HTML + PDF, mirrors `/cancellation-policy`) | ⏳ In plan (Step 6c) — not started; gated on policy being 'real' (or ships DRAFT-bannered) | +| Account page Policies block second pop-over | ⏳ In plan (Step 6d) — not started | + +--- + +## Verification + +- P12 sandbox smoke test **must run first**: confirm `POST /v2/cards` without `customer_id` actually 400s (it may not — integrations report cards can be created without it; the review marked this "unverified-high-risk"). Only proceed with customer provisioning if enforced. +- Sandbox E2E: one-off nonce charge (no card, no customer created) → save-card charge (customer created once, reused) → guest charge (no customer) → delete card (profile card disabled). +- Backend suite green; frontend build + ESLint clean. +- Frontend: pop-over renders on the consent checkbox when `canSaveCards && squareCardReady`; **Open** and **Download PDF** both work against `/privacy-policy` (incl. `?format=pdf` print path); existing 8 `/cancellation-policy` call sites unchanged (defaults preserved). +- Privacy policy diff reviewed by owner. + +--- + +## Open Questions + +1. Does Square enforce `customer_id` on `POST /v2/cards` at runtime? (P12 / R2 / M-8 — the gate.) +2. Can `CreatePayment` with a `cnon:` nonce as `source_id` be retried safely on a consumed nonce, or must the frontend re-tokenize? (Determines D3.) +3. Should the Square customer profile include phone number (Square allows it)? Decide during provisioning — email-only is the default recommendation (data minimisation). + +--- + +## What this plan explicitly does NOT do + +- No standalone "I agree to a Square customer account" checkbox. +- **No `/terms` route** (only `/privacy-policy`; a Terms page can reuse the same pattern later if the owner asks). +- No marketing use of Square customer data (out of scope; would need separate PECR/GDPR consent if ever added).