docs: rewrite README and all Obsidian documentation to match current state
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
+392
-530
File diff suppressed because it is too large
Load Diff
@@ -1,259 +1,108 @@
|
||||
**Last Updated:** June 2026 — GDPR compliance system (async export endpoint with 12h cache, 16-section SQL export, /gdpr frontend with PDF export + JSON download, DeleteAccountHandler external system scrubbing, anonymize_user child table PII scrubbing, AnonymizeStaleGuestAccounts extended field scrubbing), portfolio image upload limits (20MB frontend/backend), notes validation (max=1000000), CharCounter component, loginInProgress rate limiting cap, profile picture 15MB limit, formatDateISO utility, NavBar/TodayCalendar/lunch protection refinements
|
||||
# Future Work — Gap Backlog
|
||||
|
||||
**Last Updated:** June 2026
|
||||
**Status:** Living backlog — add to this as gaps are discovered
|
||||
|
||||
---
|
||||
|
||||
# 🟢 Local Gaps — Can Fix Now
|
||||
|
||||
No external dependencies. No paid services. No API keys needed.
|
||||
|
||||
## P0 — Critical (Fix Now)
|
||||
|
||||
| # | Gap | Effort | Area | Notes |
|
||||
| --- | ---------------------------------------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | ~~`DELETE /api/user/account` is a no-op~~ ✅ | S (1-2h) | Backend | Wired to `anonymize_user()` for registered users and `delete_guest_user()` for guests. CardDAV contact deleted best-effort. |
|
||||
| 2 | ~~**WalkInCreateModal guest booking errors out**~~ ✅ | S (1-2h) | Frontend | Guest creation now fires at submit time in both walk-in and call-in flows. Phone defaults to +447700900000 if left blank. |
|
||||
| 3 | ~~**ApprovalModal decline/cancel stub**~~ ✅ | S (2-3h) | Frontend | `handleDecline()` now calls `POST /api/admin/bookings/{id}/cancel`. Backend sets status to `we_cancelled`, acknowledges pending notification, creates cancelled_booking notification. |
|
||||
| 4 | **CurrentAppointment action stubs** | M (1d) | Frontend | `handleEdit()` ✅ — opens `EditBookingModal` for service management. `handleTakePayment()` ✅ — wired to multi-method PaymentModal. `handleReschedule()` ✅ — opens RescheduleModal with available slot lookup and conflict detection. `handleExtend()`, `handleCancel()` — dead buttons. |
|
||||
| 52 | ~~**Referral code registration**~~ ✅ | S (2-3h) | Full-stack | **Complete May 2026.** Backend validates 12-char alphanumeric codes during registration, looks up referrer, records in `user_referrals`. Frontend login page has formatted input (xxxx-xxxx-xxxx). Tests: valid + invalid referral code scenarios. |
|
||||
| 53 | ~~**Admin schedule page**~~ ✅ | M (1-2d) | Frontend | **Complete May 2026.** `/admin/schedule` — Google Calendar-style week view with drag-scroll, booking details modal, working hours overlays, status-coloured bars. Responsive: single-day view on mobile. |
|
||||
| 54 | ~~**BookingFlow welcome step**~~ ✅ | S (1h) | Frontend | **Complete May 2026.** Unauthenticated users see Step 0 welcome card encouraging login (loyalty stamps, seasonal discounts messaging). "Log In" → `/login`, "Continue as Guest" → Step 1. Step indicator adapts `startAt` based on auth state. |
|
||||
| 55 | ~~**Shared format utilities**~~ ✅ | S (1h) | Frontend | **Complete May 2026.** `lib/utils/format.ts` — `formatDuration`, `formatDateTime`, `formatDate`, `formatTime`, `calculateAge`. Used across components for consistent display. |
|
||||
| 56 | ~~**patch_test_duration_hours on services**~~ ✅ | S (1h) | Full-stack | **Complete May 2026.** Service type includes `patch_test_duration_hours`. Creating service with duration > 0 auto-creates patch test record. All service list endpoints LEFT JOIN patch_tests to populate field. |
|
||||
| 57 | ~~**created_by_name on bookings**~~ ✅ | XS (15min) | Full-stack | **Complete May 2026.** Booking type includes `created_by_name`. Admin booking handler LEFT JOINs users to resolve creator name. |
|
||||
| 58 | ~~**Admin login redirect**~~ ✅ | XS (15min) | Frontend | **Complete May 2026.** Admins redirected to `/today` after login instead of home page. Token payload decoded to check role. |
|
||||
|
||||
## P1 — High
|
||||
|
||||
| # | Gap | Effort | Area | Notes |
|
||||
| ----- | -------------------------------------------------------- | -------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ~~5~~ | ~~**Admin notification panel**~~ ✅ | ~~M (1-2d)~~ | ~~Frontend~~ | ~~Backend fully wired (GET/acknowledge). No frontend UI to display notifications. Admin has no visibility into pending bookings, cancellations, no-shows.~~ Two-tier system live: `new_booking` (all) + `pending_booking` (notes/today). Priority ordering, bell icon, `/admin/notifications` page, acknowledge flow, enriched responses.
|
||||
| 6 | **Reservation/anonymization cron** | S (2-3h) | Backend | `CleanupOldReservations()` and `AnonymizeStaleGuestAccounts()` only fire on availability fetch. If no one fetches availability, expired reservations persist and stale guests aren't anonymized. Should be a background ticker in `main.go`. |
|
||||
| 7 | ~~**GDPR data export endpoint**~~ ✅ | ~~M (1d)~~ | ~~Backend~~ | ~~`export_all_user_data()` SQL function exists (JSON export). No Go handler wired. Required for GDPR Article 15 SAR requests.~~ **Complete June 2026.** Async `GET /api/user/gdpr-export` with 12h in-memory cache, background goroutine generation (navigation away doesn't cancel), cache states (MISS/HIT/GENERATING). `/gdpr` frontend page with skeleton loading, 2s polling, styled report cards/tables, PDF export (print CSS hides navbar + verification banner), raw JSON download. 16-section SQL export: profile, bookings (with override pricing), payments, patch tests, referrals, notification preferences, saved cards, refunds, social logins, loyalty redemptions (with booking date/services), booking discounts (with campaign names), edit requests, affiliate payouts, verification codes, forgiven no-shows. Conditional rendering for empty sections, VAT breakdowns (hidden until non-zero), deposits required (hidden when 0). `DeleteAccountHandler` extended with external system scrubbing (S3 profile pic, Square saved cards). `anonymize_user()` extended with child table PII scrubbing. `AnonymizeStaleGuestAccounts()` extended with additional field scrubbing. 25 tests. |
|
||||
| 8 | **VAT/Tax export endpoints** | M (1-2d) | Backend | `get_vat_return_data()`, `export_sales_transactions()` SQL functions exist. No admin API to trigger them. Needed for HMRC compliance. |
|
||||
| ~~9~~ | ~~**Walk-in guest reservation → booking transition**~~ ✅ | S (1h) | Frontend | Done — guest accounts created at submit time, reservation system uses explicit reservation_type field, both walk-in and call-in use 15min TTL. |
|
||||
| 10 | **Password reset flow not wired to frontend** | S (2-3h) | Frontend | Backend has `/api/verify/generate` and `/api/verify/check` endpoints. Login page has no "forgot password" link or form. |
|
||||
| 11 | **Email verification flow not wired to frontend** | S (2-3h) | Frontend | Users register with `unverified_email` role. No UI to enter verification code or resend code. `+layout.svelte` has alert-based prototype. |
|
||||
| 12 | **Booking cancellation from user account** | S (2-3h) | Frontend | UserBookingModal shows booking details but no cancel button. Users must call/email to cancel. Backend endpoint exists (`DELETE /api/bookings/{id}`). |
|
||||
| 13 | ~~**Booking rescheduling for users**~~ ✅ | ~~M (1-2d)~~ | ~~Full-stack~~ | ~~Backend fully wired (request/approve/deny + time_blocker reservation). Frontend re-validation on submit implemented.~~ **Complete May 2026.** User-facing edit request modal with auto-select, responsive layout, 15-min grid alignment. Admin reschedule modal with real-time available slot lookup, conflict detection, one-click confirm. Enriched side-by-side snapshots in Pending Approvals. |
|
||||
| ~~50~~ | ~~**Admin UI for edit request approval/denial**~~ ✅ | ~~M (1-2d)~~ | ~~Frontend~~ | ~~Backend endpoints exist~~ **Complete May 2026.** Edit requests appear in Pending Approvals on Today page with side-by-side comparison. Approve/deny buttons wired. Admin reschedule modal provides direct reschedule without approval step. |
|
||||
|
||||
## P2 — Medium
|
||||
|
||||
| # | Gap | Effort | Area | Notes |
|
||||
| ------ | ---------------------------------------------------------- | ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 14 | ~~`delete_guest_user()` SQL function missing~~ ✅ | S (1h) | DB | Created next to `anonymize_user()` in init-script.sql. Called by `DeleteAccountHandler` for guest users. |
|
||||
| ~~15~~ | ~~**User notification preferences UI**~~ ✅ | ~~S (2-3h)~~ | ~~Frontend~~ | ~~DB table `user_notification_preferences` exists with email/sms/push flags. No settings page to toggle them.~~ GET/PUT endpoints wired. Toggle section in /account Admin tab. Email, SMS, Browser push channels. Auto-save on toggle. |
|
||||
| 16 | **One-off custom services** | M (1-2d) | Full-stack | Admin can't create single-use services outside the catalog. Every custom job (bridal party, special request) must be added to permanent service list. |
|
||||
| 17 | **One-off exceptional hours** | M (1d) | Full-stack | Single-day overrides (dentist appointment, afternoon off) require creating a full exceptional group. ~~Should support one-off date blocks without group overhead.~~ **Partially addressed:** Time blockers UI handles one-off unavailable periods. Remaining: one-off *open* hours (e.g., "open 2pm-5pm on a Sunday") still needs exceptional group simplification. |
|
||||
| 18 | ~~**HSTS header**~~ ✅ | XS (15min) | Backend | Added as a TODO-comment in the security headers middleware. Will be uncommented when HTTPS is enabled in production. |
|
||||
| 19 | ~~**Referrer-Policy header**~~ ✅ | XS (15min) | Backend | Added as a TODO-comment in the security headers middleware. Will be uncommented when ready for production. |
|
||||
| 20 | **Business settings management UI** | M (1-2d) | Full-stack | `business_settings` table exists (VAT registration, business name, etc.). No admin page to configure. Changes require direct SQL. |
|
||||
| 21 | **Referral system UI** | M (1-2d) | Full-stack | `user_referrals` table exists. **Backend complete** — registration accepts and validates referral codes, relationships recorded automatically. Remaining: users can't see their referral code or track uses in their account page. Admin can't manage referral campaigns. |
|
||||
| 22 | **Analytics endpoints** | M (1-2d) | Backend | `handlers/admin/analytics.go` is 1 line. `get_monthly_business_summary()`, `get_sales_totals()` SQL functions exist. No admin dashboard stats. |
|
||||
| 23 | ~~**console.log debug statements**~~ ✅ | XS (15min) | Frontend | Removed from BookingFlow.svelte and ImageUpload.svelte. |
|
||||
| 24 | ~~**Alert-based prototype UX**~~ ✅ | XS (30min) | Frontend | Replaced all `alert()` calls with `toast.success/error/info` from svelte-sonner. |
|
||||
| ~~25~~ | ~~**No customer relationship view**~~ ✅ | M (1-2d) | Frontend | **Implemented May 2026.** New `GET /api/admin/users/{id}/relationship` endpoint returns: total spend (from completed payments), total visits, first/last visit dates, avg visits/month, top 5 most booked services, notes history. New `user_notes_history` table tracks note changes. UserModal shows "Customer Relationship" section between Loyalty & Referrals and Privacy & Consent. |
|
||||
| 26 | **CSV/Excel export for bookings/payments** | M (1d) | Backend | Admin can't export data for accounting software. SQL functions exist but no endpoint to download as CSV. |
|
||||
| 27 | ~~**Graceful shutdown**~~ ✅ | S (1h) | Backend | Added signal handling for SIGTERM/SIGINT with 15-second shutdown timeout in main.go. |
|
||||
| 28 | ~~**Health check endpoint**~~ ✅ | XS (15min) | Backend | Added `GET /api/health` returning overall status plus DB, S3, Square, and frontend service statuses. |
|
||||
| 29 | **API documentation** | M (1-2d) | Backend | No OpenAPI/Swagger spec. No generated docs. New developers must read code to understand endpoints. |
|
||||
| 30 | **XSS input sanitization** | S (2-3h) | Backend | Backend validates format (regex, length) but doesn't sanitize HTML entities. Stored XSS risk in `notes`, `name`, `description` fields. |
|
||||
| 31 | **Per-user rate limiting** | M (1d) | Backend | Rate limiter is IP-based. Authenticated users could abuse from multiple IPs. Should track by user ID + IP. Pure Go — no Redis needed for single-instance. |
|
||||
| 32 | **CSRF protection** | S (2-3h) | Backend | SvelteKit handles some CSRF for its own forms, but direct API calls to `/api/*` bypass it. Consider double-submit cookie or SameSite cookies. |
|
||||
| 33 | **Begin button (Today page)** | S (2-3h) | Full-stack | Manual start for early arrivals. Gray out if >3hrs away. Currently auto-infer only. |
|
||||
| 34 | ~~**Auto lunch protection**~~ ✅ | ~~M (1d)~~ | ~~Backend~~ | ~~Block bookings that remove lunch break. 1h customer auto-block, 30min admin with warning.~~ **Complete May 2026.** `findAllLunchGaps()` replaces `findLargestLunchGap()` — returns all gaps sorted descending. `buildLunchProtection()` in shared `timeSlots.ts` consolidates logic across all booking flows. Admin journey warns if <1h gap, requires 30min minimum. User journey requires 1h minimum. |
|
||||
| 35 | ~~**Walk-in slot blocking**~~ ✅ | S (1-2h) | Frontend | **Resolved May 2026.** `WalkInBooking.svelte` reserves slot via `POST /api/admin/bookings/reserve` (15-min TTL) before opening `WalkInCreateModal`. Backend `AdminReserveSlotHandler` creates `time_blocker` entry with `RESERVATION:admin:walkin:*` description, blocking concurrent bookings. Minor gap: reservation time_blocker not deleted after booking creation (relies on TTL expiry via `CleanupOldReservations`). |
|
||||
| ~~36~~ | ~~**No idempotency keys for bookings**~~ ✅ | S (2-3h) | Full-stack | **Implemented May 2026.** `idempotency_key VARCHAR(64) UNIQUE` column added to bookings table. Both `POST /api/bookings` and `POST /api/admin/bookings` extract `Idempotency-Key` header, check for existing booking with that key, return existing booking with 200 if found (no duplicate). Frontend BookingFlow.svelte and WalkInCreateModal.svelte generate UUID via `crypto.randomUUID()`, reuse same key on retry. |
|
||||
| 37 | **No booking conflict detection for users** | S (2-3h) | Backend | Users can theoretically double-book themselves if they open two tabs. Reservation system helps but doesn't fully prevent. |
|
||||
| 38 | **Service category/tag management** | M (1-2d) | Full-stack | Services have no category field. Hard to organize (manicure vs pedicure vs nail art). Admin must scroll through flat list. |
|
||||
| 39 | ~~**No customer-facing cancellation policy display**~~ ✅ | XS (30min) | Frontend | Added cancellation policy text block in BookingFlow Step 3 below the terms & conditions line. |
|
||||
| 40 | **No no-show tracking dashboard** | S (2-3h) | Frontend | Admin can't see which users have accumulated no-shows. `forgiven_no_shows` table exists but no UI. |
|
||||
| 41 | ~~**No timezone handling for international customers**~~ ✅ | XS (15min) | Frontend | **Intentionally not implemented.** Crussell is UK-only; Cloudflare blocks non-UK traffic. Added explanatory comments in BookingFlow.svelte and README.md so this stops being flagged. |
|
||||
| 42 | **Dark mode** | M (1-2d) | Frontend | SvelteKit + Tailwind supports it easily. No dark mode toggle or `prefers-color-scheme` support. |
|
||||
| 43 | **PWA support** | L (3-5d) | Frontend | No service worker, no manifest.json, no offline support. Customers can't "install" the booking app. |
|
||||
| 44 | **Automated database backups** | M (1d) | Infrastructure | No backup strategy. PostgreSQL volume is persistent but no automated dumps, no point-in-time recovery. Can use `pg_dump` cron on the host — no external service needed. |
|
||||
| ~~45~~ | ~~**Loyalty stamp redemption + discount system**~~ ✅ | XL (5-7d) | Full-stack | **Complete June 2026.** v2 plan (`.sisyphus/plans/45-loyalty-discount-system-v2.md`). Auto-redeem at 10 stamps, discount applied at completion. All discounts stack additively (loyalty + time-based + milestones). Campaign lifecycle: draft → active → completed. 45 tests covering stacking, edge cases, and campaign status. `discount_eligible` dead column removed. 3 tables: `loyalty_redemptions`, `discount_campaigns`, `booking_discounts`. |
|
||||
| ~~46~~ | ~~**Staff management**~~ 🗑️ | — | — | Removed — single employee sole trader business, no multi-staff needed. |
|
||||
| 47 | **Recurring bookings** | L (3-5d) | Full-stack | Customers can't book the same slot weekly/monthly. Would need a `recurring_bookings` table + background job to materialize instances. |
|
||||
| ~~48~~ | ~~**Waitlist functionality**~~ 🗑️ | — | — | Removed — not desired for this business. |
|
||||
| ~~49~~ | ~~**Image optimization for portfolio**~~ ✅ | XS (30min) | Frontend | **Complete May 2026.** AVIF full-size (0.72 quality, 1500px max), WebP thumbnails (250x250), lazy loading all implemented. No srcset/picture needed — business decision. |
|
||||
| ~~51~~ | ~~**Test DB optimization**~~ ✅ | M (1d) | Backend | **Complete May 2026.** TestMain per package (schema migration runs once per package, not per test). Per-test setup changed to TRUNCATE-only (~60% faster). 3 missing tables added to TruncateTables. discount_test.go moved to package bookings. Dead code removed. Flaky time-of-day tests fixed. Test output streamed in real-time via tee in local-dev-2.sh. **286/288 tests passing** (was 222/224). |
|
||||
This document is two lists: **MVP** (must do before launch) and **Stretch** (nice-to-have after launch). Items are numbered sequentially. All done items are removed — not crossed out, not tracked. If you need to know what was done, check the git history.
|
||||
|
||||
---
|
||||
|
||||
# 🔴 External Gaps — Blocked on Third-Party Access
|
||||
## MVP — Must Do Before Launch
|
||||
|
||||
Require paid accounts, API approval, or external service credentials. **Do not attempt until access is granted.**
|
||||
These are blockers: missing functionality that prevents daily operations, legal compliance, or basic security. No external dependencies. Each is local, can be implemented today.
|
||||
|
||||
## Payment — Square
|
||||
| # | Gap | Effort | Area | Notes |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **CurrentAppointment action stubs** | M (1d) | Frontend | `Extend` and `Cancel` buttons on Today page are dead. Staff cannot cancel or extend an in-progress appointment from the Today page. Edit, Take Payment, and Reschedule are already wired. |
|
||||
| 2 | **Reservation/anonymization background cron** | S (2-3h) | Backend | `CleanupOldReservations()`, `AnonymizeStaleGuestAccounts()`, `CleanupExpiredGiftCards()`, `CleanupIdleAccounts()`, `CleanupExpiredFinancialRecords()` all run on `GET /api/availability`. If no one fetches availability for days, expired reservations persist and stale guest data isn't anonymized. Should be a background ticker in `main.go` (or a lightweight cron job). |
|
||||
| 3 | **VAT/Tax export endpoints** | M (1-2d) | Backend | `get_vat_return_data()` and `export_sales_transactions()` SQL functions exist. No admin API to trigger them. Needed for HMRC Making Tax Digital compliance. |
|
||||
| 4 | **Password reset flow** | S (2-3h) | Frontend | Backend has `/api/verify/generate` and `/api/verify/check`. Login page has no "forgot password" link or form. Customers who forget their password must call the salon. |
|
||||
| 5 | **Email verification flow** | S (2-3h) | Frontend | Users register with `unverified_email` role. No UI to enter verification code or resend. `+layout.svelte` has an alert-based prototype that needs to be wired properly. |
|
||||
| 6 | **Booking cancellation from user account** | S (2-3h) | Frontend | `UserBookingModal` shows booking details but no cancel button. Users must call/email to cancel. Backend endpoint `DELETE /api/bookings/{id}` exists. |
|
||||
| 7 | **Business settings management UI** | M (1-2d) | Frontend | `GET/PUT /api/admin/settings` backend endpoints exist (VAT, business name, gift card expiry months, voucher type SPV/MPV). No admin page. Staff must use `curl` or direct SQL. |
|
||||
| 8 | **CSV/Excel export for bookings/payments** | M (1d) | Backend | Admin can't export data for accounting software. SQL functions exist but no endpoint to download CSV. |
|
||||
| 9 | **XSS input sanitization** | S (2-3h) | Backend | Backend validates format (regex, length) but doesn't sanitize HTML entities. Stored XSS risk in `notes`, `name`, `description` fields. |
|
||||
| 10 | **CSRF protection** | S (2-3h) | Backend | SvelteKit handles CSRF for its own forms, but direct API calls to `/api/*` bypass it. Consider double-submit cookie or SameSite cookies. |
|
||||
| 11 | **Automated database backups** | M (1d) | Infrastructure | No backup strategy. PostgreSQL volume is persistent but no automated dumps, no point-in-time recovery. Can use `pg_dump` cron on the host — no external service needed. |
|
||||
|
||||
| # | Gap | Effort | Area | Status | Notes |
|
||||
|---|-----|--------|------|--------|-------|
|
||||
| ~~E1~~ | ~~**Payment integration (Square SDK)**~~ | ~~XL (3-5d)~~ | ~~Full-stack~~ | ✅ **Complete** | Dev mock (`//go:build dev`) + prod stub (`//go:build !dev`). Terminal + online payments, refunds, tips, saved cards, webhooks. **Multi-method:** Card (Terminal/online), Cash (change calc + tip), Gift Card (12-digit ID). **Amount precision:** API uses pence (int64). **Tip page:** Percentage-based (10/15/20%) + custom. |
|
||||
| ~~E2~~ | ~~**No deposit payment flow**~~ | ~~M (1-2d)~~ | ~~Full-stack~~ | ✅ **Complete** | Users with `deposits_required > 0` can pay deposits online via Web Payments SDK. One-active-booking limit enforced. Supports deposit, full, partial, and balance payment types. Saved cards for faster checkout. |
|
||||
| ~~E3~~ | ~~**Tip calculation UI**~~ | ~~S (1h)~~ | ~~Frontend~~ | ✅ **Complete** | Tip button on completed bookings via `/pay-tip/[id]`. Percentage-based (10%, 15%, 20%) or custom amount. `POST /api/bookings/{id}/tip` endpoint. Admin can also add tips during card/cash payments in PaymentModal. |
|
||||
| ~~E4~~ | ~~**Gift card system**~~ | ~~L (3-5d)~~ | ~~Full-stack~~ | ✅ **Complete** | `giftcard` in `payment_method` enum. Backend handlers wired. Admin payment modal supports gift card payments (12-digit ID entry). UI for gift card management (create/redeem) pending. |
|
||||
### External MVP (Requires Third-Party Access)
|
||||
|
||||
## Email/SMS — SMTP Provider (Resend, SendGrid, Twilio, etc.)
|
||||
| # | Gap | Effort | Area | Blocked On | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| E5 | **Email/SMS notification system** | XL (5-7d) | Backend | SMTP provider (Resend, SendGrid, Twilio) | `user_notification_preferences` table exists but no delivery system. Blocks: booking reminders, password reset emails, deposit reduction notifications. |
|
||||
| E8 | **S3/R2 production storage** | M (1d) | Backend | Cloudflare R2 or AWS S3 credentials | `s3.go` (`!dev` build tag) returns "not implemented". Prod builds cannot store portfolio images. Need AWS SDK v2 + credentials. |
|
||||
|
||||
| # | Gap | Effort | Area | Status | Notes |
|
||||
|---|-----|--------|------|--------|-------|
|
||||
| E5 | **Email/SMS notification system** | XL (5-7d) | Backend | 🔒 Blocked | No SMTP integration. No scheduled jobs for booking reminders. `user_notification_preferences` table exists but unused. Need SMTP credentials or API key from provider. |
|
||||
| E6 | **Automated deposit reduction notification** | S (1h) | Backend | 🔒 Depends on E5 | When `deposits_required` decreases, no notification is sent. User doesn't know they're closer to being unblocked. |
|
||||
| E7 | ~~**Waitlist cancellation notifications**~~ | S (1h) | Backend | 🔒 Depends on E5 + ~~#48~~ | ~~When a slot opens up, waitlisted customers need to be notified.~~ Removed — waitlist not desired. |
|
||||
---
|
||||
|
||||
## Cloud Storage — S3/R2 Production
|
||||
## Stretch — Post-Launch
|
||||
|
||||
| # | Gap | Effort | Area | Status | Notes |
|
||||
|---|-----|--------|------|--------|-------|
|
||||
| E8 | **S3/R2 production stubs** | M (1d) | Backend | 🔒 Blocked | `s3.go` (`!dev` build tag): Upload/Download/Delete all return "not implemented" errors. Prod builds cannot store images. Need AWS SDK v2 + R2/S3 credentials. Dev works fine with RustFS. |
|
||||
These improve the experience or add features, but the business can operate without them.
|
||||
|
||||
## Social Auth — OAuth Apps (Google, Microsoft, Facebook)
|
||||
| # | Gap | Effort | Area | Notes |
|
||||
|---|---|---|---|---|
|
||||
| 12 | **One-off custom services** | M (1-2d) | Full-stack | Admin can't create single-use services. Every custom job (bridal party, special request) must be added to the permanent catalog. |
|
||||
| 13 | **One-off exceptional hours** | M (1d) | Full-stack | Single-day overrides (dentist appointment, afternoon off) require creating a full exceptional group. Time blockers handle unavailable periods; one-off *open* hours (e.g., "open Sunday 2pm-5pm") still need simplification. |
|
||||
| 14 | **Referral system UI** | M (1-2d) | Full-stack | Backend complete — registration validates codes, relationships recorded. Users can't see their referral code or track uses. Admin can't manage referral campaigns. |
|
||||
| 15 | **Analytics endpoints** | M (1-2d) | Backend | `handlers/admin/analytics.go` is 1 line. `get_monthly_business_summary()`, `get_sales_totals()` SQL functions exist but not wired. |
|
||||
| 16 | **API documentation** | M (1-2d) | Backend | No OpenAPI/Swagger spec. No generated docs. New developers must read code. |
|
||||
| 17 | **Per-user rate limiting** | M (1d) | Backend | Rate limiter is IP-based. Authenticated users could abuse from multiple IPs. Should track by user ID + IP. Pure Go — no Redis needed for single-instance. |
|
||||
| 18 | **Begin button (Today page)** | S (2-3h) | Full-stack | Manual start for early arrivals. Currently auto-inferred only. Gray out if >3 hours away. |
|
||||
| 19 | **Booking conflict detection for users** | S (2-3h) | Backend | Users can theoretically double-book themselves in two tabs. Reservation system helps but doesn't fully prevent. |
|
||||
| 20 | **Service category/tag management** | M (1-2d) | Full-stack | Services have no category field. Admin scrolls through a flat list. No way to group (manicure vs pedicure vs nail art). |
|
||||
| 21 | **No-show tracking dashboard** | S (2-3h) | Frontend | `forgiven_no_shows` table exists but no UI. Admin can't see which users have accumulated no-shows. |
|
||||
| 22 | **Dark mode** | M (1-2d) | Frontend | SvelteKit + Tailwind supports it. No toggle or `prefers-color-scheme` support. |
|
||||
| 23 | **PWA support** | L (3-5d) | Frontend | No service worker, no manifest.json, no offline support. Customers can't "install" the app. |
|
||||
| 24 | **Recurring bookings** | L (3-5d) | Full-stack | Customers can't book the same slot weekly/monthly. Would need `recurring_bookings` table + background job. |
|
||||
| 25 | **Idempotency key cleanup** | S (1h) | Backend | `idempotency_key` columns added to `bookings`, `payments`, `till_sales` with unique constraints. No retention policy — keys accumulate indefinitely. |
|
||||
| 26 | **Gift card self-service portal** | M (1d) | Frontend | Users can see gift card balance on Account page but can't independently redeem to balance without admin. |
|
||||
|
||||
| # | Gap | Effort | Area | Status | Notes |
|
||||
|---|-----|--------|------|--------|-------|
|
||||
| E9 | **Social auth (Google/Microsoft/Facebook)** | L (2-3d) | Backend | 🔒 Blocked | `handlers/auth/social.go` is 1 line. Need OAuth app registrations + client secrets for each provider. |
|
||||
### External Stretch (Requires Third-Party Access)
|
||||
|
||||
## Monitoring — Sentry / Error Tracking
|
||||
|
||||
| # | Gap | Effort | Area | Status | Notes |
|
||||
|---|-----|--------|------|--------|-------|
|
||||
| E10 | **Error tracking / monitoring** | M (1-2d) | Backend | 🔒 Blocked | No Sentry, no structured logging, no error aggregation. `log.Printf()` only. No alerting on 5xx errors. Needs Sentry DSN or equivalent. |
|
||||
| # | Gap | Effort | Area | Blocked On | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| E9 | **Social auth (Google/Microsoft/Facebook)** | L (2-3d) | Backend | OAuth app registrations + client secrets | `handlers/auth/social.go` is 1 line. |
|
||||
| E10 | **Error tracking / monitoring** | M (1-2d) | Backend | Sentry DSN or equivalent | `log.Printf()` only. No alerting on 5xx. |
|
||||
|
||||
---
|
||||
|
||||
## Dependency Map
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ EXTERNAL BLOCKERS │
|
||||
│ │
|
||||
│ E1 Square API ──┬──→ E2 Deposit payments ✅ │
|
||||
│ ├──→ E3 Tips ✅ │
|
||||
│ └──→ E4 Gift cards ✅ (admin UI) │
|
||||
│ │
|
||||
│ E5 SMTP/API ────┬──→ E6 Deposit reduction notifications │
|
||||
│ └──→ E7 Waitlist notifications (removed) │
|
||||
│ │
|
||||
│ E8 S3/R2 ───────→ Portfolio images in production │
|
||||
│ │
|
||||
│ E9 OAuth Apps ──→ E9 Social auth │
|
||||
│ │
|
||||
│ E10 Sentry ─────→ E10 Error tracking │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
E5 Email/SMS → E6 deposit reduction notifications
|
||||
→ booking reminders
|
||||
→ password reset emails
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ LOCAL (UNBLOCKED) │
|
||||
│ │
|
||||
│ #1 Delete account ✅──→ #7 GDPR export ✅──→ #14 SQL func ✅ │
|
||||
│ │
|
||||
│ #3 Approval decline ✅──→ #13 Booking reschedule │
|
||||
│ │
|
||||
│ ~~#5 Admin notification panel~~ ✅ ──→ ~~#15 Preferences UI~~ ✅ │
|
||||
│ ──→ #48 Waitlist (removed) │
|
||||
│ │
|
||||
│ #2 Walk-in guest fix ──→ #9 Reservation transition │
|
||||
│ │
|
||||
│ #6 Reservation cron ──→ #42 Dark mode (no deps) │
|
||||
│ │
|
||||
│ #39 Cancellation policy ──→ ZERO deps, 30min fix │
|
||||
│ #28 Health check ──→ ZERO deps, 15min fix │
|
||||
│ #23 console.log cleanup ──→ ZERO deps, 15min fix │
|
||||
│ #24 Alert prototype cleanup ──→ ZERO deps, 30min fix │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
E8 S3/R2 → portfolio images in production
|
||||
E9 OAuth → social login flow
|
||||
E10 Sentry → error tracking, 5xx alerting
|
||||
```
|
||||
|
||||
All local (MVP + Stretch) items have zero external dependencies.
|
||||
|
||||
---
|
||||
|
||||
## Suggested Execution Order
|
||||
## Notes on Removed Items
|
||||
|
||||
### Phase 1 — Zero-Dependency Quick Wins (Week 1)
|
||||
The following items were completed and removed from this document:
|
||||
|
||||
*No external services. Each takes <30min except #1.*
|
||||
|
||||
1. **#23** Remove console.log debug statements (15min) ✅
|
||||
2. **#24** Replace alert() prototypes with toast notifications (30min) ✅
|
||||
3. **#28** Add health check endpoint (15min) ✅
|
||||
4. **#39** Add cancellation policy display to BookingFlow (30min) ✅
|
||||
5. **#41** Fix timezone display for international customers (15min) ✅
|
||||
6. **#18** Add HSTS header (15min) ✅
|
||||
7. **#19** Add Referrer-Policy header (15min) ✅
|
||||
8. **#1** Fix `DELETE /api/user/account` (1-2h) — biggest win in this phase ✅
|
||||
9. **#3** ApprovalModal decline/cancel (2-3h) ✅
|
||||
|
||||
### Phase 2 — Admin Productivity (Week 2)
|
||||
|
||||
9. **#2** Wire WalkInCreateModal guest booking (1-2h) ✅
|
||||
10. **#3** ApprovalModal decline/cancel (2-3h) ✅
|
||||
11. ~~**#5** Admin notification panel (1-2d)~~ ✅
|
||||
12. **#4** CurrentAppointment Extend + Cancel actions (1d) — skip TakePayment (blocked on E1). **Edit** ✅ — now opens `EditBookingModal` for service management. **Take Payment** ✅ — multi-method PaymentModal (Card/Cash/Gift Card) with service price overrides and tip presets. **Reschedule** ✅ — RescheduleModal with available slot lookup and conflict detection.
|
||||
13. **#12** Booking cancellation from user account (2-3h)
|
||||
14. **#40** No-show tracking dashboard (2-3h)
|
||||
15. **#35** Walk-in slot blocking (1-2h) ✅
|
||||
16. ~~**#13**~~ ~~Booking rescheduling~~ ✅ — complete (user edit request + admin reschedule modal)
|
||||
17. ~~**#50**~~ ~~Admin UI for edit requests~~ ✅ — complete (Pending Approvals integration)
|
||||
18. ~~**#34**~~ ~~Auto lunch protection~~ ✅ — complete (findAllLunchGaps + shared timeSlots.ts)
|
||||
|
||||
### Phase 3 — Compliance + Reliability (Week 3)
|
||||
|
||||
15. **#6** Reservation/anonymization background cron (2-3h)
|
||||
16. ~~**#7**~~ ~~GDPR data export endpoint~~ ✅ — complete (async endpoint + /gdpr frontend + 25 tests)
|
||||
17. **#8** VAT/Tax export endpoints (1-2d)
|
||||
18. ~~**#14**~~ ~~Create `delete_guest_user()` SQL function~~ ✅ — complete
|
||||
19. **#27** Graceful shutdown (1h) ✅
|
||||
20. ~~**#36**~~ ~~Idempotency keys for bookings~~ ✅ — implemented
|
||||
21. **#30** XSS input sanitization (2-3h)
|
||||
22. **#44** Automated database backups (1d)
|
||||
|
||||
### Phase 4 — User Experience (Week 4)
|
||||
|
||||
23. **#10** Password reset flow (2-3h)
|
||||
24. **#11** Email verification flow (2-3h)
|
||||
25. ~~**#13**~~ ~~Booking rescheduling for users~~ ✅ — complete
|
||||
26. **#20** Business settings management UI (1-2d)
|
||||
27. **#16** One-off custom services (1-2d)
|
||||
28. **#17** One-off exceptional hours (1d) — partially addressed by time blockers UI
|
||||
29. ~~**#35**~~ ~~Walk-in slot blocking~~ ✅ — resolved
|
||||
30. **#38** Service category management (1-2d)
|
||||
|
||||
### Phase 5 — Growth + Polish (Week 5+)
|
||||
|
||||
31. **#21** Referral system UI (1-2d) — **Backend complete**, remaining: user-facing referral code display, tracking dashboard
|
||||
32. **#22** Analytics endpoints (1-2d)
|
||||
33. ~~**#25**~~ ~~Customer relationship view~~ ✅ — implemented
|
||||
34. **#26** CSV/Excel export (1d)
|
||||
35. **#29** API documentation (1-2d)
|
||||
36. ~~**#31**~~ Per-user rate limiting (1d)
|
||||
37. **#32** CSRF protection (2-3h)
|
||||
38. **#33** Begin button (Today page) (2-3h)
|
||||
39. ~~**#34**~~ ~~Auto lunch protection~~ ✅ — complete
|
||||
40. **#37** Booking conflict detection (2-3h)
|
||||
41. **#45** Loyalty stamp redemption (partial — UI + redeem endpoint, payment apply blocked on E1)
|
||||
42. ~~**#46**~~ ~~Staff management~~ 🗑️ — single employee business
|
||||
43. **#47** Recurring bookings (3-5d)
|
||||
44. ~~**#48**~~ ~~Waitlist functionality~~ 🗑️ — not desired
|
||||
45. ~~**#49**~~ ~~Image optimization~~ ✅ — complete
|
||||
46. **#42** Dark mode (1-2d)
|
||||
47. **#43** PWA support (3-5d)
|
||||
48. **Gift card management UI** — Create/redeem gift cards, track balances (E4 backend + admin payment UI complete, management UI pending)
|
||||
|
||||
### Phase 6 — Latest (Week 6+)
|
||||
|
||||
49. ~~**#52**~~ ~~Referral code registration~~ ✅ — backend + frontend complete
|
||||
50. ~~**#53**~~ ~~Admin schedule page~~ ✅ — weekly calendar view complete
|
||||
51. ~~**#54**~~ ~~BookingFlow welcome step~~ ✅ — guest login encouragement complete
|
||||
52. ~~**#55**~~ ~~Shared format utilities~~ ✅ — format.ts complete
|
||||
53. ~~**#56**~~ ~~patch_test_duration_hours on services~~ ✅ — auto-creates patch tests
|
||||
54. ~~**#57**~~ ~~created_by_name on bookings~~ ✅ — admin booking details
|
||||
55. ~~**#58**~~ ~~Admin login redirect~~ ✅ — redirects to /today
|
||||
|
||||
### ⏳ Waiting on External Access
|
||||
|
||||
| Item | Blocked On | Unblocks |
|
||||
|------|-----------|----------|
|
||||
| ~~**E1 Square payment**~~ | ✅ **Complete** | ✅ E2, E3, E4 complete — multi-method payments (Card/Cash/Gift Card), saved cards, tips |
|
||||
| **E5 Email/SMS** | SMTP provider (Resend/SendGrid/Twilio) | E6, E7, booking reminders, password reset emails |
|
||||
| **E8 S3/R2 production** | Cloudflare R2 or AWS S3 credentials | Portfolio images in production builds |
|
||||
| **E9 Social auth** | OAuth app registrations (Google/Microsoft/Facebook) | Social login flow |
|
||||
| **E10 Sentry** | Sentry DSN or equivalent | Error tracking, 5xx alerting |
|
||||
- Delete account endpoint, GDPR data export, delete_guest_user SQL function, GDPR anonymization
|
||||
- Walk-in guest booking, walk-in slot blocking, reservation transitions
|
||||
- ApprovalModal decline/cancel, admin notification panel, admin schedule page
|
||||
- BookingFlow welcome step, shared format utilities, patch_test_duration_hours
|
||||
- created_by_name on bookings, admin login redirect, customer relationship view
|
||||
- Admin UI for edit requests, booking rescheduling (user + admin)
|
||||
- Auto lunch protection, health check endpoint, graceful shutdown
|
||||
- HSTS + Referrer-Policy headers, console.log cleanup, alert prototypes
|
||||
- Idempotency keys for bookings, loyalty stamp redemption + discount system
|
||||
- Test DB optimization, image optimization, image deletion fix
|
||||
- User notification preferences UI, payment integration (Square), deposit payments
|
||||
- Tips UI, gift card system (backend + admin UI), referral code registration
|
||||
- CharCounter, loginInProgress rate limiting, profile picture limits, formatDateISO
|
||||
- Portfolio upload limits, JWT revocation, MapLibre GL, NavBar enhancements
|
||||
- TodayCalendar improvements, Svelte 5 $derived, SvelteDate, $app/paths resolve
|
||||
- Vite WASM support, portfolio multi-format images, loyalty stamp redesign
|
||||
- Admin role restrictions, account restrictions, BookingsByCreatedRange, enriched edit requests
|
||||
- Email check endpoint, PhoneInput, business settings API, gift card audit log
|
||||
- Gift card expiry, idle account cleanup, inventory cards, expired balance recovery
|
||||
- Till sale idempotency, VAT on till sales, test infrastructure upgrades
|
||||
- Financial data retention, gift card transaction audit log
|
||||
|
||||
@@ -7,7 +7,7 @@ Complete reference for the loyalty and discount system, written for four audienc
|
||||
## A) New Customer — "How do discounts work?"
|
||||
|
||||
### Loyalty Card
|
||||
Every time you complete a paid appointment, you earn **1 stamp** (max 1 per day — so two appointments on the same day only count once). Free appointments don't earn stamps.
|
||||
Every time you complete a paid appointment, you earn **1 stamp** (max 1 per day — two appointments on the same day only count once). Free appointments (£0 total) don't earn stamps.
|
||||
|
||||
After **10 stamps**, you get a **10% discount** automatically applied to your next paid appointment. After that discount is used, your stamps reset and you start collecting again.
|
||||
|
||||
@@ -55,6 +55,9 @@ Each discount appears as a separate discount line on the booking. A £100 bookin
|
||||
- **Total discount: £15.00**
|
||||
- Customer pays: £85.00
|
||||
|
||||
### Tip calculation
|
||||
Tip percentages (10%, 15%, 20%) in the Take Payment modal are calculated on the **net total after discounts** (`subtotal - discountSum`), not the pre-discount subtotal. This means customers tip on what they actually owe.
|
||||
|
||||
---
|
||||
|
||||
## C) Technical Admin — "How do I manage campaigns?"
|
||||
@@ -77,12 +80,12 @@ draft → active → completed
|
||||
|
||||
### Campaign Types
|
||||
|
||||
| Type | `campaign_type` | `milestone_type` | Trigger |
|
||||
|---|---|---|---|
|
||||
| Time-based | `time_based` | — | Date range (start_date to end_date) |
|
||||
| Per-user milestone | `milestone` | `per_user_booking_count` | User's personal completed booking count hits exact value |
|
||||
| Global milestone | `milestone` | `global_booking_count` | Salon-wide completed booking count hits exact value |
|
||||
| Anniversary | `milestone` | `anniversary` | Time since user's first completed booking (months/years) |
|
||||
| Type | Trigger |
|
||||
|---|---|
|
||||
| Time-based | Date range (start_date to end_date) |
|
||||
| Per-user milestone | User's personal completed booking count hits exact value |
|
||||
| Global milestone | Salon-wide completed booking count hits exact value |
|
||||
| Anniversary | Time since user's first completed booking (months/years) |
|
||||
|
||||
### Configuration Fields
|
||||
|
||||
@@ -100,10 +103,10 @@ draft → active → completed
|
||||
| Discount | Dedup mechanism |
|
||||
|---|---|
|
||||
| Loyalty | One pending redemption per user; consumed on use |
|
||||
| Time-based | Single best selected (`ORDER BY discount_percent DESC LIMIT 1`) |
|
||||
| Per-user milestone | `NOT EXISTS` check on `booking_discounts` — one per user per campaign |
|
||||
| Time-based | Single best selected (highest % wins) |
|
||||
| Per-user milestone | One per user per campaign |
|
||||
| Global milestone | `times_redeemed < max_redemptions` |
|
||||
| Anniversary | `NOT EXISTS` check on `booking_discounts` — one per user per campaign |
|
||||
| Anniversary | One per user per campaign |
|
||||
|
||||
### Database Tables
|
||||
|
||||
@@ -118,7 +121,7 @@ draft → active → completed
|
||||
|
||||
## D) Programmer — "Give me the full spec"
|
||||
|
||||
### Stamp Earning (ProgressBookingHandler, bookings.go ~L2035)
|
||||
### Stamp Earning (ProgressBookingHandler)
|
||||
|
||||
```
|
||||
On booking completion:
|
||||
@@ -126,7 +129,7 @@ On booking completion:
|
||||
UPDATE users SET loyalty_stamps += 1
|
||||
WHERE NOT EXISTS (
|
||||
another completed booking for this user
|
||||
with updated_at >= CURRENT_DATE - 1 day
|
||||
with same-day completion
|
||||
)
|
||||
IF newStampCount == 10:
|
||||
INSERT INTO loyalty_redemptions (status='pending', stamps_redeemed=10)
|
||||
@@ -136,24 +139,24 @@ On booking completion:
|
||||
- 1-stamp-per-day enforced via `NOT EXISTS` subquery on same-day completions
|
||||
- Free bookings (`bookingTotal == 0`) skip entirely
|
||||
|
||||
### Discount Application Order (ProgressBookingHandler, bookings.go ~L2005-2195)
|
||||
### Discount Application Order (ProgressBookingHandler)
|
||||
|
||||
All 5 discount types execute **unconditionally and independently** within `if bookingTotal > 0`. Each creates:
|
||||
1. A `booking_discounts` row
|
||||
2. A `payments` row with `payment_method = 'discount'`, `payment_type = 'partial'`
|
||||
|
||||
#### Step 1: Loyalty Redemption (~L2005-2033)
|
||||
#### Step 1: Loyalty Redemption
|
||||
Finds the oldest pending, unexpired redemption for this user. Applies 10% of `bookingTotal`. Updates redemption to `status = 'applied'`. Resets stamps: `loyalty_stamps = GREATEST(0, stamps - 10)`. Oldest pending redemption used first (FIFO).
|
||||
|
||||
```sql
|
||||
SELECT id FROM loyalty_redemptions
|
||||
WHERE user_id = $1 AND status = 'pending' AND expires_at > NOW()
|
||||
ORDER BY redeemed_at ASC LIMIT 1
|
||||
```
|
||||
- Applies 10% of `bookingTotal`
|
||||
- Updates redemption: `status = 'applied'`
|
||||
- Resets stamps: `loyalty_stamps = GREATEST(0, stamps - 10)`
|
||||
- Oldest pending redemption used first (FIFO)
|
||||
|
||||
#### Step 2: Time-Based Campaign (~L2065-2091)
|
||||
#### Step 2: Time-Based Campaign
|
||||
Selects the single highest-% active campaign whose date range covers today.
|
||||
|
||||
```sql
|
||||
SELECT id, discount_percent FROM discount_campaigns
|
||||
WHERE status = 'active' AND campaign_type = 'time_based'
|
||||
@@ -161,14 +164,11 @@ AND start_date <= NOW() AND end_date >= NOW()
|
||||
AND (max_redemptions IS NULL OR times_redeemed < max_redemptions)
|
||||
ORDER BY discount_percent DESC LIMIT 1
|
||||
```
|
||||
- Single highest-% campaign selected
|
||||
- `booking_discounts.campaign_type = 'time_based'`
|
||||
- Increments `times_redeemed`
|
||||
|
||||
#### Step 3: Per-User Milestone (~L2093-2119)
|
||||
#### Step 3: Per-User Milestone
|
||||
Checks the user's completed booking count (including the current booking). If it matches a `per_user_booking_count` milestone value, applies the discount. Dedup: one per user per campaign.
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'
|
||||
-- then:
|
||||
SELECT id, discount_percent FROM discount_campaigns
|
||||
WHERE status = 'active' AND campaign_type = 'milestone'
|
||||
AND milestone_type = 'per_user_booking_count'
|
||||
@@ -176,27 +176,22 @@ AND milestone_value = $userBookingCount
|
||||
AND NOT EXISTS (SELECT 1 FROM booking_discounts
|
||||
WHERE user_id = $1 AND source_id = discount_campaigns.id)
|
||||
```
|
||||
- `userBookingCount` includes the current booking (status already set to 'completed')
|
||||
- Exact match on `milestone_value`
|
||||
- Dedup: `NOT EXISTS` on `booking_discounts` per user per campaign
|
||||
|
||||
#### Step 4: Global Milestone (~L2121-2145)
|
||||
#### Step 4: Global Milestone
|
||||
Checks the salon-wide completed booking count. If it matches a `global_booking_count` milestone value, applies the discount. Capped by `max_redemptions`.
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*) FROM bookings WHERE status = 'completed'
|
||||
-- then:
|
||||
SELECT id, discount_percent FROM discount_campaigns
|
||||
WHERE status = 'active' AND campaign_type = 'milestone'
|
||||
AND milestone_type = 'global_booking_count'
|
||||
AND milestone_value = $globalCount
|
||||
AND (max_redemptions IS NULL OR times_redeemed < max_redemptions)
|
||||
```
|
||||
- No user-level dedup — relies on `max_redemptions` cap
|
||||
- Exact match on `milestone_value`
|
||||
|
||||
#### Step 5: Anniversary (~L2147-2195)
|
||||
#### Step 5: Anniversary
|
||||
Calculates elapsed time since the user's first completed booking. Matches if `computed >= milestone_value`. Dedup: one per user per campaign.
|
||||
|
||||
```sql
|
||||
SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'
|
||||
-- then iterate:
|
||||
SELECT id, discount_percent, milestone_value, milestone_unit
|
||||
FROM discount_campaigns
|
||||
WHERE status = 'active' AND campaign_type = 'milestone'
|
||||
@@ -205,12 +200,10 @@ AND NOT EXISTS (SELECT 1 FROM booking_discounts
|
||||
WHERE user_id = $1 AND source_id = discount_campaigns.id
|
||||
AND milestone_type = 'anniversary')
|
||||
```
|
||||
- Calculates elapsed time since first completed booking
|
||||
|
||||
- `months`: `elapsed.Hours() / (30 * 24)`
|
||||
- `years`: `elapsed.Hours() / (365.25 * 24)`
|
||||
- Matches if `computed >= milestone_value`
|
||||
- Dedup: `NOT EXISTS` on `booking_discounts` per user per campaign
|
||||
- `break` after first match (only one anniversary discount per booking)
|
||||
- First match only (one anniversary discount per booking)
|
||||
|
||||
### Discount Amount Calculation
|
||||
|
||||
@@ -227,8 +220,8 @@ name VARCHAR(100)
|
||||
campaign_type ENUM('time_based', 'milestone')
|
||||
discount_percent NUMERIC(5,2)
|
||||
status ENUM('draft', 'active', 'completed', 'cancelled') DEFAULT 'draft'
|
||||
start_date TIMESTAMPTZ -- time_based only
|
||||
end_date TIMESTAMPTZ -- time_based only
|
||||
start_date TIMESTAMPTZ
|
||||
end_date TIMESTAMPTZ
|
||||
milestone_type ENUM('per_user_booking_count', 'global_booking_count', 'anniversary')
|
||||
milestone_value INT
|
||||
milestone_unit ENUM('bookings', 'months', 'years')
|
||||
@@ -250,7 +243,7 @@ id CHAR(12) PK
|
||||
booking_id CHAR(12) FK -> bookings
|
||||
user_id CHAR(12) FK -> users
|
||||
discount_source ENUM('loyalty', 'campaign')
|
||||
source_id CHAR(12) -- loyalty_redemptions.id or discount_campaigns.id
|
||||
source_id CHAR(12)
|
||||
campaign_type ENUM('time_based', 'milestone')
|
||||
milestone_type ENUM('per_user_booking_count', 'global_booking_count', 'anniversary')
|
||||
discount_percent NUMERIC(5,2)
|
||||
@@ -286,18 +279,13 @@ Customer pays: **£75.00**
|
||||
|
||||
## E) Customer Relationship Metric Separation
|
||||
|
||||
To maintain clear and accurate accounting for each client:
|
||||
1. **Total Spend** represents actual card, cash, and gift card payments only. It excludes discount amounts, giving an accurate count of actual business revenue received from the customer.
|
||||
To maintain clear accounting for each client:
|
||||
1. **Total Spend** represents actual card, cash, and gift card payments only. Excludes discount amounts — accurate count of actual revenue received from the customer.
|
||||
2. **Total Saved** represents the sum of all loyalty card redemptions and campaign discounts applied to the customer's completed bookings.
|
||||
|
||||
These are computed automatically by the `/api/admin/users/{id}/relationship` endpoint on the backend and displayed as separate, side-by-side metric cards inside the administrator's **User Details** modal under the "Customer Relationship" section.
|
||||
These are computed by the `/api/admin/users/{id}/relationship` endpoint and displayed as side-by-side metric cards in the admin User Details modal under "Customer Relationship".
|
||||
|
||||
---
|
||||
|
||||
## F) Frontend UI Enhancements
|
||||
|
||||
To deliver an incredibly clear and robust experience for administrators and customers:
|
||||
1. **Tip Suggestions on Net Total**: Tip percentages (10%, 15%, 20%) in the `Take Payment` modal are calculated dynamically on the **net total after discounts** (`subtotal - discountSum`) instead of the pre-discount subtotal.
|
||||
2. **z-index Layering Resolution**: The `Booking Details` modal uses elevated Svelte backdrop-overlay and content layers (`!z-[60]`) to ensure it always opens smoothly in front of the `User Details` modal (`z-50`) without being obscured.
|
||||
3. **Visually Distinguished Negative Discounts**: Under "Payment History" in both Admin and Customer views, applied discounts are clearly presented as negative numbers (e.g., `-£2.50`) to visually distinguish them from customer cash/card payments.
|
||||
4. **Chronological Discount Payment Matching**: To prevent duplicate descriptions on multiple discounts with identical amounts, Svelte uses index-based chronological lookup to match each discount payment row uniquely to its exact Campaign or Loyalty source.
|
||||
### Frontend display notes
|
||||
- Discounts shown as negative numbers (e.g., `-£2.50`) in Payment History to distinguish from customer cash/card payments
|
||||
- Booking Details modal uses elevated z-index (`!z-[60]`) to open in front of User Details modal (`z-50`)
|
||||
- Chronological index-based matching prevents duplicate descriptions on multiple discounts with identical amounts
|
||||
|
||||
+197
-227
@@ -1,259 +1,229 @@
|
||||
# Crussell — Overview
|
||||
|
||||
**Last Updated:** May 2026
|
||||
**Status:** Local development — not yet in production
|
||||
Full-stack booking platform for a UK sole-trader nail artist. Go 1.25 backend, SvelteKit 5 SPA frontend, PostgreSQL 17, Docker Compose. Europe/London timezone only (Cloudflare geo-blocks non-UK). No timezone conversion — times shown are actual salon times.
|
||||
|
||||
---
|
||||
|
||||
## What is Crussell?
|
||||
## Features
|
||||
|
||||
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.
|
||||
### Booking
|
||||
|
||||
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.
|
||||
Three booking flows, each with its own entry point and reservation TTL:
|
||||
|
||||
---
|
||||
|
||||
## 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, $derived, SvelteDate), Tailwind CSS, shadcn-svelte, bits-ui, MapLibre GL JS |
|
||||
| **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+)
|
||||
- **Referral code support**: optional 12-character alphanumeric code during registration; validated against `users.referral_code`, relationship recorded in `user_referrals` table
|
||||
- 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
|
||||
- **Admin login redirect**: admins are redirected to `/today` after login instead of the home page
|
||||
- **JWT revocation with JTI**: every JWT includes a unique `jti` claim (UUID v4) for revocation tracking. In-memory map of revoked JTIs with 5-minute cleanup ticker. `POST /api/logout` revokes the current token. Refresh handler revokes old JTI before issuing new token
|
||||
|
||||
### 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)
|
||||
- **Enriched edit requests**: side-by-side original vs proposed snapshots with service details, end-time calculation, and user info
|
||||
- Admin booking service editing with overlap detection and price/duration overrides
|
||||
- Idempotency keys for booking deduplication
|
||||
- **`created_by_name`**: admin booking details include the name of the admin who created the booking
|
||||
|
||||
### 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) — with full UI for creation and management
|
||||
- Available hours calculation accounting for bookings, blockers, and gaps
|
||||
- **GetBookingsByCreatedRange**: admin endpoint to query bookings within a created_at date range
|
||||
- **Admin schedule page** (`/admin/schedule`): Google Calendar-style week view with drag-scroll, booking details modal, working hours overlays, and status-coloured booking bars
|
||||
|
||||
### Customer Features
|
||||
- Profile management with profile picture upload (cropper, separate S3 bucket)
|
||||
- Loyalty stamps display (redesigned fuchsia-themed stamp card with procedural SVG flower-petal stamps)
|
||||
- **Admin account restrictions**: Admin users see simplified account page — no History tab, no Referral tab, no Danger Zone (delete account), no loyalty stamp card
|
||||
- Booking history with cancel/reschedule
|
||||
- Calendar export (.ics download)
|
||||
- Portfolio browsing with tag/category filtering (multi-format: AVIF/WebP/JPEG/JXL via `<picture>` element)
|
||||
- **Contact page map**: Interactive MapLibre GL map showing salon location with marker, popup, and zoom controls
|
||||
- **Online payments**: pay deposits, pay early, partial payments, balance payments, tips on completed bookings
|
||||
- **Saved cards**: manage cards in Account → Cards tab — add/remove cards for faster checkout (soft-deleted on removal, 7-year retention)
|
||||
- **Tip page**: percentage-based tips (10%, 15%, 20%) or custom amount on completed bookings via `/pay-tip/[id]`
|
||||
- **Auto-select**: booking flows automatically select the first available date when availability data loads
|
||||
- **BookingFlow welcome step**: unauthenticated users see a welcome card (Step 0) encouraging login before guest checkout, with messaging about lost loyalty stamps and seasonal discounts
|
||||
|
||||
### Admin Features
|
||||
- Today page (/today): current/next appointment, today's interactive calendar grid, pending approvals, today stats summary
|
||||
- Admin dashboard (/admin): services CRUD, user management, bookings list, scheduling, time blockers UI
|
||||
- **Admin schedule page** (`/admin/schedule`): Google Calendar-style week view with drag-scroll, booking details, working hours overlays
|
||||
- Walk-in booking wizard (3-step) with slot reservation
|
||||
- Call-in booking wizard (4-step) with slot reservation
|
||||
- **Reschedule modal**: full reschedule UI on Today page — search available slots, detect conflicts, one-click confirm
|
||||
- **Time blockers UI**: create/manage one-off and recurring blockers from Admin dashboard with overlap detection
|
||||
- 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)
|
||||
- **Multi-method payments**: Square Terminal (card), cash (with change calculation and "keep change as tip"), gift card (12-digit ID)
|
||||
- **Service price overrides**: admin can adjust individual service prices directly in the payment modal
|
||||
- **Tip selection**: 10%, 15%, 20% presets or custom amount during card payments
|
||||
- **Refunds**: process partial or full refunds on completed payments
|
||||
- **Financial tracking**: fees column on payments for actual Square deductions, deposit batch tracking for bank reconciliation
|
||||
- **GetBookingsByCreatedRange**: query bookings by created_at date range for admin reporting
|
||||
|
||||
### 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
|
||||
- Users with `deposits_required > 0` limited to one active booking at a time
|
||||
- Online deposit payments via Square (Web Payments SDK)
|
||||
|
||||
### Square Payment Integration
|
||||
- **Two client implementations**: dev mock (`square_dev.go`, `//go:build dev`) simulates async checkout with polling for Terminal + online payments; prod stub (`square.go`, `//go:build !dev`) connects to live Square API
|
||||
- **Payment types**: deposit, full, partial, balance, tip
|
||||
- **Payment methods**: online_square (Web Payments SDK), in_person_card (Square Terminal), cash, giftcard, discount
|
||||
- **Saved cards**: `user_saved_cards` table with soft delete (`retained_until` for 7-year UK financial retention). Users can add cards manually via Account → Cards tab or save during checkout.
|
||||
- **Refunds**: `refunds` table tracks partial/full refunds linked to original payments
|
||||
- **Webhooks**: Square webhook handler at `/api/webhooks/square` for payment status updates
|
||||
- **Idempotency**: all payments use idempotency keys to prevent duplicates
|
||||
- **Fees tracking**: `payments.fees` column stores actual Square deductions for tax reporting
|
||||
- **Deposit batch tracking**: `square_deposits` table for bank reconciliation (matching Square batch deposits to Mettle account)
|
||||
- **Amount precision**: API responses use pence (int64) for all monetary values to avoid floating-point issues; internal DB storage uses pounds (float)
|
||||
|
||||
### Data & Compliance
|
||||
- **GDPR Article 15 Subject Access Request**: Full data export via `/gdpr` frontend page and `GET /api/user/gdpr-export` async endpoint. 12h in-memory cache with background generation (navigation away doesn't cancel). 16-section JSON export covering: user profile, bookings (with override pricing and total_price), payments, patch tests, referrals, notification preferences, saved cards, refunds, social logins, loyalty redemptions (with booking date/services), booking discounts (with campaign names), edit requests, affiliate payouts, verification codes, forgiven no-shows. Frontend: skeleton loading, 2s polling, styled report cards/tables, PDF export (print CSS hides navbar + verification banner), raw JSON download. Conditional rendering for empty sections, VAT breakdowns (hidden until any payment has non-zero VAT), deposits required (hidden when 0).
|
||||
- GDPR anonymization: guest PII scrubbed 6 months after booking start_time. `AnonymizeStaleGuestAccounts()` extended with additional field scrubbing (profile_pic_url, referral_code, notes, data_retention_consent).
|
||||
- Registered user account deletion via `anonymize_user()` SQL function — extended with child table PII scrubbing (social logins deleted, saved cards soft-deleted with PCI data cleared, verification codes expired, time blocker reservations scrubbed, edit request notes nulled, notification preferences deleted). `DeleteAccountHandler` extended with external system scrubbing — S3 profile picture deletion, Square saved card deletion — before SQL-level anonymization.
|
||||
- GDPR data export function (`export_all_user_data()`) — **wired to async Go endpoint with 12h cache**
|
||||
- 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
|
||||
- **UK financial data retention**: 7-year minimum for payment records. Saved cards soft-deleted with `retained_until` timestamp.
|
||||
|
||||
### 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) — multi-format pipeline (AVIF, WebP, JPEG, optional JXL) with client-side WASM encoding
|
||||
- Square: payment processing — in-person Terminal + online Web Payments SDK. Dev mock (`//go:build dev`) simulates async checkout; prod stub (`//go:build !dev`) connects to live Square API. Build-tagged swap with no code changes.
|
||||
- **MapLibre GL JS**: Interactive maps via reusable Svelte component library (`Map`, `MapMarker`, `MapControls`, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Booking Flows
|
||||
|
||||
| Flow | Who | Entry Point | Reservation TTL |
|
||||
|------|-----|-------------|-----------------|
|
||||
| Flow | Who | Entry | 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. All booking flows feature **auto-select** — the first available date is automatically selected when availability data loads.
|
||||
Slot reservations stored as `time_blocker` entries with `RESERVATION:*` descriptions — no separate reservation table. They automatically participate in availability calculations. Anonymous reservation cap: 50 per 10-minute rolling window (429 if exceeded).
|
||||
|
||||
---
|
||||
Service eligibility filters by age (min age on service) and patch test validity (6-month expiry, 24-hour notice period). Guest accounts are disposable — no identity tracking across bookings, PII scrubbed 6 months after appointment.
|
||||
|
||||
## Quick Links
|
||||
Idempotency keys (`idempotency_key VARCHAR(64) UNIQUE`) on bookings prevent duplicates on retry.
|
||||
|
||||
- [[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
|
||||
### Payments
|
||||
|
||||
---
|
||||
Multi-method payment modal for admin: Card (Square Terminal), Cash (with change calculation + "keep change as tip"), Gift Card (12-digit ID or account balance). User-facing payment modal for online deposits, partial payments, full payments, balance payments, and tips on completed bookings.
|
||||
|
||||
## Test Coverage
|
||||
Square integration has two build-tagged implementations:
|
||||
- **Dev** (`//go:build dev`): Mock client simulates async checkout with polling. No real payments.
|
||||
- **Prod** (`//go:build !dev`): Connects to live Square API. Requires Square credentials in `.env`.
|
||||
|
||||
**576/579 tests passing** (3 skipped) across 12+ test packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, cash/gift card payments, enriched edit request workflows, GetBookingsByCreatedRange endpoint, scheduling exceptional hours, referral code validation, JWT revocation via JTI logout, and GDPR compliance (export handler cache states, anonymize_user child table scrubbing, export_all_user_data 16-section export, AnonymizeStaleGuestAccounts field scrubbing).
|
||||
Saved cards stored in `user_saved_cards` with soft delete (`retained_until` for 7-year UK compliance). Refunds tracked in `refunds` table — partial or full. Square webhooks at `/api/webhooks/square` for payment status updates.
|
||||
|
||||
| Package | Coverage Area |
|
||||
|---------|--------------|
|
||||
| `handlers/auth` | Authentication (login, register, refresh, verification) |
|
||||
| `handlers/bookings` | User booking flow, guest bookings, reservations, edit requests (create/delete/view/enriched), approval/rejection, time-blocker lifecycle, exceptional hours validation, cancellation cleanup, cross-user isolation |
|
||||
| `handlers/payments` | Square payments (terminal, online, refunds, tips, saved cards) |
|
||||
| `internal/square` | Square client interface, dev mock, prod stub |
|
||||
| `handlers/admin` | Admin bookings, today view, users, services, GetBookingsByCreatedRange |
|
||||
| `handlers/scheduling` | Working hours, exceptional groups, available hours, time blockers, exceptional hours validation |
|
||||
| `handlers/services` | Service eligibility (age + patch test filtering) |
|
||||
| `handlers/user` | User profile, guest creation, loyalty, GDPR export handler, anonymize_user child table scrubbing, export_all_user_data comprehensive export, AnonymizeStaleGuestAccounts field scrubbing |
|
||||
| `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 |
|
||||
Fees column on `payments` stores actual Square deductions. `square_deposits` table for bank reconciliation (matching batch deposits to Mettle account).
|
||||
|
||||
### 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
|
||||
### Gift Cards
|
||||
|
||||
---
|
||||
Admin creates gift cards with four payment methods: cash, card machine, online card entry, or `on_the_house` (giveaway). Users can buy gift cards online via Square with idempotency.
|
||||
|
||||
## Seed Data Summary
|
||||
Three card types:
|
||||
- **Standard**: Purchased with balance > 0, 24-month rolling expiry
|
||||
- **Inventory**: Zero-amount physical stock cards, topped up later at the till
|
||||
- **Redeemed**: Balance moved to user's pooled account (`user_giftcard_balances`)
|
||||
|
||||
Running `local-dev-2.sh` creates:
|
||||
Every action on a card is recorded in `gift_card_transactions` — purchase, topup, redeem, expire — with reference tracking to till sales and API calls.
|
||||
|
||||
Expiry is 24 months from last use (not from purchase). Each use resets the timer. Expired balances move to `gift_card_expired_balances` — only account ID + amount stored (no PII), recoverable by admin with audit trail. `CleanupExpiredGiftCards()` runs on every availability fetch.
|
||||
|
||||
Accounts idle 2+ years (no balance) or 5+ years (with balance) are anonymized. Balances before deletion move to `gift_card_expired_balances`. `CleanupIdleAccounts()` runs on availability fetch.
|
||||
|
||||
VAT treatment: gift cards are Single-Purpose Vouchers (SPVs) by default — VAT charged at purchase, not redemption. Configurable to Multi-Purpose Voucher (MPV) in business settings.
|
||||
|
||||
### Scheduling
|
||||
|
||||
Default weekly hours stored in `working_hours` table. Exceptional groups use a three-table design: group metadata, 7-day hours per group, and week-range applications. Merged via `GetWorkingHours()` with `source` field ("default" or "exceptional").
|
||||
|
||||
Available hours calculated by loading working hours, subtracting existing bookings (with gap logic), subtracting time blockers (including reservations). Late-night lock: after 22:00, blocks next morning 00:00–11:00 for non-admin users.
|
||||
|
||||
Time blockers: one-off (no cron) or recurring (cron expression via `robfig/cron/v3`). Created from Admin dashboard with overlap detection against existing bookings. Visible on Today page calendar grid as red/hatched bars.
|
||||
|
||||
Lunch protection: `findAllLunchGaps()` returns all gap durations in the middle window sorted descending. Shared `buildLunchProtection()` in `lib/utils/timeSlots.ts` consolidates logic across all booking flows. `shouldApplyLunchProtection()` skips protection on short days (≤5 hours).
|
||||
|
||||
### Admin
|
||||
|
||||
**Today page** (`/today`): current + next appointment cards, interactive daily calendar grid, pending approvals queue, today stats summary (total/confirmed/pending/completed). Walk-in booking wizard (3-step) and call-in wizard (4-step) with slot reservation.
|
||||
|
||||
**Admin dashboard** (`/admin`): users list + detail modal (profile, bookings, relationship data, patch tests, loyalty/referrals, privacy/consent), services CRUD, bookings list with search, scheduling management (default hours, exceptional groups, time blockers), discount campaigns, portfolio image upload with tag management, gift card management, business settings.
|
||||
|
||||
**Reschedule modal**: search available slots, conflict detection (overlapping bookings), one-click confirm. Side-by-side enriched snapshots in Pending Approvals for edit requests.
|
||||
|
||||
**Notifications**: pull-based queue with priority ordering. Bell icon with unread count. `/admin/notifications` page with acknowledge flow, pagination (20/page), "Show acknowledged" toggle.
|
||||
|
||||
### Loyalty & Discounts
|
||||
|
||||
1 stamp per completed paid booking (max 1 per calendar day). £0 bookings skip. At 10 stamps, a pending `loyalty_redemption` is created (6-month expiry). Next completed paid booking applies 10% discount and resets stamps (reset = `GREATEST(0, stamps - 10)`, +1 earned → net 1).
|
||||
|
||||
Discount campaigns: time-based (date range), per-user milestone (exact booking count), global milestone (salon-wide count, max redemptions cap), anniversary (time since first completed booking). All discounts stack additively against the **original** booking total — each creates its own `booking_discounts` row and discounted `payments` row.
|
||||
|
||||
Campaign lifecycle: `draft → active → completed` (or any → `cancelled`, `active → draft` for re-editing).
|
||||
|
||||
### Compliance
|
||||
|
||||
**GDPR Article 15**: Full data export via `/gdpr` frontend. Async Go endpoint (`GET /api/user/gdpr-export`) with 12h in-memory cache and background generation (navigation away doesn't cancel). 16-section JSON export: profile, bookings with overrides, payments, refunds, saved cards, social logins, loyalty redemptions, booking discounts, edit requests, affiliate payouts, verification codes, forgiven no-shows, patch tests, referrals, notification preferences, export metadata. Frontend: skeleton loading, 2s polling, styled report cards/tables, PDF export (print CSS hides navbar + verification banner), raw JSON download.
|
||||
|
||||
**Account deletion**: Registered users → `anonymize_user()` SQL function extended with child table PII scrubbing (social logins deleted, saved cards soft-deleted with PCI data cleared, verification codes expired, time blocker reservations scrubbed, edit request notes nulled, notification preferences deleted). External system scrubbing: S3 profile picture, Square saved cards. Guests → `delete_guest_user()` for full removal.
|
||||
|
||||
**Data retention**: Guest PII scrubbed 6 months post-appointment via `AnonymizeStaleGuestAccounts()`. Payment records retained 7 years (HMRC + Limitation Act), then aggregated into `financial_aggregates` (monthly totals, no PII) and deleted. Gift card dormant balances retained indefinitely in `gift_card_expired_balances` (no PII).
|
||||
|
||||
### Frontend
|
||||
|
||||
**Routing**: SvelteKit 5 static SPA with adapter-static. Routes: home, book (5-step wizard + welcome step for unauthenticated), login/register, account (profile, bookings, loyalty, saved cards, gift cards, notifications, GDPR export), admin dashboard, today page, portfolio (tag/category filtering + multi-format images), prices, contact (dynamic from first admin user + MapLibre map), schedule, `/admin/schedule` (week view), `/pay-tip/[id]`, `/gdpr`, `/admin/notifications`.
|
||||
|
||||
**Component library**: PaymentModal (multi-method, service price overrides, tip presets), UserPaymentModal (deposit/partial/full/balance), BookingFlow (5 steps, auto-select, shared timeSlots utils), TodayCalendar (interactive grid), PendingApprovals (dedup refresh), NavBar (responsive with notification badge), PhoneInput (UK validation), CharCounter (grapheme counter for notes), ImageVariant (multi-format `<picture>` element), MapLibre GL map components.
|
||||
|
||||
**State**: Svelte 5 runes (`$state`, `$bindable`, `$effect`, `$derived`, SvelteDate). Auth store wraps JWT in localStorage with auto-refresh (hourly, if <14 days remaining). Role-based UI via `hasRole()`, `isAdmin()`, `isVerified()`.
|
||||
|
||||
**Shared utilities**: `timeSlots.ts` (lunch protection, slot generation, formatting), `format.ts` (duration, date/time, age, ISO date), `phone.ts` (UK phone formatting).
|
||||
|
||||
### Infrastructure
|
||||
|
||||
**Docker Compose**: postgres:17 (init-script.sql mounted), backend (custom Go build, chi router, pgx pool), sabredav (php:8.2-fpm, CardDAV/CalDAV), nginx:stable (reverse proxy, static frontend, DAV proxy).
|
||||
|
||||
**Storage**: S3/R2 abstraction with build tags — RustFS in dev (local filesystem), Cloudflare R2 in prod (requires credentials). Portfolio: multi-format pipeline (AVIF/WebP/JPEG/JXL) with client-side WASM encoding via `@jsquash/*` and `@discourse/jxl`.
|
||||
|
||||
**Build tags**: `dev` vs `!dev` for Square client, S3 storage, DAV service. `test` for test files.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Single employee** — no multi-staff scheduling, no team management
|
||||
- **No email/SMS** — SMTP integration not wired; booking reminders, password resets, and notifications are UI-only (`user_notification_preferences` table exists but unused for delivery)
|
||||
- **No production S3/R2** — prod storage stubs return "not implemented" errors
|
||||
- **No social auth** — OAuth provider registrations pending (Google, Microsoft, Facebook)
|
||||
- **No dark mode, no PWA, no recurring bookings, no CSV/Excel export**
|
||||
- **Password reset flow exists backend-only — no frontend link or form**
|
||||
- **No error tracking** — Sentry DSN not configured, `log.Printf()` only
|
||||
- **No automated DB backups** — no `pg_dump` cron or point-in-time recovery
|
||||
- **No API documentation** — no OpenAPI/Swagger spec
|
||||
- **No rate limiting per-user** — currently IP-based only
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Tool | Version | Notes |
|
||||
|------|---------|-------|
|
||||
| Docker & Docker Compose | >= 20.10 | Compose V2 (`docker compose`, not `docker-compose`) |
|
||||
| Go | >= 1.22 | Tested with 1.25 |
|
||||
| Node | >= 18 | For frontend build |
|
||||
| tmux | >= 3.0 | For local-dev-2.sh dev helper |
|
||||
| psql | Any | Optional — for direct DB inspection during dev |
|
||||
| curl | Any | Optional — for testing API endpoints directly |
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Docker Compose (full stack)
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Required: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, JWT_SECRET_KEY
|
||||
# Square credentials optional (prod only, dev mock works without)
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
Services:
|
||||
|
||||
| Service | URL | Notes |
|
||||
|---------|-----|-------|
|
||||
| Frontend | http://localhost | SvelteKit static SPA served by Nginx |
|
||||
| API | http://localhost/api | Proxied to backend:8080 |
|
||||
| SabreDAV | http://localhost/dav | CardDAV contact sync |
|
||||
| PostgreSQL | localhost:5432 | Direct access with psql |
|
||||
|
||||
### Local Development (tmux)
|
||||
|
||||
```bash
|
||||
chmod +x local-dev-2.sh
|
||||
./local-dev-2.sh
|
||||
```
|
||||
|
||||
Creates a `crussell-dev` tmux session with 4 panes:
|
||||
1. `psql crussell` — direct DB console
|
||||
2. `go run` — backend dev server with `dev` build tag (hot-reload on save)
|
||||
3. `npm run dev` — SvelteKit dev server with HMR
|
||||
4. `Rustfs` — local S3-compatible file storage (for dev images)
|
||||
|
||||
Seeds the database with realistic test data:
|
||||
|
||||
| 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 |
|
||||
| Bookings | 43 (18 past, 4 today, 5 tomorrow, 16 upcoming) |
|
||||
| Guest Bookings | 3 |
|
||||
| Total Bookings | 43 |
|
||||
| Payments | 22 completed bookings |
|
||||
| Payments | 22 completed |
|
||||
| Time Blockers | 3 |
|
||||
| Schedule Groups | 3 |
|
||||
| Cancellations | 1 |
|
||||
|
||||
Default logins (password: `password`):
|
||||
- Admin: `admin@example.com`
|
||||
- User: `user@example.com`
|
||||
|
||||
## Building & Testing
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go build -o bin/backend ./main.go # Production build
|
||||
go build -tags dev -o bin/backend ./main.go # Dev build with Square mock
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run build # Production build to build/
|
||||
npm run dev # Dev server with HMR
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go test -tags "test,dev" -p 1 -count=1 ./... # 633/636 passing, 3 skipped
|
||||
go test -tags "test,dev" -v -run TestName ./... # Single test
|
||||
```
|
||||
|
||||
Test infrastructure notes:
|
||||
- Shared test database (`crussell_test`), sequential execution (`-p 1`)
|
||||
- `TestMain` per package — schema migration runs once per package
|
||||
- `TruncateTables()` between tests — `TRUNCATE TABLE ... CASCADE` (~60% faster than DROP+CREATE)
|
||||
- Advisory locks: `pg_advisory_lock(1337)` protects migration DDL, `pg_advisory_lock(1338)` prevents CASCADE deadlocks
|
||||
- Statement-by-statement SQL parser (`splitSQLStatements()`) respects dollar-quoted PL/pgSQL blocks
|
||||
- Build tag: all test files use `//go:build test`
|
||||
- Test JWT secret: `test-secret-key-for-testing-only`
|
||||
- Fixtures auto-generate unique emails
|
||||
|
||||
## Full Documentation
|
||||
|
||||
- [[User Manual]] — Customer journeys and FAQ, written for staff reference
|
||||
- [[Admin Manual]] — Admin workflows, today page, booking management, gift cards, scheduling
|
||||
- [[Technical Manual]] — Architecture, API reference, database schema, key system deep-dives
|
||||
- [[Future Work - Gap Backlog]] — Remaining MVP work and stretch goals
|
||||
- [[Loyalty & Discount System Reference]] — Full loyalty and discount spec for all audiences
|
||||
- [[Gift Card Terms & Conditions]] — Legal terms for gift card customers
|
||||
- [[Privacy Policy]] — GDPR privacy notice
|
||||
- [[Terms & Conditions - Overall App]] — Website terms of service
|
||||
- [[Testing Architecture & DB Management]] — Test patterns and DB management guide
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+285
-247
@@ -1,122 +1,104 @@
|
||||
# User Manual
|
||||
|
||||
How customers use the Crussell booking website — from first visit to managing their appointments.
|
||||
A staff guide to understanding the customer journey — what customers see, what they can do, and how to answer their questions.
|
||||
|
||||
---
|
||||
|
||||
## Booking Your First Appointment
|
||||
## What This Is
|
||||
|
||||
### Step 1: Arrive at the Website
|
||||
This document is written for **salon staff**. It is not a customer-facing document. It explains every customer journey so you know what the customer sees on their screen, what their options are, and what you should tell them when they call with questions.
|
||||
|
||||
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**
|
||||
Think of this as a reference guide for common customer scenarios. When a customer asks "Why can't I see my booking?" or "Where's my loyalty discount?" — the answer is in this document.
|
||||
|
||||
Tap or click **Book an Appointment** to start.
|
||||
---
|
||||
|
||||
**If you're not logged in**, you'll see a welcome screen before selecting services. This screen explains the benefits of logging in — specifically that guest bookings don't earn loyalty stamps or qualify for seasonal discounts. You can choose to:
|
||||
- **Log In** — takes you to the login/register page
|
||||
- **Continue as Guest** — proceeds to service selection without an account
|
||||
## Booking a Service (Self-Service)
|
||||
|
||||
### Step 2: Choose Your Services
|
||||
Customers book online through the website. The flow is five steps. The system handles most of the complexity automatically.
|
||||
|
||||
You'll see a list of all available treatments. Each one shows:
|
||||
- The name and a short description
|
||||
- The price
|
||||
- How long it takes
|
||||
### Step 0: Welcome (Unauthenticated Customers Only)
|
||||
|
||||
**Why might a service look greyed out?**
|
||||
If the customer is not logged in, they see a welcome card before they start booking. It explains:
|
||||
- Logged-in customers earn loyalty stamps and qualify for seasonal discounts
|
||||
- Guest bookings don't get stamps or discounts
|
||||
|
||||
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.
|
||||
**What to tell the customer:** "If you create an account, you'll earn stamps and get discounts. If you book as a guest, that's fine too — you'll still get your appointment, but you won't earn rewards."
|
||||
|
||||
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.
|
||||
### Step 1: Choose Services
|
||||
|
||||
**Why might a service not appear at all?**
|
||||
The customer sees a list of all available treatments. Each one shows the name, price, and duration. Some services may be greyed out or missing entirely.
|
||||
|
||||
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.
|
||||
**Why a service is greyed out:** It requires a patch test — a small allergy test. The customer must have a valid patch test on record (valid for 6 months, with a 24-hour waiting period after the test). If they don't have one, or it's expired, the service is greyed out.
|
||||
|
||||
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.
|
||||
**Why a service is missing:** It has a minimum age requirement. If the customer is logged in and their date of birth shows they're too young, the service is hidden. If they're not logged in, all services are shown (age is checked later).
|
||||
|
||||
**You can select multiple services** — they'll be added together for the total price and duration.
|
||||
**What to tell the customer:** "Some services need a patch test first. If you haven't had one, book a patch test appointment and come back for the main service after 24 hours."
|
||||
|
||||
### Step 3: Pick a Date
|
||||
The customer can select multiple services. The total price and duration add up automatically.
|
||||
|
||||
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)
|
||||
- A small dot appears under today's date to help orient you
|
||||
### Step 2: Pick a Date
|
||||
|
||||
**The system automatically selects the first available date** for you when the calendar loads, so you can skip straight to picking a time if the suggested date works. You can always click a different date if you prefer.
|
||||
A calendar appears. Highlighted dates have available slots. Greyed-out dates are fully booked or the salon is closed. The system automatically selects the first available date when the calendar loads.
|
||||
|
||||
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
|
||||
**What to tell the customer:** "The calendar shows the first available day automatically. If you want a different day, just click it."
|
||||
|
||||
Tap or click a date that works for you.
|
||||
### Step 3: Pick a Time
|
||||
|
||||
### Step 4: Pick a Time
|
||||
Available time slots for the chosen day appear as a scrollable list. The system calculates these by checking the salon's opening hours, existing bookings, blocked-out periods, and slots currently being held by other customers.
|
||||
|
||||
Once you've chosen a date, you'll see the available time slots for that day displayed as a scrollable list. Each slot shows the start time, and the system groups nearby slots together so you can see gaps at a glance. 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
|
||||
**Slot reservation:** When a customer picks a time, it's temporarily held:
|
||||
- Logged-in: held for **1 hour**
|
||||
- Guest (not logged in): held for **10 minutes**
|
||||
|
||||
**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 the customer takes too long and the hold expires, someone else can take the slot. The customer will need to pick a new time.
|
||||
|
||||
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.
|
||||
**What to tell the customer:** "Once you pick a time, it's held for you while you finish the form. If you're logged in, you have an hour. If you're booking as a guest, you have 10 minutes."
|
||||
|
||||
There's also a limit on how many times an unregistered visitor can hold slots — this prevents the system from being misused.
|
||||
### Step 4: Fill in Details
|
||||
|
||||
### Step 5: Fill in Your Details
|
||||
The customer enters:
|
||||
- First name and last name
|
||||
- Email address
|
||||
- Phone number (UK format, automatically formatted as they type)
|
||||
- Notes (optional)
|
||||
|
||||
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")
|
||||
**Email check:** If the customer enters an email that's already registered, the system checks their name and phone number too before suggesting they log in. This prevents false positives — if someone just happens to share an email with another customer, they won't be interrupted. If the system does suggest a login, the customer can either log in or use a different email.
|
||||
|
||||
**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.
|
||||
**What to tell the customer:** "If the system says 'Please log in,' that means there's already an account with that email. If it's really you, log in. If you don't have an account, use a different email."
|
||||
|
||||
**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.
|
||||
**Deposit check:** If the customer has outstanding deposit obligations from a previous late cancellation, they can only book appointments at least 24 hours away. Guest bookings skip this check.
|
||||
|
||||
All bookings must be made at least 1 hour before the appointment start time.
|
||||
|
||||
### Step 6: Confirmation
|
||||
### Step 5: Confirmation
|
||||
|
||||
After submitting, you'll see a confirmation screen showing:
|
||||
- Your appointment date and time
|
||||
- The services you selected (with any admin-adjusted prices or durations noted)
|
||||
- The total price
|
||||
- Payment status — if a deposit is required, you'll see the deposit amount and deadline
|
||||
- An **Add to Calendar** button — this downloads a calendar file you can import into Google Calendar, Apple Calendar, Outlook, or any other calendar app
|
||||
- A **View My Bookings** button that takes you to your Schedule page to see all upcoming appointments
|
||||
After submitting, the customer sees a confirmation screen showing:
|
||||
- Appointment date and time
|
||||
- Services booked (with any admin-adjusted prices or durations noted)
|
||||
- Total price
|
||||
- Payment status (if a deposit is required, the amount and deadline)
|
||||
- An **Add to Calendar** button — downloads a file they can import into any calendar app
|
||||
- A **View My Bookings** button — takes them to their schedule
|
||||
|
||||
**What to tell the customer:** "After booking, you'll see a confirmation page. You can add it to your phone's calendar. If you have an account, you can view all your bookings in your schedule."
|
||||
|
||||
---
|
||||
|
||||
## Guest Bookings — What You Should Know
|
||||
## Guest Bookings
|
||||
|
||||
### 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.
|
||||
If the customer books without creating an account, the system creates a temporary guest profile for that booking. It's disposable — there's no permanent identity. Each guest booking gets its own fresh guest profile.
|
||||
|
||||
### Can I use the same email for multiple guest bookings?
|
||||
### Can they 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?
|
||||
### What happens to their 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.
|
||||
Personal information (name, email, phone, date of birth) attached to a guest booking is automatically removed **6 months after the appointment date**. After that, the booking record still exists for the salon's financial records, but their personal details are replaced with anonymous placeholders.
|
||||
|
||||
**What to tell the customer:** "If you book as a guest, we keep your details for 6 months after your appointment, then they're removed automatically. If you want to keep your history, create an account."
|
||||
|
||||
---
|
||||
|
||||
@@ -124,149 +106,167 @@ Your personal information (name, email, phone, date of birth) attached to a gues
|
||||
|
||||
### 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
|
||||
Customers can register at the login page. They need:
|
||||
- **Name**: Letters, spaces, hyphens, apostrophes, and dots (1–50 characters)
|
||||
- **Phone number**: UK format — the system validates and formats it as they type. UK landlines are limited to 11 digits, mobile numbers to 11 digits. Non-diallable characters are filtered out automatically.
|
||||
- **Email address**: Standard email format
|
||||
- **Date of birth**: Must be at least 16 years old
|
||||
- **Password**: Up to 72 characters
|
||||
- **Referral code** (optional): If a friend gave you a referral code, enter it here. The code is 12 characters and will be formatted automatically as you type (xxxx-xxxx-xxxx). Using a referral code links your account to the person who referred you.
|
||||
- **Referral code** (optional): 12 characters, formatted as they type (xxxx-xxxx-xxxx). If they enter a valid code, the system links their account to the person who referred them.
|
||||
|
||||
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.
|
||||
After registering, the account starts in an "unverified" state. An email verification system exists on the backend but isn't fully connected to the frontend yet — they can still log in and book.
|
||||
|
||||
**What to tell the customer:** "After you register, you can log in and book straight away. Email verification isn't fully set up yet, but it won't stop you from using the site."
|
||||
|
||||
### 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.
|
||||
Customers enter their 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.
|
||||
Once logged in, they stay logged in for **30 days**. After that, they need to log in again. The system automatically refreshes their session in the background so they 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.
|
||||
A password reset system exists on the backend, but the "Forgot Password" link hasn't been added to the login page yet. If a customer has forgotten their password, they need to contact the salon directly.
|
||||
|
||||
**What to tell the customer:** "If you've forgotten your password, call us and we'll reset it for you. There's a backend system for this, but the button on the website isn't wired up yet."
|
||||
|
||||
---
|
||||
|
||||
## Your Account Page
|
||||
## Account Page
|
||||
|
||||
Once logged in, go to **Account** to manage your profile and bookings.
|
||||
Once logged in, customers can go to **Account** to manage their profile and bookings.
|
||||
|
||||
### Profile Information
|
||||
|
||||
You can view and edit:
|
||||
- Your name
|
||||
- Your email address
|
||||
- Your phone number
|
||||
- Your date of birth
|
||||
They can view and edit their name, email address, phone number, and 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
|
||||
They can upload a profile photo. A cropping tool lets them adjust which part is shown. The photo appears next to their name in the salon's contact system and is synced to the salon's address book so staff can recognise them when they 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.
|
||||
Every time they complete a paid appointment, they earn 1 loyalty stamp. They can see their 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 and you start earning again
|
||||
- If you don't use your discount within **6 months**, it expires
|
||||
- **1 stamp per completed paid appointment** (maximum 1 per day, even if they have multiple appointments on the same day)
|
||||
- **Appointments with a total price of £0** don't earn stamps
|
||||
- **10 stamps** → a 10% discount is automatically applied to their next completed paid appointment
|
||||
- **After the discount is used**, their stamp count resets and they start earning again
|
||||
- **If they don't use the discount within 6 months**, it expires
|
||||
|
||||
**What to tell the customer:** "You earn 1 stamp every time you complete a paid appointment. At 10 stamps, you get 10% off your next visit. It happens automatically — you don't need to do anything."
|
||||
|
||||
### Campaign Discounts
|
||||
|
||||
The salon occasionally runs promotions — like "10% off this week" or "15% off your 5th visit." If a campaign is active and you qualify, the discount is applied automatically when your appointment is completed. You don't need to do anything.
|
||||
The salon occasionally runs promotions. If a campaign is active and the customer qualifies, the discount is applied automatically when their appointment is completed.
|
||||
|
||||
### How Discounts Combine
|
||||
|
||||
Discounts **add together**. If you have a full loyalty card (10% off) AND there's an active "5% off this week" campaign, you get **15% off** — not just the better one. Every discount you qualify for stacks on top of the others. Each discount is calculated against the original booking total.
|
||||
Discounts **add together**. If a customer has a full loyalty card (10% off) AND an active campaign (5% off), they get **15% off** — each discount is calculated against the original booking total.
|
||||
|
||||
### Your Booking History
|
||||
**What to tell the customer:** "Your discounts stack. If you have a loyalty discount and a campaign is running, you get both."
|
||||
|
||||
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)
|
||||
### Booking History
|
||||
|
||||
Tap any booking to see full details.
|
||||
A list of all appointments — upcoming and past — is shown on the Account page. Each entry shows the date, time, services, total price, and current status.
|
||||
|
||||
### Calendar View
|
||||
### Schedule / 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.
|
||||
The **Schedule** page shows upcoming appointments in a calendar-style layout. Each appointment has an **Add to Calendar** button that downloads a file the customer can open in any calendar application.
|
||||
|
||||
### Saved Cards
|
||||
|
||||
If you have a verified account, you'll see a **Cards** tab on your Account page. This lets you manage your saved payment methods for faster checkout.
|
||||
Verified customers can manage saved payment methods in the **Cards** tab. They can add or remove cards. When a card is removed, it is soft-deleted and retained for 7 years for UK financial compliance.
|
||||
|
||||
**Add a Card:**
|
||||
1. Go to the **Cards** tab
|
||||
2. Click **Add a Card**
|
||||
3. Enter your card number (automatically formatted with spaces as you type)
|
||||
4. Enter the expiry date (MM/YY format)
|
||||
5. Enter the CVC (the 3- or 4-digit security code on the back of your card)
|
||||
6. Click **Add Card** — the system validates the card details before saving
|
||||
**What to tell the customer:** "You can save your card details for faster checkout. If you remove a card, we keep it for 7 years for tax records, but it's hidden from you."
|
||||
|
||||
**Remove a Card:**
|
||||
- Each saved card shows the brand (Visa, Mastercard, etc.), the last 4 digits, and the expiry date
|
||||
- Click **Remove** next to any card to delete it. The card is soft-deleted and retained for 7 years to comply with UK financial regulations.
|
||||
### Gift Cards
|
||||
|
||||
**Why save a card?**
|
||||
When you pay for deposits, early payments, or tips online, you can choose a saved card instead of entering your details each time. This makes checkout faster and more convenient.
|
||||
Customers can see their gift card details in the **Gift Cards** tab:
|
||||
|
||||
- Available gift card balance and any unredeemed cards
|
||||
- Gift cards have a **24-month expiry** from the last use — each time they use it, the expiry resets
|
||||
- **Redeem to balance**: They can move the full gift card value to their account balance. Once redeemed, the funds are held in their account and can be used across multiple bookings. A confirmation dialog explains how this works and what data is stored (just the amount and their account ID — no personal information)
|
||||
- Account balances (from redeemed gift cards) are shown as available credit when they pay online or at the till
|
||||
|
||||
**What to tell the customer:** "Your gift card lasts 24 months from the last time you use it. If you want to use it across multiple bookings, you can redeem it to your account balance. That way it doesn't expire and you can use it bit by bit."
|
||||
|
||||
---
|
||||
|
||||
## Cancelling an Appointment
|
||||
## Online Payments
|
||||
|
||||
Customers can pay online in several ways:
|
||||
|
||||
### Paying a Deposit
|
||||
|
||||
If they have deposit obligations, they can pay a deposit online before their appointment:
|
||||
|
||||
1. Go to **Schedule** from the menu
|
||||
2. Click on their upcoming booking
|
||||
3. Click **Pay Deposit** — the deposit amount (20% of the booking total) is shown
|
||||
4. They can pay the deposit, the full amount, or a partial amount
|
||||
5. Enter card details or use a saved card
|
||||
6. Confirm payment
|
||||
|
||||
If no deposit is owed but they haven't paid the full amount yet, they see a **Pay Early** button instead. They can also pay a partial amount using the **Pay Part** option. If they've already paid a deposit, they see a **Pay Balance** button to settle the remaining amount.
|
||||
|
||||
**What to tell the customer:** "If you owe a deposit, you can pay it online from your Schedule page. If you don't owe a deposit, you can pay early or pay part of the amount."
|
||||
|
||||
### Leaving a Tip
|
||||
|
||||
After their appointment is marked as **completed**, they can leave a tip:
|
||||
|
||||
1. Go to the tip page (the salon may send them a link, or they can access it from their booking confirmation)
|
||||
2. They see their appointment details — date, time, and services
|
||||
3. Choose a tip amount:
|
||||
- **Preset percentages**: 10%, 15%, or 20% of the booking total (the £ amount is shown for each)
|
||||
- **Custom amount**: Enter any amount they'd like to tip
|
||||
4. Confirm payment — their saved card can be used if they have one on file
|
||||
|
||||
**What to tell the customer:** "After your appointment, you can leave a tip online. You'll get a link from us, or you can find it in your booking confirmation."
|
||||
|
||||
---
|
||||
|
||||
## Cancelling and Rescheduling
|
||||
|
||||
### How to Cancel
|
||||
|
||||
From your Account page, find the booking you want to cancel and select the cancel option.
|
||||
From the Account page, the customer can find the booking and select the cancel option.
|
||||
|
||||
### The 24-Hour Rule
|
||||
|
||||
**Cancelling 24 hours or more before your appointment:**
|
||||
**Cancelling 24 hours or more before the 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).
|
||||
**Cancelling less than 24 hours before the appointment:**
|
||||
This counts as a late cancellation. The customer receives **3 deposit obligations**, which means they need to pay deposits before they can book again.
|
||||
|
||||
**If your booking is still pending (not yet confirmed by the salon):**
|
||||
**If the booking is still pending (not yet confirmed by the salon):**
|
||||
The booking is simply deleted — no penalty.
|
||||
|
||||
### Can a Late Cancellation Be Forgiven?
|
||||
**Can a late cancellation be forgiven?**
|
||||
Yes — the salon can forgive a late cancellation on a case-by-case basis. If the customer had a genuine emergency, you can waive the penalty. If forgiven, the cancellation is treated like a normal cancellation with no deposit obligations.
|
||||
|
||||
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 a customer needs to change their appointment time:
|
||||
|
||||
## Requesting to Reschedule
|
||||
|
||||
If you need to change your appointment time:
|
||||
|
||||
1. Go to your **Account** page and find the booking
|
||||
1. Go to their **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. Add any notes about the change (optional)
|
||||
5. Submit your reschedule request
|
||||
5. Submit their reschedule request
|
||||
|
||||
**What happens next:**
|
||||
- Your request goes to the salon for review
|
||||
- The salon sees a side-by-side comparison of your original booking versus the proposed changes
|
||||
- The request goes to the salon for review
|
||||
- The salon sees a side-by-side comparison of the original booking versus the proposed changes
|
||||
- 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
|
||||
- You can view all your pending reschedule requests from your account
|
||||
- If approved, the appointment time is updated to the new slot
|
||||
- If declined, the original appointment time stays the same
|
||||
- The customer can cancel their reschedule request at any time before the salon reviews it
|
||||
- They can view all their pending reschedule requests from their account
|
||||
|
||||
**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
|
||||
- If you cancel your booking entirely, any pending reschedule request is automatically removed
|
||||
**What to tell the customer:** "If you want to change your time, go to your Account page and request a reschedule. We'll see your request and either approve it or let you know if the time isn't available."
|
||||
|
||||
---
|
||||
|
||||
@@ -274,46 +274,19 @@ If you need to change your appointment time:
|
||||
|
||||
### 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**.
|
||||
Deposits are a way the salon protects against last-minute cancellations. If a customer cancels an appointment less than 24 hours before it starts (without the salon's forgiveness), they receive **3 deposit obligations**.
|
||||
|
||||
### What Does This Mean for You?
|
||||
### What This Means for Them
|
||||
|
||||
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
|
||||
While they have deposit obligations:
|
||||
- They can only book appointments that are **at least 24 hours away**
|
||||
- They can only have **one active booking at a time**
|
||||
- They need to complete appointments and pay for them to reduce their deposit count
|
||||
- A warning banner appears at the top of the booking flow explaining the limit
|
||||
|
||||
### How Do Deposits Go Down?
|
||||
### How 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.
|
||||
|
||||
### Paying Deposits Online
|
||||
|
||||
If you have a deposit outstanding, you can pay it online without waiting for your appointment:
|
||||
|
||||
1. Go to **Schedule** from the menu
|
||||
2. Click on your upcoming booking
|
||||
3. Click **Pay Deposit** — this opens the payment modal
|
||||
4. The deposit amount (20% of the booking total) is shown
|
||||
5. Choose to pay the deposit, pay the full amount, or pay a partial amount
|
||||
6. Enter your card details, use a saved card, or choose a card token from Square's secure form
|
||||
7. Confirm payment
|
||||
|
||||
If no deposit is owed but you haven't paid the full amount yet, you'll see a **Pay Early** button instead. You can also pay a partial amount using the **Pay Part** option with a custom amount input. If you've already paid a deposit, you'll see a **Pay Balance** option to settle the remaining amount.
|
||||
|
||||
### Leaving a Tip
|
||||
|
||||
After your appointment is marked as **completed**, you can leave a tip:
|
||||
|
||||
1. Go to the tip page (the salon may send you a link, or you can access it from your booking confirmation)
|
||||
2. You'll see your appointment details — date, time, and services
|
||||
3. Choose a tip amount:
|
||||
- **Preset percentages**: 10%, 15%, or 20% of your booking total (the £ amount is shown for each)
|
||||
- **Custom amount**: Enter any amount you'd like to tip
|
||||
4. Confirm payment — your saved card can be used if you have one on file
|
||||
|
||||
The tip page is secure and uses Square for payment processing.
|
||||
Each time they complete an appointment and pay for it, their deposit count goes down by **1**. After all 3 are cleared, they can book normally again.
|
||||
|
||||
### Quick Reference
|
||||
|
||||
@@ -321,107 +294,172 @@ The tip page is secure and uses Square for payment processing.
|
||||
|---|---|
|
||||
| Cancel ≥ 24 hours before | No penalty |
|
||||
| Cancel < 24 hours before | 3 deposit obligations |
|
||||
| Cancel < 24 hours (salon forgives you) | No penalty |
|
||||
| Cancel < 24 hours (salon forgives) | 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.
|
||||
**What to tell the customer:** "If you cancel with less than 24 hours' notice, you'll need to pay deposits on your next 3 bookings. Each time you complete an appointment, one deposit goes away."
|
||||
|
||||
---
|
||||
|
||||
## Browsing the Portfolio
|
||||
## Portfolio, Prices, and Contact
|
||||
|
||||
### Portfolio
|
||||
|
||||
The portfolio page is the salon's gallery of nail art and designs.
|
||||
|
||||
### Searching by Tags
|
||||
- **Search by tags**: Type a single tag to find photos with that tag. Type multiple tags separated by commas to find photos with **all** of those tags (e.g., "summer, glitter" shows photos tagged with both "summer" AND "glitter")
|
||||
- **Filter by categories**: Category dropdowns group tags into types (e.g., "Colour" with options like Red, Blue, Pink). Multiple categories can be selected at once — filters combine.
|
||||
- **Viewing a specific photo**: If someone shared a direct link, the page opens directly to that image, bypassing filters.
|
||||
|
||||
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.
|
||||
### Prices Page
|
||||
|
||||
- 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")
|
||||
A straightforward list of all services with prices and durations. Same information as the booking process, presented as a quick reference.
|
||||
|
||||
### Filtering by Categories
|
||||
### Contact Page
|
||||
|
||||
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.
|
||||
Shows the salon's name, phone number, email, and a profile photo. Includes an interactive map showing the salon location. The information is kept up to date by the salon.
|
||||
|
||||
---
|
||||
|
||||
## Prices Page
|
||||
## FAQ — Things Customers Call and Ask
|
||||
|
||||
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
|
||||
These are real questions customers ask. Use this section as a quick reference when you're on the phone.
|
||||
|
||||
### "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.
|
||||
Guest bookings are tied to the email they provided, but they don't have a login. They can't view them online. If they want to manage their bookings, they should create an account.
|
||||
|
||||
### "Why can't I see certain services?"
|
||||
**What to tell them:** "If you booked as a guest, you can't log in to see it, but we can look it up here for you. If you want to manage bookings online, create an account."
|
||||
|
||||
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 forgot my password — how do I reset it?"
|
||||
|
||||
### "I selected a time but when I submitted, it was taken."
|
||||
The password reset system exists on the backend but there's no "Forgot Password" link on the website yet.
|
||||
|
||||
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.
|
||||
**What to tell them:** "I can reset your password from here. What's the email on your account?"
|
||||
|
||||
### "My gift card doesn't work — it says it's expired"
|
||||
|
||||
Gift cards expire after 24 months of no activity (not from the date of purchase). Each time they use it, the timer resets. If it has expired, the balance is moved to our recovery system and we can restore it.
|
||||
|
||||
**What to tell them:** "Gift cards expire after 24 months of not being used. If yours has expired, we can restore the balance — just bring it in."
|
||||
|
||||
### "I never got my loyalty discount"
|
||||
|
||||
The discount is applied automatically when the appointment is **completed**, not when they book it. If they have 10 stamps, the 10% discount is applied to their next completed paid appointment.
|
||||
|
||||
**What to tell them:** "Your discount applies when you finish your appointment and pay for it, not when you book it. If you have 10 stamps, it'll be applied automatically."
|
||||
|
||||
### "Can I use two payment methods?"
|
||||
|
||||
Not for a single payment. If they have a gift card balance, they can use it to pay part of the total, but the remaining amount must be paid with one method (card, cash, etc.).
|
||||
|
||||
**What to tell them:** "You can use your gift card balance to pay part of it, and then pay the rest with card or cash."
|
||||
|
||||
### "My service is greyed out — why?"
|
||||
|
||||
It requires a patch test. They need a valid patch test on record (valid for 6 months, with a 24-hour wait after the test). If they haven't had one, or it's expired, they can't book that service.
|
||||
|
||||
**What to tell them:** "That service needs a patch test first. Book a patch test and come back after 24 hours for the main appointment."
|
||||
|
||||
### "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.
|
||||
If they're logged in, the booking is under their account. If they want to book for someone else, they should log out and use the other person's details. But they shouldn't book for someone else on their own account to bypass a patch test or age check — that's against the salon's policy.
|
||||
|
||||
### "What happens if I cancel and then want to rebook?"
|
||||
**What to tell them:** "If you want to book for someone else, log out first and create a booking with their details. Don't book for someone else on your own account."
|
||||
|
||||
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.
|
||||
### "Why can't I book today?"
|
||||
|
||||
### "How long does my patch test last?"
|
||||
All bookings must be made at least 1 hour before the appointment start time. Also, if they have deposit obligations, they can only book appointments at least 24 hours away. Additionally, after 22:00, the system blocks next-morning slots (00:00–11:00) for non-admin users.
|
||||
|
||||
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.
|
||||
**What to tell them:** "You need to book at least an hour before the appointment. If you owe deposits, you need to book at least 24 hours ahead. Also, late at night, the system stops showing morning slots for the next day."
|
||||
|
||||
### "Do I need a patch test for every visit?"
|
||||
### "How do I leave a tip?"
|
||||
|
||||
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.
|
||||
After their appointment is completed, they can go to the tip page. The salon may send them a link, or they can access it from their booking confirmation.
|
||||
|
||||
### "I just had a patch test — why can't I book straight away?"
|
||||
**What to tell them:** "After your appointment, you'll get a link to leave a tip. Or you can find it in your booking confirmation."
|
||||
|
||||
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.
|
||||
### "My slot was taken when I submitted"
|
||||
|
||||
### "What if two people in my household want to book?"
|
||||
Their slot was held temporarily, but the hold expired before they finished the form. Guest bookings only hold for 10 minutes. Logged-in bookings hold for 1 hour.
|
||||
|
||||
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.
|
||||
**What to tell them:** "The slot was held for you while you filled in the form, but it expired. This happens more often with guest bookings because you only get 10 minutes. If you create an account, you get an hour. Just pick a new time — it's unlikely to happen again."
|
||||
|
||||
### "Can I change my profile photo?"
|
||||
### "What happens to my data?"
|
||||
|
||||
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.
|
||||
Guest data is anonymized 6 months after the appointment. Registered account data is kept until the account is deleted. Full GDPR data export is available via `/gdpr` for registered users.
|
||||
|
||||
### "I earned loyalty stamps — where do I see them?"
|
||||
**What to tell them:** "If you book as a guest, we remove your details 6 months after your appointment. If you have an account, you can request a full copy of your data from us."
|
||||
|
||||
Your stamp count is displayed on your Account page. When you reach 10, a discount is automatically set up for your next completed appointment.
|
||||
### "Can I see my gift card balance?"
|
||||
|
||||
### "What does 'pending' mean on my booking?"
|
||||
Yes — on their Account page, under the Gift Cards tab. It shows their balance and any unredeemed cards.
|
||||
|
||||
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.
|
||||
**What to tell them:** "Log in and go to your Account page, then click Gift Cards. You'll see your balance there."
|
||||
|
||||
### "Can I redeem my gift card online?"
|
||||
|
||||
Not independently. They can see their balance online, but to redeem a gift card to their account balance, they need to ask the salon to do it.
|
||||
|
||||
**What to tell them:** "You can see your balance online, but to redeem it to your account, you need to ask us to do it. Just let us know when you come in."
|
||||
|
||||
### "What happens if my gift card expires?"
|
||||
|
||||
The balance is moved to the salon's expired balance recovery system. The admin can claim it and restore the funds to the customer (e.g., via a new gift card, bank transfer, or account credit).
|
||||
|
||||
**What to tell them:** "If your gift card expires, the balance is saved in our system. We can restore it for you — just bring it in."
|
||||
|
||||
### "Why do you need my phone number?"
|
||||
|
||||
For booking confirmation and contact. The phone number is used to send appointment reminders (when SMS is enabled) and to reach the customer if there are changes.
|
||||
|
||||
**What to tell them:** "We use your phone number to confirm your booking and send reminders. We don't share it with anyone else."
|
||||
|
||||
### "Do I need to verify my email?"
|
||||
|
||||
Not yet. The backend supports email verification but the frontend isn't fully wired. They can log in and book without verifying.
|
||||
|
||||
**What to tell them:** "Email verification isn't fully set up yet, but you can log in and book without it."
|
||||
|
||||
### "Can I change my appointment time?"
|
||||
|
||||
Yes — they can request a reschedule from their Account page. The salon reviews the request and either approves or declines it.
|
||||
|
||||
**What to tell them:** "Go to your Account page, find your booking, and click Reschedule. Choose a new time and we'll review it."
|
||||
|
||||
### "How do I cancel my booking?"
|
||||
|
||||
From their Account page, find the booking and select the cancel option. If they cancel less than 24 hours before the appointment without forgiveness, they get 3 deposit obligations.
|
||||
|
||||
**What to tell them:** "Go to your Account page, find the booking, and click Cancel. If you cancel with less than 24 hours' notice, you'll need to pay deposits on your next 3 bookings."
|
||||
|
||||
### "I paid a deposit but need to cancel — what happens?"
|
||||
|
||||
If they cancel with more than 24 hours' notice, no penalty. The deposit may be refunded depending on the salon's policy. If they cancel with less than 24 hours' notice, they get 3 deposit obligations.
|
||||
|
||||
**What to tell them:** "If you cancel with more than 24 hours' notice, there's no penalty. If you cancel with less than 24 hours' notice, you'll need to pay deposits on your next 3 bookings."
|
||||
|
||||
### "Do you take walk-ins?"
|
||||
|
||||
Yes — the salon can create a walk-in booking directly from the Today page. It bypasses the online booking flow and creates the booking immediately.
|
||||
|
||||
**What to tell them:** "Yes — just come in and we'll add you to the schedule."
|
||||
|
||||
### "I have two bookings on the same day — do I get two stamps?"
|
||||
|
||||
No. They earn a maximum of 1 stamp per calendar day, even if they have multiple paid appointments.
|
||||
|
||||
**What to tell them:** "You get 1 stamp per day, even if you have two appointments. So you'd get 1 stamp for today."
|
||||
|
||||
### "My discount is about to expire — what should I do?"
|
||||
|
||||
Loyalty discounts expire after 6 months. If they have a pending discount and haven't booked, they should book soon.
|
||||
|
||||
**What to tell them:** "Your loyalty discount expires after 6 months. If you have one waiting, book an appointment soon to use it."
|
||||
|
||||
### "Can I pay with cash online?"
|
||||
|
||||
No. Cash payments are only available at the till (in-person). Online payments are card only (via Square).
|
||||
|
||||
**What to tell them:** "Online payments are card only. You can pay with cash when you come in."
|
||||
|
||||
Reference in New Issue
Block a user