Commit Graph
838 Commits
Author SHA1 Message Date
popertotsandSisyphus b8e521f4e7 test: frontend store coverage — auth, savedCards, businessInfo reactive stores
CI / Env docs check (push) Successful in 5s
CI / Go build (push) Successful in 47s
CI / Go vet (dev) (push) Successful in 1m5s
CI / Go vet (prod) (push) Successful in 1m5s
CI / go mod tidy (push) Successful in 40s
CI / Go vulnerabilities (push) Successful in 1m0s
CI / Frontend major deps (push) Successful in 40s
CI / Frontend build (push) Successful in 47s
CI / Nginx config check (push) Successful in 6s
CI / Secrets scan (push) Failing after 6s
CI / Docker compose check (push) Failing after 5s
CI / golangci-lint (push) Failing after 34s
CI / Frontend deps check (push) Failing after 46s
CI / Knip (push) Skipped
CI / Frontend a11y check (push) Skipped
CI / Svelte strict check (push) Skipped
CI / Frontend QC (audit) (push) Skipped
CI / Frontend QC (typecheck) (push) Skipped
CI / Frontend QC (lint) (push) Skipped
CI / Frontend QC (test) (push) Skipped
CI / Staticcheck (dev) (push) Failing after 1m8s
CI / Staticcheck (prod) (push) Failing after 1m9s
CI / Security scan (prod) (push) Failing after 1m51s
CI / Security scan (dev) (push) Failing after 1m55s
CI / Tests (prod) (push) Skipped
CI / Tests (dev) (push) Skipped
CI / Race (prod) (push) Skipped
CI / Race (dev) (push) Skipped
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 5051368d9b test: GDPR erasure — fix RetainsEditRequestNotes for anonymised requested_by, PreservesFinancialRows for HMRC audit fields, adversarial test cleanup
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 509b2d926d test: payment coverage — gift card daily caps, buildSplitRecords, ValidateAmount edge cases, SquareRefundStatusToLocal
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 4d179385e8 test: security regression — IDOR payment-check scoping, CORS preflight 403, rate limiter pruning, services error leakage
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus a358c8c0ea docs: README test count, Technical Manual default hours table, local-dev-2.sh config
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus ee49532774 fix: frontend 12h time display — add hour12: true to schedule, GDPR export, reschedule modal, booking create
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 8accc2ba6f fix: dev mock parity — GetCheckout/GetPayment return structured NOT_FOUND errors matching prod
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus c38dee1f62 fix: GDPR erasure — anonymize_user scrubs 17 additional tables, delete_guest_user scrubs disputes.reason, consent default FALSE
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus d90e25d1ff fix: auth/2FA — password change requires 2FA gate, admin self-deletion blocked, twofa JSON responses, per-IP email-verify budget, OptionalAuth log sanitised
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus d03ce79c19 fix: rate limiter memory pruning in Check(), services error leakage replaced with generic messages
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 37f6723d9e fix: security — CORS preflight origin gating, IDOR payment-existence check scoped to user, per-IP anonymous reservation cap
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 348a2e5bfc fix: payment validation — tip cap ValidateAmount, RefundRequest gt=0 tag, float64 pence conversion docs
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertotsandSisyphus e5c151f994 fix: gift card safety — fail-closed expiry check on refund, source-card expiry gate on transfer, oldest-first rate limiter eviction
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-22 00:34:51 +01:00
popertots 35572e1d70 fix: persist service price overrides before payment
Service price overrides in PaymentModal were only used for frontend
calculations but not persisted to the backend. This caused receipts
and subsequent payments to use original prices instead of overridden
ones.

Added saveServiceOverrides() function that calls PUT
/api/admin/bookings/{id}/services before payment to persist any
price changes. Called in both handleCardPayment and
handleSavedCardPayment before applyLoyaltyRedemption().
2026-08-22 00:34:51 +01:00
popertots c62ed029b3 style: change partial charge link color from blue to gray
Changed the 'Partial charge' toggle link color from blue to gray to
match the overall design aesthetic.
2026-08-22 00:34:51 +01:00
popertots bb51c1512f feat: add amount input fields for card and saved card payments
PaymentModal now allows partial payments for card and saved card:
- Added amount input fields with validation (max 2 decimal places)
- Default to full amount when left blank
- Validate amount is > 0 and <= totalDue
- Button label updates to show charge amount
- Validation errors shown inline

UserPaymentModal already had partial payment functionality, so no
changes needed there.

This allows customers to pay part of the balance now and pay the
rest later online.
2026-08-22 00:34:51 +01:00
popertots 80c5c6ce43 Fix email field height to match other profile fields
The Email field was 50px while First Name/Last Name/Phone were 70px
because they contain Edit buttons with min-h-11 (44px). Changed Email
field to min-h-[70px] for consistent visual height across all profile
fields.
2026-08-22 00:34:51 +01:00
popertots efea213ccd fix: change cancellation policy icon to calendar
Replace document icon with calendar icon for Cancellation & Deposit Policy button in account page Policies section.
2026-08-22 00:34:51 +01:00
popertots e46de8e9e5 fix: email field height, remove GDPR from policies/footer
- Email field: add min-h-[44px] to match other profile fields height
- Remove /gdpr from Policies section (belongs in Data Privacy only)
- Remove /gdpr from footer (belongs in Data Privacy only)
2026-08-22 00:34:51 +01:00
popertots 73f0258b97 fix: mobile UI — touch targets, safe areas, text sizes
- Button: add min-h-11 (44px) for mobile touch targets
- NavBar: add safe-area-inset-top for notched phones
- NavBar mobile menu: increase link padding from py-2 to py-3
- Footer: increase text size from text-xs to text-sm (16px minimum)
- Footer: add px-4 for better mobile spacing
- DatePicker: increase calendar cell size on mobile to 44px
- Checkbox: add p-3 -m-3 on mobile for 44px touch target (desktop unchanged)
2026-08-22 00:34:51 +01:00
popertots 4146f8e09a fix: pre-launch review — security, money safety, privacy, legal, code quality
Security (P0):
- IsJTIRevoked fails closed on DB error (previously accepted revoked tokens)
- Remove dead consume parameter from SCA gate (prevented token replay)
- Rate limiter map TTL-based eviction (prevented memory exhaustion)
- 2FA attempt map already had LRU eviction (verified)

Money Safety (P1):
- Gift card transfer refuses expired destination cards
- Gift card balance deduction has WHERE balance >= amount guard
- Webhook clawback acquires till-sale advisory lock
- Sweep/retry lock keys aligned

Privacy/Cookies (P2):
- Self-host Google Fonts (Playfair Display woff2)
- Replace CARTO map tiles with OpenStreetMap raster tiles
- Replace Wikimedia/icon-icons external images with local SVGs
- Remove external image URLs from CSP

Legal (P3):
- Privacy policy: add 6 missing data categories (gift cards, 2FA, GDPR, notifications, technical, cookies)
- Terms: add Tips section (optionality, non-refundable, same processing as bookings)

Code Quality (P4):
- twofa.Check accepts db.Querier for testability
- depositPromotionMinPct uses literal 0.20 (not misleading alias)
- HolidayHours.svelte uses proper type (not as any[])
- Remove stale TODO comments from main.go

Testing (P5):
- 94 new float64 money validity tests across 3 test files
- Cover VAT, splits, refunds, gift cards, rounding, precision boundaries
- All 27 backend test packages pass
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 9a12a2d886 fix: round-3 — tip gate asymmetry, webhook VAT align + 503 notifications, cash-tip campaign overcharge, lockout DoS, erasure durability, S3 retry cap, env parsing, per-user rate limiters, consume dead code, frontend 2FA remnants
- tip gate: CreateTipPayment saved-card 2FA gate now has scaTokenizedSavedCard skip matching every other charge surface (booking, terminal, gift-card); isSCATokenizeResultShape escape added to tip SAVE gate
- webhook: align UPDATE clears VAT fields before re-apply (matches sweep rescue); 503 unknown-event tracking with 24h timeout notification via square_webhook_events table
- cash-tip: cashChargeBasePence no longer restores campaign or subtracts loyalty — overcharge and tip shortfall fixed; 2FA dead code remnants removed from gift-card buy flow; TwoFactorCodeInput help text deconfused; refund pre-fill unit mismatch fixed (pounds vs pence); SCA buyer names split from full_name; passwordless delete UI accepts empty password
- lockout: successful current-password clears shared failed_attempts/locked_until (victim can recover from login lockout via password change); passwordless delete condition changed to require 2FA only in enforced env
- erasure: stale-guest batch erasure persists Square card/customer targets to durable outbox before NULLing them (crash-safe); S3 deletion retry capped at 10 attempts with admin notification; S3_PROFILE_PICS_BUCKET startup check added
- env parsing: IsExplicitDevOrMockEnv and Square HTTP client base-URL switch now normalize (ToLower+TrimSpace) for consistency
- auth: change-password/delete-account get per-user rate limiters (10/min); consume param dead code suppressed with TODO
- frontend: 2FA/SCA dead code removed from gift-card buy flow, TwoFactorCodeInput help text fixed, refund pre-fill unit mismatch fixed, buyer names populated from full_name

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertotsandSisyphus fba00a10ad ci: pin go install tool versions (gitleaks/golangci-lint/staticcheck/gosec/govulncheck), env-docs check covers getEnv reads, ignore .sisyphus session artifacts
- ci.yaml: no more @latest — pinned to released versions; supply-chain audit clean (govulncheck gates CI, npm audit gate, lockfiles committed, npm ci)
- check-env-docs.py: detects env vars read via the getEnv() helper (R2_* blind spot closed); 42 vars documented
- .gitignore: .sisyphus/ review reports

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 93be93f86c docs: README + obsidian parity — test counts 2,656, frontend 160, 27 maintenance jobs, 2FA log-relay opt-in contradiction fixed, Future-Work P4 count, stale line refs de-referenced
- backend test function count 2,554 -> 2,656; frontend vitest 130 -> 160 (93+37 -> 110+37); maintenance jobs 26 -> 27 (retry-s3-deletions)
- README + User Manual: removed the false 'operator opt-in [2FA] log relay' claim — production has no delivery channel, fails closed (503)
- Future Work P4: 20 -> 32 log.Printf('CRITICAL ... manual reconciliation required') sites; T7 job-count note updated
- Technical Manual: stale main.go line references removed (line numbers drift); verified thresholds consistent across docs

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertotsandSisyphus e3fa8a22b7 chore: remove obsidian editor junk, tracked node_modules, dangling root package-lock
- obsidian/Untitled.canvas, .obsidian workspace/graph/appearance json, obsidian plugin main.js binaries (8.2MB) no longer committed
- node_modules/.svelte2tsx-language-server-files remnants removed from tracking
- root package-lock.json removed (no root package.json; dangling stub)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 049c361e16 fix: adminnotify observability — money-critical rows sort first, flood-cap suppression surfaced to operator, stale coordination doc fixed
- notifications priority ordering: money-critical reasons (webhooks, sweeps, refunds, gift-card, manual-refund failures) above routine
- admin notifications page exposes the flood-cap suppressed count
- adminnotify.go contract doc: removed stale 2FA reissue-fail site, current insert-site list

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertotsandSisyphus e9b34d0ad6 fix: dev-mock/prod parity + fail-closed config gates — 402 verification, cnon:sca binding validation, refund reconcile parity, SNAPSHOT_ENC_KEY fail-closed, webhook config gates, access-token startup validation, ClientIP validation
- mock: 400->402 for CARD_DECLINED_VERIFICATION_REQUIRED, cnon:sca- tokenize-result binding validated (prefix/amount/deny), RefundPayment exact-amount reconcile parity, ReplayPaymentByKey snapshot sanity, verify_mock_ legacy widening removed, listRefunds zero-time omits begin_time
- main.go: SNAPSHOT_ENC_KEY log.Fatalf in non-mock, webhook key-set-URL-unset log.Fatalf, SQUARE_ACCESS_TOKEN/LOCATION startup validation, empty-env base URL matches 2FA production interpretation
- mw: ClientIP rejects garbage/comma/port XFF values, documented trusted-proxy requirement

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 62dca184df fix: account security + GDPR erasure — current-password lockout budgets (atomic 15/30/60 escalation, uniform 401), DAV + S3 deletion durable in-tx, batch erasure outbox
- ChangePassword/DeleteAccount: failed-attempt lockout matching login escalation, atomic check-increment (no burst), uniform 401 with distinct bodies, passwordless accounts require 2FA unconditionally to delete, NULL-password change-password clear error
- erasure: CardDAV dav_cards rows deleted inside the erasure transaction (was fire-and-forget goroutine); S3 profile-pic deletion via pending_s3_deletions outbox + retry job; stale-guest/idle-account batch paths write the outbox in-tx and skip the guessed-bucket fallback
- S3_PROFILE_PICS_BUCKET unset -> fail-closed warning (once per process)
- scheduler test: 27 jobs (retry-s3-deletions)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 1d6d3e2f8d test: round-9/10 adversarial suites — sync-path vs webhook completion, gift-card double-refund, sweep VAT, till lock, account lockout + erasure
- handlers_round9/round10: status-guarded flips, split-key hashing, cross-booking key 409, existingCount refund exclusion, SCA save-card exemption, routeNonCompletedPayment, no phantom split rows
- giftcards_round10: saved-card SCA buy, cancel resume reconcile (pending blocks, diff-only re-issue, no over-refund)
- sweep/till_round10: split-accurate VAT, all-tip VAT-free, status/key-changed skip, final-key lock held across charge
- webhooks_round8/9: booking gate + M2, payable side-effects, unknown-event 503, refund-before-row 503, no double-complete after sync
- account_round9: password lockout budgets, S3 erasure outbox, DAV in-tx deletion

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 985b114c8b test: payments round-2 — webhook gate/M2 refund, gift-card cancel re-issue, till lock contention, sweep VAT rescue coverage
- webhooks: booking-status gate rejects cancelled bookings, M2 stranded-charge refund row + alert, gift-card rows left pending, payable-booking side-effects, unknown-event 503, refund-before-row 503, webhook-after-sync no-double-complete
- giftcards: saved_card_id SCA wire, card_id+token rejected, resume re-issue never over-refunds entitlement, pending-Square-refund blocks, diff re-issue only what is owed
- sweep: VAT on split-rescued primary, all-tip rows VAT-free, till status/key-changed-while-locked skip, recordUntrackedTillSalePayment VAT
- till: suffixed-key slot scan lock held across Square round-trip

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertotsandSisyphus 77317e4a45 fix: payments money-safety round-2 — webhook booking gate + M2 stranded-charge refund, sync-path status guards, gift-card cancel re-issue reconcile, till-sweep clawback lock, sweep VAT rescue, refund credit routing
- webhook payment.updated now re-reads the booking FOR UPDATE: non-payable booking -> payment failed + stranded-charge refund row + flood-capped alert; payable booking -> full completion side-effects; gift-card rows stay pending (C6 same-key retry); unknown events -> 503 so Square retries
- sync-path completion flips (saved-card, tip, online) guarded AND status='pending' + post-flip re-read; postChargeRecheck failed-mark guarded — no webhook-first double-processing, no phantom split rows
- CancelGiftCard resume reconciles ALL Square refunds (pending blocks re-issue; COMPLETED sum >= entitlement resolves+neutralizes; else re-issues only the difference under a fresh key) — closes double-refund
- sweep till_sales fail/clawback takes crussell:till:<key> lock + post-lock status re-read; recordUntrackedTillSalePayment applies VAT; rescue align clears VAT fields before re-apply (split-accurate VAT, tip rows stay VAT-free)
- refunds: chargeAggKey widened, redeemed-card refunds route to user_giftcard_balances, guest cash refunds recorded failed + notification
- handlers: tip split-records excluded from VAT loop, splitIdempotencyKey hashed, cross-booking key reuse 409, refunded payments excluded from existingCount, SCA-mint exemption for save_card, non-COMPLETED results routed

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-08-22 00:34:51 +01:00
popertots 01b20b4420 cleanup 2026-08-22 00:34:50 +01:00
popertots 95d2ee3ccf docs: consolidated payments review session report (findings -> fixes, full verification matrix) 2026-08-22 00:34:50 +01:00
popertots 6bd952238e 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
2026-08-22 00:34:50 +01:00
popertots 01e7cc575d fix: frontend payment surfaces — SCA wire shapes (explicit token precedence), mock token parity, infinite-loop guard, money display, delete-account re-auth, admin progress UI, mobile touch targets
- new_card_token uses explicit newCardToken ?? verificationToken precedence on every charge surface (BookingFlow, UserPaymentModal, TipPayment, PaymentModal, TillPurchases, account gift-card buy); dead verification_code/consent fields + ScaFallbackConsentDialog removed from payment flows
- mock mints cnon:sca-... tokenize-results and tokenizeWithVerification returns verificationToken:null for new cards (real-SDK parity so save-card works in dev)
- UserPaymentModal infinite /payment-methods fetch loop guarded; formatCurrency(totalPaid) no longer 100x too small
- delete-account dialog collects current_password + fresh 2FA code; admin 'Begin appointment'/'Complete' wired to /admin/bookings/{id}/progress
- mobile: 44px touch targets, active: feedback, TimeSlotPicker 50dvh, dialog close sizing, .no-scrollbar utility, CSP meta, receipt fields escaped
- vitest: policy.ts cross-check + ScaFallbackConsentDialog component tests (svelte project via happy-dom)
2026-08-22 00:34:50 +01:00
popertots f9e8385d5a fix: auth/2FA security — stdout-log code delivery is dev/test-only, production fails closed until email/SMS; verification-code hashing, lockout recovery, sabredav fail-closed
- TWO_FACTOR_ALLOW_LOG_DELIVERY production opt-in REMOVED: plaintext codes are written to the stdout log ([2FA]/[VERIFY]) only in dev/test builds as a local DEV ONLY feature while email/SMS delivery (P6) is implemented. Production builds have no delivery channel and code issuance fails closed (503) under any configuration — no silent log-based code leak
- verification/2FA codes hashed at rest (HMAC-SHA256 via TWO_FACTOR_PEPPER, CHAR(64)); [VERIFY] dev log relay; per-user brute-force budget; password_reset purpose clears lockout for self-service recovery; dummy-bcrypt on login no-user path kills timing oracle
- sabredav weak-password list + entropy gate; .env.example ships fail-closed DAV_ADMIN_PASSWORD
- delete-account re-auth (current_password + fresh 2FA code when enforced)
- prod-tag suite (run-prod-tag-tests.sh) compiles and runs the production 2FA issuance gate: production ALWAYS reports no delivery channel and refuses issuance after the pepper check
- startup_checks_test SNAPSHOT_ENC_KEY values built at runtime so gitleaks sees no secret-shaped literals
- env-docs parity updated (flag removed, 38 vars)
2026-08-22 00:34:50 +01:00
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 1d9c87d6d6 docs: README + obsidian parity with SCA-only posture, flood caps, lockout tiers; dev-script secret bootstrap, stale-backend kill, patch-test backdate
- README: payments/2FA sections rewritten for the SCA-only posture (no
  TWO_FACTOR_FALLBACK, tokenize-result wire contract, 402 refusal), deposit
  carve-out clarified, gift-card 12-hex codes + 14-day cancellation, flood-cap
  insert sites enumerated, escalating lockout tiers documented, ICO
  registration note, updated test counts (2,555 backend + 129 frontend).
- local-dev-2.sh: fail-closed dev secret bootstrap (auto-generates
  JWT_SECRET_KEY / TWO_FACTOR_PEPPER into the gitignored .env), kills stale
  backends holding :8080 before the tmux reset, passes
  RUSTFS_ENDPOINT/GO_TESTING=1 to the dev backend, and backdates seeded patch
  tests 60 days so past gel bookings pass the 24h notice gate.
- Obsidian manuals (Technical/Admin/User/Feature Catalog/Overview/Gift Card
  T&C/Privacy/T&C/Testing Architecture/payments and money processes + p14 plan
  + workspace state) updated to the post-round-2 state.
2026-08-22 00:34:50 +01:00
popertots af685df40c test: rate-limit proxy-header trust (TRUST_PROXY_HEADERS) coverage
Tests lock the TRUST_PROXY_HEADERS behavior: a trusted CF-Connecting-IP
becomes the rate-limit key (per-IP and per-user+IP), an origin-exposed header
on an untrusted proxy is ignored, and the unauthenticated per-user limiter
honors header mode. Mirrors the middleware contract (finding 4a / Loop B).
2026-08-22 00:34:50 +01:00
popertots d0d72d8caf fix: refresh-token reuse grace 60s -> 20s (cross-tab coordinated rotation)
The frontend's cross-tab coordination (auth.svelte.ts REFRESH_LOCK_TTL_MS=15s +
20s wait-for-timeout) guarantees only ONE tab rotates and every sibling adopts
the rotated pair, so the only legitimately-arriving replays are same-tick
races (sub-second). The old 60s window handed a stolen refresh token a full
minute of freshness before reuse detection fired; 20s keeps comfortable margin
over the coordination bound while cutting the undetected-theft window to a
third. The ideal fix (kill only when the replay's IP/UA differs) still needs
rotation-origin persistence the locked schema cannot express.
2026-08-22 00:34:50 +01:00
popertots 8d9f72b9f0 feat: CardDAV profile sync writes through dav.Service (photo included, DAV_BASE_URL retired)
- profile.go updateCardDAV now writes directly to the shared dav_cards table via
  dav.Service (mirroring registration) instead of PUTting a vCard to
  DAV_BASE_URL over HTTP — the Go backend no longer needs the SabreDAV URL,
  only the sabredav PHP container uses the server-side credential.
- dav/types.go: ContactInput gains PhotoURL; GenerateVCard emits the
  PHOTO;VALUE=URI line when set, so the profile photo syncs into the address
  book. DAV_BASE_URL is retained in .env.example for reference only.
2026-08-22 00:34:50 +01:00
popertots 8dcfe52ac8 fix: till saved-card SCA/consent + ownership invariant (F1/F5), money-F4 top-up expiry gate, 2FA-enforced 402 tests
- till.go: the saved-card till charge carries the C6 consent fields and enforces
  them on the (unreachable) 2FA fallback path; the card ownership SELECT became
  owner-agnostic with the owner read at the gate (F1 — no charge surface can
  act on a card it does not own); a till sale's gift-card creation/top-up now
  runs under the SAME per-admin daily-cap advisory lock as the admin API
  surfaces (F5) so two concurrent distinct sales cannot overshoot the £5,000
  day ceiling; money-F4: an expired gift card can never be topped up (expiry
  gate mirrors RedeemGiftCard's DB-clock comparison) — the top-up would
  otherwise resurrect a card the nightly cleanup already forfeited.
- charge_helpers_test.go: TestResolveChargeSource_SCATokenizeResult_UsesTokenAsSource
  pins the SCA tokenize-result wire contract (token as source, card row for the
  customer).
- errors_test.go: token-less saved-card charges are refused 402
  verification_required under 2FA enforcement (create-payment, gift-card buy +
  save-card), even for a user with 2FA enabled — the homegrown gate can never
  substitute for SCA.
2026-08-22 00:34:50 +01:00
popertots 8ba76aa958 docs: legal pages — SCA/3-D Secure disclosure, gift-card terms route, footer links, GDPR verification-code scrub
- Terms & Conditions: DRAFT badge removed, updated to August 2026; SCA / 3-D
  Secure disclosure (online + till refusal behaviour), chargeback section,
  non-refundable-gift-card position, Liability and Acceptable Use sections.
- New /gift-card-terms route with the full gift-card position (14-day online
  cancellation, non-refundable except as the law requires, SPV VAT treatment);
  linked from the Terms page and the new footer.
- Footer now links Privacy Policy / Terms / Cancellation Policy / Gift Card
  Terms / Your Data instead of a bare copyright line.
- GDPR export page: verification-codes card removed — verification codes are
  authentication tokens excluded from the export, so the card could never
  populate (parity with the backend scrub).
2026-08-22 00:34:50 +01:00
popertots 0fdb2f02cd feat: frontend SCA-only posture — tokenize-result as charge source (C1), C6 refusal dialog, no 2FA fallback
- square.ts: shouldFallbackTo2FA replaced by shouldShowSCARefusal — a genuine
  'sca-unavailable' now drives the REFUSAL path (the customer is told the
  payment cannot complete and to pay online later), never the 2FA code fallback
  (PSR 2017 SCA is non-waivable; merchant liability is not cured by consent).
  SCA_REFUSAL_MESSAGE_ONLINE/TILL copy added; SCA_FALLBACK_CONSENT_VERSION 'v1'
  + scaFallbackConsentFields() carry the versioned consent on the explicit
  opt-in path only (shipped surfaces send none). SquareTokenizeResult docs
  updated: tokenize-result token is the charge source, tokenless OK proceeds
  token-less under the backend's SCA-only gate.
- C1 wire contract on every saved-card surface (booking, tip, gift-card buy,
  till, account): the proactive SCA tokenize-result is sent as new_card_token
  (the charge SOURCE alongside the saved-card ref), never the legacy
  verification_token; 402 verification-required now means the tokenize-result
  was consumed/expired between tokenize and charge.
- New ScaFallbackConsentDialog surfaces the refusal notice; the code input
  (useTwoFactorCodeForSavedCard scaAvailable: () => true) only ever appears via
  a backend gate rejection (defensive/opt-in).
- Till (M10): proactive saved-card SCA runs per sale line BEFORE the first
  charge; sca-unavailable aborts the whole sale before any charge.
- Card save (M11/M12): STORE-intent tokenizeForStore with SCA at tokenization;
  402 verification-required on save surfaces SCA-first guidance instead of a
  generic failure.
2026-08-22 00:34:50 +01:00
popertots 9a75ebc794 fix: booking hardening — notification flood caps (C5) + pending-release eviction refunds (C4)
- C5: new_booking, pending_booking, cancelled_booking and edit_requested admin
  notifications are flood-capped per reason (pre-check logs suppression; atomic
  fold inside the INSERT), so a booking/cancellation flood cannot bury the
  operator's notification centre.
- C4: EvictPendingReleaseOverlapping no longer re-sells a slot over a
  customer's money. Evicted pending_release bookings that carry a paid deposit
  are refunded FIRST — through payments.ProcessCancellationRefundTx (exported
  cancellation-refund machinery) inside the same transaction, full-refund
  override (business is re-selling the slot) — and only THEN flipped to
  'deposit_lapsed'. Rows are SELECTed FOR UPDATE first so the guard predicate
  stays true; a refund failure aborts the eviction so the caller rolls the
  whole transaction back. Card refunds record 'pending' and settle via the
  pending-refund sweep post-commit.
- Reschedule-fee audit payload whitespace alignment fix.
2026-08-22 00:34:50 +01:00
popertots 69a854d857 fix: admin notification flood caps (C5) at every remaining insert site; gift-card expiry-sweep TOCTOU (M4)
- adminnotify: MaxUnacknowledgedCriticalLogs global cap exposed as
  CriticalLogsCapExceeded — a pre-check helper every insert site pairs with the
  atomic fold inside its INSERT (count-then-insert is atomic, closing the
  TOCTOU where concurrent inserts could both read a below-cap count).
- jobs/cleanup.go ScanCriticalPaymentLogs: capped at the shared cap, pre-check
  skips the scan and logs the suppression.
- scheduling: 1_week_no_pay, 1_month_no_pay, default_hours_changed,
  deposit_not_paid_by_deadline and the Square-erasure critical notification all
  flood-capped with pre-check + atomic fold (per-booking/per-user dedup kept).
- time-blockers.go CleanupExpiredGiftCards (M4): the expiry SELECT now runs
  under FOR UPDATE row locks so the read-expired-then-zero window is atomic —
  a concurrent top-up either commits before the SELECT (refreshed last_used_at
  drops the card out of the predicate) or blocks until the sweep's tx ends and
  revives the zeroed card via its own expiry refresh; the top-up value can
  never be destroyed by the sweep.
- flood-cap tests added for 1_week_no_pay; adminnotify unit coverage added.
2026-08-22 00:34:50 +01:00
popertots 16304bd295 fix: refund sweep — manual-refund audit rows (admin_refund) + refund_failed flood cap
- MEDIUM-3a audit coverage: the refund sweep's re-issue of manual refund rows
  now records the admin actor, payment, pence amount and reason under
  action_type 'admin_refund' via the shared InsertAdminAuditCharge helper
  (best-effort own-transaction, non-fatal; distinct from the booking-level
  'admin_booking_refund'); legacy rows with NULL created_by fail harmlessly.
- C5 flood cap: the unacknowledged 'refund_failed' notification queue is capped
  at adminnotify.MaxUnacknowledgedCriticalLogs — pre-check logs the suppression,
  the fold inside the INSERT enforces it atomically, and the (reason,
  booking_id) NOT EXISTS dedup is preserved.
2026-08-22 00:34:50 +01:00
popertots 42130865f4 fix: gift-card money fixes — partial clawback surfaces CRITICAL (M6), per-admin daily-cap lock (M7), DB-clock expiry
- M6: RevertGiftCardFunding no longer silently drops unreclaimable money. A
  partially-spent create/top-up claws back everything still on the card/balance
  (GREATEST(0, ...) clamp instead of the old guarded 0-row block), inserts a
  CRITICAL admin notification, and returns errClawbackPartiallyReversed so every
  caller (till handler, stale-pending sweep, webhook) surfaces the residual
  without forking the money logic; balance comparisons use pence (penceLess).
- M7: the £5,000/day admin gift-card value cap (create/top-up/transfer) is now
  serialized per-admin under a bounded advisory try-lock
  (acquireGiftCardDailyCapLock) so two concurrent operations cannot both read
  the day's value before either writes and over-issue value.
- M4/M3: every gift-card expiry comparison now reads the DATABASE clock
  (giftCardExpired -> SELECT NOW()), the same clock that wrote expiry_date, so
  app-clock drift can neither extend nor shorten card life; applied on redeem,
  cancellation assessment, cancel-for-user and the reversal re-verification.
- C6 consent fields carried on BuyGiftCardRequest and enforced on the
  (now unreachable) 2FA fallback audit path; fallback audit row captures the
  versioned consent.
2026-08-22 00:34:50 +01:00
popertots 2a47021673 fix: stale-pending sweep hardening — auto-refund stranded charges (M2), VAT re-apply (M5), single clock source (M3)
- M2: a stale pending payment COMPLETED at Square on a cancelled/lapsed/no-show
  booking no longer just fails the row + admin-notifies: an automatic pending
  refund row for the full stranded charge is created (same shape/origin as
  ProcessCancellationRefundTx, deterministic idempotency key, square_payment_id
  written when missing) so the pending-refund sweep issues it at Square.
- M5: sweep rescues re-apply VAT — rescued till sales run ApplyVATToTillSale and
  rescued payments apply ApplyVATToBookingPayment per record after the align
  UPDATE (which no longer NULLs the VAT fields), keeping rescued charges in VAT
  reporting. Both SQL functions are idempotent (guarded on vat_amount IS NULL).
- M3: every age-guard cutoff in the sweep is computed from clock.Now() and
  passed into SQL as parameters (never a DB NOW()-derived comparison) so the
  23h/24h Square idempotency-key retention decision cannot flip on clock skew;
  replayRescueUpperBoundSkew (5s) stops a legit same-key retry that raced the
  sweep from being misclassified as the sweep's own replay-created duplicate.
- C2: till cash/giftcard charges now serialize under the same
  crussell:payment:<bookingID> advisory lock as the online path (bounded
  try-lock) so remaining-balance checks can never both pass.
- webhooks_completion_asymmetry_test: webhook-first completion + sweep rescue
  double-complete race locked end-to-end through the real handler.
2026-08-22 00:34:50 +01:00
popertots d25ba16aa7 feat: Square SCA tokenize-result wire contract — token as source_id, byte-identical dev mock
The CURRENT saved-card SCA contract (Square card.tokenize(verificationDetails,
cardId)) returns a one-time tokenize-result that must be sent as the charge
SOURCE (source_id), not a separate verification_token.

- square_dev.go: the mock validates the WIRE BODY (mockPaymentWireBody — an
  independently assembled copy of buildCreatePaymentBody) so it accepts exactly
  the request shape the real client emits. SimulateSavedCardVerificationRequired
  now demands SCA on every saved-card charge in both wire shapes: (a) a genuine
  tokenize-result (cnon:sca-... — isSCATokenizeResultSource) as source_id +
  customer_id is ACCEPTED (the token IS the buyer verification); a RAW
  card.tokenize() nonce in the tokenize-result slot is REJECTED
  CARD_DECLINED_VERIFICATION_REQUIRED (money-F2 — the mock is the enforcement
  point that stops the forged shape); (b) legacy ccof: + verification_token is
  kept for backward-compat.
- square_http_client.go: byte-identical body assembly shared with the mock, so
  TestCreatePayment_SCA_SavedCard_WireBody_ByteIdentical pins the mock and the
  real client emit identical CreatePayment bodies (a wire drift fails the test
  before reaching prod).
2026-08-22 00:34:50 +01:00