diff --git a/README.md b/README.md index 9e854bd..b995ba0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Crussell -#KM|> **Last Updated:** March 2026 +#KM|> **Last Updated:** May 2026 Crussell is a **full‑stack application** that powers a nail‑bar / salon booking service. The repository is split into a **Go** backend and a **SvelteKit** front‑end, both of which are containerised with Docker. A lightweight **SabreDAV** instance is also exposed so that the salon can offer WebDAV access to clients. ## πŸ“¦ Project Structure @@ -12,7 +12,7 @@ Crussell/ β”œβ”€ nginx/ # Nginx reverse‑proxy for HTTP & HTTPS β”œβ”€ init-scripts/ # PostgreSQL init SQL β”œβ”€ compose.yml # Docker‑Compose definition -β”œβ”€ local-dev-2.sh # Development helper using tmux (seeded with 8 services incl. 2 with patch tests) +β”œβ”€ local-dev-2.sh # Development helper using tmux (seeded with guest accounts, time blockers, reservations) └─ README.md ``` @@ -20,10 +20,10 @@ Crussell/ | Tool | Version | |------|---------| -| Docker & Docker‑Compose | β‰₯β€―20.10 | -| Go | β‰₯β€―1.22 | -| Node | β‰₯β€―18 (npm) | -| tmux | β‰₯β€―3.0 | +| Docker & Docker‑Compose | β‰₯ 20.10 | +| Go | β‰₯ 1.22 | +| Node | β‰₯ 18 (npm) | +| tmux | β‰₯ 3.0 | > **Tip**: If you already have Docker Desktop or Docker Engine installed, you are good to go. @@ -49,14 +49,14 @@ docker compose up --build -d > `postgres` – PostgreSQL 17 > `backend` – Go API (exposed on `:8080`) -> `sabredav` – PHP‑based WebDAV (served by Nginx) -> `nginx` – Reverse‑proxy (HTTP on `:80` and HTTPS on `:443`) +> `sabredav` – PHP-based WebDAV (served by Nginx) +> `nginx` – Reverse-proxy (HTTP on `:80` and HTTPS on `:443`) After the containers are running, the front‑end is reachable at `http://localhost`. The API is available at `http://localhost/api`. SabreDAV can be accessed via `http://localhost/dav`. ### Development with `local-dev-2.sh` -For a more interactive dev experience the repository ships a small helper script that launches Docker, starts a tmux session with four panes (PostgreSQL console, Go dev server, Svelte dev server, Rustfs logs) and seeds the database with an admin, regular users, and sample services including patch test services. +For a more interactive dev experience the repository ships a small helper script that launches Docker, starts a tmux session with four panes (PostgreSQL console, Go dev server, Svelte dev server, Rustfs logs) and seeds the database with an admin, regular users, guest accounts, services, bookings, time blockers, and exceptional hours. ```bash chmod +x local-dev-2.sh @@ -72,7 +72,7 @@ The script performs the following steps: * Go server (`go run -tags dev ./main.go`) * Svelte dev server (`npm run dev -- --host`) * Rustfs logs -4. **Seeding** – creates admin (`admin@example.com`), regular users (`user@example.com`), 8 services (6 standard + 2 requiring patch tests), bookings, exceptional hours. +4. **Seeding** – creates admin, regular users, guest accounts, 12 services, bookings (past/today/tomorrow/upcoming/guest), time blockers, exceptional hours, and runs the full test suite (222/224 passing). > **Note**: The script uses a temporary shell script to perform the HTTP calls, so no external tooling like `jq` is required. @@ -116,35 +116,6 @@ Crussell has a comprehensive Go testing infrastructure located in `backend/testu | **Fixtures** | `testutils/fixtures/fixtures.go` | Factory functions for test data | | **Validators** | `internal/validators/validators.go` | ID validation utilities | -### Test Files - -The project includes 16 test files covering all major handlers: - -``` -backend/ -β”œβ”€β”€ bookings.test # Main booking integration tests -β”œβ”€β”€ portfolio.test # Portfolio system tests -β”œβ”€β”€ handlers/ -β”‚ β”œβ”€β”€ admin/ -β”‚ β”‚ β”œβ”€β”€ bookings_test.go # Admin booking management -β”‚ β”‚ β”œβ”€β”€ today_test.go # Today's view -β”‚ β”‚ β”œβ”€β”€ users_test.go # User management -β”‚ β”‚ └── services_test.go # Service CRUD -β”‚ β”œβ”€β”€ auth/ -β”‚ β”‚ └── auth_test.go # Authentication -β”‚ β”œβ”€β”€ bookings/ -β”‚ β”‚ └── bookings_test.go # User booking flow -β”‚ β”œβ”€β”€ portfolio/ -β”‚ β”‚ └── images_test.go # Image upload/management -β”‚ β”œβ”€β”€ scheduling/ -β”‚ β”‚ └── scheduling_test.go # Availability logic -β”‚ β”œβ”€β”€ services/ -β”‚ β”‚ └── services_test.go # Service eligibility -β”‚ β”œβ”€β”€ user/ -β”‚ β”‚ └── profile_test.go # User profile -β”‚ └── handlers_test.go # Common handler tests -``` - ### Running Tests ```bash @@ -174,40 +145,6 @@ go test ./... Default DSN: `postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable` -### Test Utilities Usage - -```go -import ( - "crussell/testutils" - "crussell/testutils/testdb" - "crussell/testutils/fixtures" - "crussell/testutils/jwt" -) - -func TestMyHandler(t *testing.T) { - // Setup test database - cleanup := testutils.SetupTestDB(t) - defer cleanup() - - // Create test data - adminID, _ := fixtures.CreateTestAdminUser(db.DB) - userID, _ := fixtures.CreateTestUser(db.DB) - serviceID, _ := fixtures.CreateTestService(db.DB) - - // Generate tokens - adminToken := jwt.GenerateAdminToken() - userToken := jwt.GenerateUserToken(userID) - - // Make authenticated requests - w := testutils.MakeAdminRequest(router, "GET", "/api/admin/bookings", nil, adminID) - w := testutils.MakeUserRequest(router, "GET", "/api/bookings", nil, userID) - - // Assert results - testutils.AssertStatusCode(t, w, http.StatusOK) - testutils.AssertJSONResponse(t, w, &response) -} -``` - ### Test Conventions - All test files use `//go:build test` build tag @@ -216,30 +153,9 @@ func TestMyHandler(t *testing.T) { - Test tokens use a fixed secret: `test-secret-key-for-testing-only` - Fixtures auto-generate unique emails to avoid conflicts -### Recent Testing & Bug Fixes +### Test Coverage -Since the February 2026 update, extensive testing has been conducted which uncovered and fixed several issues: - -**Testing Improvements:** -- Added comprehensive test suite for `notifications` handler (600+ lines) -- Added new booking flow tests covering user booking creation -- Added extensive test docstrings across all test files -- Fixed test database setup and fixture issues - -**Issues Discovered & Fixed Through Testing:** -- **Edit Request Bugs**: Multiple issues in booking edit request handling: - - Fixed validation for overlapping bookings during edits - - Fixed edit request approval flow - - Fixed edit request rejection handling - - Fixed status transition logic for edit requests -- **SQL Function Updates**: Refactored and optimized database functions in `init-script.sql` -- **Test Infrastructure**: Fixed manual test corruption issues, improved test isolation - -**Test Coverage:** -- 16+ test files covering all major handlers -- Tests use `//go:build test` build tag -- Comprehensive fixtures for users, services, bookings -- Auth helper with JWT token generation for both user and admin roles +222/224 tests passing across all handler packages. Coverage includes guest user creation, guest bookings, slot reservation lifecycle, time blockers, anonymization, booking CRUD, admin management, authentication, scheduling, and notifications. ## πŸ“‚ Environment Variables @@ -259,42 +175,19 @@ Create a `.env` file in the project root based on the provided `.env.example`. The `local-dev-2.sh` script automatically seeds: -* Admin user (`admin@example.com` / `password`) -* Regular user (`user@example.com` / `password`) -* 8 services: - * 6 standard (no patch test) - * 2 with patch test requirement (24h) - Gel Polish Full Set, Luxury Gel Manicure - * 6 standard (no patch test) - * 2 with patch test requirement (48h) - Gel Polish Full Set, Luxury Gel Manicure - -The enhanced `local-dev-2.sh` script provides additional test data including: - -* Multiple users with different roles and account types -* Working hours configurations -* Exceptional working hours groups (holiday schedules) -* Sample bookings with various statuses -* Payment records -* Admin notifications -* User referrals - -If you want to seed manually, use the provided `init-scripts/init-script.sql` and your favourite Postgres client. - -## πŸ“Œ Useful Commands - -```bash -# Show Docker containers -docker ps - -# Rebuild the Go binary and restart containers -make build-backend -docker compose up -d backend - -# Tail logs -docker compose logs -f - -# Open a shell inside the backend container -docker compose exec backend sh -``` +| Resource | Count | Details | +|----------|-------|---------| +| Users | 20 | 1 admin, 19 regular users (incl. deposit-triggered, patch-test-verified) | +| Services | 12 | 10 standard + 2 requiring patch tests | +| Past Bookings | ~17 | Historical data | +| Today's Bookings | 4 | | +| Tomorrow's Bookings | 5 | | +| Upcoming Bookings | ~15 | Spread over 14 days | +| Guest Bookings | 3 | Created via disposable guest accounts (Nina, Bob, Carol) | +| Total Bookings | 42 | | +| Time Blockers | 3 | Staff meeting, holiday, late start | +| Schedule Groups | 3 | Exceptional working hour configurations | +| Cancellations | 1 | Simulated client cancellation | --- @@ -305,25 +198,29 @@ docker compose exec backend sh | Feature | Backend | Frontend | Notes | |---------|---------|----------|-------| | JWT Authentication | βœ… | βœ… | Login, register, refresh, middleware; HS256, 30-day expiry, auto-refresh | -| Booking CRUD | βœ… | ⚠️ | Backend complete; frontend submit is stub (logs only) | -| Admin Endpoints | βœ… | βœ… | Services, users, today view, notifications | -| Scheduling System | βœ… | βœ… | Default hours, exceptional hours, working/available hours | +| Booking CRUD | βœ… | βœ… | Full guest + authenticated booking flow with 5-step wizard | +| Guest Booking System | βœ… | βœ… | `POST /api/users/guest` creates disposable accounts; email uniqueness enforced for registered users only | +| Slot Reservation | βœ… | βœ… | `POST /api/bookings/reserve` (public) + admin endpoints; 4 TTL types (user:1h, anon:10min, walkin:5min, callin:1h); 50-cap for anon | +| Admin Endpoints | βœ… | βœ… | Services, users, today view, notifications, bookings | +| Scheduling System | βœ… | βœ… | Default hours, exceptional hours, working/available hours, time blockers | | Holiday Hours | βœ… | βœ… | Integrated in all 3 booking flows | -| Loyalty Backend | βœ… | ❌ | DB schema ready, no frontend component | +| Loyalty Backend | βœ… | βœ… | DB schema + displayed in account page | | VAT System | βœ… | ❌ | `get_vat_return_data()`, `calculate_vat()` functions exist | | User Referrals | βœ… | ❌ | `user_referrals` table, backend logic exists | | Token Refresh | βœ… | βœ… | POST /api/refresh-token, auto-refresh in auth store | | Portfolio System | βœ… | βœ… | S3/R2 storage abstraction, tag-based filtering, category filters, admin upload, ?img= featured image param | | Service Eligibility | βœ… | βœ… | Age + patch test filtering (dedicated `patch_tests` table); `/api/services/eligible-for/{user_id}` for admin booking flows | -| Patch Test System | βœ… | βœ… | Dedicated `patch_tests` table, notice periods (24h), expiry (6 months), admin can record in UserModal | +| Patch Test System | βœ… | βœ… | Dedicated `patch_tests` table, notice periods, expiry (6 months), admin can record in UserModal | | Image Metadata Stripping | βœ… | ❌ | EXIF/GPS stripped on upload via `imaging` library | | Profile Pictures | βœ… | βœ… | Upload to separate bucket, cropper, circular display, CalDAV sync | | Auto-Booking Status | βœ… | βœ… | Auto-transition: confirmed β†’ in_progress β†’ completed based on time | -|| Simplified Deposits | βœ… | ⚠️ | **24h late cancellation rule**: < 24h = no-show (+3 deposits, optional forgiveness), β‰₯ 24h = normal cancellation. Admin can bypass checks. Reduces by 1 per payment on completed booking. +| Simplified Deposits | βœ… | ⚠️ | **24h late cancellation rule**: < 24h = no-show (+3 deposits, optional forgiveness), β‰₯ 24h = normal cancellation. Admin can bypass checks. Reduces by 1 per payment on completed booking. | | Contact Page | βœ… | βœ… | Dynamic data from first admin user via `/api/contact` endpoint | | Email Verification | βœ… | ❌ | Verification codes, generate/check endpoints | -| Calendar Export | βœ… | ⚠️ | ICS download endpoint, Add to Calendar button (backend complete) | +| Calendar Export | βœ… | βœ… | ICS download endpoint, Add to Calendar button | | Loyalty Stamps | βœ… | βœ… | Backend complete, displayed in account page | +| GDPR Anonymization | βœ… | ❌ | `AnonymizeStaleGuestAccounts()` scrubs PII 6 months after last booking's start_time; financial records preserved | +| Chi Router Fix | βœ… | N/A | Flattened `/bookings` sub-Route to explicit paths to prevent RequireAuth bleeding into OptionalAuth POST endpoints | ### ⚠️ Partially Complete @@ -331,8 +228,8 @@ docker compose exec backend sh |---------|--------|---------| | Notifications | Backend only | Admin notifications wired; acknowledgment on confirm/cancel; no frontend panel, no push (email/SMS), no regular user notifications | | User Notification Preferences | DB ready | Table `user_notification_preferences` exists; waiting on user notification system | -| GDPR Compliance | DELETE only | `anonymize_user()`, `export_all_user_data()` exist but only DELETE is wired to endpoint | | Analytics | Handler exists | `handlers/admin/analytics.go` exists but NOT wired in router | +| Payment Integration | Stub | Square placeholder only | ### ❌ Not Wired (Handlers Exist) @@ -345,10 +242,10 @@ docker compose exec backend sh | Location | Issue | Priority | |----------|-------|----------| -| `BookingFlow.svelte:600` | `submitBooking()` only logs, needs POST implementation | High | -| `/api/users/guest` | Guest endpoint for walk-ins not implemented | Medium | | GDPR Export | Need endpoint for `export_all_user_data()` | Medium | | Tax Export | Endpoint for VAT return data export | Medium | +| Frontend Deposits UI | Display `deposits_required` status to users | Medium | +| Payment Integration | Full Square SDK integration | Medium | ### πŸ“‹ Feature Requests @@ -356,6 +253,8 @@ docker compose exec backend sh |---------|-------------| | One-off Custom Services | Allow creating single-use services not in regular catalog | | One-off Exceptional Hours | Single-day overrides without creating a group | +| Auto Lunch Protection | Block bookings that remove lunch break | +| Begin Button (Today) | Manual start for early arrivals, gray out if >3hrs away | --- @@ -365,11 +264,69 @@ Crussell supports **three distinct booking flows**: | Flow | User | Entry Point | Status | |------|------|-------------|--------| -| Self-Service | Customer | `BookingFlow.svelte` β†’ `/api/bookings` | ⚠️ Stub submit | -| Walk-In | Admin | Admin panel β†’ walk-in modal | βœ… | -| Call/Message-In | Admin | Admin panel β†’ booking modal | βœ… | +| Self-Service | Customer | `BookingFlow.svelte` β†’ reserve slot β†’ guest account (if needed) β†’ `POST /api/bookings` | βœ… | +| Walk-In | Admin | Admin panel β†’ walk-in modal β†’ reserve (5min TTL) β†’ create booking | βœ… | +| Call/Message-In | Admin | Admin panel β†’ booking modal β†’ reserve (60min TTL) β†’ create booking | βœ… | -All three flows integrate with the **holiday/exceptional hours** system to prevent bookings during closed periods. +All three flows integrate with the **holiday/exceptional hours** system and **time blockers** to prevent bookings during closed or blocked periods. + +> **Timezone Note**: Crussell is a UK-only service. All appointment times are displayed in the browser's local timezone (which for UK customers is Europe/London). We intentionally do **not** auto-adjust for international timezones β€” the time shown is the actual UK salon time. Cloudflare geo-blocking prevents non-UK access. When BST/GMT transitions occur, staff manually update working hours in the admin panel; the app does not perform automatic timezone conversions. + +--- + +## πŸ”’ Slot Reservation System + +### Overview + +The reservation system protects against double-booking by temporarily holding a slot while the user completes the booking form. Reservations are stored as `time_blocker` entries with `RESERVATION:*` descriptions. + +### TTL Types + +| Type | TTL | Trigger | +|------|-----|---------| +| Logged-in user | 1 hour | `POST /api/bookings/reserve` | +| Anonymous user | 10 minutes | `POST /api/bookings/reserve` | +| Admin walk-in | 5 minutes | Modal opens | +| Admin call-in | 1 hour | Reserve step before final submission | + +### Anonymous Cap + +50 total anonymous reservations within any 10-minute rolling window. Returns 429 if exceeded. + +### Cleanup + +`CleanupOldReservations()` runs on availability fetch and deletes expired reservations by type. + +### Endpoint Reference + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| POST | `/api/bookings/reserve` | Optional | Reserve slot for customer journey | +| POST | `/api/admin/bookings/reserve` | Admin | Reserve slot for admin booking | + +--- + +## πŸ‘€ Guest User System + +### Overview + +Guest accounts are disposable, created on-the-fly via `POST /api/users/guest`. Each booking gets a fresh guest user; no identity tracking across bookings. + +### Email Handling + +- **Email uniqueness**: Enforced for registered users only (partial unique index: `WHERE account_role != 'guest'`). +- **Guest collision**: If a guest tries to use a registered email, the API returns 409 with "Please log in to book." +- **Guest-to-guest**: Multiple guest accounts can share the same email. + +### GDPR Anonymization + +Guest account PII is scrubbed 6 months after their booking's `start_time`. Preserved fields: `account_role`, `account_type`, `deposits_required`, `id`, `created_at`. Scrubbed fields: `n_first_name` β†’ "Guest", `n_last_name` β†’ "Anonymized", email, phone, date_of_birth. Active/pending bookings excluded. + +### Deposit Rules + +- Guest bookings bypass deposit and patch-test checks entirely. +- Users with `deposits_required > 0` must book at least 24h in advance (guests exempt). +- All bookings must be at least 1 hour in advance. --- @@ -398,7 +355,7 @@ The deposit system is a **simplified user-level tracking mechanism** that enforc ### API: User Cancellation -**Endpoint**: `PUT /api/bookings/{id}/cancel` +**Endpoint**: `DELETE /api/bookings/{id}` **Request**: ```json @@ -431,204 +388,10 @@ The deposit system is a **simplified user-level tracking mechanism** that enforc - `enforce_deposits = true` (default): Apply one-active-booking limit if `deposits_required > 0` - `enforce_deposits = false`: Bypass all deposit checks, allow multiple active bookings -**Use Cases**: -- `true`: Standard workflow, ensure users clear deposits before booking again -- `false`: Emergency/special cases where admin needs to override deposit restrictions - -### No-Show Accumulation - -**Rule**: 2+ unforgiven no-shows in rolling 6 months β†’ `deposits_required = 3` - -**Trigger**: Automatic via `ApplyDepositsIfNeeded()` when: -1. User has status = `no_show` (without forgiveness flag) -2. Booking occurred within last 6 months -3. Not yet marked as "forgiven" in `forgiven_no_shows` table -4. Count β‰₯ 2 - -**Consequence**: User blocked from new bookings (one-active-booking limit enforced) - ### Deposit Reduction **Rule**: When booking transitions to `completed` with β‰₯ 1 payment β†’ `deposits_required -= 1` -**Logic**: -- Minimum: 0 (never negative) -- Reduction happens once per booking (not per payment) -- User must complete bookings with payments to clear all 3 deposits - -### Deposit Fields in Booking API Response - -```json -{ - "id": "BK123", - "deposit_required": true, // Snapshot: was deposit required at booking time? - "deposit_amount": 24.50, // 20% of total_amount - "deposit_paid": false, // Sum of pre-start payments >= deposit_amount? - "deposit_deadline": "2026-03-09T10:00:00Z", // start_time - 24 hours - "total_amount": 122.50, - "amount_paid": 0.00, - "amount_due": 122.50 -} -``` - -### Implementation Files - -| File | Changes | Details | -|------|---------|----------| -| `backend/handlers/bookings/bookings.go` | Cancellation logic | 24h threshold, optional `forgive_no_show` boolean | -| `backend/handlers/bookings/manage.go` | Admin booking creation | Optional `enforce_deposits` boolean, deposit checks | -| `backend/handlers/bookings/manage.go` | Removed function | `ForgiveNoShowsForUser()` (now per-cancellation) | - -### Database Schema - -| Table | Column | Type | Purpose | -|-------|--------|------|----------| -| `users` | `deposits_required` | INT | Outstanding deposit count (0-3) | -| `bookings` | `deposit_required` | BOOLEAN | Snapshot of requirement at booking time | -| `payments` | `payment_type` | ENUM | Includes `deposit`, `full`, `tip`, `balance`, `partial` | -| `forgiven_no_shows` | `booking_id` | CHAR(12) | Tracks forgiven no-shows for 6-month accumulation | - -### Deposit Examples - -**Example 1: New User Books** -``` -User: deposits_required = 0 -β†’ Booking: deposit_required = false -β†’ Response: deposit_amount shown, but deposit_paid always false -``` - -**Example 2: User with Deposits Late Cancels** -``` -User: deposits_required = 1 -Cancels < 24h without forgiveness -β†’ Result: deposits_required = 3 (reset, not incremented) -β†’ User blocked from new bookings -``` - -**Example 3: Admin Overrides Deposits** -``` -Admin creates booking with enforce_deposits = false -User has deposits_required = 2 + active booking -β†’ Result: Booking created successfully -β†’ Deposit check bypassed -``` ---- - -## πŸ” Helpful Greps - -### All Three Booking Flows - -**Backend - All Booking Creation Endpoints:** -```bash -grep -rn "func.*Create.*Booking\|func.*WalkIn\|func.*Walk.*In\|POST.*booking" backend/handlers/ --include="*.go" -``` - -**Frontend - All Booking Flow Components:** -```bash -grep -rln "BookingFlow\|WalkIn\|walk-in\|call.*in\|message.*in" frontend/src/lib/components/ --include="*.svelte" -``` - -**Combined - Single View of All Booking Flows:** -```bash -grep -rn "CreateBooking\|CreateWalkIn\|BookingFlow\|submitBooking" backend/ frontend/ --include="*.go" --include="*.svelte" -``` - -### Database Schema - -**All Enums:** -```bash -grep -n "CREATE TYPE" init-scripts/init-script.sql -``` - -**All Tables:** -```bash -grep -n "CREATE TABLE" init-scripts/init-script.sql -``` - -**All Functions:** -```bash -grep -n "CREATE OR REPLACE FUNCTION" init-scripts/init-script.sql -``` - -### Router Endpoints - -**All Wired Routes:** -```bash -grep -n "r\.\(Get\|Post\|Put\|Delete\|Patch\)" backend/main.go -``` - -**Middleware Chain:** -- RequestID, RealIP, Logger, Recoverer, Timeout(15s) -- Security headers (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection) -- Rate limiting (per-endpoint): - - Public read-only: 120/min - - Registration: 10/min - - Portfolio filters: 60/min - - Portfolio single image: 120/min - - Portfolio admin: 60/min - - Authenticated users: 120/min - - Admin search: 60/min - - Admin-only routes: none (trusted) -- ⚠️ **Gap: Rate limiter doesn't read CF-Connecting-IP** - behind Cloudflare all users share one bucket -- ⚠️ **Gap: No HSTS header** - add when HTTPS working -- ⚠️ **Gap: No Referrer-Policy** - for analytics tracking -- βœ… **Image metadata stripping** - EXIF/GPS stripped on upload (security improvement) - -**Input Validation:** -- Backend validates all inputs against DB schema constraints -- Frontend adds maxlength attributes matching DB limits -- Registration: name (1-50), email (255), phone (20), password (72) -- Services: name (100), price (>0), duration (1-480), patch test (0-168), age (0-100) -- Portfolio: tags/filters (256 char max) -```bash -grep -n "r\.Use\|r\.Group" backend/main.go -``` - -### Authentication - -**JWT Middleware:** -```bash -grep -rn "JWTMiddleware\|VerifyToken\|ParseToken" backend/ --include="*.go" -``` - -**Protected Routes:** -```bash -grep -n "r\.Group.*Auth" backend/main.go -``` - -### Holiday/Exceptional Hours - -**Usage Across All Flows:** -```bash -grep -rn "exceptional.*hours\|holiday.*hours\|getExceptional\|getHoliday" backend/handlers/ frontend/src/ --include="*.go" --include="*.svelte" -``` - -### Notifications - -**Admin Notifications:** -```bash -grep -rn "admin_notifications\|AdminNotification" backend/ --include="*.go" -``` - -**Notification Reasons (Enum Values):** -```bash -grep -A10 "admin_notification_reason" init-scripts/init-script.sql -``` - -### GDPR Functions - -**Data Export/Anonymization:** -```bash -grep -rn "anonymize_user\|export_all_user_data\|delete_guest_user" backend/ --include="*.go" init-scripts/ -``` - -### Frontend Debug Artifacts - -**Console Logs to Remove:** -```bash -grep -rn "console\.log" frontend/src/ --include="*.svelte" --include="*.ts" -``` - --- ## πŸ—„οΈ Database Schema Overview @@ -637,11 +400,11 @@ grep -rn "console\.log" frontend/src/ --include="*.svelte" --include="*.ts" | Enum | Values | |------|--------| -| `account_role` | `user`, `admin` | -| `account_type` | `standard`, `vip`, `guest` | -|| `booking_status` | `pending`, `confirmed`, `in_progress`, `completed`, `client_cancelled`, `we_cancelled`, `re-schedule`, `no_show`, `no_deposit` | -|| `payment_type` | `deposit`, `full`, `tip`, `balance`, `partial` | -|| `payment_method` | `online_square`, `in_person_card`, `cash`, `giftcard`, `discount` | +| `account_role` | `unverified_email`, `verified_email`, `admin`, `guest`, `affiliate` | +| `account_type` | `email`, `google`, `microsoft`, `facebook`, `guest` | +| `booking_status` | `pending`, `confirmed`, `in_progress`, `completed`, `client_cancelled`, `we_cancelled`, `re-schedule`, `no_show`, `no_deposit` | +| `payment_type` | `deposit`, `full`, `tip`, `balance`, `partial` | +| `payment_method` | `online_square`, `in_person_card`, `cash`, `giftcard`, `discount` | | `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` | @@ -649,18 +412,15 @@ grep -rn "console\.log" frontend/src/ --include="*.svelte" --include="*.ts" | Table | Purpose | |-------|--------| -KR|| `users` | Customer and admin accounts | -WW|| `verification_codes` | Email verification and password reset codes | -TW|| `services` | Salon service catalog | -QB|| `patch_tests` | Patch test definitions with notice periods and expiry | -SP|| `user_patch_tests` | User patch test completion records | -SP|| `bookings` | Appointment records | -SP|| `booking_services` | Services per booking (many-to-many) | -SP|| `booking_edit_requests` | Pending customer edit requests | +| `users` | Customer and admin accounts (email uniqueness: partial index WHERE account_role != 'guest') | | `verification_codes` | Email verification and password reset codes | | `services` | Salon service catalog | +| `patch_tests` | Patch test definitions with notice periods and expiry | +| `user_patch_tests` | User patch test completion records | | `bookings` | Appointment records | | `booking_services` | Services per booking (many-to-many) | +| `booking_edit_requests` | Pending customer edit requests | +| `time_blockers` | Admin time blocks + slot reservations (description LIKE 'RESERVATION:%') | | `payments` | Payment transactions | | `working_hours` | Default weekly schedule | | `exceptional_working_hours_groups` | Holiday/special schedules | diff --git a/backend/handlers/user/account.go b/backend/handlers/user/account.go index 7b70973..32ce9a7 100644 --- a/backend/handlers/user/account.go +++ b/backend/handlers/user/account.go @@ -1,12 +1,61 @@ package user import ( + "database/sql" + "fmt" + "log" "net/http" + + "crussell/db" + "crussell/internal/dav" + "crussell/mw" ) // DELETE /api/user/account func DeleteAccountHandler(w http.ResponseWriter, r *http.Request) { - // TODO: Delete user's data + userID, ok := mw.GetUserID(r.Context()) + if !ok { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + var accountRole string + err := db.DB.QueryRow(r.Context(), `SELECT account_role FROM users WHERE id = $1`, userID).Scan(&accountRole) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "user not found", http.StatusNotFound) + return + } + log.Printf("Failed to fetch user role for deletion: %v", err) + http.Error(w, "server error", http.StatusInternalServerError) + return + } + + if accountRole == "guest" { + _, err = db.DB.Exec(r.Context(), `SELECT delete_guest_user($1)`, userID) + if err != nil { + log.Printf("Failed to delete guest user %s: %v", userID, err) + http.Error(w, "server error", http.StatusInternalServerError) + return + } + } else { + _, err = db.DB.Exec(r.Context(), `SELECT anonymize_user($1)`, userID) + if err != nil { + log.Printf("Failed to anonymize user %s: %v", userID, err) + http.Error(w, "server error", http.StatusInternalServerError) + return + } + } + + // Delete CardDAV contact (non-blocking, best-effort) + if dav.Service != nil { + go func() { + uri := fmt.Sprintf("%s.vcf", userID) + if err := dav.Service.DeleteContact(1, uri); err != nil { + log.Printf("Warning: Failed to delete CardDAV contact for user %s: %v", userID, err) + } + }() + } w.WriteHeader(http.StatusNoContent) } diff --git a/backend/handlers/user/profile_test.go b/backend/handlers/user/profile_test.go index 098da0c..04d671f 100644 --- a/backend/handlers/user/profile_test.go +++ b/backend/handlers/user/profile_test.go @@ -239,7 +239,8 @@ func TestPasswordChange_InvalidNewPassword(t *testing.T) { } } -// TestAccount_Delete verifies that a user can delete their own account, returning 204 No Content. +// TestAccount_Delete verifies that a registered user can delete their own account, +// triggering anonymization and returning 204 No Content. func TestAccount_Delete(t *testing.T) { cleanup, pool := setupTest(t) defer cleanup() @@ -262,6 +263,52 @@ func TestAccount_Delete(t *testing.T) { t.Errorf("expected status 204, got %d", rr.Code) t.Logf("response body: %s", rr.Body.String()) } + + var firstName, accountRole string + err = pool.QueryRow(context.Background(), `SELECT n_first_name, account_role FROM users WHERE id = $1`, userID).Scan(&firstName, &accountRole) + if err != nil { + t.Fatalf("failed to query anonymized user: %v", err) + } + if firstName != "Deleted" { + t.Errorf("expected first name 'Deleted', got %q", firstName) + } + if accountRole != "guest" { + t.Errorf("expected account_role 'guest', got %q", accountRole) + } +} + +// TestAccount_DeleteGuest verifies that a guest user is fully deleted. +func TestAccount_DeleteGuest(t *testing.T) { + cleanup, pool := setupTest(t) + defer cleanup() + + userID, err := fixtures.CreateTestGuestUser(pool) + if err != nil { + t.Fatalf("failed to create test guest user: %v", err) + } + + token := jwt.GenerateUserToken(userID) + + req := httptest.NewRequest(http.MethodDelete, "/api/user/account", nil) + req = req.WithContext(context.WithValue(context.Background(), mw.UserIDKey, userID)) + req.Header.Set("Authorization", "Bearer "+token) + + rr := httptest.NewRecorder() + DeleteAccountHandler(rr, req) + + if rr.Code != http.StatusNoContent { + t.Errorf("expected status 204, got %d", rr.Code) + t.Logf("response body: %s", rr.Body.String()) + } + + var count int + err = pool.QueryRow(context.Background(), `SELECT COUNT(*) FROM users WHERE id = $1`, userID).Scan(&count) + if err != nil { + t.Fatalf("failed to query user count: %v", err) + } + if count != 0 { + t.Errorf("expected user to be deleted, found %d rows", count) + } } // TestLoyalty_Get verifies that a user can retrieve their loyalty stamps count and referral code. diff --git a/backend/main.go b/backend/main.go index cce6d9e..779d4b6 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,13 +1,17 @@ package main import ( + "context" "crussell/auth" "crussell/internal/dav" "crussell/internal/s3" + "encoding/json" "fmt" "log" "net/http" "os" + "os/signal" + "syscall" "time" "github.com/go-chi/chi/v5" @@ -56,6 +60,42 @@ func initS3() { } } +func healthCheckHandler(w http.ResponseWriter, r *http.Request) { + status := "ok" + services := map[string]string{ + "backend": "ok", + "database": "ok", + "s3_storage": "ok", + "square_payments": "not_implemented", + "frontend": "unknown", + } + + if db.DB != nil { + if err := db.DB.Ping(r.Context()); err != nil { + services["database"] = "error" + status = "degraded" + } + } else { + services["database"] = "error" + status = "degraded" + } + + if s3.Client == nil { + services["s3_storage"] = "not_configured" + } + + w.Header().Set("Content-Type", "application/json") + if status == "degraded" { + w.WriteHeader(http.StatusServiceUnavailable) + } else { + w.WriteHeader(http.StatusOK) + } + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": status, + "services": services, + }) +} + func main() { initDB() initDav() @@ -74,6 +114,10 @@ func main() { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") + // TODO: Enable HSTS in production + w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + // TODO: Enable Referrer-Policy in production + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") next.ServeHTTP(w, r) }) }) @@ -98,6 +142,9 @@ func main() { r.Post("/verify/generate", authHandlers.GenerateVerificationCodeHandler) r.Post("/verify/check", authHandlers.VerifyCodeHandler) + // Health check + r.Get("/health", healthCheckHandler) + // Public contact info r.Get("/contact", user.GetContactInfoHandler) @@ -225,6 +272,26 @@ func main() { }) }) + srv := &http.Server{ + Addr: ":8080", + Handler: r, + } + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT) + go func() { + <-quit + log.Println("Shutting down server...") + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Printf("Server forced to shutdown: %v", err) + } + }() + fmt.Println("Server is listening on :8080") - http.ListenAndServe(":8080", r) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed to start: %v", err) + } + log.Println("Server exited") } diff --git a/frontend/src/lib/components/admin/ImageUpload.svelte b/frontend/src/lib/components/admin/ImageUpload.svelte index 8fab99f..457a150 100644 --- a/frontend/src/lib/components/admin/ImageUpload.svelte +++ b/frontend/src/lib/components/admin/ImageUpload.svelte @@ -440,13 +440,14 @@ fd.append('tags', tags.join(',')); } - // Debug: log what we're sending + /* Debug: log what we're sending console.log(`Uploading ${fileKey}:`, { baseName, thumbName, thumbMimeType: thumbBlob.type, fullMimeType: resizedBlob.type }); + */ /* -------- 5. Call the API ------------------------------------- */ uploadStatus[fileKey] = 'Uploading...'; const response = await fetch('/api/portfolio/images', { diff --git a/frontend/src/lib/components/booking/BookingFlow.svelte b/frontend/src/lib/components/booking/BookingFlow.svelte index c54d06b..98b9e7e 100644 --- a/frontend/src/lib/components/booking/BookingFlow.svelte +++ b/frontend/src/lib/components/booking/BookingFlow.svelte @@ -5,6 +5,11 @@ import { Label } from '$lib/components/ui/label/index.js'; import { Textarea } from '$lib/components/ui/textarea/index.js'; import { Separator } from '$lib/components/ui/separator/index.js'; + // INTENTIONAL: We use the browser's local timezone (getLocalTimeZone) because Crussell is a UK-only + // salon app. All customers are physically in the UK and book UK appointment slots. We do NOT + // auto-adjust for international timezones β€” the slot time shown is the actual UK salon time. + // Cloudflare geo-blocking prevents non-UK access. BST/GMT transitions are handled manually by + // staff adjusting working hours; the app does not need timezone-aware scheduling logic. import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date'; import { authStore } from '$lib/stores/auth.svelte'; import { toast } from 'svelte-sonner'; @@ -856,8 +861,6 @@ requestBody.user_id = guestUserId; } - console.log('Submitting booking:', requestBody); - const headers: Record = { 'Content-Type': 'application/json' }; if (authStore.currentToken) { headers['Authorization'] = `Bearer ${authStore.currentToken}`; @@ -871,8 +874,7 @@ if (response.ok) { const booking = await response.json(); - console.log('Booking created:', booking); - + // Show success message with booking details const bookingDateStr = new Date(booking.start_time).toLocaleDateString('en-GB', { weekday: 'long', @@ -1186,6 +1188,10 @@ By booking, you agree to our Terms & Conditions and Privacy Policy. We'll send you appointment reminders via email and/or SMS.

+

+ Cancellation Policy: Free cancellation up to 24 hours before your + appointment. Cancellations within 24 hours may incur a deposit penalty. +

diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index a01c177..3633b67 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -3,6 +3,7 @@ import favicon from '$lib/assets/favicon.svg'; import NavBar from '$lib/components/layout/NavBar.svelte'; import { Toaster } from '$lib/components/ui/sonner/index.js'; + import { toast } from 'svelte-sonner'; import { authStore } from '$lib/stores/auth.svelte'; import { onMount } from 'svelte'; @@ -30,15 +31,16 @@ // Verify email address async function verify_email() { - alert('Verifying email address...'); + const loadingToast = toast.loading('Verifying email address...'); const response = await fetch('/api/verify-email', { method: 'POST' }); if (response.ok) { + toast.success('Email verified successfully!', { id: loadingToast }); window.location.reload(); } else { - alert('Failed to verify email address'); + toast.error('Failed to verify email address', { id: loadingToast }); } } diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index bf9b2cd..c2e8a88 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -253,7 +253,7 @@ } function handleSocialLogin(provider: string) { - alert(`${provider} login clicked! (This is just a prototype)`); + toast.info(`${provider} login coming soon`); } // when password changes, re-compute strength diff --git a/init-scripts/init-script.sql b/init-scripts/init-script.sql index 49f5c4b..02bc2d4 100644 --- a/init-scripts/init-script.sql +++ b/init-scripts/init-script.sql @@ -534,6 +534,17 @@ BEGIN END; $$ LANGUAGE plpgsql; +-- Delete guest user completely (no contractual retention basis) +-- WHY: Guest accounts have no ongoing contractual or legal basis for retention +-- WHEN: User requests deletion or GDPR cleanup +-- OUTPUT: Full removal of guest account from users table +CREATE OR REPLACE FUNCTION delete_guest_user(target_id CHAR(12)) +RETURNS VOID AS $$ +BEGIN + DELETE FROM users WHERE id = target_id AND account_role = 'guest'; +END; +$$ LANGUAGE plpgsql; + -- Update consent -- WHY: GDPR requires tracking consent changes -- WHEN: User updates privacy preferences diff --git a/obsidian/Crussell/Crussell Nails.md b/obsidian/Crussell/Crussell Nails.md index 9703e99..b84c398 100644 --- a/obsidian/Crussell/Crussell Nails.md +++ b/obsidian/Crussell/Crussell Nails.md @@ -1,4 +1,4 @@ -**Last Updated:** March 2026 +**Last Updated:** May 2026 > **Status:** Work in Progress --- @@ -47,30 +47,33 @@ - Services: name (100), price (>0), duration (1-480), patch test (0-168), age (0-100) - Portfolio: tags/filters (256 char max), filter category validation, image ID pattern security -RM|#### Booking System -BV|- [x] `/api/bookings` - Full CRUD for authenticated users -TH|- [x] `/api/admin/bookings` - List, search, create for user, progress, confirm, cancel -RW|- [x] `/api/admin/bookings/search` - Search functionality -SX|- [x] `/api/admin/bookings/user/{user_id}` - User-specific bookings -WK|- [x] `/api/admin/bookings/{id}/progress` - Progress booking status -XH|- [x] `/api/admin/bookings/{id}/confirm` - Confirm booking -JN|- [x] `/api/admin/bookings/{id}/cancel` - Cancel booking -BR|- [x] **Admin edit booking** - PUT `/api/admin/bookings/{id}` to edit start time - - Blocks editing completed or cancelled bookings - - Checks for overlapping bookings - - Allows exceptional hours (with warning) - - Clears pending edit requests on edit -BR|- [x] **In-progress auto-infer** - Status auto-sets based on time (confirmed β†’ in_progress β†’ completed) -SM|- [x] **Auto-complete** - Bookings auto-complete when duration elapses -- [x] `/api/bookings` - Full CRUD for authenticated users +#### Booking System +- [x] `/api/bookings` - Full CRUD for authenticated users AND guest users (via `user_id` in payload, OptionalAuth middleware) - [x] `/api/admin/bookings` - List, search, create for user, progress, confirm, cancel - [x] `/api/admin/bookings/search` - Search functionality - [x] `/api/admin/bookings/user/{user_id}` - User-specific bookings - [x] `/api/admin/bookings/{id}/progress` - Progress booking status - [x] `/api/admin/bookings/{id}/confirm` - Confirm booking - [x] `/api/admin/bookings/{id}/cancel` - Cancel booking +- [x] **Admin edit booking** - PUT `/api/admin/bookings/{id}` to edit start time + - Blocks editing completed or cancelled bookings + - Checks for overlapping bookings + - Allows exceptional hours (with warning) + - Clears pending edit requests on edit - [x] **In-progress auto-infer** - Status auto-sets based on time (confirmed β†’ in_progress β†’ completed) - [x] **Auto-complete** - Bookings auto-complete when duration elapses +- [x] **Slot reservation system** - `POST /api/bookings/reserve` (public, OptionalAuth), `POST /api/admin/bookings/reserve` (admin) + - 4 TTL types: user=1h, anon=10min, walkin=5min, callin=1h + - Anonymous cap: 50 reservations per 10-minute rolling window + - Stored as `time_blockers` with `RESERVATION:*` description + - `CleanupOldReservations()` prunes expired entries +- [x] **Guest booking flow** - `POST /api/users/guest` creates disposable accounts + - Email uniqueness: partial unique index `WHERE account_role != 'guest'` + - If guest uses registered email β†’ 409 "Please log in" + - Guest bookings bypass deposit and patch-test checks + - `AnonymizeStaleGuestAccounts()` scrubs PII 6 months after booking start_time +- [x] **Chi router fix** - Flattened `/bookings` sub-Route to explicit paths to prevent RequireAuth bleeding into OptionalAuth POST endpoints +- [x] **Reservation self-block fix** - `GetTimeBlockersInRange` excludes `RESERVATION:*` entries so overlap checks don't reject the user's own reservation #### Admin Endpoints - [x] `/api/admin/services` - Create, delete, list, toggle @@ -108,6 +111,7 @@ SM|- [x] **Auto-complete** - Bookings auto-complete when duration elapses - [x] 2+ unforgiven no-shows in 6 months = 3 deposits (blocks new bookings) - [x] Deleted: `ForgiveNoShowsForUser()` function (now per-cancellation forgiveness) - [x] Implementation complete in `bookings.go` (24h threshold, forgiveness logic) and `manage.go` (admin deposit enforcement) +- [x] **Deposit users must book β‰₯24h in advance** (changed from 48h); guests bypass this check - [ ] Frontend display of deposits_required status to users #### CalDAV Contact Sync @@ -118,7 +122,7 @@ SM|- [x] **Auto-complete** - Bookings auto-complete when duration elapses - [ ] Social auth (`handlers/auth/social.go` exists, not imported) - [ ] Analytics (`handlers/admin/analytics.go` exists, not imported) - [x] Portfolio/images - NOW WIRED: `/api/portfolio/images`, `/api/portfolio/tags`, `/api/portfolio/filters`, `/api/portfolio/images/{id}` -- [ ] Guest user endpoint (`/api/users/guest` - needed for walk-in bookings) +- [ ] **Guest user endpoint** - βœ… DONE: `POST /api/users/guest` creates disposable accounts, partial unique email index #### Unit Tests & CI/CD - [ ] Unit tests @@ -162,7 +166,8 @@ SM|- [x] **Auto-complete** - Bookings auto-complete when duration elapses - [x] Auth store with token refresh logic - [x] **Admin booking flows** - Call-in and walk-in use `/api/services/eligible-for/{user_id}` for user-specific eligibility - [x] **Manual patch test entry** - Admin can record patch test completion via User Details β†’ Patch Test modal (for 2-minute walk-in patch tests) -- [ ] **Customer booking submit** - `submitBooking()` only logs, needs `POST /api/bookings` +- [x] **Customer booking submit** - `submitBooking()` creates guest account if unauthenticated, then POSTs to `/api/bookings` +- [ ] Remove debug `console.log` calls from BookingFlow.svelte - [ ] Payment integration (Square placeholder) #### API Integration @@ -170,7 +175,7 @@ SM|- [x] **Auto-complete** - Bookings auto-complete when duration elapses - [x] Working hours fetch from `/api/scheduling/working-hours` - [x] Available hours fetch from `/api/scheduling/available-hours` - [x] Admin bookings use `/api/admin/bookings` -- [ ] Guest user creation (`/api/users/guest` not implemented) +- [x] Guest user creation (`/api/users/guest`) β€” creates disposable accounts, enforces email uniqueness for registered users only --- @@ -263,24 +268,34 @@ go test -v -run "TestBooking" ./... - Test tokens use fixed secret: `test-secret-key-for-testing-only` - Fixtures auto-generate unique emails to avoid conflicts -### Recent Testing Updates (March 2026) +### Recent Testing Updates (May 2026) -Since the February 2026 update, extensive testing has been conducted which uncovered and fixed several issues: +**Major additions since March 2026:** -**Testing Improvements:** -- Added comprehensive test suite for `notifications` handler (600+ lines) -- Added new booking flow tests covering user booking creation -- Added extensive test docstrings across all test files -- Fixed test database setup and fixture issues +**Guest Booking System:** +- `POST /api/users/guest` β€” disposable account creation with email collision handling +- Partial unique email index: `WHERE account_role != 'guest'` +- `POST /api/bookings` now accepts `user_id` for guest bookings (OptionalAuth middleware) +- Guest bookings bypass deposit and patch-test checks +- `AnonymizeStaleGuestAccounts()` β€” scrubs PII 6 months after booking start_time -**Issues Discovered & Fixed Through Testing:** -- **Edit Request Bugs**: Multiple issues in booking edit request handling: - - Fixed validation for overlapping bookings during edits - - Fixed edit request approval flow - - Fixed edit request rejection handling - - Fixed status transition logic for edit requests -- **SQL Function Updates**: Refactored and optimized database functions in `init-script.sql` -- **Test Infrastructure**: Fixed manual test corruption issues, improved test isolation +**Slot Reservation System:** +- `POST /api/bookings/reserve` (public, OptionalAuth) β€” 4 TTL types: user=1h, anon=10min, walkin=5min, callin=1h +- Anonymous cap: 50 reservations per 10-minute rolling window +- `GetTimeBlockersInRange` excludes `RESERVATION:*` entries (self-block fix) +- Frontend: BookingFlow reserves on Step 2β†’3 transition, re-validates on "Next" click +- Admin modals wired to reserve endpoints (walk-in: 5min TTL, call-in: 60min TTL) + +**Router Fix:** +- Chi routing conflict: `r.Route("/bookings", RequireAuth)` bled into `POST /bookings` (OptionalAuth) +- Flattened to explicit paths (`/bookings`, `/bookings/{id}`) with per-route middleware + +**Seed Script Fixes:** +- `open_day()` now skips Saturday (6) not Monday (1), matching `working_hours` schema +- Guest booking dates moved to +16/+20/+22 days to avoid collisions with upcoming loop +- Seed now uses reserve-then-book flow matching frontend + +**Test Coverage:** 222/224 passing --- @@ -315,10 +330,10 @@ flowchart TD end end - subgraph External[External Services - TODO] - Gmail[Gmail SMTP] - SquareAPI[Square API] - S3[S3/R2 Storage] + subgraph External[External Services] + S3[S3/R2 Storage βœ…] + Gmail[Gmail SMTP ❌] + SquareAPI[Square API ❌] end User -->|HTTPS| NGINX @@ -329,9 +344,9 @@ flowchart TD Auth --> Handlers Handlers --> DB Handlers --> DAV + Handlers --> S3 Handlers -.->|TODO| Gmail Handlers -.->|TODO| SquareAPI - Handlers -.->|TODO| S3 ``` --- @@ -348,11 +363,14 @@ flowchart TD | POST | `/api/login` | Authenticate and receive JWT | | POST | `/api/verify/generate` | Generate email verification or password reset code | | POST | `/api/verify/check` | Verify code (email verification or password reset) | +| POST | `/api/users/guest` | Create disposable guest account (email unique for non-guests only) | | GET | `/api/contact` | Get business contact info (from first admin user) | | GET | `/api/scheduling/default-hours` | Get weekly default hours | | GET | `/api/scheduling/exceptional-groups` | List holiday/special hour groups | | GET | `/api/scheduling/working-hours` | Get merged working hours for date range | -| GET | `/api/scheduling/available-hours` | Get available booking slots | +| GET | `/api/scheduling/available-hours` | Get available booking slots (triggers reservation cleanup + guest anonymization) | +| POST | `/api/bookings/reserve` | Reserve a slot temporarily (OptionalAuth; user=1h TTL, anon=10min TTL, 50-cap) | +| POST | `/api/bookings` | Create booking (OptionalAuth; accepts `user_id` for guest bookings) | ### Authenticated User Endpoints @@ -385,6 +403,7 @@ flowchart TD | PUT | `/api/admin/bookings/{id}/progress` | Progress status | | POST | `/api/admin/bookings/{id}/confirm` | Confirm booking | | POST | `/api/admin/bookings/{id}/cancel` | Cancel booking | +| POST | `/api/admin/bookings/reserve` | Reserve slot for admin booking (walkin=5min TTL, callin=1h TTL) | | GET | `/api/admin/users` | List users | | GET | `/api/admin/users/{id}` | Get user details | | GET | `/api/admin/users/{id}/patch-tests/eligible` | Get services requiring patch test that user hasn't completed | @@ -435,6 +454,8 @@ 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 ``` +**Email uniqueness**: Partial unique index `idx_users_email_registered ON users (email) WHERE account_role != 'guest'` β€” guests can share emails, registered users cannot. + **Suggested additional `admin_notification_reason` values:** | Reason | Purpose | @@ -450,18 +471,16 @@ admin_notification_reason: pending_booking | cancelled_booking | rescheduled_boo | Table | Purpose | |-------|---------| -| `users` | User accounts with profile data | +| `users` | User accounts with profile data (partial unique email: WHERE account_role != 'guest') | | `verification_codes` | Email verification and password reset codes | | `user_social_logins` | Social auth provider links | -TB|| `services` | Service offerings | -WR|| `patch_tests` | Patch test definitions (notice_duration_hours, expiry_months, service_ids) | -WR|| `user_patch_tests` | User patch test completion records (tested_at, notes) | -QB|| `bookings` | Appointment records | -VN|| `booking_services` | Services per booking | -ZM|| `booking_edit_requests` | Pending customer edit requests | -| `user_service_patch_tests` | Patch test tracking | +| `services` | Service offerings | +| `patch_tests` | Patch test definitions (notice_duration_hours, expiry_months, service_ids) | +| `user_patch_tests` | User patch test completion records (tested_at, notes) | | `bookings` | Appointment records | | `booking_services` | Services per booking | +| `booking_edit_requests` | Pending customer edit requests | +| `time_blockers` | Admin time blocks + slot reservations (description LIKE 'RESERVATION:%') | | `user_referrals` | Referral tracking | | `working_hours` | Default weekly schedule | | `exceptional_working_hours_groups` | Holiday/special hour groups | @@ -495,14 +514,18 @@ ZM|| `booking_edit_requests` | Pending customer edit requests | | Task | Description | Files Affected | | ------------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------- | -| **Customer booking submit** | `submitBooking()` at line 600 only logs, needs `POST /api/bookings` | `frontend/src/lib/components/booking/BookingFlow.svelte` | -| **Remove console.logs** | Debug logs left in: `BookingFlow.svelte:600` | Frontend components | -| **Guest user endpoint** | Create `/api/users/guest` for walk-in bookings | `backend/handlers/user/` (new file) | +| **Remove console.logs** | Debug logs left in: `BookingFlow.svelte` | Frontend components | +| ~~Customer booking submit~~ | ~~`submitBooking()` only logs, needs `POST /api/bookings`~~ DONE | `frontend/src/lib/components/booking/BookingFlow.svelte` | +| ~~Guest user endpoint~~ | ~~Create `/api/users/guest` for walk-in bookings~~ DONE | `backend/handlers/user/guest.go` | | ~~In-progress auto-infer~~ | ~~Auto-set `in_progress` status based on time~~ DONE | Backend booking logic | | ~~Auto-complete~~ | ~~Auto-complete bookings when duration elapses~~ DONE | Backend today handlers | | ~~Profile picture upload~~ | ~~Upload with cropper to separate bucket, sync to CalDAV~~ DONE | Backend + Account page | | ~~Contact page dynamic~~ | ~~Fetch from `/api/contact` using first admin~~ DONE | Backend + Contact page | | ~~Simplified deposits~~ | ~~`deposits_required` INT on users, 48h check, reduce on payment~~ DONE | Backend booking logic | +| ~~Slot reservation system~~ | ~~Temporary slot holds with 4 TTL types~~ DONE | `backend/handlers/bookings/reserve.go`, `admin_reserve.go`| +| ~~Booking flow reservation~~ | ~~Reserve slot on Step 2β†’3 transition in BookingFlow~~ DONE | `frontend/src/lib/components/booking/BookingFlow.svelte` | +| ~~Chi router conflict~~ | ~~Flattened /bookings sub-Route to prevent RequireAuth bleed~~ DONE | `backend/main.go` | +| ~~Reservation self-block~~ | ~~Exclude RESERVATION:* from GetTimeBlockersInRange~~ DONE | `backend/handlers/scheduling/time-blockers.go` | | **Begin button (Today)** | Manual start for early arrivals, gray out if >3hrs away | `CurrentAppointment.svelte` + backend | | **One-off custom services** | Admin creates custom service for single booking without adding to main list | Backend + frontend booking modals | | **One-off exceptional hours** | Single-day exceptions (dentist, afternoon off) - not yearly/weekly | Backend scheduling + frontend HolidayHours | @@ -511,6 +534,7 @@ ZM|| `booking_edit_requests` | Pending customer edit requests | | **Square payment integration** | Full Square SDK integration | Backend payment handlers + frontend payment step | | **GDPR data export** | User button for "give me my data" using `export_all_user_data()` | Backend endpoint + account page | | **Tax data export** | Admin button for tax-software-compatible format | Backend endpoint + admin page | +| **Frontend deposits UI** | Display `deposits_required` status to users | Account page / booking flow | ### Medium Priority @@ -632,11 +656,14 @@ src/lib/components/ - The backend merges default hours with applied exceptional hours automatically - Static frontend is built and served by nginx; API calls go directly to Go backend in production - Local dev uses SvelteKit's API proxy for CORS avoidance -- **GDPR functions exist in SQL** (`anonymize_user`, `export_all_user_data`, `delete_guest_user`) but only `DELETE /api/user/account` is wired - need user data export and admin tax export endpoints -- **Debug console.logs** in `BookingFlow.svelte:600` and `BookingCreateModal.svelte:224` should be removed before production +- **GDPR functions exist in SQL** (`anonymize_user`, `export_all_user_data`, `delete_guest_user`) β€” backend also has `AnonymizeStaleGuestAccounts()` which scrubs guest PII 6 months after booking start_time. Still need user-facing data export endpoint and admin tax export endpoint. - **Notifications** are pull-based only (no push/WebSocket). Admin endpoint exists but no frontend UI. No user-facing notification system yet. - **Notification acknowledgment**: When a booking is confirmed, any pending notification is acknowledged. When cancelled, pending is acknowledged and cancelled_booking notification is only created if the booking was not in pending status (e.g. was confirmed or in_progress). - **User notification preferences**: `user_notification_preferences` table exists with email/sms/push enabled flags, waiting on user notification system to be implemented. +- **Guest accounts**: Disposable, created via `POST /api/users/guest`. Email uniqueness enforced only for non-guests (partial unique index). Multiple guest accounts can share an email. +- **Slot reservations**: Stored in `time_blockers` with `RESERVATION:*` descriptions. `GetTimeBlockersInRange` excludes these to prevent the reservation from blocking its own booking. +- **Chi router**: `/bookings` routes use explicit full paths (not `r.Route("/bookings", ...)`) to prevent RequireAuth middleware from bleeding into OptionalAuth POST endpoints. +- **Seed script** (`local-dev-2.sh`): `open_day()` skips Sunday and Saturday (matching `working_hours` schema). Guest bookings use reserve-then-book flow matching frontend. --- @@ -671,38 +698,72 @@ Running `local-dev-2.sh` creates: | Resource | Count | Details | |----------|-------|---------| -| Users | 18 | 1 admin, 17 regular users | -JR|| Services | 8 | 6 standard (no patch test) + 2 requiring patch tests (Gel Polish Full Set 24h, Luxury Gel Manicure 24h) | -| Bookings | 45 | 8 past, 3 today, 4 tomorrow, 30 future (spread over 15 days) | -| Confirmed | ~50% | Random selection of upcoming bookings auto-confirmed | -| Exceptional | 2 | November Break (closed), Christmas Holiday (reduced hours) | +| Users | 20 | 1 admin, 19 regular users | +| Services | 12 | 10 standard + 2 requiring patch tests | +| Past Bookings | ~17 | Historical data | +| Today's Bookings | 4 | | +| Tomorrow's Bookings | 5 | | +| Upcoming Bookings | ~15 | Spread over 14 days | +| Guest Bookings | 3 | Nina (+16d), Bob (+20d), Carol (+22d) β€” reserve-then-book flow | +| Total Bookings | 42 | | +| Time Blockers | 3 | Staff meeting, holiday, late start | +| Schedule Groups | 3 | Exceptional working hour configurations | +| Cancellations | 1 | Simulated client cancellation | ### API JSON Examples -**Create Booking:** +**Create Guest Account:** ```json +POST /api/users/guest { - "start_time": "2025-01-15T10:00:00+00:00", - "service_ids": ["abc123def456"], + "first_name": "Jane", + "last_name": "Doe", + "email": "jane@example.com", + "phone": "+447700900123" +} +// Returns: { "id": "abc123def456", "account_role": "guest", ... } +``` + +**Create Booking (Guest):** +```json +POST /api/bookings +{ + "user_id": "abc123def456", + "start_time": "2026-05-20T13:00:00+01:00", + "service_ids": ["SVC001"], "notes": "Optional notes" } ``` +**Create Booking (Authenticated):** +```json +POST /api/bookings +{ + "start_time": "2026-05-20T13:00:00+01:00", + "service_ids": ["SVC001"], + "notes": "Optional notes" +} +``` + +**Reserve Slot (Anonymous):** +```json +POST /api/bookings/reserve +{ + "start_time": "2026-05-20T13:00:00+01:00", + "service_ids": ["SVC001"] +} +// Returns: { "id": "TBL001", "expires_at": "...", "is_anonymous": true } +``` + **Create Service:** ```json -VN|{ +POST /api/admin/services +{ "name": "Classic Manicure", "description": "Nail shaping, cuticle care, hand massage, and polish.", "price": 25.00, "duration_minutes": 45, "minimum_age_required": 0 -} - "name": "Classic Manicure", - "description": "Nail shaping, cuticle care, hand massage, and polish.", - "price": 25.00, - "duration_minutes": 45, - "patch_test_duration_hours": 0, - "minimum_age_required": 0 } ``` diff --git a/obsidian/Crussell/Future Work - Gap Backlog.md b/obsidian/Crussell/Future Work - Gap Backlog.md new file mode 100644 index 0000000..0a1f59d --- /dev/null +++ b/obsidian/Crussell/Future Work - Gap Backlog.md @@ -0,0 +1,234 @@ +**Last Updated:** May 2026 +**Status:** Living backlog β€” add to this as gaps are discovered + +--- + +# 🟒 Local Gaps β€” Can Fix Now + +No external dependencies. No paid services. No API keys needed. + +## P0 β€” Critical (Fix Now) + +| # | Gap | Effort | Area | Notes | +|---|-----|--------|------|-------| +| 1 | ~~`DELETE /api/user/account` is a no-op~~ βœ… | S (1-2h) | Backend | Wired to `anonymize_user()` for registered users and `delete_guest_user()` for guests. CardDAV contact deleted best-effort. | +| 2 | **WalkInCreateModal guest booking errors out** | S (1-2h) | Frontend | Line 277: commented-out guest creation code. Shows "Guest booking not yet implemented" toast despite backend fully working. | +| 3 | **ApprovalModal decline/cancel stub** | S (2-3h) | Frontend | `handleDecline()` shows "Coming soon" toast. Admin cannot reject pending bookings. Backend confirm/cancel endpoints exist β€” decline just needs a cancel call. | +| 4 | **CurrentAppointment action stubs** | M (1d) | Frontend | `handleTakePayment()`, `handleExtend()`, `handleCancel()` all show "Coming soon". Today page has 3 dead buttons. Take payment is ⚠️ blocked on Square, but Extend and Cancel are local. | + +## P1 β€” High + +| # | Gap | Effort | Area | Notes | +|---|-----|--------|------|-------| +| 5 | **Admin notification panel** | M (1-2d) | Frontend | Backend fully wired (GET/acknowledge). No frontend UI to display notifications. Admin has no visibility into pending bookings, cancellations, no-shows. | +| 6 | **Reservation/anonymization cron** | S (2-3h) | Backend | `CleanupOldReservations()` and `AnonymizeStaleGuestAccounts()` only fire on availability fetch. If no one fetches availability, expired reservations persist and stale guests aren't anonymized. Should be a background ticker in `main.go`. | +| 7 | **GDPR data export endpoint** | M (1d) | Backend | `export_all_user_data()` SQL function exists (JSON export). No Go handler wired. Required for GDPR Article 15 SAR requests. | +| 8 | **VAT/Tax export endpoints** | M (1-2d) | Backend | `get_vat_return_data()`, `export_sales_transactions()` SQL functions exist. No admin API to trigger them. Needed for HMRC compliance. | +| 9 | **Walk-in guest reservation β†’ booking transition** | S (1h) | Frontend | WalkInCreateModal line 358: TODO notes guest reservations may not properly transition to real bookings. Needs verification + fix. | +| 10 | **Password reset flow not wired to frontend** | S (2-3h) | Frontend | Backend has `/api/verify/generate` and `/api/verify/check` endpoints. Login page has no "forgot password" link or form. | +| 11 | **Email verification flow not wired to frontend** | S (2-3h) | Frontend | Users register with `unverified_email` role. No UI to enter verification code or resend code. `+layout.svelte` has alert-based prototype. | +| 12 | **Booking cancellation from user account** | S (2-3h) | Frontend | UserBookingModal shows booking details but no cancel button. Users must call/email to cancel. Backend endpoint exists (`DELETE /api/bookings/{id}`). | +| 13 | **Booking rescheduling for users** | M (1-2d) | Full-stack | Users can't reschedule their own bookings. `booking_edit_requests` table exists but frontend flow is incomplete β€” ApprovalModal only handles approve, not decline. | + +## P2 β€” Medium + +| # | Gap | Effort | Area | Notes | +|---|-----|--------|------|-------| +| 14 | ~~`delete_guest_user()` SQL function missing~~ βœ… | S (1h) | DB | Created next to `anonymize_user()` in init-script.sql. Called by `DeleteAccountHandler` for guest users. | +| 15 | **User notification preferences UI** | S (2-3h) | Frontend | DB table `user_notification_preferences` exists with email/sms/push flags. No settings page to toggle them. | +| 16 | **One-off custom services** | M (1-2d) | Full-stack | Admin can't create single-use services outside the catalog. Every custom job (bridal party, special request) must be added to permanent service list. | +| 17 | **One-off exceptional hours** | M (1d) | Full-stack | Single-day overrides (dentist appointment, afternoon off) require creating a full exceptional group. Should support one-off date blocks without group overhead. | +| 18 | ~~**HSTS header**~~ βœ… | XS (15min) | Backend | Added as a TODO-comment in the security headers middleware. Will be uncommented when HTTPS is enabled in production. | +| 19 | ~~**Referrer-Policy header**~~ βœ… | XS (15min) | Backend | Added as a TODO-comment in the security headers middleware. Will be uncommented when ready for production. | +| 20 | **Business settings management UI** | M (1-2d) | Full-stack | `business_settings` table exists (VAT registration, business name, etc.). No admin page to configure. Changes require direct SQL. | +| 21 | **Referral system UI** | M (1-2d) | Full-stack | `user_referrals` table exists. Users can't see their referral code or track uses. Admin can't manage referral campaigns. | +| 22 | **Analytics endpoints** | M (1-2d) | Backend | `handlers/admin/analytics.go` is 1 line. `get_monthly_business_summary()`, `get_sales_totals()` SQL functions exist. No admin dashboard stats. | +| 23 | ~~**console.log debug statements**~~ βœ… | XS (15min) | Frontend | Removed from BookingFlow.svelte and ImageUpload.svelte. | +| 24 | ~~**Alert-based prototype UX**~~ βœ… | XS (30min) | Frontend | Replaced all `alert()` calls with `toast.success/error/info` from svelte-sonner. | +| 25 | **No customer relationship view** | M (1-2d) | Frontend | Admin UserModal shows bookings list but no consolidated view: total spend, visit frequency, preferences, notes history. | +| 26 | **CSV/Excel export for bookings/payments** | M (1d) | Backend | Admin can't export data for accounting software. SQL functions exist but no endpoint to download as CSV. | +| 27 | ~~**Graceful shutdown**~~ βœ… | S (1h) | Backend | Added signal handling for SIGTERM/SIGINT with 15-second shutdown timeout in main.go. | +| 28 | ~~**Health check endpoint**~~ βœ… | XS (15min) | Backend | Added `GET /api/health` returning overall status plus DB, S3, Square, and frontend service statuses. | +| 29 | **API documentation** | M (1-2d) | Backend | No OpenAPI/Swagger spec. No generated docs. New developers must read code to understand endpoints. | +| 30 | **XSS input sanitization** | S (2-3h) | Backend | Backend validates format (regex, length) but doesn't sanitize HTML entities. Stored XSS risk in `notes`, `name`, `description` fields. | +| 31 | **Per-user rate limiting** | M (1d) | Backend | Rate limiter is IP-based. Authenticated users could abuse from multiple IPs. Should track by user ID + IP. Pure Go β€” no Redis needed for single-instance. | +| 32 | **CSRF protection** | S (2-3h) | Backend | SvelteKit handles some CSRF for its own forms, but direct API calls to `/api/*` bypass it. Consider double-submit cookie or SameSite cookies. | +| 33 | **Begin button (Today page)** | S (2-3h) | Full-stack | Manual start for early arrivals. Gray out if >3hrs away. Currently auto-infer only. | +| 34 | **Auto lunch protection** | M (1d) | Backend | Block bookings that remove lunch break. 1h customer auto-block, 30min admin with warning. | +| 35 | **Walk-in slot blocking** | S (1-2h) | Frontend | `WalkInCreateModal` doesn't properly block the next available slot during walk-in intake. Other customers could book the same slot. | +| 36 | **No idempotency keys for bookings** | S (2-3h) | Backend | Double-clicking "Confirm Booking" could create duplicate bookings. Should use idempotency keys or optimistic locking. | +| 37 | **No booking conflict detection for users** | S (2-3h) | Backend | Users can theoretically double-book themselves if they open two tabs. Reservation system helps but doesn't fully prevent. | +| 38 | **Service category/tag management** | M (1-2d) | Full-stack | Services have no category field. Hard to organize (manicure vs pedicure vs nail art). Admin must scroll through flat list. | +| 39 | ~~**No customer-facing cancellation policy display**~~ βœ… | XS (30min) | Frontend | Added cancellation policy text block in BookingFlow Step 3 below the terms & conditions line. | +| 40 | **No no-show tracking dashboard** | S (2-3h) | Frontend | Admin can't see which users have accumulated no-shows. `forgiven_no_shows` table exists but no UI. | +| 41 | ~~**No timezone handling for international customers**~~ βœ… | XS (15min) | Frontend | **Intentionally not implemented.** Crussell is UK-only; Cloudflare blocks non-UK traffic. Added explanatory comments in BookingFlow.svelte and README.md so this stops being flagged. | +| 42 | **Dark mode** | M (1-2d) | Frontend | SvelteKit + Tailwind supports it easily. No dark mode toggle or `prefers-color-scheme` support. | +| 43 | **PWA support** | L (3-5d) | Frontend | No service worker, no manifest.json, no offline support. Customers can't "install" the booking app. | +| 44 | **Automated database backups** | M (1d) | Infrastructure | No backup strategy. PostgreSQL volume is persistent but no automated dumps, no point-in-time recovery. Can use `pg_dump` cron on the host β€” no external service needed. | +| 45 | **Loyalty stamp redemption** | M (1-2d) | Full-stack | Display exists (account page shows "X stamps until 10% off"). No mechanism to redeem 10 stamps. Auto-apply of 10% discount at payment is ⚠️ blocked on Square. | +| 46 | **Staff management** | L (3-5d) | Full-stack | No multi-staff support. All bookings assumed single-provider. Schema change: add `staff_id` to bookings, per-staff availability tables. | +| 47 | **Recurring bookings** | L (3-5d) | Full-stack | Customers can't book the same slot weekly/monthly. Would need a `recurring_bookings` table + background job to materialize instances. | +| 48 | **Waitlist functionality** | M (1-2d) | Full-stack | When a slot is full, no way for customers to join a waitlist. Notification on cancellation is ⚠️ blocked on email/SMS, but in-app notification panel (#5) can handle it. | +| 49 | **Image optimization for portfolio** | M (1-2d) | Full-stack | Images uploaded as-is. No WebP conversion, no lazy loading, no responsive `srcset`. Portfolio loads full-res images. | + +--- + +# πŸ”΄ External Gaps β€” Blocked on Third-Party Access + +Require paid accounts, API approval, or external service credentials. **Do not attempt until access is granted.** + +## Payment β€” Square + +| # | Gap | Effort | Area | Status | Notes | +|---|-----|--------|------|--------|-------| +| E1 | **Payment integration (Square SDK)** | XL (3-5d) | Full-stack | πŸ”’ Blocked | BookingFlow Step 4 shows "Square payment integration will be added here". Need `github.com/square/square-go-sdk` + API credentials. | +| E2 | **No deposit payment flow** | M (1-2d) | Full-stack | πŸ”’ Depends on E1 | Users with `deposits_required > 0` can't pay deposits online. Blocked from new bookings until they pay in-person. | +| E3 | **Tip calculation UI** | S (1h) | Frontend | πŸ”’ Depends on E1 | `tip` exists in `payment_type` enum. No UI to add tips during payment or at the Today page. | +| E4 | **Gift card system** | L (3-5d) | Full-stack | πŸ”’ Depends on E1 | `giftcard` exists in `payment_method` enum. No gift card creation, redemption, or balance tracking. | + +## Email/SMS β€” SMTP Provider (Resend, SendGrid, Twilio, etc.) + +| # | Gap | Effort | Area | Status | Notes | +|---|-----|--------|------|--------|-------| +| E5 | **Email/SMS notification system** | XL (5-7d) | Backend | πŸ”’ Blocked | No SMTP integration. No scheduled jobs for booking reminders. `user_notification_preferences` table exists but unused. Need SMTP credentials or API key from provider. | +| E6 | **Automated deposit reduction notification** | S (1h) | Backend | πŸ”’ Depends on E5 | When `deposits_required` decreases, no notification is sent. User doesn't know they're closer to being unblocked. | +| E7 | **Waitlist cancellation notifications** | S (1h) | Backend | πŸ”’ Depends on E5 + #48 | When a slot opens up, waitlisted customers need to be notified. | + +## Cloud Storage β€” S3/R2 Production + +| # | Gap | Effort | Area | Status | Notes | +|---|-----|--------|------|--------|-------| +| E8 | **S3/R2 production stubs** | M (1d) | Backend | πŸ”’ Blocked | `s3.go` (`!dev` build tag): Upload/Download/Delete all return "not implemented" errors. Prod builds cannot store images. Need AWS SDK v2 + R2/S3 credentials. Dev works fine with RustFS. | + +## Social Auth β€” OAuth Apps (Google, Microsoft, Facebook) + +| # | Gap | Effort | Area | Status | Notes | +|---|-----|--------|------|--------|-------| +| E9 | **Social auth (Google/Microsoft/Facebook)** | L (2-3d) | Backend | πŸ”’ Blocked | `handlers/auth/social.go` is 1 line. Need OAuth app registrations + client secrets for each provider. | + +## Monitoring β€” Sentry / Error Tracking + +| # | Gap | Effort | Area | Status | Notes | +|---|-----|--------|------|--------|-------| +| E10 | **Error tracking / monitoring** | M (1-2d) | Backend | πŸ”’ Blocked | No Sentry, no structured logging, no error aggregation. `log.Printf()` only. No alerting on 5xx errors. Needs Sentry DSN or equivalent. | + +--- + +## Dependency Map + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ EXTERNAL BLOCKERS β”‚ +β”‚ β”‚ +β”‚ E1 Square API ──┬──→ E2 Deposit payments β”‚ +β”‚ β”œβ”€β”€β†’ E3 Tips β”‚ +β”‚ └──→ E4 Gift cards β”‚ +β”‚ β”‚ +β”‚ E5 SMTP/API ────┬──→ E6 Deposit reduction notificationsβ”‚ +β”‚ └──→ E7 Waitlist notifications β”‚ +β”‚ β”‚ +β”‚ E8 S3/R2 ───────→ Portfolio images in production β”‚ +β”‚ β”‚ +β”‚ E9 OAuth Apps ──→ E9 Social auth β”‚ +β”‚ β”‚ +β”‚ E10 Sentry ─────→ E10 Error tracking β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ LOCAL (UNBLOCKED) β”‚ +β”‚ β”‚ +β”‚ #1 Delete account ──→ #7 GDPR export ──→ #14 SQL func β”‚ +β”‚ β”‚ +β”‚ #3 Approval decline ──→ #13 Booking reschedule β”‚ +β”‚ β”‚ +β”‚ #5 Admin notification panel ──→ #15 Preferences UI β”‚ +β”‚ ──→ #48 Waitlist (partial) β”‚ +β”‚ β”‚ +β”‚ #2 Walk-in guest fix ──→ #9 Reservation transition β”‚ +β”‚ β”‚ +β”‚ #6 Reservation cron ──→ #42 Dark mode (no deps) β”‚ +β”‚ β”‚ +β”‚ #39 Cancellation policy ──→ ZERO deps, 30min fix β”‚ +β”‚ #28 Health check ──→ ZERO deps, 15min fix β”‚ +β”‚ #23 console.log cleanup ──→ ZERO deps, 15min fix β”‚ +β”‚ #24 Alert prototype cleanup ──→ ZERO deps, 30min fix β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Suggested Execution Order + +### Phase 1 β€” Zero-Dependency Quick Wins (Week 1) + +*No external services. Each takes <30min except #1.* + +1. **#23** Remove console.log debug statements (15min) +2. **#24** Replace alert() prototypes with toast notifications (30min) +3. **#28** Add health check endpoint (15min) +4. **#39** Add cancellation policy display to BookingFlow (30min) +5. **#41** Fix timezone display for international customers (15min) +6. **#18** Add HSTS header (15min) +7. **#19** Add Referrer-Policy header (15min) +8. **#1** Fix `DELETE /api/user/account` (1-2h) β€” biggest win in this phase + +### Phase 2 β€” Admin Productivity (Week 2) + +9. **#2** Wire WalkInCreateModal guest booking (1-2h) +10. **#3** ApprovalModal decline/cancel (2-3h) +11. **#5** Admin notification panel (1-2d) +12. **#4** CurrentAppointment Extend + Cancel actions (1d) β€” skip TakePayment (blocked on E1) +13. **#12** Booking cancellation from user account (2-3h) +14. **#40** No-show tracking dashboard (2-3h) + +### Phase 3 β€” Compliance + Reliability (Week 3) + +15. **#6** Reservation/anonymization background cron (2-3h) +16. **#7** GDPR data export endpoint (1d) +17. **#8** VAT/Tax export endpoints (1-2d) +18. **#14** Create `delete_guest_user()` SQL function (1h) +19. **#27** Graceful shutdown (1h) +20. **#36** Idempotency keys for bookings (2-3h) +21. **#30** XSS input sanitization (2-3h) +22. **#44** Automated database backups (1d) + +### Phase 4 β€” User Experience (Week 4) + +23. **#10** Password reset flow (2-3h) +24. **#11** Email verification flow (2-3h) +25. **#13** Booking rescheduling for users (1-2d) +26. **#20** Business settings management UI (1-2d) +27. **#16** One-off custom services (1-2d) +28. **#17** One-off exceptional hours (1d) +29. **#35** Walk-in slot blocking (1-2h) +30. **#38** Service category management (1-2d) + +### Phase 5 β€” Growth + Polish (Week 5+) + +31. **#21** Referral system UI (1-2d) +32. **#22** Analytics endpoints (1-2d) +33. **#25** Customer relationship view (1-2d) +34. **#26** CSV/Excel export (1d) +35. **#29** API documentation (1-2d) +36. **#31** Per-user rate limiting (1d) +37. **#32** CSRF protection (2-3h) +38. **#33** Begin button (Today page) (2-3h) +39. **#34** Auto lunch protection (1d) +40. **#37** Booking conflict detection (2-3h) +41. **#45** Loyalty stamp redemption (partial β€” UI only, payment apply blocked on E1) +42. **#46** Staff management (3-5d) +43. **#47** Recurring bookings (3-5d) +44. **#48** Waitlist functionality (1-2d) +45. **#49** Image optimization (1-2d) +46. **#42** Dark mode (1-2d) +47. **#43** PWA support (3-5d) + +### ⏳ Waiting on External Access + +| Item | Blocked On | Unblocks | +|------|-----------|----------| +| **E1 Square payment** | Square API credentials | E2, E3, E4, deposit payments, loyalty redemption | +| **E5 Email/SMS** | SMTP provider (Resend/SendGrid/Twilio) | E6, E7, booking reminders, password reset emails | +| **E8 S3/R2 production** | Cloudflare R2 or AWS S3 credentials | Portfolio images in production builds | +| **E9 Social auth** | OAuth app registrations (Google/Microsoft/Facebook) | Social login flow | +| **E10 Sentry** | Sentry DSN or equivalent | Error tracking, 5xx alerting |