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
2025-10-31 23:11:33 +00:00

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

# 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.

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.

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

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

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

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:

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:

{
  "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
  • ≥ 24hstatus = client_cancelled, no penalty (always)

Admin Booking Creation with Deposit Control

Endpoint: POST /api/admin/bookings

Request:

{
  "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!

S
Description
Nail salon website with booking system and portfolio
Readme
48 MiB
Languages
Go 64.6%
JavaScript 16.5%
Svelte 14.8%
TypeScript 1.9%
PLpgSQL 1.2%
Other 0.9%