# Admin · Shops — Business Rules (numbered, implementable)

Source of truth: demo `Shops.tsx`, `store/store.ts`, `lib/finance.ts`, `selectors.ts`, `types.ts`.
"Ours" = `backend/controllers/ShopController.php`, `common/models/Shop.php` / `common/models/base/Shop.php`, `backend/views/shop/*`.

---

### Identity & creation
1. **Shop ID format** is `NSH-YYMM…` issued at creation, unique across shops (`store.ts:1088` `uniqueShopId`). Ours: numeric PK + `formatted_id` via `IdGeneratorHelper::generateShopId` (`index.php:230`). — different scheme, functionally present.
2. **On create, a deep link** `https://navagoo.app/s/<slug>` and a **shopToken** are generated (`store.ts:1113-1114`). Ours: **NOT generated / not stored** — missing.
3. **On create, a grace window** is opened: `graceWindowEndsAt = simNow + config.graceWindowDays` (`store.ts:1089-1091`). Ours: **no grace window** — missing.
4. **Admin-created shop starts `status:'inactive'`, `verificationStatus:'requested'`** (`store.ts:1115-1140`). Ours **violates this**: admin-created shops are forced `STATUS_ACTIVE` + `VERIFICATION_STATUS_VERIFIED` (`ShopController.php:206-207`).
5. **Create also provisions an owner User** (role owner, `passwordSet:false`) (`store.ts:1137-1140`). Ours: `signup()` creates User + UserProfile (`ShopController.php:228`). — present.
6. **Save is allowed only when `commercialName` and `email` are non-empty** (`Shops.tsx:255`). Ours enforces via Yii validation rules (`common/models/base/Shop.php` rules) — present, stricter.

### Defaults
7. **`minWithdrawalAmount` default = 5000 SAR** (`types.ts:29`, `store.ts:1108`). Ours: `minimum_withdrawal_amount` default 5000 (`base/Shop.php:218`, `_form.php:487`). — DONE.
8. **`settlementHoldDays` default = 7** (`types.ts:31`, GlobalConfig 530). Ours: `minimum_elapsed_period_days` default 7 (`_form.php:495`). — DONE (renamed).
9. **`depositPct` default = 30** for new shops (`store.ts:1124`). Ours: deposit % lives in `ShopPaymentSettings`; shop-level default not asserted here.
10. **`marketingFeeRatePct`** is a per-shop rate, default 5 in the form (`Shops.tsx:228`); GlobalConfig.marketingFeePct = 5 (`types.ts:523`). Ours: **no per-shop marketing-fee rate column** — marketing fee is computed per booking on `ShopEarning` (`navagoo_marketing_percentage`/`navagoo_marketing_fees`). Missing as an editable shop attribute.

### VAT
11. **`vatRegistered` boolean; "15% VAT on services"** when registered (`Shops.tsx:331`, GlobalConfig.vatPct = 15). Ours: `is_taxable` (IS_TAXABLE_YES/NO) (`_form.php:432-445`, `index.php:260`). — DONE (boolean equivalent). The 15% rate itself is applied in earnings, not here.

### Deposit cap
12. **`maxDepositPct` is an optional per-shop override of the global deposit cap; blank = inherit global** (`types.ts:48-50`, `Shops.tsx:308-318`). Ours: `max_deposit_percent_override`, integer 1–100 (`base/Shop.php:27`, `_form.php:318`). — DONE.

### Credit limit / carry-forward
13. **`carryForwardThreshold` optional per-shop credit-limit override; blank = inherit `GlobalConfig.carryForwardThreshold`** (`types.ts:30`, `Shops.tsx:336-348`). Ours: **no such field** — missing.

### Verification state machine
14. Demo `verificationStatus ∈ {requested, activated_pending_auth, active, rejected}` (`types.ts:74-79`). Transitions:
    - 14a. `requested → activated_pending_auth` on admin **Activate** (`store.ts:1196`).
    - 14b. `activated_pending_auth → active` only after owner OTP-verifies AND accepts terms (`store.ts:1242-1244`), which also sets `status:'active'`.
    - 14c. `* → rejected` on admin **Reject** (`store.ts:1209`).
    Ours uses numeric `status ∈ {NOT_ACTIVE=0, ACTIVE=1, NEW=2, REJECTED=3, RE_ORDER=4, AWAITING_CONTRACT=5}` (`base/Shop.php:83-88`) PLUS `verification_status ∈ {0,1}` and `verification_contract_status ∈ {0,1}` (`base/Shop.php:94-99`). Activation branch (`ShopController.php:983-1010`): has-contract+accepted → ACTIVE; has-contract+not-accepted → AWAITING_CONTRACT; no-contract → ACTIVE. — **PARTIAL / different model**; the demo never makes a shop active purely from admin action (always needs owner auth), whereas ours can flip to ACTIVE on activate when no contract is required.

### Activation captures
15. **Activate persists**: onboardingDocs (6 doc types), vatRegistered, and commercials {marketingFeeRatePct, minWithdrawalAmount, settlementHoldDays, maxDepositPct, carryForwardThreshold, bankName, iban} (`store.ts:1187-1196`). Ours: activate persists docs + status + verification; commercials partially editable elsewhere (`_form`), but **marketingFeeRatePct & carryForwardThreshold not captured at all**. — PARTIAL.
16. **Activate queues an owner email** with link `#/activate?shop=<id>` (`store.ts:1196-1205`); owner then OTP + terms. Ours: `sendAuthenticationEmail` + (if active) activation email (`ShopController.php:1016-1025`). — DONE (equivalent intent).

### Onboarding documents
17. Six document slots: **cr, vatCert, shopPermit, bankCert, nationalAddress, enrolmentAgreement** (`types.ts:85-92`, `Shops.tsx:461-468`). Ours: cr_document, tax_certificate, shop_permit, iban_account (bank cert), national_address, service_provider_contract (`index.php:62-68`). — DONE (1:1 mapping; "enrolmentAgreement" ≈ service_provider_contract).

### Reject
18. **Reject sets verificationStatus='rejected'** after confirm; no reason captured (`store.ts:1209`, `Shops.tsx:403-419`). Ours: `STATUS_REJECTED` + **mandatory-style reason** + owner email with the reason (`ShopController.php:759-761,818-822`). — DONE+ (richer).

### Activate/deactivate live shop
19. **Active↔inactive toggle is blocked while in grace** (grace badge replaces the toggle) (`Shops.tsx:131-167`). Deactivate requires confirm and "does not affect existing bookings". Ours: no grace gating; status changed via routed status page; `actionToggleApprovalStatus` flips active/rejected (`ShopController.php:795`). — PARTIAL (no grace gate, no inline toggle).
20. **`isInGraceWindow(shop, now)` = now ≤ graceWindowEndsAt** (`lib/finance.ts:293`). Ours: **no equivalent** — missing.

### Subscription plans
21. **Plan prices**: 6-month = round(monthly × 6 × 0.9), 12-month = round(monthly × 12 × 0.8) auto-set on create (10% / 20% discounts) (`Shops.tsx:608-610`). Ours: **no plans** — missing.
22. **`subscriptionPrice(plan, period)`** returns 12mo→twelveMonthPrice, 6mo→sixMonthPrice, else monthly (`lib/finance.ts:648`). Ours: **missing**.
23. **New plan default**: `freePeriodDays` 30 (default in form), `autoDeactivateOnExpiry: true`, seed features (`Shops.tsx:611-619`). Ours: **missing**.
24. **Delete plan** removes the plan (confirm) (`store.ts:1787`, `Shops.tsx:572-579`). Ours: **missing**.

### Subscriptions
25. **One subscription per shop**, resolved by `subscriptionForShop` (first match on shopId) (`selectors.ts:41`). Status ∈ {free_period, active, cancelled, flagged} (`types.ts:471`). Card shown masked as `••<last4>`. Next-billing shown with relative-days. Ours: **missing**.

### Permissions / scoping
26. The whole page is **admin-portal scoped** (`portals/admin/Shops.tsx`). Ours: `ShopController` resolves the current user's RBAC role (`ShopController.php:39-41`); admin backend context. No per-shop scoping needed (admin sees all shops). — DONE (admin-only).

### Type → label
27. **Shop `type ∈ {men, women, all}`; `all` displays "Unisex"** (`types.ts:13`, `Shops.tsx:115`). Ours: shop `gender` map (`index.php:255` `gender()`); "all"→Unisex mapping should be verified in the label map. — PARTIAL/verify.
