docs: reorganise Obsidian docs from monolith into multi-page knowledge base
Split Crussell Nails.md (822 lines) into 5 focused documents: - README.md: slimmed to 84 lines (elevator pitch + run instructions) - Overview.md: expanded landing page with architecture diagram and quick links - User Manual.md: 358 lines, customer-facing journeys in plain English - Admin Manual.md: 493 lines, staff operations guide with zero technical jargon - Technical Manual.md: 736 lines, full API reference, schema, key systems All user/admin manuals rewritten for non-technical audiences — no API endpoints, component names, or implementation details.
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
# Admin Manual
|
||||
|
||||
A practical guide for salon staff — how to use the admin side of the Crussell booking system for day-to-day operations.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Logging In
|
||||
|
||||
Go to the website and log in with your admin email and password. Once logged in as an admin, you'll see additional menu options that regular customers don't have access to — specifically the **Today** page and the **Admin** dashboard.
|
||||
|
||||
### Two Main Admin Pages
|
||||
|
||||
- **Today** — your daily operations hub. This is where you manage today's appointments, approve new bookings, and handle walk-ins.
|
||||
- **Admin** — the management dashboard. This is where you set up services, manage customers, adjust schedules, and handle the salon's settings.
|
||||
|
||||
---
|
||||
|
||||
## Today Page
|
||||
|
||||
This is the page you'll use most often during the working day. It's divided into three main sections.
|
||||
|
||||
### Current and Next Appointment
|
||||
|
||||
At the very top of the page, you'll see two cards:
|
||||
|
||||
**Current Appointment** (if one is happening right now):
|
||||
- Shows the customer's name
|
||||
- Lists the services they booked
|
||||
- Shows the start time and how long the appointment lasts
|
||||
- The appointment status updates automatically as time passes
|
||||
|
||||
**Next Appointment** (the one coming up after the current one):
|
||||
- Shows the customer's name and time
|
||||
- Shows how long until they arrive
|
||||
- Lists their services
|
||||
|
||||
#### Buttons on the Current Appointment Card
|
||||
|
||||
**Edit** — Opens a window where you can change which services are included in the appointment. You can add services, remove services, or swap them out. You can also adjust the price or duration of individual services if needed (for example, if a service took longer than expected last time). You can also update the appointment notes here.
|
||||
|
||||
**Take Payment** — This button exists but isn't connected to a payment system yet. For now, payments are handled separately.
|
||||
|
||||
**Extend** and **Cancel** — These buttons are placeholders and don't do anything yet.
|
||||
|
||||
### Today's Appointments
|
||||
|
||||
Below the current/next cards is a full list of every appointment scheduled for today, in chronological order.
|
||||
|
||||
Each appointment shows:
|
||||
- The customer's name
|
||||
- The time
|
||||
- The services
|
||||
- A colour-coded status indicator:
|
||||
- **Pending** — waiting for you to review and confirm
|
||||
- **Confirmed** — locked in and ready to go
|
||||
- **In Progress** — the appointment has started (this happens automatically when the start time arrives)
|
||||
- **Completed** — the appointment has finished (this happens automatically when the appointment duration has elapsed)
|
||||
- **Cancelled** — the appointment was cancelled
|
||||
|
||||
**Tap or click any appointment** to open its full details window, where you can see everything about that booking and take actions like approving or cancelling it.
|
||||
|
||||
### Pending Approvals
|
||||
|
||||
This section shows all bookings that are waiting for your review. When a customer books online, their booking starts as "pending" — it won't appear in the main schedule until you confirm it.
|
||||
|
||||
For each pending booking, you'll see:
|
||||
- The customer's name
|
||||
- The requested date and time
|
||||
- The services they want
|
||||
|
||||
**Confirm** — Accepts the booking. It moves from "pending" to "confirmed" and appears in the main schedule.
|
||||
|
||||
**Decline** — Cancels the booking. The customer's appointment is removed and marked as cancelled by the salon.
|
||||
|
||||
---
|
||||
|
||||
## Edit Booking Window
|
||||
|
||||
You reach this by going to **Today** → **Next Appointment** → **Edit**.
|
||||
|
||||
This window lets you change the services on an appointment that's currently happening or coming up.
|
||||
|
||||
### What You Can Do
|
||||
|
||||
- **Add services** — Tap to add extra treatments to the appointment
|
||||
- **Remove services** — Remove treatments the customer no longer wants
|
||||
- **Swap services** — Replace one service with another
|
||||
- **Adjust prices** — If a service needs a different price than usual (for example, extra-long nails), you can set a custom price for that specific service on this booking
|
||||
- **Adjust durations** — Similarly, you can set a custom duration for a service on this booking only
|
||||
- **Edit notes** — Add or change the notes on the booking
|
||||
|
||||
### What the System Checks
|
||||
|
||||
- You can't edit appointments that are already completed or cancelled
|
||||
- The system checks that your changes don't clash with the next appointment of the day
|
||||
- If the appointment falls during a special hours period (like a holiday schedule), you'll see a warning but can still proceed
|
||||
- If the customer had previously requested a reschedule, your edit clears their request
|
||||
|
||||
---
|
||||
|
||||
## Admin Dashboard
|
||||
|
||||
The Admin page is your management centre. It has several sections, each handling a different part of the salon's operations.
|
||||
|
||||
### Services
|
||||
|
||||
This is where you manage the list of treatments the salon offers.
|
||||
|
||||
**Add a New Service:**
|
||||
- Enter the service name (for example, "Gel Manicure")
|
||||
- Add a description (optional — shown to customers during booking)
|
||||
- Set the price
|
||||
- Set how long the service takes (in minutes)
|
||||
- Set a minimum age requirement (if the service isn't suitable for under-16s, for example)
|
||||
|
||||
**Edit a Service:**
|
||||
- Click on any service to change its name, description, price, duration, or age requirement
|
||||
|
||||
**Delete a Service:**
|
||||
- Removes the service from the list. Note: if the service has ever been used in a past booking, it can't be deleted (this keeps historical records accurate). You can deactivate it instead.
|
||||
|
||||
**Activate / Deactivate:**
|
||||
- Each service has an on/off toggle. Turning a service off hides it from the customer booking page, but keeps it in your system for records. Turn it back on when you want customers to be able to book it again.
|
||||
|
||||
**Patch Test Configuration:**
|
||||
- Some services require a patch test before booking. You can configure which services need one and set up the patch test details (how long the customer must wait after the test, and how long the test is valid for).
|
||||
|
||||
### Customers
|
||||
|
||||
This section shows a searchable list of everyone who has booked with the salon.
|
||||
|
||||
**What You See:**
|
||||
- Each customer's name, email address, and account type
|
||||
- You can filter the list by account type (admin, verified, unverified, guest, affiliate)
|
||||
|
||||
**Click on Any Customer** to open their full profile (see the Customer Profile section below for details).
|
||||
|
||||
### Bookings List
|
||||
|
||||
A chronological list of every booking in the system — past, present, and future.
|
||||
|
||||
**What You Can Do:**
|
||||
- Browse all bookings by date
|
||||
- Search for a specific booking (by customer name, date, or other details)
|
||||
- Filter by status (pending, confirmed, completed, cancelled, etc.)
|
||||
- Click any booking to see its full details and take actions
|
||||
|
||||
### Holiday Hours
|
||||
|
||||
This is where you set up special opening hours for holidays, bank holidays, or any period when your normal schedule changes.
|
||||
|
||||
**How It Works:**
|
||||
|
||||
1. **Create a schedule group** — Give it a name like "Christmas Week" or "August Bank Holiday"
|
||||
2. **Set the hours for each day of the week** — For example, during Christmas week you might want Monday–Friday to be 10am–2pm instead of your usual 9am–5pm, and Saturday to be closed
|
||||
3. **Apply the group to specific weeks** — Choose which Monday-starting weeks this schedule applies to. The system uses the Monday of each week as the reference point
|
||||
|
||||
**Example:** If you create a "Christmas Schedule" group with reduced hours and apply it to the week starting Monday 22nd December, those hours will be used for that entire week (Monday 22nd through Sunday 28th).
|
||||
|
||||
**To Remove:** Delete the schedule group when it's no longer needed.
|
||||
|
||||
### Weekly Schedule
|
||||
|
||||
This is where you set your regular, week-to-week opening hours.
|
||||
|
||||
- For each day of the week (Monday through Sunday), set the opening time and closing time
|
||||
- Toggle any day on or off — for example, if you decide to start opening on Saturdays, just turn Saturday on and set the hours
|
||||
- Changes take effect immediately — customers will see the updated availability right away
|
||||
|
||||
### Discount Campaigns
|
||||
|
||||
This is where you set up promotional discounts.
|
||||
|
||||
**Time-Based Campaigns:**
|
||||
- Set a start date and end date
|
||||
- Choose a discount percentage (for example, 5% off)
|
||||
- Choose who it applies to: all bookings, first-time bookings only, or new customers only
|
||||
- The discount automatically applies to eligible bookings during the campaign period
|
||||
|
||||
**Milestone Campaigns:**
|
||||
- These trigger when a customer reaches a certain milestone — for example, their 5th booking, or the anniversary of their first booking
|
||||
- Set the milestone type and value
|
||||
- The discount applies automatically when the milestone is reached
|
||||
|
||||
**Viewing Campaign Stats:**
|
||||
- For each campaign, you can see how many times it's been used and how much discount has been given out
|
||||
|
||||
**Campaign Statuses:**
|
||||
- **Draft** — set up but not yet active
|
||||
- **Active** — currently running
|
||||
- **Completed** — the campaign period has ended
|
||||
- **Cancelled** — you've stopped the campaign early
|
||||
|
||||
---
|
||||
|
||||
## Walk-In Bookings
|
||||
|
||||
For customers who arrive at the salon without a prior appointment.
|
||||
|
||||
### How to Book a Walk-In
|
||||
|
||||
1. **Open the Walk-In booking tool** from the Today page or Admin dashboard
|
||||
2. **Select the services** the customer wants — the same service list appears as for online bookings, showing prices and durations. The system will show which services the customer is eligible for based on their age and patch test history
|
||||
3. **Choose a time slot** — the system shows available slots. When you pick one, it's temporarily held for **5 minutes** while you complete the booking. This is a shorter hold than other booking types because walk-ins are happening right now
|
||||
4. **Select or create the customer:**
|
||||
- If they're an existing customer, search for them by name or email
|
||||
- If they're new, fill in their details (name, email, phone number). A guest account will be created for them
|
||||
- If you leave the phone number blank, it defaults to a standard UK number
|
||||
5. **Confirm the date and time** and add any notes
|
||||
6. **Submit the booking** — it's created immediately
|
||||
|
||||
---
|
||||
|
||||
## Call-In / Message-In Bookings
|
||||
|
||||
For customers who phone or message to book an appointment.
|
||||
|
||||
### How to Book a Call-In
|
||||
|
||||
1. **Open the Call-In booking tool** from the Admin dashboard
|
||||
2. **Find or create the customer** — search for an existing customer, or fill in details for a new one
|
||||
3. **Select the services** they want — again, eligibility is checked based on their age and patch test records
|
||||
4. **Choose a date and time** — the system shows available slots. When you pick one, it's held for **60 minutes** while you complete the booking. This is longer than walk-in because you're still on the phone or in conversation with the customer
|
||||
5. **Review the booking** — check all the details are correct
|
||||
6. **Submit** — the booking is created
|
||||
|
||||
### The Difference Between Walk-In and Call-In
|
||||
|
||||
The only real difference is the **hold time** on the slot:
|
||||
- Walk-in: 5 minutes (the customer is standing right there, so you'll be quick)
|
||||
- Call-in: 60 minutes (you might need time to discuss options with the customer on the phone)
|
||||
|
||||
---
|
||||
|
||||
## Customer Profile
|
||||
|
||||
When you click on a customer's name (from the Customers list or from a booking), their full profile opens. This is your complete view of everything about that customer.
|
||||
|
||||
### Personal Details
|
||||
|
||||
- Name, email, phone number, date of birth
|
||||
- Profile photo (if they've uploaded one)
|
||||
- When they registered and when they last logged in
|
||||
- A notes field where you can add any information about the customer (allergies, preferences, etc.)
|
||||
|
||||
### Appointment History
|
||||
|
||||
A complete list of every appointment this customer has ever had with the salon — past and upcoming. For each one, you can see:
|
||||
- Date and time
|
||||
- Services
|
||||
- Price
|
||||
- Current status
|
||||
|
||||
### Customer Relationship Summary
|
||||
|
||||
This section gives you a quick overview of the customer's history with the salon:
|
||||
- **Total spend** — how much they've paid across all completed appointments
|
||||
- **Total visits** — how many appointments they've completed
|
||||
- **First visit** and **last visit** dates
|
||||
- **Average visits per month** — how regularly they come in
|
||||
- **Top 5 most booked services** — which treatments they get most often
|
||||
- **Notes history** — a log of changes you've made to the customer's notes over time
|
||||
|
||||
### Patch Tests
|
||||
|
||||
This section shows the customer's patch test history.
|
||||
|
||||
**View existing patch tests:**
|
||||
- See which patch tests the customer has had and when they were done
|
||||
|
||||
**Record a new patch test:**
|
||||
- Select which patch test was completed (for example, "Gel Allergy Test")
|
||||
- Add notes if needed (for example, "2-minute walk-in patch test — no reaction")
|
||||
- This is especially useful for walk-in customers who get a quick patch test done on the spot
|
||||
|
||||
**Why this matters:**
|
||||
- When you're booking for this customer, the system uses their patch test records to determine which services they're eligible for. If they need a patch test for a service and don't have one on record, that service will be greyed out.
|
||||
|
||||
### Loyalty and Referrals
|
||||
|
||||
- **Loyalty stamps** — their current stamp count
|
||||
- **Referral code** — their unique code they can share with friends
|
||||
- **Referral history** — who they've referred and who referred them
|
||||
|
||||
### Privacy and Consent
|
||||
|
||||
- Whether the customer has agreed to the privacy policy and terms
|
||||
- Whether they've agreed to data retention
|
||||
- When these consents were last updated
|
||||
|
||||
---
|
||||
|
||||
## Booking Details Window
|
||||
|
||||
When you click on any booking (from the Bookings list or from Today's appointments), a window opens showing everything about that booking.
|
||||
|
||||
### What You See
|
||||
|
||||
- Customer name and contact details
|
||||
- Date and time of the appointment
|
||||
- How long it lasts
|
||||
- The services included and their prices
|
||||
- The total price
|
||||
- Any notes on the booking
|
||||
- The current status
|
||||
|
||||
### Actions You Can Take
|
||||
|
||||
**Approve** — If the booking is pending, this confirms it. The appointment moves into the confirmed schedule.
|
||||
|
||||
**Cancel** — Cancels the booking. If the booking was pending, it's simply removed. If it was already confirmed, it's marked as cancelled by the salon and a note is made of the cancellation.
|
||||
|
||||
**Edit** — Opens the Edit Booking window (described above) where you can change the services, prices, durations, and notes.
|
||||
|
||||
---
|
||||
|
||||
## Approval Window
|
||||
|
||||
When you click **Decline** on a pending booking, or when you choose to confirm one, the Approval window appears.
|
||||
|
||||
**Confirm** — Accepts the booking. It becomes confirmed and appears in the schedule.
|
||||
|
||||
**Decline** — Rejects the booking. It's cancelled and marked as cancelled by the salon.
|
||||
|
||||
The window shows the full booking details so you can review them before making your decision.
|
||||
|
||||
---
|
||||
|
||||
## Uploading Portfolio Images
|
||||
|
||||
The portfolio is the salon's gallery of nail art photos that customers can browse on the website.
|
||||
|
||||
### How to Upload
|
||||
|
||||
1. Go to the **Admin** dashboard
|
||||
2. Find the image upload section
|
||||
3. **Drag and drop** an image file, or click to select one from your device
|
||||
4. A preview of the image appears so you can check it looks right
|
||||
5. **Add tags** — type keywords that describe the image (for example, "french tip", "red", "summer", "glitter"). As you type, the system suggests tags that have been used before, so you can keep tags consistent
|
||||
6. A confirmation window appears before the image is uploaded — this is your last chance to double-check
|
||||
7. **Confirm** — the image is uploaded and appears in the portfolio gallery
|
||||
|
||||
### Tips for Tagging
|
||||
|
||||
- Use consistent tag names — the system suggests previously used tags to help with this
|
||||
- You can use the keyboard to navigate tag suggestions quickly
|
||||
- Tags are how customers find images in the gallery, so be descriptive
|
||||
|
||||
---
|
||||
|
||||
## Time Blockers
|
||||
|
||||
Time blockers are periods when the salon is unavailable for bookings — for example, a staff meeting, a personal appointment, or a lunch break.
|
||||
|
||||
### How to Add a Time Blocker
|
||||
|
||||
1. Go to the **Admin** dashboard
|
||||
2. Find the time blockers section
|
||||
3. Set the **start date and time**
|
||||
4. Set the **duration** (how long the block lasts)
|
||||
5. Add a **description** (for example, "Staff meeting" or "Lunch break")
|
||||
6. Save
|
||||
|
||||
The blocked period will no longer appear as available when customers or you are booking appointments.
|
||||
|
||||
### One-Off vs. Recurring
|
||||
|
||||
- **One-off blockers** happen once — for example, "Dentist appointment on 15th March at 2pm"
|
||||
- **Recurring blockers** happen on a schedule — for example, "Lunch break every Wednesday from 1pm to 2pm". You set a pattern and the system applies it automatically
|
||||
|
||||
### Removing a Time Blocker
|
||||
|
||||
Simply delete it from the list. The time slot becomes available for booking again.
|
||||
|
||||
---
|
||||
|
||||
## How Booking Statuses Work
|
||||
|
||||
Every booking has a status that tells you where it is in its lifecycle. Here's what each one means and how it changes.
|
||||
|
||||
### The Status Flow
|
||||
|
||||
```
|
||||
Pending → Confirmed → In Progress → Completed
|
||||
```
|
||||
|
||||
A booking can also branch off to:
|
||||
- **Cancelled by customer** — the customer cancelled
|
||||
- **Cancelled by salon** — you cancelled
|
||||
- **No-show** — the customer cancelled less than 24 hours before the appointment
|
||||
- **Re-schedule** — the booking time was changed
|
||||
|
||||
### Automatic Status Changes
|
||||
|
||||
Some statuses change on their own:
|
||||
- **Pending → Confirmed**: This doesn't happen automatically — you need to approve pending bookings
|
||||
- **Confirmed → In Progress**: Happens automatically when the appointment's start time arrives
|
||||
- **In Progress → Completed**: Happens automatically when the appointment's duration has elapsed
|
||||
|
||||
### Manual Status Changes
|
||||
|
||||
You change statuses by taking action:
|
||||
- **Approve** a pending booking → it becomes confirmed
|
||||
- **Decline** a pending booking → it's cancelled by the salon
|
||||
- **Cancel** a confirmed booking → it's cancelled by the salon
|
||||
- A customer cancelling more than 24 hours before → cancelled by customer
|
||||
- A customer cancelling less than 24 hours before → no-show (unless you forgive it)
|
||||
|
||||
### What Happens When You Cancel
|
||||
|
||||
- **If the booking was pending**: It's simply deleted. No record is kept.
|
||||
- **If the booking was confirmed or in progress**: It's marked as cancelled by the salon, and a note is made of the cancellation.
|
||||
|
||||
---
|
||||
|
||||
## Reschedule Requests from Customers
|
||||
|
||||
When a customer wants to change their appointment time, they can submit a reschedule request from their account page.
|
||||
|
||||
### What You'll See
|
||||
|
||||
The request appears in the **Pending Approvals** section on the Today page. You'll see:
|
||||
- The customer's name
|
||||
- Their current appointment time
|
||||
- The new time they're requesting
|
||||
- Any notes they've added
|
||||
- Any services they want to change
|
||||
|
||||
### What You Can Do
|
||||
|
||||
**Approve** — The booking is updated to the new time and services. The customer's request is cleared.
|
||||
|
||||
**Decline** — The booking stays as it is. The customer's request is cleared and their original appointment remains unchanged.
|
||||
|
||||
### Things to Check Before Approving
|
||||
|
||||
- The new time doesn't clash with another appointment
|
||||
- The new time falls within your working hours
|
||||
- If the customer is changing services, the new total duration fits in the slot
|
||||
|
||||
### Can the Customer Withdraw Their Request?
|
||||
|
||||
Yes — a customer can cancel their own reschedule request at any time before you've reviewed it.
|
||||
|
||||
---
|
||||
|
||||
## The Deposit System (Admin View)
|
||||
|
||||
### How Deposits Work
|
||||
|
||||
When a customer cancels less than 24 hours before their appointment (and you don't forgive it), they receive **3 deposit obligations**. This is tracked on their account.
|
||||
|
||||
### What This Means
|
||||
|
||||
- Customers with outstanding deposits can only book appointments that are **at least 24 hours away**
|
||||
- They can only have **one active booking at a time**
|
||||
- Each time they complete an appointment and pay, their deposit count goes down by 1
|
||||
- Once all deposits are cleared, they can book normally again
|
||||
|
||||
### When Creating a Booking Manually
|
||||
|
||||
When you create a booking for a customer (walk-in or call-in), the system checks whether they have outstanding deposits. If they do, it applies the same restrictions.
|
||||
|
||||
**However**, you can **bypass deposit checks** when creating a booking manually. There's an option to override the deposit rules — useful if you want to book a customer in despite them having outstanding deposits (for example, if you've spoken to them and agreed on an arrangement).
|
||||
|
||||
### Forgiving a Late Cancellation
|
||||
|
||||
When a customer cancels late, you can choose to forgive the penalty. If you do:
|
||||
- The cancellation is treated as a normal cancellation (no deposit obligations)
|
||||
- The booking is marked as "cancelled by customer" rather than "no-show"
|
||||
|
||||
---
|
||||
|
||||
## Loyalty Stamps (Admin View)
|
||||
|
||||
### How Stamps Are Earned
|
||||
|
||||
- Customers earn **1 stamp** for each completed appointment
|
||||
- Maximum **1 stamp per day** — even if a customer has multiple appointments on the same day, they only get one stamp
|
||||
- Appointments with a total price of £0 don't earn stamps
|
||||
|
||||
### How Discounts Are Applied
|
||||
|
||||
- At **10 stamps**, a discount is automatically set up
|
||||
- The discount is **10% off** the customer's next completed appointment
|
||||
- After the discount is used, stamps reset to 0 and the cycle starts again
|
||||
- Discounts expire after **6 months** if not used
|
||||
|
||||
### What You Can See
|
||||
|
||||
On a customer's profile, you can see their current stamp count and any pending or applied discounts.
|
||||
@@ -1,822 +0,0 @@
|
||||
**Last Updated:** May 2026 — Booking edit modal for /today page, 306/308 tests passing
|
||||
> **Status:** Work in Progress
|
||||
|
||||
---
|
||||
|
||||
## Project Checklist
|
||||
|
||||
### Backend (Go / Chi / Postgres)
|
||||
|
||||
#### Authentication & Authorization
|
||||
- [x] JWT authentication (login, refresh, role verification)
|
||||
- [x] User registration with input validation
|
||||
- Names: 1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot
|
||||
- Phone: UK format → E.164 (+44...)
|
||||
- Email: standard format
|
||||
- Age: Must be 16+ years
|
||||
- [x] Password hashing with bcrypt
|
||||
- [x] Middleware for auth/roles (`mw.RequireAuth`, `mw.RequireAdmin`)
|
||||
- [x] DB connection pooling
|
||||
- [x] Refresh token endpoint - Wired to `POST /api/refresh-token`, auto-refresh in frontend
|
||||
- [x] Login rate limiting (1 attempt per 5 seconds)
|
||||
- [x] Global rate limiting middleware (per-endpoint: 120/min public, 10/min register, 60/min filters, none admin)
|
||||
- [x] Security headers (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection)
|
||||
- [x] **Image metadata stripping** - All EXIF/GPS stripped on upload via `imaging` library (security)
|
||||
- [x] Service eligibility system - Age and patch test filtering for bookings
|
||||
- Uses dedicated `patch_tests` table (notice_period_hours, expiry_months)
|
||||
- `/api/services` - Returns services with eligibility for authenticated users
|
||||
- `/api/services/eligible-for/{user_id}` - Returns services with eligibility for specific user (admin booking flows)
|
||||
- Age < minimum_age_required → Service EXCLUDED
|
||||
- Patch test required + no record → Service GRAYED OUT
|
||||
- Patch test within notice period (24h) → Service GRAYED OUT
|
||||
- Patch test expired → Service GRAYED OUT
|
||||
- Patch test valid / not required → Normal
|
||||
- [x] **Patch test validation on booking** - Both user and admin booking creation/editing validates patch test requirements
|
||||
- `/api/services` - Returns services with eligibility for authenticated users
|
||||
- `/api/services/eligible-for/{user_id}` - Returns services with eligibility for specific user (admin booking flows)
|
||||
- Age < minimum_age_required → Service EXCLUDED
|
||||
- Patch test required + no record → Service GRAYED OUT
|
||||
- Patch test expired → Service GRAYED OUT
|
||||
- Patch test valid / not required → Normal
|
||||
- [ ] **Strict-Transport-Security (HSTS)** - Tell browsers to only access via HTTPS, prevents downgrade attacks. Add after HTTPS is working in prod.
|
||||
- [ ] **Referrer-Policy** - Track referrer sources for analytics (social media tracking). Use `strict-origin-when-cross-origin` to send origin but not full URLs.
|
||||
- [ ] **Rate limiter + Cloudflare** - Currently doesn't read CF-Connecting-IP header, so behind Cloudflare all users share one rate limit bucket.
|
||||
- [ ] **Account creation spam** - Registration endpoint (10/min) could benefit from additional bot protection beyond rate limiting.
|
||||
- [x] Input validation on all endpoints:
|
||||
- Registration: name (1-50), email (255), phone (20), password (72), age 16+
|
||||
- Services: name (100), price (>0), duration (1-480), patch test (0-168), age (0-100)
|
||||
- Portfolio: tags/filters (256 char max), filter category validation, image ID pattern security
|
||||
|
||||
#### Booking System
|
||||
- [x] `/api/bookings` - Full CRUD for authenticated users AND guest users (via `user_id` in payload, OptionalAuth middleware)
|
||||
- [x] `/api/admin/bookings` - List, search, create for user, progress, confirm, cancel
|
||||
- [x] `/api/admin/bookings/search` - Search functionality
|
||||
- [x] `/api/admin/bookings/user/{user_id}` - User-specific bookings
|
||||
- [x] `/api/admin/bookings/{id}/progress` - Progress booking status
|
||||
- [x] `/api/admin/bookings/{id}/confirm` - Confirm booking
|
||||
- [x] `/api/admin/bookings/{id}/cancel` - Cancel booking
|
||||
- [x] **Admin edit booking** - PUT `/api/admin/bookings/{id}` to edit start time
|
||||
- Blocks editing completed or cancelled bookings
|
||||
- Checks for overlapping bookings
|
||||
- Allows exceptional hours (with warning)
|
||||
- Clears pending edit requests on edit
|
||||
- [x] **Admin edit booking services** - PUT `/api/admin/bookings/{id}` (services endpoint) to replace services, apply price/duration overrides, and update notes
|
||||
- Validates service IDs, overlap with next booking, rejected statuses
|
||||
- Transactional: deletes old services, inserts new with overrides
|
||||
- Admin-only, accessed from `/today` → Next Appointment → Edit button
|
||||
- [x] **In-progress auto-infer** - Status auto-sets based on time (confirmed → in_progress → completed)
|
||||
- [x] **Auto-complete** - Bookings auto-complete when duration elapses
|
||||
- [x] **Slot reservation system** - `POST /api/bookings/reserve` (public, OptionalAuth), `POST /api/admin/bookings/reserve` (admin)
|
||||
- 4 TTL types: user=1h, anon=10min, walkin=5min, callin=1h
|
||||
- Anonymous cap: 50 reservations per 10-minute rolling window
|
||||
- Stored as `time_blockers` with `RESERVATION:*` description
|
||||
- `CleanupOldReservations()` prunes expired entries
|
||||
- [x] **Guest booking flow** - `POST /api/users/guest` creates disposable accounts
|
||||
- Email uniqueness: partial unique index `WHERE account_role != 'guest'`
|
||||
- If guest uses registered email → 409 "Please log in"
|
||||
- Guest bookings bypass deposit and patch-test checks
|
||||
- `AnonymizeStaleGuestAccounts()` scrubs PII 6 months after booking start_time
|
||||
- [x] **Chi router fix** - Flattened `/bookings` sub-Route to explicit paths to prevent RequireAuth bleeding into OptionalAuth POST endpoints
|
||||
- [x] **Reservation self-block fix** - `GetTimeBlockersInRange` excludes `RESERVATION:*` entries so overlap checks don't reject the user's own reservation
|
||||
|
||||
#### Admin Endpoints
|
||||
- [x] `/api/admin/services` - Create, delete, list, toggle
|
||||
- [x] `/api/admin/users` - List, view with booking history
|
||||
- [x] `/api/admin/today` - Current/next appointment, today's appointments, pending approvals
|
||||
- [x] `/api/admin/notifications` - GET/acknowledge endpoint wired, but:
|
||||
- [x] **Pending notification acknowledgment** - Acknowledged on confirm/cancel (not deleted)
|
||||
- [x] **Cancelled booking notification** - Created when cancelling non-pending bookings
|
||||
- [ ] Frontend UI to display notifications
|
||||
- [ ] Push mechanism (currently only pull-based)
|
||||
- [ ] User notifications (only admin notifications exist)
|
||||
- [ ] User notification preferences (DB table ready)
|
||||
|
||||
#### Scheduling System
|
||||
- [x] `/api/scheduling/default-hours` - GET public, PUT admin
|
||||
- [x] `/api/scheduling/exceptional-groups` - CRUD for holiday/special hours
|
||||
- [x] `/api/scheduling/working-hours` - Merged default + exceptional hours
|
||||
- [x] `/api/scheduling/available-hours` - Available slots accounting for bookings
|
||||
|
||||
#### User Endpoints
|
||||
- [x] `/api/user/profile` - GET, PUT
|
||||
- [x] `/api/user/profile-picture` - POST upload profile picture (separate bucket)
|
||||
- [x] `/api/user/account` - DELETE (GDPR compliant)
|
||||
- [x] `/api/user/loyalty` - GET loyalty stamps
|
||||
- [x] `/api/contact` - Public endpoint returning first admin's contact info (name, phone, email, profilePicUrl)
|
||||
- [ ] **GDPR data export** - `export_all_user_data()` exists but not wired to endpoint
|
||||
- [ ] **Tax data export** - Admin endpoint for tax-software-compatible format
|
||||
|
||||
#### Deposits System (Simplified)
|
||||
- [x] `users.deposits_required` INT (0-3) tracks outstanding deposit obligations
|
||||
- [x] **24h late cancellation rule**: < 24h without forgiveness = `no_show` + 3 deposits (resets to 3, not +=), ≥ 24h = normal cancellation
|
||||
- [x] Optional `forgive_no_show` boolean at cancellation - admin can forgive penalty case-by-case, counts as `client_cancelled`
|
||||
- [x] Optional `enforce_deposits` boolean in admin booking creation - admin can bypass deposit checks when needed
|
||||
- [x] Reduces by 1 when booking completes with payment
|
||||
- [x] 2+ unforgiven no-shows in 6 months = 3 deposits (blocks new bookings)
|
||||
- [x] Deleted: `ForgiveNoShowsForUser()` function (now per-cancellation forgiveness)
|
||||
- [x] Implementation complete in `bookings.go` (24h threshold, forgiveness logic) and `manage.go` (admin deposit enforcement)
|
||||
- [x] **Deposit users must book ≥24h in advance** (changed from 48h); guests bypass this check
|
||||
- [ ] Frontend display of deposits_required status to users
|
||||
|
||||
#### CalDAV Contact Sync
|
||||
- [x] Profile photos synced to CardDAV contacts (PHOTO field in vCard)
|
||||
- [x] Auto-updates when profile is changed
|
||||
|
||||
#### Not Yet Wired
|
||||
- [ ] Social auth (`handlers/auth/social.go` exists, not imported)
|
||||
- [ ] Analytics (`handlers/admin/analytics.go` exists, not imported)
|
||||
- [x] Portfolio/images - NOW WIRED: `/api/portfolio/images`, `/api/portfolio/tags`, `/api/portfolio/filters`, `/api/portfolio/images/{id}`
|
||||
- [ ] **Guest user endpoint** - ✅ DONE: `POST /api/users/guest` creates disposable accounts, partial unique email index
|
||||
|
||||
#### Unit Tests & CI/CD
|
||||
- [x] 306/308 tests passing across all handler packages
|
||||
- [x] TestMain per package — schema migration runs once per package (not per test)
|
||||
- [x] TruncateTables() between tests — TRUNCATE CASCADE, ~60% faster than DROP+CREATE
|
||||
- [x] Test output streamed in real-time via `tee` in local-dev-2.sh
|
||||
- [ ] CI/CD pipeline
|
||||
|
||||
---
|
||||
|
||||
### Frontend (SvelteKit / Tailwind / shadcn)
|
||||
|
||||
#### Core Pages
|
||||
- [x] Home (`/`)
|
||||
- [x] Prices (`/prices`)
|
||||
- [x] Contact (`/contact`) - Dynamic, fetches from `/api/contact`
|
||||
- [x] Book (`/book`) - Full wizard with service selection, date/time, customer details
|
||||
- [x] Portfolio (`/portfolio`) - S3/R2 storage with tag filtering, category filters, pagination, ?img= featured image, admin upload
|
||||
- [x] Today (`/today`) - Admin only, real-time schedule view with auto-status transitions
|
||||
- [x] Schedule (`/schedule`) - User's upcoming bookings with .ics export and calendar download button
|
||||
- [x] Account (`/account`) - Profile management, profile picture upload with cropper, loyalty stamps display
|
||||
- [x] Login (`/login`)
|
||||
- [x] Manage (`/manage`)
|
||||
|
||||
#### Admin Dashboard (`/admin`)
|
||||
- [x] Auth guard with role check
|
||||
- [x] ImageUpload component
|
||||
- [x] UsersCard + UserModal + PatchTestModal
|
||||
- [x] BookingsCard + BookingModal
|
||||
- [x] HolidayHours (exceptional hours management)
|
||||
- [x] WeeklySchedule (default hours management)
|
||||
- [x] ServicesManagement
|
||||
- [x] BookingCreateModal (call-in/admin booking creation)
|
||||
- [x] WalkInBooking + WalkInCreateModal
|
||||
- [x] CallInBooking
|
||||
- [x] ApprovalModal
|
||||
|
||||
#### Booking Flow
|
||||
- [x] Service selection with pricing/duration
|
||||
- [x] **Service eligibility display** - Gray out services requiring patch test or below minimum age
|
||||
- [x] Calendar with availability detection
|
||||
- [x] Time slot generation with gap logic
|
||||
- [x] Customer details form (guest or authenticated)
|
||||
- [x] Auth store with token refresh logic
|
||||
- [x] **Admin booking flows** - Call-in and walk-in use `/api/services/eligible-for/{user_id}` for user-specific eligibility
|
||||
- [x] **Manual patch test entry** - Admin can record patch test completion via User Details → Patch Test modal (for 2-minute walk-in patch tests)
|
||||
- [x] **Customer booking submit** - `submitBooking()` creates guest account if unauthenticated, then POSTs to `/api/bookings`
|
||||
- [ ] Remove debug `console.log` calls from BookingFlow.svelte
|
||||
- [ ] Payment integration (Square placeholder)
|
||||
|
||||
#### API Integration
|
||||
- [x] Services fetch from `/api/services`
|
||||
- [x] Working hours fetch from `/api/scheduling/working-hours`
|
||||
- [x] Available hours fetch from `/api/scheduling/available-hours`
|
||||
- [x] Admin bookings use `/api/admin/bookings`
|
||||
- [x] Guest user creation (`/api/users/guest`) — creates disposable accounts, enforces email uniqueness for registered users only
|
||||
|
||||
---
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- [x] `.env` config
|
||||
- [x] Docker Compose (postgres, backend, sabredav, nginx)
|
||||
- [x] Static frontend build served via nginx
|
||||
- [ ] nginx reverse proxy - config under review
|
||||
- [ ] Monitoring/logging - no stack configured
|
||||
- [ ] CI/CD pipeline (Gitea) - not yet defined
|
||||
- [ ] Prometheus metrics integration
|
||||
|
||||
---
|
||||
|
||||
### Integrations
|
||||
|
||||
- [x] CardDAV sync for contacts (SabreDAV)
|
||||
- [x] CalDAV ready
|
||||
- [x] Profile pics bucket - separate bucket `crussell-profile-pics` for user profile pictures
|
||||
- [ ] Email/SMS reminders - not yet implemented
|
||||
- [ ] Square payment - placeholder only
|
||||
- [x] S3/R2 image hosting - Rustfs for dev, Cloudflare R2 for prod via build tags
|
||||
|
||||
---
|
||||
|
||||
## Testing Infrastructure
|
||||
|
||||
Crussell has a comprehensive Go testing infrastructure located in `backend/testutils/`:
|
||||
|
||||
### Test Utilities
|
||||
|
||||
| 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 (users, services, bookings) |
|
||||
| **Validators** | `internal/validators/validators.go` | ID validation utilities |
|
||||
|
||||
### Test Files
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go test ./...
|
||||
go test -v ./handlers/bookings
|
||||
go test -v -run "TestBooking" ./...
|
||||
```
|
||||
|
||||
### Test Database
|
||||
|
||||
- DSN: `postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable`
|
||||
- Override: `export POSTGRES_USER=myuser POSTGRES_PASSWORD=mypassword POSTGRES_HOST=localhost POSTGRES_DB=crussell_test GO_TESTING=1`
|
||||
- Each package has a `TestMain` that runs `testdb.Migrate()` once at package level
|
||||
- Tables truncated between tests via `testdb.TruncateTables()` (TRUNCATE CASCADE)
|
||||
- Tests run sequentially (`-p 1`) — all packages share same test database
|
||||
|
||||
### Conventions
|
||||
|
||||
- All test files use `//go:build test` build tag
|
||||
- Test tokens use fixed secret: `test-secret-key-for-testing-only`
|
||||
- Fixtures auto-generate unique emails to avoid conflicts
|
||||
- `-count=N` is safe — each count re-runs TestMain + all tests (good for flaky test detection)
|
||||
|
||||
### Recent Testing Updates (May 2026)
|
||||
|
||||
**Booking Edit Modal + Endpoint Tests:**
|
||||
- **`PUT /api/admin/bookings/{id}`** — New endpoint for replacing booking services with overlap detection, status validation, and transactional updates. 20 new tests covering success paths (replace/add/remove services, price/duration overrides, notes), validation errors (invalid IDs, empty services, negative price, zero duration), status rejections (completed/cancelled/no_show), overlap detection, response shape, and edge cases (pending/in_progress bookings).
|
||||
|
||||
**Major Test Infrastructure Overhaul:**
|
||||
- **TestMain per package** — Schema migration (DROP+CREATE) now runs once per package instead of once per test. 10 package-level TestMain functions across 10 test packages.
|
||||
- **Truncate-only between tests** — Per-test setup changed from full schema rebuild to `TRUNCATE TABLE ... CASCADE` only. ~60% reduction in test DB setup time.
|
||||
- **3 missing tables added to TruncateTables** — `booking_edit_requests`, `exceptional_group_applications`, `business_settings` were previously relying on implicit CASCADE cleanup.
|
||||
- **discount_test.go moved to package bookings** — Was external test package (`bookings_test`), now shares TestMain with other booking tests.
|
||||
- **Dead code removed** — `truncateDiscountTables()` helper (never called) deleted.
|
||||
- **Flaky test fixes** — `TestAdminReserveSlot_WalkIn_Success` and `TestAdminReserveSlot_ReplacesExisting` used `time.Now()` which could fall after working hours. Fixed to use noon tomorrow.
|
||||
- **local-dev-2.sh** — Test output now streamed in real-time via `tee` instead of captured silently.
|
||||
|
||||
**Test Coverage: 306/308 passing** (was 286/288)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Overview Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User([Customer])
|
||||
Admin([Admin])
|
||||
|
||||
subgraph Docker[Docker Compose Stack]
|
||||
subgraph NGINX[Nginx :80/:443]
|
||||
Static[Static Frontend Build]
|
||||
Proxy[API Proxy → Backend:8080]
|
||||
DAVProxy[DAV Proxy → SabreDAV]
|
||||
end
|
||||
|
||||
subgraph Backend[Go + Chi :8080]
|
||||
Router[chi Router]
|
||||
Auth[JWT Middleware]
|
||||
Handlers[API Handlers]
|
||||
end
|
||||
|
||||
subgraph Database[PostgreSQL :5432]
|
||||
DB[(Users / Bookings<br/>Payments / Services<br/>Scheduling)]
|
||||
end
|
||||
|
||||
subgraph DAV[SabreDAV :9000]
|
||||
CardDAV[(vCard Contacts)]
|
||||
CalDAV[(Calendar Events)]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph External[External Services]
|
||||
S3[S3/R2 Storage ✅]
|
||||
Gmail[Gmail SMTP ❌]
|
||||
SquareAPI[Square API ❌]
|
||||
end
|
||||
|
||||
User -->|HTTPS| NGINX
|
||||
Admin -->|HTTPS| NGINX
|
||||
Static --> User
|
||||
Proxy --> Router
|
||||
Router --> Auth
|
||||
Auth --> Handlers
|
||||
Handlers --> DB
|
||||
Handlers --> DAV
|
||||
Handlers --> S3
|
||||
Handlers -.->|TODO| Gmail
|
||||
Handlers -.->|TODO| SquareAPI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Public Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/services` | List active services (with eligibility for authenticated users) |
|
||||
| GET | `/api/services/eligible-for/{user_id}` | List services filtered by user's age and patch test status (admin only) |
|
||||
| POST | `/api/register` | Create new user account |
|
||||
| POST | `/api/login` | Authenticate and receive JWT |
|
||||
| POST | `/api/verify/generate` | Generate email verification or password reset code |
|
||||
| POST | `/api/verify/check` | Verify code (email verification or password reset) |
|
||||
| POST | `/api/users/guest` | Create disposable guest account (email unique for non-guests only) |
|
||||
| GET | `/api/contact` | Get business contact info (from first admin user) |
|
||||
| GET | `/api/scheduling/default-hours` | Get weekly default hours |
|
||||
| GET | `/api/scheduling/exceptional-groups` | List holiday/special hour groups |
|
||||
| GET | `/api/scheduling/working-hours` | Get merged working hours for date range |
|
||||
| GET | `/api/scheduling/available-hours` | Get available booking slots (triggers reservation cleanup + guest anonymization) |
|
||||
| POST | `/api/bookings/reserve` | Reserve a slot temporarily (OptionalAuth; user=1h TTL, anon=10min TTL, 50-cap) |
|
||||
| POST | `/api/bookings` | Create booking (OptionalAuth; accepts `user_id` for guest bookings) |
|
||||
|
||||
### Authenticated User Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/user/profile` | Get current user profile |
|
||||
| PUT | `/api/user/profile` | Update profile |
|
||||
| POST | `/api/user/profile-picture` | Upload profile picture with cropper |
|
||||
| DELETE | `/api/user/account` | Delete account (GDPR) |
|
||||
| GET | `/api/user/loyalty` | Get loyalty stamp count |
|
||||
| GET | `/api/bookings` | List user's bookings |
|
||||
| POST | `/api/bookings` | Create booking |
|
||||
| GET | `/api/bookings/{id}` | Get specific booking |
|
||||
| GET | `/api/bookings/{id}/calendar` | Download ICS calendar file |
|
||||
| PUT | `/api/bookings/{id}` | Update booking |
|
||||
| DELETE | `/api/bookings/{id}` | Cancel booking |
|
||||
|
||||
### Admin Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/admin/services` | List all services |
|
||||
| POST | `/api/admin/services` | Create service |
|
||||
| DELETE | `/api/admin/services/{id}` | Delete service |
|
||||
| PUT | `/api/admin/services/{id}/toggle` | Toggle active status |
|
||||
| GET | `/api/admin/bookings` | List all bookings |
|
||||
| POST | `/api/admin/bookings` | Create booking for user |
|
||||
| GET | `/api/admin/bookings/search` | Search bookings |
|
||||
| GET | `/api/admin/bookings/user/{user_id}` | User's bookings |
|
||||
| PUT | `/api/admin/bookings/{id}/progress` | Progress status |
|
||||
| PUT | `/api/admin/bookings/{id}` | Update booking services, overrides, and notes |
|
||||
| POST | `/api/admin/bookings/{id}/confirm` | Confirm booking |
|
||||
| POST | `/api/admin/bookings/{id}/cancel` | Cancel booking |
|
||||
| POST | `/api/admin/bookings/reserve` | Reserve slot for admin booking (walkin=5min TTL, callin=1h TTL) |
|
||||
| GET | `/api/admin/users` | List users |
|
||||
| GET | `/api/admin/users/{id}` | Get user details |
|
||||
| GET | `/api/admin/users/{id}/patch-tests/eligible` | Get services requiring patch test that user hasn't completed |
|
||||
| POST | `/api/admin/users/{id}/patch-tests` | Record patch test completion for user |
|
||||
| GET | `/api/admin/today/current-next` | Current and next appointment |
|
||||
| GET | `/api/admin/today/appointments` | Today's appointments |
|
||||
| GET | `/api/admin/today/pending-approvals` | Pending approval queue |
|
||||
| GET | `/api/admin/notifications` | List notifications |
|
||||
| POST | `/api/admin/notifications/{id}/acknowledge` | Acknowledge |
|
||||
| PUT | `/api/scheduling/default-hours` | Update weekly hours |
|
||||
| POST | `/api/scheduling/exceptional-groups` | Create exception group |
|
||||
| DELETE | `/api/scheduling/exceptional-groups` | Delete exception group |
|
||||
| PUT | `/api/scheduling/exceptional-applications` | Apply exceptions to dates |
|
||||
|
||||
### Portfolio Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/portfolio/images` | List images with pagination, filter by tag/tags, filter by category:value |
|
||||
| GET | `/api/portfolio/tags` | List all tags for autocomplete (public) |
|
||||
| GET | `/api/portfolio/filters` | Get filter categories with counts. Supports tag filtering. Unselected categories show counts reduced by other filters. |
|
||||
| GET | `/api/portfolio/images/{id}` | Get single image by UUID or timestamp (fallback to URL pattern match) |
|
||||
| POST | `/api/portfolio/images` | Upload new image with thumbnail and tags (admin only) |
|
||||
| DELETE | `/api/portfolio/images/{id}` | Delete image (admin only) |
|
||||
|
||||
**Portfolio Frontend Features:**
|
||||
- `/portfolio` page with tag-based filtering and category filters
|
||||
- Category filters: `?filter[color]=red&filter[season]=summer`
|
||||
- Tag search: `?tag=toby` or `?tags=toby,summer`
|
||||
- Featured image: `?img=<id|timestamp>` - loads image directly, bypasses filters
|
||||
- Filter UI: scrollable dropdowns, keyboard navigation, mobile-optimized
|
||||
- Admin upload: ImageUpload component with live tag suggestions, keyboard nav, confirmation modal
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Enums
|
||||
|
||||
```sql
|
||||
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
|
||||
verification_purpose: email_verify | password_reset
|
||||
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
|
||||
```
|
||||
|
||||
**Email uniqueness**: Partial unique index `idx_users_email_registered ON users (email) WHERE account_role != 'guest'` — guests can share emails, registered users cannot.
|
||||
|
||||
**Suggested additional `admin_notification_reason` values:**
|
||||
|
||||
| Reason | Purpose |
|
||||
| --------------------- | ------------------------------------------------------------------------ |
|
||||
| `payment_failed` | Payment processing failed |
|
||||
| `patch_test_due` | Customer needs patch test before appointment |
|
||||
| `first_time_customer` | New customer's first booking |
|
||||
| `inactive_customer` | Regular hasn't booked in X months - 5% discount (non stacking) |
|
||||
| `birthday_this_week` | Customer birthday - 5% discount (stacking) |
|
||||
| `schedule_conflict` | Potential double-booking detected - admin alert, 'second customer' alert |
|
||||
|
||||
### Core Tables
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | User accounts with profile data (partial unique email: WHERE account_role != 'guest') |
|
||||
| `verification_codes` | Email verification and password reset codes |
|
||||
| `user_social_logins` | Social auth provider links |
|
||||
| `services` | Service offerings |
|
||||
| `patch_tests` | Patch test definitions (notice_duration_hours, expiry_months, service_ids) |
|
||||
| `user_patch_tests` | User patch test completion records (tested_at, notes) |
|
||||
| `bookings` | Appointment records |
|
||||
| `booking_services` | Services per booking |
|
||||
| `booking_edit_requests` | Pending customer edit requests |
|
||||
| `time_blockers` | Admin time blocks + slot reservations (description LIKE 'RESERVATION:%') |
|
||||
| `user_referrals` | Referral tracking |
|
||||
| `working_hours` | Default weekly schedule |
|
||||
| `exceptional_working_hours_groups` | Holiday/special hour groups |
|
||||
| `exceptional_working_hours` | Hours for exception groups |
|
||||
| `exceptional_group_applications` | Apply exceptions to date ranges |
|
||||
| `payments` | Payment transactions |
|
||||
| `business_settings` | Business configuration |
|
||||
| `admin_notifications` | Admin notification queue |
|
||||
| `images` | Portfolio gallery images with tags |
|
||||
| `tags` | Image tag autocomplete |
|
||||
| `user_notification_preferences` | User notification preferences (email/sms/push) |
|
||||
|
||||
### Key Functions
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `generate_short_id()` | Generate 12-char IDs |
|
||||
| `anonymize_user()` | GDPR data removal |
|
||||
| `delete_guest_user()` | Clean up guest accounts |
|
||||
| `export_all_user_data()` | GDPR subject access |
|
||||
| `get_monthly_business_summary()` | Analytics |
|
||||
| `get_vat_return_data()` | VAT reporting |
|
||||
| `calculate_vat()` | VAT calculation |
|
||||
| `get_receipt_data()` | Receipt generation |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Work
|
||||
|
||||
### High Priority
|
||||
|
||||
| Task | Description | Files Affected |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------- |
|
||||
| **Remove console.logs** | Debug logs left in: `BookingFlow.svelte` | Frontend components |
|
||||
| ~~Customer booking submit~~ | ~~`submitBooking()` only logs, needs `POST /api/bookings`~~ DONE | `frontend/src/lib/components/booking/BookingFlow.svelte` |
|
||||
| ~~Guest user endpoint~~ | ~~Create `/api/users/guest` for walk-in bookings~~ DONE | `backend/handlers/user/guest.go` |
|
||||
| ~~In-progress auto-infer~~ | ~~Auto-set `in_progress` status based on time~~ DONE | Backend booking logic |
|
||||
| ~~Auto-complete~~ | ~~Auto-complete bookings when duration elapses~~ DONE | Backend today handlers |
|
||||
| ~~Profile picture upload~~ | ~~Upload with cropper to separate bucket, sync to CalDAV~~ DONE | Backend + Account page |
|
||||
| ~~Contact page dynamic~~ | ~~Fetch from `/api/contact` using first admin~~ DONE | Backend + Contact page |
|
||||
| ~~Simplified deposits~~ | ~~`deposits_required` INT on users, 48h check, reduce on payment~~ DONE | Backend booking logic |
|
||||
| ~~Slot reservation system~~ | ~~Temporary slot holds with 4 TTL types~~ DONE | `backend/handlers/bookings/reserve.go`, `admin_reserve.go`|
|
||||
| ~~Booking flow reservation~~ | ~~Reserve slot on Step 2→3 transition in BookingFlow~~ DONE | `frontend/src/lib/components/booking/BookingFlow.svelte` |
|
||||
| ~~Chi router conflict~~ | ~~Flattened /bookings sub-Route to prevent RequireAuth bleed~~ DONE | `backend/main.go` |
|
||||
| ~~Reservation self-block~~ | ~~Exclude RESERVATION:* from GetTimeBlockersInRange~~ DONE | `backend/handlers/scheduling/time-blockers.go` |
|
||||
| **Begin button (Today)** | Manual start for early arrivals, gray out if >3hrs away | `CurrentAppointment.svelte` + backend |
|
||||
| **One-off custom services** | Admin creates custom service for single booking without adding to main list | Backend + frontend booking modals |
|
||||
| **One-off exceptional hours** | Single-day exceptions (dentist, afternoon off) - not yearly/weekly | Backend scheduling + frontend HolidayHours |
|
||||
| **Auto lunch protection** | Block bookings that remove lunch break (1h customer, 30min admin with warning) | Backend `available-hours` logic |
|
||||
| **Walk-in slot blocking** | Properly block next available slot during walk-in intake | `WalkInCreateModal.svelte` |
|
||||
| **Square payment integration** | Full Square SDK integration | Backend payment handlers + frontend payment step |
|
||||
| **GDPR data export** | User button for "give me my data" using `export_all_user_data()` | Backend endpoint + account page |
|
||||
| **Tax data export** | Admin button for tax-software-compatible format | Backend endpoint + admin page |
|
||||
| **Frontend deposits UI** | Display `deposits_required` status to users | Account page / booking flow |
|
||||
|
||||
### Medium Priority
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| **Notifications UI** | Frontend panel to display admin notifications |
|
||||
| **Notifications push** | Real-time notification mechanism (WebSocket/polling) |
|
||||
| **User notification preferences** | [x] DB table ready, waiting on user notification system |
|
||||
| **User notifications** | Notification system for regular users (booking confirmations, reminders) |
|
||||
| **Remove debug logs** | `console.log` in BookingFlow.svelte:600 and BookingCreateModal.svelte:224 |
|
||||
| **Loyalty display component** | Show stamps in account/bookings |
|
||||
| **Email/SMS reminders** | Scheduled notification jobs |
|
||||
| **Prometheus metrics** | Monitoring integration |
|
||||
|
||||
### Low Priority
|
||||
|
||||
| Task | Description |
|
||||
| ----------------------- | ----------------------------------- |
|
||||
| **Social auth** | Wire `handlers/auth/social.go` |
|
||||
| **Analytics** | Wire `handlers/admin/analytics.go` |
|
||||
| **nginx config review** | Finalize production config |
|
||||
| **CI/CD pipeline** | Gitea Actions workflow |
|
||||
| And many more | |
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Purpose | Required |
|
||||
|----------|---------|----------|
|
||||
| `JWT_SECRET_KEY` | Secret for JWT signing | **Yes** |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | **Yes** |
|
||||
| `POSTGRES_USER` | Database username | Docker |
|
||||
| `POSTGRES_PASSWORD` | Database password | Docker |
|
||||
| `POSTGRES_DB` | Database name | Docker |
|
||||
| `S3_BUCKET` | Main image bucket (portfolio) | No (default: crussell) |
|
||||
| `S3_PROFILE_PICS_BUCKET` | Profile pictures bucket | No (default: crussell-profile-pics) |
|
||||
| `S3_ENDPOINT` | S3/Rustfs endpoint | Dev |
|
||||
| `S3_PUBLIC_URL` | Public URL for S3 bucket | Dev |
|
||||
| `S3_ACCESS_KEY` | S3 access key | Dev |
|
||||
| `S3_SECRET_KEY` | S3 secret key | Dev |
|
||||
| `R2_ENDPOINT` | Cloudflare R2 endpoint | Prod |
|
||||
| `R2_BUCKET` | R2 bucket name | Prod |
|
||||
| `R2_PUBLIC_URL` | R2 public URL | Prod |
|
||||
| `R2_ACCESS_KEY` | R2 access key | Prod |
|
||||
| `R2_SECRET_KEY` | R2 secret key | Prod |
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./init-scripts/init-script.sql:/docker-entrypoint-initdb.d/init-script.sql:ro
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
depends_on: [postgres]
|
||||
|
||||
sabredav:
|
||||
image: php:8.2-fpm
|
||||
volumes:
|
||||
- ./sabredav:/var/www/dav
|
||||
depends_on: [postgres]
|
||||
|
||||
nginx:
|
||||
image: nginx:stable
|
||||
ports: ["80:80", "443:443"]
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d
|
||||
- ./frontend/build:/usr/share/nginx/html
|
||||
depends_on: [backend, sabredav]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Component Structure
|
||||
|
||||
```
|
||||
src/lib/components/
|
||||
├── admin/
|
||||
│ ├── ApprovalModal.svelte
|
||||
│ ├── BookingCreateModal.svelte
|
||||
│ ├── BookingModal.svelte
|
||||
│ ├── BookingsCard.svelte
|
||||
│ ├── CallInBooking.svelte
|
||||
│ ├── EditBookingModal.svelte
|
||||
│ ├── HolidayHours.svelte
|
||||
│ ├── ImageUpload.svelte
|
||||
│ ├── PatchTestModal.svelte
|
||||
│ ├── ServicesManagement.svelte
|
||||
│ ├── UserModal.svelte
|
||||
│ ├── UsersCard.svelte
|
||||
│ ├── WalkInBooking.svelte
|
||||
│ └── WalkInCreateModal.svelte
|
||||
├── booking/
|
||||
│ ├── BookingActions.svelte
|
||||
│ ├── BookingFlow.svelte
|
||||
│ ├── BookingSummary.svelte
|
||||
│ ├── DatePicker.svelte
|
||||
│ ├── ServiceCard.svelte
|
||||
│ ├── ServiceSelector.svelte
|
||||
│ ├── StepIndicator.svelte
|
||||
│ └── TimeSlotPicker.svelte
|
||||
└── today/
|
||||
├── CurrentAppointment.svelte
|
||||
├── PendingApprovals.svelte
|
||||
└── TodayCalendar.svelte
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Holiday hours ARE integrated into all 3 booking flows (customer, call-in, walk-in) via `/api/scheduling/working-hours` and `/api/scheduling/available-hours`
|
||||
- The backend merges default hours with applied exceptional hours automatically
|
||||
- Static frontend is built and served by nginx; API calls go directly to Go backend in production
|
||||
- Local dev uses SvelteKit's API proxy for CORS avoidance
|
||||
- **GDPR functions exist in SQL** (`anonymize_user`, `export_all_user_data`, `delete_guest_user`) — backend also has `AnonymizeStaleGuestAccounts()` which scrubs guest PII 6 months after booking start_time. Still need user-facing data export endpoint and admin tax export endpoint.
|
||||
- **Notifications** are pull-based only (no push/WebSocket). Admin endpoint exists but no frontend UI. No user-facing notification system yet.
|
||||
- **Notification acknowledgment**: When a booking is confirmed, any pending notification is acknowledged. When cancelled, pending is acknowledged and cancelled_booking notification is only created if the booking was not in pending status (e.g. was confirmed or in_progress).
|
||||
- **User notification preferences**: `user_notification_preferences` table exists with email/sms/push enabled flags, waiting on user notification system to be implemented.
|
||||
- **Guest accounts**: Disposable, created via `POST /api/users/guest`. Email uniqueness enforced only for non-guests (partial unique index). Multiple guest accounts can share an email.
|
||||
- **Slot reservations**: Stored in `time_blockers` with `RESERVATION:*` descriptions. `GetTimeBlockersInRange` excludes these to prevent the reservation from blocking its own booking.
|
||||
- **Chi router**: `/bookings` routes use explicit full paths (not `r.Route("/bookings", ...)`) to prevent RequireAuth middleware from bleeding into OptionalAuth POST endpoints.
|
||||
- **Seed script** (`local-dev-2.sh`): `open_day()` skips Sunday and Saturday (matching `working_hours` schema). Guest bookings use reserve-then-book flow matching frontend.
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
./local-dev-2.sh # Creates tmux session 'crussell-dev'
|
||||
```
|
||||
|
||||
Creates 3 panes:
|
||||
- **Pane 0**: psql interactive shell
|
||||
- **Pane 1**: Backend (`go run -tags dev ./main.go`)
|
||||
- **Pane 2**: Frontend (`npm run dev -- --host`)
|
||||
|
||||
### Dev Build Tag
|
||||
|
||||
Backend uses `-tags dev` - check for dev-specific behavior in code with build constraints.
|
||||
|
||||
### Admin User Setup
|
||||
|
||||
No API endpoint exists for role promotion. Admin users are created via direct SQL:
|
||||
|
||||
```sql
|
||||
UPDATE users SET account_role = 'admin' WHERE email = 'admin@example.com';
|
||||
```
|
||||
|
||||
### Seed Data
|
||||
|
||||
Running `local-dev-2.sh` creates:
|
||||
|
||||
| Resource | Count | Details |
|
||||
|----------|-------|---------|
|
||||
| Users | 20 | 1 admin, 19 regular users |
|
||||
| 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 | Nina (+16d), Bob (+20d), Carol (+22d) — reserve-then-book flow |
|
||||
| Total Bookings | 42 | |
|
||||
| Time Blockers | 3 | Staff meeting, holiday, late start |
|
||||
| Schedule Groups | 3 | Exceptional working hour configurations |
|
||||
| Cancellations | 1 | Simulated client cancellation |
|
||||
|
||||
### API JSON Examples
|
||||
|
||||
**Create Guest Account:**
|
||||
```json
|
||||
POST /api/users/guest
|
||||
{
|
||||
"first_name": "Jane",
|
||||
"last_name": "Doe",
|
||||
"email": "jane@example.com",
|
||||
"phone": "+447700900123"
|
||||
}
|
||||
// Returns: { "id": "abc123def456", "account_role": "guest", ... }
|
||||
```
|
||||
|
||||
**Create Booking (Guest):**
|
||||
```json
|
||||
POST /api/bookings
|
||||
{
|
||||
"user_id": "abc123def456",
|
||||
"start_time": "2026-05-20T13:00:00+01:00",
|
||||
"service_ids": ["SVC001"],
|
||||
"notes": "Optional notes"
|
||||
}
|
||||
```
|
||||
|
||||
**Create Booking (Authenticated):**
|
||||
```json
|
||||
POST /api/bookings
|
||||
{
|
||||
"start_time": "2026-05-20T13:00:00+01:00",
|
||||
"service_ids": ["SVC001"],
|
||||
"notes": "Optional notes"
|
||||
}
|
||||
```
|
||||
|
||||
**Reserve Slot (Anonymous):**
|
||||
```json
|
||||
POST /api/bookings/reserve
|
||||
{
|
||||
"start_time": "2026-05-20T13:00:00+01:00",
|
||||
"service_ids": ["SVC001"]
|
||||
}
|
||||
// Returns: { "id": "TBL001", "expires_at": "...", "is_anonymous": true }
|
||||
```
|
||||
|
||||
**Create Service:**
|
||||
```json
|
||||
POST /api/admin/services
|
||||
{
|
||||
"name": "Classic Manicure",
|
||||
"description": "Nail shaping, cuticle care, hand massage, and polish.",
|
||||
"price": 25.00,
|
||||
"duration_minutes": 45,
|
||||
"minimum_age_required": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Confirm Booking:**
|
||||
```json
|
||||
POST /api/admin/bookings/{id}/confirm
|
||||
Body: {"serviceOverrides": []}
|
||||
```
|
||||
|
||||
**Create Exceptional Group:**
|
||||
```json
|
||||
{
|
||||
"name": "Christmas Holiday Period",
|
||||
"description": "Reduced hours for Christmas and New Year",
|
||||
"hours": [
|
||||
{"weekday": 0, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false},
|
||||
{"weekday": 1, "startTime": "10:00:00", "endTime": "15:00:00", "isOpen": true}
|
||||
],
|
||||
"weekStarts": ["2025-12-22", "2025-12-29"]
|
||||
}
|
||||
```
|
||||
|
||||
### Time Format
|
||||
|
||||
All timestamps use ISO 8601 with timezone: `YYYY-MM-DDTHH:MM:SS±HH:MM` (e.g., `2025-01-15T10:00:00+00:00`)
|
||||
|
||||
Timezone is always `Europe/London` (handles BST automatically).
|
||||
|
||||
---
|
||||
|
||||
## Code Patterns
|
||||
|
||||
### Transaction Pattern
|
||||
Used throughout for atomic operations:
|
||||
|
||||
```go
|
||||
tx, err := db.DB.Begin(r.Context())
|
||||
if err != nil { /* handle error */ }
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
// Use tx instead of db.DB for queries
|
||||
err = tx.QueryRow(r.Context(), `INSERT INTO...`)
|
||||
|
||||
if err := tx.Commit(r.Context()); err != nil { /* handle error */ }
|
||||
```
|
||||
|
||||
### CardDAV Synchronization
|
||||
- **On registration**: Creates vCard in SabreDAV via `dav.Service.CreateContact()`
|
||||
- **On profile update**: Updates existing vCard via `updateCardDAV()` helper
|
||||
- Uses internal HTTP calls to DAV server
|
||||
|
||||
### Role Change Detection
|
||||
`RefreshTokenHandler` verifies user's current role hasn't changed since token was issued. If role changed, forces re-login with `401 Unauthorized`.
|
||||
|
||||
### Build Tags
|
||||
- `db_dev.go` - Used with `-tags dev` for local development (localhost connection)
|
||||
- `db.go` - Production build (uses env var for host)
|
||||
- `internal/dav/service_dev.go` / `service_prod.go` - Same pattern for DAV service
|
||||
@@ -0,0 +1,217 @@
|
||||
# Crussell — Overview
|
||||
|
||||
**Last Updated:** May 2026
|
||||
**Status:** Local development — not yet in production
|
||||
|
||||
---
|
||||
|
||||
## What is Crussell?
|
||||
|
||||
Crussell is a full-stack booking platform built for a UK-based sole-trader nail artist. It handles the complete customer journey: browsing services, checking availability, booking appointments, managing profiles, and exporting calendar data. Admins manage the schedule, approve bookings, handle walk-ins and call-ins, and track customer relationships.
|
||||
|
||||
The business operates exclusively in the UK (Europe/London timezone). Cloudflare geo-blocking prevents non-UK access. No automatic timezone conversion is performed — times shown are actual salon times.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| **Backend** | Go 1.25, chi router, PostgreSQL driver (pgx) |
|
||||
| **Frontend** | SvelteKit 5 (static adapter SPA), Svelte 5 runes ($state, $bindable, $effect), Tailwind CSS, shadcn-svelte, bits-ui |
|
||||
| **Database** | PostgreSQL 17 with custom ID generation, partial indexes, GDPR functions |
|
||||
| **DAV** | SabreDAV (PHP 8.2-FPM) for CardDAV/CalDAV contact and calendar sync |
|
||||
| **Storage** | S3/R2 abstraction — RustFS in dev, Cloudflare R2 in prod (build tags) |
|
||||
| **Proxy** | Nginx (static frontend + API reverse proxy + DAV proxy) |
|
||||
| **Containerisation** | Docker Compose (postgres, backend, sabredav, nginx) |
|
||||
| **Testing** | Go test with shared test database, TestMain per package, TRUNCATE CASCADE between tests |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User([Customer])
|
||||
Admin([Admin])
|
||||
|
||||
subgraph Docker[Docker Compose Stack]
|
||||
subgraph NGINX[Nginx :80/:443]
|
||||
Static[Static Frontend Build]
|
||||
Proxy[API Proxy → Backend:8080]
|
||||
DAVProxy[DAV Proxy → SabreDAV]
|
||||
end
|
||||
|
||||
subgraph Backend[Go + Chi :8080]
|
||||
Router[chi Router]
|
||||
Auth[JWT Middleware]
|
||||
Handlers[API Handlers]
|
||||
end
|
||||
|
||||
subgraph Database[PostgreSQL :5432]
|
||||
DB[(Users / Bookings<br/>Payments / Services<br/>Scheduling)]
|
||||
end
|
||||
|
||||
subgraph DAV[SabreDAV :9000]
|
||||
CardDAV[(vCard Contacts)]
|
||||
CalDAV[(Calendar Events)]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph External[External Services]
|
||||
S3[S3/R2 Storage]
|
||||
Gmail[Gmail SMTP]
|
||||
SquareAPI[Square API]
|
||||
end
|
||||
|
||||
User -->|HTTPS| NGINX
|
||||
Admin -->|HTTPS| NGINX
|
||||
Static --> User
|
||||
Proxy --> Router
|
||||
Router --> Auth
|
||||
Auth --> Handlers
|
||||
Handlers --> DB
|
||||
Handlers --> DAV
|
||||
Handlers --> S3
|
||||
Handlers -.->|TODO| Gmail
|
||||
Handlers -.->|TODO| SquareAPI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
### Authentication & Identity
|
||||
- JWT authentication (HS256, 30-day expiry) with auto-refresh
|
||||
- User registration with input validation (names, UK phone, email, age 16+)
|
||||
- Password hashing with bcrypt
|
||||
- Role-based access: `unverified_email`, `verified_email`, `admin`, `guest`, `affiliate`
|
||||
- Refresh token with role-change detection (forces re-login if role changed)
|
||||
- Email verification and password reset endpoints (backend ready, frontend not wired)
|
||||
- Guest/disposable accounts for one-off bookings
|
||||
|
||||
### Booking System
|
||||
- Three booking flows: self-service (customer), walk-in (admin), call-in (admin)
|
||||
- Service eligibility filtering (age requirements + patch test validation)
|
||||
- Slot reservation system with 4 TTL types (user: 1h, anonymous: 10min, walk-in: 5min, call-in: 1h)
|
||||
- Anonymous reservation cap (50 per 10-minute rolling window)
|
||||
- Auto-status transitions: confirmed → in_progress → completed
|
||||
- Booking edit requests (customers can request reschedule, admin approves/denies)
|
||||
- Admin booking service editing with overlap detection and price/duration overrides
|
||||
- Idempotency keys for booking deduplication
|
||||
|
||||
### Scheduling
|
||||
- Default weekly working hours (Mon-Fri, closed Sat/Sun)
|
||||
- Exceptional schedule groups (holiday periods, special hours)
|
||||
- Time blockers (admin-defined unavailable periods, plus slot reservations)
|
||||
- Available hours calculation accounting for bookings, blockers, and gaps
|
||||
|
||||
### Customer Features
|
||||
- Profile management with profile picture upload (cropper, separate S3 bucket)
|
||||
- Loyalty stamps display
|
||||
- Booking history with cancel/reschedule
|
||||
- Calendar export (.ics download)
|
||||
- Portfolio browsing with tag/category filtering
|
||||
|
||||
### Admin Features
|
||||
- Today page (/today): current/next appointment, today's grid, pending approvals
|
||||
- Admin dashboard (/admin): services CRUD, user management, bookings list, scheduling
|
||||
- Walk-in booking wizard (3-step) with slot reservation
|
||||
- Call-in booking wizard (4-step) with slot reservation
|
||||
- User modal: profile, booking history, customer relationship data (spend, visits, top services), patch test recording, loyalty/referrals, privacy/consent
|
||||
- Booking modal: view details, approve/cancel
|
||||
- Approval modal for pending bookings
|
||||
- Portfolio image upload with tag management
|
||||
- Discount campaign management (time-based and milestone campaigns)
|
||||
- Admin notification system (pull-based, acknowledgment)
|
||||
|
||||
### Deposit System
|
||||
- Simplified tracking: `deposits_required` integer (0-3) on users table
|
||||
- 24-hour late cancellation rule: < 24h without forgiveness = no-show + 3 deposits
|
||||
- Per-cancellation forgiveness option
|
||||
- Deposit reduction by 1 when booking completes with payment
|
||||
- Admin can bypass deposit checks (`enforce_deposits: false`)
|
||||
- Guest bookings bypass deposit checks entirely
|
||||
|
||||
### Data & Compliance
|
||||
- GDPR anonymization: guest PII scrubbed 6 months after booking start_time
|
||||
- Registered user account deletion via `anonymize_user()` SQL function
|
||||
- GDPR data export function (`export_all_user_data()`) — not yet wired to endpoint
|
||||
- VAT/tax export functions (`get_vat_return_data()`, `export_sales_transactions()`) — not yet wired
|
||||
- Image metadata stripping (EXIF/GPS removed on upload)
|
||||
- Partial unique email index: guests can share emails, registered users cannot
|
||||
|
||||
### Integrations
|
||||
- CardDAV: profile photos synced to SabreDAV contacts (vCard PHOTO field)
|
||||
- CalDAV: ready for calendar event sync
|
||||
- S3/R2: portfolio image storage (RustFS dev, Cloudflare R2 prod)
|
||||
- Square: placeholder only (payment integration pending)
|
||||
|
||||
---
|
||||
|
||||
## Booking Flows
|
||||
|
||||
| Flow | Who | Entry Point | Reservation TTL |
|
||||
|------|-----|-------------|-----------------|
|
||||
| **Self-Service** | Customer | `/book` → BookingFlow wizard → `POST /api/bookings` | 1h (logged-in) / 10min (anonymous) |
|
||||
| **Walk-In** | Admin | Admin panel → WalkInBooking → `POST /api/admin/bookings/reserve` → WalkInCreateModal | 5min |
|
||||
| **Call-In** | Admin | Admin panel → CallInBooking → `POST /api/admin/bookings/reserve` → BookingCreateModal | 1h |
|
||||
|
||||
All flows integrate with holiday/exceptional hours and time blockers.
|
||||
|
||||
---
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [[User Manual]] — Customer-facing journeys: booking, account management, portfolio
|
||||
- [[Admin Manual]] — Admin workflows: today page, booking management, user management, scheduling
|
||||
- [[Technical Manual]] — Architecture, API reference, database schema, key systems
|
||||
- [[Future Work - Gap Backlog]] — Prioritised backlog of remaining work
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage
|
||||
|
||||
**306/308 tests passing** across 10+ test packages.
|
||||
|
||||
| Package | Coverage Area |
|
||||
|---------|--------------|
|
||||
| `handlers/auth` | Authentication (login, register, refresh, verification) |
|
||||
| `handlers/bookings` | User booking flow, guest bookings, reservations, edit requests, discounts |
|
||||
| `handlers/admin` | Admin bookings, today view, users, services |
|
||||
| `handlers/scheduling` | Working hours, exceptional groups, available hours, time blockers |
|
||||
| `handlers/services` | Service eligibility (age + patch test filtering) |
|
||||
| `handlers/user` | User profile, guest creation, loyalty |
|
||||
| `handlers/portfolio` | Image upload, listing, tags, filters |
|
||||
| `handlers/notifications` | Admin notifications (GET, acknowledge) |
|
||||
| `handlers/handlers_test.go` | Common handler tests |
|
||||
| `bookings_test.go` | Main booking integration tests |
|
||||
|
||||
### Test Infrastructure
|
||||
- TestMain per package — schema migration runs once per package
|
||||
- TruncateTables() between tests — TRUNCATE CASCADE, ~60% faster than DROP+CREATE
|
||||
- Shared test database — tests run sequentially (`-p 1`)
|
||||
- Test tokens use fixed secret: `test-secret-key-for-testing-only`
|
||||
- Fixtures auto-generate unique emails
|
||||
- All test files use `//go:build test` build tag
|
||||
|
||||
---
|
||||
|
||||
## Seed Data Summary
|
||||
|
||||
Running `local-dev-2.sh` creates:
|
||||
|
||||
| Resource | Count |
|
||||
|----------|-------|
|
||||
| Users | 20 (1 admin, 19 regular) |
|
||||
| Services | 12 (10 standard + 2 requiring patch tests) |
|
||||
| Past Bookings | 18 |
|
||||
| Today's Bookings | 4 |
|
||||
| Tomorrow's Bookings | 5 |
|
||||
| Upcoming Bookings | 16 |
|
||||
| Guest Bookings | 3 |
|
||||
| Total Bookings | 43 |
|
||||
| Payments | 22 completed bookings |
|
||||
| Time Blockers | 3 |
|
||||
| Schedule Groups | 3 |
|
||||
| Cancellations | 1 |
|
||||
@@ -0,0 +1,736 @@
|
||||
# Technical Manual
|
||||
|
||||
Architecture, schema, API reference, and deep-dive technical reference for Crussell.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Docker Compose Stack
|
||||
|
||||
| Service | Image | Port | Purpose |
|
||||
|---------|-------|------|---------|
|
||||
| `postgres` | postgres:17 | 5432 | Primary database, init-script.sql mounted |
|
||||
| `backend` | Custom Go build | 8080 | API server (chi router) |
|
||||
| `sabredav` | php:8.2-fpm | 9000 | CardDAV/CalDAV server |
|
||||
| `nginx` | nginx:stable | 80, 443 | Reverse proxy, static frontend, DAV proxy |
|
||||
|
||||
### Request Flow
|
||||
|
||||
```
|
||||
Client → Nginx (:80/:443)
|
||||
├── Static files → frontend/build/
|
||||
├── /api/* → Proxy to backend:8080
|
||||
└── /dav/* → Proxy to sabredav:9000
|
||||
|
||||
Backend (:8080)
|
||||
├── chi Router → Middleware → Handlers
|
||||
├── PostgreSQL (pgx pool)
|
||||
├── SabreDAV (HTTP calls for CardDAV)
|
||||
└── S3/R2 (RustFS dev, Cloudflare R2 prod)
|
||||
```
|
||||
|
||||
### External Services
|
||||
|
||||
| Service | Status | Purpose |
|
||||
|---------|--------|---------|
|
||||
| SabreDAV (CardDAV/CalDAV) | Active | Contact sync (profile photos), calendar events |
|
||||
| S3/R2 | Active | Portfolio images (AVIF), profile pictures (WebP) |
|
||||
| Square | Placeholder | Payment processing (not wired) |
|
||||
| SMTP | Not implemented | Email/SMS notifications |
|
||||
|
||||
---
|
||||
|
||||
## Backend Structure
|
||||
|
||||
### Handler Packages
|
||||
|
||||
| Package | File(s) | Purpose |
|
||||
|---------|---------|---------|
|
||||
| `handlers/auth` | local.go, social.go | Registration, login, refresh, email verification |
|
||||
| `handlers/bookings` | bookings.go, reserve.go, manage.go, admin_reserve.go | Booking CRUD, reservations, admin management, edit requests, discounts |
|
||||
| `handlers/admin` | users.go, analytics.go, discount_campaigns.go | Admin user management, discount campaigns, analytics (stub) |
|
||||
| `handlers/today` | today.go | Current/next appointment, today's grid, pending approvals |
|
||||
| `handlers/user` | profile.go, account.go, guest.go, loyalty.go, customer_relationship.go | User profile, guest creation, loyalty, contact info |
|
||||
| `handlers/services` | services.go | Service catalog, eligibility filtering |
|
||||
| `handlers/scheduling` | default-hours.go, exceptional-hours.go, time-blockers.go | Working hours, exceptional groups, time blockers |
|
||||
| `handlers/portfolio` | images.go | Image upload, listing, tags, filters |
|
||||
| `handlers/notifications` | notifications.go | Admin notifications (GET, acknowledge) |
|
||||
|
||||
### Middleware (`mw/`)
|
||||
|
||||
| Middleware | Purpose |
|
||||
|------------|---------|
|
||||
| `RequireAuth` | Validates JWT, adds user_id and user_role to context |
|
||||
| `OptionalAuth` | Extracts user info if token present, passes through otherwise |
|
||||
| `RequireAdmin` | Allows admin role only |
|
||||
| `RequireVerified` | Allows verified_email or admin |
|
||||
| `RequireRole(roles...)` | Generic role check |
|
||||
| `RateLimit(limit, window)` | IP-based rate limiting (supports CF-Connecting-IP header) |
|
||||
|
||||
### Database Layer (`db/`)
|
||||
|
||||
- **Driver**: pgx/v5 (PostgreSQL)
|
||||
- **Connection**: `postgres://USER:PASSWORD@HOST:5432/DB`
|
||||
- **Connection pooling**: Built-in via pgxpool
|
||||
- **Build tags**: `db_dev.go` (dev, localhost) vs `db.go` (prod, env var)
|
||||
|
||||
### Internal Packages
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `internal/validators` | ID validation (12-char hex format) |
|
||||
| `internal/dav` | SabreDAV CardDAV integration (build tags: `service_dev.go` / `service_prod.go`) |
|
||||
| `internal/s3` | S3/R2 storage abstraction (build tags: dev vs prod) |
|
||||
|
||||
---
|
||||
|
||||
## Frontend Structure
|
||||
|
||||
### SvelteKit Routes
|
||||
|
||||
| Route | File | Purpose |
|
||||
|-------|------|---------|
|
||||
| `/` | +page.svelte | Home — welcome, services overview, portfolio carousel |
|
||||
| `/book` | book/+page.svelte | Customer booking wizard (wraps BookingFlow) |
|
||||
| `/login` | login/+page.svelte | Login/Register with form validation |
|
||||
| `/account` | account/+page.svelte | User profile, bookings, loyalty stamps |
|
||||
| `/admin` | admin/+page.svelte | Admin dashboard — users, bookings, services, scheduling |
|
||||
| `/today` | today/+page.svelte | Staff daily view — appointments, approvals |
|
||||
| `/portfolio` | portfolio/+page.svelte | Image gallery with tag/category filtering |
|
||||
| `/prices` | prices/+page.svelte | Service price list |
|
||||
| `/schedule` | schedule/+page.svelte | User's upcoming appointments with .ics export |
|
||||
| `/contact` | contact/+page.svelte | Dynamic contact info from first admin user |
|
||||
| `/manage` | manage/+page.svelte | Booking management |
|
||||
| `/demo` | demo/+page.svelte | Demo mode |
|
||||
| `/api/[...path]` | api/[...path]/+server.ts | API catch-all proxy (dev) |
|
||||
|
||||
### Component Hierarchy
|
||||
|
||||
```
|
||||
src/lib/components/
|
||||
├── admin/
|
||||
│ ├── ApprovalModal.svelte # Booking approval/decline
|
||||
│ ├── BookingCreateModal.svelte # Admin booking creation (4-step)
|
||||
│ ├── BookingModal.svelte # View booking details
|
||||
│ ├── BookingsCard.svelte # Bookings list
|
||||
│ ├── CallInBooking.svelte # Call-in booking flow
|
||||
│ ├── EditBookingModal.svelte # Edit booking services
|
||||
│ ├── HolidayHours.svelte # Exceptional schedule management
|
||||
│ ├── ImageUpload.svelte # Portfolio image upload
|
||||
│ ├── PatchTestModal.svelte # Record patch test
|
||||
│ ├── ServicesManagement.svelte # Service CRUD
|
||||
│ ├── DiscountsManagement.svelte # Discount campaign management
|
||||
│ ├── UserModal.svelte # User details + relationship data
|
||||
│ ├── UsersCard.svelte # Users list
|
||||
│ ├── WalkInBooking.svelte # Walk-in booking flow
|
||||
│ └── WalkInCreateModal.svelte # Walk-in 3-step wizard
|
||||
├── booking/
|
||||
│ ├── BookingActions.svelte # Next/Back buttons
|
||||
│ ├── BookingFlow.svelte # 5-step booking wizard (1287 lines)
|
||||
│ ├── BookingSummary.svelte # Booking review summary
|
||||
│ ├── DatePicker.svelte # Calendar date selection
|
||||
│ ├── ServiceCard.svelte # Individual service display
|
||||
│ ├── ServiceSelector.svelte # Service selection UI
|
||||
│ ├── StepIndicator.svelte # Progress steps
|
||||
│ └── TimeSlotPicker.svelte # Time slot grid
|
||||
├── today/
|
||||
│ ├── CurrentAppointment.svelte # Active appointment display
|
||||
│ ├── PendingApprovals.svelte # Pending booking approvals
|
||||
│ └── TodayCalendar.svelte # Day view calendar
|
||||
├── layout/
|
||||
│ ├── NavBar.svelte # Navigation bar
|
||||
│ └── PortfolioCarousel.svelte # Home page gallery
|
||||
├── ui/ # shadcn-svelte components
|
||||
└── account/ # Account-related components
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
- **Svelte 5 runes**: `$state`, `$bindable`, `$effect`
|
||||
- **Auth store** (`lib/stores/auth.svelte.ts`): JWT tokens in localStorage, auto-refresh, role checks
|
||||
- **Token refresh**: Every hour, refreshes if < 14 days remaining
|
||||
- **Role-based UI**: `hasRole()`, `isAdmin()`, `isVerified()` control visible elements
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Public Endpoints
|
||||
|
||||
| Method | Path | Auth | Rate Limit | Description |
|
||||
|--------|------|------|------------|-------------|
|
||||
| GET | `/api/services` | Optional | 120/min | List active services (eligibility for authenticated) |
|
||||
| GET | `/api/services/eligible-for/{user_id}` | Admin | 120/min | Services filtered by user's age/patch test |
|
||||
| POST | `/api/register` | None | 10/min | Create user account |
|
||||
| POST | `/api/login` | None | 1/5s | Authenticate, receive JWT |
|
||||
| POST | `/api/verify/generate` | None | — | Generate email verification or password reset code |
|
||||
| POST | `/api/verify/check` | None | — | Verify code |
|
||||
| GET | `/api/health` | None | — | Health check (DB, S3, Square, frontend status) |
|
||||
| GET | `/api/contact` | None | — | Business contact info (first admin user) |
|
||||
| POST | `/api/users/guest` | None | 10/min | Create disposable guest account |
|
||||
| GET | `/api/scheduling/default-hours` | None | 120/min | Get weekly default hours |
|
||||
| GET | `/api/scheduling/exceptional-groups` | None | 120/min | List holiday/special hour groups |
|
||||
| GET | `/api/scheduling/working-hours` | None | 120/min | Merged default + exceptional hours |
|
||||
| GET | `/api/scheduling/available-hours` | None | 120/min | Available slots (triggers cleanup) |
|
||||
| POST | `/api/bookings/reserve` | Optional | 30/min | Reserve slot (user=1h, anon=10min, 50-cap) |
|
||||
| POST | `/api/bookings` | Optional | 30/min | Create booking |
|
||||
| GET | `/api/portfolio/images` | None | — | List images (pagination, tag/category filters) |
|
||||
| GET | `/api/portfolio/tags` | None | — | List all tags (autocomplete) |
|
||||
| GET | `/api/portfolio/filters` | None | 60/min | Get filter categories with counts |
|
||||
| GET | `/api/portfolio/images/{id}` | None | 120/min | Get single image by UUID or timestamp |
|
||||
|
||||
### Authenticated User Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/refresh-token` | Refresh JWT (role-change detection) |
|
||||
| GET | `/api/user/profile` | Get current user profile |
|
||||
| PUT | `/api/user/profile` | Update profile |
|
||||
| POST | `/api/user/profile-picture` | Upload profile picture (cropper) |
|
||||
| PUT | `/api/user/change-password` | Change password |
|
||||
| DELETE | `/api/user/account` | Delete account (GDPR anonymization) |
|
||||
| GET | `/api/user/loyalty` | Get loyalty stamp count |
|
||||
| GET | `/api/bookings` | List user's bookings |
|
||||
| GET | `/api/bookings/{id}` | Get specific booking |
|
||||
| GET | `/api/bookings/{id}/calendar` | Download ICS calendar file |
|
||||
| PUT | `/api/bookings/{id}` | Update booking (user edit) |
|
||||
| DELETE | `/api/bookings/{id}` | Cancel booking (with forgiveness option) |
|
||||
| POST | `/api/bookings/{id}/edit-request` | Request booking reschedule |
|
||||
| DELETE | `/api/bookings/{id}/edit-request` | Cancel edit request |
|
||||
|
||||
### Admin Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/admin/services` | List all services |
|
||||
| POST | `/api/admin/services` | Create service |
|
||||
| DELETE | `/api/admin/services/{id}` | Delete service |
|
||||
| PUT | `/api/admin/services/{id}/toggle` | Toggle active status |
|
||||
| GET | `/api/admin/bookings` | List all bookings |
|
||||
| POST | `/api/admin/bookings` | Create booking for user |
|
||||
| GET | `/api/admin/bookings/search` | Search bookings |
|
||||
| GET | `/api/admin/bookings/user/{user_id}` | User's bookings |
|
||||
| GET | `/api/admin/bookings/{id}` | Get booking details |
|
||||
| PUT | `/api/admin/bookings/{id}` | Update booking services, overrides, notes |
|
||||
| GET | `/api/admin/bookings/{id}/overlapping` | Get overlapping bookings |
|
||||
| PUT | `/api/admin/bookings/{id}/progress` | Progress booking status |
|
||||
| POST | `/api/admin/bookings/{id}/confirm` | Confirm booking |
|
||||
| POST | `/api/admin/bookings/{id}/cancel` | Cancel booking |
|
||||
| POST | `/api/admin/bookings/reserve` | Reserve slot (walkin=5min, callin=1h) |
|
||||
| GET | `/api/admin/bookings/{id}/edit-requests` | List edit requests |
|
||||
| POST | `/api/admin/bookings/{id}/edit-requests/{request_id}/approve` | Approve edit request |
|
||||
| POST | `/api/admin/bookings/{id}/edit-requests/{request_id}/deny` | Deny edit request |
|
||||
| GET | `/api/admin/users` | List users |
|
||||
| GET | `/api/admin/users/{id}` | Get user details |
|
||||
| GET | `/api/admin/users/{id}/relationship` | Customer relationship data |
|
||||
| GET | `/api/admin/users/{id}/patch-tests/eligible` | Eligible patch test services |
|
||||
| POST | `/api/admin/users/{id}/patch-tests` | Record patch test |
|
||||
| GET | `/api/admin/today/current-next` | Current and next appointment |
|
||||
| GET | `/api/admin/today/appointments` | Today's appointments |
|
||||
| GET | `/api/admin/today/pending-approvals` | Pending approval queue |
|
||||
| GET | `/api/admin/notifications` | List notifications |
|
||||
| POST | `/api/admin/notifications/{id}/acknowledge` | Acknowledge notification |
|
||||
| GET | `/api/admin/time-blockers` | List time blockers |
|
||||
| POST | `/api/admin/time-blockers` | Create time blocker |
|
||||
| DELETE | `/api/admin/time-blockers/{id}` | Delete time blocker |
|
||||
| GET | `/api/admin/discount-campaigns` | List discount campaigns |
|
||||
| POST | `/api/admin/discount-campaigns` | Create discount campaign |
|
||||
| PUT | `/api/admin/discount-campaigns/{id}` | Update discount campaign |
|
||||
| DELETE | `/api/admin/discount-campaigns/{id}` | Cancel discount campaign |
|
||||
| GET | `/api/admin/discount-campaigns/{id}/stats` | Campaign stats |
|
||||
| PUT | `/api/scheduling/default-hours` | Update weekly hours |
|
||||
| POST | `/api/scheduling/exceptional-groups` | Create exception group |
|
||||
| DELETE | `/api/scheduling/exceptional-groups` | Delete exception group |
|
||||
| PUT | `/api/scheduling/exceptional-applications` | Apply exceptions to dates |
|
||||
| POST | `/api/portfolio/images` | Upload portfolio image |
|
||||
| DELETE | `/api/portfolio/images/{id}` | Delete portfolio image |
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Enums (13 total)
|
||||
|
||||
| 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` |
|
||||
| `verification_purpose` | `email_verify`, `password_reset` |
|
||||
| `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`, `edit_request` |
|
||||
| `campaign_type` | `time_based`, `milestone` |
|
||||
| `milestone_type` | `per_user_booking_count`, `global_booking_count`, `anniversary` |
|
||||
| `milestone_unit` | `bookings`, `months`, `years` |
|
||||
| `discount_campaign_scope` | `all_bookings`, `first_booking_only`, `new_customers_only` |
|
||||
| `discount_campaign_status` | `draft`, `active`, `completed`, `cancelled` |
|
||||
|
||||
### Tables (25 total)
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | User accounts (partial unique email: WHERE account_role != 'guest') |
|
||||
| `user_social_logins` | Social auth provider links |
|
||||
| `verification_codes` | Email verification and password reset codes |
|
||||
| `patch_tests` | Patch test definitions (notice_duration_hours, expiry_months, service_ids) |
|
||||
| `user_patch_tests` | User patch test completion records (tested_at, notes) |
|
||||
| `services` | Service offerings |
|
||||
| `bookings` | Appointment records (idempotency_key, discount_eligible) |
|
||||
| `booking_services` | Services per booking (override_price, override_duration_minutes) |
|
||||
| `booking_edit_requests` | Pending customer edit requests |
|
||||
| `user_referrals` | Referral tracking |
|
||||
| `working_hours` | Default weekly schedule |
|
||||
| `exceptional_working_hours_groups` | Holiday/special hour groups |
|
||||
| `exceptional_working_hours` | Hours for exception groups |
|
||||
| `exceptional_group_applications` | Apply exceptions to date ranges |
|
||||
| `time_blockers` | Admin time blocks + slot reservations (description LIKE 'RESERVATION:%') |
|
||||
| `forgiven_no_shows` | Tracks forgiven no-shows |
|
||||
| `payments` | Payment transactions (VAT fields, invoice_number sequence) |
|
||||
| `loyalty_redemptions` | Loyalty stamp redemptions (6-month expiry) |
|
||||
| `discount_campaigns` | Discount campaigns (time-based and milestone) |
|
||||
| `booking_discounts` | Applied discounts per booking |
|
||||
| `business_settings` | Business configuration (VAT, currency, contact) |
|
||||
| `admin_notifications` | Admin notification queue |
|
||||
| `user_notification_preferences` | User notification preferences (email/sms/push) |
|
||||
| `images` | Portfolio gallery images with tags |
|
||||
| `tags` | Image tag autocomplete |
|
||||
|
||||
### Key Functions
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `generate_short_id(table_name)` | Generate 12-char hex IDs with collision detection |
|
||||
| `generate_user_id()` | Wrapper for users table |
|
||||
| `generate_service_id()` | Wrapper for services table |
|
||||
| `generate_booking_id()` | Wrapper for bookings table |
|
||||
| `generate_payment_id()` | Wrapper for payments table |
|
||||
| `generate_verification_code()` | 12-char verification code |
|
||||
| `generate_referral_code()` | 12-char referral code with collision detection |
|
||||
| `anonymize_user(target_id)` | GDPR right-to-be-erased for registered users |
|
||||
| `delete_guest_user(target_id)` | Full removal of guest account |
|
||||
| `update_data_consent(target_id, consent)` | Update GDPR consent flags |
|
||||
| `export_all_user_data(target_user_id)` | GDPR Article 15 SAR (JSON export) |
|
||||
| `get_vat_return_data(start, end)` | VAT return summary for MTD |
|
||||
| `export_sales_transactions(start, end, include_vat)` | Tax-compatible transaction export |
|
||||
| `get_monthly_business_summary(start, end)` | Monthly revenue breakdown |
|
||||
| `get_sales_totals(start, end)` | Quick sales snapshot |
|
||||
| `enable_vat_registration(...)` | Enable VAT registration |
|
||||
| `apply_vat_to_payment(payment_id, vat_rate)` | Apply VAT to a payment |
|
||||
| `calculate_vat(gross_amount, vat_rate)` | Calculate net + VAT from gross |
|
||||
| `get_receipt_data(payment_id)` | Receipt generation data |
|
||||
|
||||
### Partial Indexes
|
||||
|
||||
| Index | Table | Condition |
|
||||
|-------|-------|-----------|
|
||||
| `idx_users_email_registered` | users | `WHERE account_role != 'guest'` |
|
||||
| `idx_verification_codes_user_purpose` | verification_codes | `WHERE used_at IS NULL` |
|
||||
| `idx_verification_codes_expires` | verification_codes | `WHERE used_at IS NULL` |
|
||||
| `idx_time_blockers_cron` | time_blockers | `WHERE cron_expression IS NOT NULL` |
|
||||
| `idx_discount_campaigns_dates` | discount_campaigns | `WHERE start_date IS NOT NULL` |
|
||||
|
||||
### Default Working Hours
|
||||
|
||||
| Weekday | Start | End | Open |
|
||||
|---------|-------|-----|------|
|
||||
| Monday | 09:00 | 17:00 | Yes |
|
||||
| Tuesday | 09:00 | 17:00 | Yes |
|
||||
| Wednesday | 12:00 | 20:00 | Yes |
|
||||
| Thursday | 09:00 | 17:00 | Yes |
|
||||
| Friday | 09:00 | 17:00 | Yes |
|
||||
| Saturday | — | — | No |
|
||||
| Sunday | — | — | No |
|
||||
|
||||
---
|
||||
|
||||
## Key Systems
|
||||
|
||||
### Slot Reservation System
|
||||
|
||||
**How it works:** Reservations are stored as `time_blocker` entries with `RESERVATION:*` descriptions. When a user selects a slot, it's temporarily blocked to prevent double-booking.
|
||||
|
||||
**4 TTL Types:**
|
||||
|
||||
| Type | TTL | Description Pattern |
|
||||
|------|-----|---------------------|
|
||||
| Logged-in user | 1 hour | `RESERVATION:user:{userID}:{timestamp}` |
|
||||
| Anonymous user | 10 minutes | `RESERVATION:anon:{ipHash}:{timestamp}` |
|
||||
| Admin walk-in | 5 minutes | `RESERVATION:admin:walkin:{timestamp}` |
|
||||
| Admin call-in | 1 hour | `RESERVATION:admin:callin:{timestamp}` |
|
||||
|
||||
**Anonymous Cap:** 50 reservations per 10-minute rolling window. Returns 429 if exceeded.
|
||||
|
||||
**Self-Block Fix:** `GetTimeBlockersInRange` excludes `RESERVATION:*` entries so overlap checks don't reject the user's own reservation.
|
||||
|
||||
**Cleanup:** `CleanupOldReservations()` runs on availability fetch. Deletes expired entries by type:
|
||||
- User reservations: > 1 hour old
|
||||
- Anonymous: > 10 minutes old
|
||||
- Admin walk-in/call-in: > 15 minutes old
|
||||
- Edit request reservations: > 24 hours old
|
||||
|
||||
**Why designed this way:** Storing reservations in `time_blockers` means they automatically participate in availability calculations — no separate reservation table needed. The TTL-based cleanup is lazy (triggered on availability fetch) rather than cron-based.
|
||||
|
||||
---
|
||||
|
||||
### Guest User System
|
||||
|
||||
**How it works:** Disposable accounts created on-the-fly via `POST /api/users/guest`. Each booking gets a fresh guest user — no identity tracking across bookings.
|
||||
|
||||
**Email Uniqueness:** Partial unique index `idx_users_email_registered ON users (email) WHERE account_role != 'guest'`. Guests can share emails; registered users cannot.
|
||||
|
||||
**Creation Flow:**
|
||||
1. Validate name (1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot), email, phone (UK format → E.164)
|
||||
2. Check if email exists for non-guest user → 409 "Please log in to book"
|
||||
3. Always create new guest (no guest-to-guest collision check)
|
||||
4. Set `account_role = 'guest'`, `account_type = 'email'`, `date_of_birth = '1900-01-01'`
|
||||
|
||||
**GDPR Anonymization:** `AnonymizeStaleGuestAccounts()` runs on every availability fetch:
|
||||
- Scrubs PII 6 months after booking's `start_time`
|
||||
- Preserved: `account_role`, `account_type`, `deposits_required`, `id`, `created_at`
|
||||
- Scrubbed: name → "Guest Anonymized", email → "anon-{id}@anon.invalid", phone → "000000000000", DOB → "1900-01-01"
|
||||
- Excludes users with active/pending bookings
|
||||
|
||||
---
|
||||
|
||||
### Deposit System
|
||||
|
||||
**How it works:** `users.deposits_required` integer (0-3) tracks outstanding deposit obligations.
|
||||
|
||||
**24-Hour Late Cancellation Rule:**
|
||||
- < 24h without forgiveness → `no_show` status, `deposits_required = 3` (resets, not adds)
|
||||
- < 24h with forgiveness → `client_cancelled`, no penalty
|
||||
- ≥ 24h → `client_cancelled`, no penalty
|
||||
- Pending bookings → deleted, no penalty
|
||||
|
||||
**Deposit Reduction:** When booking transitions to `completed` with ≥ 1 payment → `deposits_required -= 1`
|
||||
|
||||
**Booking Restriction:** Users with `deposits_required > 0` must book ≥ 24h in advance. Limited to one active booking.
|
||||
|
||||
**Admin Bypass:** `enforce_deposits: false` in admin booking creation bypasses all deposit checks.
|
||||
|
||||
**Guest Exemption:** Guest bookings bypass deposit and patch-test checks entirely.
|
||||
|
||||
---
|
||||
|
||||
### Scheduling System
|
||||
|
||||
**Default Hours:** `working_hours` table (weekday 0-6, start_time, end_time, is_open). Bulk updateable via PUT.
|
||||
|
||||
**Exceptional Groups:** Three-table design:
|
||||
1. `exceptional_working_hours_groups` — group metadata
|
||||
2. `exceptional_working_hours` — 7 days of hours per group
|
||||
3. `exceptional_group_applications` — which weeks the group applies to (week_start = Monday)
|
||||
|
||||
**Working Hours Merge:** `GetWorkingHours` loads default hours, overlays exceptional groups for applicable weeks. Returns `DayWorkingHours[]` with `source` field ("default" or "exceptional").
|
||||
|
||||
**Available Hours:** `GetAvailableHours` calculates free slots by:
|
||||
1. Loading working hours for the date range
|
||||
2. Subtracting existing bookings (with gap logic)
|
||||
3. Subtracting time blockers (including reservations)
|
||||
4. **Late night lock**: After 22:00, blocks next morning 00:00-11:00 for non-admin users
|
||||
5. Triggers `CleanupOldReservations()` and `AnonymizeStaleGuestAccounts()`
|
||||
|
||||
**Time Blockers:** Can be one-off (no cron) or recurring (cron expression). Cron expansion via `robfig/cron/v3` parser.
|
||||
|
||||
---
|
||||
|
||||
### Patch Test System
|
||||
|
||||
**How it works:** Services can require patch tests. `patch_tests` table defines notice periods and expiry. `user_patch_tests` records completions.
|
||||
|
||||
**Validation on Booking:**
|
||||
1. For each service, find associated patch test
|
||||
2. No record → 400 "Patch test required"
|
||||
3. Within notice period (24h default) → 400 "Must wait X hours"
|
||||
4. Expired (6 months default) → 400 "Patch test expired"
|
||||
5. Valid → service available
|
||||
|
||||
**Admin Recording:** Admin can record patch tests via UserModal. On booking completion, patch test validity is updated (UPSERT pattern).
|
||||
|
||||
---
|
||||
|
||||
### Loyalty & Discount System
|
||||
|
||||
**Loyalty Stamps:**
|
||||
- 1 stamp per completed booking (1 per day max)
|
||||
- Zero-total bookings don't earn stamps
|
||||
- At 10 stamps → pending `loyalty_redemption` created
|
||||
- Next completed booking → 10% discount applied, stamps reset to 0, +1 for completion
|
||||
- Redemption expires after 6 months
|
||||
|
||||
**Discount Campaigns:**
|
||||
- **Time-based**: Active during date range
|
||||
- **Milestone**: `per_user_booking_count`, `global_booking_count`, `anniversary`
|
||||
- Scopes: `all_bookings`, `first_booking_only`, `new_customers_only`
|
||||
- Priority: loyalty > campaign (one discount per booking)
|
||||
|
||||
**Tables:** `loyalty_redemptions`, `discount_campaigns`, `booking_discounts`
|
||||
|
||||
---
|
||||
|
||||
### Service Eligibility
|
||||
|
||||
**Age Filtering:** `services.minimum_age_required` compared to user's `date_of_birth`. If user's age < minimum → service excluded.
|
||||
|
||||
**Patch Test Filtering:** Services linked to `patch_tests` via `service_ids[]`. User must have valid `user_patch_tests` record.
|
||||
|
||||
**Admin vs. Customer:**
|
||||
- `/api/services` — returns services with eligibility for authenticated users
|
||||
- `/api/services/eligible-for/{user_id}` — admin-only, returns services for specific user (used in admin booking flows)
|
||||
|
||||
---
|
||||
|
||||
### Idempotency Keys
|
||||
|
||||
**How it works:** `Idempotency-Key` header (optional, 64-char max). If provided, backend checks for existing booking with that key. If found, returns existing booking (200, no duplicate). If not found, creates new booking with key stored.
|
||||
|
||||
**Used by:** BookingFlow.svelte and WalkInCreateModal.svelte generate UUIDs via `crypto.randomUUID()`, reuse same key on retry.
|
||||
|
||||
---
|
||||
|
||||
### Chi Router Fix
|
||||
|
||||
**Problem:** Using `r.Route("/bookings", ...)` with sub-router caused `RequireAuth` middleware to bleed into `OptionalAuth` POST endpoints.
|
||||
|
||||
**Solution:** Flattened `/bookings` routes to explicit full paths in `main.go`. Each route registered directly under `/api` group with its own middleware chain.
|
||||
|
||||
---
|
||||
|
||||
### Build Tags
|
||||
|
||||
| Tag | Files | Purpose |
|
||||
|-----|-------|---------|
|
||||
| `dev` | `db_dev.go`, `service_dev.go` | Local development (localhost DB, RustFS S3) |
|
||||
| (none/prod) | `db.go`, `service_prod.go` | Production (env var DB host, Cloudflare R2) |
|
||||
| `test` | All `*_test.go` files | Test build (separate test database) |
|
||||
|
||||
---
|
||||
|
||||
## Code Patterns
|
||||
|
||||
### Transaction Pattern
|
||||
|
||||
```go
|
||||
tx, err := db.DB.Begin(r.Context())
|
||||
if err != nil { /* handle error */ }
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
// Use tx instead of db.DB for queries
|
||||
err = tx.QueryRow(r.Context(), `INSERT INTO...`)
|
||||
|
||||
if err := tx.Commit(r.Context()); err != nil { /* handle error */ }
|
||||
```
|
||||
|
||||
Used throughout for atomic operations: booking creation, service updates, scheduling changes.
|
||||
|
||||
### CardDAV Synchronization
|
||||
|
||||
- **On registration**: Creates vCard in SabreDAV via `dav.Service.CreateContact()`
|
||||
- **On profile update**: Updates existing vCard via `updateCardDAV()` helper
|
||||
- Profile photos synced to vCard PHOTO field
|
||||
- Uses internal HTTP calls to DAV server
|
||||
|
||||
### Role Change Detection
|
||||
|
||||
`RefreshTokenHandler` verifies user's current role hasn't changed since token was issued. If role changed (e.g., admin demoted), returns 401 and forces re-login.
|
||||
|
||||
---
|
||||
|
||||
## Testing Infrastructure
|
||||
|
||||
### Test Utilities
|
||||
|
||||
| Component | File | Purpose |
|
||||
|-----------|------|---------|
|
||||
| **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 Architecture
|
||||
|
||||
- **TestMain per package**: Schema migration runs once per package (not per test)
|
||||
- **TruncateTables()**: Between tests — `TRUNCATE TABLE ... CASCADE`, ~60% faster than DROP+CREATE
|
||||
- **Shared test database**: Tests run sequentially (`-p 1`)
|
||||
- **Test DSN**: `postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable`
|
||||
- **Build tag**: All test files use `//go:build test`
|
||||
- **Test secret**: `test-secret-key-for-testing-only`
|
||||
- **Fixtures**: Auto-generate unique emails
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go test -tags test ./... # All tests
|
||||
go test -tags test -v ./... # Verbose
|
||||
go test -tags test -v ./handlers/bookings # Specific package
|
||||
go test -tags test -v -run "TestBooking" ./... # Pattern match
|
||||
go test -tags test -v -p 1 -count=2 ./... # Run twice for flaky detection
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
**306/308 tests passing** across 10+ packages:
|
||||
- `handlers/auth` — Authentication
|
||||
- `handlers/bookings` — User booking flow, guest bookings, reservations, edit requests, discounts
|
||||
- `handlers/admin` — Admin bookings, today view, users, services
|
||||
- `handlers/scheduling` — Working hours, exceptional groups, available hours, time blockers
|
||||
- `handlers/services` — Service eligibility
|
||||
- `handlers/user` — User profile, guest creation, loyalty
|
||||
- `handlers/portfolio` — Image upload, listing, tags, filters
|
||||
- `handlers/notifications` — Admin notifications
|
||||
- `bookings_test.go` — Main booking integration tests
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### SabreDAV (CardDAV/CalDAV)
|
||||
|
||||
- **Contact sync**: Profile photos synced to vCard PHOTO field
|
||||
- **Auto-update**: Triggered on profile changes
|
||||
- **Internal HTTP**: Go backend makes HTTP calls to SabreDAV on port 9000
|
||||
- **Build tags**: `service_dev.go` (dev) vs `service_prod.go` (prod)
|
||||
|
||||
### S3/R2 Storage
|
||||
|
||||
- **Dev**: RustFS (local S3-compatible server)
|
||||
- **Prod**: Cloudflare R2
|
||||
- **Buckets**: `crussell` (portfolio), `crussell-profile-pics` (profile pictures)
|
||||
- **Image formats**: AVIF full-size (0.72 quality, 1500px max), WebP thumbnails (250x250)
|
||||
- **Security**: EXIF/GPS metadata stripped on upload via `imaging` library
|
||||
|
||||
### Square (Placeholder)
|
||||
|
||||
- `handlers/payments/square.go` exists but returns "not implemented"
|
||||
- `payment_method` enum includes `online_square`
|
||||
- Frontend BookingFlow Step 4 shows placeholder
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### JWT Authentication
|
||||
|
||||
- **Algorithm**: HS256
|
||||
- **Expiry**: 30 days
|
||||
- **Secret**: `JWT_SECRET_KEY` environment variable (required, checked in `init()`)
|
||||
- **Refresh**: Auto-refresh via `POST /api/refresh-token`
|
||||
- **Role-change detection**: Forces re-login if role changed
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
| Endpoint | Limit | Purpose |
|
||||
|----------|-------|---------|
|
||||
| General public | 120/min | Services, scheduling, portfolio |
|
||||
| Registration | 10/min | Prevent spam accounts |
|
||||
| Guest creation | 10/min | Prevent guest abuse |
|
||||
| Booking/reserve | 30/min | Prevent booking spam |
|
||||
| Portfolio filters | 60/min | Moderate filter queries |
|
||||
| Admin endpoints | None | Trusted users |
|
||||
| Login | 1/5s | Internal rate limiting |
|
||||
|
||||
**IP extraction**: CF-Connecting-IP → X-Real-IP → X-Forwarded-For → RemoteAddr
|
||||
|
||||
### Security Headers
|
||||
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- `X-Frame-Options: DENY`
|
||||
- `X-XSS-Protection: 1; mode=block`
|
||||
- `Strict-Transport-Security: max-age=31536000; includeSubDomains` (TODO: enable in prod)
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin` (TODO: enable in prod)
|
||||
|
||||
### Input Validation
|
||||
|
||||
- **Names**: 1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot
|
||||
- **Phone**: UK format → E.164 (+44...)
|
||||
- **Email**: Standard format via `net/mail.ParseAddress`
|
||||
- **Age**: Must be 16+ years
|
||||
- **Services**: name (100), price (>0), duration (1-480), patch test (0-168), age (0-100)
|
||||
- **Portfolio**: tags/filters (256 char max), filter category validation, image ID pattern security
|
||||
|
||||
### Image Security
|
||||
|
||||
- EXIF/GPS metadata stripped on all uploads via `imaging` library
|
||||
- Profile pictures stored in separate bucket from portfolio images
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Purpose | Required |
|
||||
|----------|---------|----------|
|
||||
| `JWT_SECRET_KEY` | HMAC key for JWT (HS256) | **Yes** |
|
||||
| `DATABASE_URL` | PostgreSQL connection string | **Yes** |
|
||||
| `POSTGRES_USER` | Database username | Docker |
|
||||
| `POSTGRES_PASSWORD` | Database password | Docker |
|
||||
| `POSTGRES_DB` | Database name | Docker |
|
||||
| `POSTGRES_HOST` | Database host | Dev |
|
||||
| `S3_BUCKET` | Main image bucket (portfolio) | No (default: crussell) |
|
||||
| `S3_PROFILE_PICS_BUCKET` | Profile pictures bucket | No (default: crussell-profile-pics) |
|
||||
| `S3_ENDPOINT` | S3/RustFS endpoint | Dev |
|
||||
| `S3_PUBLIC_URL` | Public URL for S3 bucket | Dev |
|
||||
| `S3_ACCESS_KEY` | S3 access key | Dev |
|
||||
| `S3_SECRET_KEY` | S3 secret key | Dev |
|
||||
| `R2_ENDPOINT` | Cloudflare R2 endpoint | Prod |
|
||||
| `R2_BUCKET` | R2 bucket name | Prod |
|
||||
| `R2_PUBLIC_URL` | R2 public URL | Prod |
|
||||
| `R2_ACCESS_KEY` | R2 access key | Prod |
|
||||
| `R2_SECRET_KEY` | R2 secret key | Prod |
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
./local-dev-2.sh # Creates tmux session 'crussell-dev'
|
||||
```
|
||||
|
||||
Creates 4 panes:
|
||||
- **Pane 0**: psql interactive shell
|
||||
- **Pane 1**: Backend (`go run -tags dev ./main.go`)
|
||||
- **Pane 2**: Frontend (`npm run dev -- --host`)
|
||||
- **Pane 3**: Rustfs logs
|
||||
|
||||
### Dev Build Tag
|
||||
|
||||
Backend uses `-tags dev` — check for dev-specific behavior with build constraints.
|
||||
|
||||
### Admin User Setup
|
||||
|
||||
No API endpoint exists for role promotion. Admin users are created via direct SQL:
|
||||
|
||||
```sql
|
||||
UPDATE users SET account_role = 'admin' WHERE email = 'admin@example.com';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### UK-Only Timezone (No Auto-Conversion)
|
||||
|
||||
Crussell is a UK-only service. Cloudflare geo-blocking prevents non-UK access. All appointment times are displayed in the browser's local timezone (which for UK customers is Europe/London). BST/GMT transitions are handled by staff manually updating working hours — the app does not perform automatic timezone conversions.
|
||||
|
||||
### Sole-Trader Single-Staff
|
||||
|
||||
The business is a single nail artist. No multi-staff scheduling, no staff management, no staff-specific bookings. All bookings are for the sole trader.
|
||||
|
||||
### Static Frontend Build
|
||||
|
||||
Frontend is built as a static SPA (SvelteKit static adapter) and served by Nginx. API calls go directly to the Go backend. In local dev, SvelteKit's API proxy handles CORS avoidance.
|
||||
|
||||
### Cloudflare Geo-Blocking
|
||||
|
||||
Non-UK traffic is blocked at the Cloudflare level. No application-level geo-checking needed.
|
||||
|
||||
### Schema Management
|
||||
|
||||
No migration framework. Database schema is managed entirely through `init-scripts/init-script.sql`. To add tables, edit the init script directly — nothing exists to migrate from in local development.
|
||||
@@ -0,0 +1,358 @@
|
||||
# User Manual
|
||||
|
||||
How customers use the Crussell booking website — from first visit to managing their appointments.
|
||||
|
||||
---
|
||||
|
||||
## Booking Your First Appointment
|
||||
|
||||
### Step 1: Arrive at the Website
|
||||
|
||||
When you open the website, you'll see the salon's homepage with:
|
||||
- The salon's name and branding
|
||||
- A showcase of recent nail art from the portfolio
|
||||
- A navigation bar at the top with links to **Book**, **Prices**, **Portfolio**, and **Contact**
|
||||
|
||||
Tap or click **Book an Appointment** to start.
|
||||
|
||||
### Step 2: Choose Your Services
|
||||
|
||||
You'll see a list of all available treatments. Each one shows:
|
||||
- The name and a short description
|
||||
- The price
|
||||
- How long it takes
|
||||
|
||||
**Why might a service look greyed out?**
|
||||
|
||||
Some treatments require a **patch test** — a small allergy test done before your first appointment with certain products (like gel nails). If you haven't had a patch test on record, those services will appear greyed out so you can see them but can't select them yet. You'll need to book a patch test first, or mention it when you call the salon.
|
||||
|
||||
If you've already had a patch test, the system checks whether it's still valid (they last 6 months) and whether enough time has passed since the test (at least 24 hours). If either condition isn't met, the service stays greyed out.
|
||||
|
||||
**Why might a service not appear at all?**
|
||||
|
||||
Some treatments have a minimum age requirement. If the system knows your age (because you're logged in and your date of birth is on file) and you're below the minimum, that service won't show up at all.
|
||||
|
||||
If you're not logged in, age-based filtering is skipped — the system doesn't know your age yet, so all services are shown. Age is checked when you submit your booking.
|
||||
|
||||
**You can select multiple services** — they'll be added together for the total price and duration.
|
||||
|
||||
### Step 3: Pick a Date
|
||||
|
||||
A calendar appears showing which dates have availability:
|
||||
- **Highlighted dates** have open slots
|
||||
- **Greyed-out dates** are fully booked, the salon is closed that day, or the salon has blocked out that time (for example, a holiday or personal appointment)
|
||||
|
||||
The calendar takes into account:
|
||||
- The salon's regular opening hours (Monday to Friday, closed Saturday and Sunday for example)
|
||||
- Any special holiday hours the salon has set
|
||||
- Existing bookings that fill up the day
|
||||
- Blocked-out periods the salon has marked as unavailable
|
||||
|
||||
Tap or click a date that works for you.
|
||||
|
||||
### Step 4: Pick a Time
|
||||
|
||||
Once you've chosen a date, you'll see the available time slots for that day. The system calculates these by looking at:
|
||||
- The salon's opening hours for that day
|
||||
- Existing appointments (there's always a small gap between appointments for cleaning and setup)
|
||||
- Any blocked-out times the salon has set
|
||||
- Slots that other customers are currently in the process of booking
|
||||
|
||||
**Important — your chosen slot is temporarily held:**
|
||||
- If you're **logged in**, your slot is held for **1 hour** while you complete the form
|
||||
- If you're **not logged in**, your slot is held for **10 minutes**
|
||||
|
||||
If you take too long and the hold expires, someone else might take that slot. If this happens, you'll need to pick a new time.
|
||||
|
||||
There's also a limit on how many times an unregistered visitor can hold slots — this prevents the system from being misused.
|
||||
|
||||
### Step 5: Fill in Your Details
|
||||
|
||||
You'll be asked for:
|
||||
- **First name** and **last name**
|
||||
- **Email address**
|
||||
- **Phone number** (UK format)
|
||||
- **Notes** (optional — for example, "I'd like a French tip design" or "I have a nail condition to mention")
|
||||
|
||||
**If you're not logged in:**
|
||||
The system creates a temporary guest account for you automatically. This just means your booking is recorded under a disposable profile — you won't have a permanent account, but you'll still receive a confirmation. You can use the same email address for multiple guest bookings.
|
||||
|
||||
**If you enter an email that's already registered:**
|
||||
You'll see a message saying "Please log in to book." The system recognises the email belongs to an existing account and wants to make sure it's really you. Either log in, or use a different email if you genuinely don't have an account.
|
||||
|
||||
**Deposit check:**
|
||||
If you have any outstanding deposit obligations from a previous late cancellation (see the Deposit System section below), you'll only be able to book appointments that are at least 24 hours away. Guest bookings skip this check entirely.
|
||||
|
||||
All bookings must be made at least 1 hour before the appointment start time.
|
||||
|
||||
### Step 6: Confirmation
|
||||
|
||||
After submitting, you'll see a confirmation screen showing:
|
||||
- Your appointment date and time
|
||||
- The services you selected
|
||||
- The total price
|
||||
- An **Add to Calendar** button — this downloads a calendar file you can import into Google Calendar, Apple Calendar, Outlook, or any other calendar app
|
||||
|
||||
---
|
||||
|
||||
## Guest Bookings — What You Should Know
|
||||
|
||||
### What is a guest booking?
|
||||
|
||||
If you book without creating an account, the system creates a temporary "guest" profile for that booking. It's disposable — there's no permanent identity attached to it. Each guest booking gets its own fresh guest profile.
|
||||
|
||||
### Can I use the same email for multiple guest bookings?
|
||||
|
||||
Yes. Guest bookings can share email addresses. This is different from registered accounts, where each email can only be used once.
|
||||
|
||||
### What happens to my guest data over time?
|
||||
|
||||
Your personal information (name, email, phone, date of birth) attached to a guest booking is automatically removed **6 months after your appointment date**. After that point, the booking record still exists for the salon's financial records, but your personal details are replaced with anonymous placeholders. If your booking is still upcoming (not yet happened), your data is kept until the appointment passes.
|
||||
|
||||
---
|
||||
|
||||
## Creating an Account
|
||||
|
||||
### Registration
|
||||
|
||||
To create a permanent account, go to the **Login** page and choose to register. You'll need:
|
||||
- **Name**: Letters, spaces, hyphens, apostrophes, and dots are allowed (1–50 characters)
|
||||
- **Phone number**: UK format — the system will format it correctly for you
|
||||
- **Email address**: A standard email format
|
||||
- **Date of birth**: You must be at least 16 years old to create an account
|
||||
- **Password**: Up to 72 characters
|
||||
|
||||
After registering, your account starts in an "unverified" state. An email verification system exists but isn't fully connected yet — for now, you can still log in and book.
|
||||
|
||||
### Logging In
|
||||
|
||||
Enter your email and password on the Login page. There's a short delay between login attempts (about 5 seconds) to prevent unauthorised access.
|
||||
|
||||
Once logged in, you stay logged in for **30 days**. After that, you'll need to log in again. The system automatically refreshes your session in the background so you won't get logged out unexpectedly while using the site.
|
||||
|
||||
### Forgotten Password
|
||||
|
||||
A password reset system exists but the "Forgot Password" link hasn't been added to the login page yet. If you've forgotten your password, contact the salon directly.
|
||||
|
||||
---
|
||||
|
||||
## Your Account Page
|
||||
|
||||
Once logged in, go to **Account** to manage your profile and bookings.
|
||||
|
||||
### Profile Information
|
||||
|
||||
You can view and edit:
|
||||
- Your name
|
||||
- Your email address
|
||||
- Your phone number
|
||||
- Your date of birth
|
||||
|
||||
### Profile Picture
|
||||
|
||||
You can upload a profile photo:
|
||||
- Tap the upload area to select an image from your device
|
||||
- A cropping tool lets you adjust which part of the image is shown
|
||||
- The photo appears next to your name in the salon's contact system
|
||||
- Your photo is also synced to the salon's address book so they can recognise you when you arrive
|
||||
|
||||
### Loyalty Stamps
|
||||
|
||||
Every time you complete an appointment, you earn a loyalty stamp. You can see your current stamp count on the Account page.
|
||||
|
||||
- You earn **1 stamp per completed appointment** (maximum 1 per day, even if you have multiple appointments on the same day)
|
||||
- Appointments with a total price of £0 don't earn stamps
|
||||
- When you reach **10 stamps**, a discount is automatically set up for your next completed appointment
|
||||
- The discount gives you **10% off** your next appointment
|
||||
- After the discount is used, your stamp count resets to 0 and you start earning again
|
||||
- If you don't use your discount within **6 months**, it expires
|
||||
|
||||
### Your Booking History
|
||||
|
||||
A list of all your appointments — upcoming and past — is shown on the Account page. Each entry shows:
|
||||
- The date and time
|
||||
- The services booked
|
||||
- The total price
|
||||
- The current status (pending, confirmed, completed, cancelled)
|
||||
|
||||
Tap any booking to see full details.
|
||||
|
||||
### Calendar View
|
||||
|
||||
The **Schedule** page shows your upcoming appointments in a calendar-style layout. Each appointment has an **Add to Calendar** button that downloads a calendar file (.ics) you can open in any calendar application — Google Calendar, Apple Calendar, Outlook, and others all support this format.
|
||||
|
||||
---
|
||||
|
||||
## Cancelling an Appointment
|
||||
|
||||
### How to Cancel
|
||||
|
||||
From your Account page, find the booking you want to cancel and select the cancel option.
|
||||
|
||||
### The 24-Hour Rule
|
||||
|
||||
**Cancelling 24 hours or more before your appointment:**
|
||||
No penalty. The booking is cancelled with no consequences.
|
||||
|
||||
**Cancelling less than 24 hours before your appointment:**
|
||||
This counts as a late cancellation. You'll receive **3 deposit obligations**, which means you'll need to pay deposits before you can book again (see the Deposit System section below).
|
||||
|
||||
**If your booking is still pending (not yet confirmed by the salon):**
|
||||
The booking is simply deleted — no penalty.
|
||||
|
||||
### Can a Late Cancellation Be Forgiven?
|
||||
|
||||
Yes — the salon can forgive a late cancellation on a case-by-case basis. If you had a genuine emergency, contact the salon and they may waive the penalty. If forgiven, the cancellation is treated like a normal cancellation with no deposit obligations.
|
||||
|
||||
---
|
||||
|
||||
## Requesting to Reschedule
|
||||
|
||||
If you need to change your appointment time:
|
||||
|
||||
1. Go to your **Account** page and find the booking
|
||||
2. Select **Reschedule**
|
||||
3. Pick a new date and time (the same availability rules apply — the new slot must be open)
|
||||
4. Submit your reschedule request
|
||||
|
||||
**What happens next:**
|
||||
- Your request goes to the salon for review
|
||||
- The salon can either **approve** or **decline** it
|
||||
- If approved, your appointment time is updated to the new slot
|
||||
- If declined, your original appointment time stays the same
|
||||
- You can cancel your reschedule request at any time before the salon reviews it
|
||||
|
||||
**Things to know:**
|
||||
- You can't reschedule a completed or cancelled appointment
|
||||
- The new time must not clash with any of your other existing appointments
|
||||
- If the salon has already adjusted the price or duration of your booking, those adjustments are respected in the reschedule
|
||||
|
||||
---
|
||||
|
||||
## The Deposit System
|
||||
|
||||
### What Are Deposits?
|
||||
|
||||
Depits are a way the salon protects against last-minute cancellations. If you cancel an appointment less than 24 hours before it starts (without the salon's forgiveness), you receive **3 deposit obligations**.
|
||||
|
||||
### What Does This Mean for You?
|
||||
|
||||
While you have deposit obligations:
|
||||
- You can only book appointments that are **at least 24 hours away**
|
||||
- You can only have **one active booking at a time**
|
||||
- You need to complete appointments and pay for them to reduce your deposit count
|
||||
|
||||
### How Do Deposits Go Down?
|
||||
|
||||
Each time you complete an appointment and pay for it, your deposit count goes down by **1**. After all 3 are cleared, you can book normally again.
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| What happens | Result |
|
||||
|---|---|
|
||||
| Cancel ≥ 24 hours before | No penalty |
|
||||
| Cancel < 24 hours before | 3 deposit obligations |
|
||||
| Cancel < 24 hours (salon forgives you) | No penalty |
|
||||
| Cancel a pending booking | No penalty — booking is deleted |
|
||||
| Complete an appointment and pay | Deposit count goes down by 1 |
|
||||
|
||||
### Guest Bookings and Deposits
|
||||
|
||||
If you book as a guest (without an account), deposit rules don't apply to you. Each guest booking is independent.
|
||||
|
||||
---
|
||||
|
||||
## Browsing the Portfolio
|
||||
|
||||
The portfolio page is the salon's gallery of nail art and designs.
|
||||
|
||||
### Searching by Tags
|
||||
|
||||
There's a **search bar** where you can type tag names. Tags are keywords the salon adds to each photo — things like "french tip", "summer", "nail art", "glitter", and so on.
|
||||
|
||||
- Type a single tag to find all photos with that tag
|
||||
- Type multiple tags separated by commas to find photos that have **all** of those tags (for example, "summer, glitter" shows photos tagged with both "summer" AND "glitter")
|
||||
|
||||
### Filtering by Categories
|
||||
|
||||
Below the search bar, there are **category filter dropdowns**. Categories group tags into types — for example, a "Colour" category might have options like "Red", "Blue", "Pink", and a "Season" category might have "Spring", "Summer", "Autumn", "Winter".
|
||||
|
||||
- Select one option from a category dropdown to filter by that value
|
||||
- You can select options from **multiple categories at once** — the filters combine, so you'll only see photos that match **all** of your selections
|
||||
- The number shown next to each filter option tells you how many photos match. When you select filters in other categories, these numbers update to show how many results you'd get if you added that filter too
|
||||
|
||||
### Combining Search and Filters
|
||||
|
||||
The tag search and category filters work together. If you search for "french tip" AND select "Red" from the Colour category, you'll see photos that are tagged with "french tip" AND have the colour "Red".
|
||||
|
||||
### Viewing a Specific Photo
|
||||
|
||||
If someone shared a direct link to a particular photo, the page will open directly to that image, bypassing any filters. This is useful when the salon sends you a "here's what I can do for you" link.
|
||||
|
||||
### Image Quality
|
||||
|
||||
Photos load in a high-quality format that looks great on any screen. Thumbnails load first for speed, then the full image appears as you scroll.
|
||||
|
||||
---
|
||||
|
||||
## Prices Page
|
||||
|
||||
The **Prices** page shows a straightforward list of all the salon's services with their prices and how long each one takes. This is the same information you see during the booking process, but presented as a quick reference.
|
||||
|
||||
---
|
||||
|
||||
## Contact Page
|
||||
|
||||
The **Contact** page shows the salon's contact details — name, phone number, email, and a profile photo. This information is kept up to date by the salon, so if they change their phone number or email, the page reflects it automatically.
|
||||
|
||||
---
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### "I booked as a guest — can I see my booking later?"
|
||||
|
||||
Guest bookings are tied to the email you provided. If you used the same email each time, the salon can look up your bookings. However, guest accounts don't have a login, so you can't view them yourself online. If you want to manage your bookings online, create an account.
|
||||
|
||||
### "Why can't I see certain services?"
|
||||
|
||||
If you're logged in and a service doesn't appear, it's likely because you don't meet the minimum age requirement for that treatment. If a service appears but is greyed out, it requires a patch test you haven't completed yet.
|
||||
|
||||
### "I selected a time but when I submitted, it was taken."
|
||||
|
||||
Your slot was held temporarily, but the hold expired before you finished the form. This happens more often with guest bookings (10-minute hold) than logged-in bookings (1-hour hold). Just pick a new time — it's unlikely to happen again if you complete the form promptly.
|
||||
|
||||
### "Can I book for someone else?"
|
||||
|
||||
If you're logged in, the booking is made under your account. If you want to book for someone else, you can use their details but must log out to do so, but the booking won't be linked to your account. Booking for someone else on your account to bypass a patch test check will result in a warning or ban / barred from using our salon.
|
||||
|
||||
### "What happens if I cancel and then want to rebook?"
|
||||
|
||||
If you cancelled with more than 24 hours' notice, you can rebook immediately with no restrictions. If you cancelled with less than 24 hours' notice and weren't forgiven, you'll have 3 deposit obligations — you can still book, but only for appointments at least 24 hours away, and only one at a time.
|
||||
|
||||
### "How long does my patch test last?"
|
||||
|
||||
Patch tests are valid for **6 months** from the date they were done or last relevant service was rendered. After that, you'll need a new one before booking services that require it.
|
||||
|
||||
### "Do I need a patch test for every visit?"
|
||||
|
||||
No — once you have a valid patch test on record, you're good for any service that requires it, as long as the test hasn't expired.
|
||||
|
||||
### "I just had a patch test — why can't I book straight away?"
|
||||
|
||||
There's a **24-hour waiting period** after a patch test before you can book the services it covers. This is a safety measure to make sure any allergic reaction shows up before your appointment.
|
||||
|
||||
### "What if two people in my household want to book?"
|
||||
|
||||
Each person should use their own email address. If you both want to book, you can each create separate accounts, or one can book as a guest with their own email.
|
||||
|
||||
### "Can I change my profile photo?"
|
||||
|
||||
Yes — go to your Account page and use the photo upload tool. You can crop the image to make sure the right part is shown.
|
||||
|
||||
### "I earned loyalty stamps — where do I see them?"
|
||||
|
||||
Your stamp count is displayed on your Account page. When you reach 10, a discount is automatically set up for your next completed appointment.
|
||||
|
||||
### "What does 'pending' mean on my booking?"
|
||||
|
||||
A pending booking means the salon hasn't confirmed it yet. The salon reviews some new bookings and will either confirm or decline them. Once confirmed, your booking is locked in.
|
||||
Reference in New Issue
Block a user