diff --git a/README.md b/README.md index 3b11975..cc23f63 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Nail salon booking platform — Go 1.25 backend + SvelteKit 5 SPA + PostgreSQL 1 ## Features -**Booking**: Self-service (customer), walk-in (admin), call-in (admin). Slot reservations prevent double-booking (4 TTL types). **Self-blocking prevention**: `excludeUserID` parameter filters a user's own `RESERVATION` entries from time blocker overlap checks, allowing re-reservation and booking at overlapping slots. **Explicit cancellation**: `DELETE /api/bookings/reserve` releases a user's reservation. **Background cleanup**: 5-minute goroutine clears expired reservations. Guest accounts with GDPR-compliant anonymization (including `RESERVATION:edit_request:%` scrubbing). Service eligibility based on age + patch test validity. Overlap checks use `FOR UPDATE` row locks inside transactions. Closing-hours validation extracted into a reusable `closing_time` helper. +**Booking**: Self-service (customer), walk-in (admin), call-in (admin). Slot reservations prevent double-booking (4 TTL types). **Self-blocking prevention**: `excludeUserID` parameter filters a user's own `RESERVATION` entries from time blocker overlap checks, allowing re-reservation and booking at overlapping slots. **Explicit cancellation**: `DELETE /api/bookings/reserve` releases a user reservation; `DELETE /api/admin/bookings/reserve` releases an admin walk-in/call-in reservation. **Background cleanup**: 5-minute goroutine clears expired reservations. Guest accounts with GDPR-compliant anonymization (including `RESERVATION:edit_request:%` scrubbing). Service eligibility based on age + patch test validity. Overlap checks use `FOR UPDATE` row locks inside transactions. Closing-hours validation extracted into a reusable `closing_time` helper. **Payments**: Square Terminal (in-person) + Web Payments SDK (online). Cash with change calculation. Gift cards (12-digit code or account balance). Saved cards for faster checkout. Tips on completed bookings. Refunds with notice-period tiers and deposit protection (72h/24h thresholds). All payment types: deposit, full, partial, balance, tip. Payment >20% of total promotes `pending_release` bookings back to `confirmed`. Deposit paid is computed from payments on-the-fly. The first 50% of each payment is always carved out as deposit (via `buildSplitRecords`); any overflow beyond the booking total becomes a tip. A PostgreSQL `pg_advisory_lock` serializes payment attempts per-booking to prevent two-tab double-payment races. Gift card purchases now insert a pending payment record with VAT before calling Square — the DB transaction commits first, so Square failures leave a retryable pending record. @@ -76,7 +76,7 @@ Default logins (password: `password`): ```bash cd backend && go build -o bin/backend ./main.go cd frontend && npm ci && npm run build -cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 1,180 tests, 0 failures, 4 skipped (~14s) +cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 1,198 tests, 0 failures, 4 skipped (~13s) cd backend && go test -tags "test,dev" -count=1 -race -timeout 480s ./... # race detector (all packages, ~4min) cd backend && go test -tags "test,dev" -count=10 -parallel 8 ./... # thorough verification (~2-3min) ``` diff --git a/obsidian/Crussell/Overview.md b/obsidian/Crussell/Overview.md index 40ad395..e833b69 100644 --- a/obsidian/Crussell/Overview.md +++ b/obsidian/Crussell/Overview.md @@ -215,7 +215,7 @@ npm run dev # Dev server with HMR ```bash cd backend -go test -tags "test,dev" ./... # 1,180 tests, 0 failures, 4 skipped +go test -tags "test,dev" ./... # 1,198 tests, 0 failures, 4 skipped go test -tags "test,dev" -v -run TestName ./... # Single test ``` diff --git a/obsidian/Crussell/Technical Manual.md b/obsidian/Crussell/Technical Manual.md index c0de9ae..03ae573 100644 --- a/obsidian/Crussell/Technical Manual.md +++ b/obsidian/Crussell/Technical Manual.md @@ -62,7 +62,7 @@ Backend (:8080) | Package | File(s) | Purpose | |---------|---------|---------| | `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 | 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, active booking limits, GetBookingsByCreatedRange, created_by_name resolution, **explicit reservation cancellation** (`DELETE /api/bookings/reserve`) | +| `handlers/bookings` | bookings.go, reserve.go, manage.go, admin_reserve.go, cancel_reservation.go, admin_cancel_reservation.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, 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. Still uses hex-encoding stub (`verifySquareSignature`) — production requires HMAC-SHA256 with base64 output, `x-square-hmacsha256-signature` header. See `TODO(PROD)` in source. | | `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) | @@ -315,6 +315,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS. | DELETE | `/api/bookings/{id}/edit-request` | Cancel edit request | | GET | `/api/bookings/{id}/edit-request` | View own pending edit request (enriched) | | GET | `/api/bookings/edit-requests` | List all own pending edit requests (enriched) | +| DELETE | `/api/bookings/reserve` | Release user's own active reservation. Targets `RESERVATION:user:%` only (does not touch admin or anon reservations — partitioned by WHERE clause). Idempotent: 200 with no-op if no reservation exists. | | POST | `/api/bookings/{id}/payment` | Create online payment (deposit, full, partial, balance) | | POST | `/api/bookings/{id}/tip` | Add tip to completed booking | | GET | `/api/bookings/{id}/payment-summary` | Get payment summary for booking | @@ -342,6 +343,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS. | POST | `/api/admin/bookings/{id}/confirm` | Confirm booking | | POST | `/api/admin/bookings/{id}/cancel` | Cancel booking | | POST | `/api/admin/bookings/reserve` | Reserve slot (walkin=5min, callin=1h) | +| DELETE | `/api/admin/bookings/reserve` | Release admin's walk-in/call-in reservation. Targets `RESERVATION:admin:%` only (does not touch user or anon reservations — partitioned by WHERE clause). Idempotent: 200 with no-op if no reservation exists. | | GET | `/api/admin/bookings/{id}/edit-requests` | List edit requests (paginated, with total) | | GET | `/api/admin/bookings/edit-requests` | List ALL edit requests (enriched) | | GET | `/api/admin/bookings/{id}/edit-request` | View pending edit request for specific booking (enriched) | @@ -524,8 +526,8 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS. |------|-----|---------------------| | Logged-in user | 1 hour | `RESERVATION:user:{userID}:{timestamp}` | | Anonymous user | 10 minutes | `RESERVATION:anon:{ipHash}:{timestamp}` | -| Admin walk-in | 5 minutes | `RESERVATION:admin:walkin:{timestamp}` | -| Admin call-in | 1 hour | `RESERVATION:admin:callin:{timestamp}` | +| Admin walk-in | 5 minutes (15min cleanup) | `RESERVATION:admin:walkin:{customerID}:{timestamp}` | +| Admin call-in | 1 hour (15min cleanup) | `RESERVATION:admin:callin:{customerID}:{timestamp}` | | Edit request | 24 hours | `RESERVATION:edit_request:{timestamp}` | **Anonymous Cap:** 50 reservations per 10-minute rolling window. Returns 429 if exceeded. @@ -1242,7 +1244,7 @@ Files with this pattern: `bookings.go` (4 handlers), `custom_services.go`, `user ### Test Coverage -**1,180 tests run** across all packages (4 skipped, 0 failures). Recent additions: self-blocking prevention tests (excludeUserID coverage for GetAvailableHours, EditBookingHandler, AdminRescheduleBookingHandler, ReserveSlotHandler anon IP cleanup, AdminReserveSlotHandler anon IP cleanup), closing_time tests (3), content-type middleware tests (2), booking handler tests (FOR UPDATE overlap checks, admin reserve with closing_time, gift card buy with VAT). Booking integration tests continue to expand: duplicate completion guard, daily stamp cap (handler + SQL subquery), invalid status transitions, sequential edit, timezone independence, and past-booking no-show guard. The `clock` package itself has tests for Now() and clock interface correctness. +**1,198 tests run** across all packages (4 skipped, 0 failures). Recent additions: self-blocking prevention tests (excludeUserID coverage for GetAvailableHours, EditBookingHandler, AdminRescheduleBookingHandler, ReserveSlotHandler anon IP cleanup, AdminReserveSlotHandler anon IP cleanup), closing_time tests (3), content-type middleware tests (2), booking handler tests (FOR UPDATE overlap checks, admin reserve with closing_time, gift card buy with VAT), and the new admin reservation cancel coverage (12 tests covering walkin + callin + isolation + no-op + idempotency + response format parity, plus 2 inverse-isolation tests on the user-side `cancel_reservation_test.go` to prove the two cancel endpoints are properly partitioned by their WHERE clauses). Booking integration tests continue to expand: duplicate completion guard, daily stamp cap (handler + SQL subquery), invalid status transitions, sequential edit, timezone independence, and past-booking no-show guard. The `clock` package itself has tests for Now() and clock interface correctness. | Package | Coverage Area | |---------|--------------| diff --git a/obsidian/Crussell/Testing Architecture & DB Management.md b/obsidian/Crussell/Testing Architecture & DB Management.md index 870a5cc..8cf4278 100644 --- a/obsidian/Crussell/Testing Architecture & DB Management.md +++ b/obsidian/Crussell/Testing Architecture & DB Management.md @@ -1,6 +1,6 @@ # Testing Architecture & DB Management -**Last Updated:** June 2026 (v2 — flakiness elimination, full t.Parallel coverage, titleCaser concurrency fix, -count=10 verification) +**Last Updated:** July 2026 (v3 — admin reservation cancel coverage, 1,198 tests) --- @@ -499,9 +499,9 @@ This appears in `TestAccount_DeleteGuest` and `TestLoyalty_Get`. The `dav.Servic | Metric | Value | |--------|-------| -| Quick check (`-count=1`) | **~14s** | +| Quick check (`-count=1`) | **~13s** | | Packages | 19 tested, 0 failures | -| Tests | 1,169 run, 4 skipped, 0 failing | +| Tests | 1,198 run, 4 skipped, 0 failing | New test additions in this batch: | Test | Coverage | @@ -516,8 +516,11 @@ New test additions in this batch: | `TestContentType_*` | 2 tests for the new `JsonContentType` middleware | | `TestAdminReserveSlot_*` (expanded) | New overlap coverage using `FOR UPDATE` inside transactions | | `TestBuyGiftCard_*` (expanded) | Tests for pending-payment-first flow with VAT integration | +| `TestAdminCancelReservation_*` (12 new) | New admin reservation-cancel coverage: walkin + callin success, isolation, no-op, unauth, empty ctx, walkin+callin coexistence, anon untouched, response format parity with user handler, overlapping reservations deleted, user reservations untouched, idempotent double-cancel | +| `TestCancelReservation_DoesNotTouchAnonReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:anon:%` (defensive — the WHERE clause only matches `RESERVATION:user:%`) | +| `TestCancelReservation_DoesNotTouchAdminReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:admin:%`. Pairs with the admin-side test that verifies admin cancel ignores `RESERVATION:user:%`. Proves the two endpoints are properly partitioned. | -**Total tests:** 1,169 run (4 skipped) across 19 packages. 0 failures. Growth driven by new `clock` package tests, `closing_time` tests (3), `contenttype` middleware tests (2), and expanded booking/payment handler test coverage. +**Total tests:** 1,198 run (4 skipped) across 19 packages. 0 failures. Growth driven by new `clock` package tests, `closing_time` tests (3), `contenttype` middleware tests (2), expanded booking/payment handler test coverage, and the new admin reservation cancel coverage (12 tests: walkin + callin success, isolation, no-op, unauth, empty ctx, walkin+callin coexistence, anon untouched, response format parity, overlapping reservations deleted, user reservations untouched, idempotent double-cancel). Two inverse-isolation tests added to the existing user-side `cancel_reservation_test.go` to verify the user and admin cancel endpoints are properly partitioned by `RESERVATION:user:%` / `RESERVATION:admin:%` WHERE clauses. ### What Drives Test Time @@ -634,9 +637,9 @@ This shouldn't appear anymore — the auth package's TestMain was updated to use ### Q: What's the total test count? -1,180 tests run across all packages (4 skipped). 0 failures across 20 packages. +1,198 tests run across all packages (4 skipped). 0 failures across 20 packages. -**Notable new tests:** Duplicate completion guard (idempotent second `"completed"` call), daily stamp cap (two completions same day → 1 stamp), invalid status transitions (no-show→completed rejected with 400), sequential edit (two edits in sequence), timezone independence (UTC in, UTC out — no shift), past-booking no-show guard (past confirmed booking cancelled → `client_cancelled`, not `no_show`). New closing_time tests (3), content-type middleware tests (2), clock package tests, expanded admin reserve overlap tests, and expanded gift card buy flow tests with VAT. +**Notable new tests:** Duplicate completion guard (idempotent second `"completed"` call), daily stamp cap (two completions same day → 1 stamp), invalid status transitions (no-show→completed rejected with 400), sequential edit (two edits in sequence), timezone independence (UTC in, UTC out — no shift), past-booking no-show guard (past confirmed booking cancelled → `client_cancelled`, not `no_show`). New closing_time tests (3), content-type middleware tests (2), clock package tests, expanded admin reserve overlap tests, expanded gift card buy flow tests with VAT, and full admin reservation cancel coverage (12 tests covering walkin + callin + isolation + no-op + idempotency + response format parity). ### Q: Why use `-count=10` for thorough verification?