Follow-up to the comprehensive payment-system review. Fixes the issues the
review found in the initial integration, plus the rough edges it introduced.
Money-safety:
- Replay-by-key now replays the FULL original request verbatim from a stored
square_request_snapshot, so a retained idempotency key returns the original
payment instead of IDEMPOTENCY_KEY_REUSED (previously the row sat pending
forever). IDEMPOTENCY_KEY_REUSED remains ambiguous (never proof of no charge).
- Dev mock mirrors real Square for unknown-key replays: ccof: saved-card
sources are charged and rescued; spent cnon: nonces surface
ErrReplayKeyNotRetained. (Fixes dev/prod parity divergence.)
- Webhook dedup row committed AFTER dispatch (at-least-once); FAILED till sales
claw back gift-card funding; event-type strings match Square's real catalog.
- Expired-gift-card cancellation refunds set creditFailed (never a phantom
'completed' refund); cancellation refunds lock all payment rows ascending.
- Sweep never rescue-completes a gift-card purchase without delivering the card.
- Tip no-client-key fallback is a deterministic count-based key under the
booking advisory lock (retry-safe, distinct tips don't collapse).
- M-cap subtracts completed refunds, clamped to [0, total].
2FA (PSD2 SCA stand-in) for online saved-card payments:
- Full feature: status/setup/verify/disable endpoints, gating helper wired into
all 7 saved-card charge paths (incl. BuyGiftCard + admin saved-card), account
admin-tab settings UI, frontend gating across all payment surfaces.
- Enforcement is FAIL-CLOSED: on unless REQUIRE_2FA=false or an explicit
mock/dev SQUARE_ENVIRONMENT; startup warning when off in a non-dev env.
- Verify is brute-force hardened (5-attempt lockout, timing-safe compare);
plaintext codes only logged when enforcement is off (dev).
- GDPR: anonymize_user also scrubs 2FA columns and staff notes.
Infra/docs:
- nginx: /api/ response cache removed (cross-user disclosure); port 80
redirects to HTTPS (localhost/RFC1918 exempt, end-anchored regexes); HSTS;
separate webhook rate-limit zone.
- Schema: users 2FA columns; payments/till_sales square_source_id +
square_request_snapshot.
- Legal docs: gift-card cooling-off, international-transfers section, tips
policy; Gap Backlog P3 webhooks marked done; stale counts/wording corrected.
- Flaky test race fixed (t.Parallel + global mock mutation); suite 26/26
packages green, 2,142 tests, svelte-check clean.
- Convert HTML comments in script sections to eslint-disable-next-line
- Fix err->_err references in catch blocks across 8 files
- Fix required→_required and onclose→_onclose prop mismatches
- Revert BookingCreateModal.svelte from no-unused-vars agent damage
- Fix broken regex in account page
- Fix .writable (not in Svelte 5 stable) back to +
- Fix NavBar dynamic href links with proper eslint-disable
Introduce getLondonTodayCalendarDate(), parseWallClockDate(), and formatLocalDateTime() for reliable Europe/London timezone handling. Replace ad-hoc SvelteDate/new Date() usage with these utilities across all components and stores.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Update frontend types, stores, and components to support name history display and new API fields.
- Add previousFirstName/previousLastName to User type and BookingUser type
- Add referral discount_source type and refunds array to Booking type
- Create nameDisplay.ts utility for rendering '(formerly ...)' labels
- Create booking.ts utility for booking-related helpers
- Update 25+ admin, payments, today, and account components to display
former names on booking cards, modals, appointment views, and user lists
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The 14-day refresh threshold was designed for the old 30-day tokens but the backend now emits 1-hour tokens. The threshold caused every page load to trigger an immediate /refresh-token call, which revoked the old JTI mid-flight while concurrent requests (profile, giftcards, bookings, etc.) were still using it, causing 401 errors.
Changes:
- Refresh threshold: 14 days -> 5 minutes (matches 1-hour token lifetime)
- Interval check: 60 min -> 2 min (must be shorter than threshold)
- Race condition: refresh now completes before profile fetch, preventing JTI invalidation during concurrent requests
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Add editable phone field in /account General tab with UK phone
validation
- Create PUT /api/user/change-password endpoint in backend
- Add zxcvbn password strength meter to change password modal
- Add "passwords don't match" validation message to both /account and
/register
- Fix navbar logout reactivity with invalidateAll and $derived values
- Fix a11y warnings: add labels, roles, and keyboard handlers
- Remove unused CSS from account page
- Add POST /api/refresh-token endpoint to router
- Auth store now calls refreshTokenIfNeeded on init and every hour
- Token refreshes automatically when within 2 weeks of expiry