Commit Graph
3 Commits
Author SHA1 Message Date
popertots 1429eddd34 fix: payments hardening — SCA wire contract (saved-card ref + tokenize-result), terminal/till token routing, tip-cap overflow carve, completion campaign atomicity, orphan B1-evidence gate, gift-card gates/locks, admin backstops
- ValidateCardInfo accepts saved-card ref + new_card_token coexistence (matches resolveChargeSource); new_card_token added to terminal/till request structs so SCA tokens are never dropped
- maxOnlineTipPence (£250) enforced on the overflow-tip carve AND buildSplitRecords (both carve paths) — closes the £10k bypass
- completion-path campaign increments made atomic reserve-first (conditional UPDATE ... RETURNING) + schema backstops (chk_times_redeemed, partial unique index on milestone redemptions)
- webhook orphan detection gated on B1 evidence (b1_attempts / sweep-duplicate refund row) so a delayed legit completion is never marked failed
- gift-card: per-user £500/day cap lock held across read-modify-write, expired-card top-up gate, NaN/Inf float bounds, refund_failed ack filter, on_the_house excluded from balance, postChargeRecheck notification
- admin apply-redemption route + admin-or-owner, in-handler isAdminRequest on 4 gift-card handlers, tip lock key aligned
- 2FA fallback machinery removed (insertTwoFAFallbackAudit/reissue/consent), dead fields stripped from charge structs
- tests: prod-tag suite, mock SCA parity, tip-cap overflow, completion races, cards pagination, ValidateCardInfo tables
2026-08-22 00:34:50 +01:00
popertots 2cdbad0cea feat: SCA-only saved-card charges — 2FA charge fallback removed (C6), versioned consent fields, token provenance
PSR 2017 reg 100 makes SCA mandatory and non-waivable for customer-initiated
stored-credential charges; a merchant-side 2FA check cannot legally substitute
for it (authorising a token-less charge via 2FA leaves the MERCHANT liable for
ECI 7 / SLI 210 chargebacks and reg 77(6) compensation regardless of consent).

- payments/twofa.go: the homegrown 2FA fallback for token-less saved-card
  charges is REMOVED ENTIRELY. requireTwoFactorForCardAccess is now SCA-only:
  a non-empty Square verification_token (charge surfaces, token forwarded to
  Square) skips the gate; anything else is refused 402 verification_required.
  enforceSCAFallbackConsent is a compile-compatible no-op (fallback never runs).
- New requireTwoFactorForCardAccessWithTokenValidation distinguishes surfaces
  where the token IS forwarded to Square (charge — Square validates it) from
  card-SAVE surfaces (token client-asserted, never forwarded: a non-empty token
  must NOT skip the save gate, auth-F1).
- SCA tokenize-result wire contract (C1): a saved card charged with a fresh
  one-time tokenize-result sends the token as the charge SOURCE (new_card_token
  -> source_id) alongside saved_card_id, never a separate verification_token.
  resolveChargeSource resolves the saved-card branch FIRST (customer from the
  card row, token as source) so combined token+card requests are SCA-clean.
- C6 consent fields (consent_version / consent_accepted) added to the booking/
  tip/till/gift-card charge requests, enforced server-side before any fallback
  charge could reach Square and recorded on the 2fa_fallback_charge audit row;
  logVerificationTokenProvenance traces minted tokens to their charge.
- user 2FA issuance gate refactored into pure build-agnostic functions
  (twoFAPepperConfigured / twoFADeliveryChannelConfigured /
  twoFAEnsureIssueAllowedStrict) shared with the payments re-issue path and
  exercised directly by the test,dev suite; TWO_FACTOR_FALLBACK switch and
  .env.example entry removed; startup posture notes updated.
- Test coverage: fail-closed 2FA production gates (pepper/delivery), token
  validation on save vs charge surfaces, completion idempotency, idempotency
  key determinism, refund-policy 72h/24h epsilon boundaries, VAT parity.
2026-08-22 00:34:50 +01:00
popertots b7122be3a0 fix: SCA review round + gitea pipeline green — GDPR audit scrub, backend test gaps, frontend SCA/Square-API, docs parity
7 review agents (pipeline run, self-review, codebase-context, frontend-placement,
backend testing-gaps, Square-API, docs-parity) audited the SCA-primary work.
ALL findings fixed, including every pre-existing red CI job:

GDPR (HIGH):
- anonymize_user() now scrubs admin_audit_log.target_user_id (mirrors
  delete_guest_user) so 2fa_fallback_charge rows (customer id + card_last4 PII)
  no longer survive registered-user account deletion; gdpr test added

BACKEND TEST GAPS (all 10):
- delivery-unavailable 503 branch: prod-tag predicate test + dev-variant marker
- twoFactorFallbackEnabled alias/case/default matrix tests + exported wrapper
- insertTwoFAFallbackAudit details-JSON shape + audit-row assertions for all
  6 gate sites (booking/tip/gift-card/payment-method/terminal/till, both actors)
- CreateTerminalPayment.VerificationToken: passthrough, too-long 400, 2FA-skip,
  token-less fallback + SCA-required (new terminal_sca_test.go)
- isVerificationRequiredError at all 5 charge sites (402 + code:verification_required)
- customer_initiated handler-level assertions (MIT false admin / CIT true customer)
- Mock: ApprovePendingVerification, ChallengeResult auto/deny, _deny token suffix,
  parseVerifyToken unit tests

FRONTEND SCA + Square-API (CRITICAL):
- tokenizeSavedCardWithVerification reads result.token (the verified token) not
  result.verificationResult (deprecated verifyBuyer shape — saved-card SCA could
  never succeed in production before); parseTokenizeVerificationResult pure fn
  extracted + pinned in square.test.ts; 'verified' with no token proceeds tokenless
- HIGH: saved-card idempotency key regenerated after a definitive 402 (fresh token
  under the same key = IDEMPOTENCY_KEY_REUSED dead-loop); kept on 503/cancelled
- challenge-cancelled copy no longer promises a 2FA fallback the UI doesn't show;
  'waiting for approval in your banking app' state on CIT surfaces
- sca-unavailable demotion resets per attempt; card selection disabled mid-challenge;
  genuine saved-card declines no longer relabeled 'requires verification';
  modal-close guard during processing; retry affordance standardized

PIPELINE (every red job now green):
- prod-tag build break fixed (shared square stub + test_helpers_test.go, prod-safe)
- govulncheck: x/image 0.45.0 bumped (x/text resolved); go mod tidy clean
- race: TestDeleteAccount_InvalidatesSquareCustomerCache made deterministic
- DAV_ADMIN_PASSWORD placeholder in .env.example (compose config passes)
- frontend: prettier 28 files, eslint, a11y 38 errors, knip (currentZIndex),
  deps in-range, audit vulns (nanoid/postcss) — all fixed; 67 vitest cases

DOCS PARITY (6 DRIFTs + 5 GAPs): payments doc Ch4/Ch14/Appendix A, Technical
Manual 2FA + counter-reset + payment sections, README test counts + SNAPSHOT_ENC_KEY,
Feature Catalog, .env.example REQUIRE_2FA — SCA-primary/2FA-backup posture verified
against code everywhere

Verified: 26/26 dev + 24/24 prod packages, both vet tags, golangci-lint/staticcheck/
gosec 0 on both tags, gitleaks clean, 2,464 backend + 67 frontend tests.
2026-08-22 00:34:50 +01:00