# Shop · Specialist time-off — Business rules

Numbered, implementable rules the demo encodes, with our status.

1. **Two scopes.** A block is either specialist-scoped or whole-shop. Demo: `scope` + optional `specialistId` (`NewTimeOffModal.tsx:65-74`, `types.ts:367,373`). Ours: `agent_id` NULL = whole-shop, else specialist (`AgentTimeOff.php:14-16`, `BookingController.php:648-649`). **Done.**

2. **Shop-wide block applies to every specialist that day.** Demo: `timeOffForBusinessDay` returns a block when `scope==='shop'` regardless of specialist (`schedule.ts:207`). Ours: SQL `agent_id IS NULL OR agent_id = :id` (`BookingScheduleService.php:161`). **Done.**

3. **Specialist must belong to the current shop.** Demo: specialist list is filtered to the shop's active specialists (`NewTimeOffModal.tsx:37`). Ours: enforced server-side — a per-specialist `agent_id` must be an AGENT of the session `shop_id`, else "Invalid specialist." (`BookingController.php:657-663`). **Done (ours is stronger — server-validated).**

4. **Shop scoping is mandatory on every write.** Ours: `shop_id` is taken from `Yii::$app->user->identity->shop_id`, never from the request (`BookingController.php:644,647`). Demo: `shopId` is a prop. **Done.**

5. **Remove is shop-scoped.** A block can only be deleted if it belongs to the session shop. Ours: `AgentTimeOff::findOne(['id'=>id,'shop_id'=>shopId])` (`BookingController.php:678`). Demo store has no multi-tenant boundary. **Done (ours adds the guard).**

6. **All-day spans the shop's open→close window.** Demo: `shopWindow(shop)` → start/end ISO (`NewTimeOffModal.tsx:54-57`). Ours: when `all_day`, from/to nulled on save (`BookingController.php:652-653`) and the interval is `shopWindow()` at read time (`BookingScheduleService.php:168-170`). **Done.**

7. **Overnight blocks (end ≤ start) cross midnight (+1 day / +1440 min).** Demo: at write time, `if (endMin <= startMin) endMin += 1440` (`NewTimeOffModal.tsx:61`). Ours: at read time, `if ($end <= $start) $end += 1440` (`BookingScheduleService.php:177-178`). **Done — equivalent, computed on opposite ends.**

8. **A non-all-day block must have non-equal times.** Demo: `valid` requires `start !== end` (`NewTimeOffModal.tsx:48`). Ours: **NOT enforced** — no client guard, and `AgentTimeOff::rules()` only requires both hours present when not all-day (`AgentTimeOff.php:64-68`), not that they differ. **Missing.**

9. **When not all-day, both From and To are required.** Demo: implied by the inputs. Ours: conditional `required` rule (`AgentTimeOff.php:65-68`). **Done (ours explicit).**

10. **Reason restricted to the fixed enum** (`break/vacation/sick/holiday/closed/other`). Demo: `TimeOffReason` union (`types.ts:368`). Ours: `in` range validator over `REASONS` keys (`AgentTimeOff.php:62`). **Done.**

11. **Time-off subtracts from working shifts to yield availability** (working − time-off = free). Demo: `specialistAvailability` (`schedule.ts:212-223`). Ours: `availability()` (`BookingScheduleService.php:217-227`). **Done.**

12. **A booking cannot be placed over a time-off block** (specialist or shop-wide); rejection precedence overlap → time-off → outside-availability → cannot-perform. Demo: `checkPlacement` (`schedule.ts:312-348`). Ours: `checkPlacement` (`BookingScheduleService.php:273+`, time-off at `:301`). **Done.**

13. **No quota / pricing / VAT / status transitions** apply to time-off. It is a pure availability artifact and never touches the ledger (demo header comment, `schedule.ts:1-17`). Ours likewise stores no money fields. **Done (N/A by design).**

14. **Block has no edit flow — only create + delete.** Demo: no edit action in store (`store.ts:1695,1715`). Ours: only `actionTimeOff` (create) and `actionRemoveTimeOff` (delete). **Done (parity by omission).**

15. **Permission/auth.** Demo runs in the authenticated shop portal. Ours: actions live in the shop `BookingController` behind the portal's access control; `time-off` and `remove-time-off` are POST-only (`BookingController.php:38-39`). **Done.**
