# Navagoo Plans — shop-side subscription page (port + re-wire)

**Date:** 2026-07-06 · **Branch:** tailwind-poc · **Demo source:** `Navagoo_MI/navagoo-app/src/portals/shop/finance/Subscription.tsx`

## Problem

The demo's **Subscription** page (Finance → Subscription) is the shop's **SaaS subscription to
Navagoo** — it lists Navagoo's plan tiers (monthly / 6-mo / 12-mo pricing, free trial, feature
checklist), shows the shop's current plan + status, and lets the owner activate / cancel and manage
billing cards. These are the demo `SubscriptionPlan` + `Subscription` types.

The portal already has the correct data layer for this:
- `navagoo_subscription_plan` (catalogue of Navagoo tiers) + `NavagooSubscriptionPlan` model
- `shop_subscription` (one row per shop) + `ShopSubscription` model
- backend admin CRUD (`ShopController::actionPlans` / `actionSubscriptions`)

**But the frontend Finance → Subscription tab is wired to the WRONG table:** it reads
`subscription_package` (the shop's *own* prepaid packages sold to *customers* — sessions /
validity_days / per-session price). That is a different concept (the demo's `SubscriptionPackage`,
managed separately at `/package/subscriptions`). So the page shows the wrong content and doesn't
match the demo. This is what the user means by "make the content + design like the demo".

## Fix — re-point the tab to Navagoo plans + rebuild the view to demo parity

Keep it as the Finance → Subscription tab (matches the demo IA). Re-wire data + rebuild the pane.
Shop's own packages keep their existing home at `/package/subscriptions` (not orphaned).

### 1. Migration `m260706_*_navagoo_plans_shop_page`
- Add `shop_subscription.free_period_ends_at` INT NULL — the demo's `freePeriodEndsAt`, for the
  "Trial ends {date} · then {price}/mo" line. (portal-only table; NOT in the shared mobile API.)
- Idempotent seed of `navagoo_subscription_plan` (only if no active rows): Starter / Professional /
  Business, monthly price authored, 6-mo = round(m*6*0.9), 12-mo = round(m*12*0.8), free_period_days=30,
  features JSON (written with `yii\db\JsonExpression`). Gives the shop page real cards out of the box.
- Do NOT touch `subscription_package` or `user_card` schema.

### 2. Models
- `NavagooSubscriptionPlan`: `STATUS_ACTIVE=1/STATUS_ARCHIVED=0`, `PERIOD_*`, `priceForPeriod($p)`,
  `perMonthPrice($p)`, `savePctForPeriod($p)`, `featureList(): string[]` (robust JSON decode —
  Yii returns json columns as arrays), `findActivePlans()`.
- `ShopSubscription`: `PERIOD_MONTHLY|SIX_MONTH|TWELVE_MONTH`, `periodMonths()`, add
  `free_period_ends_at` to rules + docblock, `isActive/isFreePeriod/isCancelled`, `periodLabel()`.

### 3. `EarningsController`
- `subscription` tab branch → `$navagooPlans = NavagooSubscriptionPlan::findActivePlans()`,
  `$subscription = ShopSubscription::findCurrentForShop($shop->id)`, `$currentPlan = $subscription?->plan`,
  `$savedCards = UserCard::find()->where(['user_id'=>$shop->user_id])`. Pass `navagooPlans`,
  `subscription`, `currentPlan`, `savedCards`.
- `actionSubscriptionActivate`: POST `plan_id` + `period` (monthly|six_month|twelve_month). Validate
  plan active. Upsert the shop's `shop_subscription`: plan_id, period, status=active,
  next_billing_at = now + N months, card_last4 = default card. JSON.
- `actionSubscriptionCancel`: set the shop's `shop_subscription.status = cancelled`. JSON.
- `actionAddCard` / `actionSetDefaultCard`: unchanged (already user_card-backed).

### 4. View `frontend/views/earnings/_subscription.php` (full rebuild)
- Current-status card: Crown, plan name / "No active plan", status badge (Free period amber /
  Active teal / Cancelled slate), sub-line (trial ends · then/mo | Renews {date} · card ••1234 |
  "Choose a plan below"), Cancel when active.
- "Choose a plan" + **functional** Monthly / 6 months / 12 months segmented.
- Plan grid (md:2): per-month price for the selected period, "Save X%" badge, "SAR billed …·
  N-day free trial", feature checklist (Check icon), Activate / Current plan. Data attrs
  (`data-price-monthly|six_month|twelve_month`, `data-monthly-base`) so JS recomputes per-month +
  save% + the activate period on toggle (demo behaviour).
- Payment cards list + add-card modal + make-default (kept).

### 5. i18n — every new `Yii::t('frontend', …)` in both `ar` + `en` (real Arabic).

### 6. Build + verify — `npm run build:css`; run migration; `php -l`; curl + preview screenshot the
tab vs the demo; fix diffs.

## Guardrails
- No change to the shared `api/` contract or `subscription_package`/`user_card` schema.
- `navagoo_subscription_plan`/`shop_subscription` already exist (m260624_130100) — only add the one
  column + seed. Admin CRUD stays the source of truth for plans.

---

# WAVE 2 (2026-07-06) — the STANDALONE "Navagoo Plans" page (demo v0.19)

**Discovery:** the local demo checkout was at v0.13; origin/dev moved to **v0.19** where
Subscription left the Finance tabs and became a **top-level shop nav item "Navagoo Plans"**
(`/shop/plans`, Gem icon, sits directly above Settings; old Finance URL redirects). The page
gained: tier-gradient hero banner (status pill incl. PAST DUE/Lapsed + countdown + payment line +
"X / Y specialists used" + enrolled-offer panel), **Offers** ("coupon tickets" — Founding Partner
10% / Eid 5% — selectable, live-repricing, auto-enrol at subscribe, lock-in), 3 gradient plan
cards (Starter 99 · Growth 225 MOST-POPULAR · Pro 349; specialist bands 1–3/4–10/11+;
strikethrough offer pricing; "Everything in X, plus:" feature deltas), tier-aware CTAs
(Activate/Upgrade/Downgrade/Current), confirm modal with full disclosure (trial chip, offer
save, first charge + VAT, upgrade proration, downgrade term-end + band warning) + payment-method
picker (card|bank), payment cards, and a **Billing** card (subscription invoices: outstanding +
history). Demo source: `src/portals/shop/finance/Subscription.tsx` (1142 lines), `lib/offers.ts`,
`lib/entitlements.ts` (planPriceFor/FEATURE_LABELS/TIER_RANK/exceedsBand), `lib/planTheme.ts`.

## Portal implementation (real DB, shared-API safe — no api/ tables touched)

1. **Migration `m260706_150000_navagoo_plans_v2`**
   - `navagoo_subscription_plan` += tier, most_popular, specialist_min/max,
     six/twelve_month_discount_pct, sms_included, wa_included. Seed-update the wave-1 trio →
     demo rate card (Starter 99/535/950 · Growth 225/1215/2160 popular · Pro 349/1885/3350,
     bands, sms 500/wa 300) — only when the rows still match the wave-1 seed (name-matched).
   - `shop_subscription` += payment_method, current_term_start/end, current_term_price,
     past_due_since, pending_plan_id, trial_consumed. (+ STATUS past_due/expired constants.)
   - NEW `navagoo_offer` (name, description, status draft|active|ended, start_at, end_at,
     max_shops, sub_discount_type/value, rate_discount_pct, applies_to_charge_types JSON,
     lock_in_months) + seed Founding Partner (10%, cap 50, lock 12) + Eid Al Adha (5%,
     Jun–Aug 2026, lock 6).
   - NEW `shop_offer_enrollment` (shop_id UNIQUE, offer_id, enrolled_at, locked_until) —
     separate table so the shared `shop` table is untouched.
   - `invoice` += nullable `trigger` varchar ('subscription') — additive; api/ never reads it.
2. **Models:** `NavagooOffer` (eligibility: active+window+cap; netFor gross−subDiscount;
   bestFor), `ShopOfferEnrollment` (findForShop, isLocked), plan += TIER_*, bandLabel();
   sub += past_due/expired consts + helpers.
3. **Controller `NavagooPlansController`** (`/navagoo-plans`): index (full view-model incl.
   per-period×per-offer price maps, specialists used = shop agents count, sub invoices by
   trigger) · subscribe (trial → free_period + trial_consumed; no-trial card → active + PAID
   `charge` row TYPE_SUBSCRIPTION; no-trial bank → `invoice` trigger=subscription + past_due;
   auto-enrol selected/best eligible offer) · upgrade (free_period switches; active prorates
   (newNet − current_term_price)×remaining → card = paid charge + switch / bank = invoice +
   pending_plan_id) · downgrade (pending_plan_id, term-end) · cancel. Cards reuse
   `/earnings/add-card` + `set-default-card`.
   **Hook:** `EarningsController::actionPayInvoice` — paying a trigger=subscription invoice
   activates the sub / applies pending_plan_id (card instant; bank stays pending-verify).
4. **Finance tab removal** (demo parity): earnings loses the Subscription tab;
   `?tab=subscription` 302 → `/navagoo-plans`. Old subscription-activate/cancel actions removed.
5. **Menu:** `_tw_sidebar.php` + legacy `menu/Menu.php` — "Navagoo Plans" (gem) directly above
   Settings.
6. **View** `navagoo-plans/index.php`: full demo structure. Gradients as arbitrary classes
   (aurora = linear-gradient(165deg,#3d2960 0%,#594279 28%,#7554a8 52%,#4aa6b5 78%,#2ebf91 100%);
   starter/pro per planTheme.ts); glass-sheen + @property --nav-angle conic border via
   registerCss. JS: period switch + offer-ticket live repricing from server-rendered maps;
   confirm modal (mode-aware disclosure + simplified card/bank picker); all via ngToast/ngConfirm.
7. **i18n ar+en** for every string. 8. build:css → migrate → lint → **browser interaction-test**
   (owner login) → smoke → docs.

## Explicitly DEFERRED (need product sign-off / their own wave)
- **Global gating/entitlements** (demo locks a no-plan shop to the Plans page + gates features
  per tier): portal-wide enforcement would lock out every existing shop (none have subs) —
  needs backfill + sign-off. Page renders statuses; no enforcement.
- Renewal/dunning engine (past_due/expired transitions over time), scheduled-downgrade
  application at term end, backend admin CRUD for offers + new plan fields (tier/bands/popular
  are seed-driven for now), backend bank-verify → sub-activation hook.
