# Shop · Dashboard — Business Rules

Rules the demo encodes (`Dashboard.tsx` + `store/selectors.ts` + `types.ts`), numbered and
implementable. "Ours" notes where (if at all) we enforce the same.

1. **Active shop scoping.** All figures are scoped to one shop id (`Dashboard.tsx:44`; `bookingsForShop`/`chargesForShop` filter by `shopId`). — Ours scopes by the logged-in user's single shop (`SiteController.php:85,333`). ✅ equivalent for our model.

2. **Upcoming-Bookings KPI = count of `scheduled` bookings** for the shop (`selectors.ts:361` via `statusCounts`, all-time). — Ours counts `STATUS_SCHEDULED` but **within the selected date range** (`SiteController.php:337,349`). PARTIAL (scope differs: all-time vs range).

3. **Completed KPI = count of `completed` bookings** ("realised", all-time) (`Dashboard.tsx:173-179`). — Ours counts `STATUS_COMPLETED` within date range (`:338,358`). PARTIAL.

4. **Net Available = withdrawable settlement balance** = sum over *available* (not consumed by a transfer/invoice and not yet settled) bookings of collected − refunds + tips − settlement fees, i.e. `availableSettlement.eligibleNet` (`selectors.ts:331-338,365`). — **No equivalent.** No Charge/Transfer/Settlement model. Closest is `Shop::getTotal_services_amount`/`getTotal_amountsWithdrawn` (`base/Shop.php:424-464`) but that is not surfaced on the dashboard and not computed the same way. MISSING.

5. **Withdrawal-minimum rule:** withdrawal allowed only when `eligibleNet >= shop.minWithdrawalAmount` (default **5000**) (`selectors.ts:367`, `types.ts:29`). — We *have* the field (`minimum_withdrawal_amount`, default 5000, `base/Shop.php:67,216-218`) but the dashboard does **not** show/enforce eligibility. MISSING on dashboard.

6. **Costs to Date = Σ(chargeAmount + vatAmount)** over non-pending charges, grouped into Subscription / Marketing fee / Payment-processing fee / Notifications (sms_fee + wa_fee); reversals net out because reversal rows are negative (`selectors.ts:399-409`). — **No Charge model.** MISSING. (Note: VAT is added to each cost line — relevant if we ever build this.)

7. **Subscription display rules** (`Dashboard.tsx:142-154`):
   - `status === 'free_period'` → amber "Free period" badge + "Trial ends {relativeDays} · then {monthlyPrice}/mo".
   - active → "Active · next billing {date} · card ••{last4}".
   - none → "Subscribe to unlock the full platform".
   — **No Subscription/Plan model.** MISSING. (Our analogue is contract acceptance + shop status, a different lifecycle: `SiteController.php:820-933`.)

8. **Recent-bookings list = 6 most recent by `appointmentDate` desc** with Value + dual badges (`Dashboard.tsx:51-53,99-104`). — Ours = up to 5/6 **future** scheduled/in-progress bookings ascending, no value/badges (`SiteController.php:443-450`, `index.php:31`). PARTIAL.

9. **Settlement badge per booking** (`settlementStateFor` `selectors.ts:461-478`): `in_tr` if in a non-settled transfer; `settled` if in a settled transfer OR all `net_from_settlement` fee rows paid and collectable ≤ 0.005; else `pending`. — **No equivalent.** MISSING.

10. **Booking value = subtotal − discount** (`types.ts:317`, `bookingValue`). — We compute service totals/commission in `base/Shop.php:424-441` but don't surface a per-booking value on the dashboard. N/A on this screen.

11. **Booking-link / deep link:** each shop has a `deepLinkUrl` exposed for copy-to-clipboard (`types.ts:36`, `Dashboard.tsx:267-273`). — **No `deepLinkUrl` field** on our Shop model. MISSING.

12. **Permissions / access (ours-only, stronger):** dashboard requires non-guest with `manager`/`administrator`/`shopOwner`; enforces phone-verification and shop verification+status (`ACTIVE`/`AWAITING_CONTRACT`) gates, else forced logout (`SiteController.php:84-148`). The demo mock encodes none of this. NEW on our side (keep).

13. **Date-range rule (ours-only):** presets today/7d/30d/custom, custom capped at 30 days, invalid range falls back to today (`SiteController.php:270-329`). Demo has no range on the dashboard. NEW on our side.

14. **Specialist-occupancy capacity assumption (ours-only):** capacity = `days × 8h × 60` min per specialist; occupancy% = min(100, booked/capacity) (`SiteController.php:547-560`). Hard-coded 8h/day. NEW (but un-rendered).
