docs: README + obsidian parity — 2FA/verification-code delivery is dev/test-only stdout log, production fails closed until email/SMS (P6); fixed posture, counts, tiers

- TWO_FACTOR_ALLOW_LOG_DELIVERY production opt-in removed from every doc: log delivery reframed as a local DEV ONLY feature while email/SMS is implemented
- Technical Manual: 2FA state + delivery, /api/verify/generate route table, future-work item
- Feature Catalog: SCA posture + verification-code feature
- payments and money processes: relay model + env var reference (removed)
- Overview, User Manual: delivery posture
- test counts, refresh-token grace, session lifetime, deposit advance, gift-card expiry, patch-test notice kept accurate
This commit is contained in:
2026-08-22 00:34:50 +01:00
parent 01e7cc575d
commit 6bd952238e
12 changed files with 94 additions and 74 deletions
+3 -3
View File
@@ -14,7 +14,7 @@ Nail salon booking platform — Go 1.26.5 backend + SvelteKit 5 SPA + PostgreSQL
**Custom Services**: One-off or special-request services not in the permanent catalog. Admin management with create, edit, promote to permanent service (migrates booking references), and delete. Full CRUD API with search, popular sorting, and pagination. Can be added to any booking alongside regular services.
**Admin**: Today page with interactive calendar grid. Booking management (create, edit, reschedule, approve, cancel). User management with customer relationship data (spend, visits, top services). Custom services (one-off services with create/edit/promote/delete). Discount campaigns (time-based and milestone). Time blocker CRUD. Portfolio image upload with tag management. Gift card management. Business settings (VAT, gift card config). Notification queue with priority ordering. The money-critical `critical_payment_log` / `refresh_token_reuse` notification queue is flood-capped at `adminnotify.MaxUnacknowledgedCriticalLogs` (100 unacknowledged rows per reason), folded atomically into the INSERT at every insert site (Square webhooks, account-erasure cleanup, time-blockers cleanup, the critical-log scan job, refresh-token-reuse detection, and the payment-sweep path); at the cap, further inserts are suppressed with an operator-facing log until outstanding notifications are acknowledged (which re-arms inserts).
**Admin**: Today page with interactive calendar grid. Booking management (create, edit, reschedule, approve, cancel). User management with customer relationship data (spend, visits, top services). Custom services (one-off services with create/edit/promote/delete). Discount campaigns (time-based and milestone). Time blocker CRUD. Portfolio image upload with tag management. Gift card management. Business settings (VAT, gift card config). Notification queue with priority ordering. The money-critical `critical_payment_log` / `refresh_token_reuse` notification queue is flood-capped at `adminnotify.MaxUnacknowledgedCriticalLogs` (100 unacknowledged rows per reason), folded atomically into the INSERT at every insert site (Square webhooks, account-erasure cleanup, time-blockers cleanup, the critical-log scan job, refresh-token-reuse detection, the payment-sweep path, the booking-creation `new_booking`/`pending_booking` inserts, the cancellation and edit-request inserts, the `refund_failed` insert, the deposit-deadline cleanup's `deposit_not_paid_by_deadline` insert, and the 1-week/1-month unpaid-booking notices); at the cap, further inserts are suppressed with an operator-facing log until outstanding notifications are acknowledged (which re-arms inserts).
**Loyalty & Discounts**: 1 stamp per paid appointment (max 1/day). 10 stamps → 10% off via opt-in checkbox at payment or till. Stamps refunded on cancellation. Campaigns auto-apply at both payment and completion: time-based, per-user milestone, global milestone (in-person only), anniversary. All discounts stack additively against original total. Discount payment records excluded from refund calculations.
@@ -82,7 +82,7 @@ The backend replays a byte-identical request to Square when it rescues a stale p
Saved-card online payments are authorised **exclusively** by Square **PSD2 SCA** (buyer verification via the Web Payments SDK's `tokenizeWithVerification`). A customer-initiated stored-credential charge is a PSR 2017-regulated transaction: Square's verification token both satisfies SCA and shifts chargeback liability to the card scheme. On the wire, the tokenize-result is sent as the charge **source** (`new_card_token`, which the backend passes to Square as `source_id`) alongside the saved-card reference — not as a separate `verification_token` (the legacy `ccof:` + `verification_token` shape is still accepted but is no longer the primary contract). A saved-card charge carrying **no** Square verification token is **refused outright** — 402 `verification_required` — and the payment does not go through (the customer can try again later; at the till, the customer is told they can pay online later instead). There is **no homegrown 2FA fallback**: PSR 2017 reg 100 makes SCA mandatory and non-waivable for customer-initiated stored-credential charges, and a merchant-side 2FA check with no bank involvement cannot legally substitute for it (authorising a token-less charge via 2FA would leave the merchant liable for ECI 7 / SLI 210 chargebacks and PSR 2017 reg 77(6) compensation regardless of consent). The `TWO_FACTOR_FALLBACK` switch was **removed entirely**. The dev Square mock simulates SCA (`SimulateSavedCardVerificationRequired` + `cnon:sca-...` tokenize-results), so development has full parity with the SCA-only production posture.
Homegrown 2FA remains for **admin and account verification only** — 2FA setup, disable, and delete-account re-authentication — **never** for authorising a card charge. The gate itself is **fail-closed**: enforcement is ON by default for any `SQUARE_ENVIRONMENT` except an explicit `mock`/`dev`/`development`/`test` value — empty or unknown values are treated as production-enforced. Disable it with `REQUIRE_2FA=false` or an explicit mock env. The intended 2FA delivery channel is email/SMS (the method chosen at setup), **not yet wired**; until it lands, the 6-digit code is delivered via the server log (`[2FA]` prefix; the operator relays it) — but only in production builds when `TWO_FACTOR_ALLOW_LOG_DELIVERY=true` is set (an explicit, insecure opt-in); without it, production code issuance fails closed (503) and no user can complete 2FA setup or disable.
Homegrown 2FA remains for **admin and account verification only** — 2FA setup, disable, and delete-account re-authentication — **never** for authorising a card charge. The gate itself is **fail-closed**: enforcement is ON by default for any `SQUARE_ENVIRONMENT` except an explicit `mock`/`dev`/`development`/`test` value — empty or unknown values are treated as production-enforced. Disable it with `REQUIRE_2FA=false` or an explicit mock env. The intended 2FA delivery channel is email/SMS (the method chosen at setup), **not yet wired** (P6). Until it lands, the 6-digit code is delivered to the **local dev stdout log** (`[2FA]` prefix; the developer/operator relays it) in **dev/test builds only** — stdout-log delivery is a local-dev convenience, never a production channel. Production builds have **no delivery channel at all** and 2FA code issuance **fails closed (503)** no user can complete 2FA setup or disable — until the email/SMS transport is implemented.
### Local dev (tmux)
@@ -101,7 +101,7 @@ Default logins (password: `password`):
```bash
cd backend && go build -o bin/backend ./main.go
cd frontend && npm ci && npm run build
cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 2,555 backend test functions under the test,dev tags + 129 frontend vitest cases, as of 15 Aug 2026 (~2min)
cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 2,554 backend test functions under the test,dev tags (per `go test -tags "test,dev" -list 'Test.*'`) + 130 frontend vitest cases (93 plain `it(` + 37 `it.each` rows in square.test.ts), as of 16 Aug 2026 (~2min)
cd backend && go test -tags "test,dev" -count=1 -race -timeout 480s ./... # race detector (all packages, ~4min)
# NOTE: -count=N>1 is unreliable for handlers/payments and handlers/webhooks —
# those suites share package-global state (Square mock ledger, in-memory webhook