36887167c658c04979addf51cd7d9220cd03e5ea
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b7122be3a0 |
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. |
||
|
|
5dae0bba08 |
feat: Square 3DS2 SCA primary authorisation for saved-card charges; 2FA demoted to audited backup
SCA is now the PRIMARY authorisation for saved-card (ccof) charges (PSR 2017 /
chargeback liability shift); the homegrown 2FA becomes a BACKUP used only when
SCA is unavailable (e.g. a bank without in-app approval), with a strict audit
trail. The 'approve in your banking app' UX comes from Square buyer
verification. Email/SMS remains the intended 2FA delivery channel; the [2FA]
stdout-log relay (TWO_FACTOR_ALLOW_LOG_DELIVERY=true) is the explicit-insecure
pre-email/SMS stopgap.
BACKEND:
- CreateTerminalPaymentRequest gains VerificationToken (forwarded to Square in
the admin saved-card branch; validated like the other charge handlers)
- Structured SCA-required error surfacing: isVerificationRequiredError +
writeVerificationRequiredResponse (HTTP 402 with {code:'verification_required'})
at all 5 charge error sites — the frontend keys on it to trigger the challenge
- requireTwoFactorForCardAccess reworked: SCA token present => 2FA skipped
(SCA primary); no token => 2FA fallback requires delivery channel + consume +
insertTwoFAFallbackAudit (admin_audit_log reason 2fa_fallback_charge,
{sca_performed:false,...}); TWO_FACTOR_FALLBACK env flag (default true) gates
the fallback; false => SCA-only posture
- MIT vs CIT: admin till saved-card + admin booking saved-card charges now flag
customer_initiated=false (merchant-initiated, no SCA, no liability shift);
customer-initiated online flows keep true
FRONTEND:
- square_card_id threaded through SavedCard/SelectableCard + admin lists
- isVerificationRequiredSignal + shouldFallbackTo2FA helpers (402 + code / text
fallback); VERIFICATION_REQUIRED_MESSAGE
- tokenizeSavedCardWithVerification (Square SDK tokenize(details, squareCardId))
with verified/challenge-cancelled/sca-unavailable/sca-failed outcomes
- Per-surface SCA retry with the SAME idempotency key + fresh verification_token
(booking/tip/till/gift-card/admin); 'waiting for approval in your banking
app' state on admin surfaces; 2FA backup-only UX in the shared composable
MOCK PARITY:
- SimulateSavedCardVerificationRequired toggle (default off) + grandfathering
- Challenge state (ApprovePendingVerification/DenyPendingVerification,
ChallengeResult config, token-encoded _ok|_deny outcome)
- One-time-use verify_mock_ token ledger + amount/source binding
- MockCardForm saved-card verification simulation + mock Approve button
- Tests: saved-card SCA gate, one-time-use, denied, amount-mismatch,
grandfathered; frontend helper tests
DOCS: payments-doc SCA appendix, Technical Manual 2FA section, README,
Overview, Feature Catalog updated to SCA-primary + 2FA-backup; env-var
documented (42/42).
26/26 backend packages; 95/95 frontend tests + build; env-docs 42/42.
|
||
|
|
6d82535780 |
fix: adversarial review round — replay-rescue double-charge, discount credit, 2FA/per-IP limits, snapshot encryption, refund reconciliation, VAT, frontend parity, tests+docs
Addresses the adversarial fresh-eyes audit (findings A1-A20) plus review-round fixes: - CRITICAL A1: replay-by-key rescue cross-checks replayed CreatedAt; ccof blind-fail leaves pending with CRITICAL + notification instead of clawing back - A2/A3/A4: till idempotency key restored to unconditional hash; tip rejected in CreateBookingPayment; campaign discount now reduces the charged amount (deposit credit) - A5: admin notifications on blind-fail, manual-refund re-arm, cap-stranded charge-group, webhook FAILED/REJECTED refunds - A6/A10: BuyGiftCard idempotency user-scoped; gift-card slot scan advances past failed rows - A7/A14/A15: 2FA user+IP limiter, SNAPSHOT_ENC_KEY startup validation, accurate pepper/log-delivery docs - A8/A9: snapshot encryption on all write+reuse sites; MPV->SPV effective voucher type (single VAT point) - A11/A12/A13/A16: amount-aware refund reconciliation; completed-booking refund re-validation; till retry dedup; PaymentWasRefunded on SquareClient interface - A17/A18/A19/A20: CI runs npm test; confirm_overflow_tip frontend dialog; unknown-event admin notification; mock token redaction - M7 ConfirmOverflowTip, M9 snapshot encryption, C1 discount ordering regression test - Frontend vitest framework (41 tests), backend coverage for fixed functions, docs corrected (2,269 tests, SUPPORT_EMAIL tokens, resolution status) All 25 backend packages pass; frontend 41/41; build + env-docs green. |
||
|
|
78e6d00dc5 |
fix: payments review rounds — money-safety, GDPR, security, gift-card cancel, modal stacking
Money-safety: - Deterministic till idempotency fallback (Square-charging only); cash/on_the_house keep unique keys; £250 till gift-card cap; 45-char key validation - Gift-card admin caps £250/tx + £5,000/day; user buy £500/day; BuyGiftCard allowlist unchanged - CancelGiftCard: CCR 2013 14-day right with partial-spend refund of the unspent balance (spend verified via payments.gift_card_id); atomic vs redeem/transfer; refunds stay pending until reversal commits; admin cancel surface (AdminCancelGiftCard) - Sweep: cancelled-booking charges failed+notified instead of silently completed; source-override replay uses live square_source_id; legacy square-less refund sweep; snapshot refresh on pending reuse - Refund lock consolidation; recordTerminalPaymentTx shared recorder; structured Square error codes; terminal checkout CustomerID GDPR / security: - Notes retained as de-identified medical/safety record at erasure (single field treated as health data; rest of record wiped, no re-identification map) + comments updated per UK GDPR/Art 9/Equality Act 2010 - square_request_snapshot PII scrubbed on all erasure paths; delete_guest_user FK unlinks; verification codes + dispute reasons handled; idle/stale-guest erasure deletes Square cards/customers + CardDAV/R2 - Durable square-erasure outbox job (retry-square-erasures); 2FA dev/prod build split, pepper fail-closed, no prod code-in-log; prod 2FA delivery fail-loud without a channel - Webhook unknown-type family split (non-money acked, money retried); untracked dispute notifications; rate-limit CF/X-Real-IP trust gating; nginx CSP nonce + api_limit Frontend: - Dynamic z-index stack (ui/dialog/zindex.ts) claimed in open order via data-state observer; re-claims on every reopen; removes stale !z-* overrides — nested modals (booking→user→booking) always paint newest-on-top (browser-verified 3-level + reopen) - Mobile: iOS zoom fixes, bottom-sheet dialogs, 44px touch targets, inputmode decimal, dvh - Gift-card buy/cancel UI, admin £250 + daily limits, cancellation/privacy/terms policy accuracy S3: - Connect() creates buckets before probing; in-memory fallback only on genuine unreachability; health reports degraded; stale S3_PUBLIC_URL documented (host-specific) Tests/docs: - 2263 test functions; all 22 backend packages green; round8/9/10 regression suites; NextEditWindowTime removes wall-clock flake; docs reconciled (notes retention, gift-card partial-use, modal T15 future work) |
||
|
|
5cc5a7f6d2 |
fix: review round 7 — fresh-eyes audit fixes (6 agents) + full test suites for every backend change
Fresh-eyes review round with 6 independent agents (money-safety, concurrency, Square wire parity, security, frontend flow, testing-gaps). Every finding was independently verified against the code before fixing. All backend changes now carry full test suites (10+ new tests, each verified to FAIL without its guard). All 20 packages green, race detector clean. Money-safety: - Gift-card purchase refunds no longer create money: manual refunds of a no-booking (gift-card purchase) payment are rejected with a clear message in the direct handler AND never re-issued by the sweep-resume path (processManualPaymentGroup skips them; reconcile-then-fail, no re-issue). - BuyGiftCard no-client-key fallback: derived deterministically under the advisory lock (pending-row reuse fixes lost-response double-charge; completed-row sequence advance preserves distinct-purchase collapse fix). - Terminal completion is never unrecorded: activeTerminalCheckoutID now calls recordUntrackedTerminalPayment when a provisional (tmp-) checkout is found COMPLETED at Square (previously only marked the row COMPLETED — a lost poll left the payment invisible and unrefundable). - Sweep: provisional tmp- checkout rows are resolved against Square first (COMPLETED → record; live → keep guard; NOT_FOUND/CANCELED → fail; ambiguous → leave pending) instead of blind-failing a possibly-live checkout. recordUntrackedTerminalPayment re-checks the booking status (FOR UPDATE) and refuses to record on a cancelled booking, inserting a critical_payment_log admin notification instead. Till-sale post-charge UPDATE now requires status='pending' (no resurrection of a clawed-back sale). Frontend (Svelte 5): - UserPaymentModal keeps CardSelection mounted through processing (bind:this ref + Square iframe survive the loyalty/tokenize awaits) — new-card payments work again. - BookingFlow clears the cached nonce/verification pair on any failure (retry re-tokenizes fresh; idempotency key retained for dedup); 409 'already paid' refetches the booking and reconciles depositPaid so the confirmation gate opens; Back button disabled during processing. - Synchronous double-submit guards on buyGiftCard/redeemGiftCard/submitTip. Square wire parity (mock vs real): - processing_fee sign unified (negated at paymentFromSquare; mock agrees). - SimulateSourceUsed (SOURCE_USED, 400) matches real CreateCard. - GetCardsOnFile excludes disabled cards (matches ListCards). - ForcePaymentStatus toggle + tests prove the charge path can't be status-blind. - CreateCheckout rejects empty device_id (env fallback SQUARE_TERMINAL_DEVICE_ID); completed terminal checkout's payment resolvable by id. Security: - 2FA attempt-map data race fixed: lastAt is atomic.Int64 (nanos) — eviction scan reads race-free; concurrent verify+evict tests under -race. - Backend refuses to start on weak/placeholder JWT_SECRET_KEY (<32 chars or known public placeholders) with openssl rand -hex 32 guidance. - Dockerfile no longer COPYs .env (secrets injected via compose env_file). - SabreDAV requires DAV_ADMIN_PASSWORD (no admin/admin default); compose fails at config time when missing. Testing gaps closed (each verified to FAIL without its guard): - refunded-dedup 409 (CreateBookingPayment), keyed sweep past-retention blind-fail, reconcile status-switch (CANCELED/FAILED/APPROVED/PENDING/unknown in both by-key and by-id paths), resolveChargeSource Square-failure branches, structured 500 / CARD_DECLINED / cancelled-context E2E (row stays pending), deriveBookingPaymentIdempotencyKey >45-char truncation, webhook findPaymentByDisputeID fallback, clawbackOneTillSale non-gift-card branch, dispute.evidence / terminal.checkout dispatch. Infra: - local-dev-2.sh fails loudly on port-5432 squatters / docker compose failures (previously died silently under ERR_EXIT with hidden output). - Test harness defaults SQUARE_TERMINAL_DEVICE_ID; money_safety_fixes_test.go gained the missing build tag. Verification: go test -tags test,dev -count=1 -parallel 8 ./... (20/20 ok), -race clean on 2FA + payments money paths, go build ./... + -tags dev, go vet clean, svelte-check 0 errors, env-docs gate OK (36 vars), docker compose config valid. |
||
|
|
67cf5b9a45 |
fix: review round 6 — P0 deposit charge, idempotency rotation, dev-safety guard, 2FA/webhook hardening
Sixth fresh-eyes review pass (5 agents: goal, QA, code-quality, security, context-mining). QA FAILED the deposit-required new-card flow; the P0 root cause was backend + frontend, now fixed. All 20 packages green. P0 money-safety: - Deposit-required bookings now actually charge the deposit on new-card payment. Two-part fix: (1) CreateBookingHandler re-reads the trigger-maintained total_amount/total_duration_minutes from the DB after the booking_services insert (the INSERT..RETURNING row predates the recalc trigger, so TotalAmount serialized as 0 and DepositPaid computed TRUE on an unpaid booking — the frontend gate trusted deposit_paid:true, never charged, and confirmed the booking with zero payment rows); (2) BookingFlow.svelte gates the confirmation view on depositPaid and guards against re-creating a booking on retry. Regression test TestBookings_Create_DepositPaidFalseOnUnpaidBooking. Payments (idempotency + money): - deriveBookingPaymentIdempotencyKey: no-client-key fallback now advances a sequence for repeatable types (partial) and rotates past refunded completed rows, so refund-then-repay and equal-amount partials diverge onto distinct keys; an un-refunded completed row keeps its key (double-charge protection holds). Dedup hits on refunded rows now 409, never stale success. - chargeFailureStatus default is 503 (ambiguous), never 402; table test. - Flaky TestBookingPayment_FullPayment_SplitsIntoDepositAndBalance fixed (ORDER BY payment_type). - resolveChargeSource: orphaned card-on-file disabled via DeleteCardOnFile when SaveCardForUser fails (best-effort, redacted log); retry path preserved. Square client: - Dev builds HARD-FAIL (panic) on SQUARE_ENVIRONMENT=production without SQUARE_ALLOW_REAL_API=1; sandbox routes with a loud banner. - Mock fault-injection FailAfterCommit (commit-then-5xx) exercises the exact lost-response same-key retry; SimulateCardTokenUsed; 45-char idempotency-key cap parity; SquareEnvironment/SquareLocationID shared env helpers used by the sweep (env contract no longer comment-only). - listRefunds truncation now errors (money-sensitive reconcile retries instead of over-refunding); getCardsOnFile truncation loudly logged. Webhooks + 2FA: - square-environment header checked fail-closed (403) when configured env is production/sandbox; dispatch DB work bounded by 30s timeout contexts. - 2FA codes HMAC-SHA256 pepper'd (TWO_FACTOR_PEPPER) with legacy-hash migration + upgrade-on-verify; disable-flow mint cooldown (1/min, 429) caps the brute-force loop; in-lockout records never LRU-evicted. Repo hygiene: - env-docs CI gate green again (FRONTEND_ORIGIN + SQUARE_ALLOW_REAL_API + TWO_FACTOR_PEPPER documented; Vite DEV built-in allowlisted). - Dead square_deposits schema dropped; obsidian/README/legal-page drift fixed (consumeradvice.scot signposting, CORS allowlist, p11 R3/P13, T1). - 2FA disable residual documented; P6 email/SMS delivery and P12 sandbox smoke test remain the pre-go-live gates. Verification: go test -tags test,dev -count=1 -parallel 8 ./... (20/20 ok), go build ./... + -tags dev, go vet clean, svelte-check 0 errors, env-docs gate OK, live deposit-required flow re-verified end-to-end (deposit £11 charged, square_payment_id recorded). |
||
|
|
9bb812669e |
fix: fresh-review round — 2FA deliverability, disable re-verification, GDPR batch scrub, dispute alerting, docs accuracy
Second fresh-eyes review pass (7 agents: goal, security, code-quality, context-mining, webhooks+2FA, client+mock+sweep, refunds/giftcards/handlers). Money-safety core verified sound (identical-body replay byte-lossless, clawback gated on definitive proof, no double-charge window). This round fixes the issues the fresh pass surfaced: 2FA: - Setup now DELIVERS the code via the [2FA] server log in ALL modes (was: nothing in enforced mode -> production 2FA was an unbreakable dead-end and saved-card charges were permanently 403). Enforced mode still withholds the code from the API response; the log line is the fake delivery channel until email/SMS lands (P6). - Disabling 2FA now requires a fresh verification code when enforcement is ON (previously ignored the code -> a password-only attacker could lift the gate). Shares the 5-attempt lockout and timing-safe compare. Dev bypass retained. - REQUIRE_2FA parsing normalized (false/0/off/no, case-insensitive); startup warning extended to the empty-env/mock-client/enforced-2FA confusion. GDPR: - anonymize_user() SQL now scrubs two_factor_* columns + staff notes, so the idle-account batch cleanup (CleanupIdleAccounts) is erasure-clean, not just the user-initiated delete path. Webhooks: - dispute.created for an untracked Square payment now raises a critical_payment_log admin notification (chargeback the app can't reconcile is never silent). Reason strings truncated on rune boundaries (valid UTF-8). Stale at-most-once comment corrected; revertTillSaleGiftCardFunding duplication noted. Sweep/mock parity: - Mock CreatePayment dedup is now source-aware (IDEMPOTENCY_KEY_REUSED on source mismatch) matching ReplayPaymentByKey and real Square. - COMPLETED-but-never-polled terminal till-sale checkouts are now recorded by the sweep (previously only booking checkouts were; till charges were invisible until the 24h blind-fail WARN). - Legacy snapshot-less minimal-body replay, SQUARE_LOCATION_ID drift, and in-memory-mock-restart limitations documented. Docs: - Webhook path corrected everywhere (/webhooks/square, not /api/webhooks/square - a deployer following the old path would 404 and silently lose all webhook reconciliation). - 2FA enforcement semantics + code-delivery mechanism documented accurately (fail-closed default; log-delivery channel; disable re-verification). - README/User Manual note the 2FA requirement on online saved-card payments. Tests: 2,151 (up from 2,142). Backend 26/27 packages green (crussell/db fails only in this environment: local postgres doesn't offer scram-sha-256 for the test role; package is byte-identical to HEAD and untouched here). Frontend builds; svelte-check 0 errors. |
||
|
|
e9b0f0f2a7 |
fix: review-loop hardening — identical-body replay, 2FA gates, webhook at-least-once, GDPR scrub
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. |
||
|
|
91fe5ea399 |
Fix compose backend env file; document R2 and Square webhook variables
compose.yml backend service now reads the root ./.env (which README instructs users to create) instead of the nonexistent backend/.env. Promote R2_ACCESS_KEY/R2_SECRET_KEY/R2_BUCKET/R2_PUBLIC_URL to active vars (prod S3 reads all four via getEnv) and document the webhook URL/signature-key exact-match requirement with fail-closed (503/403) wording. |
||
|
|
52e2bfff55 |
Add dev-only frontend Square mock mode for as-if-live walkthroughs
VITE_SQUARE_ENVIRONMENT=mock renders a plain HTML card form (MockCardForm) instead of the Square Web Payments SDK iframe, minting the same cnon: tokens the backend dev mock accepts — all 8 payment flows run end-to-end locally with zero credentials. - isSquareMock() gated on import.meta.env.DEV: structurally impossible in a production build even if the env var is mis-set - MockCardForm: Luhn/brand/expiry/CVC validation, Amex 15-digit + 4-digit CVC, error states, disabled propagation — mirrors the real form's onReady contract so CardSelection.isCardValid and submit guards behave identically - tokenize() maps typed card -> deterministic cnon: token matching backend detectCardInfo (4242->test-card, 4111->visa, 5555->mastercard, 3782->amex) - lazy-loaded via dynamic import: mock code ships in its own chunk, referenced only from the mock branch, never statically imported into the main bundle - docs: .env.example (mock pairing with SQUARE_ENVIRONMENT=mock), P11 plan (mock opt-in + canonical-last4 caveat), Feature Catalog (2.1, 2.5) - prettier formatting fixes in 10 unrelated files (line wrapping only) |
||
|
|
7439fa86c1 |
Fix payment review round 3: saved-card idempotency, stale-pending sweep, webhook fail-closed
R1/R4: saved_card branch in CreateTerminalPayment now mirrors CreateTipPayment - advisory lock (crussell:payment:<bookingID>) serializes concurrent double-clicks - deterministic key bookingID-sc-type-amount-cardID (<=45 chars) so a lost-response retry derives the same key and dedups instead of double-charging - idempotency switch inside the lock: completed -> dedup, pending -> reuse with pence amount-guard, failed -> clean 409 - success response includes card_brand/card_last4 (frontend already reads them) R2: add 'failed' case to all four retry switches (tip, booking, gift card, till) - a swept/definitively-rejected record returns 409 instead of 500-ing on the idempotency_key UNIQUE constraint R3: extend SweepStalePendingPayments to till_sales card rows - sweeps pending till_sales (online_square/in_person_card) past Square's ~24h key retention, closing the double-charge window for till sales - swept rows logged with the same CRITICAL manual-reconciliation marker as the refund sweep Webhook fail-closed: reject 503 when SQUARE_WEBHOOK_SIGNATURE_KEY unset, 403 on bad signature (was: skip verification in dev) Refund status resolution: refunds now resolve by Square status (COMPLETED/PENDING/FAILED/REJECTED) instead of assuming completed; real error codes (REFUND_AMOUNT_INVALID, PAYMENT_NOT_REFUNDABLE, REFUND_ALREADY_PENDING) added to the definitive/processed classification HTTP client: CreateCard key truncated to <=45 chars, device_options always sent (env SQUARE_TERMINAL_DEVICE_ID fallback), processing_fee reads amount_money, ListCards cursor loop, refund keys hashed to <=45 chars Other fixes: payment/till/gift-card advisory-lock + FOR UPDATE asymmetries, GetPaymentByID NULL scans, loyalty redemption lock, card upsert on conflict, mock ccof: prefix parity, IsValidSquareCheckoutID for real Square IDs, isAdminRequest defense-in-depth on all 6 admin payment handlers, webhook signature docs, M8/L5 debug markers removed Docs: README/FC/TM/Overview updated (22 jobs, 20 CRITICAL sites, 23-section GDPR export, sweep jobs, webhook fail-closed); P11 plan marks remaining items (sandbox smoke test, M-8 customer_id, saved-card key dedup trade-off) as deferred with rationale; gap backlog pruned of completed items |
||
|
|
64d4b65083 |
Implement P11: Square Web Payments SDK new-card tokenization
Re-enable new-card entry across all 8 flows via Square Web Payments SDK cnon: nonces (backend was already P11-ready): - Add square.ts SDK loader (env-gated on VITE_SQUARE_APPLICATION_ID/LOCATION_ID, sandbox vs prod URL auto-derived from app-ID prefix) + SquareCardInput.svelte (tokenize() via bind:this, onReady state, CardEntryUnavailable fallback) - CardSelection.svelte: replace newCardDisabled gate with new-card toggle + SquareCardInput; expose tokenize() for parent flows - Wire new-card mode into tip x3, booking payment (UserPaymentModal), deposit (BookingFlow incl. guest), Buy a Gift Card + Add a Card (account), and admin till online_square (GiftCardsManagement create/topup) - Retry-safe: each flow caches the one-shot nonce and reuses it on retry so the backend idempotency key dedups instead of re-tokenizing - Docs: README, Gap Backlog P11, Feature Catalog, Technical Manual, P11 plan |
||
|
|
b3729c8a5a |
docs: add DAV_SKIP_INIT to .env.example
CI / Nginx config check (push) Failing after 1s
CI / Docker compose check (push) Successful in 34s
CI / Env docs check (push) Successful in 35s
CI / Frontend deps check (push) Successful in 38s
CI / Frontend major deps (push) Successful in 40s
CI / Go build (push) Successful in 41s
CI / Staticcheck (dev) (push) Failing after 1s
CI / Frontend build (push) Successful in 46s
CI / Secrets scan (push) Successful in 47s
CI / Security scan (prod) (push) Failing after 1s
CI / go mod tidy (push) Failing after 0s
CI / Go vulnerabilities (push) Failing after 1s
CI / Knip (push) Successful in 45s
CI / Frontend QC (typecheck) (push) Failing after 0s
CI / Frontend QC (lint) (push) Failing after 0s
CI / Frontend QC (audit) (push) Successful in 41s
CI / Svelte strict check (push) Has been skipped
CI / Go vet (prod) (push) Successful in 1m29s
CI / Frontend a11y check (push) Successful in 1m52s
CI / Go vet (dev) (push) Successful in 1m51s
CI / Staticcheck (prod) (push) Successful in 2m1s
CI / golangci-lint (push) Successful in 2m30s
CI / Security scan (dev) (push) Successful in 2m54s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
|
||
|
|
85c043ad7f |
fix: add golangci-lint to pre-commit hook, clarify R2_ENDPOINT in env example
Pre-commit now runs golangci-lint (3m timeout) after go vet, before staticcheck. R2_ENDPOINT comment updated to clarify it is prod-only and local dev uses S3_* vars. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> |
||
|
|
9e3c9323c3 |
ci: fix nginx config check cp path, add R2_ENDPOINT to .env.example
CI / Nginx config check (push) Failing after 11s
CI / Docker compose check (push) Successful in 11s
CI / Env docs check (push) Successful in 12s
CI / Frontend deps check (push) Failing after 24s
CI / Knip (push) Has been skipped
CI / Frontend a11y check (push) Has been skipped
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / go mod tidy (push) Successful in 21s
CI / Frontend build (push) Successful in 1m25s
CI / Svelte strict check (push) Has been skipped
CI / Frontend QC (audit) (push) Has been skipped
CI / Frontend QC (typecheck) (push) Has been skipped
CI / Frontend QC (lint) (push) Has been skipped
CI / Go vulnerabilities (push) Successful in 1m38s
CI / Go vet (push) Successful in 1m40s
CI / Staticcheck (push) Failing after 2m19s
CI / golangci-lint (push) Successful in 2m30s
CI / Security scan (gosec) (push) Failing after 2m51s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
- nginx-check: cp needs trailing filename when copying to a directory - .env.example: uncomment R2_ENDPOINT as an active variable so the env-docs-check script detects it as documented |
||
|
|
49ee167d69 |
chore: add missing env vars to .env.example
Add 11 previously undocumented env vars: TEST_DB_HOST, TEST_DB_DSN, GO_TESTING, SQUARE_WEBHOOK_NOTIFICATION_URL, DAV_BASE_URL, DAV_ADMIN_PASSWORD, NO_COLOR, VITE_BACKEND_URL, RUSTFS_* Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> |
||
|
|
2e1ab9d745 |
feat: Square payment integration, booking flow redesign, and timezone/weekday fixes
- Add Square payment integration (mock + handlers + UI): terminal/online payments, refunds, tips, saved cards, webhooks. Build-tagged dev/prod clients. - Redesign booking flow: Step 4 conditional (deposit only), Step 5 confirmation screen with booking ID, auto-submit on transition. - Redesign schedule modal: 2x3 button grid with Pay Deposit/Pay Early logic. - Add deposit warning banner at Step 1 for users with outstanding deposits. - Fix weekday conversion bug: Go 0=Sunday vs DB 0=Monday mismatch in 6 locations. - Fix timezone bug: UTC vs London time in closing hours validation. - Fix frontend error parsing: plain text backend errors now displayed correctly. - Fix crypto.randomUUID fallback for environments without Web Crypto. - Add 7 new regression tests: closing hours, advance check, active booking limit, weekday conversion, UTC/London, deposit snapshot, exceptional hours. - Fix 3 flaky tests: dynamic dates instead of fixed, no-show timing. |
||
|
|
970cc5554d |
feat: add email verification, profile pictures, deposits, and calendar
export Backend: - Add email verification code generation and verification endpoints - Add profile picture upload with S3 storage and image processing - Add deposit_required field to users with 48h advance booking requirement - Add loyalty stamps that accumulate on completed bookings - Auto-transition bookings: confirmed → in_progress → completed - Add booking cancellation handler with no-show detection - Add ICS calendar file download endpoint for bookings - Sync bookings to CalDAV on confirmation Frontend: - Add schedule page route - Add avatar and image-cropper UI components - Update shadcn-svelte components (button, dialog) - Add "Add to Calendar" button in booking modal Database: - Add verification_codes table - Add profile_pic_url, loyalty_stamps, deposits_required to users - Various schema updates |
||
|
|
9259de9393 |
Portfolio: add filtering, URL sharing, and improved tag input
- Add category filters with dynamic counts that reduce as filters applied - Add ?filter[category]=value URL params for filterable links - Add ?img= timestamp param that bypasses filters to show specific image - Update URL when opening/navigating/closing modal for shareable links - Backend: add /api/portfolio/filters endpoint with filter logic - Backend: add timestamp lookup fallback for GetImage endpoint Frontend: - Portfolio page: filter dropdowns, keyboard nav, mobile improvements - ImageUpload: live tag suggestions from API, arrow/Tab navigation, confirmation modal before upload, mobile-optimized touch targets - Add scrollbar-hide utility and fix filter dropdown overflow - Move Clear all button, add vertical separator on desktop |
||
|
|
dfd552b02f |
feat(storage): add S3/R2 abstraction with local Rustfs dev
- Add backend/internal/s3/ with build-tag pattern (dev vs prod) - Dev: Uses local Rustfs container (S3-compatible) - Prod: Stub for R2 Cloudflare (add AWS SDK to implement) - Add S3 env vars to .env.example and .env - Add Rustfs service to compose.yml - Add Rustfs reset to local-dev-2.sh (wipes data on each run) |
||
|
|
28815d3b25 | Update readme |