Crussell

#KM|> Last Updated: March 2026 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 8 services incl. 2 with patch tests)
└─ 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 PHPbased WebDAV (served by Nginx) nginx Reverseproxy (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, and sample services including patch test services.

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 (admin@example.com), regular users (user@example.com), 8 services (6 standard + 2 requiring patch tests), bookings, exceptional hours.

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

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

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:

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 Utilities Usage

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
  • 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

Recent Testing & Bug Fixes

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

📂 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:

  • 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

# 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

🏗️ Implementation Status

Complete

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
Holiday Hours Integrated in all 3 booking flows
Loyalty Backend DB schema ready, no frontend component
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
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 ⚠️
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)
Loyalty Stamps Backend complete, displayed in account page

⚠️ 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
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

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

📋 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

🔄 Booking Flows

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

All three flows integrate with the holiday/exceptional hours system to prevent bookings during closed periods.


💰 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: PUT /api/bookings/{id}/cancel

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

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

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

grep -rn "func.*Create.*Booking\|func.*WalkIn\|func.*Walk.*In\|POST.*booking" backend/handlers/ --include="*.go"

Frontend - All Booking Flow Components:

grep -rln "BookingFlow\|WalkIn\|walk-in\|call.*in\|message.*in" frontend/src/lib/components/ --include="*.svelte"

Combined - Single View of All Booking Flows:

grep -rn "CreateBooking\|CreateWalkIn\|BookingFlow\|submitBooking" backend/ frontend/ --include="*.go" --include="*.svelte"

Database Schema

All Enums:

grep -n "CREATE TYPE" init-scripts/init-script.sql

All Tables:

grep -n "CREATE TABLE" init-scripts/init-script.sql

All Functions:

grep -n "CREATE OR REPLACE FUNCTION" init-scripts/init-script.sql

Router Endpoints

All Wired Routes:

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)
grep -n "r\.Use\|r\.Group" backend/main.go

Authentication

JWT Middleware:

grep -rn "JWTMiddleware\|VerifyToken\|ParseToken" backend/ --include="*.go"

Protected Routes:

grep -n "r\.Group.*Auth" backend/main.go

Holiday/Exceptional Hours

Usage Across All Flows:

grep -rn "exceptional.*hours\|holiday.*hours\|getExceptional\|getHoliday" backend/handlers/ frontend/src/ --include="*.go" --include="*.svelte"

Notifications

Admin Notifications:

grep -rn "admin_notifications\|AdminNotification" backend/ --include="*.go"

Notification Reasons (Enum Values):

grep -A10 "admin_notification_reason" init-scripts/init-script.sql

GDPR Functions

Data Export/Anonymization:

grep -rn "anonymize_user\|export_all_user_data\|delete_guest_user" backend/ --include="*.go" init-scripts/

Frontend Debug Artifacts

Console Logs to Remove:

grep -rn "console\.log" frontend/src/ --include="*.svelte" --include="*.ts"

🗄️ Database Schema Overview

Enums

Enum Values
account_role user, admin
account_type standard, vip, guest
booking_status
payment_type
payment_method
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
KR
WW
TW
QB
SP
SP
SP
SP
verification_codes Email verification and password reset codes
services Salon service catalog
bookings Appointment records
booking_services Services per booking (many-to-many)
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%