Update docs: test counts, Square wire contract, planned upcoming integrations

Refresh README and obsidian docs to the post-review state: 1,902 tests passed (4 skipped), 23 jobs / three sweeps, nonce-direct one-off charges, save-only card-on-file, GDPR square-reference scrubbing, /terms and /privacy-policy routes, webhook fail-closed wording. Mark Email, S3/R2, Mettle/FreeAgent accounting, and user notification delivery as planned upcoming bodies of work (including new backlog item P15) so references no longer read as dead features.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 91fe5ea399
commit 457f7a452e
9 changed files with 118 additions and 79 deletions
+31 -10
View File
@@ -4,9 +4,9 @@ Nail salon booking platform — Go 1.26.5 backend + SvelteKit 5 SPA + PostgreSQL
## 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 reservation; `DELETE /api/admin/bookings/reserve` releases an admin walk-in/call-in reservation. **Background cleanup**: Centralised cron scheduler (`backend/internal/jobs/`) runs 22 maintenance jobs: reservation/deposit cleanup every 5min, hourly campaign transitions, daily unpaid-booking notifications, staged default hours auto-apply, GDPR anonymization, financial aggregation, and token/code cleanup. 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 (`closing_time.go`) resolves both current and staged default hours.
**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**: Centralised cron scheduler (`backend/internal/jobs/`) runs 23 maintenance jobs: reservation/deposit cleanup every 5min, hourly campaign transitions, daily unpaid-booking notifications, staged default hours auto-apply, GDPR anonymization, financial aggregation, and token/code cleanup. 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 (`closing_time.go`) resolves both current and staged default hours.
**Payments**: Square Terminal (in-person, via `CreateTerminalCheckout`) + online card payments via saved cards or new cards tokenized through the Square Web Payments SDK (`cnon:` nonces — gated off in local dev until `VITE_SQUARE_APPLICATION_ID`/`VITE_SQUARE_LOCATION_ID` are set). The backend accepts only tokens, never raw PANs (PCI-DSS parity, mirrored in the dev mock). 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 insert a pending payment record with VAT before calling Square — the DB transaction commits first, so Square failures leave a retryable pending record (same-key retries reuse it). Two background sweeps close Square's ~24h idempotency-key retention window: `sweep-pending-square-refunds` reconciles/retries stuck refunds (with a 23h age guard), and `sweep-stale-pending-payments` fails stale pending payments/till-sales so a late retry cannot issue a second charge.
**Payments**: Square Terminal (in-person, via `CreateTerminalCheckout`) + online card payments via saved cards or new cards tokenized through the Square Web Payments SDK (`cnon:` nonces — new-card entry falls back to `CardEntryUnavailable` only when neither mock mode nor Square credentials are configured). The backend accepts only tokens, never raw PANs (PCI-DSS parity, mirrored in the dev mock). 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 insert a pending payment record with VAT before calling Square — the DB transaction commits first, so Square failures leave a retryable pending record (same-key retries reuse it). Three background sweeps close Square's ~24h idempotency-key retention window: `sweep-pending-square-refunds` reconciles/retries stuck refunds (with a 23h age guard), `sweep-stale-pending-payments` fails stale pending payments/till-sales so a late retry cannot issue a second charge, and `sweep-stale-terminal-checkouts` cancels card-machine checkouts still pending at Square after an hour so a never-polled checkout cannot complete into an invisible, untracked charge.
**Gift Cards**: Multi-method purchase (cash, card machine, online card, giveaway). Inventory cards for stock management. 24-month rolling expiry. Idle account cleanup (2yr/5yr thresholds). Expired balance recovery with admin audit trail. Transaction audit log. Idempotency keys for purchases.
@@ -29,8 +29,8 @@ Nail salon booking platform — Go 1.26.5 backend + SvelteKit 5 SPA + PostgreSQL
## Limitations
- **Single employee** — no multi-staff scheduling, no team management
- **No email/SMS** — SMTP integration not wired; booking reminders, password resets, and notifications are UI-only
- **No production S3/R2** — prod storage stubs return "not implemented"
- **No email/SMS** — SMTP integration not wired; booking reminders, password resets, and notifications are UI-only (planned upcoming body of work)
- **No production S3/R2** — prod storage stubs return "not implemented" (planned upcoming body of work)
- **No social auth** — OAuth providers (Google, Microsoft, Facebook) not registered
- **No dark mode, no PWA, no recurring bookings, no CSV export**
- **Password reset flow exists backend-only — no frontend link**
@@ -47,18 +47,27 @@ Nail salon booking platform — Go 1.26.5 backend + SvelteKit 5 SPA + PostgreSQL
## Getting Started
Create the two env files first. The backend container reads `.env` at the repo root, and the frontend build reads `frontend/.env`:
```bash
cp .env.example .env
# Edit .env — set POSTGRES_*, JWT_SECRET_KEY
cp .env.example .env # backend + postgres + Square + S3 credentials
cp frontend/.env.example frontend/.env # frontend VITE_* vars (VITE_SQUARE_ENVIRONMENT=mock)
# Edit .env — set POSTGRES_*, JWT_SECRET_KEY, and any Square credentials for sandbox/production
docker compose up --build -d
```
`VITE_SQUARE_ENVIRONMENT=mock` (default in `frontend/.env`) makes the frontend render its built-in mock card form, pairing with the backend's `SQUARE_ENVIRONMENT=mock` for a token-only local walkthrough. Set it to `sandbox` or `production` only once real Square credentials are configured, never `mock` in a deployed build.
| Service | URL |
|---------|-----|
| Frontend | http://localhost |
| API | http://localhost/api |
| SabreDAV | http://localhost/dav |
### Square webhooks (production)
`SQUARE_WEBHOOK_NOTIFICATION_URL` and `SQUARE_WEBHOOK_SIGNATURE_KEY` in `.env` must exactly match the webhook subscription configured in the Square Dashboard. An unset URL defaults to `http://localhost:8080/webhooks/square`, which is fail-closed (503 without the signing key, 403 on missing/bad signature). If you don't need webhooks, leave both empty — the handler still rejects cleanly.
### Local dev (tmux)
```bash
@@ -76,7 +85,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,716 tests passed (4 skipped, ~13s)
cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 1,902 tests passed (4 skipped, ~2min)
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)
```
@@ -84,12 +93,14 @@ cd backend && go test -tags "test,dev" -count=10 -parallel 8 ./... # thorough v
### Pre-commit hooks
`.githooks/pre-commit` runs on every commit (configured via `git config core.hooksPath .githooks`):
- **Frontend**: `prettier --write` auto-format, then `eslint` all files
- **Backend** (only if `backend/` changed): `go vet`, `golangci-lint` (3m timeout), `staticcheck`, `gosec`, `go mod tidy` check
- **Frontend**: `eslint` all files (runs on every commit); `prettier --write` auto-format only when `frontend/` files are staged
- **Backend** (only if `backend/` files changed): `go vet` (with `test,dev` tags) and a `go mod tidy` drift check
- **Global**: `gitleaks` secret scan (skips gracefully if not installed)
To bypass: `git commit --no-verify`.
The heavier static analyzers (`golangci-lint`, `staticcheck`, `gosec`) are **not** part of the local hook — they run in CI (`.gitea/workflows/ci.yaml`): `golangci-lint` runs once without build tags, `staticcheck` and `gosec` run against both `test,dev` and `test,!dev` build tags, and CI also runs `govulncheck` (dependency vulnerabilities), alongside `go vet`, `go mod tidy`, and the gitleaks scan.
### CI caching
CI caches Go modules (`~/go/pkg/mod`) and npm dependencies (`~/.npm`, `node_modules`) via `actions/cache` — keyed on `go.sum` and `package-lock.json` respectively. Cache is served by Gitea's built-in cache server at `git.popertots.com`. First run downloads everything (~3m35s), subsequent runs restore from cache in seconds.
@@ -127,7 +138,7 @@ CREATE INDEX IF NOT EXISTS idx_terminal_checkouts_booking ON terminal_checkouts(
CREATE INDEX IF NOT EXISTS idx_terminal_checkouts_status ON terminal_checkouts(status);
```
The sweep job (`internal/jobs/cleanup.go`) and `refunds.go` cast `'refund_failed'::admin_notification_reason`, so an un-migrated DB fails at runtime — apply these before deploying the payment changes.
`backend/handlers/payments/refunds.go` casts `'refund_failed'::admin_notification_reason` (the sweep job in `internal/jobs/cleanup.go` only registers the handler), so an un-migrated DB fails at runtime — apply these before deploying the payment changes.
#### Saved-card per-user uniqueness + Square customer provisioning (P14)
@@ -144,6 +155,16 @@ ALTER TABLE user_saved_cards ADD CONSTRAINT user_saved_cards_user_id_square_card
ALTER TABLE user_saved_cards ADD COLUMN IF NOT EXISTS square_customer_id TEXT;
```
#### Saved-card Square references scrubbing (GDPR account anonymization)
Account anonymization (`anonymize_user`, `delete_guest_user`, `AnonymizeStaleGuestAccounts`) now NULLs `square_card_id` / `square_customer_id` on `user_saved_cards` so external Square references are removed on erasure (Feature Catalog §9.2). This requires `square_card_id` to be nullable — existing deployments must apply:
```sql
ALTER TABLE user_saved_cards ALTER COLUMN square_card_id DROP NOT NULL;
```
Fresh installs get the nullable column from `init-scripts/init-script.sql`; the schema is not migration-managed, so this one-liner is required for existing DBs before deploying the anonymization changes.
## Full Documentation
Detailed architecture, schema, admin workflows, user journeys, and backlog in [obsidian/Crussell/](obsidian/Crussell/).