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.
This commit is contained in:
2026-08-22 00:34:50 +01:00
parent c4c65d9dd8
commit b7122be3a0
78 changed files with 2861 additions and 1120 deletions
+6 -2
View File
@@ -1046,7 +1046,7 @@ func GetDefaultHoursConflictingBookings(w http.ResponseWriter, r *http.Request)
bookingLondon := ob.StartTime.In(londonLocation)
ourWeekday := int((bookingLondon.Weekday() + 6) % 7)
proposed, _ := proposedByWeekday[ourWeekday]
proposed := proposedByWeekday[ourWeekday]
isConflict := false
if !proposed.IsOpen {
isConflict = true
@@ -1204,7 +1204,11 @@ func GetScheduledDefaultHoursChange(w http.ResponseWriter, r *http.Request) {
}
var scheduledHours []DefaultHours
json.Unmarshal([]byte(*hoursJSON), &scheduledHours)
if err := json.Unmarshal([]byte(*hoursJSON), &scheduledHours); err != nil {
log.Printf("Failed to unmarshal scheduled default hours JSON: %v", err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
resp := ScheduledHoursChange{
EffectiveDate: *effDate,
@@ -483,7 +483,7 @@ func GetConflictingBookingsForExceptionHandler(w http.ResponseWriter, r *http.Re
// Go: Sun=0,Mon=1,...,Sat=6 → Our convention: Mon=0,...,Sun=6
ourWeekday := int((bookingLondon.Weekday() + 6) % 7)
proposed, _ := proposedByWeekday[ourWeekday]
proposed := proposedByWeekday[ourWeekday]
isConflict := false
if !proposed.IsOpen {
@@ -501,13 +501,14 @@ func GetConflictingBookingsForExceptionHandler(w http.ResponseWriter, r *http.Re
// For midnight-crossing bookings (endMinutes < startMinutes), the booking
// extends past midnight and always conflicts with daily hours since the
// day's open window cannot span past midnight.
if startMinutes < 0 || endMinutes < 0 {
switch {
case startMinutes < 0 || endMinutes < 0:
// parse error — treat as conflict
isConflict = true
} else if endMinutes < startMinutes {
case endMinutes < startMinutes:
// Booking crosses midnight — always a conflict with daily hours
isConflict = true
} else if startMinutes < propStartMinutes || endMinutes > propEndMinutes {
case startMinutes < propStartMinutes || endMinutes > propEndMinutes:
isConflict = true
}
}
@@ -30,6 +30,7 @@ import (
"crussell/handlers/payments"
"crussell/internal/square"
"crussell/mw"
"crussell/testutils"
"crussell/testutils/fixtures"
"github.com/go-chi/chi/v5"
@@ -3888,7 +3889,7 @@ func TestAnonymizeStaleGuestAccounts_InvalidatesSquareCustomerCache(t *testing.T
}
origSquare := payments.SquareClient
payments.SquareClient = square.NewDevClient()
payments.SquareClient = testutils.NewTestSquareClient()
defer func() { payments.SquareClient = origSquare }()
t.Cleanup(func() { payments.InvalidateSquareCustomerCache(guestID) })