# Shop · Marketing — Business rules

Numbered, implementable rules. "D" = encoded in demo, "N" = Navagoo-only (absent from demo),
"D+N" = both encode it (note divergence).

## Promo codes / deals

1. **(D) Shop scoping — list.** A shop sees only its own deals/codes.
   Demo: `rows = deals.filter(d => d.shopId === shopId)` (`Deals.tsx:21`).
   Ours: `PromoCodeSearch::search` forces `where(['shop_id' => current shop])`
   (`PromoCodeSearch.php:45`). ✅ server-enforced.

2. **(N) Shop scoping — create.** `shop_id` is set from the session, never from input
   (`PromoCodeController.php:80`). Demo passes `shopId` as a prop, no auth.

3. **(N) Ownership on view/update/delete.** Every single-record action calls
   `checkOwnership($model)` → 404 if `model.shop_id !== user shop`
   (`PromoCodeController.php:60,137,210`; `FrontEndController.php:120-144`). Demo has no
   per-record auth.

4. **(D+N) Discount types = {percentage, fixed}.**
   Demo `'percent' | 'fixed'` (`types.ts:495`).
   Ours `TYPE_PERCENTAGE=1`, `TYPE_FIXED_AMOUNT=0` (`base/PromoCode.php:39-40`).
   Display: percent → "N %", fixed → "N SAR" (both sides).

5. **(N) VAT handling for fixed-amount codes.** The user enters a **VAT-inclusive**
   `actual_discount_value`; the stored `discount_value` is VAT-exclusive:
   `discount_value = actual_discount_value / (1 + taxes/100)`, `taxes` from `Settings(1)`
   (`PromoCodeController.php:88-99`, `165-176`). For percentage, `discount_value =
   actual_discount_value`. **The demo has no VAT concept** — `discountValue` is raw.

6. **(N) Legacy back-fill.** On edit, if `actual_discount_value` is empty/0, recompute it from
   the stored `discount_value` (× (1+VAT) for fixed) so old rows edit correctly
   (`PromoCodeController.php:140-152`). Navagoo-only.

7. **(N) Required fields.** `code`, `discount_value`, `expiry_date` are required server-side
   (`base/PromoCode.php:67`). Demo only soft-disables submit on `desc && value`
   (`Deals.tsx:130`) — no `code`, no required expiry (defaults to 2026-12-31).

8. **(N) Promo code string.** Ours has a unique-ish human `code` (e.g. WELCOME10). The demo
   has **no code** — its "deal" is description-only. (Note: our `rules()` does NOT enforce
   uniqueness of `code` per shop — potential gap to flag.)

9. **(D+N) Usage cap & counter.**
   Demo: `usageCap` (default 100), `usageCount` (starts 0). Bar = count/cap.
   Ours: `max_uses` (nullable ⇒ "Unlimited"), `uses`, `remaining_uses` seeded to `max_uses`
   on create (`PromoCodeController.php:102-103`). Consumption/decrement happens at booking
   redemption (outside this view).

10. **(N) Status lifecycle.** `STATUS_ACTIVE=1` / `STATUS_NOT_ACTIVE=0`
    (`base/PromoCode.php:37-38`). UI also derives an **Expired** display state when
    `status=active AND expiry_date < today` (`index.php:48-66`). Demo has no status — a deal
    is live until expiry only.

11. **(D+N) Expiry.** Both store an expiry date. Demo defaults blank → far-future; ours
    requires it.

12. **(D+N) Delete semantics.** Hard delete on both sides. Ours is POST-only + ownership
    guarded + cascades (`deleteWithRelated`); demo is a plain store filter with a confirm
    dialog.

13. **(D) Platform-wide deals.** Demo: `shopId === undefined` ⇒ platform-wide
    (`Deals.tsx:18`, seed `DEAL-3`). **Not modelled on our shop portal** — every shop code has
    a `shop_id`. Would require a backend-admin surface. Gap (likely out of shop scope).

## Customer invitations (Navagoo-only — NO demo rule set)

14. **(N) Batch size cap = 50** numbers per submission (`CustomerInvitationsController.php:88`).
15. **(N) Number cleaning + dedupe** — strip non-digits, `array_unique`
    (`CustomerInvitationsController.php:94-100`).
16. **(N) Method = WhatsApp | SMS**, template language AR/EN (`:81,80`).
17. **(N) Admin approval gate** — new campaigns start `STATUS_PENDING_APPROVAL`
    (`:113`); recipients start delivery=PENDING, onboarding=PENDING (`:127-128`).
18. **(N) Role gate** — `shopOwner` only (AccessControl, `:23-31`); create is POST-only.
> The demo encodes none of 14–18 (it only shows a simulated toast). These are Navagoo
> business features with no parity target.
