# Reschedule / Reassign + Slot Picker — Business Rules

Numbered, implementable rules the demo encodes. Each notes our enforcement.

## Permissions / status gate
1. **A booking may be rescheduled or reassigned only when its status is in the
   admin-configured allow-list** (`config.rescheduleStatuses`, default
   `['scheduled']`). Enforced in the UI AND as the store safety net for both
   actions. Demo: `lib/status.ts:85-91`, `store.ts:1381` (reschedule), `:1419`
   (reassign), `SpecialistColumn.tsx:235` (drag handle).
   **Ours: NOT ENFORCED on the write path.** `moveBooking()`
   (`BookingController.php:525-609`) never checks status before saving. Client
   gates the drag handle via `isDraggable()` = scheduled OR accepted
   (`BookingScheduleService.php:42-45`), but the `/booking/reschedule` and
   `/booking/reassign` endpoints trust the POST. **MISSING / divergent default**
   (we allow accepted too; demo defaults to scheduled-only). ⚠️

## Conflict / placement validation
2. **A move must not overlap another non-cancelled booking on the target
   specialist for that day** (the booking being moved is excluded via
   `ignoreBookingId`). Demo `checkPlacement` reason `overlap`
   (`schedule.ts`). Ours `:277-298` (excludes `$ignoreBookingId`, queries
   active statuses only).
3. **A move must not land on a time-off block** (specialist-specific OR
   shop-wide). Reason `time-off`. Ours `:300-305`; time-off resolved by
   `timeOffBlocks()` with `agent_id IS NULL OR agent_id = X` (`:160-161`).
4. **A move must sit entirely inside one of the specialist's working shifts.**
   Reason `outside-availability`. Ours `:307-317`.
5. **Rejection order is fixed: overlap → time-off → outside-availability →
   cannot-perform.** Matched exactly (`schedule.ts` vs `:273-325`).
6. **Reassign (and only reassign) additionally verifies the target specialist
   can perform every booked service.** Reason `cannot-perform`, gated on
   `serviceIds` being supplied. Demo `store.ts:1437`. Ours passes `serviceIds`
   only when `$isReassign` (`BookingController.php:561-567`); can-perform =
   `user_shop_service` covers all ids, empty link list ⇒ allowed
   (`BookingScheduleService.php:250-259`). Demo: a service with no linked
   specialists is performable by anyone (`schedule.ts canPerform`). ✅

## Slot generation
7. **Slots step by `shop.slotStepMin` (default 15 min)**, starting at the
   ceiling of each free block. Demo `SlotPicker.tsx:46`, ours
   `BookingScheduleService.php:351,358`. ✅
8. **On today, slots earlier than "now" are excluded.** Demo `:38`, ours
   `:360`. ✅
9. **Duration has a 15-minute floor** so a zero/unknown-duration booking is
   still placeable. Demo `bookingMinutes` `Math.max(total,15)`; ours
   `freeSlots` `max($durationMin, MIN_DURATION=15)` (`:33,352`). ✅
10. **End time is always DERIVED (start + summed service duration), never
    stored independently, so finance/charge math is never touched by a move.**
    Demo `schedule.ts` header + `store.ts:184-185`. Ours derives `to_hour`
    from `getScheduledDuration()` when absent (`BookingController.php:554-557`)
    and never re-derives charges. ✅

## Scoping
11. **Slot search and placement are scoped to a single shop.** Demo via
    `sp.shopId`. Ours via `shop_id = user.identity.shop_id`
    (`BookingController.php:540,619`) and `checkOwnership($model)` on move
    (`:538`). ✅ (Ours is arguably stronger — explicit ownership check.)
12. **Reschedule keeps the same specialist; reassign changes it.** Reschedule
    passes original `agent_id`, reassign reads POST `agent_id`
    (`BookingController.php:541`). ✅

## Side effects of a successful move
13. Demo pushes an **activity-log entry** ("{id} rescheduled" / "reassigned to
    {name}") capped to 60 (`store.ts:1399-1408`, `:1444-1452`). Ours does NOT
    write any audit/activity entry on move. **MISSING** (note: ours increments
    a `reschedule_count` column instead — a different, finer-grained signal the
    demo lacks).
14. Demo performs no slot-mirror bookkeeping (in-memory store). Ours
    transactionally deletes + recreates the `agent_slots` mirror row
    (`BookingController.php:577-598`) — an implementation detail with no demo
    analogue. ✅ (extra correctness on our side).

## Overnight / business-day model
15. Demo treats the day as a **business day** (open→close, overnight-aware): a
    slot can roll into the next calendar morning. `schedule.ts businessDayOf` /
    `fromBusinessMinutes`. **Ours partially: working-hour and time-off blocks are
    overnight-aware (+1440), but `freeSlots` emits `value="$date $HH:MM"` on a
    single calendar date and the overlap query is `LIKE booking_date date%`** —
    a post-midnight slot cannot be represented or matched. **PARTIAL.**

## NEW in dev demo (no rule on our side)
- Admin-configurable `rescheduleStatuses` allow-list (rule 1) — ours has no
  config equivalent and no enforcement.
- `rescheduleGroupBooking` — atomic move of every guest in a group booking to a
  shared new start (`store.ts:1614-1655`); rejects if any child would conflict.
  No equivalent group-reschedule path found on our side.
