feat: enriched edit request system with side-by-side snapshots, calendar preloading, and admin review UI

Backend:
- Add enriched response types (EditSnapshot, EnrichedEditRequest) with original vs proposed snapshots
- Add 4 new GET endpoints for viewing edit requests (user and admin scoped)
- Remove github.com/lib/pq dependency — use native PostgreSQL array scanning
- Clean up edit requests, time blockers, and notifications on booking cancellation
- Validate exceptional closed hours on admin approve (409 Conflict)
- Notification upsert on edit request replace (no duplicate admin notifications)

Frontend:
- New user EditRequestModal with time/services/both modes and lunch protection
- New admin EditRequestModal with side-by-side diff (date/time, services, notes)
- Integrate edit requests into PendingApprovals card and notifications page
- Preload 3 months of availability to prevent calendar snap-back
- Apply lunch protection to isDateUnavailable in BookingFlow and BookingCreateModal
- Fix accessibility: card list items use <button> instead of <div>

Dev & Docs:
- Seed edit requests in local-dev-2.sh
- Update all Obsidian manuals with enriched edit request documentation
- 42 new tests (438/441 passing)
This commit is contained in:
2026-05-26 11:59:07 +01:00
parent 3ccc017716
commit 8574bf2221
19 changed files with 5270 additions and 599 deletions
+11
View File
@@ -541,6 +541,12 @@ The request appears in the **Pending Approvals** section on the Today page. You'
- Any notes they've added
- Any services they want to change
The system shows a **side-by-side comparison** of the original booking versus the proposed changes:
- **Original snapshot**: current start time, end time, services (with prices and durations), and notes
- **Proposed snapshot**: the new start time, recalculated end time, updated services, and new notes
This lets you see exactly what will change before you approve or decline.
### What You Can Do
**Approve** — The booking is updated to the new time and services. The customer's request is cleared.
@@ -551,12 +557,17 @@ The request appears in the **Pending Approvals** section on the Today page. You'
- The new time doesn't clash with another appointment
- The new time falls within your working hours
- **The new time doesn't fall during a holiday/closed period** — the system will block approval if the proposed time is during exceptional closed hours
- If the customer is changing services, the new total duration fits in the slot
### Can the Customer Withdraw Their Request?
Yes — a customer can cancel their own reschedule request at any time before you've reviewed it.
### What Happens When a Booking is Cancelled
If a customer cancels their booking entirely, any pending reschedule request for that booking is automatically removed, along with the associated time block and notification.
---
## The Deposit System (Admin View)
+3 -2
View File
@@ -97,6 +97,7 @@ flowchart TD
- 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
@@ -195,12 +196,12 @@ All flows integrate with holiday/exceptional hours and time blockers.
## Test Coverage
**396/399 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, and cash/gift card payments.
**438/441 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, and enriched edit request workflows.
| Package | Coverage Area |
|---------|--------------|
| `handlers/auth` | Authentication (login, register, refresh, verification) |
| `handlers/bookings` | User booking flow, guest bookings, reservations, edit requests, discounts, closing hours validation, active booking limits |
| `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 |
+58 -2
View File
@@ -209,6 +209,8 @@ src/lib/components/
| DELETE | `/api/bookings/{id}` | Cancel booking (with forgiveness option) |
| POST | `/api/bookings/{id}/edit-request` | Request booking reschedule |
| DELETE | `/api/bookings/{id}/edit-request` | Cancel edit request |
| GET | `/api/bookings/{id}/edit-request` | View own pending edit request (enriched) |
| GET | `/api/bookings/edit-requests` | List all own pending edit requests (enriched) |
| POST | `/api/bookings/{id}/payment` | Create online payment (deposit, full, partial, balance) |
| POST | `/api/bookings/{id}/tip` | Add tip to completed booking |
| GET | `/api/bookings/{id}/payment-summary` | Get payment summary for booking |
@@ -235,7 +237,9 @@ src/lib/components/
| POST | `/api/admin/bookings/{id}/confirm` | Confirm booking |
| POST | `/api/admin/bookings/{id}/cancel` | Cancel booking |
| POST | `/api/admin/bookings/reserve` | Reserve slot (walkin=5min, callin=1h) |
| GET | `/api/admin/bookings/{id}/edit-requests` | List edit requests |
| GET | `/api/admin/bookings/{id}/edit-requests` | List edit requests (paginated, with total) |
| GET | `/api/admin/bookings/edit-requests` | List ALL edit requests across all bookings (enriched) |
| GET | `/api/admin/bookings/{id}/edit-request` | View pending edit request for specific booking (enriched) |
| POST | `/api/admin/bookings/{id}/edit-requests/{request_id}/approve` | Approve edit request |
| POST | `/api/admin/bookings/{id}/edit-requests/{request_id}/deny` | Deny edit request |
| GET | `/api/admin/users` | List users |
@@ -516,6 +520,58 @@ Users manage their preferred notification channels via `/account` → Admin tab
- `GET /api/user/notification-preferences` — Returns `{emailEnabled, smsEnabled, browserPushEnabled}`. Defaults to all `true` if no row exists.
- `PUT /api/user/notification-preferences` — Accepts partial updates (only provided fields change, unset fields retain current value). Upserts on first call.
### Enriched Edit Request System
**How it works:** When a user requests a booking edit (time change, notes, or services), the system creates a `booking_edit_requests` row and returns an **enriched response** with side-by-side `original` and `proposed` snapshots. Each snapshot includes start/end times, full service details (name, price, duration), and notes.
**Enriched Response Types:**
```go
type EditServiceDetail struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
DurationMinutes int `json:"duration_minutes"`
}
type EditSnapshot struct {
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
Services []EditServiceDetail `json:"services"`
Notes *string `json:"notes"`
}
type EnrichedEditRequest struct {
ID string `json:"id"`
BookingID string `json:"booking_id"`
RequestedBy string `json:"requested_by"`
RequestedAt time.Time `json:"requested_at"`
Notes *string `json:"notes"`
Original *EditSnapshot `json:"original"`
Proposed *EditSnapshot `json:"proposed"`
User *EditUserSummary `json:"user,omitempty"`
}
```
**End-time calculation:** `end_time = start_time + sum(service durations)`. If total duration is 0, falls back to 60 minutes.
**`has_overrides` branch:** When a booking has override prices/durations on its services, the `proposed` snapshot uses the original booking services (not the `new_services` array) since service changes are blocked for overridden bookings.
**New endpoints:**
| Endpoint | Auth | Response |
|----------|------|----------|
| `GET /api/bookings/{id}/edit-request` | User (owner only) | `{"edit_request": EnrichedEditRequest}` |
| `GET /api/bookings/edit-requests` | User (own only) | `{"edit_requests": [EnrichedEditRequest]}` |
| `GET /api/admin/bookings/edit-requests` | Admin | `{"edit_requests": [EnrichedEditRequest]}` |
| `GET /api/admin/bookings/{id}/edit-request` | Admin | `{"edit_request": EnrichedEditRequest}` |
**Cancellation cleanup:** When a user cancels their booking (`UserCancelBookingHandler`), any pending edit request, associated `RESERVATION:edit_request` time_blocker, and `edit_requested` admin_notification are all deleted.
**Notification upsert:** When a user submits a second edit request (upsert), the old `edit_requested` notification is deleted and a fresh one is created — admins see a single refreshed notification with an updated timestamp, never duplicates.
**Exceptional hours validation:** When admin approves an edit request, the proposed time is checked against `exceptional_working_hours`. If the time falls during a closed period, approval is rejected with 409 Conflict.
### Loyalty & Discount System
**Loyalty Stamps:**
@@ -640,7 +696,7 @@ go test -tags "test,dev" -v -p 1 -count=2 ./... # Run twice for flaky detection
### Test Coverage
**396/399 tests passing** (3 skipped) across 12+ packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, and cash/gift card payments.
**438/441 tests passing** (3 skipped) across 12+ packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, and cash/gift card payments.
- `handlers/auth` — Authentication
- `handlers/bookings` — User booking flow, guest bookings, reservations, edit requests, discounts, closing hours validation, active booking limits
- `handlers/payments` — Square payments (terminal, online, refunds, tips, saved cards)
+5 -1
View File
@@ -234,19 +234,23 @@ If you need to change your appointment time:
1. Go to your **Account** page and find the booking
2. Select **Reschedule**
3. Pick a new date and time (the same availability rules apply — the new slot must be open)
4. Submit your reschedule request
4. Add any notes about the change (optional)
5. Submit your 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 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
**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
---