- backend/handlers/user/account.go: Wire DELETE /api/user/account to call anonymize_user() for registered users and delete_guest_user() for guests, with CardDAV contact cleanup - backend/handlers/user/profile_test.go: Add TestAccount_DeleteGuest and enhance TestAccount_Delete to verify anonymization results - backend/main.go: Add GET /api/health endpoint with DB ping and S3 status check; add HSTS and Referrer-Policy security headers; replace http.ListenAndServe with http.Server + graceful SIGTERM/SIGINT shutdown - frontend/routes/+layout.svelte: Replace alert() with toast notifications for email verification flow - frontend/routes/login/+page.svelte: Replace alert() with toast.info for social login prototype buttons - frontend/booking/BookingFlow.svelte: Remove 2 console.log debug calls; add cancellation policy note in Step 3; add timezone policy comment - frontend/ImageUpload.svelte: Comment out debug console.log - init-scripts/init-script.sql: Add delete_guest_user() SQL function - docs: Update README.md and Obsidian notes to reflect completed items
443 lines
18 KiB
Markdown
443 lines
18 KiB
Markdown
# Crussell
|
||
#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
|
||
|
||
```
|
||
Crussell/
|
||
├─ backend/ # Go 1.25 + chi router API
|
||
├─ frontend/ # SvelteKit 5 SPA (static build)
|
||
├─ sabredav/ # PHP + Composer for DAV
|
||
├─ 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 guest accounts, time blockers, reservations)
|
||
└─ README.md
|
||
```
|
||
|
||
## ⚙️ Prerequisites
|
||
|
||
| Tool | Version |
|
||
|------|---------|
|
||
| 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.
|
||
|
||
## 📥 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.
|
||
```
|
||
|
||
### Docker‑Compose
|
||
|
||
The simplest way to bring the whole stack up is with Docker‑Compose.
|
||
|
||
```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 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, 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 PHP‑FPM and Composer. The Docker image installs dependencies automatically during the container start‑up.
|
||
|
||
## 🧪 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 ./...
|
||
|
||
# Run with verbose output
|
||
go test -v ./...
|
||
|
||
# Run specific test file
|
||
go test -v ./handlers/bookings
|
||
|
||
# Run tests matching pattern
|
||
go test -v -run "TestBooking" ./...
|
||
```
|
||
|
||
### Test Database Setup
|
||
|
||
Tests use a dedicated PostgreSQL database. Set the connection string via:
|
||
|
||
```bash
|
||
export TEST_DB_DSN="postgres://user:pass@localhost:5432/crussell_test?sslmode=disable"
|
||
go test ./...
|
||
```
|
||
|
||
Default DSN: `postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable`
|
||
|
||
### Test Conventions
|
||
|
||
- All test files use `//go:build test` build tag
|
||
- Database is migrated fresh per test run via `testdb.Migrate()`
|
||
- Tables are truncated between tests via `testdb.TruncateTables()`
|
||
- Test tokens use a fixed secret: `test-secret-key-for-testing-only`
|
||
- Fixtures auto-generate unique emails to avoid conflicts
|
||
|
||
### Test Coverage
|
||
|
||
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
|
||
|
||
| 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 |
|
||
| 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 | ✅ |
|
||
|
||
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!**
|