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)
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
The customer_id-required question (Card schema says Required, spec says
nullable) can only be resolved by a real sandbox Create Card call. The smoke
test remains the gate before any production flip: new-card tokenization ->
payment -> saved card -> refund -> reconcile against a real endpoint.
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
The P11 plan was written when the frontend sent raw PANs and the backend
had CreateCardOnFileRaw blocked. Four rounds of payment-safety work
changed the reality: the backend is fully P11-ready (CreateCardOnFileRaw
deleted; all card-creation paths accept cnon:/ccof: tokens via
CreateCardOnFile) and new-card entry is gated behind CardEntryUnavailable.
- Rewrite plans/p11-square-web-payments-sdk.md: accurate current state
(8 gated flows), backend-readiness section with verified line evidence,
Steps 4/5 marked done, revised to a frontend-only 2-3 day integration
(SquareCardInput replaces the deleted CardInput.svelte)
- README: online payments via saved cards; new-card entry gated until
nonces land; backend accepts tokens only (PCI-DSS parity)
- Gap Backlog P11: backend groundwork DONE, remaining work is
frontend-only re-enable of the 8 gated flows
- Feature Catalog 2.1/2.5 and Technical Manual: gated-not-dead-end;
Add Card posts card_token to CreatePaymentMethodFromToken
N1 (HIGH) — BuyGiftCard concurrent same-key retry could double-issue gift
cards (2× value for 1 charge). Added pg_advisory_lock on the idempotency key
(mirroring the tip pattern) acquired before the idempotency check, so
concurrent same-key retries serialize and only one executes gift-card
creation.
N2 — Amount-equality guards in both reuse branches (CreateTipPayment and
BuyGiftCard). A same-key retry with a different amount now returns 400
instead of silently mutating the pending record's books/VAT/refund caps.
N3 — test coverage:
- TestBuyGiftCard_RetryPending_ReattemptsCharge: pending record + same-key
retry re-attempts, reuses the record (count=1), completes, and issues the
gift card exactly once.
- TestCreateCheckoutHTTP_DeviceOptionsWireShape: httptest.Server asserts
device_id is under checkout.device_options (not top-level). Extracted
createCheckoutHTTPWithClient for injectable base URL.
- MockClient.CreatePayment now dedups on idempotency key (paymentByKey map),
matching real Square behaviour.
N4 — Corrected the savepoint comments in handlers.go and giftcards.go: the
savepoint only exists in the test harness; in production db.Conn.Begin is a
plain tx and the status UPDATE runs on a separate pooled connection. Commit
is a harmless no-op in prod but required in tests.
Bonus bug fixed: CheckIdempotencyByKey scanned NULL booking_id/gift_card_id
(gift-card purchases) into plain string, failing with 'cannot scan NULL'.
Now uses sql.NullString.
Docs: Technical Manual.md:53 and Feature Catalog.md (2.1, 2.5) corrected —
no longer claim Web Payments SDK is live; new-card entry is documented as
pending P11, saved-card flow works via ccof tokens, dev mock rejects raw PANs.
CRITICAL — same-amount tip retry silently never charged:
- CreateTipPayment idempotency check now only short-circuits when the
existing record is 'completed'. A 'pending' record (previous Square call
failed) is REUSED and the charge re-attempted with the same key (Square
dedups safely), instead of returning the stale pending record as 200 with
a success toast and no charge.
- Same fix in BuyGiftCard: pending records trigger a re-attempt, not a
false-success response. Unique idempotency_key constraint means the
pending record must be reused, not re-inserted.
- Fixes the savepoint/rollback interaction: the nested tx (savepoint) is
now committed in the reuse path so the deferred rollback doesn't undo the
later status UPDATE on the same connection.
- Regression test: TestTipPayment_RetryPending_ReattemptsCharge verifies a
pending record + same-key retry re-attempts and completes, reusing the
record (count stays 1).
MAJOR — terminal checkout wire contract:
- device_id now sent as checkout.device_options.device_id (Square's required
shape), not a top-level field which Square rejects with 400.
- 'checkout pending' detection now uses typed sentinel ErrCheckoutPending
with errors.Is in both handlers, matching mock and real HTTP client.
MAJOR — exp_month/exp_year omitted from card creation payload when unset
(now *int with omitempty) — Square would 400 on 0/0; expiry comes from the
tokenized source.
Docs:
- README payments/infrastructure sections corrected (Web Payments SDK claim
replaced with accurate P11-backlog note; dev mock parity described)
- Future Work P11 updated to reflect raw-PAN rejection is now enforced in
both mock and prod (new-card flows are a documented dead end)
- Added plans/p11-square-web-payments-sdk.md: full implementation plan +
handoff prompt for the agent picking up P11 (Web Payments SDK nonces)
Feature Catalog: comprehensive audit of all 15 feature areas with cross-references, verified against source code (18 parallel deep-dive agents). Gap Backlog: rewritten with dev-to-prod integration framing, 40 items across Pre-Launch/MVP/Stretch/Tech Debt. Gitleaks: whitelist obsidian/ docs containing curl examples.
Update Technical Manual (API endpoints, DB schema, scheduling system, job catalogue), Admin Manual (scheduled changes workflow), Overview, Future Work backlog, and README to reflect the new staged default hours change scheduling system with conflict detection and auto-apply at 00:05.
Update README and obsidian documentation with the new admin reservation
cancel endpoint (DELETE /api/admin/bookings/reserve), updated API route tables,
reservation TTL descriptions, and test count from 1,180 to 1,198.
Document self-blocking prevention (excludeUserID), explicit reservation cancellation endpoint, background cleanup goroutine, and edit_request reservation scrubbing. Bump test counts from 1,169 to 1,180 and package count from 19 to 20. Add race detector command to README.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Update README with middleware section, FOR UPDATE + closing_time in bookings, pending-payment-first VAT flow in gift cards, and bumped test counts (1,043 run / 1,190 defined). Update Overview.md with transaction pattern documentation, middleware details, and new files (closing_time, repo). Update Technical Manual.md test count (1,190). Update Testing Architecture with new test additions and timing.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Update project documentation and development scripts.
- Update README test counts (953/957 passing, 8 skipped)
- Simplify dev script: remove test DB seeding, add name history creation,
clean up stale test databases on startup, remove -p 1 test flag
- Update obsidian documentation for new features:
- Name history system docs
- Referral discount system docs
- Database migration docs (CHAR(12) short IDs)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Admin Manual: add summary card section (end-of-day/closed-day states). Technical Manual: update today.go, API docs for current-next and bookings endpoints, fix exceptional_group_applications column name. Testing Architecture: add working hours seeding, exceptional hours seeding, pagination test patterns, and current test suite status.
Ultraworked with Sisyphus
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Update README feature list with GDPR compliance system details. Update Overview.md Data & Compliance section with full Article 15 SAR details. Update Technical Manual: add /gdpr route, gdpr_export.go handler, GET /api/user/gdpr-export endpoint, update Key Functions table, update Guest User System section. Update Future Work backlog: mark #7 GDPR export as complete, update dependency map and Phase 3 execution order. Update test count: 446/449 -> 576/579.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Backend:
- Add enriched response types (EditSnapshot, EnrichedEditRequest) with original vs proposed snapshots
- Add 4 new GET endpoints for viewing edit requests (user and admin scoped)
- Remove github.com/lib/pq dependency — use native PostgreSQL array scanning
- Clean up edit requests, time blockers, and notifications on booking cancellation
- Validate exceptional closed hours on admin approve (409 Conflict)
- Notification upsert on edit request replace (no duplicate admin notifications)
Frontend:
- New user EditRequestModal with time/services/both modes and lunch protection
- New admin EditRequestModal with side-by-side diff (date/time, services, notes)
- Integrate edit requests into PendingApprovals card and notifications page
- Preload 3 months of availability to prevent calendar snap-back
- Apply lunch protection to isDateUnavailable in BookingFlow and BookingCreateModal
- Fix accessibility: card list items use <button> instead of <div>
Dev & Docs:
- Seed edit requests in local-dev-2.sh
- Update all Obsidian manuals with enriched edit request documentation
- 42 new tests (438/441 passing)
Split Crussell Nails.md (822 lines) into 5 focused documents:
- README.md: slimmed to 84 lines (elevator pitch + run instructions)
- Overview.md: expanded landing page with architecture diagram and quick links
- User Manual.md: 358 lines, customer-facing journeys in plain English
- Admin Manual.md: 493 lines, staff operations guide with zero technical jargon
- Technical Manual.md: 736 lines, full API reference, schema, key systems
All user/admin manuals rewritten for non-technical audiences — no API
endpoints, component names, or implementation details.
Reflect new PUT /api/admin/bookings/{id} services endpoint, EditBookingModal
component, and updated test count (306/308). Mark handleEdit() stub resolved
in gap backlog.
- Add time blocker management for booking edit requests
- Add closing_time field to admin today/current-next endpoint
- Update UserBookingModal and CurrentAppointment UI components
- Fix fmt import in bookings_test.go (was missing)
- Fix created_by FK in TestAdminApproveEditRequest_TimeBlockerOverlap
- Update test coverage for edit request time blocker overlap
- Update gap backlog documentation