Guest flow: CreateGuestUserHandler creates disposable guest accounts on-the-fly. CreateBookingHandler uses OptionalAuth — accepts authenticated or guest (user_id in body, validated as account_role='guest'). Guests bypass deposits, patch tests, and the 24h deposit advance rule. Admin reserve: AdminReserveSlotHandler supports walk-in (5min TTL) and call-in (60min TTL) reservations with configurable TTL. Validates against bookings, blockers, working hours. Route restructuring: POST /bookings moved to OptionalAuth group. POST /bookings/reserve added for public reservation. POST /admin/bookings/reserve added for admin. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Crussell
#KM|> Last Updated: March 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 8 services incl. 2 with patch tests)
└─ 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
# 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.
docker compose up --build -d
postgres– PostgreSQL 17backend– Go API (exposed on:8080)sabredav– PHP‑based WebDAV (served by Nginx)nginx– Reverse‑proxy (HTTP on:80and 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.
chmod +x local-dev-2.sh
./local-dev-2.sh
The script performs the following steps:
- Docker checks – starts Docker if it isn't already running.
- PostgreSQL reset – removes the old volume and starts a fresh container.
- tmux session – creates
crussell-devwith panes:psqlconsole- Go server (
go run -tags dev ./main.go) - Svelte dev server (
npm run dev -- --host) - Rustfs logs
- 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
jqis 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 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 |
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 testbuild 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
notificationshandler (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 testbuild 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 obligationsdeposit_required: Boolean on bookings table, snapshotted at creation timedeposit_amount: Calculated as 20% of booking total for displaydeposit_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:
< 24hwithout forgiveness →status = no_show,deposits_required = 3< 24hwith 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:
{
"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 ifdeposits_required > 0enforce_deposits = false: Bypass all deposit checks, allow multiple active bookings
Use Cases:
true: Standard workflow, ensure users clear deposits before booking againfalse: 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:
- User has status =
no_show(without forgiveness flag) - Booking occurred within last 6 months
- Not yet marked as "forgiven" in
forgiven_no_showstable - 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!