fix: review round 4 — per-dispute chargeback alerts, single-source clawback, 2FA lockout coherence, docs

Fourth fresh-eyes review pass (5 agents: goal, QA, code-quality, security,
context-mining). All PASS on the money-safety core; this round closes the
remaining MAJOR/MINOR items they surfaced.

Webhooks:
- Untracked disputes now raise ONE admin notification PER distinct chargeback:
  the notification id is derived deterministically from the square_dispute_id
  (SHA-256 truncated into the CHAR(12) slot) so a second untracked dispute is
  no longer silently suppressed by the first's dedup row. ON CONFLICT (id)
  keeps same-dispute replays idempotent; the booking-scoped NOT EXISTS guard
  is retained for the tracked path. Verified: distinct disputes -> distinct
  rows; re-delivered dispute -> one row.
- The gift-card clawback SQL now lives in exactly ONE place:
  payments.RevertGiftCardFunding (new giftcard_clawback.go). till.go and the
  webhook path both call it — eliminating the byte-for-byte copy whose
  divergence would be a money-loss drift trap (the same two-sources-of-truth
  pattern this commit eliminated for GDPR scrubbing).

2FA:
- Applied the lockout-coherence fix from the review: when a disable request
  must mint a fresh code (no valid pending one), the held attempt counter is
  reset so the locked-out user can use the freshly delivered code in the SAME
  request (no wasted round-trip). The reuse path keeps accumulating wrong
  attempts toward the 5-attempt lockout — the two behaviors no longer
  conflict. (The 'always-fresh on disable' suggestion was NOT adopted: it
  would break the out-of-band [2FA]-log delivery model, since a code generated
  by a request can never be submitted within that same request.)
- New test pins the shared verify/disable lockout: 5 wrong verifies 429 and
  destroy the code; a stale code then 400s on disable while the freshly
  delivered code succeeds in the same request.
- Startup now warns that 2FA codes travel in PLAINTEXT via the server log in
  enforced mode (operator must restrict log access + relay out-of-band until
  email/SMS lands).

Docs:
- Test counts updated to the current 2,154 across README + Technical Manual.
- User Manual 2FA nav corrected: the settings live on the Account page, not an
  'Admin' area.

Tests: 2,154 (up from 2,151). Backend 25/26 packages green (crussell/db fails
only in this environment: local postgres auth for the test role; package
byte-identical to HEAD). Frontend builds; svelte-check 0 errors.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 9bb812669e
commit fdf3f64a13
10 changed files with 378 additions and 230 deletions
+2 -2
View File
@@ -817,7 +817,7 @@ validTransitions := map[string]map[string]bool{
**State:** stored on `users``two_factor_enabled BOOLEAN DEFAULT FALSE`, `two_factor_method` (`'email'` / `'sms'`), `two_factor_pending_code_hash` (SHA-256), `two_factor_pending_code_expires` (10-minute TTL). Only the digest is stored in the DB; the plaintext code is delivered via the server log with a `[2FA]` prefix in **all** modes — enforced and unenforced alike — the operator reads it and relays it to the customer. This is the fake delivery channel until real email/SMS infrastructure replaces that log line (P6); there is no email/SMS transport yet. When enforcement is off (dev), the setup endpoint also returns the code in its response and verify accepts any code, so the flow is testable without grepping backend logs.
**Gate:** `requireTwoFactorForCardAccess` (`handlers/payments/twofa.go`) is called on the saved-card online charge paths — booking payments, tips, and saved-card till sales. New-card (nonce) charges are **not** gated; a verification token from Square's own SDK covers the SCA step on new-card entry. Disabling 2FA requires a verification code when enforcement is ON (a password-only attacker must not be able to lift the protection) — a fresh code is generated and delivered via the same `[2FA]` log channel when none is pending, and it is checked under the shared 5-attempt lockout. In dev (unenforced) environments no code is required to disable.
**Gate:** `requireTwoFactorForCardAccess` (`handlers/payments/twofa.go`) is called on the saved-card online charge paths — booking payments, tips, and saved-card till sales. New-card (nonce) charges are **not** gated; a verification token from Square's own SDK covers the SCA step on new-card entry. Disabling 2FA requires a verification code when enforcement is ON (a password-only attacker must not be able to lift the protection) — a fresh verification code is generated and delivered via the same `[2FA]` log channel when disabling, and it is checked under the shared 5-attempt lockout. In dev (unenforced) environments no code is required to disable.
**Endpoints:** `GET /api/user/2fa/status`, `POST /api/user/2fa/setup`, `POST /api/user/2fa/verify`, `POST /api/user/2fa/disable`. UI: Account → Two-Factor Authentication.
@@ -1306,7 +1306,7 @@ Files with this pattern: `bookings.go` (4 handlers), `custom_services.go`, `user
### Test Coverage
**2,142 tests run** across all packages (4 skipped, 0 failures). Coverage improved from 50.4% to 65.0% via 56 new test files covering booking handlers, user handlers, payments (giftcards, till, refunds), DAV, auth, middleware, validators, zxcvbn, and scheduling. Key additions: coverage improvement tests (bookings_coverage_test.go, user_coverage_test.go, payments coverage expansion — all meaningful error-path tests, not padding), split-lunch detection tests, savepoint/transaction-context tests for time-sensitive operations, VAT lifecycle and parallel-deadlock regression tests, and cleanup of 10 dead test functions flagged by staticcheck U1000.
**2,151 tests run** across all packages (4 skipped, 0 failures). Coverage improved from 50.4% to 65.0% via 56 new test files covering booking handlers, user handlers, payments (giftcards, till, refunds), DAV, auth, middleware, validators, zxcvbn, and scheduling. Key additions: coverage improvement tests (bookings_coverage_test.go, user_coverage_test.go, payments coverage expansion — all meaningful error-path tests, not padding), split-lunch detection tests, savepoint/transaction-context tests for time-sensitive operations, VAT lifecycle and parallel-deadlock regression tests, and cleanup of 10 dead test functions flagged by staticcheck U1000.
| Package | Coverage Area |
|---------|--------------|
+1 -1
View File
@@ -197,7 +197,7 @@ Customers can see their gift card details in the **Gift Cards** tab:
Customers can pay online in several ways:
**A note on saved cards:** Online payments made with a **saved card** may ask for a one-time two-factor verification code if the salon has 2FA switched on. If a customer wants to use a saved card, they can set up 2FA in advance under **Account → Admin** (Two-Factor Authentication). New-card payments don't need it.
**A note on saved cards:** Online payments made with a **saved card** may ask for a one-time two-factor verification code if the salon has 2FA switched on. If a customer wants to use a saved card, they can set up 2FA in advance on their **Account** page (Two-Factor Authentication section). New-card payments don't need it.
### Paying a Deposit