# Page Review — Customers (P10) + Invitations (P11)

- **Routes:** `/customers` (index, freeze, unfreeze) · `/customer-invitations` (index, create 🔴 outbound)
- **Controllers:** `CustomersController` · `CustomerInvitationsController`
- **Reviewed:** 2026-07-01 · **Status:** 🔍 (code agent running)

## 2. Design findings
| # | What | Actual | Sev | Evidence |
|---|------|--------|-----|----------|
| D1 | Customers directory | ✅ RTL; QR booking-link card (copy/WhatsApp/SMS), grace-period banner, tabs العملاء(9)/قائمة التجميد(1)/التصنيفات, table (mobile/gender/bookings/classification/freeze toggle) | — | ss_93589l6gj |
| D2 | "Bulk invitations are reviewed…" note | English text in Arabic UI | LOW | ss_93589l6gj |
| D3 | Invitations composer | ✅ RTL; paste-numbers textarea + dedup counter, WhatsApp/SMS toggle, AR/EN template preview with `{BusinessName}` interpolation | — | ss_0457vcu9j |

## 3. Logic findings
| Action | Result |
|---|---|
| 🟡 freeze / unfreeze (customer marketing-fee status) | NOT live-triggered (affects real customer); code-review covers IDOR |
| 🔴 invitation create (sends WhatsApp/SMS) | NOT live-triggered (outbound spam risk); code-review covers rate-limit/injection |
| 🟢 index | render verified |

## 4. Code trace
**Code-review agent — 0 CRIT, 1 HIGH, 2 MED. IDOR question answered: clean.**
| Sev | File:line | Issue | Status |
|---|---|---|---|
| HIGH | CustomerInvitationsController.php:69,100 | `$shop = identity->shop` deref with no null guard → 500 for shopOwner-without-shop | ✅ FIXED (R-H010) |
| MED | CustomerInvitationsController.php:104 | rate-limit cooldown is cache-only → cache flush removes throttle (spam window) | ⚠️ LOGGED R-L19 (persist last-sent in campaign table) |
| MED | CustomerInvitationsController.php:135 | `array_slice(.., MAX+1)` then `count>MAX` always true → silent-cap branch is dead (harmless: always rejects oversized) | ⚠️ LOGGED R-L20 |
| LOW | CustomersController behaviors | `roles:['@']` (not `['shopOwner']`) — weaker than CustomerInvitations; FrontEndController gate covers in practice | ⚠️ LOGGED |

**Confirmed clean:** `unfreeze` IDOR scoped (`['id'=>$id,'shop_id'=>$shopId]`); `freeze` operates on a `(shop_id, mobile)` freeze-list row, not a user account → no cross-user targeting (by design); customers index derived from `Booking::where(shop_id)` then bounded `User::where(id IN …)` — no cross-shop PII leak, sensitive columns not selected; CSRF on freeze/unfreeze/create (VerbFilter POST + X-CSRF-Token).

## 5. Fixes applied
| Issue | File:line | Change | Re-verified |
|-------|-----------|--------|-------------|
| R-H010 null-`$shop` 500 | CustomerInvitationsController.php:69,100 | null guard (redirect / JSON error) before `$shop->id` | ✅ php -l clean |

## 6. Sign-off
- [x] Customers + Invitations design verified (RTL)
- [x] code-review folded; HIGH null-guard fixed; IDOR confirmed clean
- [ ] MED cooldown/slice logged for hardening
