Files
Crussell/README.md
T
popertots a4ea994c69 docs: update README, Obsidian docs, and gap backlog for booking edit modal
Reflect new PUT /api/admin/bookings/{id} services endpoint, EditBookingModal
component, and updated test count (306/308). Mark handleEdit() stub resolved
in gap backlog.
2026-05-16 17:32:32 +01:00

447 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Crussell
#KM|> **Last Updated:** May 2026 — Booking edit modal for /today page, 306/308 tests passing
Crussell is a **fullstack application** that powers a nailbar / salon booking service. The repository is split into a **Go** backend and a **SvelteKit** frontend, 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
```
Crussell/
├─ backend/ # Go 1.25 + chi router API
├─ frontend/ # SvelteKit 5 SPA (static build)
├─ sabredav/ # PHP + Composer for DAV
├─ nginx/ # Nginx reverseproxy for HTTP & HTTPS
├─ init-scripts/ # PostgreSQL init SQL
├─ compose.yml # DockerCompose definition
├─ local-dev-2.sh # Development helper using tmux (seeded with guest accounts, time blockers, reservations)
└─ README.md
```
## ⚙️ Prerequisites
| Tool | Version |
|------|---------|
| Docker & DockerCompose | ≥ 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.
## 📥 Getting Started
```bash
# Clone the repository
git clone http://git.popertots.com/popertots/Crussell.git
cd Crussell
# Copy the example environment file and edit it
cp .env.example .env
# Open .env and provide values for POSTGRES_*, JWT_SECRET_KEY, etc.
```
### DockerCompose
The simplest way to bring the whole stack up is with DockerCompose.
```bash
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`)
After the containers are running, the frontend 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, guest accounts, services, bookings, time blockers, and exceptional hours.
```bash
chmod +x local-dev-2.sh
./local-dev-2.sh
```
The script performs the following steps:
1. **Docker checks** starts Docker if it isn't already running.
2. **PostgreSQL reset** removes the old volume and starts a fresh container.
3. **tmux session** creates `crussell-dev` with panes:
* `psql` console
* Go server (`go run -tags dev ./main.go`)
* Svelte dev server (`npm run dev -- --host`)
* Rustfs logs
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.
## 🔧 Building & Testing
### Backend
```bash
cd backend
# Build the binary
go build -o bin/backend ./main.go
```
The binary is then copied into the Docker image via the `Dockerfile`.
### Frontend
```bash
cd frontend
npm ci
npm run build # Production build (static)
npm run dev # Development server
```
### SabreDAV
SabreDAV is bundled with PHPFPM and Composer. The Docker image installs dependencies automatically during the container startup.
## 🧪 Testing
### Test Infrastructure
Crussell has a comprehensive Go testing infrastructure located in `backend/testutils/`:
| Component | File | Description |
|-----------|------|-------------|
| **Database** | `testutils/testdb/testdb.go` | PostgreSQL test pool, migrations, table truncation |
| **HTTP Helpers** | `testutils/helpers.go` | Request builders, auth helpers, assertions |
| **JWT** | `testutils/jwt/jwt.go` | Test token generation for users/admins |
| **HTTP Client** | `testutils/httptest/client.go` | REST client wrapper with auth support |
| **Fixtures** | `testutils/fixtures/fixtures.go` | Factory functions for test data |
| **Validators** | `internal/validators/validators.go` | ID validation utilities |
### Running Tests
```bash
cd backend
# Run all tests
go test -tags test ./...
# Run with verbose output
go test -tags test -v ./...
# Run specific test package
go test -tags test -v ./handlers/bookings
# Run tests matching pattern
go test -tags test -v -run "TestBooking" ./...
# Run tests twice to catch state leakage
go test -tags test -v -p 1 -count=2 ./...
```
### Test Database Setup
Tests use a dedicated PostgreSQL database (`crussell_test`). Set the connection via environment variables:
```bash
export POSTGRES_USER=myuser POSTGRES_PASSWORD=mypassword POSTGRES_HOST=localhost POSTGRES_DB=crussell_test GO_TESTING=1
go test -tags test ./...
```
### Test Architecture
- All test files use `//go:build test` build tag
- Each test package has a `TestMain` that runs schema migration **once** (not per test)
- Between tests, `TruncateTables()` clears data via `TRUNCATE TABLE ... CASCADE` — fast, no schema rebuild
- Tests run sequentially (`-p 1`) because all packages share the same test database
- Test tokens use a fixed secret: `test-secret-key-for-testing-only`
- Fixtures auto-generate unique emails to avoid conflicts
- `-count=N` is safe to increase for flaky test detection (each count re-runs TestMain + all tests)
### Test Coverage
**306/308 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, notifications, portfolio, user profiles, deposit system, edit requests, discount campaigns, and **booking service editing**.
## 📂 Environment Variables
| Variable | Purpose | Example |
|----------|---------|---------|
| `POSTGRES_USER` | DB username | `myuser` |
| `POSTGRES_PASSWORD` | DB password | `mysecret` |
| `POSTGRES_DB` | DB name | `mydb` |
| `JWT_SECRET_KEY` | HMAC key for JWT (HS256) | `supersecret` |
| `SABRE_DAV_*` | Optional SabreDAV overrides | |
Create a `.env` file in the project root based on the provided `.env.example`.
> **Note**: JWT tokens expire after 30 days. Login is rate-limited to 1 attempt per 5 seconds.
## 📊 Seeding Data
The `local-dev-2.sh` script automatically seeds:
| 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 |
---
## 🏗️ Implementation Status
### ✅ Complete
| Feature | Backend | Frontend | Notes |
|---------|---------|----------|-------|
| JWT Authentication | ✅ | ✅ | Login, register, refresh, middleware; HS256, 30-day expiry, auto-refresh |
| 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, **booking service editing** |
| Scheduling System | ✅ | ✅ | Default hours, exceptional hours, working/available hours, time blockers |
| Holiday Hours | ✅ | ✅ | Integrated in all 3 booking flows |
| 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, 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. |
| 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 |
| 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
| Feature | Status | Details |
|---------|--------|---------|
| 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 |
| Analytics | Handler exists | `handlers/admin/analytics.go` exists but NOT wired in router |
| Payment Integration | Stub | Square placeholder only |
### ❌ Not Wired (Handlers Exist)
| Handler | File | Notes |
|---------|------|-------|
| Social Auth | `handlers/auth/social.go` | OAuth integration placeholder |
| Analytics | `handlers/admin/analytics.go` | Statistics/dashboard endpoint |
### 🚧 Critical TODOs
| Location | Issue | Priority |
|----------|-------|----------|
| 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
| Feature | Description |
|---------|-------------|
| 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 |
---
## 🔄 Booking Flows
Crussell supports **three distinct booking flows**:
| Flow | User | Entry Point | Status |
|------|------|-------------|--------|
| 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 | ✅ |
| **Booking Edit** | Admin | `/today` → Next Appointment → Edit → `PUT /api/admin/bookings/{id}` | ✅ |
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.
---
## 💰 Deposit System
### Overview
The deposit system is a **simplified user-level tracking mechanism** that enforces a 3-deposit requirement before new bookings are allowed. It's designed to reduce booking abandonment and protect against chronic no-shows.
**Key Concepts**:
- `deposits_required`: Integer (0-3) on users table tracking outstanding deposit obligations
- `deposit_required`: Boolean on bookings table, snapshotted at creation time
- `deposit_amount`: Calculated as 20% of booking total for display
- `deposit_paid`: True when pre-start payments cover the deposit amount
### Cancellation Rules (Late = < 24 Hours)
| Scenario | Time Until Start | Forgiveness | Result Status | Penalty | User Blocked? |
|----------|------------------|-------------|---------------|---------|---------------|
| **Late cancellation (normal)** | < 24h | No/Omitted | `no_show` | +3 deposits (resets to 3, not +=) | Yes* |
| **Late cancellation (forgiven)** | < 24h | Yes | `client_cancelled` | None | No |
| **Normal cancellation** | ≥ 24h | Any | `client_cancelled` | None | No |
| **Pending cancellation** | Any | Any | Deleted | None | No |
*Assuming `deposits_required` > 0 after penalty
### API: User Cancellation
**Endpoint**: `DELETE /api/bookings/{id}`
**Request**:
```json
{
"reason": "no_show",
"forgive_no_show": true // Optional: true = forgive penalty, false/omitted = enforce penalty
}
```
**Behavior**:
- `< 24h` without forgiveness → `status = no_show`, `deposits_required = 3`
- `< 24h` with forgiveness → `status = client_cancelled`, no penalty
- `≥ 24h``status = client_cancelled`, no penalty (always)
### Admin Booking Creation with Deposit Control
**Endpoint**: `POST /api/admin/bookings`
**Request**:
```json
{
"user_id": "USR123",
"start_time": "2026-03-10T10:00:00Z",
"service_ids": ["SVC1"],
"enforce_deposits": false // Optional: true (default) = enforce checks, false = bypass checks
}
```
**Behavior**:
- `enforce_deposits = true` (default): Apply one-active-booking limit if `deposits_required > 0`
- `enforce_deposits = false`: Bypass all deposit checks, allow multiple active bookings
### Deposit Reduction
**Rule**: When booking transitions to `completed` with ≥ 1 payment → `deposits_required -= 1`
---
## 🗄️ Database Schema Overview
### Enums
| Enum | Values |
|------|--------|
| `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` |
### Key Tables
| Table | Purpose |
|-------|--------|
| `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 |
| `admin_notifications` | Admin alerts |
| `user_notification_preferences` | User notification preferences (email/sms/push) |
| `user_referrals` | Referral tracking |
### Validation Rules
| Field | Rules |
|-------|-------|
| Names | 1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot |
| Phone | UK format → E.164 (+44...) |
| Age | Must be 16+ years |
| Login Rate Limit | 1 attempt per 5 seconds |
---
**Happy coding!**