---
feature: Featured subscription
slug: featured-subscription
surface: provider
consumer: flutter
version: 1.2.1
status: ready
brd_version: "1.3"
brd_ids: [PROV-007]
business_rules: [BR-03, BR-09, BR-14, BR-27]
decisions: [DEC-06, DEC-12, DEC-23, TD-19, TD-94, TD-98, SCR-34, TD-130, TD-165]
schema_tables: [featured_plans, featured_plan_translations, provider_subscriptions, payments, invoices, earnings, saved_cards, v_provider_featured]
screens: ["provider--featured--*"]
related_docs: [../admin/featured-subscriptions.md, auth.md, ../client/catalog.md]
last_updated: 2026-11-08
---

# Featured subscription — Flutter Integration Guide (provider)

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

## Version history

Newest first. Never edit an old row.

| Version | Date | Type | Summary | App action required |
|---------|------|------|---------|---------------------|
| 1.2.1 | 2026-11-08 | PATCH | Requirement table corrected: push goes through the shared FCM sender, `GET /provider/home` returns `featured`, the 3-day expiry reminder exists. | Show the featured status and subscribe shortcut on the home from `GET /provider/home`; expect push for the featured events. |
| 1.2.0 | 2026-11-04 | MINOR | `GET /featured/payment-methods` omits methods an admin switched off; `POST /featured/subscription` answers 422 `PAYMENT_METHOD_DISABLED` for one (TD-168). | Handle the new error; the list already reflects the switches. |
| 1.1.1 | 2026-11-04 | PATCH | Staging QA path for the payment simulator and a captured `PAYMENT_GATEWAY_UNAVAILABLE` example. | None |
| 1.1.0 | 2026-11-03 | MINOR | Subscription invoices list and details (TD-165). | Integrate invoice screens for featured subscriptions. |
| 1.0.2 | 2026-10-27 | PATCH | Document non-production simulation. | None |
| 1.0.1 | 2026-10-25 | Docs | Added the requirement status table (done / not done). | None |
| 1.0.0 | 2026-10-25 | Initial | Plans with VAT, payment methods, subscribe and pay, retry after failure, current subscription, auto-renew switch. | Integrate the five endpoints. |

## Changelog

### 1.2.1 — 2026-11-08

- **Changed:** Requirement table corrected: push goes through the shared FCM sender, `GET /provider/home` returns `featured`, the 3-day expiry reminder exists.
- **Breaking:** No. **App action required:** Show the featured status and subscribe shortcut on the home from `GET /provider/home`; expect push for the featured events.

### 1.1.1 — 2026-11-04
- **Documented:** section 3.3.1, the staging QA path and what is not built (DEC-06). New Postman example for the gateway-unavailable error. No contract change.
- **Breaking:** No. **App action required:** None.

### 1.1.0 — 2026-11-03

- **Added:** `GET /featured/invoices` (`provider.featured-invoices.index`) and `GET /featured/invoices/{invoice}` (`provider.featured-invoices.show`) for subscription electronic tax invoices.
- **Breaking:** No. **App action required:** Integrate subscription invoice list and detail view.
- **BRD / decisions:** PROV-007, TD-165.

### 1.0.2 — 2026-10-27

- **Fixed:** Document non-production simulation.
- **Breaking:** No. **App 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.
- **App action required:** None.
- **BRD / decisions:** PROV-007

### 1.0.0 — 2026-10-25
- **Added:** `GET /featured/plans`, `GET /featured/payment-methods`, `GET /featured/subscription`, `POST /featured/subscription`, `PATCH /featured/subscription/auto-renew`; in-app notices `featured_subscription_activated`, `featured_payment_failed`, `featured_subscription_expired`; error codes `FEATURED_PLAN_UNAVAILABLE`, `FEATURED_SUBSCRIPTION_ACTIVE`.
- **Changed / Fixed / Removed / Deprecated:** —
- **Breaking:** No.
- **Endpoints affected:** the five above (new).
- **App action required:** build the plans screen, the payment screen with the VAT summary, and the status card; drive the buttons from `can_subscribe`, `can_retry` and `is_active`.
- **BRD / decisions:** PROV-007, BR-09/14/27, DEC-12, DEC-23, TD-94, TD-98.

---

## 1. Business summary

An **approved** provider can subscribe to one of the **featured plans** Joshen
publishes (silver, gold, gold yearly… — the admin decides how many). Paying the fee
inside the app **activates the subscription immediately**, stamps `starts_at`, and sets
`ends_at = starts_at + duration_days`. While it is active **and her account is approved**,
she ranks **ahead of non-subscribers** in customer search, the Joshen finder and provider
lists (BR-09, BR-27); a higher plan `priority_level` ranks first among subscribers.
Priority only reorders providers who are **already eligible** (coverage, availability,
service offered): it never makes her appear elsewhere, never changes orders, prices,
acceptance, commissions or ratings. A **failed payment activates nothing** (BR-14); she
retries with the same endpoint. When `ends_at` passes, priority stops on its own, the
status becomes `expired`, and she can subscribe again. She cannot buy a second plan while
one is running (DEC-12). Everything on these screens is shown **with 15 % VAT** (`vat_rate`
comes from the plan) and paid by **saved card, bank card, Apple Pay or "deduct from
earnings"** (SCR-34).

## 2. User flow and screens

| Step | Screen / widget | Endpoint | Notes |
|------|-----------------|----------|-------|
| 1 | Featured plans (`provider--featured--01-plans-p1/p2`) | `GET /featured/plans` | One card per plan: name, `billing_period.label`, `price`, `features[]`. Header banner "the Verified badge comes with the plan" is a static app string (DEC-23). Disable "Subscribe now" when `can_subscribe` is `false`; mark the card with `is_current`. |
| 2 | Payment (`provider--featured--02-payment-methods`) | `GET /featured/payment-methods` | Saved cards first (`brand` + `last4`), then bank card, Apple Pay and deduct-from-earnings. |
| 3 | Payment summary (`provider--featured--03-payment-summary-vat`) | — (values from step 1) | Plan, period, renewal (`auto_renew` switch), fee `price`, `vat_amount` (VAT), total `total_amount`. Show money as given; do not recompute. |
| 4 | Pay button "ادفعي وفعّلي الباقة" | `POST /featured/subscription` | `201` + `status.value = active` → success. `200` + `payment_failed` → failure message with a retry button. |
| 5 | Plan status card (home / profile) | `GET /featured/subscription` | `null` when she never subscribed. Shows status, `ends_at`, `days_remaining`, renewal switch. |
| 6 | Renewal switch | `PATCH /featured/subscription/auto-renew` | Stopping renewal never ends the current period. |

```
Plans ──► Payment method ──► Summary (VAT) ──► Pay ──┬─► 201 active  ──► Status card
                                                     └─► 200 payment_failed ──► Retry (same call)
```

The login/`GET /auth/me` resource already carries `profile.is_subscribed` and
`profile.subscription_plan` (DEC-23, [auth.md](auth.md)); they turn `true` right after step 4.

## 3. Endpoints

All five require a **Bearer token with the `provider` app claim** and an **approved
account** (BR-03); a non-approved provider gets `403`, a missing token `401`.

### 3.1 List plans

`GET /api/v1/provider/featured/plans`

Returns only plans Joshen has enabled, ordered by `sort_order`. No parameters. `data` is a list.

**Success — `200`** — Postman: `provider.featured-plans.index` › "200 Enabled plans with VAT"

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": [
    {
      "id": "01a17330-cd1d-709a-9e56-b52abd3b69b0",
      "name": "Plan 0326",
      "description": null,
      "features": ["Verified badge"],
      "billing_period": { "value": "monthly", "label": "Monthly" },
      "duration_days": 30,
      "price": "299.00",
      "vat_rate": "15.00",
      "vat_amount": "44.85",
      "total_amount": "343.85",
      "priority_level": 2,
      "grants_verified_badge": true,
      "grants_featured_badge": true,
      "features_packages_on_home": false,
      "is_current": false,
      "can_subscribe": true
    }
  ]
}
```

| Field | Type | Nullable | Description |
|-------|------|----------|-------------|
| `id` | uuid | no | Send as `plan_id` when subscribing |
| `name`, `description` | string | description yes | Localized by `Accept-Language`, Arabic fallback |
| `features` | string[] | no | Bullet list for the card; `[]` when none |
| `billing_period` | `{value,label}` | no | `monthly` / `yearly` |
| `duration_days` | int | no | Days of priority a payment buys |
| `price`, `vat_amount`, `total_amount` | money string | no | Fee, VAT (rounded half-up to the halala) and amount charged |
| `vat_rate` | decimal string | no | Percent, e.g. `"15.00"` |
| `priority_level` | int | no | Ranking weight among subscribers |
| `grants_verified_badge`, `grants_featured_badge`, `features_packages_on_home` | bool | no | What the plan card may advertise (DEC-23) |
| `is_current` | bool | no | This is her running plan |
| `can_subscribe` | bool | no | `false` while any plan is running |

### 3.2 List payment methods

`GET /api/v1/provider/featured/payment-methods`

Postman: `provider.featured-payment-methods.index` › "200 Saved cards and generic methods"

`data` is a list: her **saved cards** first (default first), then `card`, `apple_pay`, `earnings_deduction`.

| Field | Type | Nullable | Description |
|-------|------|----------|-------------|
| `type` | string | no | `saved_card`, `card`, `apple_pay`, `earnings_deduction` |
| `method` | `{value,label}` | no | The value to send as `method` (a saved mada card is `mada`, any other brand `card`) |
| `saved_card_id` | uuid | yes | Send as `saved_card_id` for a saved card |
| `brand` | `{value,label}` | yes | `mada`, `visa`, `mastercard`, `amex` (saved cards only) |
| `last4` | string | yes | Display as `•••• 4431` |
| `is_default` | bool | no | Preselect |

Wallet and Tabby are customer methods and are not offered here.

### 3.3 Subscribe and pay

`POST /api/v1/provider/featured/subscription`

| | |
|---|---|
| Auth | Bearer · approved provider |
| Rate limit | none |

**Request body**

| Field | Type | Required | Validation (mirror on the client) | Description |
|-------|------|----------|-----------------------------------|-------------|
| `plan_id` | uuid | yes | exists in `featured_plans`; plan must be enabled | Plan from step 1 |
| `method` | string | yes | `card`, `mada`, `apple_pay`, `earnings_deduction` | How she pays |
| `saved_card_id` | uuid | no | exists; only with `card`/`mada`; must be her own | Saved card from step 2 |
| `auto_renew` | bool | no | boolean, default `true` | Renewal switch of the summary screen |
| `simulate_failure` | bool | no | boolean | **Non-production** — fails the attempt for QA (TD-94) |

```json
{ "plan_id": "01a17330-cd8c-7333-98d4-fd763fad47ea", "method": "mada", "saved_card_id": "01a17330-cd64-7177-90c9-293e7ab0c5f4", "auto_renew": true }
```

**Success — `201`** (activated) — Postman: `provider.featured-subscription.store` › "201 Paid with a saved mada card"

```json
{
  "status": "success",
  "message": "Your featured plan is active.",
  "data": {
    "id": "01a17330-cda3-7318-97e7-fc53c106957f",
    "status": { "value": "active", "label": "Active" },
    "plan": { "id": "01a17330-cd8c-7333-98d4-fd763fad47ea", "name": "Plan 5375", "billing_period": { "value": "monthly", "label": "Monthly" }, "priority_level": 2 },
    "price": "299.00",
    "vat_amount": "44.85",
    "total_amount": "343.85",
    "auto_renew": true,
    "starts_at": "2026-10-25T13:12:20+00:00",
    "ends_at": "2026-11-24T13:12:20+00:00",
    "days_remaining": 30,
    "is_active": true,
    "can_retry": false,
    "can_subscribe": false,
    "last_payment": {
      "id": "01a17330-cda5-727b-a988-344029b22ab2",
      "method": { "value": "mada", "label": "mada" },
      "status": { "value": "paid", "label": "Paid" },
      "amount": "343.85",
      "failure_reason": null,
      "paid_at": "2026-10-25T13:12:20+00:00"
    },
    "created_at": "2026-10-25T13:12:20+00:00"
  }
}
```

**Payment failed — `200`** — Postman: `provider.featured-subscription.store` › "200 Payment failed, nothing activated"

Same shape with `data.status.value = "payment_failed"`, `starts_at`/`ends_at` `null`,
`is_active: false`, `can_retry: true`, `last_payment.status.value = "failed"`, and
`message` = "The payment failed, so the plan was not activated. You can try again."
Show `message` and a retry button that calls this endpoint again; the same subscription row
is reused (its `id` does not change) and the failed attempt is kept for the admin.
The order payment (`client/booking`) answers failures the same way (200 + status in `data`).

**Response fields** (this shape is shared by 3.4 and 3.5)

| Field | Type | Nullable | Description |
|-------|------|----------|-------------|
| `id` | uuid | no | Subscription |
| `status` | `{value,label}` | no | See §4 |
| `plan` | object | no | `id`, `name`, `billing_period`, `priority_level` |
| `price`, `vat_amount`, `total_amount` | money string | no | Snapshot taken at purchase; later plan edits never change them |
| `auto_renew` | bool | no | Renewal switch |
| `starts_at`, `ends_at` | ISO 8601 | yes | `null` until activated |
| `days_remaining` | int | yes | Whole days left while running, else `null` |
| `is_active` | bool | no | Dated priority runs now (status `active` and `ends_at` in the future) |
| `can_retry` | bool | no | Show "try again" (`payment_failed`) |
| `can_subscribe` | bool | no | Show the plans CTA (nothing running) |
| `last_payment` | object | yes | Latest attempt: `id`, `method`, `status`, `amount`, `failure_reason`, `paid_at` |
| `created_at` | ISO 8601 | no | |

**Errors**

| HTTP | `error_code` | When | What the app should do |
|------|--------------|------|------------------------|
| 401 / 403 | — | No token / not approved / token of another app | Standard auth handling |
| 404 | `ENTITY_NOT_FOUND` | `saved_card_id` is not hers (or deleted) | Reload payment methods. Postman: "404 Saved card belongs to someone else" |
| 409 | `FEATURED_SUBSCRIPTION_ACTIVE` | A subscription is already running | Refresh the status card. Postman: "409 Already subscribed" |
| 422 | — | Validation (`plan_id`, `method`, `saved_card_id`) | Show `data.errors`. Postman: "422 Plan and method missing" |
| 422 | `FEATURED_PLAN_UNAVAILABLE` | Joshen disabled the plan meanwhile | Reload plans. Postman: "422 Plan disabled by Joshen" |

### 3.3.1 Staging QA path (payment simulator)

Same simulator as the customer app ([client/payment §4.1](../client/payment.md)); the real gateway, callbacks, saved-card charging and `auto_renew` charging are **not built** and wait on DEC-06. What is built and real: payment by **deduction from earnings** (`earnings_deduction`, TD-98), netted at payout time (TD-158).

| Topic | Behaviour |
|---|---|
| Enabling (DevOps) | `PAYMENT_SIMULATOR_ENABLED=true`, a non-production `APP_ENV`, `config:clear`, restart workers. |
| Methods | `card`, `mada` (optionally with `saved_card_id`) and `apple_pay` run on the simulator; `earnings_deduction` needs no gateway and works with the simulator off. Wallet and Tabby do not apply to providers (DEC-12). |
| Declined | `"simulate_failure": true` (non-production only) answers 200 with `status.value = payment_failed` and `can_retry = true`; nothing is activated and a `featured_payment_failed` notification is written. The retry reuses the same subscription. |
| Expiry | There is no payment window. A subscription expires at `ends_at` through `featured:expire` (every minute) and lazily on read. |
| Repeated call | A second subscribe while one is active is 409 (Postman: "Already subscribed"); after a failed attempt a new call retries the same subscription. |
| Simulator off or production | Gateway methods answer 422 `PAYMENT_GATEWAY_UNAVAILABLE` (Postman: `provider.featured-subscription.store` › "422 Gateway method while the simulator is off (production behaviour)"). |
| Cleanup | Simulated payments move no money. Use test providers; an active test subscription ends at `ends_at` or is ended from the admin desk. An earnings-deduction subscription writes a negative earning that is netted at the next payout; use a test provider so no real payout is affected. |

### 3.4 Current subscription

`GET /api/v1/provider/featured/subscription`

Postman: `provider.featured-subscription.show` › "200 Current subscription", "200 Expired subscription"

Returns her **running** subscription, else her **latest attempt** (expired or `payment_failed`),
else `data: null`. Same fields as 3.3. An expired subscription reads `status.value = "expired"`,
`is_active: false`, `days_remaining: null`, `can_subscribe: true`.

### 3.5 Switch automatic renewal

`PATCH /api/v1/provider/featured/subscription/auto-renew`

| Field | Type | Required | Validation | Description |
|-------|------|----------|-----------|-------------|
| `auto_renew` | bool | yes | boolean | New setting |

**Success — `200`** — the subscription (shape of 3.3). Postman: "200 Automatic renewal stopped".

| HTTP | `error_code` | When |
|------|--------------|------|
| 404 | `ENTITY_NOT_FOUND` | She has no running subscription ("404 No running subscription") |
| 422 | — | `auto_renew` missing ("422 Renewal flag missing") |

### 3.6 List subscription invoices

`GET /api/v1/provider/featured/invoices` · route `provider.featured-invoices.index`

| | |
|---|---|
| Auth | Bearer token (`provider` app) · approved provider required |
| Extra headers | None |
| Rate limit | Standard API rate limit |

**Query parameters**

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `page` | query | integer | no | Target page (1-based, default 1) |
| `per_page` | query | integer | no | Page size (1 to 100, default 15) |

**Success — `200` (Postman: `provider.featured-invoices.index` › "200 Provider subscription invoices list")**

```json
{
    "status": "success",
    "message": "Data retrieved successfully",
    "data": {
        "invoices": [
            {
                "id": "01a1a5a6-d872-7289-8cb2-6e1eb4a017b7",
                "invoice_no": "INV-100012",
                "order_id": null,
                "subscription_id": "01a1a5a6-d86c-7258-b3f8-0905088b894b",
                "subtotal": "100.00",
                "delivery_fee": "0.00",
                "discount": "0.00",
                "vat_amount": "15.00",
                "total": "115.00",
                "seller": null,
                "payment_method": {
                    "value": "card",
                    "label": "Bank card"
                },
                "order_code": null,
                "items": null,
                "plan_name": "Plan 8210",
                "zatca_uuid": null,
                "qr_payload": null,
                "issued_at": "2026-11-04T08:22:17+00:00"
            }
        ],
        "links": {
            "first": "http://localhost/api/v1/provider/featured/invoices?page=1",
            "last": "http://localhost/api/v1/provider/featured/invoices?page=1",
            "prev": null,
            "next": null
        },
        "meta": {
            "current_page": 1,
            "from": 1,
            "last_page": 1,
            "links": [
                {
                    "url": null,
                    "label": "&laquo; Previous",
                    "page": null,
                    "active": false
                },
                {
                    "url": "http://localhost/api/v1/provider/featured/invoices?page=1",
                    "label": "1",
                    "page": 1,
                    "active": true
                },
                {
                    "url": null,
                    "label": "Next &raquo;",
                    "page": null,
                    "active": false
                }
            ],
            "path": "http://localhost/api/v1/provider/featured/invoices",
            "per_page": 10,
            "to": 1,
            "total": 1
        }
    }
}
```

### 3.7 Subscription invoice details

`GET /api/v1/provider/featured/invoices/{invoice}` · route `provider.featured-invoices.show`

| | |
|---|---|
| Auth | Bearer token (`provider` app) · approved provider required |
| Extra headers | None |
| Rate limit | Standard API rate limit |

**Path parameters**

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `invoice` | path | uuid (string) | yes | Subscription invoice UUID |

**Success — `200` (Postman: `provider.featured-invoices.show` › "200 Provider subscription invoice details")**

```json
{
    "status": "success",
    "message": "Data retrieved successfully",
    "data": {
        "id": "01a1a5a6-d8bf-7139-9b38-22d9dafb6d68",
        "invoice_no": "INV-100013",
        "order_id": null,
        "subscription_id": "01a1a5a6-d8b9-7162-b00d-dc81f77effa0",
        "subtotal": "100.00",
        "delivery_fee": "0.00",
        "discount": "0.00",
        "vat_amount": "15.00",
        "total": "115.00",
        "seller": {
            "name": "Joshen / جوشن",
            "vat_number": "300000000000003"
        },
        "payment_method": {
            "value": "card",
            "label": "Bank card"
        },
        "order_code": null,
        "items": null,
        "plan_name": "Plan 8994",
        "zatca_uuid": null,
        "qr_payload": "ARFKb3NoZW4gLyDYrNmI2LTZhgIPMzAwMDAwMDAwMDAwMDAzAxkyMDI2LTExLTA0VDA4OjIyOjE3KzAwOjAwBAYxMTUuMDAFBTE1LjAw",
        "issued_at": "2026-11-04T08:22:17+00:00"
    }
}
```

**Errors**

| HTTP | `error_code` | When |
|------|--------------|------|
| 401 | `UNAUTHENTICATED` | Missing or expired token |
| 403 | `PERMISSION_DENIED` | Non-provider token |
| 404 | `INVOICE_NOT_FOUND` | Invoice does not exist or belongs to another provider |

## 4. Statuses and enums

| Enum | `value` | Meaning | App behavior |
|------|---------|---------|--------------|
| subscription status | `pending_payment` | Row exists, payment not finished (transient) | Treat like "not subscribed" |
| | `payment_failed` | Last attempt failed; **no priority** | `can_retry` → retry button |
| | `active` | Paid; priority runs until `ends_at` | `is_active`, show days left |
| | `expired` | `ends_at` passed; priority gone | `can_subscribe` → offer plans again |
| | `cancelled`, `suspended` | Reserved; not produced yet (DEC-12 open) | Show the label, no actions |
| payment status | `paid`, `failed` | Result of the last attempt | |
| billing period | `monthly`, `yearly` | Label on the card | |

## Data model

| Table (`db-schema.sql`) | Read / write | Notes |
|-------------------------|--------------|-------|
| `featured_plans`, `featured_plan_translations` | read | Only `is_active` plans; names/features by locale |
| `provider_subscriptions` | read + write | One row per purchase; price, VAT and total are snapshots; one `active` row per provider (`ux_one_active_subscription`) |
| `payments` | write | One row per attempt, `purpose = featured_subscription`; failures kept |
| `earnings` | write | Only for `earnings_deduction`: `source_type = subscription_deduction`, negative `gross`/`net` |
| `saved_cards` | read | Her own cards |
| `v_provider_featured` | (read by search) | Ranking input: approved + `active` + `ends_at > now` |

## 5. Dart models

```dart
class FeaturedSubscription {
  final String id; // uuid (TD-19)
  final EnumValue status;
  final FeaturedPlanMini plan;
  final String price, vatAmount, totalAmount; // money strings
  final bool autoRenew, isActive, canRetry, canSubscribe;
  final DateTime? startsAt, endsAt;
  final int? daysRemaining;
  final FeaturedPayment? lastPayment;
  FeaturedSubscription.fromJson(Map<String, dynamic> j)
      : id = j['id'] as String,
        status = EnumValue.fromJson(j['status']),
        plan = FeaturedPlanMini.fromJson(j['plan']),
        price = j['price'] as String,
        vatAmount = j['vat_amount'] as String,
        totalAmount = j['total_amount'] as String,
        autoRenew = j['auto_renew'] as bool,
        isActive = j['is_active'] as bool,
        canRetry = j['can_retry'] as bool,
        canSubscribe = j['can_subscribe'] as bool,
        startsAt = parseDate(j['starts_at']),
        endsAt = parseDate(j['ends_at']),
        daysRemaining = j['days_remaining'] as int?,
        lastPayment = j['last_payment'] == null ? null : FeaturedPayment.fromJson(j['last_payment']);
}
```

## 6. Business rules enforced by the backend

| Rule | What the backend does | What the user sees |
|------|-----------------------|--------------------|
| BR-03 | Plans, payment and status need an approved account | `403` for incomplete / pending / suspended accounts |
| BR-14 | A failed payment never activates; no dates are set | `payment_failed` + message + retry |
| BR-27 | Dated priority from `starts_at` to `ends_at`; a non-approved or suspended account gets none even with a paid plan | Status card still reads `active` for a suspended account; ranking ignores it |
| BR-09 | Ranking priority is applied **after** eligibility filtering; higher `priority_level` first | No effect on her own screens |
| DEC-12 | One active subscription per provider | `409 FEATURED_SUBSCRIPTION_ACTIVE` |
| ADMIN-005 | Only enabled plans can be bought; price edits affect new purchases only | `422 FEATURED_PLAN_UNAVAILABLE`; running subscriptions keep their price |
| Money | VAT `price × vat_rate`, rounded half-up to the halala; exact 2-decimal strings | Totals such as `343.85` — the prototype screen shows rounded riyals |

## 7. Notifications and real-time events

Written to the in-app notification inbox (`app = provider`); push is delivered through the shared FCM sender for the events whose template has push enabled (payment succeeded/failed, activation, expiry and the 3-day expiry reminder `featured_subscription_expiring`, TD-167); see [notifications](notifications.md).

| `event_key` | Sent when | Payload `data` keys | App action on tap |
|-------------|-----------|---------------------|-------------------|
| `featured_subscription_activated` | Payment succeeded | `subscription_id`, `status` | Open the status card |
| `featured_payment_failed` | Payment failed | `subscription_id`, `status` | Open the payment screen |
| `featured_subscription_expired` | `ends_at` passed (`featured:expire`, every minute, or the next time she reads her status) | `subscription_id`, `status` | Open the plans screen |

## 8. Localization and UI notes

- Plan `name`, `description`, `features` and every `label` follow `Accept-Language` (Arabic fallback).
- Money and VAT come ready as strings; format with the app's currency helper (`ر.س`), never re-add VAT.
- The "Verified" badge shown to customers comes from verification status, not from the plan (DEC-23); this screen only explains that the plan includes it for her.

## 9. Integration checklist

- [ ] Models parse the real responses above (`data` of `GET /featured/subscription` can be `null`)
- [ ] `201` and `200 payment_failed` of `POST /featured/subscription` handled, with a retry that reuses the same call
- [ ] `409`, `404`, `422` (both kinds) handled
- [ ] Buttons driven by `can_subscribe`, `can_retry`, `is_active`
- [ ] Wallet / Tabby not offered as methods
- [ ] Tested in `ar` (RTL) and `en`

## 10. Acceptance scenarios

| AC ID | Scenario | Expected |
|-------|----------|----------|
| AC-PROV-007 | Pay successfully | Subscription `active`, `starts_at`/`ends_at` from the plan duration, `is_subscribed` true, ranking priority applies |
| AC-PROV-007 | Payment fails | Nothing activated, failure message, retry succeeds on the same row |
| AC-PROV-007 | Reach expiry | Priority gone (`v_provider_featured`), status `expired`, notice sent, she can subscribe again |

Tests: `tests/Feature/Api/V1/Provider/FeaturedSubscriptionTest.php`.

## 11. Open decisions and known limitations

| ID | Impact on this feature | Current behavior |
|----|------------------------|------------------|
| DEC-06 | Payment vendor and callbacks open | Card / mada / Apple Pay go through the **non-production simulator** (TD-94); in production they answer `PAYMENT_GATEWAY_UNAVAILABLE` until a real gateway exists. **Launch blocker.** Earnings deduction works without a gateway. |
| DEC-12 | Refunds on cancellation, suspension effects, renewal/overlap | No cancel/refund endpoint. `auto_renew` is stored and shown, but **no automatic charge happens yet** (needs the gateway and a stored token): at `ends_at` the plan expires and she subscribes again (TD-98). |
| DEC-23 | Customer-facing "Featured" label, home package highlighting, monthly performance report | `grants_featured_badge` / `features_packages_on_home` are returned for display only; nothing reads them yet. |
| TD-98 | Earnings deduction | Writes a negative `subscription_deduction` earning (status `available`); the payout feature must net it from her next transfer. No balance check — the deduction may exceed her current amounts due. |

## 12. 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 |
|--------|-------------|------|------|
| PROV-007 | Provider can view the available plans | true | `GET /featured/plans` (`FeaturedSubscriptionTest`) |
| PROV-007 | Name, description, benefits, duration and fee shown before payment | true | Plus VAT and total (SCR-34) |
| PROV-007 | Before payment, priority in search, Joshan results and provider lists shown as separate items | false | Benefits are admin-written bullets (`features`); there are no structured priority fields |
| PROV-007 | Subscribe by choosing a plan and paying in the app | true | Card, mada and Apple Pay on the local simulator; earnings deduction without a gateway |
| PROV-007 | Real payment gateway for card, mada and Apple Pay | false | DEC-06 open; production answers `PAYMENT_GATEWAY_UNAVAILABLE` (TD-94, launch blocker) |
| PROV-007 | Successful payment activates, sets start and end dates, shows Active | true | Tested, including `is_subscribed` on `GET /auth/me` |
| PROV-007 | Failed payment does not activate, shows a message, offers retry | true | Retry reuses the same subscription row |
| PROV-007 | Ranking priority only while active and the account is approved | true | `v_provider_featured`; suspended account tested |
| PROV-007 | One test that subscribes through the endpoint and then proves she ranks first in customer search | false | The view and the ranking query are tested separately, not together |
| PROV-007 | Featured status does not change eligibility, orders, prices, acceptance or ratings | true | No code outside the ranking `ORDER BY` reads it |
| PROV-007 | Expiry removes priority, sets Expired, provider returns to normal ranking | true | `featured:expire` and lazy expiry on read |
| PROV-007 | Provider can subscribe again after expiry | true | Tested |
| SCR-34 | Auto-renew shown and switchable | true | Flag stored and returned |
| SCR-34 | Auto-renew actually charges at the end date | false | Needs the real gateway (TD-98); the plan just expires |
| BRD MOD-10 | Notices: successful payment, failed payment, activation, expiry | true | In-app inbox and FCM push |
| BRD MOD-10 | Notice for upcoming expiry | true | `featured_subscription_expiring` is sent 3 days before the end date by `featured:alert-expiring` (TD-167) |
| BRD provider home | Status and dates, plus a shortcut to subscribe, on the provider home | true | `GET /provider/home` returns `featured` (status, dates, `can_subscribe`), see [home](home.md) 1.1.0 |
| Screen 1 | Plan cards with price, period, benefit bullets, "Subscribe now" | true | |
| Screen 1 | Banner about the Verified badge and the badge footnote | true | Static app text (DEC-23); the API returns `grants_verified_badge` |
| Screen 2 | Payment methods incl. saved mada card, bank card, Apple Pay, deduct from earnings | true | |
| Screen 2 | Saved cards actually exist for a provider | false | No way for a provider to save a card yet; the list shows none until the gateway exists |
| Screen 3 | Summary with plan, period, renewal, fee, VAT 15 % and total | true | Exact amounts (`44.85`, `343.85`); the prototype rounds to whole riyals |
| Screen 3 | "Deducted from the next transfer" applied to a payout | false | The negative earning is recorded; the payout feature that nets it does not exist |
| Screen 1 | Plan benefits "monthly performance report" and packages highlighted on the home page | false | Flags `features_packages_on_home` and `grants_featured_badge` are stored only; DEC-23 open |
| Seed data | Seeded plans match the screens (149 / 299 / 2,690 SAR) | false | Seeder still has 199 and 399 SAR |
| Tests | Feature tests, Postman examples for every route, `postman:generate --check` | true | 12 provider tests |
| Rate limit | `POST /featured/subscription` throttled | false | No throttle; add one if wanted |
