docs: update API docs and test counts for admin cancel

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.
This commit is contained in:
2026-07-06 17:58:08 +01:00
parent c2aa0b93cb
commit 98d561e8b6
4 changed files with 18 additions and 13 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ Nail salon booking platform — Go 1.25 backend + SvelteKit 5 SPA + PostgreSQL 1
## Features ## 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. **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 ```bash
cd backend && go build -o bin/backend ./main.go cd backend && go build -o bin/backend ./main.go
cd frontend && npm ci && npm run build 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=1 -race -timeout 480s ./... # race detector (all packages, ~4min)
cd backend && go test -tags "test,dev" -count=10 -parallel 8 ./... # thorough verification (~2-3min) cd backend && go test -tags "test,dev" -count=10 -parallel 8 ./... # thorough verification (~2-3min)
``` ```
+1 -1
View File
@@ -215,7 +215,7 @@ npm run dev # Dev server with HMR
```bash ```bash
cd backend 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 go test -tags "test,dev" -v -run TestName ./... # Single test
``` ```
+6 -4
View File
@@ -62,7 +62,7 @@ Backend (:8080)
| Package | File(s) | Purpose | | Package | File(s) | Purpose |
|---------|---------|---------| |---------|---------|---------|
| `handlers/auth` | local.go, social.go | Registration (with referral code validation), login, refresh, email verification | | `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/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/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) | | `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 | | DELETE | `/api/bookings/{id}/edit-request` | Cancel edit request |
| GET | `/api/bookings/{id}/edit-request` | View own pending edit request (enriched) | | GET | `/api/bookings/{id}/edit-request` | View own pending edit request (enriched) |
| GET | `/api/bookings/edit-requests` | List all own pending edit requests (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}/payment` | Create online payment (deposit, full, partial, balance) |
| POST | `/api/bookings/{id}/tip` | Add tip to completed booking | | POST | `/api/bookings/{id}/tip` | Add tip to completed booking |
| GET | `/api/bookings/{id}/payment-summary` | Get payment summary for 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}/confirm` | Confirm booking |
| POST | `/api/admin/bookings/{id}/cancel` | Cancel booking | | POST | `/api/admin/bookings/{id}/cancel` | Cancel booking |
| POST | `/api/admin/bookings/reserve` | Reserve slot (walkin=5min, callin=1h) | | 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/{id}/edit-requests` | List edit requests (paginated, with total) |
| GET | `/api/admin/bookings/edit-requests` | List ALL edit requests (enriched) | | 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) | | 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}` | | Logged-in user | 1 hour | `RESERVATION:user:{userID}:{timestamp}` |
| Anonymous user | 10 minutes | `RESERVATION:anon:{ipHash}:{timestamp}` | | Anonymous user | 10 minutes | `RESERVATION:anon:{ipHash}:{timestamp}` |
| Admin walk-in | 5 minutes | `RESERVATION:admin:walkin:{timestamp}` | | Admin walk-in | 5 minutes (15min cleanup) | `RESERVATION:admin:walkin:{customerID}:{timestamp}` |
| Admin call-in | 1 hour | `RESERVATION:admin:callin:{timestamp}` | | Admin call-in | 1 hour (15min cleanup) | `RESERVATION:admin:callin:{customerID}:{timestamp}` |
| Edit request | 24 hours | `RESERVATION:edit_request:{timestamp}` | | Edit request | 24 hours | `RESERVATION:edit_request:{timestamp}` |
**Anonymous Cap:** 50 reservations per 10-minute rolling window. Returns 429 if exceeded. **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 ### 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 | | Package | Coverage Area |
|---------|--------------| |---------|--------------|
@@ -1,6 +1,6 @@
# Testing Architecture & DB Management # Testing Architecture & DB Management
**Last Updated:** June 2026 (v2flakiness elimination, full t.Parallel coverage, titleCaser concurrency fix, -count=10 verification) **Last Updated:** July 2026 (v3admin reservation cancel coverage, 1,198 tests)
--- ---
@@ -499,9 +499,9 @@ This appears in `TestAccount_DeleteGuest` and `TestLoyalty_Get`. The `dav.Servic
| Metric | Value | | Metric | Value |
|--------|-------| |--------|-------|
| Quick check (`-count=1`) | **~14s** | | Quick check (`-count=1`) | **~13s** |
| Packages | 19 tested, 0 failures | | 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: New test additions in this batch:
| Test | Coverage | | Test | Coverage |
@@ -516,8 +516,11 @@ New test additions in this batch:
| `TestContentType_*` | 2 tests for the new `JsonContentType` middleware | | `TestContentType_*` | 2 tests for the new `JsonContentType` middleware |
| `TestAdminReserveSlot_*` (expanded) | New overlap coverage using `FOR UPDATE` inside transactions | | `TestAdminReserveSlot_*` (expanded) | New overlap coverage using `FOR UPDATE` inside transactions |
| `TestBuyGiftCard_*` (expanded) | Tests for pending-payment-first flow with VAT integration | | `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 ### 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? ### 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? ### Q: Why use `-count=10` for thorough verification?