# Shop · Customers directory + freeze — Business rules

Rules the demo encodes. Each marked with our implementation status.

## Directory scoping
1. The customer directory is **derived from booking history**, not a separate roster: a customer appears only if they have ≥1 booking at the active shop. Demo `Customers.tsx:26-29`; ours `CustomersController.php:57-69`. **DONE.**
2. The directory is **shop-scoped** to the logged-in shop. Demo `shop.id`; ours `Yii::$app->user->identity->shop_id` (`CustomersController.php:45-48`). **DONE.**
3. Bookings count per customer = number of bookings for `(shop, customer)`. Demo `Customers.tsx:121-122`; ours `COUNT(*)` (`CustomersController.php:60`). **DONE.**

## Customer classification (attribution)
4. On a customer's **first booking** at a shop, a classification is assigned and **persisted immutably**; subsequent bookings reuse it. Demo `store.ts:1264-1294`. **MISSING on our side** (no classification table/engine).
5. Classification is decided by this **priority order**: walk-in → `shop_owned`(`shop_admin_walkin`); deep-link → `shop_owned`(`deep_link`); on freeze list → `shop_owned`(`freeze_list`); via app → `navagoo_sourced`(`app_first_booking`). Demo `lib/finance.ts:308-312`. **MISSING.**
6. There are exactly **two classifications**: `navagoo_sourced` and `shop_owned`. Default when none recorded = `shop_owned`. Demo `selectors.ts:205-210`. **MISSING.**
7. Only `navagoo_sourced` (app-first) customers incur the **marketing fee**; `shop_owned` (walk-in, deep-link, freeze-listed) do not. This is the entire business purpose of the freeze list. Demo `lib/finance.ts` + `Customers.tsx:66-70` explainer. **MISSING** (we store the freeze list but there is no marketing-fee/classification engine consuming it on this side).
8. Classification is **read-only to the shop**; only a Navagoo **admin** may override it, stamping `overriddenBy` + `overrideReason` for audit. Demo `Customers.tsx:183`, `store.ts:1719-1758`. **MISSING.**

## Grace window
9. A shop is "in grace" while `now <= graceWindowEndsAt`. Demo `lib/finance.ts:293-294`. **MISSING** (no `graceWindowEndsAt` on our `Shop`).
10. **Inside** the grace window, shops are prompted to upload existing customers so they stay `shop_owned` (no marketing fee). **After** grace, freeze-listed numbers are *still always* treated as shop customers on first booking. Demo `Customers.tsx:233-237`. **MISSING** (banner + behavioural distinction absent).

## Freeze list
11. A freeze entry is unique per **(shop, mobile)**. Demo: per-shop list keyed by mobile; ours enforces `unique(shop_id,mobile)` (`CustomerFreeze.php:49-51`) + idempotent add (`CustomersController.php:119-122`). **DONE (ours stricter).**
12. Mobile is **required**; Name is **optional**. Demo `Customers.tsx:275,289-298`; ours `CustomerFreeze.php:43` + `CustomersController.php:115-117`. **DONE.**
13. Mobile should be matched **format-insensitively**. Demo compares raw strings (no normalization — weakness). Ours normalizes to digits via `normalizeMobile` (`CustomerFreeze.php:69-72`) for both storage and freeze-membership checks. **DONE (ours better).**
14. Removing a freeze entry lets that number book again (and, per the model, be re-classified). Removal is **shop-scoped**. Demo `Customers.tsx:213-216`; ours `findOne(id, shop_id)` (`CustomersController.php:139`). **DONE.**
15. Removal should be **confirmed** before deleting. Demo `confirm()` `Customers.tsx:213`. **MISSING** on our side (immediate delete).

## Booking link / invitations
16. Each shop has a shareable **deep-link URL + token** (`deepLinkUrl`, `shopToken`); customers who book via the app first become `navagoo_sourced`, deep-link sign-ups stay `shop_owned`. Demo `types.ts:36-37`, `Customers.tsx:66-70`. **MISSING** on this screen (no deep link/QR/copy surfaced).
17. Shops can **invite a customer** (SMS + WhatsApp). Demo `Customers.tsx:86-92` (simulated). **MISSING** on this screen (a separate `customer-invitations` feature exists but is not linked here).

## Permissions
18. Screen requires an authenticated shop user (`roles ['@']`, `CustomersController.php:31-32`). Freeze/unfreeze are **POST-only** (`VerbFilter`, `CustomersController.php:35-41`) and CSRF-protected (`index.php:170-173`). **DONE.**
