# Admin · Shops — Parity Matrix

Demo (canonical): `portals/admin/Shops.tsx` + `store/store.ts`, `lib/finance.ts`, `selectors.ts`, `types.ts`
Ours: `backend/controllers/ShopController.php`, `backend/views/shop/*`, `common/models/(base/)Shop.php`

| # | Demo behavior | Demo ref | Our ref | Status | Note |
|---|---|---|---|---|---|
| 1 | 4-tab IA: Shops / Requests / Plans / Subscriptions | Shops.tsx:49-63 | backend/views/shop/index.php (single dir) | partial | Only directory exists; other 3 tabs absent |
| 2 | Shops directory table (avatar, city, type, VAT, mkt%, status, deep-link, edit) | Shops.tsx:89-200 | index.php:188-336 | partial | Most columns present; missing mkt%, deep-link, grace |
| 3 | Add-shop button → create modal | Shops.tsx:44-47,204 | ShopController.php:183; views/shop/create.php | done | Full-page form instead of modal (Tailwind divergence) |
| 4 | Create generates NSH-… id | store.ts:1088 | index.php:230 (IdGeneratorHelper) | done | Different scheme, equivalent |
| 5 | Create generates deep link + shopToken | store.ts:1113-1114 | — | missing | No deep-link/token columns |
| 6 | Create opens grace window (simNow+graceWindowDays) | store.ts:1089-1091 | — | missing | No grace-window concept |
| 7 | New shop starts inactive / requested | store.ts:1115-1140 | ShopController.php:206-207 | missing | Ours forces ACTIVE+VERIFIED on admin-create (opposite) |
| 8 | Create provisions owner User (passwordSet:false) | store.ts:1137-1140 | ShopController.php:228 (signup) | done | |
| 9 | After create, auto-open Activate modal | Shops.tsx:33-35,239-243 | — | missing | No post-create activation step |
| 10 | Save gated on name+email | Shops.tsx:255 | base/Shop.php rules | done | Yii validation, stricter |
| 11 | Edit via same form (patch merge) | store.ts:1146; Shops.tsx:204 | ShopController.php:331; _form.php | done | Richer edit form |
| 12 | minWithdrawal default 5000 | types.ts:29; store.ts:1108 | base/Shop.php:218; _form.php:487 | done | |
| 13 | settlementHoldDays default 7 | types.ts:31 | _form.php:495 (minimum_elapsed_period_days) | done | Renamed field |
| 14 | Per-shop marketingFeeRatePct (default 5) editable | Shops.tsx:228,290 | — | missing | Marketing fee is per-booking on ShopEarning, not a shop rate |
| 15 | maxDepositPct override (blank=inherit) | types.ts:48-50 | base/Shop.php:27; _form.php:318 | done | max_deposit_percent_override 1–100 |
| 16 | carryForwardThreshold override (blank=inherit) | types.ts:30; Shops.tsx:336 | — | missing | No credit-limit override field |
| 17 | vatRegistered toggle (15% VAT) | Shops.tsx:331 | _form.php:432-445; index.php:260 | done | is_taxable boolean |
| 18 | Requests queue (owner contact, submitted-at) | Shops.tsx:351-457 | — | missing | No dedicated queue; status-chip funnel only |
| 19 | shopsAwaitingReview = verificationStatus 'requested' | selectors.ts:456 | index.php:38 (status_new count) | partial | Approximated by NEW/RE_ORDER/AWAITING counts |
| 20 | Activate modal: 6 docs + commercials + bank + VAT | Shops.tsx:474-541 | ShopController.php:947; views/shop/activate.php | partial | Docs+status yes; mkt-fee & carry-forward not captured |
| 21 | Activate sets verificationStatus 'activated_pending_auth' (not active) | store.ts:1196 | ShopController.php:974-1010 | partial | Ours may flip ACTIVE on activate when no contract |
| 22 | Activate emails owner authenticate link | store.ts:1196-1205 | ShopController.php:1016 | done | sendAuthenticationEmail |
| 23 | Owner OTP + accept terms → active | store.ts:1211-1245 | (frontend/onboarding flow) | partial | Exists but different state model; out of this controller |
| 24 | 6 onboarding doc slots | types.ts:85-92 | index.php:62-68 | done | 1:1 mapping |
| 25 | Reject → verificationStatus 'rejected' (confirm) | store.ts:1209; Shops.tsx:403 | ShopController.php:759-761,818-822 | done | Ours adds reason + owner email |
| 26 | Inline active/inactive Toggle (confirm on deactivate) | Shops.tsx:131-167 | index.php:299-308; ShopController.php:795 | partial | Routed status page, no inline toggle |
| 27 | Grace badge blocks toggle while in grace | Shops.tsx:138-145; finance.ts:293 | — | missing | No grace window |
| 28 | Type 'all' renders "Unisex" | Shops.tsx:115 | index.php:255 (gender map) | partial | Verify 'all'→Unisex label mapping |
| 29 | Deep-link "open" external anchor in row | Shops.tsx:170-184 | — | missing | No deep link |
| 30 | Subscription Plans: cards + create (auto 10%/20% discount) | Shops.tsx:543-658; store.ts:1784 | — | missing | No plan model/UI |
| 31 | subscriptionPrice(plan, period) | finance.ts:648 | — | missing | |
| 32 | Delete plan (confirm) | store.ts:1787 | — | missing | |
| 33 | Subscription Dashboard (plan/status/next-billing/card) | Shops.tsx:660-755 | — | missing | No subscription model/UI |
| 34 | subscriptionForShop / planById selectors | selectors.ts:40-41 | — | missing | |
| 35 | EmptyState components (requests, subs) | Shops.tsx:357-364,752 | index.php:226 | partial | Dir has "No results"; missing screens have none |
| 36 | Admin-portal scoping | portals/admin/Shops.tsx | ShopController.php:39-41 | done | Admin-only backend |
| — | Live/Demo mode toggle (NOT in demo) | — | ShopController.php:847; index.php:287-298 | n/a | Our extra feature |
| — | Rejection reason + email (richer than demo) | — | ShopController.php:759-761 | n/a | Our extra |

## Tally
- done: 13
- partial: 10
- missing: 11
- (n/a / our extras: 2 — excluded from score)

Weighting partial as 0.5: (13 + 10×0.5) / 34 = 18 / 34 ≈ **53%**.

**Area score: 53%**

### Biggest gaps (priority order)
1. Subscription **Plans** + **Subscription Dashboard** tabs — entirely absent (rows 30-34). Largest scope hole.
2. **Grace window** + deep link + shop token — not modeled (rows 5,6,27,29).
3. Admin-create should land **inactive/requested**, not auto-ACTIVE (row 7) — behavioral mismatch.
4. **Per-shop marketing-fee rate** + **carry-forward credit limit** fields (rows 14,16).
5. Dedicated **Requests queue** screen + inline activate/reject (rows 18,26).

## Verified verdict (adversarial)

Re-checked every "done"/high claim against both demo and our code. Most held; two
overclaims downgraded with concrete evidence.

### Confirmed (held up under scrutiny)
- **#3 Add shop (create)** — `ShopController.php:183` builds + persists Shop. Core create works (modal→full-page is a Tailwind divergence, already noted).
- **#12 minWithdrawal default 5000** — `common/models/base/Shop.php:218` + `_form.php:487`. Match.
- **#13 settlementHoldDays default 7** — `common/models/base/Shop.php:219` (renamed `minimum_elapsed_period_days`). Match.
- **#15 maxDepositPct override** — `_form.php:318`, range 1–100. Field present (blank-inherit semantics not strictly proven but acceptable).
- **#17 vatRegistered toggle (15% VAT)** — toggle at `_form.php:432-445`; VAT actually wired in `common/models/base/Earnings.php:547-573` (`calculateShopVat` reads `Settings.taxes` rate). Rate is a global setting, not hardcoded 15%, but the toggle drives the calc. Match.
- **#22 Activate emails owner authenticate link** — `ShopController.php:1016` (`sendAuthenticationEmail`, builds activation token + URL). Match.
- **#24 6 onboarding doc slots** — `index.php:62-68` lists 6 doc labels. Match.
- **#25 Reject request** — `ShopController.php:759-761,818-822`, adds reason + owner email. Richer than demo. Match.
- **#36 Admin-portal scoping** — `ShopController.php:35-52` (`beforeAction`): guest → loginRequired, manager → per-permission check. Backend admin-only. Match.

### Overclaims downgraded
| # | Feature | Was | Now | Evidence |
|---|---|---|---|---|
| 8 | Create provisions owner User | done | partial | `ShopController.php:277-320` (`signup`) sets `User::STATUS_ACTIVE` + `setPassword(...)`. Demo `store.ts:1137-1140` provisions owner with `passwordSet:false` (owner must set password via the activate→OTP flow). Ours provisions a fully-active, password-set owner at admin-create time — wrong lifecycle, compounds the #7 ACTIVE-on-create mismatch. UserProfile is created, so the row is not "missing". |
| 28 | Type 'all' renders "Unisex" | partial | partial (label mismatch confirmed) | `common/models/base/Shop.php:710-716` maps `GENDER_ALL(3)` → `"Men & Women"` / `"رجال ونساء"`, never "Unisex". Demo `Shops.tsx:115` renders `type==='all' ? 'Unisex'`. Label diverges; status stays partial but the claim of a clean mapping is refuted. |

### Other notes from re-check
- **#19 shopsAwaitingReview (partial — confirmed)**: `index.php:38` counts `status IN (NEW, RE_ORDER, AWAITING_CONTRACT)`, whereas demo `selectors.ts:456` filters `verificationStatus === 'requested'`. Status (operational) ≠ verificationStatus (review) axis — approximation only.
- **#21 Activate sets activated_pending_auth (partial — confirmed)**: `ShopController.php:974-1010` can flip straight to `STATUS_ACTIVE` when no contract is required; demo `store.ts:1196` always lands `activated_pending_auth`. Divergence holds.

### Adjusted tally
Downgrading #8 done→partial: **done 12, partial 11, missing 11** (n/a excluded).
Score (partial = 0.5): (12 + 11×0.5) / 34 = 17.5 / 34 ≈ **51%**.

**Adjusted area score: 51%** (analyst claimed 53%).
