# Admin · Marketing — Parity Matrix

Demo = `portals/admin/Marketing.tsx` + `portals/marketing/Deals.tsx` (canonical).
Ours = `backend/` PromoCode / Ads / PushNotification controllers + views.

| Demo behavior | Demo ref | Our ref | Status | Note |
|---|---|---|---|---|
| Unified Marketing hub with Deals/Ads/Push tabs | Marketing.tsx:55-67 | — | missing | We have 3 separate admin screens, no tabbed hub |
| Deals-count badge (platform-wide only) | Marketing.tsx:52 | — | missing | No count badge anywhere |
| **Deals** — list platform-wide deals | Deals.tsx:21 | backend/views/promo-code/index.php | partial | Lists ALL promo codes; no platform-only filter |
| Discount type percent/fixed render (%/SAR) | Deals.tsx:31-37 | common/models/base/PromoCode.php (TYPE_*); promo-code/index.php | done | Same semantics |
| Usage progress bar (count/cap) | Deals.tsx:42-49 | — | partial | uses/max_uses stored, not rendered in admin |
| Expiry column | Deals.tsx:51 | promo-code/index.php (Expiry Date) | done | dd/mm/yyyy stored |
| Create deal modal: desc/type/value/cap/expiry | Deals.tsx:115-176 | backend/views/promo-code/_form.php | partial | Full-page form; no description, no usage-cap, no shop_id field |
| Usage cap default 100 | Deals.tsx:130 | — | missing | max_uses not exposed/defaulted in form |
| Expiry default 2026-12-31 if blank | Deals.tsx:138 | PromoCode rules (expiry_date required) | done | Ours requires it (stronger) |
| Create disabled until valid | Deals.tsx:124 | PromoCode required rules | done | Server-side validation |
| Delete deal w/ confirm | Deals.tsx:55-67 | PromoCodeController.php:163-168 | done | POST + deleteWithRelated cascade |
| Deals empty state | Deals.tsx:84-90 | promo-code/index.php ("No results found.") | done | Less rich copy |
| Shop-scoped deals (shopId set) | Deals.tsx:17,21 | promo_code.shop_id (nullable) | partial | Data supports it; admin form can't set it |
| **Ads** — campaign cards (name/placement) | Marketing.tsx:71-83 | backend/views/ads/index.php | missing | Ours = banner image, no campaign metadata |
| Ad impressions metric | Marketing.tsx:84-87 | — | missing | No analytics |
| Ad CTR metric | Marketing.tsx:88-92 | — | missing | No analytics |
| Ad status live/scheduled | Marketing.tsx:78-80 | — | missing | No status field on Ads |
| Ad placement enum | Marketing.tsx:13-44 | — | missing | No placement concept |
| Ad image upload + 340x160 crop | — | AdsController.php:54-72 | done* | *New in ours; demo has none |
| Ads shop filter / CRUD | — | AdsController.php; ads/index.php | done* | *New in ours |
| **Push** — compose (title + audience) | Marketing.tsx:104-127 | backend/views/push-notification/index.php | done | Ours richer (bilingual + route) |
| Push send | Marketing.tsx:113-122 | PushNotificationController.php:83-141 | done | Ours real FCM; demo simulated |
| Push audience targeting → channel | Marketing.tsx:108 | PushNotificationController.php:103-129 | done | customers/agents/all → FCM topics |
| Push recent sends list | Marketing.tsx:128-153 | push-notification/history.php; actionHistory | done | Ours persisted/paginated |
| Push open/delivery rate metric | Marketing.tsx:46,149 | — | missing | No analytics (demo mock-only) |
| Permission/role gating | — (no RBAC in demo) | beforeAction in all 3 controllers | done* | *Ours stronger |
| Bilingual + RTL | — (EN/LTR only) | common/messages/{ar,en}/backend.php | done* | *Ours stronger |

## Summary
- **Deals**: strong real backing (PromoCode), but UI/form gaps (no description, usage bar,
  usage-cap input, shop-scope selector) and no platform-vs-shop list filter.
- **Ads**: divergent — ours is a banner-image manager; demo is a (mock) campaign-analytics view.
  No shared analytics concepts. Largest functional gap.
- **Push**: ours **exceeds** the demo (real FCM, bilingual, audience, persisted history); only the
  demo's mock open-rate analytics are absent.
- **Structural**: no unified tabbed Marketing hub; three separate admin entries.

Area score: 55%

## Verified verdict (adversarial)

Verifier opened every "done" claim against BOTH the demo ref and our code. Findings below.
Most "done" claims survived; two were tightened and the blended score was adjusted down
for the size of the Ads feature gap + the missing structural hub.

### Confirmed (claim holds)
- **Discount type %/SAR render** — `promo-code/index.php:111-114` emits ` % ` for
  `TYPE_PERCENTAGE` (=1) and ` S.R ` for fixed; `PromoCode::discountTypes()`
  (`common/models/base/PromoCode.php:130-135`) matches demo `Deals.tsx:31-37` semantics.
- **Expiry required (stronger than demo default)** — `PromoCode` rules line 68 makes
  `expiry_date` `required`; demo merely defaults to `2026-12-31` (`Deals.tsx:131,138`).
- **Delete deal w/ confirm** — POST-only via `VerbFilter` (`PromoCodeController.php:39-45`) +
  `deleteWithRelated()` (`:165`); confirm dialog wired on the detail page
  (`promo-code/view.php:69-74`, `data-confirm`). NOTE: delete lives on the VIEW page, not the
  list row (demo deletes inline). Still a real confirmed-delete → claim holds.
- **Push compose + real FCM send** — `PushNotificationController::actionSend` (`:83-141`)
  validates all 5 required fields, calls `NotificationHelper::sendTopicNotification`
  (`common/helpers/NotificationHelper.php:721-753` → `SendTopic()` persists a `Notifications`
  row + broadcasts the FCM topic). Demo (`Marketing.tsx:117-119`) is a toast only. Exceeds demo.
- **Audience → FCM topic mapping** — `:103-129` maps customers/agents/all to
  `TOPIC_PUBLIC_CUSTOMER` / `TOPIC_PUBLIC_AGENT`. Index select offers customers/agents/everyone
  (`push-notification/index.php:75-80`). Real targeting vs demo's free-text Input. Exceeds demo.
- **Push history persisted + paginated** — `actionHistory` (`:59-78`) ActiveDataProvider,
  pageSize 20, LinkPager (`push-notification/history.php`). Demo list is static. Exceeds demo.
- **Ad image upload + 340×160 crop + CRUD** — `AdsController::actions` filekit Upload +
  `ImageManagerStatic::make(...)->fit(340,160)` (`:54-72`); full create/update/_form with
  filekit widget + `shop_id` selector. Real CRUD; demo has no ad CRUD. Holds (divergent feature).
- **Permission/role gating + bilingual/RTL** — `beforeAction` manager-permission guard in all
  3 controllers; sampled strings (`Promo codes List`, `Specialists`, `Notification History`,
  `Max Uses`, `Everyone`, `Discount Type`) all present in BOTH `ar/` and `en/backend.php`.

### Overclaims downgraded / tightened
- **Usage progress bar (count/cap)** — analyst marked *partial* ("not rendered in admin").
  Tightened note, status unchanged: the raw numbers `Max Uses / Uses / Remaining Uses` ARE
  rendered, but only on `promo-code/view.php:49-51` as plain figures — NO progress `Bar`
  (demo `Deals.tsx:42-49`) and NOT on the list. Stays **partial**.
- **Push "all" audience → history** — minor divergence (not in analyst matrix): selecting
  audience=`all` calls `sendTopicNotification` twice (`:103-129`), so the history table records
  TWO rows (one per topic) where the demo would show a single "All customers" row. Behavioral,
  not a regression. Noted for completeness; does not change a status.

### Net assessment
No "done" claim was found to be false — Push genuinely exceeds the demo and Ads CRUD is real.
The analyst's matrix is sound. The adjustment is to the blended SCORE, not the rows: the demo's
single largest feature (Ads = campaign cards with impressions/CTR/status/placement,
`Marketing.tsx:13-44,71-96`) is entirely absent on our side (ours is a divergent banner-image
manager), and the unified tabbed hub + platform-deals badge (`Marketing.tsx:52,55-67`) are
missing structural pieces. Weighing Deals (~60% real), Ads (~25% — demo feature absent, substitute
present), Push (~85% — exceeds, only mock analytics missing), minus the structural hub/badge:

**Adjusted area score: 50%** (was 55% — trimmed for the size of the Ads divergence and the
missing Marketing hub/badge; not lowered further because Push genuinely exceeds the demo).
