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
This commit is contained in:
@@ -205,11 +205,11 @@ Multi-method payment system accepting Square (card terminal & online), cash, gif
|
||||
**Related:** [[Booking System|1. Booking System]] (completed bookings trigger tip eligibility), [[Today Page|7. Today Page]] (daily tips summary)
|
||||
|
||||
### 2.7 Refunds
|
||||
**What it does:** Admin processes refunds with automatic routing by payment method. Square card payments are refunded via Square API (post-DB-commit). Cash refunds are credited as gift card balance. Gift card payments are returned to the card's balance.
|
||||
**What it does:** Admin processes refunds with automatic routing by payment method. Square card payments are refunded via Square API (post-DB-commit). Cash refunds are credited as gift card balance. Gift card payments are returned to the card's balance. Refunds are resolved by Square's actual status (COMPLETED→completed, PENDING→left pending for the sweep, FAILED/REJECTED→failed) so an in-flight refund never blocks the over-refund guard. A background sweep (`sweep-pending-square-refunds`) retries/reconciles stuck refunds with a 23h age guard and surfaces failures as admin notifications.
|
||||
|
||||
**Layman summary:** "If a booking is cancelled, the system automatically refunds the right amount to the right place."
|
||||
|
||||
**Related:** [[Cancellation|1.11 Cancellation]], [[Refund to Gift Card|4.11 Refund to Gift Card]]
|
||||
**Related:** [[Cancellation|1.11 Cancellation]], [[Refund to Gift Card|4.11 Refund to Gift Card]], [[Background Jobs|13. Background Jobs]] (refund sweep)
|
||||
|
||||
### 2.8 Payment Split Logic (`buildSplitRecords`)
|
||||
**What it does:** When a customer pays online, a single Square charge is automatically split into up to 3 records: deposit portion (first 50%), balance portion (remaining owed), and tip portion (overflow beyond the total). This protects the deposit accounting.
|
||||
@@ -219,11 +219,11 @@ Multi-method payment system accepting Square (card terminal & online), cash, gif
|
||||
**Related:** [[Deposit System|1.2 Deposit System]], [[Tips|2.6 Tips]]
|
||||
|
||||
### 2.9 Double-Payment Prevention
|
||||
**What it does:** Two mechanisms prevent accidental double-charges: PostgreSQL advisory locks (serialize payment attempts per-booking at the database level) and idempotency keys (client-generated unique IDs).
|
||||
**What it does:** Two mechanisms prevent accidental double-charges: PostgreSQL advisory locks (serialize payment attempts per-booking at the database level) and idempotency keys (client-generated unique IDs, or deterministic keys derived from booking+type+amount+card for the admin saved-card path). Same-key retries reuse the original pending record instead of charging again. A background sweep fails stale pending payments older than Square's ~24h key-retention window, so a late retry is cleanly rejected rather than issuing a second charge.
|
||||
|
||||
**Layman summary:** "If you open two tabs and try to pay twice, only one payment goes through."
|
||||
|
||||
**Related:** [[Idempotency Keys|1.12 Idempotency Keys]], [[Idempotent Purchases|4.10 Idempotent Purchases]]
|
||||
**Related:** [[Idempotency Keys|1.12 Idempotency Keys]], [[Idempotent Purchases|4.10 Idempotent Purchases]], [[Background Jobs|13. Background Jobs]] (stale-pending sweep)
|
||||
|
||||
### 2.10 VAT Calculation
|
||||
**What it does:** Applies VAT to payments and till sales based on business settings (rate, registration status, SPV/MPV voucher type). Discount, on-the-house, and tip payments are exempt.
|
||||
@@ -473,7 +473,7 @@ The central management hub for salon operations — managing users, bookings, se
|
||||
**Related:** [[Patch Tests|1.6 Patch Tests]], [[Services Catalog|1.5 Services Catalog]]
|
||||
|
||||
### 5.9 Till Purchases (POS)
|
||||
**What it does:** Point-of-sale interface for selling gift cards at the counter (cash, card machine, saved card, online card entry, on the house).
|
||||
**What it does:** Point-of-sale interface for selling gift cards at the counter (cash, card machine, saved card, online card entry, on the house). The saved-card path charges the customer's card-on-file directly via Square (pending-first, deterministic idempotency key, advisory lock per booking); card-machine sales create a Square Terminal checkout and are completed by polling. Pending card till-sales are failed by the stale-pending sweep after Square's ~24h key-retention window.
|
||||
|
||||
**Layman summary:** "Sell gift cards at the till — take cash or card."
|
||||
|
||||
@@ -842,7 +842,7 @@ The platform infrastructure — Docker Compose stack, CI/CD, local development e
|
||||
|
||||
## 13. Background Jobs (Cron Scheduler)
|
||||
|
||||
A centralized cron scheduler that runs 20 maintenance jobs for cleanup, transitions, and data management.
|
||||
A centralized cron scheduler that runs 22 maintenance jobs for cleanup, transitions, and data management.
|
||||
|
||||
**Related:** [[Availability & Scheduling|3. Scheduling]] (hours apply), [[GDPR & Compliance|9. GDPR & Compliance]] (cleanup), [[Gift Cards|4. Gift Cards]] (expiry/cleanup), [[Payments|2. Payments]] (idempotency cleanup)
|
||||
|
||||
@@ -851,8 +851,10 @@ A centralized cron scheduler that runs 20 maintenance jobs for cleanup, transiti
|
||||
- **cleanup-expired-deposits**: Move unpaid bookings to `pending_release` state
|
||||
- **cleanup-rate-limiters**: Purge stale rate limiter entries
|
||||
- **cleanup-gdpr-export-cache**: Expire old GDPR export caches
|
||||
- **sweep-pending-square-refunds**: Reconcile/retry stuck Square refunds (23h age guard; aggregates per charge, single refund per charge, resolves by Square status, notifies on failure) — starts on the `:00` ticks
|
||||
- **sweep-stale-pending-payments**: Fail stale pending payments and till-sales older than Square's ~24h idempotency-key retention, so a late retry is cleanly rejected instead of issuing a second charge — starts on the `:01` ticks, offset one minute from the refund sweep to avoid table contention
|
||||
|
||||
**Related:** [[Slot Reservation TTLs|1.14 Slot Reservation TTLs]], [[Deposit System|1.2 Deposit System]]
|
||||
**Related:** [[Slot Reservation TTLs|1.14 Slot Reservation TTLs]], [[Deposit System|1.2 Deposit System]], [[Payments|2. Payments]] (refund sweeps, stale-pending payment sweep)
|
||||
|
||||
### 13.2 Every Minute
|
||||
- **cleanup-progressive-rate-limiter**: Clean progressive rate limiter state
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Future Work — Gap Backlog
|
||||
|
||||
**Last Updated:** July 2026
|
||||
**Last Updated:** August 2026
|
||||
**Status:** Living backlog — add to this as gaps are discovered
|
||||
**Previous version:** OUT OF DATE — this replaces the prior document. Completed items removed, new items added from exhaustive codebase audit.
|
||||
|
||||
This document has three lists: **Pre-Launch** (integration tasks), **MVP** (missing features for daily ops), **Stretch** (nice-to-have). Items are numbered sequentially. All done items are removed without tracking.
|
||||
This document has three lists: **Pre-Launch** (integration tasks), **MVP** (missing features for daily ops), **Stretch** (nice-to-have). Items are numbered sequentially. All done items are removed without tracking (see "Previously Completed" sections).
|
||||
|
||||
### Development Context
|
||||
|
||||
@@ -25,17 +25,16 @@ These are things that work fine in dev (with mocks) but need real implementation
|
||||
|
||||
| # | Task | Effort | Area | Dev Status | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| P1 | **Square payments: wire prod client alongside dev mock** | XL (5-7d) | Backend | ✅ **COMPLETED Aug 2026** — `internal/square/square_http_client.go` implements the real REST client (payments, terminal checkouts, refunds, cards, list-refunds). Prod client (`internal/square/square.go`) and dev `devProdClient` (`square_dev.go`) both call real Square when `SQUARE_ENVIRONMENT=sandbox|production`; `mock` uses the in-memory client. The health endpoint reports `"mock"`/`"ok"` accordingly (was `"not_implemented"`). | |
|
||||
| P2 | **S3/R2 storage: implement prod side of the abstraction** | M (2-3d) | Backend | Dev works (`internal/s3/s3_dev.go` — RustFS + in-memory fallback). Prod side (`internal/s3/s3.go:52-62`) returns "not implemented" for Upload/Download/Delete. The prod `S3Client` struct lacks the `*s3.Client` field entirely — it was never populated. | The storage abstraction was defined early and the dev side got a full implementation. The prod side needs the AWS SDK v2 dependency and real S3/R2 calls. Portfolio images and profile pictures will start working in prod once this is done. |
|
||||
| P3 | **Square webhook event handling: from log-only to action** | S (1d) | Backend | Webhook signature verification works (HMAC-SHA256, references Square docs). Event parsing works. But `handlePaymentUpdated` and `handleRefundUpdated` (`square.go:88-94`) only log the event data — they never update booking/payment state. | The webhook receiver was built first (parse + verify). The handlers that act on events were deferred. Now they need to: update payment status on `payment.updated`, update refund status on `refund.updated`. |
|
||||
| P4 | **Payment reconciliation: add recovery for split-brain scenarios** | L (3-5d) | Backend | 11 `log.Printf("CRITICAL: ... manual reconciliation required")` calls exist across payment, refund, and till handlers. When Square succeeds but the DB transaction fails afterwards, state diverges with no automated recovery. | This happens when the application correctly processes a Square payment but then hits a DB error on commit. In dev, this was handled by just logging it. For prod, we need a reconciliation job or retry mechanism. |
|
||||
| P4 | **Payment reconciliation: add recovery for split-brain scenarios** | L (3-5d) | Backend | 20 `log.Printf("CRITICAL: ... manual reconciliation required")` calls exist across payment, refund, and till handlers. When Square succeeds but the DB transaction fails afterwards, state diverges with no automated recovery. | This happens when the application correctly processes a Square payment but then hits a DB error on commit. In dev, this was handled by just logging it. For prod, we need a reconciliation job or retry mechanism. (Count grew from 18 to 20 with the stale-pending sweep's manual-reconciliation warnings in the Aug 2026 review round.) |
|
||||
| P5 | **Till Purchases: wire the backend payment flow** | M (1d) | Frontend + Backend | Frontend (`TillPurchases.svelte:203-208`) has the UI built but the Charge button is disabled with "Payment flow and backend integration coming soon." The till sale submission path was deferred. | The till UI is fully designed — service selection, gift card types, payment method selection. Only the final "submit payment" path was left as a placeholder. Needs the backend `till.go` sale endpoint wired. |
|
||||
| P6 | **Email/SMS notification delivery** | XL (5-7d) | Backend | `user_notification_preferences` table stores delivery preferences. 8 TODO markers reference this blocker. Notification creation works (admin_notifications table), but no delivery channel exists. No SMTP configuration, no SMS provider. 2 tests skipped as "WIP handler." | The notification queue works (reasons, priorities, acknowledging). What's missing is the delivery backend. Affects: slot eviction alerts, edit request approvals/denials, gift card codes, unpaid booking reminders, idle account warnings. |
|
||||
| P7 | **Production security headers** | S (1h) | Backend | HSTS and Referrer-Policy headers are commented out in `main.go:231-233` with TODO markers. They were left disabled for dev HTTP convenience. | Uncomment and configure for production. |
|
||||
| P8 | **Social auth stubs (Google/Microsoft/Facebook)** | L (2-3d) | Backend + Frontend | `handlers/auth/social.go` is 1 line (`package auth`). Frontend login page has 3 social buttons that show `toast.info("${provider} login coming soon")`. The `user_social_logins` table and `account_type` enum values exist from early schema design. | The schema was designed for social auth from the start (table + enum values). The OAuth flow itself was never implemented. Buttons exist as UI placeholders. |
|
||||
| P9 | **Tip payments: replace placeholder card tokens** | S (1d) | Frontend | ✅ COMPLETED July 2026 — `card_token: 'placeholder'` replaced with real saved card selection + CardInput with Luhn/expiry/CVC validation across all 3 tip pages. | |
|
||||
| P10 | **No automated database backups** | M (1d) | Infrastructure | PostgreSQL volume is persistent in Docker but no `pg_dump` cron, no point-in-time recovery. | Standard production DB setup task. |
|
||||
| P11 | **Square Web Payments SDK: re-enable new-card entry with nonce-based flow** | S-M (2-3d) | Frontend | ✅ **COMPLETED Aug 2026** — `SquareCardInput.svelte` tokenizes cards to `cnon:` nonces via the Web Payments SDK (env-gated on `VITE_SQUARE_APPLICATION_ID`/`VITE_SQUARE_LOCATION_ID`); all 8 flows re-enabled (tips ×3, booking payment, deposit, Buy a Gift Card, account Add Card, till `online_square`); `CardEntryUnavailable` kept only as the no-credentials fallback. See `plans/p11-square-web-payments-sdk.md`. | |
|
||||
| P12 | **Square sandbox smoke test (pre-go-live gate)** | S-M (1d, once credentials available) | E2E | **BLOCKED — no real Square credentials available.** Must exercise the real API path end-to-end: new-card tokenization → payment → saved card → refund → reconcile, against Square's sandbox. Also verifies the M-8 open question (is `card.customer_id` enforced as Required?). | The dev mock cannot exercise Square's real wire contract (key-length limits, `device_options`, refund statuses, error codes). This is the sole remaining item before the production flip. See `plans/p11-square-web-payments-sdk.md` Remaining Items. |
|
||||
| P13 | **Reconcile deterministically-keyed saved-card charges** | S (2-3h) | Backend | **Deferred — deliberate trade-off (N-OBS-1).** The admin "Charge Saved Card" idempotency key `bookingID-sc-type-amount-cardID` dedups two *identical* repeat charges on one booking. Not UI-reachable today (PaymentModal always sends the current `totalDue`, which changes after each charge). | Revisit if the admin flow ever gains a "charge exact amount twice" path — the key would then need a client nonce or attempt counter. Tracked from the final payment review. |
|
||||
|
||||
---
|
||||
|
||||
@@ -50,7 +49,6 @@ These are missing functionality that prevents daily operations, legal compliance
|
||||
| M3 | **Email verification calls wrong API endpoint** | S (2h) | Frontend | `+layout.svelte:33` calls `/api/verify-email` which 404s. Correct endpoints: `POST /api/verify/generate` and `POST /api/verify/check`. Every login triggers a silent failure. |
|
||||
| M4 | **CSRF protection** | S (2-3h) | Backend | SvelteKit handles CSRF for its own forms, but direct API calls to `/api/*` bypass it. |
|
||||
| M5 | **XSS input sanitization** | S (2-3h) | Backend | Backend validates format (regex, length) but doesn't sanitize HTML entities in stored fields. CSP mitigates but doesn't eliminate risk. |
|
||||
| M6 | **Square webhook signature verification — enforce always** | S (1-2h) | Backend | Currently skips if `SQUARE_WEBHOOK_SIGNATURE_KEY` is empty. Prod must always verify. Implementation exists (HMAC-SHA256), just needs enforcement. |
|
||||
| M7 | **Booking cancellation UI from user account** | S (2-3h) | Frontend | `UserBookingModal` shows details but has no cancel button. Backend `DELETE /api/bookings/{id}` exists. |
|
||||
| M8 | **Business settings management UI** | M (1-2d) | Frontend | `GET/PUT /api/admin/settings` endpoints exist. No admin page — staff use curl or SQL. |
|
||||
| M9 | **CSV/Excel export for bookings/payments** | M (1d) | Backend | No endpoint for accounting software export. SQL functions exist but not wired. |
|
||||
@@ -98,21 +96,28 @@ These don't add features but reduce maintenance cost and risk.
|
||||
| T4 | **Create or remove documented `update_data_consent()` function** | S (1h) | DB Schema | Listed in FUNCTION USAGE SUMMARY comment (~line 2401) but no `CREATE FUNCTION` exists. |
|
||||
| T5 | **Resolve 2 route-conflicted lint-ignored handlers** | S (1h) | Backend | `manage.go:27,314` — handlers exist only for tests but routes conflict. |
|
||||
| T6 | **Resolve portfolio lint-ignored handler** | S (1h) | Backend | `images.go:53` — handler referenced from tests only, never routed. |
|
||||
| T7 | **Fix README job count: 20 not 21** | S (5min) | Docs | README says "21 maintenance jobs", code registers 20. |
|
||||
| T7 | **Fix README job count: 22 not 21** | S (5min) | Docs | ✅ **COMPLETED Aug 2026** — README updated to 22 maintenance jobs (the two payment sweeps added in the review round: `sweep-pending-square-refunds`, `sweep-stale-pending-payments`). |
|
||||
| T8 | **Audit 18 silent catch blocks** | M (1d) | Frontend | 1 `catch (e) {}`, 17 `catch (_err)` — errors swallowed silently. Many should show user-facing toasts. |
|
||||
| T9 | **33 `svelte/no-navigation-without-resolve` suppressions** | M (1d) | Frontend | Create a project-wide `goto` wrapper instead of suppressing per-file. |
|
||||
| T10 | **Replace `as any` in HolidayHours** | S (30min) | Frontend | `HolidayHours.svelte:234` — `(group.hours as any[])?.map(…)`. Hours array has known shape. |
|
||||
| T11 | **Replace `e: any` in button onclick** | S (30min) | Frontend | `button.svelte:101` — click handler typed as `e: any`. |
|
||||
| T12 | **Former name display (4 TODO sites)** | S (1d) | Frontend + Backend | 4 TODOs across GiftCards + notifications needing `previousFirstName`/`previousLastName` from backend. |
|
||||
| T13 | **Fix `devProdClient` rune-arithmetic in test** | S (30min) | Backend | ✅ COMPLETED July 2026 — `rune('0'+idx)` replaced with `fmt.Sprintf("concurrent-key-%d", idx)` for proper numeric formatting beyond index 9. |
|
||||
| T14 | **Error tracking / monitoring (Sentry)** | M (1-2d) | Backend | `log.Printf()` only. No alerting on 5xx. 39 ALERT + 11 CRITICAL logs will never be seen. |
|
||||
| T14 | **Error tracking / monitoring (Sentry)** | M (1-2d) | Backend | `log.Printf()` only. No alerting on 5xx. 39 ALERT + 20 CRITICAL logs will never be seen. |
|
||||
|
||||
---
|
||||
|
||||
## Previously Completed Items (August 2026 backlog)
|
||||
|
||||
- ~~**Square payments: wire prod client alongside dev mock (P1)** — `internal/square/square_http_client.go` implements the real REST client (payments, terminal checkouts, refunds, cards, list-refunds). Prod client (`internal/square/square.go`) and dev `devProdClient` (`square_dev.go`) both call real Square when `SQUARE_ENVIRONMENT=sandbox|production`; `mock` uses the in-memory client. The health endpoint reports `"mock"`/`"ok"` accordingly (was `"not_implemented"`).~~
|
||||
- ~~**Square Web Payments SDK: re-enable new-card entry with nonce-based flow (P11)** — `SquareCardInput.svelte` tokenizes cards to `cnon:` nonces via the Web Payments SDK (env-gated on `VITE_SQUARE_APPLICATION_ID`/`VITE_SQUARE_LOCATION_ID`); all 8 flows re-enabled (tips ×3, booking payment, deposit, Buy a Gift Card, account Add Card, till `online_square`); `CardEntryUnavailable` kept only as the no-credentials fallback. See `plans/p11-square-web-payments-sdk.md`.~~
|
||||
- ~~**Square webhook signature verification — enforce always (M6)** — the webhook handler is now **fail-closed**: rejects with 503 when `SQUARE_WEBHOOK_SIGNATURE_KEY` is unset and 403 when the signature header is missing/invalid (`handlers/webhooks/square.go`).~~
|
||||
- ~~**Fix README job count (T7)** — README updated to 22 maintenance jobs.~~
|
||||
- ~~**Fix `devProdClient` rune-arithmetic in test (T13)** — `rune('0'+idx)` replaced with `fmt.Sprintf("concurrent-key-%d", idx)`.~~
|
||||
|
||||
## Previously Completed Items (July 2026 backlog)
|
||||
|
||||
- ~~**Tip payments: replace placeholder card tokens (P9)** — `card_token: 'placeholder'` replaced with real saved card selection + CardInput + Luhn/expiry/CVC validation across all 3 tip pages. CardBrandIcon SVGs added for all Square-supported brands.~~
|
||||
- ~~**Fix `devProdClient` rune-arithmetic in test (T13)** — `rune('0'+idx)` replaced with `fmt.Sprintf("concurrent-key-%d", idx)`.~~
|
||||
- ~~**Tip payments: replace placeholder card tokens (P9)** — `card_token: 'placeholder'` replaced with real saved card selection + CardInput with Luhn/expiry/CVC validation across all 3 tip pages. CardBrandIcon SVGs added for all Square-supported brands.~~
|
||||
|
||||
## Previously Completed Items (June 2026 backlog)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Square integration has two build-tagged implementations:
|
||||
- **Dev** (`//go:build dev`): Mock client simulates async checkout with polling. No real payments.
|
||||
- **Prod** (`//go:build !dev`): Connects to live Square API. Requires Square credentials in `.env`.
|
||||
|
||||
Saved cards stored in `user_saved_cards` with soft delete (`retained_until` for 7-year UK compliance). Refunds tracked in `refunds` table — partial or full. Square webhooks at `/api/webhooks/square` receive payment/refund events (HMAC-verified; currently log-only — status is tracked via the synchronous + sweep/reconcile paths, backlog P3).
|
||||
Saved cards stored in `user_saved_cards` with soft delete (`retained_until` for 7-year UK compliance). Refunds tracked in `refunds` table — partial or full. Square webhooks at `/api/webhooks/square` receive payment/refund events (HMAC-verified **fail-closed** — 503 without the signing key, 403 on bad signature; currently log-only — status is tracked via the synchronous + sweep/reconcile paths, backlog P3).
|
||||
|
||||
Fees column on `payments` stores actual Square deductions. `square_deposits` table for bank reconciliation (matching batch deposits to Mettle account).
|
||||
|
||||
@@ -53,7 +53,7 @@ Expiry is 24 months from last use (not from purchase). Each use resets the timer
|
||||
|
||||
Accounts idle 2+ years (no balance) or 5+ years (with balance) are anonymized. Balances before deletion move to `gift_card_expired_balances`. `CleanupIdleAccounts()` runs on availability fetch.
|
||||
|
||||
VAT treatment: gift cards are Single-Purpose Vouchers (SPVs) by default — VAT charged at purchase, not redemption. Configurable to Multi-Purpose Voucher (MPV) in business settings. Gift card purchases now insert a pending payment record with VAT applied before calling Square — the DB transaction commits first, so Square failures leave a retryable pending record rather than losing the payment.
|
||||
VAT treatment: gift cards are Single-Purpose Vouchers (SPVs) by default — VAT charged at purchase, not redemption. Configurable to Multi-Purpose Voucher (MPV) in business settings. Gift card purchases now insert a pending payment record with VAT applied before calling Square — the DB transaction commits first, so Square failures leave a retryable pending record rather than losing the payment. Two background sweeps close Square's ~24h idempotency-key retention window: `sweep-pending-square-refunds` reconciles/retries stuck refunds, and `sweep-stale-pending-payments` fails stale pending payments and till-sales so a late retry cannot issue a second charge.
|
||||
|
||||
### Scheduling
|
||||
|
||||
@@ -87,7 +87,7 @@ Campaign lifecycle: `draft → active → completed` (or any → `cancelled`, `a
|
||||
|
||||
### Compliance
|
||||
|
||||
**GDPR Article 15**: Full data export via `/gdpr` frontend. Async Go endpoint (`GET /api/user/gdpr-export`) with 12h in-memory cache and background generation (navigation away doesn't cancel). 21-section JSON export: user profile, bookings with overrides, payments, refunds, saved cards, social logins, loyalty redemptions, booking discounts, edit requests, affiliate payouts, forgiven no-shows, patch tests, referrals, referral discounts, notification preferences, gift_card_balance, gift_card_transactions, gift_cards, admin_audit_log, login_audit, refresh_tokens, name_history, export metadata. **Verification codes excluded** (authentication tokens are not personal data under GDPR Art 15). Frontend: skeleton loading, 2s polling, styled report cards/tables, PDF export (print CSS hides navbar + verification banner), raw JSON download.
|
||||
**GDPR Article 15**: Full data export via `/gdpr` frontend. Async Go endpoint (`GET /api/user/gdpr-export`) with 12h in-memory cache and background generation (navigation away doesn't cancel). 23-section JSON export: user profile, bookings with overrides, payments, refunds, saved cards, social logins, loyalty redemptions, booking discounts, edit requests, affiliate payouts, forgiven no-shows, patch tests, referrals, referral discounts, notification preferences, gift_card_balance, gift_card_transactions, gift_cards, admin_audit_log, login_audit, refresh_tokens, name_history, export metadata. **Verification codes excluded** (authentication tokens are not personal data under GDPR Art 15). Frontend: skeleton loading, 2s polling, styled report cards/tables, PDF export (print CSS hides navbar + verification banner), raw JSON download.
|
||||
|
||||
**Account deletion**: Registered users → `anonymize_user()` SQL function extended with child table PII scrubbing (social logins deleted, saved cards soft-deleted with PCI data cleared, verification codes expired, time blocker reservations scrubbed including `RESERVATION:edit_request:%` entries, edit request notes nulled, notification preferences deleted). External system scrubbing: S3 profile picture, Square saved cards. Guests → `delete_guest_user()` for full removal.
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ Backend (:8080)
|
||||
| `handlers/auth` | local.go, social.go | Registration (with referral code validation), login, refresh, email verification |
|
||||
| `handlers/bookings` | bookings.go, reserve.go, manage.go, admin_reserve.go, cancel_reservation.go, admin_cancel_reservation.go, closing_time.go | Booking CRUD, reservations with **self-blocking prevention** (`excludeUserID` parameter on `CheckTimeBlockerOverlap` + pre-overlap DELETE with IP hash anon cleanup), admin management, edit requests, discounts, closing hours validation (`checkClosingHours` + `getClosingTimeForDate` resolves staged default hours for bookings), active booking limits, GetBookingsByCreatedRange, created_by_name resolution, **explicit reservation cancellation** (`DELETE /api/bookings/reserve` for users, `DELETE /api/admin/bookings/reserve` for admin walk-in/call-in) |
|
||||
| `handlers/payments` | handlers.go, service.go, validators.go, giftcards.go, till.go, refunds.go, refund_policy.go | Square payments: terminal, online, refunds, tips, saved cards, gift cards (CRUD, topup, transfer, redeem, buy, expired balances, till sales). Refund calculation with notice-period tiers and deposit protection |
|
||||
| `handlers/webhooks` | square.go | Square webhook handler for payment status updates. **Fail-closed signature check** — rejects requests with 403 when `SQUARE_WEBHOOK_SIGNATURE_KEY` is set but header is missing. Dev mode: skips verification when env var is empty. HMAC-SHA256 signature verified per Square spec (base64 output, `x-square-hmacsha256-signature` header, notificationURL + body). `payment.updated`/`refund.updated` events are currently **log-only** (backlog P3 — status flows through the synchronous + sweep/reconcile paths instead). |
|
||||
| `handlers/webhooks` | square.go | Square webhook handler for payment status updates. **Fail-closed signature check** — rejects with 503 when `SQUARE_WEBHOOK_SIGNATURE_KEY` is unset, and 403 when the `x-square-hmacsha256-signature` header is missing or invalid. HMAC-SHA256 signature verified per Square spec (base64 output, notificationURL + body). `payment.updated`/`refund.updated` events are currently **log-only** (backlog P3 — status flows through the synchronous + sweep/reconcile paths instead). |
|
||||
| `handlers/admin` | users.go, analytics.go, custom_services.go, discount_campaigns.go, settings.go | Admin user management, custom services CRUD (list/create/get/update/promote/delete), discount campaigns, analytics (stub), business settings (GET/PUT with VAT, gift card config) |
|
||||
| `handlers/today` | today.go | Current/next appointment, today's grid, pending approvals, `DoneForDay` state with daily/weekly summary (`DailySummary` with `total_bookings`, `customers_served`, `summary_scope`), auto-status transitions, closed-day aggregation via `findWeekSummaryRange` + `computeAggregateSummary`. Exceptional hours lookup uses `exceptional_group_applications.week_start` (0=Monday). |
|
||||
| `handlers/user` | profile.go, account.go, guest.go, loyalty.go, customer_relationship.go, gdpr_export.go | User profile, guest creation (with CheckEmailHandler for registered-email detection), loyalty, contact info, GDPR export (async with 12h cache) |
|
||||
@@ -259,7 +259,7 @@ Added in the June 2026 security pass:
|
||||
| Fix | File | Description |
|
||||
|-----|------|-------------|
|
||||
| Removed verification code logging | `handlers/auth/local.go:545` | Deleted `log.Printf("DEBUG: Verification code for %s: %s ...")` — was leaking verification codes to stdout |
|
||||
| Webhook signature fail-closed | `handlers/webhooks/square.go:59-69` | Changed from "skip verification if header missing" to "reject 403 if key set but header missing" |
|
||||
| Webhook signature fail-closed | `handlers/webhooks/square.go:34-60` | Webhook verification is fully **fail-closed**: 503 when `SQUARE_WEBHOOK_SIGNATURE_KEY` is unset (a misconfigured deployment must not silently accept forged events), 403 when the signature header is missing or invalid. Previously it skipped verification when the key was empty. |
|
||||
| S3 delete error checking | `handlers/portfolio/images.go:975` | Changed `s3.Client.Delete(...)` (ignored return) → `if err := s3.Client.Delete(...); err != nil { log.Printf(...) }` |
|
||||
|
||||
CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS. No CSP violations expected — the SvelteKit SPA doesn't load external scripts or fonts.
|
||||
@@ -417,7 +417,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
||||
| `payment_type` | `deposit`, `full`, `tip`, `balance`, `partial` |
|
||||
| `payment_method` | `online_square`, `in_person_card`, `cash`, `giftcard`, `discount`, `on_the_house` |
|
||||
| `payment_status` | `pending`, `completed`, `failed`, `refunded` |
|
||||
| `admin_notification_reason` | `pending_booking`, `cancelled_booking`, `rescheduled_booking`, `1_week_no_pay`, `1_month_no_pay`, `affiliate_claim`, `late_cancellation`, `no_deposit`, `deposit_paid`, `edit_request`, `new_booking`, `edit_requested`, `deposit_not_paid_by_deadline`, `default_hours_changed` |
|
||||
| `admin_notification_reason` | `pending_booking`, `cancelled_booking`, `rescheduled_booking`, `1_week_no_pay`, `1_month_no_pay`, `affiliate_claim`, `late_cancellation`, `no_deposit`, `deposit_paid`, `edit_request`, `new_booking`, `edit_requested`, `deposit_not_paid_by_deadline`, `default_hours_changed`, `gift_card_purchased_for_friend`, `refund_failed` |
|
||||
| `campaign_type` | `time_based`, `milestone` |
|
||||
| `milestone_type` | `per_user_booking_count`, `global_booking_count`, `anniversary` |
|
||||
| `milestone_unit` | `bookings`, `months`, `years` |
|
||||
@@ -450,7 +450,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
||||
| `forgiven_no_shows` | Tracks no-shows forgiven by admin (booking_id, forgiven_by FK to users, created_at). Used by `CountUnforgivenNoShows()` to exclude forgiven records |
|
||||
| `payments` | Payment transactions (VAT fields, invoice_number sequence, fees column for Square deductions, saved_card_id, gift_card_id) |
|
||||
| `user_saved_cards` | Saved card details (square_card_id, brand, last4, fingerprint, soft delete with retained_until) |
|
||||
| `refunds` | Refund records linked to bookings (amount, reason, square_refund_id, created_by FK to users, ON DELETE SET NULL) |
|
||||
| `refunds` | Refund records linked to a payment (amount, reason, `square_refund_id`, `refund_attempts` int, `origin` manual|cancellation, `idempotency_key` unique, `created_by` FK to users, ON DELETE SET NULL; `booking_id` is nullable — NULL for non-booking payments such as gift-card purchase refunds) |
|
||||
| `financial_aggregates` | Monthly aggregated financial statistics (no PII) — populated when granular records expire |
|
||||
| `square_deposits` | Square deposit batch tracking for bank reconciliation (batch_id, total_amount, deposited_at) |
|
||||
| `affiliate_payouts` | Affiliate commission tracking |
|
||||
@@ -481,7 +481,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
||||
| `generate_referral_code()` | 12-char referral code with collision detection |
|
||||
| `anonymize_user(target_id)` | GDPR right-to-be-erased for registered users — child table PII scrubbing |
|
||||
| `delete_guest_user(target_id)` | Full removal of guest account |
|
||||
| `export_all_user_data(target_user_id)` | GDPR Article 15 SAR — 21-section JSON export (excludes verification_codes; includes admin_audit_log, gift_cards, name_history) |
|
||||
| `export_all_user_data(target_user_id)` | GDPR Article 15 SAR — 23-section JSON export (excludes verification_codes; includes admin_audit_log, gift_cards, name_history) |
|
||||
| `get_vat_return_data(start, end)` | VAT return summary for MTD. **Updated:** Now includes `till_sales` via `UNION ALL` — till sales (gift cards, merchandise, services) are counted alongside booking payments for VAT reporting. |
|
||||
| `export_sales_transactions(start, end, include_vat)` | Tax-compatible transaction export. **Updated:** Uses dynamic `vat_rate` from the `payments` table (or `business_settings.default_vat_rate`) instead of hardcoded 1.20. |
|
||||
| `get_monthly_business_summary(start, end)` | Monthly revenue breakdown |
|
||||
@@ -1807,7 +1807,7 @@ FROM bookings b LEFT JOIN payments p ON p.booking_id = b.id WHERE b.user_id = $1
|
||||
| Frequency | Jobs | Cron |
|
||||
|-----------|------|------|
|
||||
| Every min | Progressive rate limiter cleanup | `* * * * *` |
|
||||
| Every 5 min | Reservation cleanup, expired deposits, rate limiter cleanup, GDPR cache cleanup | `*/5 * * * *` |
|
||||
| Every 5 min | Reservation cleanup, expired deposits, rate limiter cleanup, GDPR cache cleanup, **refund sweep** (`sweep-pending-square-refunds`), **stale-pending payment/till-sale sweep** (`sweep-stale-pending-payments`, offset +1 min) | `*/5 * * * *` |
|
||||
| Hourly | Loyalty redemptions, idempotency keys, revoked JTIs, stale login entries, discount campaign auto-transition | `0 * * * *` |
|
||||
| Daily 7am | Unpaid booking notifications (1-week and 1-month overdue) | `0 7 * * *` |
|
||||
| Daily 2am | Expired verification codes, expired/revoked refresh tokens | `0 2 * * *` |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# P11 — Square Web Payments SDK Implementation Plan
|
||||
|
||||
**Status:** ✅ COMPLETE (implemented August 2026 — all 8 flows re-enabled; new-card entry tokenized via `cnon:` nonces)
|
||||
**Status:** ✅ COMPLETE — IMPLEMENTED (August 2026). All 8 flows re-enabled; new-card entry tokenized via `cnon:` nonces. Two follow-up items remain intentionally open (see [Remaining Items](#remaining-items--why-deferred) — both require real Square credentials and gate the production flip).
|
||||
**Owner:** Agent implementing P11 (Square Web Payments SDK)
|
||||
**Estimated effort:** 2-3 days (backend groundwork already landed; this is now a frontend-only integration)
|
||||
**Backlog reference:** `Future Work - Gap Backlog.md` item P11
|
||||
@@ -109,13 +109,27 @@ All 8 flows render `SquareCardInput` and send the resulting `cnon:xxx` as `new_c
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (ALL COMPLETE)
|
||||
## Definition of Done
|
||||
|
||||
### Completed (all verified in the final review round)
|
||||
- [x] Square Web Payments SDK loads (sandbox + prod URLs, env-gated)
|
||||
- [x] `SquareCardInput` tokenizes cards → `cnon:xxx`
|
||||
- [x] All 8 flows re-enabled to send nonces, not PANs (tip ×3, booking payment, deposit, Buy a Gift Card, account Add Card, admin till `online_square`)
|
||||
- [x] `CardEntryUnavailable` kept only as the no-credentials fallback
|
||||
- [x] Backend nonce paths verified unchanged (Step 4/5 done)
|
||||
- [x] Frontend checks pass: svelte-check 0 errors, eslint 0 errors, build succeeds
|
||||
- [ ] Sandbox smoke test (BLOCKED — no real Square credentials available; must run before any production flip): new-card tokenization → payment → saved card → refund → reconcile, exercised against a real Square endpoint
|
||||
- [x] Docs updated (README, Gap Backlog, Feature Catalog, Technical Manual)
|
||||
|
||||
## Remaining Items & Why Deferred
|
||||
|
||||
> Both items below require **real Square credentials** (sandbox or production). They cannot be exercised against the dev mock — the mock does not enforce Square's real wire contract. They are the **sole gate on the production flip** and are tracked as open backlog items.
|
||||
|
||||
| # | Item | Why deferred |
|
||||
|---|---|---|
|
||||
| R1 | **Sandbox smoke test** — new-card tokenization → payment → saved card → refund → reconcile, exercised against a real Square endpoint | **BLOCKED — no real Square credentials available.** The full end-to-end path (Web Payments SDK nonce → `POST /v2/cards` → `POST /v2/payments` → refund → `ListRefunds` reconcile) can only be validated against Square's sandbox. Must run before any production flip. |
|
||||
| R2 | **M-8 open question: is `card.customer_id` enforced as Required at runtime?** | The app deliberately omits `customer_id` (no Square customer provisioning — linkage uses `reference_id`). Square's API reference documents `customer_id` as Required, but integrations report cards can be created without it. If a sandbox `POST /v2/cards` 400s with `MISSING_REQUIRED_PARAMETER`, the app must provision Square customers before go-live. Gated on the same sandbox credentials as R1. |
|
||||
| R3 | **N-OBS-1: saved_card deterministic idempotency key dedups identical repeat charges** | **Deliberate, accepted trade-off** (not a credential blocker). The admin "Charge Saved Card" key `bookingID-sc-type-amount-cardID` means two *identical* charges on one booking dedup to the first. Not UI-reachable today (PaymentModal always sends the current `totalDue`, which changes after a charge), and the double-click protection is worth more than a hypothetical "charge exact amount twice" path. Flagged for revisit in the Gap Backlog if that path ever appears. |
|
||||
|
||||
---
|
||||
|
||||
*Plan record — implementation complete. Remaining items require external credentials and are the pre-go-live gate.*
|
||||
|
||||
Reference in New Issue
Block a user