docs: update all docs for staged default hours feature
Update Technical Manual (API endpoints, DB schema, scheduling system, job catalogue), Admin Manual (scheduled changes workflow), Overview, Future Work backlog, and README to reflect the new staged default hours change scheduling system with conflict detection and auto-apply at 00:05.
This commit is contained in:
@@ -4,13 +4,13 @@ Nail salon booking platform — Go 1.26.5 backend + SvelteKit 5 SPA + PostgreSQL
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
**Booking**: Self-service (customer), walk-in (admin), call-in (admin). Slot reservations prevent double-booking (4 TTL types). **Self-blocking prevention**: `excludeUserID` parameter filters a user's own `RESERVATION` entries from time blocker overlap checks, allowing re-reservation and booking at overlapping slots. **Explicit cancellation**: `DELETE /api/bookings/reserve` releases a user reservation; `DELETE /api/admin/bookings/reserve` releases an admin walk-in/call-in reservation. **Background cleanup**: Centralised cron scheduler (`backend/internal/jobs/`) runs 20 maintenance jobs: reservation/deposit cleanup every 5min, hourly campaign transitions, daily unpaid-booking notifications, GDPR anonymization, financial aggregation, and token/code cleanup. Guest accounts with GDPR-compliant anonymization (including `RESERVATION:edit_request:%` scrubbing). Service eligibility based on age + patch test validity. Overlap checks use `FOR UPDATE` row locks inside transactions. Closing-hours validation extracted into a reusable `closing_time` helper.
|
**Booking**: Self-service (customer), walk-in (admin), call-in (admin). Slot reservations prevent double-booking (4 TTL types). **Self-blocking prevention**: `excludeUserID` parameter filters a user's own `RESERVATION` entries from time blocker overlap checks, allowing re-reservation and booking at overlapping slots. **Explicit cancellation**: `DELETE /api/bookings/reserve` releases a user reservation; `DELETE /api/admin/bookings/reserve` releases an admin walk-in/call-in reservation. **Background cleanup**: Centralised cron scheduler (`backend/internal/jobs/`) runs 21 maintenance jobs: reservation/deposit cleanup every 5min, hourly campaign transitions, daily unpaid-booking notifications, staged default hours auto-apply, GDPR anonymization, financial aggregation, and token/code cleanup. Guest accounts with GDPR-compliant anonymization (including `RESERVATION:edit_request:%` scrubbing). Service eligibility based on age + patch test validity. Overlap checks use `FOR UPDATE` row locks inside transactions. Closing-hours validation (`closing_time.go`) resolves both current and staged default hours.
|
||||||
|
|
||||||
**Payments**: Square Terminal (in-person) + Web Payments SDK (online). Cash with change calculation. Gift cards (12-digit code or account balance). Saved cards for faster checkout. Tips on completed bookings. Refunds with notice-period tiers and deposit protection (72h/24h thresholds). All payment types: deposit, full, partial, balance, tip. Payment >20% of total promotes `pending_release` bookings back to `confirmed`. Deposit paid is computed from payments on-the-fly. The first 50% of each payment is always carved out as deposit (via `buildSplitRecords`); any overflow beyond the booking total becomes a tip. A PostgreSQL `pg_advisory_lock` serializes payment attempts per-booking to prevent two-tab double-payment races. Gift card purchases now insert a pending payment record with VAT before calling Square — the DB transaction commits first, so Square failures leave a retryable pending record.
|
**Payments**: Square Terminal (in-person) + Web Payments SDK (online). Cash with change calculation. Gift cards (12-digit code or account balance). Saved cards for faster checkout. Tips on completed bookings. Refunds with notice-period tiers and deposit protection (72h/24h thresholds). All payment types: deposit, full, partial, balance, tip. Payment >20% of total promotes `pending_release` bookings back to `confirmed`. Deposit paid is computed from payments on-the-fly. The first 50% of each payment is always carved out as deposit (via `buildSplitRecords`); any overflow beyond the booking total becomes a tip. A PostgreSQL `pg_advisory_lock` serializes payment attempts per-booking to prevent two-tab double-payment races. Gift card purchases now insert a pending payment record with VAT before calling Square — the DB transaction commits first, so Square failures leave a retryable pending record.
|
||||||
|
|
||||||
**Gift Cards**: Multi-method purchase (cash, card machine, online card, giveaway). Inventory cards for stock management. 24-month rolling expiry. Idle account cleanup (2yr/5yr thresholds). Expired balance recovery with admin audit trail. Transaction audit log. Idempotency keys for purchases.
|
**Gift Cards**: Multi-method purchase (cash, card machine, online card, giveaway). Inventory cards for stock management. 24-month rolling expiry. Idle account cleanup (2yr/5yr thresholds). Expired balance recovery with admin audit trail. Transaction audit log. Idempotency keys for purchases.
|
||||||
|
|
||||||
**Scheduling**: Default weekly hours, holiday/exceptional groups, time blockers (one-off + recurring with cron). Lunch protection. Late-night lock (22:00–11:00). Admin schedule page (Google Calendar-style week view).
|
**Scheduling**: Default weekly hours, holiday/exceptional groups, time blockers (one-off + recurring with cron), **staged default hours changes** (schedule future changes with effective date picker, conflict detection, and auto-apply at midnight). Lunch protection. Late-night lock (22:00–11:00). Admin schedule page (Google Calendar-style week view).
|
||||||
|
|
||||||
**Custom Services**: One-off or special-request services not in the permanent catalog. Admin management with create, edit, promote to permanent service (migrates booking references), and delete. Full CRUD API with search, popular sorting, and pagination. Can be added to any booking alongside regular services.
|
**Custom Services**: One-off or special-request services not in the permanent catalog. Admin management with create, edit, promote to permanent service (migrates booking references), and delete. Full CRUD API with search, popular sorting, and pagination. Can be added to any booking alongside regular services.
|
||||||
|
|
||||||
|
|||||||
@@ -202,14 +202,53 @@ Custom services can be added to bookings from the **Booking Create** and **Walk-
|
|||||||
|
|
||||||
### Scheduling
|
### Scheduling
|
||||||
|
|
||||||
The Scheduling section has three parts:
|
The Scheduling section has four parts:
|
||||||
|
|
||||||
**Default Hours** — set the salon's regular weekly opening hours. You can set start and end times for each day, and mark days as closed.
|
**Default Hours** — set the salon's regular weekly opening hours. You can set start and end times for each day, and mark days as closed.
|
||||||
|
|
||||||
|
**Scheduled Changes** — schedule future changes to your default opening hours. See the "Scheduled Default Hours Changes" section below.
|
||||||
|
|
||||||
**Exceptional Hours** — create holiday periods or special opening hours. For example, "Christmas Week: closed 25th–27th December, open 28th–30th 10am–4pm." You create a group of days, then apply that group to specific weeks.
|
**Exceptional Hours** — create holiday periods or special opening hours. For example, "Christmas Week: closed 25th–27th December, open 28th–30th 10am–4pm." You create a group of days, then apply that group to specific weeks.
|
||||||
|
|
||||||
**Time Blockers** — create one-off or recurring unavailable periods. See the "Time Blockers" section below for full details.
|
**Time Blockers** — create one-off or recurring unavailable periods. See the "Time Blockers" section below for full details.
|
||||||
|
|
||||||
|
### Scheduled Default Hours Changes
|
||||||
|
|
||||||
|
This feature lets you schedule future changes to your default opening hours. For example, "From next Monday, open at 10am instead of 9am" — you set the new hours and a future date, and the system applies them automatically at midnight.
|
||||||
|
|
||||||
|
#### How It Works
|
||||||
|
|
||||||
|
1. Open the **Default Hours** card on the Admin dashboard
|
||||||
|
2. Click **Edit Schedule** — the modal shows the current weekly hours
|
||||||
|
3. Adjust the hours for any day as needed
|
||||||
|
4. Set an **effective date** — when you want the new hours to start (must be tomorrow or later)
|
||||||
|
5. The system automatically checks for **conflicting bookings** — existing appointments that would overlap with the proposed new hours. If a booking falls outside the new hours (e.g., you're closing earlier), it appears in a conflict warning panel
|
||||||
|
6. If there are conflicts, you can click **Refresh** to re-check, or **View Booking**/**View Client** on each conflict for more context
|
||||||
|
7. Click **Schedule Change** to save — the change is staged and will take effect at 23:59 on the effective date
|
||||||
|
|
||||||
|
#### What Happens After Scheduling
|
||||||
|
|
||||||
|
- A banner appears on the Default Hours card showing: "Default hours are scheduled to change" with the effective date
|
||||||
|
- Customers see a notice on the website's business hours display: "Opening hours will change from [date]"
|
||||||
|
- The new hours don't affect existing bookings — they only apply to new bookings from the effective date onwards
|
||||||
|
- At midnight on the effective date, the system automatically updates the salon's working hours and creates an admin notification
|
||||||
|
|
||||||
|
#### Cancelling a Scheduled Change
|
||||||
|
|
||||||
|
Before the effective date:
|
||||||
|
1. Click **Cancel** on the scheduled change banner
|
||||||
|
2. The change is cancelled and the current working hours remain as they are
|
||||||
|
|
||||||
|
If the effective date has passed and the change has already been applied, you cannot cancel it. Edit the default hours directly instead.
|
||||||
|
|
||||||
|
#### Conflict Detection Details
|
||||||
|
|
||||||
|
When you propose new hours, the system checks all active (confirmed/in-progress/completed) bookings in a 90-day window from the effective date. A booking is flagged as conflicting if:
|
||||||
|
- It falls on a day you're proposing to close
|
||||||
|
- Its time range starts before or ends after the proposed opening window
|
||||||
|
|
||||||
|
Conflicts are informational — they won't block you from scheduling the change. They help you see which customers might be affected so you can contact them if needed.
|
||||||
|
|
||||||
### Gift Cards
|
### Gift Cards
|
||||||
|
|
||||||
The Gift Cards section lets you manage the salon's gift card system:
|
The Gift Cards section lets you manage the salon's gift card system:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ These are blockers: missing functionality that prevents daily operations, legal
|
|||||||
| # | Gap | Effort | Area | Notes |
|
| # | 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. |
|
| 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/cleanup background cron~~** | S (3h) | Backend | **DONE**: All cleanup functions migrated to `backend/internal/jobs/` — centralised cron scheduler. 20 jobs registered with staggered schedules: cleanup-reservations (5min), cleanup-expired-deposits (5min), cleanup-rate-limiters (5min), cleanup-gdpr-export-cache (5min), cleanup-progressive-rate-limiter (1min), cleanup-expired-loyalty-redemptions (hourly), cleanup-old-idempotency-keys (hourly), cleanup-revoked-jtis (hourly), cleanup-stale-login-entries (hourly), transition-discount-campaigns (hourly), notify-unpaid-1-week (7am daily), notify-unpaid-1-month (7am daily), cleanup-verification-codes (2am daily), cleanup-refresh-tokens (2am daily), anonymize-stale-guest-accounts (3am daily), cleanup-idle-accounts (3:30am daily), cleanup-expired-financial-records (4am daily), cleanup-old-name-history (4:30am daily), cleanup-expired-gift-cards (5am daily). |
|
| 2 | **~~Reservation/cleanup background cron~~** | S (3h) | Backend | **DONE**: All cleanup functions migrated to `backend/internal/jobs/` — centralised cron scheduler. 21 jobs registered with staggered schedules: cleanup-reservations (5min), cleanup-expired-deposits (5min), cleanup-rate-limiters (5min), cleanup-gdpr-export-cache (5min), cleanup-progressive-rate-limiter (1min), cleanup-expired-loyalty-redemptions (hourly), cleanup-old-idempotency-keys (hourly), cleanup-revoked-jtis (hourly), cleanup-stale-login-entries (hourly), transition-discount-campaigns (hourly), notify-unpaid-1-week (7am daily), notify-unpaid-1-month (7am daily), cleanup-verification-codes (2am daily), cleanup-refresh-tokens (2am daily), anonymize-stale-guest-accounts (3am daily), cleanup-idle-accounts (3:30am daily), cleanup-expired-financial-records (4am daily), cleanup-old-name-history (4:30am daily), cleanup-expired-gift-cards (5am daily), apply-default-hours (00:05 daily). |
|
||||||
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Three booking flows, each with its own entry point and reservation TTL:
|
|||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
Overlap checks now use `FOR UPDATE` row locks inside transactions — the overlap query runs inside `Begin`/`Commit` to prevent race conditions. Closing-hours validation extracted into a reusable `checkClosingHours()` helper and `closing_time.go`. A shared `repo.go` provides common DB query helpers across booking handlers.
|
Overlap checks now use `FOR UPDATE` row locks inside transactions — the overlap query runs inside `Begin`/`Commit` to prevent race conditions. Closing-hours validation extracted into reusable helpers: `checkClosingHours()` validates end-time against closing, `getClosingTimeForDate()` resolves closing time from either current `working_hours` or a pending staged default hours change. A shared `repo.go` provides common DB query helpers across booking handlers.
|
||||||
|
|
||||||
**Deposit system:** Bookings have a 24h deposit deadline. Unpaid bookings enter `pending_release` — the slot becomes vulnerable to eviction by overlapping new bookings. Payment of >= 20% of the total at any point promotes back to `confirmed`. Evicted bookings enter `deposit_lapsed`. Admin forgiveness (`forgive_fees`/`forgive_noshow`) on cancel and reschedule. Refund calculation uses notice-period tiers (72h/24h full/partial/none) with deposit protection capping retention at 50% of total.
|
**Deposit system:** Bookings have a 24h deposit deadline. Unpaid bookings enter `pending_release` — the slot becomes vulnerable to eviction by overlapping new bookings. Payment of >= 20% of the total at any point promotes back to `confirmed`. Evicted bookings enter `deposit_lapsed`. Admin forgiveness (`forgive_fees`/`forgive_noshow`) on cancel and reschedule. Refund calculation uses notice-period tiers (72h/24h full/partial/none) with deposit protection capping retention at 50% of total.
|
||||||
|
|
||||||
@@ -59,6 +59,8 @@ VAT treatment: gift cards are Single-Purpose Vouchers (SPVs) by default — VAT
|
|||||||
|
|
||||||
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").
|
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").
|
||||||
|
|
||||||
|
**Staged Default Hours Changes:** Admins can schedule future changes to opening hours with an effective date picker. The system stores one pending change at a time. Conflict detection checks all active bookings in a 90-day window. `GetWorkingHours` and `computeAvailableHours` apply staged hours for dates on/after the effective date. A daily cron (`ApplyScheduledDefaultHours` at 00:05) bulk-updates `working_hours` when a change takes effect. All booking creation paths also check staged hours via `getClosingTimeForDate()`.
|
||||||
|
|
||||||
Available hours calculated by loading working hours, subtracting existing bookings (with gap logic), subtracting time blockers (including reservations). **Self-blocking prevention**: `GetAvailableHours` passes `excludeUserID` (from `OptionalAuth` context) to `GetTimeBlockersInRange`, excluding the user's own `RESERVATION` entries so their existing hold doesn't hide the slot. All booking/reservation handlers also pass `excludeUserID` to `CheckTimeBlockerOverlap`. Late-night lock: after 22:00, blocks next morning 00:00–11:00 for non-admin users.
|
Available hours calculated by loading working hours, subtracting existing bookings (with gap logic), subtracting time blockers (including reservations). **Self-blocking prevention**: `GetAvailableHours` passes `excludeUserID` (from `OptionalAuth` context) to `GetTimeBlockersInRange`, excluding the user's own `RESERVATION` entries so their existing hold doesn't hide the slot. All booking/reservation handlers also pass `excludeUserID` to `CheckTimeBlockerOverlap`. 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.
|
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.
|
||||||
@@ -69,7 +71,7 @@ Lunch protection: `findAllLunchGaps()` returns all gap durations in the middle w
|
|||||||
|
|
||||||
**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.
|
**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, **custom services management** (one-off services with create/edit/promote/delete), 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.
|
**Admin dashboard** (`/admin`): users list + detail modal (profile, bookings, relationship data, patch tests, loyalty/referrals, privacy/consent), services CRUD, **custom services management** (one-off services with create/edit/promote/delete), bookings list with search, scheduling management (default hours with **staged change scheduling** and effective date picker, 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.
|
**Reschedule modal**: search available slots, conflict detection (overlapping bookings), one-click confirm. Side-by-side enriched snapshots in Pending Approvals for edit requests.
|
||||||
|
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ Backend (:8080)
|
|||||||
| Package | File(s) | Purpose |
|
| Package | File(s) | Purpose |
|
||||||
|---------|---------|---------|
|
|---------|---------|---------|
|
||||||
| `handlers/auth` | local.go, social.go | Registration (with referral code validation), login, refresh, email verification |
|
| `handlers/auth` | local.go, social.go | Registration (with referral code validation), login, refresh, email verification |
|
||||||
| `handlers/bookings` | bookings.go, reserve.go, manage.go, admin_reserve.go, cancel_reservation.go, admin_cancel_reservation.go | Booking CRUD, reservations with **self-blocking prevention** (`excludeUserID` parameter on `CheckTimeBlockerOverlap` + pre-overlap DELETE with IP hash anon cleanup), admin management, edit requests, discounts, closing hours validation, active booking limits, GetBookingsByCreatedRange, created_by_name resolution, **explicit reservation cancellation** (`DELETE /api/bookings/reserve` for users, `DELETE /api/admin/bookings/reserve` for admin walk-in/call-in) |
|
| `handlers/bookings` | bookings.go, reserve.go, manage.go, admin_reserve.go, cancel_reservation.go, admin_cancel_reservation.go, closing_time.go | Booking CRUD, reservations with **self-blocking prevention** (`excludeUserID` parameter on `CheckTimeBlockerOverlap` + pre-overlap DELETE with IP hash anon cleanup), admin management, edit requests, discounts, closing hours validation (`checkClosingHours` + `getClosingTimeForDate` resolves staged default hours for bookings), active booking limits, GetBookingsByCreatedRange, created_by_name resolution, **explicit reservation cancellation** (`DELETE /api/bookings/reserve` for users, `DELETE /api/admin/bookings/reserve` for admin walk-in/call-in) |
|
||||||
| `handlers/payments` | handlers.go, service.go, validators.go, giftcards.go, till.go, refunds.go, refund_policy.go | Square payments: terminal, online, refunds, tips, saved cards, gift cards (CRUD, topup, transfer, redeem, buy, expired balances, till sales). Refund calculation with notice-period tiers and deposit protection |
|
| `handlers/payments` | handlers.go, service.go, validators.go, giftcards.go, till.go, refunds.go, refund_policy.go | Square payments: terminal, online, refunds, tips, saved cards, gift cards (CRUD, topup, transfer, redeem, buy, expired balances, till sales). Refund calculation with notice-period tiers and deposit protection |
|
||||||
| `handlers/webhooks` | square.go | Square webhook handler for payment status updates. **Fail-closed signature check** — rejects requests with 403 when `SQUARE_WEBHOOK_SIGNATURE_KEY` is set but header is missing. Dev mode: skips verification when env var is empty. Still uses hex-encoding stub (`verifySquareSignature`) — production requires HMAC-SHA256 with base64 output, `x-square-hmacsha256-signature` header. See `TODO(PROD)` in source. |
|
| `handlers/webhooks` | square.go | Square webhook handler for payment status updates. **Fail-closed signature check** — rejects requests with 403 when `SQUARE_WEBHOOK_SIGNATURE_KEY` is set but header is missing. Dev mode: skips verification when env var is empty. Still uses hex-encoding stub (`verifySquareSignature`) — production requires HMAC-SHA256 with base64 output, `x-square-hmacsha256-signature` header. See `TODO(PROD)` in source. |
|
||||||
| `handlers/admin` | users.go, analytics.go, custom_services.go, discount_campaigns.go, settings.go | Admin user management, custom services CRUD (list/create/get/update/promote/delete), discount campaigns, analytics (stub), business settings (GET/PUT with VAT, gift card config) |
|
| `handlers/admin` | users.go, analytics.go, custom_services.go, discount_campaigns.go, settings.go | Admin user management, custom services CRUD (list/create/get/update/promote/delete), discount campaigns, analytics (stub), business settings (GET/PUT with VAT, gift card config) |
|
||||||
| `handlers/today` | today.go | Current/next appointment, today's grid, pending approvals, `DoneForDay` state with daily/weekly summary (`DailySummary` with `total_bookings`, `customers_served`, `summary_scope`), auto-status transitions, closed-day aggregation via `findWeekSummaryRange` + `computeAggregateSummary`. Exceptional hours lookup uses `exceptional_group_applications.week_start` (0=Monday). |
|
| `handlers/today` | today.go | Current/next appointment, today's grid, pending approvals, `DoneForDay` state with daily/weekly summary (`DailySummary` with `total_bookings`, `customers_served`, `summary_scope`), auto-status transitions, closed-day aggregation via `findWeekSummaryRange` + `computeAggregateSummary`. Exceptional hours lookup uses `exceptional_group_applications.week_start` (0=Monday). |
|
||||||
| `handlers/user` | profile.go, account.go, guest.go, loyalty.go, customer_relationship.go, gdpr_export.go | User profile, guest creation (with CheckEmailHandler for registered-email detection), loyalty, contact info, GDPR export (async with 12h cache) |
|
| `handlers/user` | profile.go, account.go, guest.go, loyalty.go, customer_relationship.go, gdpr_export.go | User profile, guest creation (with CheckEmailHandler for registered-email detection), loyalty, contact info, GDPR export (async with 12h cache) |
|
||||||
| `handlers/services` | services.go | Service catalog, eligibility filtering, patch_test_duration_hours auto-creates patch test records |
|
| `handlers/services` | services.go | Service catalog, eligibility filtering, patch_test_duration_hours auto-creates patch test records |
|
||||||
| `handlers/scheduling` | default-hours.go, exceptional-hours.go, time-blockers.go | Working hours, exceptional groups, time blockers with **excludeUserID filtering** (user's own `RESERVATION` entries excluded from blocker results when authenticated), gift card expiry cleanup (24-month rolling), idle account cleanup (2yr/5yr) |
|
| `handlers/scheduling` | default-hours.go, scheduled-cleanup.go, exceptional-hours.go, time-blockers.go | Working hours, exceptional groups, staged default hours changes (schedule/conflict detection/auto-apply), time blockers with **excludeUserID filtering** (user's own `RESERVATION` entries excluded from blocker results when authenticated), gift card expiry cleanup (24-month rolling), idle account cleanup (2yr/5yr) |
|
||||||
| `handlers/portfolio` | images.go | Image CRUD, cursor-paginated listing with fuzzy tag search & exact category filters, relevance-sorted tag results |
|
| `handlers/portfolio` | images.go | Image CRUD, cursor-paginated listing with fuzzy tag search & exact category filters, relevance-sorted tag results |
|
||||||
| `handlers/notifications` | notifications.go | Admin notifications (GET, acknowledge) |
|
| `handlers/notifications` | notifications.go | Admin notifications (GET, acknowledge) |
|
||||||
|
|
||||||
@@ -157,6 +157,7 @@ src/lib/components/
|
|||||||
│ ├── CustomServicesManagement.svelte # Custom services CRUD (list/create/edit/promote/delete)
|
│ ├── CustomServicesManagement.svelte # Custom services CRUD (list/create/edit/promote/delete)
|
||||||
│ ├── CallInBooking.svelte # Call-in booking flow
|
│ ├── CallInBooking.svelte # Call-in booking flow
|
||||||
│ ├── EditBookingModal.svelte # Edit booking services
|
│ ├── EditBookingModal.svelte # Edit booking services
|
||||||
|
│ ├── WeeklySchedule.svelte # Weekly default hours — edit, schedule future changes with effective date picker + conflict detection UI
|
||||||
│ ├── HolidayHours.svelte # Exceptional schedule management
|
│ ├── HolidayHours.svelte # Exceptional schedule management
|
||||||
│ ├── ImageUpload.svelte # Portfolio image upload
|
│ ├── ImageUpload.svelte # Portfolio image upload
|
||||||
│ ├── PatchTestModal.svelte # Record patch test
|
│ ├── PatchTestModal.svelte # Record patch test
|
||||||
@@ -188,6 +189,7 @@ src/lib/components/
|
|||||||
│ ├── TodayCalendar.svelte # Interactive day view with time blockers
|
│ ├── TodayCalendar.svelte # Interactive day view with time blockers
|
||||||
│ └── TodayStats.svelte # Today's appointment stats summary
|
│ └── TodayStats.svelte # Today's appointment stats summary
|
||||||
├── layout/
|
├── layout/
|
||||||
|
│ ├── BusinessHours.svelte # Business hours display with upcoming schedule change indicator
|
||||||
│ ├── NavBar.svelte # Navigation bar (responsive, notification badge)
|
│ ├── NavBar.svelte # Navigation bar (responsive, notification badge)
|
||||||
│ └── PortfolioCarousel.svelte # Home page gallery
|
│ └── PortfolioCarousel.svelte # Home page gallery
|
||||||
├── ui/
|
├── ui/
|
||||||
@@ -280,7 +282,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
|||||||
| GET | `/api/contact` | None | — | Business contact info (first admin user) |
|
| GET | `/api/contact` | None | — | Business contact info (first admin user) |
|
||||||
| POST | `/api/users/guest` | None | 10/min | Create disposable guest account |
|
| POST | `/api/users/guest` | None | 10/min | Create disposable guest account |
|
||||||
| GET | `/api/check-email` | None | 60/min | Proactive registered-email detection (params: email, firstName, lastName, phone). Returns `suggestion`: `"login"` (full match), `"check"` (partial), or `null` (not found/guest) |
|
| GET | `/api/check-email` | None | 60/min | Proactive registered-email detection (params: email, firstName, lastName, phone). Returns `suggestion`: `"login"` (full match), `"check"` (partial), or `null` (not found/guest) |
|
||||||
| GET | `/api/scheduling/default-hours` | None | 120/min | Get weekly default hours |
|
| GET | `/api/scheduling/default-hours` | None | 120/min | Get weekly default hours — returns `{ current: [...], scheduled_change: ... }` when a pending staged change exists |
|
||||||
| GET | `/api/scheduling/exceptional-groups` | None | 120/min | List holiday/special hour groups |
|
| GET | `/api/scheduling/exceptional-groups` | None | 120/min | List holiday/special hour groups |
|
||||||
| GET | `/api/scheduling/working-hours` | None | 120/min | Merged default + exceptional hours |
|
| GET | `/api/scheduling/working-hours` | None | 120/min | Merged default + exceptional hours |
|
||||||
| GET | `/api/scheduling/available-hours` | None | 120/min | Available slots (cleanup removed — runs on cron) |
|
| GET | `/api/scheduling/available-hours` | None | 120/min | Available slots (cleanup removed — runs on cron) |
|
||||||
@@ -369,6 +371,10 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
|||||||
| DELETE | `/api/admin/discount-campaigns/{id}` | Cancel discount campaign |
|
| DELETE | `/api/admin/discount-campaigns/{id}` | Cancel discount campaign |
|
||||||
| GET | `/api/admin/discount-campaigns/{id}/stats` | Campaign stats |
|
| GET | `/api/admin/discount-campaigns/{id}/stats` | Campaign stats |
|
||||||
| PUT | `/api/scheduling/default-hours` | Update weekly hours |
|
| PUT | `/api/scheduling/default-hours` | Update weekly hours |
|
||||||
|
| POST | `/api/scheduling/default-hours/conflicting` | Check which active bookings conflict with proposed default hours (90-day lookahead). Returns `{ bookings: [...] }` with services |
|
||||||
|
| POST | `/api/scheduling/default-hours/schedule` | Schedule a future default hours change (effective_date must be tomorrow or later). Accepts `{ hours: [7 entries], effective_date: "YYYY-MM-DD" }`. Returns `{ effective_date, message }` |
|
||||||
|
| GET | `/api/scheduling/default-hours/scheduled` | Get pending scheduled change, if any. Returns `ScheduledHoursChange` or 404 |
|
||||||
|
| DELETE | `/api/scheduling/default-hours/scheduled` | Cancel pending scheduled change. Returns 204 on success, 404 if none |
|
||||||
| POST | `/api/scheduling/exceptional-groups` | Create exception group |
|
| POST | `/api/scheduling/exceptional-groups` | Create exception group |
|
||||||
| DELETE | `/api/scheduling/exceptional-groups` | Delete exception group |
|
| DELETE | `/api/scheduling/exceptional-groups` | Delete exception group |
|
||||||
| PUT | `/api/scheduling/exceptional-applications` | Apply exceptions to dates |
|
| PUT | `/api/scheduling/exceptional-applications` | Apply exceptions to dates |
|
||||||
@@ -410,7 +416,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
|||||||
| `payment_type` | `deposit`, `full`, `tip`, `balance`, `partial` |
|
| `payment_type` | `deposit`, `full`, `tip`, `balance`, `partial` |
|
||||||
| `payment_method` | `online_square`, `in_person_card`, `cash`, `giftcard`, `discount`, `on_the_house` |
|
| `payment_method` | `online_square`, `in_person_card`, `cash`, `giftcard`, `discount`, `on_the_house` |
|
||||||
| `payment_status` | `pending`, `completed`, `failed`, `refunded` |
|
| `payment_status` | `pending`, `completed`, `failed`, `refunded` |
|
||||||
| `admin_notification_reason` | `pending_booking`, `cancelled_booking`, `rescheduled_booking`, `1_week_no_pay`, `1_month_no_pay`, `affiliate_claim`, `late_cancellation`, `no_deposit`, `deposit_paid`, `edit_request`, `new_booking`, `edit_requested`, `deposit_not_paid_by_deadline` |
|
| `admin_notification_reason` | `pending_booking`, `cancelled_booking`, `rescheduled_booking`, `1_week_no_pay`, `1_month_no_pay`, `affiliate_claim`, `late_cancellation`, `no_deposit`, `deposit_paid`, `edit_request`, `new_booking`, `edit_requested`, `deposit_not_paid_by_deadline`, `default_hours_changed` |
|
||||||
| `campaign_type` | `time_based`, `milestone` |
|
| `campaign_type` | `time_based`, `milestone` |
|
||||||
| `milestone_type` | `per_user_booking_count`, `global_booking_count`, `anniversary` |
|
| `milestone_type` | `per_user_booking_count`, `global_booking_count`, `anniversary` |
|
||||||
| `milestone_unit` | `bookings`, `months`, `years` |
|
| `milestone_unit` | `bookings`, `months`, `years` |
|
||||||
@@ -418,7 +424,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
|||||||
| `discount_campaign_status` | `draft`, `active`, `completed`, `cancelled` |
|
| `discount_campaign_status` | `draft`, `active`, `completed`, `cancelled` |
|
||||||
| `till_item_type` | `gift_card`, `merchandise`, `service` |
|
| `till_item_type` | `gift_card`, `merchandise`, `service` |
|
||||||
|
|
||||||
### Tables (37 total)
|
### Tables (38 total)
|
||||||
|
|
||||||
| Table | Purpose |
|
| Table | Purpose |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
@@ -435,6 +441,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
|||||||
| `booking_edit_requests` | Pending customer edit requests |
|
| `booking_edit_requests` | Pending customer edit requests |
|
||||||
| `user_referrals` | Referral tracking |
|
| `user_referrals` | Referral tracking |
|
||||||
| `working_hours` | Default weekly schedule |
|
| `working_hours` | Default weekly schedule |
|
||||||
|
| `default_hours_scheduled_changes` | Staged default hours changes — insert one pending row (enforced by partial unique index `idx_one_pending_change`), with `effective_date`, `hours` (JSONB), `applied_at`, `cancelled_at` |
|
||||||
| `exceptional_working_hours_groups` | Holiday/special hour groups |
|
| `exceptional_working_hours_groups` | Holiday/special hour groups |
|
||||||
| `exceptional_working_hours` | Hours for exception groups |
|
| `exceptional_working_hours` | Hours for exception groups |
|
||||||
| `exceptional_group_applications` | Apply exceptions to date ranges |
|
| `exceptional_group_applications` | Apply exceptions to date ranges |
|
||||||
@@ -486,6 +493,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
|||||||
| `CleanupExpiredFinancialRecords(ctx)` | Go — aggregates expired payments/refunds into monthly stats |
|
| `CleanupExpiredFinancialRecords(ctx)` | Go — aggregates expired payments/refunds into monthly stats |
|
||||||
| `CleanupExpiredGiftCards(ctx)` | Go — expires gift cards unused for 24+ months |
|
| `CleanupExpiredGiftCards(ctx)` | Go — expires gift cards unused for 24+ months |
|
||||||
| `CleanupIdleAccounts(ctx)` | Go — anonymizes accounts idle 2+ years (no balance) or 5+ years (with balance) |
|
| `CleanupIdleAccounts(ctx)` | Go — anonymizes accounts idle 2+ years (no balance) or 5+ years (with balance) |
|
||||||
|
| `ApplyScheduledDefaultHours(ctx)` | Go — applies pending `default_hours_scheduled_changes` whose `effective_date` has been reached. Bulk-updates `working_hours` via `unnest()`, marks change as applied, inserts `default_hours_changed` admin notification |
|
||||||
|
|
||||||
### Partial Indexes
|
### Partial Indexes
|
||||||
|
|
||||||
@@ -499,6 +507,7 @@ CORS uses `*` in local dev. In production behind Cloudflare, nginx handles CORS.
|
|||||||
| `idx_gift_cards_last_used_at` | gift_cards | `WHERE last_used_at IS NOT NULL AND redeemed_by IS NULL AND amount_remaining > 0` |
|
| `idx_gift_cards_last_used_at` | gift_cards | `WHERE last_used_at IS NOT NULL AND redeemed_by IS NULL AND amount_remaining > 0` |
|
||||||
| `idx_gift_card_expired_balances_unclaimed` | gift_card_expired_balances | `WHERE claimed_at IS NULL` |
|
| `idx_gift_card_expired_balances_unclaimed` | gift_card_expired_balances | `WHERE claimed_at IS NULL` |
|
||||||
| `idx_gift_card_transactions_created_at` | gift_card_transactions | — |
|
| `idx_gift_card_transactions_created_at` | gift_card_transactions | — |
|
||||||
|
| `idx_one_pending_change` | default_hours_scheduled_changes | `WHERE applied_at IS NULL AND cancelled_at IS NULL` |
|
||||||
|
|
||||||
### Default Working Hours
|
### Default Working Hours
|
||||||
|
|
||||||
@@ -784,6 +793,19 @@ validTransitions := map[string]map[string]bool{
|
|||||||
|
|
||||||
**Default Hours:** `working_hours` table (weekday 0-6, start_time, end_time, is_open). Bulk updateable via PUT.
|
**Default Hours:** `working_hours` table (weekday 0-6, start_time, end_time, is_open). Bulk updateable via PUT.
|
||||||
|
|
||||||
|
**Staged Default Hours Changes:** Admins can schedule future changes to default hours via `POST /api/scheduling/default-hours/schedule`. The system stores one pending change at a time in `default_hours_scheduled_changes` (enforced by partial unique index `idx_one_pending_change`). A change includes:
|
||||||
|
- `effective_date` — the date (London-timezone) from which the new hours apply
|
||||||
|
- `hours` — JSONB array of 7 weekday entries (startTime, endTime, isOpen)
|
||||||
|
- `applied_at` / `cancelled_at` — lifecycle timestamps
|
||||||
|
|
||||||
|
**Conflict Detection:** Before scheduling, `POST /api/scheduling/default-hours/conflicting` queries all active bookings in a 90-day window from the effective date and returns any that would fall outside the proposed hours (closed days or outside opening window). Each conflict includes customer details and services.
|
||||||
|
|
||||||
|
**Working Hours Integration:** `GetWorkingHours` and `computeAvailableHours` both check for pending staged changes. For dates on/after `effective_date`, staged hours overlay the current `working_hours` defaults (exceptional groups still take precedence over staged hours).
|
||||||
|
|
||||||
|
**Auto-Apply:** `ApplyScheduledDefaultHours` runs daily at 00:05 via the cron scheduler. On the effective date, it bulk-updates `working_hours` with staged values in a single `UPDATE ... FROM unnest()` query, marks the change as applied, and inserts a `default_hours_changed` admin notification.
|
||||||
|
|
||||||
|
**Booking Validation:** All booking creation/reschedule paths (`CreateBookingHandler`, `AdminReserveSlotHandler`, `AdminCreateBookingForUserHandler`, `AdminRescheduleBookingHandler`) use `getClosingTimeForDate` to check staged hours. If a staged change has reached its effective date and marks a weekday as closed (00:00), the booking is rejected with a clear error message.
|
||||||
|
|
||||||
**Exceptional Groups:** Three-table design:
|
**Exceptional Groups:** Three-table design:
|
||||||
1. `exceptional_working_hours_groups` — group metadata
|
1. `exceptional_working_hours_groups` — group metadata
|
||||||
2. `exceptional_working_hours` — 7 days of hours per group
|
2. `exceptional_working_hours` — 7 days of hours per group
|
||||||
@@ -851,6 +873,7 @@ validTransitions := map[string]map[string]bool{
|
|||||||
- Public bookings (`POST /api/bookings`) → always `new_booking`, plus `pending_booking` if notes or today
|
- Public bookings (`POST /api/bookings`) → always `new_booking`, plus `pending_booking` if notes or today
|
||||||
- Edit requests (`POST /api/bookings/{id}/edit-request`) → always `edit_requested`, plus `pending_booking` if booking status is pending
|
- Edit requests (`POST /api/bookings/{id}/edit-request`) → always `edit_requested`, plus `pending_booking` if booking status is pending
|
||||||
- Admin bookings (`POST /api/admin/bookings`) → no notifications (admin already knows)
|
- Admin bookings (`POST /api/admin/bookings`) → no notifications (admin already knows)
|
||||||
|
- Scheduled default hours applied (`ApplyScheduledDefaultHours`) → `default_hours_changed` (low priority, acknowledge-only)
|
||||||
|
|
||||||
**User notification preferences:**
|
**User notification preferences:**
|
||||||
Users manage their preferred notification channels via `/account` → Admin tab → Notifications section. The `user_notification_preferences` table stores per-user flags for email, SMS, and browser push. These flags are not yet used by any delivery system — they will be consumed when the email/SMS notification system (E5) is built.
|
Users manage their preferred notification channels via `/account` → Admin tab → Notifications section. The `user_notification_preferences` table stores per-user flags for email, SMS, and browser push. These flags are not yet used by any delivery system — they will be consumed when the email/SMS notification system (E5) is built.
|
||||||
@@ -1791,6 +1814,7 @@ FROM bookings b LEFT JOIN payments p ON p.booking_id = b.id WHERE b.user_id = $1
|
|||||||
| Daily 4am | Financial record aggregation + deletion | `0 4 * * *` |
|
| Daily 4am | Financial record aggregation + deletion | `0 4 * * *` |
|
||||||
| Daily 4:30am | Name history cleanup | `30 4 * * *` |
|
| Daily 4:30am | Name history cleanup | `30 4 * * *` |
|
||||||
| Daily 5am | Expired gift cards | `0 5 * * *` |
|
| Daily 5am | Expired gift cards | `0 5 * * *` |
|
||||||
|
| Daily 00:05 | Apply scheduled default hours (checks for pending changes whose effective_date has been reached) | `5 0 * * *` |
|
||||||
|
|
||||||
**Previously lazy (side-effects in `GetAvailableHours`):** All cleanup functions were called synchronously on every `GET /api/scheduling/available-hours` request. They are now extracted into the cron scheduler, removing ~43 lines of side-effect code from the HTTP handler.
|
**Previously lazy (side-effects in `GetAvailableHours`):** All cleanup functions were called synchronously on every `GET /api/scheduling/available-hours` request. They are now extracted into the cron scheduler, removing ~43 lines of side-effect code from the HTTP handler.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user