---
feature: Platform commission management
slug: commissions
surface: admin
consumer: react
version: 1.0.1
status: ready
brd_version: "1.3"
brd_ids: [ADMIN-016]
business_rules: [BR-24, BR-25, BR-29]
decisions: [DEC-09, TD-93, TD-149, TD-158, TD-159]
schema_tables: [commission_rules, orders, earnings]
related_docs: [payouts.md, ../provider/earnings.md, ../delivery/earnings.md]
last_updated: 2026-11-10
---

# Platform commission 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.1 | 2026-11-10 | PATCH | Validation messages for the commission-rule form use Arabic field names; no English labels or sentences in Arabic responses (QA-NEW-01). No contract change. | None. |
| 1.0.0 | 2026-11-02 | Initial | Commission rules per party and type, scheduled changes, timeline state, on/off switch. | Build the Commissions page in the Payouts section. |

## Changelog

### 1.0.1 — 2026-11-10

- **Fixed:** Validation messages for the commission-rule 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.0 — 2026-11-02

- **Added:** `GET/POST /commission-rules`, `GET/PUT/DELETE /commission-rules/{rule}`, `PATCH /commission-rules/{rule}/toggle-active`.
- **Breaking:** No (new). **BRD / decisions:** ADMIN-016, BR-24, TD-159.

## 1. Business summary

The platform takes a commission from the provider on the **service or package value** and, separately, from the driver on the **delivery fee** (BR-24). Each rule has a party, what it applies to, a percent or a fixed SAR amount, and a start time (`effective_from`). For each pair (provider·service, provider·package, driver·delivery_fee) the rule **in force** is the newest active rule whose start has passed; checkout snapshots the provider rate on the order, and arrival confirmation writes the driver commission on the trip earning, so changing a rule never rewrites past orders or earnings.

A change is a **new rule** with a start date. A rule that has taken effect is history: it can no longer be edited or deleted, only switched off (TD-159). The delivery fee goes to the provider when she travels herself and to the driver otherwise (BR-25); that split is applied when earnings are written ([payouts](payouts.md)).

## 2. Pages and permissions

| Page / component | Purpose | Endpoint(s) | RBAC permission (route name) | Section · action |
|------------------|---------|-------------|------------------------------|------------------|
| CommissionRulesPage | All rules with their state | `GET /commission-rules` | `admin.commission-rules.index` | payouts · view |
| CommissionRuleDrawer | One rule | `GET /commission-rules/{rule}` | `admin.commission-rules.show` | payouts · view |
| CommissionRuleForm (create) | New or scheduled rule | `POST /commission-rules` | `admin.commission-rules.store` | payouts · create |
| CommissionRuleForm (edit) | Change a scheduled rule | `PUT /commission-rules/{rule}` | `admin.commission-rules.update` | payouts · update |
| Switch | Turn a rule on/off | `PATCH /commission-rules/{rule}/toggle-active` | `admin.commission-rules.toggle-active` | payouts · update |
| Delete | Remove a scheduled rule | `DELETE /commission-rules/{rule}` | `admin.commission-rules.destroy` | payouts · delete |

## 3. List / table

`GET /commission-rules` — standard `page`, `per_page`, `paginate=false`, `search` (party, type, calc type), `sort[...]`. Filters: `filters[party]` (`provider`, `driver`), `filters[applies_to]` (`service`, `package`, `delivery_fee`), `filters[calc_type]` (`percent`, `fixed`), `filters[is_active]` (`1`/`0`). Rows are wrapped in `data.commission_rules`. Postman: `admin.commission-rules.index` › "200 Commission Rules List" (two rows shown):

```json
{
    "status": "success",
    "message": "Data retrieved successfully",
    "data": {
        "commission_rules": [
            {
                "id": "01a19be2-dcf0-724c-bfac-f09bbaed101c",
                "party": {
                    "value": "provider",
                    "label": "Provider"
                },
                "applies_to": {
                    "value": "service",
                    "label": "Service value"
                },
                "calc_type": {
                    "value": "percent",
                    "label": "Percentage"
                },
                "value": "12.50",
                "is_active": true,
                "effective_from": "2026-11-02T10:51:38+00:00",
                "state": {
                    "value": "current",
                    "label": "In force"
                },
                "can_edit": false,
                "created_at": "2026-11-02T10:51:38+00:00"
            },
            {
                "id": "01a19be2-dce8-7297-8d70-7dad7a5d0192",
                "party": {
                    "value": "provider",
                    "label": "Provider"
                },
                "applies_to": {
                    "value": "service",
                    "label": "Service value"
                },
                "calc_type": {
                    "value": "percent",
                    "label": "Percentage"
                },
                "value": "15.00",
                "is_active": true,
                "effective_from": "2026-10-02T10:51:38+00:00",
                "state": {
                    "value": "superseded",
                    "label": "Superseded"
                },
                "can_edit": false,
                "created_at": "2026-11-02T10:51:38+00:00"
            }
        ],
        "links": {
            "first": "http://localhost/api/v1/admin/commission-rules?page=1",
            "last": "http://localhost/api/v1/admin/commission-rules?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/admin/commission-rules?page=1",
                    "label": "1",
                    "page": 1,
                    "active": true
                },
                {
                    "url": null,
                    "label": "Next &raquo;",
                    "page": null,
                    "active": false
                }
            ],
            "path": "http://localhost/api/v1/admin/commission-rules",
            "per_page": 15,
            "to": 2,
            "total": 2
        }
    }
}
```

## 4. Details view

`GET /commission-rules/{rule}` adds `created_by` (`null` or `{id, full_name}`). Postman: `admin.commission-rules.show` › "200 Commission Rule Details":

```json
{
    "status": "success",
    "message": "Data retrieved successfully",
    "data": {
        "id": "01a19be2-dce8-7297-8d70-7dad7a5d0192",
        "party": {
            "value": "provider",
            "label": "Provider"
        },
        "applies_to": {
            "value": "service",
            "label": "Service value"
        },
        "calc_type": {
            "value": "percent",
            "label": "Percentage"
        },
        "value": "15.00",
        "is_active": true,
        "effective_from": "2026-10-02T10:51:38+00:00",
        "state": {
            "value": "superseded",
            "label": "Superseded"
        },
        "can_edit": false,
        "created_at": "2026-11-02T10:51:38+00:00",
        "created_by": null
    }
}
```

## 5. Forms (create / edit)

| Field | Rules (mirror client-side) | Meaning |
|-------|----------------------------|---------|
| `party` | required on create; `provider` \| `driver` | Who pays |
| `applies_to` | required on create; `service` \| `package` (provider), `delivery_fee` (driver) | Base of the commission |
| `calc_type` | required on create; `percent` \| `fixed` — provider rules must be `percent` | Percent of the base, or SAR |
| `value` | required on create; numeric ≥ 0, 2 decimals; percent ≤ 100 | 0 = no commission |
| `effective_from` | optional; date, not in the past; default now | When it takes over |
| `is_active` | optional boolean, default true | Use the switch afterwards |

Create, starting now — Postman: `admin.commission-rules.store` › "201 Commission Rule Created In Force":

```json
{
    "status": "success",
    "message": "Data saved successfully",
    "data": {
        "id": "01a19be2-dcf0-724c-bfac-f09bbaed101c",
        "party": {
            "value": "provider",
            "label": "Provider"
        },
        "applies_to": {
            "value": "service",
            "label": "Service value"
        },
        "calc_type": {
            "value": "percent",
            "label": "Percentage"
        },
        "value": "12.50",
        "is_active": true,
        "effective_from": "2026-11-02T10:51:38+00:00",
        "state": {
            "value": "current",
            "label": "In force"
        },
        "can_edit": false,
        "created_at": "2026-11-02T10:51:38+00:00",
        "created_by": {
            "id": "01a19be2-dce4-7045-85b1-ede59238f193",
            "full_name": "Itzel Hamill"
        }
    }
}
```

A start in the future gives `state: scheduled` and `can_edit: true` ("201 Commission Rule Scheduled"). `PUT` works only while the rule is `scheduled` ("200 Commission Rule Scheduled Edited").

| HTTP | error_code | When | Postman |
|------|------------|------|---------|
| 422 | — (`data.errors.applies_to`) | Driver rule not on `delivery_fee`, or provider rule on `delivery_fee` | `admin.commission-rules.store` › "422 Commission Rule Driver On Service" |
| 422 | — (`data.errors.calc_type`) | Fixed provider rule (orders snapshot a rate, TD-159) | "422 Commission Rule Provider Fixed" |
| 422 | — (`data.errors.value`) | Percent above 100 | "422 Commission Rule Percent Above 100" |
| 422 | — | Start in the past, missing fields | "422 Commission Rule Starts In The Past", "422 Commission Rule Missing Fields" |
| 409 | `COMMISSION_RULE_LOCKED` | `PUT` / `DELETE` on a rule that has taken effect | `admin.commission-rules.update` › "409 Commission Rule In Force Cannot Be Edited"; `admin.commission-rules.destroy` › "409 Commission Rule In Force Cannot Be Deleted" |

## 6. Actions (non-CRUD)

### 6.1 Switch on / off — `PATCH /commission-rules/{rule}/toggle-active`

Flips `is_active` and returns the rule. Switching off the rule in force makes the previous active rule of that pair current again. The **provider** rule in force cannot be switched off unless another provider rule of that type is in force — checkout would otherwise refuse every booking (TD-93); to stop charging providers, add a 0% rule. A **driver** rule may always be switched off: trips then complete with no commission. Postman: `admin.commission-rules.toggle-active` › "200 Commission Rule Switched Off", "409 Commission Rule Last Provider Rule Required" (`COMMISSION_RULE_REQUIRED`).

## 7. Statuses and enums

| Field | Values |
|-------|--------|
| `party` | `provider`, `driver` |
| `applies_to` | `service`, `package`, `delivery_fee` |
| `calc_type` | `percent`, `fixed` |
| `state` (derived) | `scheduled` (start not reached; editable, deletable) · `current` (in force) · `superseded` (active but replaced by a newer rule) · `inactive` (switched off) |

## Data model

`commission_rules` (party, applies_to, calc_type, value, is_active, effective_from, created_by). Readers: checkout snapshots `orders.provider_commission_rate` (service) and package purchases snapshot their own rate; arrival confirmation writes `earnings.commission_rate/commission_amount` for drivers (a fixed driver commission is capped at the fee, TD-149).

## 8. TypeScript types

```ts
type EnumObj<T extends string> = { value: T; label: string };
interface CommissionRule {
  id: string;
  party: EnumObj<'provider' | 'driver'>;
  applies_to: EnumObj<'service' | 'package' | 'delivery_fee'>;
  calc_type: EnumObj<'percent' | 'fixed'>;
  value: string;              // "15.00" percent or SAR
  is_active: boolean;
  effective_from: string;     // ISO 8601
  state: EnumObj<'scheduled' | 'current' | 'superseded' | 'inactive'>;
  can_edit: boolean;          // true only while scheduled
  created_at: string;
  created_by?: { id: string; full_name: string } | null; // details only
}
```

## 9. Business rules enforced by the backend

- BR-24: provider commission on service/package value only; driver commission on the delivery fee only.
- TD-159: provider rules are percent; a rule in force is never edited or deleted; the provider rule in force cannot be switched off alone.
- A rule's start can't be in the past.

## 10. Notifications triggered

None.

## 11. Integration checklist

- [ ] Show `state` as a badge; enable Edit/Delete only when `can_edit` is true.
- [ ] "Change rate" = create a new rule (optionally with a future `effective_from`).
- [ ] Hide `fixed` in the form when the party is `provider`.
- [ ] Handle 409 `COMMISSION_RULE_LOCKED` and `COMMISSION_RULE_REQUIRED` with the returned message.

## 12. Acceptance scenarios

AC-ADMIN-016: `CommissionRuleManagementTest` — a new rule takes over while the old one stays as history; scheduled rules are editable until they take effect; party/type pairing, provider percent-only and 100% cap; the provider rule in force needs a successor before switching off; switching off a driver rule leaves trips commission-free.

## 13. Open decisions and known limitations

| ID | Impact | Current behavior |
|----|--------|------------------|
| TD-159 | Fixed provider commission (BRD "percentage or amount") | Not offered: orders snapshot only a rate; would need an order column for a fixed amount |
| ADMIN-022 | Commission reports by type | In [reports](reports.md) |
