Add CleanupExpiredFinancialRecords to enforce HMRC + Limitation Act compliance (7-year retention, 1-year post-anonymization buffer). - financial_aggregates table: monthly totals by payment method/type (no PII) - CleanupExpiredFinancialRecords(): aggregates expired payments/refunds, deletes granular records, idempotent via ON CONFLICT DO UPDATE - Wired into GET /api/availability alongside existing cleanup functions - 8 tests: 7yr expiry, 1yr buffer, 9yr override, aggregation totals, idempotency, active user protection, both-thresholds elapsed, refunds - testdb.go: financial_aggregates in drop-order and truncate lists - README + Technical Manual updated
142 lines
12 KiB
Markdown
142 lines
12 KiB
Markdown
# Crussell
|
|
|
|
Nail salon booking platform — Go 1.25 backend + SvelteKit 5 frontend + Docker. Built for a UK sole-trader nail artist.
|
|
|
|
## Features
|
|
|
|
- **Three booking flows**: self-service (customer), walk-in (admin), call-in (admin)
|
|
- **Slot reservations**: temporary holds prevent double-booking (4 TTL types)
|
|
- **Guest accounts**: disposable accounts for one-off bookings, GDPR-compliant anonymization
|
|
- **Service eligibility**: age requirements + patch test validation
|
|
- **Deposit system**: 3-strike rule for late cancellations, 24-hour threshold
|
|
- **Square payments**: in-person Terminal + online Web Payments SDK (dev mock + prod stub, build-tagged)
|
|
- **Payment methods**: Card (Square Terminal/online), cash, gift card — with saved cards, refunds, and webhook handling
|
|
- **Payment types**: deposit, full, partial, balance, tip — admin can override service prices during payment
|
|
- **Scheduling**: default hours, holiday overrides, time blockers
|
|
- **Loyalty & discounts**: stamp-based loyalty, time-based and milestone campaigns
|
|
- **Portfolio gallery**: S3/R2 storage with tag/category filtering
|
|
- **CardDAV sync**: profile photos synced to SabreDAV contacts
|
|
- **Admin notifications**: priority-sorted queue with bell icon, `/notifications` page, acknowledge flow
|
|
- **User notification preferences**: per-channel toggles (email, SMS, browser) in account settings
|
|
- **Enriched edit requests**: side-by-side original vs proposed booking snapshots (time, services, prices, durations, user details) for admin review
|
|
- **Admin reschedule modal**: full reschedule UI on Today page with real-time available slot lookup, conflict detection, and one-click confirmation
|
|
- **Time blockers UI**: admin can create/manage one-off and recurring time blockers directly from the Admin dashboard, with overlap detection against existing bookings
|
|
- **Today page enhancements**: interactive daily calendar grid with visual time blockers, today stats summary, responsive layout improvements
|
|
- **Auto-select availability**: all booking flows (self-service, admin create, edit request) automatically select the first available date when data loads
|
|
- **Shared time slot utilities**: extracted common time slot generation, lunch protection, and formatting logic into a reusable module
|
|
- **Shared formatting utilities**: `formatDuration`, `formatDateTime`, `formatDate`, `formatTime`, `calculateAge` in `lib/utils/format.ts`
|
|
- **Referral code registration**: new users can enter a 12-character referral code during registration; relationship recorded in `user_referrals` table
|
|
- **Admin schedule page**: Google Calendar-style week view at `/admin/schedule` with drag-scroll, booking details modal, and working hours overlays
|
|
- **BookingFlow welcome step**: unauthenticated users see a welcome card (Step 0) encouraging login before guest checkout, with clear messaging about lost loyalty/discount benefits
|
|
- **Admin login redirect**: admins are redirected to `/today` after login instead of the home page
|
|
- **Patch test duration on services**: `patch_test_duration_hours` exposed on Service type; creating a service with duration > 0 auto-creates a patch test record
|
|
- **`created_by_name` on bookings**: admin booking details now show who created the booking (admin name)
|
|
- **Exceptional scheduling fix**: exceptional application queries expand start to Monday of the week, fixing single-day queries missing Monday-based `week_start` records
|
|
- **Portfolio image upload limits**: 20MB file size limit with visual feedback (red borders, error text), upload button disabled for oversized files
|
|
- **Multi-format portfolio images**: Backend stores AVIF, WebP, JPEG, and optional JXL variants for both full-size and thumbnail images. Frontend `ImageVariant` component renders `<picture>` elements with format-aware fallback. WASM-based client-side encoders (`@jsquash/avif`, `@jsquash/webp`, `@jsquash/jpeg`, `@discourse/jxl`) run in Web Workers for parallel encoding. Database schema extended with `full_avif_url`, `full_webp_url`, `full_jpg_url`, `full_jxl_url`, `thumb_avif_url`, `thumb_webp_url`, `thumb_jpg_url` columns. Image validation supports AVIF and JXL magic bytes. Legacy single-URL images remain backward-compatible.
|
|
- **MapLibre GL map components**: Reusable Svelte map components (`Map`, `MapMarker`, `MapControls`, `MapPopup`, `MapRoute`, `MapClusterLayer`, `MapArc`) wrapping MapLibre GL JS. Features: light/dark theme auto-detection, controlled/uncontrolled viewport, zoom/compass/locate/fullscreen controls, draggable markers, popup/tooltip/label children. Used on contact page for salon location display.
|
|
- **NavBar accessibility improvements**: Hamburger menu animated with 4-line to X transform, keyboard-accessible backdrop overlay (Enter/Space to close), ARIA labels, `fly` transitions on notification badge.
|
|
- **Loyalty stamp redesign**: Fuchsia-themed stamp card with procedurally-generated flower-petal SVG stamps (unique shape per slot via sine-wave perturbation), star cutout mask, hover scale animation, gradient background. Admin users see simplified account view (no History/Referral/Danger Zone tabs).
|
|
- **Admin role restrictions**: Admin accounts hide History tab, Referral tab, Danger Zone (delete account), and loyalty stamp card on the account page.
|
|
- **Svelte 5 `SvelteDate`**: Replaced `new Date()` with `new SvelteDate()` in reactive contexts (admin schedule, account page booking filter) for proper Svelte 5 reactivity.
|
|
- **`$derived` for computed values**: `totalPages` in notifications page now uses `$derived` instead of runtime calculation.
|
|
- **Svelte 5 each block keys**: Added proper keys to `{#each}` blocks across multiple components (NavBar, PortfolioCarousel, TodayCalendar, BookingConfirmed, Demo, PayTip, Tip, Account, Schedule) to fix reordering bugs and improve render performance.
|
|
- **`$app/paths` resolve**: All internal navigation hrefs now use `resolve()` from `$app/paths` for proper SPA routing.
|
|
- **Tailwind fuchsia ring safelist**: Added `ring-fuchsia-*` pattern to Tailwind safelist for loyalty stamp theming.
|
|
- **Vite WASM support**: `vite.config.ts` configured to include `.wasm` files as assets and exclude JSquash/Discourse encoder packages from dependency optimization.
|
|
- **NavBar enhancements**: admin links reordered (Today/Schedule promoted), unread notification badge on mobile burger icon, backdrop overlay + slide transition for mobile menu
|
|
- **TodayCalendar improvements**: fetches working/available hours for full week range instead of single day, closing time indicator on timeline, skips lunch suggestion for short days (5h or less)
|
|
- **Lunch protection refinement**: `shouldApplyLunchProtection()` skips lunch protection for days with 5 or fewer working hours
|
|
- **`formatDateISO` utility**: new `formatDateISO(date)` function in `lib/utils/format.ts` returning YYYY-MM-DD format for API calls, later extracted to shared utilities
|
|
- **JWT revocation with JTI**: every JWT carries a unique `jti` claim (UUID v4), in-memory revoked JTI tracking with 5-minute cleanup ticker, `POST /api/logout` endpoint revokes current token, refresh handler revokes old JTI before issuing replacement
|
|
- **Portfolio image deletion fix**: `extractKey` correctly extracts full S3 key path from URLs instead of just the filename, preventing orphaned files in storage
|
|
- **Notes validation**: all `Notes *string` fields across booking structs validated with `max=1000000` tag (13 fields across 4 files)
|
|
- **CharCounter component**: reusable grapheme counter using `Intl.Segmenter`, shows counter only above 750K graphemes, color-coded (green <800K, yellow 800K-950K, red >950K), integrated into 6 booking/admin components
|
|
- **loginInProgress rate limiting**: switched from `map[string]bool` to `map[string]time.Time` with 30-second staleness check, 20-entry cap (returns 429 when full), ticker goroutine cleans up stuck entries
|
|
- **Profile picture upload limit**: 15MB client-side check before crop dialog in account page
|
|
- **Portfolio image upload backend limit**: separate `portfolioBodyLimit` (20MB) applied to `/images` route, distinct from `uploadBodyLimit` (15MB) for profile pictures
|
|
- **GDPR compliance system**: Full Article 15 Subject Access Request via `/gdpr` — async Go endpoint (`GET /api/user/gdpr-export`) with 12h in-memory cache and background generation, 16-section SQL export (`export_all_user_data()`) covering profile, bookings (with override pricing), payments, refunds, saved cards, social logins, loyalty redemptions, booking discounts, edit requests, affiliate payouts, verification codes, forgiven no-shows, patch tests, referrals, notification preferences. 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 non-zero), deposits required (hidden when 0). Account deletion (`DELETE /api/user/account`) extended with external system scrubbing — S3 profile picture deletion, Square saved card deletion — before SQL-level anonymization. `anonymize_user()` SQL function extended with child table PII scrubbing (social logins, saved cards soft-delete with PCI clearance, verification code expiry, time blocker reservation scrubbing, edit request notes nulling, notification preference deletion). `AnonymizeStaleGuestAccounts()` extended with additional field scrubbing (profile_pic_url, referral_code, notes, data_retention_consent). 25 tests covering all backend additions.
|
|
- **Financial data retention & aggregation**: Granular payment/refund records retained for `MAX(created_at + 7 years, user_anonymized_at + 1 year)` (HMRC + Limitation Act compliance). Expired records are aggregated into `financial_aggregates` (monthly totals by payment method/type, no PII) and deleted. Triggered lazily on `GET /api/availability` alongside existing cleanup functions. Idempotent — safe to run repeatedly. 8 tests covering retention edge cases, aggregation correctness, refund handling, and idempotency.
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
Crussell/
|
|
├─ backend/ # Go 1.25 + chi router API
|
|
│ ├─ handlers/ # API handlers (auth, bookings, payments, scheduling, etc.)
|
|
│ │ ├─ payments/ # Square payment handlers (terminal, online, refunds, tips)
|
|
│ │ └─ webhooks/ # Square webhook handler
|
|
│ ├─ internal/ # Internal packages
|
|
│ │ └─ square/ # Square client (dev mock + prod stub, build-tagged)
|
|
│ └─ testutils/ # Test helpers (fixtures, testdb, JWT)
|
|
├─ frontend/ # SvelteKit 5 SPA (static build)
|
|
│ └─ src/lib/components/
|
|
│ ├─ payments/ # PaymentModal (admin), UserPaymentModal (user)
|
|
│ ├─ booking/ # BookingFlow (5-step wizard), DatePicker, TimeSlotPicker, TimeSlotList, SelectedTimeSummary
|
|
│ ├─ admin/ # BookingCreateModal, RescheduleModal, TimeBlockers, WeeklySchedule, HolidayHours, etc.
|
|
│ ├─ today/ # TodayCalendar (interactive grid), CurrentAppointment, PendingApprovals, TodayStats
|
|
│ └─ account/ # EditRequestModal, UserBookingModal
|
|
├─ frontend/src/lib/utils/ # Shared utilities (timeSlots.ts, format.ts)
|
|
├─ sabredav/ # PHP + Composer for DAV
|
|
├─ nginx/ # Nginx reverse-proxy for HTTP & HTTPS
|
|
├─ init-scripts/ # PostgreSQL init SQL
|
|
├─ compose.yml # Docker-Compose definition
|
|
├─ local-dev-2.sh # Dev helper using tmux (seeds DB with test data)
|
|
└─ README.md
|
|
```
|
|
|
|
## Prerequisites
|
|
|
|
| Tool | Version |
|
|
|------|---------|
|
|
| Docker & Docker Compose | >= 20.10 |
|
|
| Go | >= 1.22 |
|
|
| Node | >= 18 (npm) |
|
|
| tmux | >= 3.0 |
|
|
|
|
## Getting Started
|
|
|
|
### Docker Compose
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
# Edit .env — set POSTGRES_*, JWT_SECRET_KEY
|
|
docker compose up --build -d
|
|
```
|
|
|
|
| Service | URL |
|
|
|---------|-----|
|
|
| Frontend | http://localhost |
|
|
| API | http://localhost/api |
|
|
| SabreDAV | http://localhost/dav |
|
|
|
|
### Local Development
|
|
|
|
```bash
|
|
chmod +x local-dev-2.sh
|
|
./local-dev-2.sh
|
|
```
|
|
|
|
Launches a tmux session (`crussell-dev`) with 4 panes: psql console, Go dev server, Svelte dev server, and Rustfs logs. Seeds the database with 20 users, 12 services, 43 bookings, guest accounts, time blockers, and exceptional hours.
|
|
|
|
Default login (all password: `password`):
|
|
- Admin: `admin@example.com`
|
|
- User: `user@example.com`
|
|
|
|
## Building & Testing
|
|
|
|
```bash
|
|
# Backend
|
|
cd backend && go build -o bin/backend ./main.go
|
|
|
|
# Frontend
|
|
cd frontend && npm ci && npm run build
|
|
|
|
# Tests (584/587 passing, 3 skipped)
|
|
cd backend && go test -tags "test,dev" ./...
|
|
```
|
|
|
|
## Full Documentation
|
|
|
|
For detailed user journeys, admin workflows, architecture, API reference, and database schema, see [obsidian/Crussell/](obsidian/Crussell/).
|