# Admin · Marketing — Business Rules

Rules the demo encodes (numbered, implementable). Status against our Yii2 app noted inline.

## Deals (demo) ↔ Promo Codes (ours)

1. **Scope.** A deal with no `shopId` is **platform-wide**; with a `shopId` it is shop-scoped.
   The admin Marketing screen manages **only platform-wide** deals (`!shopId`).
   Demo: `Marketing.tsx:52,68`; `Deals.tsx:17,21`.
   OURS: `promo_code.shop_id` is nullable so the data model supports scope, BUT the admin list
   shows ALL promo codes (no platform-only filter) and the create form omits `shop_id`.
   → **Partial.**

2. **Discount type is one of {percent, fixed}.** `percent → "{value}%"`, `fixed → "SAR {value}"`.
   Demo: `types.ts:495`; `Deals.tsx:31-37`.
   OURS: `PromoCode::TYPE_PERCENTAGE=1`, `TYPE_FIXED_AMOUNT=0`, same `%`/SAR rendering. → **Done.**

3. **Fixed amounts are in SAR and VAT-inclusive.** Demo renders `SAR {value}`.
   OURS: form help text states "amount in SAR (VAT-inclusive)" (`_form.php`). → **Done.**

4. **Usage cap.** Each deal has a `usageCap` (default **100**) and tracks `usageCount`
   (starts at 0). Demo: `Deals.tsx:130,137`; `store.ts:1849`.
   OURS: `max_uses` + `uses` + `remaining_uses` columns exist; default-100 is **not** enforced
   in the form (field not exposed). → **Partial.**

5. **Usage progress is `usageCount / usageCap` (×100%).** Demo: `Deals.tsx:48`.
   OURS: computable from `uses/max_uses` but **not rendered** in admin. → **Partial (data yes, UI no).**

6. **Quota does not auto-disable a deal.** Demo never blocks creation/usage when cap reached
   (display only). OURS: `status` + `remaining_uses` exist; redemption enforcement lives in
   redemption code (frontend/api), not admin. Admin parity = display only. → **Done (admin scope).**

7. **Expiry is required-ish; defaults to `2026-12-31` if blank.** Demo: `Deals.tsx:138`.
   OURS: `expiry_date` is **`required`** (server validation) — stricter, no silent default.
   → **Done (stronger).**

8. **Create validation: description + value required** (button disabled otherwise).
   Demo: `Deals.tsx:124`.
   OURS: required = `code, discount_value, expiry_date`. Keys on `code` not free-text
   description. → **Partial (different required set, no `description` concept).**

9. **Delete is destructive + confirmed.** Demo: confirm dialog, then remove from store.
   OURS: POST-only delete (`VerbFilter`), `deleteWithRelated()` (cascades `UserPromoCode`).
   → **Done.**

## Ads (demo)

10. **Ad campaign attributes: name, placement, impressions, CTR, status (live|scheduled).**
    Demo static: `Marketing.tsx:13-44`.
    OURS: `Ads` is an **image banner** (`image_path, shop_id`) — none of these attributes exist.
    → **Missing (divergent feature).**

11. **Placement enum {App home banner, Discover carousel, Category top}.** Demo: `Marketing.tsx`.
    OURS: no placement concept. → **Missing.**

12. **Ad image is resized to 340×160 on upload.** (Ours-only rule, not in demo.)
    OURS: `AdsController.php:64`. → **New in ours.**

## Push (demo)

13. **Push has a Title and an Audience.** Demo: `Marketing.tsx:104-112`.
    OURS: requires `target_audience ∈ {customers, agents, all}`, bilingual title+message, route.
    → **Done (richer).**

14. **Audience targeting maps to delivery channel.** Demo: free-text "All customers" (cosmetic).
    OURS: `customers→TOPIC_PUBLIC_CUSTOMER`, `agents→TOPIC_PUBLIC_AGENT`, `all→both` FCM topics.
    `PushNotificationController.php:103-129`. → **Done (real).**

15. **All push fields required before send.** Demo: not enforced (always toasts success).
    OURS: `actionSend` validates all required, flashes error otherwise (lines 94-97).
    → **Done (stronger).**

16. **Open / delivery rates tracked per send.** Demo: static `opened`/`sent` mock numbers.
    OURS: history persisted but **no open/delivery-rate analytics**. → **Missing (demo mock-only).**

## Permissions / scoping (cross-cutting)
17. **Manager role is permission-gated per controller_action.** All three controllers run the
    same `beforeAction` guard (`checkPermissions($controller.'_'.$action)`); guests → login.
    Refs: `AdsController.php:22-40`, `PromoCodeController.php:16-34`,
    `PushNotificationController.php:15-33`. Demo has no real RBAC (single admin persona).
    → **Done (ours stronger).**
