---
feature: Featured subscription management
slug: featured-subscriptions
surface: admin
consumer: react
version: 1.0.3
status: ready
brd_version: "1.3"
brd_ids: [ADMIN-005]
business_rules: [BR-09, BR-14, BR-27, BR-29]
decisions: [DEC-06, DEC-12, DEC-23, TD-12, TD-19, TD-27, TD-98, SCR-34]
schema_tables: [featured_plans, featured_plan_translations, provider_subscriptions, payments, earnings, v_provider_featured]
related_docs: [../provider/featured-subscription.md, providers.md]
last_updated: 2026-11-10
---

# Featured subscription management — React Dashboard Integration Guide

> **Base URL:** `{HOST}/api/v1/admin` · Shared rules (headers, envelope, errors,
> pagination, axios setup, TS types): [API_CONVENTIONS.md](../API_CONVENTIONS.md)

## Version history

Newest first. Never edit an old row.

| Version | Date | Type | Summary | Dashboard action required |
|---------|------|------|---------|---------------------------|
| 1.0.3 | 2026-11-10 | PATCH | Validation messages for the featured-plan form use Arabic field names; no English labels or sentences in Arabic responses (QA-NEW-01). No contract change. | None. |
| 1.0.2 | 2026-11-08 | Docs | Plan update methods clarified: `PUT` and `PATCH /featured-plans/{id}` are the same partial update (TD-179). | None; either method works, no workaround needed. |
| 1.0.1 | 2026-10-25 | Docs | Added the requirement status table (done / not done). | None |
| 1.0.0 | 2026-10-25 | Initial | Plan CRUD (several plans, VAT, priority, badges, visibility) and a read-only monitor of subscribing providers with their payment attempts. | Build the two pages. |

## Changelog

### 1.0.3 — 2026-11-10

- **Fixed:** Validation messages for the featured-plan form use Arabic field names; no English labels or sentences in Arabic responses (QA-NEW-01). No contract change.
- **Breaking:** No. **App action required:** None.

### 1.0.2 — 2026-11-08
- **Clarified:** `PUT /featured-plans/{id}` and `PATCH /featured-plans/{id}` are both registered (Laravel `PUT|PATCH`), run the same request class and are both **partial**: send only the changed fields (for example `{"is_active": false}` or `{"price": "130.00"}`), everything else is kept. Create (`POST`) still requires `billing_period`, `duration_days`, `price` and `ar.name`. An unknown plan uuid is 404; invalid values are 422. Covered for both methods by `test_td_179_patch_and_put_both_update_a_plan_partially…`. Postman: "200 Featured Plan Patched".
- **Breaking:** No. **Dashboard action required:** None.

### 1.0.1 — 2026-10-25
- **Added:** requirement status table (what is done and what is not).
- **Changed / Fixed / Removed / Deprecated:** —
- **Breaking:** No.
- **Endpoints affected:** none.
- **Dashboard action required:** None.
- **BRD / decisions:** ADMIN-005

### 1.0.0 — 2026-10-25
- **Added:** `GET|POST /featured-plans`, `GET|PUT|PATCH|DELETE /featured-plans/{id}`, `GET /featured-subscriptions`, `GET /featured-subscriptions/{id}`; permission section `featured_subscriptions`; error code `FEATURED_PLAN_IN_USE`.
- **Changed / Fixed / Removed / Deprecated:** —
- **Breaking:** No.
- **Endpoints affected:** the endpoints above (new).
- **Dashboard action required:** add the "Featured subscriptions" sidebar group with *Plans* and *Subscribers*; gate by the permissions in §2.
- **BRD / decisions:** ADMIN-005, BR-09/14/27, DEC-12, DEC-23, TD-98.

---

## 1. Business summary

Joshen sells ranking priority to providers as **featured plans**. The admin
configures the plans — name, description, benefit bullets, duration, fee, VAT, ranking
priority, badges, and whether the plan is **visible in the provider app** — and monitors
who subscribed: status (active / expired), start and end dates, the fee charged and every
payment attempt, including failures. Providers subscribe and pay inside their app;
**activation and expiry are automatic** (payment success, then `ends_at`), so this screen
has no activate/expire button. Priority only reorders already-eligible providers and
requires an active subscription **and** an approved account (BR-09, BR-27). A plan's price
can change at any time: **existing subscriptions keep the price they paid**.
A plan that has subscriptions cannot be deleted — disable it instead.

## 2. Pages and permissions

| Page / component | Purpose | Endpoint(s) | RBAC permission (route name) | Section · action |
|------------------|---------|-------------|------------------------------|------------------|
| PlansListPage | Table of plans | `GET /featured-plans` | `admin.featured-plans.index` | featured_subscriptions · view |
| PlanFormPage (edit) | Load a plan with both locales | `GET /featured-plans/{id}` | `admin.featured-plans.show` | featured_subscriptions · view |
| PlanFormPage (create) | New plan | `POST /featured-plans` | `admin.featured-plans.store` | featured_subscriptions · create |
| PlanFormPage (save) / visibility switch | Edit, enable, disable | `PUT\|PATCH /featured-plans/{id}` | `admin.featured-plans.update` | featured_subscriptions · update |
| DeletePlanDialog | Remove an unused plan | `DELETE /featured-plans/{id}` | `admin.featured-plans.destroy` | featured_subscriptions · delete |
| SubscribersListPage | Subscribing providers | `GET /featured-subscriptions` | `admin.featured-subscriptions.index` | featured_subscriptions · view |
| SubscriptionDetailsDrawer | Dates, fee, all payment attempts | `GET /featured-subscriptions/{id}` | `admin.featured-subscriptions.show` | featured_subscriptions · view |

Sidebar entry: *Featured subscriptions → Plans / Subscribers*. Hide it when the admin lacks the index permission.

## 3. Lists

### 3.1 Plans — `GET /api/v1/admin/featured-plans` — list key `featured_plans`

| Column | Field | Sortable (`sort[...]`) | Filter (`filters[...]`) | Notes |
|--------|-------|------------------------|-------------------------|-------|
| ID | `id` (uuid) | no | — | |
| Name | `name` | no | — | Request locale, Arabic fallback |
| Period | `billing_period.label` | no | `filters[billing_period]=monthly\|yearly` | |
| Days | `duration_days` | no | — | |
| Fee / total | `price`, `total_amount` | no | — | `total_amount` = fee + VAT |
| Priority | `priority_level` | no | `filters[priority_level]` | |
| Visible | `is_active` | no | `filters[is_active]=1\|0` | Switch → `PATCH {is_active}` |
| Order | `sort_order` | no | — | Position in the provider app |

`search` matches the plan name. Pagination, `paginate=false` as in API_CONVENTIONS §2.4.

Postman: `admin.featured-plans.index` › "200 Featured Plans List". Example row:

```json
{
  "id": "01a17330-d415-73ab-8e92-d5e327f14902",
  "name": "Sample-hazn99124538",
  "billing_period": { "value": "monthly", "label": "Monthly" },
  "duration_days": 30,
  "price": "120.00",
  "vat_rate": "15.00",
  "total_amount": "138.00",
  "priority_level": 1,
  "grants_verified_badge": true,
  "grants_featured_badge": false,
  "features_packages_on_home": false,
  "is_active": false,
  "sort_order": 0,
  "created_at": "2026-10-25T13:12:22+00:00"
}
```

### 3.2 Subscribers — `GET /api/v1/admin/featured-subscriptions` — list key `featured_subscriptions`

| Column | Field | Sortable | Filter | Notes |
|--------|-------|----------|--------|-------|
| Provider | `provider.display_name`, `provider.account_status` | no | `filters[provider_id]` (uuid), `search` | `search` = provider trading name |
| Plan | `plan.name` | no | `filters[plan_id]` (uuid) | |
| Status | `status` | no | `filters[status]` | Badge by `status.value`: `active`, `expired`, `payment_failed`, `pending_payment` |
| Start / End | `starts_at`, `ends_at` | `sort[starts_at]`, `sort[ends_at]` | `filters[starts_from]`, `filters[starts_to]`, `filters[ends_from]`, `filters[ends_to]` (`YYYY-MM-DD`) | `null` until activated; use the `ends_*` pair to find subscriptions about to expire |
| Fee | `price`, `vat_amount`, `total_amount` | `sort[total_amount]` | — | Snapshot paid |
| Renewal | `auto_renew` | no | `filters[auto_renew]` | |
| Priority now | `is_priority_active` | no | — | `true` only when status `active`, not past `ends_at`, **and** the provider account is `approved` |
| Last payment | `last_payment`, `payments_count` | no | — | |
| Created | `created_at` | `sort[created_at]` (default, newest first) | — | |

Unknown `filters` / `sort` keys answer `422`. Postman: `admin.featured-subscriptions.index` › "200 Featured Subscriptions List", "422 Featured Subscriptions Unsupported Filter". Example row:

```json
{
  "id": "01a17330-d6f4-7240-8ba5-ffd29071321a",
  "provider": { "id": "01a17330-d6ee-7247-9ae6-01f7ace8e7cf", "display_name": "Noor Beauty", "account_status": { "value": "approved", "label": "Approved" } },
  "plan": { "id": "01a17330-d6f5-7208-9b84-fd42e4789c1f", "name": "Gold", "priority_level": 1 },
  "status": { "value": "active", "label": "Active" },
  "price": "100.00",
  "vat_amount": "15.00",
  "total_amount": "115.00",
  "auto_renew": true,
  "starts_at": "2026-10-24T13:12:23+00:00",
  "ends_at": "2026-11-23T13:12:23+00:00",
  "is_priority_active": true,
  "payments_count": 0,
  "last_payment": null,
  "created_at": "2026-10-24T13:12:23+00:00"
}
```

## 4. Details view

### 4.1 Plan — `GET /api/v1/admin/featured-plans/{id}`

Postman: "200 Featured Plan Details". The list fields plus `updated_at` and the **stored locales** (`null` when a locale was never filled — no fallback):

```json
{
  "id": "01a17330-d415-73ab-8e92-d5e327f14902",
  "price": "120.00",
  "is_active": false,
  "updated_at": "2026-10-25T13:12:22+00:00",
  "ar": { "name": "جديدة", "description": null, "features": ["ميزة"] },
  "en": { "name": "Sample-hazn99124538", "description": null, "features": [] }
}
```

(abridged — all list fields are present too.)

### 4.2 Subscription — `GET /api/v1/admin/featured-subscriptions/{id}`

Postman: "200 Featured Subscription Details". All list fields plus `cancelled_at`, `updated_at` and `payments[]` (newest first), one entry per **attempt**, failures included:

| Field | Type | Nullable | Description |
|-------|------|----------|-------------|
| `payments[].id` | uuid | no | |
| `payments[].method` | `{value,label}` | no | `card`, `mada`, `apple_pay`, `earnings_deduction` |
| `payments[].status` | `{value,label}` | no | `paid`, `failed` |
| `payments[].amount` | money string | no | Fee + VAT |
| `payments[].gateway`, `gateway_reference` | string | yes | `null` for earnings deduction; `simulated` locally (TD-94) |
| `payments[].failure_reason` | string | yes | Gateway data for failed attempts |
| `payments[].paid_at`, `created_at` | ISO 8601 | paid_at yes | |

A provider who is suspended keeps `status.value = "active"` with `is_priority_active: false`
(AC-ADMIN-004 / TD-55 read the same rule). Unknown or integer ids answer `404`.

## 5. Forms (create / edit)

`POST /api/v1/admin/featured-plans` · `PUT /api/v1/admin/featured-plans/{id}` (update accepts partial payloads)

| Field | Input type | Required on create | Validation (mirror in zod/yup) | Notes |
|-------|-----------|--------------------|-------------------------------|-------|
| `billing_period` | select | yes | `monthly` \| `yearly` | Label shown to providers |
| `duration_days` | number | yes | integer 1–3650 | Priority days one payment buys |
| `price` | money | yes | decimal ≤ 2 places, > 0 | Fee in SAR **excluding VAT** |
| `vat_rate` | number | no | decimal ≤ 2 places, 0–100 | Default 15.00 |
| `priority_level` | number | no | integer 1–32767 | Higher ranks first among subscribers |
| `grants_verified_badge` | switch | no | boolean | Provider-app presentation (DEC-23) |
| `grants_featured_badge` | switch | no | boolean | Customer-facing label still open (DEC-23) |
| `features_packages_on_home` | switch | no | boolean | Display only; open (DEC-23) |
| `is_active` | switch | no | boolean | **Visibility in the provider app** |
| `sort_order` | number | no | integer | Ascending in the provider app |
| `ar.name` | text | yes | string ≤ 100, **unique per language** | Arabic is required (TD-12) |
| `ar.description` | textarea | no | string ≤ 2000 | |
| `ar.features` | list | no | array ≤ 12 items, each string ≤ 150 | Benefit bullets in order |
| `en.name` | text | when any `en.*` is sent | string ≤ 100, unique per language | English optional |
| `en.description`, `en.features` | | no | as Arabic | |

```json
{
  "billing_period": "monthly", "duration_days": 30, "price": "299.00", "vat_rate": "15.00", "priority_level": 2,
  "grants_verified_badge": true, "grants_featured_badge": true, "features_packages_on_home": true, "sort_order": 2,
  "ar": { "name": "تمييز ذهبي", "description": "الأكثر طلباً", "features": ["شارة «موثّقة» + «مميزة»", "الظهور الأول في نتائج العميلات"] },
  "en": { "name": "Gold", "description": "Most popular", "features": ["Verified + Featured badge", "First in customer results"] }
}
```

`201` (create) / `200` (update) return the details resource. Postman: "201 Featured Plan Created", "201 Featured Plan Arabic Only", "200 Featured Plan Updated", "422 Featured Plan Missing Fields", "422 Featured Plan Invalid Values", "422 Featured Plan Update Invalid". A validation failure is `422` with `data.errors` keyed by field (`ar.name`, `en.name`, …).

## 6. Actions (non-CRUD)

### 6.1 Delete a plan

`DELETE /api/v1/admin/featured-plans/{id}` · Permission: `admin.featured-plans.destroy`

`200` with `data: null` when the plan has no subscription. Postman: "200 Featured Plan Deleted".

| HTTP | `error_code` | When | Dashboard behavior |
|------|--------------|------|--------------------|
| 403 | — | Missing permission | Hide/disable the action |
| 409 | `FEATURED_PLAN_IN_USE` | Any subscription (even expired or failed) references the plan | Toast `message` and offer "Disable instead" (`PATCH {is_active:false}`). Postman: "409 Featured Plan Delete Blocked" |

There is no activate / expire / cancel action on subscriptions (see §13).

## 7. Statuses and enums

| Enum | `value` | Label (en) | Badge color | Notes |
|------|---------|------------|-------------|-------|
| subscription status | `active` | Active | green | Paid, dated priority (if the account is approved) |
| | `expired` | Expired | grey | Past `ends_at`; set by `featured:expire` every minute |
| | `payment_failed` | Payment failed | red | Never activated (BR-14) |
| | `pending_payment` | Awaiting payment | amber | Transient |
| | `cancelled`, `suspended` | Cancelled, Suspended | — | Reserved, not produced yet (DEC-12 open) |
| payment status | `paid`, `failed` | Paid, Failed | green, red | |

## Data model

| Table (`db-schema.sql`) | Read / write | Notes |
|-------------------------|--------------|-------|
| `featured_plans`, `featured_plan_translations` | read + write | Translations: `name`, `description`, `features` (JSON list, `[]` when empty) |
| `provider_subscriptions` | read | Written by the provider's payment and the expiry command only |
| `payments` | read | `purpose = featured_subscription` attempts |
| `earnings` | (written by provider payment) | Negative `subscription_deduction` rows when she pays from earnings |

## 8. TypeScript types

```ts
export interface FeaturedPlan {
  id: string; // uuid (TD-19)
  name: string | null;
  billing_period: EnumValue<'monthly' | 'yearly'>;
  duration_days: number;
  price: Money; vat_rate: string; total_amount: Money;
  priority_level: number;
  grants_verified_badge: boolean; grants_featured_badge: boolean; features_packages_on_home: boolean;
  is_active: boolean; sort_order: number;
  created_at: string; // ISO 8601
}
export interface FeaturedPlanDetails extends FeaturedPlan {
  updated_at: string;
  ar: { name: string; description: string | null; features: string[] } | null;
  en: { name: string; description: string | null; features: string[] } | null;
}
export type FeaturedPlanList = Paginated<'featured_plans', FeaturedPlan>;

export interface FeaturedPayment {
  id: string; method: EnumValue; status: EnumValue<'paid' | 'failed'>; amount: Money;
  gateway: string | null; gateway_reference: string | null; failure_reason: string | null;
  paid_at: string | null; created_at: string;
}
export interface FeaturedSubscription {
  id: string;
  provider: { id: string; display_name: string; account_status: EnumValue };
  plan: { id: string; name: string | null; priority_level: number };
  status: EnumValue<'pending_payment' | 'payment_failed' | 'active' | 'expired' | 'cancelled' | 'suspended'>;
  price: Money; vat_amount: Money; total_amount: Money;
  auto_renew: boolean;
  starts_at: string | null; ends_at: string | null;
  is_priority_active: boolean;
  payments_count: number; last_payment: FeaturedPayment | null;
  created_at: string;
}
export interface FeaturedSubscriptionDetails extends FeaturedSubscription {
  cancelled_at: string | null; updated_at: string; payments: FeaturedPayment[];
}
export type FeaturedSubscriptionList = Paginated<'featured_subscriptions', FeaturedSubscription>;
```

## 9. Business rules enforced by the backend

| Rule | What the backend does | What the admin sees |
|------|-----------------------|---------------------|
| ADMIN-005 | Only `is_active` plans are offered or purchasable; price edits affect new purchases only | Switching `is_active` off hides the plan; running subscriptions continue |
| BR-14 | A failed payment leaves the subscription inactive and records the attempt | `payment_failed` row with the failed payment and `failure_reason` |
| BR-27 | Priority runs only while `active`, before `ends_at`, and the provider is `approved` | `is_priority_active` |
| BR-09 | Ranking: eligibility first, then featured, then higher `priority_level` | — (search is not changed by this screen) |
| BR-29 | Supervisors need the `featured_subscriptions` section | `403` otherwise |
| — | One active subscription per provider | Never two `active` rows for a provider |

## 10. Notifications triggered

| Event | Who is notified | Channel |
|-------|-----------------|---------|
| Payment succeeds → plan active | Provider | in-app inbox (`featured_subscription_activated`) |
| Payment fails | Provider | in-app inbox (`featured_payment_failed`) |
| `ends_at` passes | Provider | in-app inbox (`featured_subscription_expired`) |

## 11. Integration checklist

- [ ] Types match the real responses above
- [ ] Menu items/buttons gated by the listed permissions
- [ ] Table filters, sorting, search, and pagination wired to the documented params
- [ ] Form validation mirrors §5; `ar.name` / `en.name` uniqueness errors shown on the field
- [ ] `409 FEATURED_PLAN_IN_USE` handled with a "disable instead" path
- [ ] RTL (`ar`) and `en` verified

## 12. Acceptance scenarios

| AC ID | Scenario | Expected |
|-------|----------|----------|
| AC-ADMIN-005 | Configure plans | Create / edit / disable plans; Arabic required; names unique per language; delete blocked when used |
| AC-ADMIN-005 | Process successful and failed payments and expiry | Subscribers list shows `active` with dates, `payment_failed` with its attempt, `expired` after `ends_at` |
| AC-ADMIN-005 | Priority consistent with eligibility | A suspended provider with a paid plan shows `active` but `is_priority_active: false` |

Tests: `tests/Feature/Api/V1/Admin/FeaturedSubscriptionManagementTest.php`.

## 13. Open decisions and known limitations

| ID | Impact on this feature | Current behavior |
|----|------------------------|------------------|
| DEC-06 | Payment vendor/callbacks open | Payments use the local/test simulator (TD-94). Launch blocker. |
| DEC-12 | Refunds on cancellation, suspension effects, renewal overlap | No admin cancel/suspend/refund action. `auto_renew` is stored but no automatic charge runs yet (TD-98). |
| DEC-23 | Customer-facing "Featured" label, home highlighting, monthly performance report | Badge flags are stored and shown to the provider only. |
| DEC-20 | Dashboard KPIs (total active / expired subscriptions, revenue) | Not part of this slice; the dashboard feature owns them. |
| ADMIN-015 | Cross-order/subscription payments monitor | The attempts of one subscription are visible here; the global payments list belongs to ADMIN-015. |

## 14. Requirement status (verified 2026-10-25)

`true` = built and covered by a passing test or verified in code. `false` = not built, not tested, or only partly delivered; the note says what is missing.

| Source | Requirement | Done | Note |
|--------|-------------|------|------|
| ADMIN-005 | Add and edit plan name, description, benefits, duration, fee | true | `FeaturedSubscriptionManagementTest` |
| ADMIN-005 | Enable or disable visibility in the provider app | true | `is_active` |
| ADMIN-005 | Define the plan's priority in search, Joshan results and provider lists as separate benefits | false | Benefits are free-text bullets; `priority_level` is one ranking weight |
| ADMIN-005 | Several plans, VAT, badges (SCR-34) | true | |
| ADMIN-005 | Monitor subscribing providers | true | List with filters, search and sort |
| ADMIN-005 | See status (active or expired), start and end dates | true | Also `payment_failed` and `pending_payment` |
| ADMIN-005 | Monitor subscription payments | true | Every attempt of one subscription, failures included |
| ADMIN-005 | One list of all featured payments across subscriptions | false | Belongs to ADMIN-015 |
| ADMIN-005 | Successful payment activates automatically with dates | true | Provider-side flow, shown here |
| ADMIN-005 | Failed payment leaves it inactive and records the attempt | true | |
| ADMIN-005 | Expiry stops priority and sets Expired | true | |
| ADMIN-005 | Priority needs an active subscription and an enabled provider account | true | `is_priority_active` |
| ADMIN-005 | No effect on orders, acceptance, drivers, prices, commissions or ratings | true | |
| BRD MOD-10 | Admin notices: successful payment, failed payment, activation | false | Only the provider is notified |
| BRD dashboard | Totals of active and expired subscriptions and subscription revenue | false | DEC-20 open; not part of this slice |
| BRD ADMIN-024 | Supervisors limited to their sections | true | Section `featured_subscriptions` |
| DEC-12 | Cancel, refund or suspend a subscription from the dashboard | false | Open decision; none built |
| Behavior | Plan with subscriptions cannot be deleted, only disabled | true | `409 FEATURED_PLAN_IN_USE` |
| Tests | Feature tests, Postman examples for every route, `postman:generate --check` | true | 8 admin tests |
