---
feature: Driver management
slug: drivers
surface: admin
consumer: react
version: 2.3.0
status: ready
brd_version: "1.3"
brd_ids: [ADMIN-006, ADMIN-007, ADMIN-021, DRIV-002, DRIV-003, DRIV-006]
business_rules: [BR-01, BR-03, BR-04, BR-29]
decisions: [DEC-25, TD-83, TD-06, TD-19, TD-25, TD-27, TD-28, TD-35, TD-36, TD-38, TD-43, TD-44, TD-45, TD-47, TD-48, TD-50, TD-51, TD-52, TD-54, TD-03, TD-173, TD-178]
schema_tables: [users, drivers, driver_districts, vehicles, user_documents, payout_accounts, profile_change_requests, account_review_events, governorates, governorate_translations, regions, region_translations, media, audit_logs, notifications]
related_docs: [providers.md, customers.md, admins-permissions.md, ../delivery/auth.md]
last_updated: 2026-11-10
---

# Driver management — React Dashboard Integration Guide

Base URL: `{HOST}/api/v1/admin`. Shared headers/envelope/errors/TypeScript helpers: [API_CONVENTIONS](../API_CONVENTIONS.md). Every route requires `auth:api`, `app:admin`, `active`, `rbac.auto`. Supervisors need the exact route grant; super admins pass.

Examples below are captured from passing MySQL HTTP tests in `DriverManagementTest`. Names, numbers and document files are synthetic fixtures. All date-times are **ISO 8601 with offset** (`2026-10-22T06:57:15+00:00`, TD-03); parse them directly with `new Date(...)`, not `parseLegacyUtc` (that helper is only for legacy TD-38 contracts). Birth/expiry dates are calendar YYYY-MM-DD.

## Version history

| Version | Date | Type | Summary | Dashboard action required |
|---------|------|------|---------|---------------------------|
| 2.3.0 | 2026-11-10 | MINOR | Change requests are now reviewed here: list, approve exactly as submitted, reject with a reason (R01–R03, TD-184). The read-only metadata of TD-44 is superseded; see [profile-change-requests.md](profile-change-requests.md). | Add the approve and reject actions to the driver details. |
| 2.2.0 | 2026-11-08 | MINOR | Recipient pickers can filter existing customer, provider and driver lists with `filters[notification_reachable]` (TD-178). | Use `filters[notification_reachable]=1` for notification picks; keep handling send-time 422. |
| 2.1.2 | 2026-11-08 | PATCH | Trips, earnings and ratings point to the sections that show them. | None. |
| 2.1.1 | 2026-10-26 | PATCH | Documentation only: validation 422 examples and rows now show `data.errors` (`{field: [messages]}`, TD-02, API_CONVENTIONS 2.7.0) instead of `data: null`; examples are the captured Postman responses. | None. |
| 2.1.0 | 2026-10-25 | MINOR | `PUT /drivers/{id}/work-area`: the admin links a driver to her city and coverage districts in any status (ADMIN-021, TD-83). | Add a coverage editor to the driver page, gated by `admin.drivers.work-area`. |
| 2.0.1 | 2026-10-25 | PATCH | Documentation only (PATCH): ADMIN-007 is built as a combined queue over this review — see [registration-review.md](registration-review.md). No contract change. | None. |
| 2.0.0 | 2026-10-22 | MAJOR | ISO 8601 date-times; search no longer matches national ID/iqama. | Parse date-times as ISO; stop offering ID search. |
| 1.1.0 | 2026-10-21 | MINOR | Localized approval prerequisites, transactional document verification and full document numbers in audited identity review. Public vehicle plates and active-vehicle approval prerequisite. | Render identity.documents; refetch documents/history after approval. Display full vehicle plates; handle the Active vehicle prerequisite. |
| 1.0.0 | 2026-10-21 | Initial | Driver list/details, audited identity reveal, shared review actions and inbox notices. | Integrate eight driver routes and exact grants. Keep content, financial/trip details and change requests read-only/deferred. |

## Changelog

### 2.2.0 — 2026-11-08

- **Added:** Optional boolean `filters[notification_reachable]` on `GET /admin/users`, `GET /admin/providers` and `GET /admin/drivers` (TD-173/178). `1` returns reachable rows; `0` returns unreachable rows within each list's existing scope; omission preserves the usual list.
- **Eligibility:** Active, not admin-disabled, not banned (`is_ban = false` or `null`), not deleted; providers/drivers must be `approved`. The filter reuses the general-send audience query. It does not require a registered device, enabled notification preferences, city coverage, an online driver or an active vehicle: in-app recipients remain eligible without push delivery.
- **Dashboard action required:** Use `filters[notification_reachable]=1` in the selected-users picker. Existing search, city, account-status filters, sorting, pagination and list permissions still apply. `paginate=false` returns the existing direct array capped at 500; use paginated search for larger audiences. Pass the user UUID from `/users` to `customer_ids`; pass profile UUIDs from `/providers` and `/drivers` to their respective keys.
- **Errors:** Invalid values return 422 with `data.errors["filters.notification_reachable"]`. Account state can change after picking; still handle `NOTIFICATION_RECIPIENTS_UNREACHABLE` at send time and refresh the picker. Reachability is account eligibility, not a delivery guarantee.
- **Postman:** `admin.users.index`, `admin.providers.index`, `admin.drivers.index` › "200 Reachable general notification recipients", "200 Reachable recipients dropdown", "422 Invalid notification reachability filter" (real responses from `NotificationRecipientFilterTest`).
- **Breaking:** No. Existing endpoints, response keys and default results are preserved.

### 2.1.2 — 2026-11-08

- **Changed:** Trips, earnings and ratings point to the sections that show them.
- **Breaking:** No. **App action required:** None.

### 2.1.1 — 2026-10-26

- **Added:** N/A.
- **Changed:** Validation 422 examples and error-table rows show `data.errors`, not `data: null` (TD-02).
- **Fixed:** Documentation only; the server already returned `data.errors`.
- **Removed / Deprecated:** N/A.
- **Breaking:** No.
- **Endpoints affected:** None (documentation).
- **App action required:** None; map `data.errors` onto form fields, fall back to `message`.
- **BRD / decisions:** TD-02.

### 2.1.0 — 2026-10-25

- **Added:** `PUT /drivers/{id}/work-area` (permission `drivers` · update): city and coverage districts, any account status, audited (§6.7).
- **Changed / Fixed / Removed / Deprecated:** —
- **Breaking:** No.
- **Endpoints affected:** the one above (new).
- **Dashboard action required:** re-sync the permission catalog; show the editor to admins with the cell.
- **BRD / decisions:** ADMIN-021, TD-83.

### 2.0.0 — 2026-10-22

- **Changed (breaking):** every date-time field is ISO 8601 with offset instead of `Y-m-d H:i:s` — list `location_updated_at`/`submitted_at`/`created_at`; details `consents.*`, `approved_at`, `current_location.updated_at`, `change_requests.created_at`, `review_history.created_at`, `updated_at`; and `approved_at` in every action response. Date-only `birth_date` and `documents.expires_at` stay `YYYY-MM-DD`. These endpoints are new, so they follow TD-03 rather than the TD-38 legacy format.
- **Changed (breaking):** `search` matches user name, phone and vehicle plate only. It no longer matches `national_id`: a view-only admin could confirm or rebuild a masked number through search, bypassing the audited identity grant (TD-54).
- **Endpoints affected:** all eight driver routes (dates); `GET /drivers` (search).
- **Dashboard action required:** parse date-times as ISO 8601 (drop `parseLegacyUtc` for these screens); remove any "search by ID" hint — use the audited identity panel instead.
- **Migration note (old → new):** `"2026-10-21 14:52:18"` → `"2026-10-21T14:52:18+00:00"`. Both are UTC, so stored values are unchanged.
- **BRD / decisions:** ADMIN-006, TD-03, TD-45, TD-54.

### 1.1.0 — 2026-10-21

- **Added:** `documents: [{id, type: {value,label}, doc_number}]` to the existing audited identity endpoint. Saved document numbers are full here; audit records field names and document UUIDs, never numbers. Missing numbers remain null; IBAN is never revealed.
- **Changed:** approval verifies exactly the qualifying required documents in the same transaction, setting reviewed_by/reviewed_at on pending rows. Existing verified rows keep their attribution. The review event summarizes qualifying/newly verified counts; the profile audit links the event and document UUIDs, alongside document change audits. Non-qualifying rows remain untouched (TD-50).
- **Fixed:** missing-document 422 messages use English/Arabic document-type labels and a localized submission label.
- **Changed:** vehicle plates are public in lists/details. DRIV-002/003 require vehicle details at registration, so initial approval also requires at least one active vehicle. Added local no-vehicle fixture +966 500009204; existing manual decisions are preserved (TD-51/52).
- **Removed / Deprecated:** none. **Breaking:** no response type, route, permission or error-code changes; existing review prerequisites are enforced with localized messages.
- **Endpoints affected:** `GET /drivers`, `GET /drivers/{id}`, `GET /drivers/{id}/identity`, `POST /drivers/{id}/approve`.
- **Dashboard action required:** render full document numbers only in the explicit identity panel; keep default numbers and all IBANs masked. Refetch document statuses/history after approval. Show the public plate and handle the localized active-vehicle 422.
- **BRD / decisions:** ADMIN-004/006/007, TD-50/51/52, DRIV-002/003.

### 1.0.0 — 2026-10-21

- Adds driver list/details, default masking, audited full national_id/iqama reveal and five profile-status actions using the same AccountReviewService/AccountReviewRequest as Providers.
- Adds driver view/approve route permissions; existing additive review-event columns are reused without new schema changes. ADMIN-007 stays planned and will reuse the service.
- Extends the opt-in local review seeder with two complete driver submissions and one missing a driving licence.
- **App action required:** sync RBAC, assign the new routes and integrate §2/§8. No driver create/edit/delete, trip or payout controls.

## 1. Business summary

Review drivers who self-register in the delivery app, compare their identity/licence/vehicle documents and decide approval, rejection, corrections, suspension or reactivation. Account registration starts incomplete; admin approval requires submission, valid attached documents and at least one active vehicle (DRIV-002/003).

Driver suspension updates only drivers.account_status. It preserves users.is_active and any provider/customer profile. is_online is the saved flag, not proof of eligibility; operations require approved status. Dispatch/trip execution and financial screens are separate features (§13).

## 2. Pages and permissions

| Page / component | Purpose | Endpoint | Exact permission | Section · action |
|------------------|---------|----------|------------------|------------------|
| DriversPage | Search/filter/page | GET /drivers | admin.drivers.index | drivers · view |
| DriverDetailsPage | View saved record | GET /drivers/{id} | admin.drivers.show | drivers · view |
| IdentityReviewPanel | Compare full identity with documents | GET /drivers/{id}/identity | admin.drivers.identity | drivers · approve |
| CoverageEditor | Set city and coverage districts | PUT /drivers/{id}/work-area | admin.drivers.work-area | drivers · update |
| ApproveDialog | Approve submission | POST /drivers/{id}/approve | admin.drivers.approve | drivers · approve |
| RejectDialog | Reject with reason | POST /drivers/{id}/reject | admin.drivers.reject | drivers · approve |
| RequestChangesDialog | Request corrections | POST /drivers/{id}/request-changes | admin.drivers.request-changes | drivers · approve |
| SuspendDialog | Suspend driver profile | POST /drivers/{id}/suspend | admin.drivers.suspend | drivers · approve |
| ReactivateDialog | Restore prior approval | POST /drivers/{id}/reactivate | admin.drivers.reactivate | drivers · approve |

Sidebar: Accounts → Drivers, hidden without admin.drivers.index. Gate each control by its exact route. A view grant cannot reveal identity. Granting an entire matrix cell assigns its routes; direct grants can be narrower.

Paths use **drivers.uuid**. users.uuid, integer IDs, unknown UUIDs and accounts without that driver profile return 404. Admin/deleted identities are excluded even with a driver row. Other app profiles do not prevent access. Nested IDs/user_id/approved_by are UUIDs. Invalid/unknown **city filters** return 422 naming filters.city_id, not 404.

## 3. List / table

`GET /drivers` returns **data.drivers** with standard links/meta. Default 15 per page; page integer ≥1, per_page 1…100. Default paginate=true|1; paginate=false|0 returns a direct data array capped at 500.

| Field | Type / meaning | Filtering / sorting |
|-------|----------------|---------------------|
| id, full_name | Driver UUID, user name | Search name |
| phone_code, phone | Separate stored user phone fields | Search phone |
| nationality | Saved text/null | — |
| city | `{id,name}`/null, localized with Arabic fallback | filters[city_id] UUID/exists |
| districts_count | Actual linked district count | — |
| vehicle | Active vehicle summary/null | Search plate across saved vehicles |
| account_status | `{value,label}`, §7 | filters[account_status] |
| is_online | Stored boolean | filters[is_online]=0|1 |
| location_updated_at | ISO 8601 date-time/null, also shown offline | Coordinates are absent from list |
| rating_avg, rating_count | Decimal string/null and stored count | sort rating_avg |
| completed_trips_count | Existing profile counter | sort completed_trips_count |
| submitted_at, created_at | Submission nullable, ISO 8601 date-times | Sort both; date ranges below |

Vehicle summary: id UUID, type enum car/motorbike, make_model, model_year/null, color/null, public unmasked plate_number, is_active. There is no separate make/model column in the schema. No active vehicle means null; details retain historical vehicles.

Search: nullable string max255, partial match on user name, phone and saved vehicle plate. National ID/iqama is never searchable: matching it would let a view-only admin confirm a masked value without the audited identity grant (TD-54); compare numbers through `GET /drivers/{id}/identity`. Identity/document numbers are not serialized in list rows; public vehicle plates are returned in full.

For notification recipient picks, use `GET /api/v1/admin/drivers?filters[notification_reachable]=1`. Optional `0` selects unreachable accounts; see the 2.2.0 changelog above for eligibility and errors. Existing permissions and other filters apply.

Allowed filters: notification_reachable, account_status, city_id, is_online, created_from/created_to, submitted_from/submitted_to under filters[...]. Dates are inclusive UTC YYYY-MM-DD; end ≥ start when both supplied. Null submission dates do not match a submission filter. Allowed sorts: created_at, submitted_at, rating_avg, completed_trips_count under sort[...], asc|desc. Default created_at DESC, then internal key DESC. Unsupported keys/values return 422 naming the concrete path, including sort.rating_avg. Keep nested query serialization as in customers/providers.

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "drivers": [
      {
        "id": "01a16266-7abf-7345-a074-e40a0e7ddbe1",
        "full_name": "Mark Pacocha II",
        "phone_code": "966",
        "phone": "541473115",
        "nationality": null,
        "city": null,
        "districts_count": 0,
        "vehicle": {
          "id": "01a16266-7aca-716c-ac39-6fdf47e095c9",
          "type": {
            "value": "car",
            "label": "Car"
          },
          "make_model": "Toyota Yaris",
          "model_year": null,
          "color": null,
          "plate_number": "CAR-icwe31496214",
          "is_active": true
        },
        "account_status": {
          "value": "pending_review",
          "label": "Pending review"
        },
        "is_online": false,
        "location_updated_at": null,
        "rating_avg": null,
        "rating_count": 0,
        "completed_trips_count": 0,
        "submitted_at": "2026-10-22T06:57:25+00:00",
        "created_at": "2026-10-22T06:57:25+00:00"
      }
    ],
    "links": {
      "first": "http://localhost/api/v1/admin/drivers?page=1",
      "last": "http://localhost/api/v1/admin/drivers?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/drivers?page=1",
          "label": "1",
          "page": 1,
          "active": true
        },
        {
          "url": null,
          "label": "Next &raquo;",
          "page": null,
          "active": false
        }
      ],
      "path": "http://localhost/api/v1/admin/drivers",
      "per_page": 15,
      "to": 1,
      "total": 1
    }
  }
}
```

## 4. Details view

`GET /drivers/{id}` returns list fields plus:

| Field | Type / nullability | Meaning |
|-------|--------------------|---------|
| user_id | UUID | App identity; not the driver path ID |
| birth_date, national_id | Calendar date/null, masked string/null | ID and iqama share national_id |
| status_reason | string/null | Adverse-action reason; cleared on approve/reactivate |
| consents | Three ISO 8601 date-times/null | female_declared_at, terms_accepted_at, privacy_accepted_at; no inferred acceptance |
| approved_at, approved_by | Timestamp/null, admin UUID/null | Original approval attribution, preserved on reactivate |
| image | Existing image URL/null | No invented upload/placeholder |
| districts | `{id,name}[]` | Driver coverage from pure driver_districts links |
| current_location | `{lat,lng,updated_at}`/null | Only when is_online=true; lat/lng numbers or null, updated_at ISO 8601/null. Offline →null even with saved coordinates |
| vehicles | Vehicle summary[] | Active first then newest, public saved plates, no edit controls |
| documents | Document[] | driver-role only: id, type/status enums, masked doc_number, expiry date/null, file_url/null |
| payout_account | Payout summary/null | Current primary non-inactive driver account; holder_name, bank_name, masked IBAN, status enum |
| change_requests | Pending request metadata[] | id, change_type/status enum, created_at; payload/files omitted until safe contract exists |
| review_history | Review event[] | Newest first, id/action/from_status/to_status/reason/actor/created_at; actor UUID/name or null |
| updated_at | ISO 8601 date-time | Saved profile update time |

Other-role documents/payout accounts/change requests/review events are excluded. Arrays remain arrays. Historical/non-transition review states can be null. Sensitive strings show a fixed **** prefix plus last four; short strings are fully masked. IBAN has no reveal endpoint.

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "01a16266-9b19-7167-9802-709a66399087",
    "full_name": "Dr. Jefferey Hyatt",
    "phone_code": "966",
    "phone": "527970129",
    "nationality": "Saudi",
    "city": {
      "id": "01a16266-9b13-7162-9184-153fc3e4e121",
      "name": "Riyadh"
    },
    "districts_count": 1,
    "vehicle": {
      "id": "01a16266-9b25-71af-9ad3-cfd6d61e7d9f",
      "type": {
        "value": "motorbike",
        "label": "Motorbike"
      },
      "make_model": "Toyota Yaris",
      "model_year": null,
      "color": null,
      "plate_number": "ABC123456",
      "is_active": true
    },
    "account_status": {
      "value": "pending_review",
      "label": "Pending review"
    },
    "is_online": true,
    "location_updated_at": "2026-10-22T06:57:33+00:00",
    "rating_avg": "4.75",
    "rating_count": 8,
    "completed_trips_count": 12,
    "submitted_at": "2026-10-22T06:57:33+00:00",
    "created_at": "2026-10-22T06:57:33+00:00",
    "user_id": "01a16266-9b19-7167-9802-709a667843be",
    "birth_date": "1994-03-20",
    "national_id": "****7890",
    "status_reason": null,
    "consents": {
      "female_declared_at": null,
      "terms_accepted_at": null,
      "privacy_accepted_at": null
    },
    "approved_at": null,
    "approved_by": null,
    "image": null,
    "districts": [
      {
        "id": "01a16266-9b21-71f5-b35f-4bab428a2307",
        "name": "Sample-jqws74435441"
      }
    ],
    "current_location": {
      "lat": 24.7136,
      "lng": 46.6753,
      "updated_at": "2026-10-22T06:57:33+00:00"
    },
    "vehicles": [
      {
        "id": "01a16266-9b25-71af-9ad3-cfd6d61e7d9f",
        "type": {
          "value": "motorbike",
          "label": "Motorbike"
        },
        "make_model": "Toyota Yaris",
        "model_year": null,
        "color": null,
        "plate_number": "ABC123456",
        "is_active": true
      },
      {
        "id": "01a16266-9b26-70d8-92f5-3d4d606c11c3",
        "type": {
          "value": "car",
          "label": "Car"
        },
        "make_model": "Toyota Yaris",
        "model_year": null,
        "color": null,
        "plate_number": "HISTORY9876",
        "is_active": false
      }
    ],
    "documents": [
      {
        "id": "01a16266-9b1f-7234-9615-d42a39ba66ba",
        "type": {
          "value": "vehicle_registration",
          "label": "Vehicle registration"
        },
        "status": {
          "value": "pending",
          "label": "Pending"
        },
        "doc_number": null,
        "expires_at": null,
        "file_url": "http://localhost/storage/demo/placeholder.jpg"
      },
      {
        "id": "01a16266-9b1e-71f3-ad3d-1b7d4fff628c",
        "type": {
          "value": "driving_license",
          "label": "Driving license"
        },
        "status": {
          "value": "pending",
          "label": "Pending"
        },
        "doc_number": null,
        "expires_at": null,
        "file_url": "http://localhost/storage/demo/placeholder.jpg"
      },
      {
        "id": "01a16266-9b1c-7285-ad17-b531f572c904",
        "type": {
          "value": "national_id",
          "label": "National ID / iqama"
        },
        "status": {
          "value": "pending",
          "label": "Pending"
        },
        "doc_number": null,
        "expires_at": null,
        "file_url": "http://localhost/storage/demo/placeholder.jpg"
      }
    ],
    "payout_account": {
      "id": "01a16266-9b27-7355-b37b-173505e15838",
      "holder_name": "qkruciegbqii",
      "bank_name": "eykghurowghx",
      "iban": "****9012",
      "status": {
        "value": "pending",
        "label": "Pending"
      }
    },
    "change_requests": [
      {
        "id": "01a16266-9b28-7261-a8b6-3a808d465fc2",
        "change_type": {
          "value": "profile",
          "label": "Profile"
        },
        "status": {
          "value": "pending",
          "label": "Pending"
        },
        "created_at": "2026-10-22T06:57:33+00:00"
      }
    ],
    "review_history": [],
    "updated_at": "2026-10-22T06:57:33+00:00"
  }
}
```

Review history from a tested approval/suspension/reactivation cycle, including a historical submitted event:

```json
[
  {
    "id": "01a16266-8bdb-71b9-adcf-9b2bf358b9ee",
    "action": {
      "value": "reactivated",
      "label": "Reactivated"
    },
    "from_status": {
      "value": "suspended",
      "label": "Suspended"
    },
    "to_status": {
      "value": "approved",
      "label": "Approved"
    },
    "reason": "Verified again",
    "actor": {
      "id": "01a16266-8bb4-731b-9a80-88742d19df6e",
      "full_name": "Mrs. Alena Mertz"
    },
    "created_at": "2026-10-22T06:57:30+00:00"
  },
  {
    "id": "01a16266-8bd0-71a2-9d58-1e05159ee54e",
    "action": {
      "value": "suspended",
      "label": "Suspended"
    },
    "from_status": {
      "value": "approved",
      "label": "Approved"
    },
    "to_status": {
      "value": "suspended",
      "label": "Suspended"
    },
    "reason": "Licence verification",
    "actor": {
      "id": "01a16266-8bb4-731b-9a80-88742d19df6e",
      "full_name": "Mrs. Alena Mertz"
    },
    "created_at": "2026-10-22T06:57:30+00:00"
  },
  {
    "id": "01a16266-8bc5-7357-b055-b5b79c2c93c8",
    "action": {
      "value": "approved",
      "label": "Approved"
    },
    "from_status": {
      "value": "pending_review",
      "label": "Pending review"
    },
    "to_status": {
      "value": "approved",
      "label": "Approved"
    },
    "reason": "Approval documents: 3 qualifying; 3 newly verified.",
    "actor": {
      "id": "01a16266-8bb4-731b-9a80-88742d19df6e",
      "full_name": "Mrs. Alena Mertz"
    },
    "created_at": "2026-10-22T06:57:30+00:00"
  },
  {
    "id": "01a16266-8be3-707b-8e41-0b54bdea9592",
    "action": {
      "value": "submitted",
      "label": "Submitted"
    },
    "from_status": null,
    "to_status": null,
    "reason": null,
    "actor": null,
    "created_at": "2026-10-21T06:57:30+00:00"
  }
]
```

## 5. Forms (create / edit)

N/A. Drivers self-register in their app. No admin create/edit/delete, vehicle/document-content/location/bank edit or financial/trip write route. Approval changes document verification metadata (§6.1). Pending important-change requests stay read-only; approval/rejection/apply semantics remain TD-44.

## 6. Actions (non-CRUD)

All five POST actions use the shared transactional account-review service; approval includes qualifying document verification. A row lock serializes concurrent decisions. In one transaction, each updates driver status/reason, records the review event with source/destination/actor/note, writes the audit and creates the driver-app inbox notice. Audit/inbox failure rolls back everything.

### 6.1 Approve

`POST /drivers/{id}/approve` · admin.drivers.approve. Empty body; reason/note prohibited. Only pending_review → approved.

Requires submitted_at and all of **national_id (also iqama), driving_license, vehicle_registration**, each with attached file media, status pending or verified and expiry null or today/later UTC. Expired/rejected/replaced/wrong-role documents and metadata without file do not qualify. DRIV-002/003 also require at least one saved vehicle belonging to this driver with is_active=true; inactive-only vehicles do not qualify. The 422 BUSINESS_RULE_VIOLATED message uses localized submission/document-type/Active vehicle labels.

Approval records approved_at/approved_by and clears status_reason. All required-type, correct-role, pending-or-verified, non-expired documents with attached files used by the check are locked. Qualifying pending rows become verified with reviewed_by/reviewed_at; already verified rows retain their original attribution. Non-required, rejected/expired/replaced, wrong-role, past-expiry and missing-file rows remain untouched. All qualifying duplicates are included; no newest-only rule is assumed. The event reason summarizes qualifying/newly-verified counts. The profile audit links review_event_id, qualifying_document_ids and verified_document_ids (UUIDs), and each newly verified document has its own status-change audit. These writes, driver approval, history and notice share one transaction (TD-50).

Approval does not fabricate consents, create a vehicle or go online. Reactivation restores an earlier approval without rerunning these initial prerequisites (TD-52). Other onboarding/dispatch completeness checks belong to their features.

```json
{
  "status": "success",
  "message": "Data updated successfully",
  "data": {
    "id": "01a16266-7db8-702a-b270-2c0a77c22af0",
    "account_status": {
      "value": "approved",
      "label": "Approved"
    },
    "status_reason": null,
    "approved_at": "2026-10-22T06:57:26+00:00"
  }
}
```

### 6.2 Reject

`POST /drivers/{id}/reject` · admin.drivers.reject. pending_review → rejected. Required nonblank reason string max500; saved as status_reason and event note. note prohibited.

```json
{
  "status": "success",
  "message": "Data updated successfully",
  "data": {
    "id": "01a16266-7ec1-70d1-ab28-84762ae17f91",
    "account_status": {
      "value": "rejected",
      "label": "Rejected"
    },
    "status_reason": "Please correct the documents",
    "approved_at": null
  }
}
```

### 6.3 Request changes

`POST /drivers/{id}/request-changes` · admin.drivers.request-changes. pending_review → needs_changes. Same reason validation as reject. This concerns registration, not profile_change_requests.

```json
{
  "status": "success",
  "message": "Data updated successfully",
  "data": {
    "id": "01a16266-7fbb-72d4-8ee4-c4d86f8f99af",
    "account_status": {
      "value": "needs_changes",
      "label": "Changes required"
    },
    "status_reason": "Please correct the documents",
    "approved_at": null
  }
}
```

### 6.4 Suspend

`POST /drivers/{id}/suspend` · admin.drivers.suspend. approved → suspended. Same required reason max500; note prohibited. Profile-level only; no automatic trip cancellation, payout change or account-wide block. is_online remains the saved flag; it does not override account approval.

```json
{
  "status": "success",
  "message": "Data updated successfully",
  "data": {
    "id": "01a16266-80b8-72f8-8e82-8c289f867ecf",
    "account_status": {
      "value": "suspended",
      "label": "Suspended"
    },
    "status_reason": "Please correct the documents",
    "approved_at": null
  }
}
```

### 6.5 Reactivate

`POST /drivers/{id}/reactivate` · admin.drivers.reactivate. suspended → approved. Optional nullable note string max500; reason prohibited. Clears status_reason, preserves original approved_at/approved_by, writes reactivated event/note. Initial approval checks do not run again for this restoration transition.

```json
{
  "status": "success",
  "message": "Data updated successfully",
  "data": {
    "id": "01a16266-81b7-7051-8bc2-c37d4f0226c9",
    "account_status": {
      "value": "approved",
      "label": "Approved"
    },
    "status_reason": null,
    "approved_at": null
  }
}
```

### 6.6 Identity review

`GET /drivers/{id}/identity` · **admin.drivers.identity**, drivers · **approve**. Returns full saved national_id/iqama (or null) and `documents: [{id,type,doc_number}]`. Each driver-role document has its UUID, localized type and full saved number or null: driving licence, vehicle registration, identity and other saved documents. Newest first with ID as the tie-breaker; no documents gives `[]`. Provider-role documents are excluded. IBAN is never returned. Vehicle plates are already public in the normal list/details responses (TD-51).

Each successful call audits drivers.identity.viewed with actor, target UUID, field names (`national_id`, `documents.doc_number`), document UUIDs and request context; never full identity or document-number values. Audit failure prevents reveal. Response Cache-Control is no-store, private. Fetch only when opening the review panel; clear on navigation and do not persist in browser storage.

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "01a16266-9a03-706a-a5bb-710b5eacc80f",
    "national_id": "1234567890",
    "documents": [
      {
        "id": "01a16266-9a07-724e-b27c-ab36a07b33c7",
        "type": {
          "value": "driving_license",
          "label": "Driving license"
        },
        "doc_number": "FULL-DOCUMENT-12345"
      }
    ]
  }
}
```

### Shared errors and action handling

| HTTP | data.error_code | Trigger | Dashboard behavior |
|------|-----------------|---------|--------------------|
| 401 | UNAUTHENTICATED | Missing/invalid/expired admin token | Shared refresh/login flow |
| 403 | PERMISSION_DENIED | Missing exact route grant / wrong app token | Deny page/action; view cannot reveal identity |
| 403 | ACCOUNT_INACTIVE | Disabled/banned operator | Stop protected requests |
| 404 | absent, data:null | Unknown/integer/users UUID/wrong-profile/admin/deleted route identity | Show unavailable |
| 409 | INVALID_STATE_TRANSITION | Action outside §7 | Refetch state/history; message uses localized action/status labels |
| 422 | BUSINESS_RULE_VIOLATED | Missing submission/current attached document/active vehicle on approve | Show named prerequisites; request correction |
| 422 | absent (`data.errors`) | Bad filters/sorts/city UUID/reason/note/pagination | Mark the named field from `data.errors`; fall back to the localized message (TD-02) |
| 405 | absent | Unsupported admin create/edit/delete | No such controls |
| 500 | shared server-error contract | Audit/inbox/database failure | Do not show success; refetch before retry |

Disable repeated submissions. After an uncertain timeout, refetch: replaying a completed action returns 409 without another event/notice. No Idempotency-Key required. Only the user app can resubmit incomplete/needs_changes/rejected accounts; no admin resubmit route.

```json
{
  "status": "fail",
  "message": "Approval requires: Account submission, National ID / iqama, Driving license, Vehicle registration, Active vehicle.",
  "data": {
    "error_code": "BUSINESS_RULE_VIOLATED",
    "context": {
      "violations": [
        "Approval requires: Account submission, National ID / iqama, Driving license, Vehicle registration, Active vehicle."
      ]
    },
    "correlation_id": "5f17ca5d-ef83-4aa0-b558-5df19c0979bd"
  }
}
```

The same prerequisite failure with `Accept-Language: ar`:

```json
{
  "status": "fail",
  "message": "الموافقة تتطلب: تقديم الحساب للمراجعة, هوية / إقامة, رخصة قيادة, استمارة المركبة, مركبة نشطة.",
  "data": {
    "error_code": "BUSINESS_RULE_VIOLATED",
    "context": {
      "violations": [
        "الموافقة تتطلب: تقديم الحساب للمراجعة, هوية / إقامة, رخصة قيادة, استمارة المركبة, مركبة نشطة."
      ]
    },
    "correlation_id": "3f970255-6c74-44da-af04-21159dae2b62"
  }
}
```

A submitted driver with all required documents but no active vehicle receives:

```json
{
  "status": "fail",
  "message": "Approval requires: Active vehicle.",
  "data": {
    "error_code": "BUSINESS_RULE_VIOLATED",
    "context": {
      "violations": [
        "Approval requires: Active vehicle."
      ]
    },
    "correlation_id": "5c64364c-9237-4d4e-84e6-4d1764eb9af0"
  }
}
```

```json
{
  "status": "fail",
  "message": "الموافقة تتطلب: مركبة نشطة.",
  "data": {
    "error_code": "BUSINESS_RULE_VIOLATED",
    "context": {
      "violations": [
        "الموافقة تتطلب: مركبة نشطة."
      ]
    },
    "correlation_id": "bce73860-def9-41fd-bf7d-6f3627d8b0e9"
  }
}
```

```json
{
  "status": "fail",
  "message": "Cannot Approve from Suspended.",
  "data": {
    "error_code": "INVALID_STATE_TRANSITION",
    "correlation_id": "1a0d3c36-6fd6-4277-b332-fea75c4877e1"
  }
}
```

```json
{
  "status": "fail",
  "message": "The filters.city_id field must be a valid UUID.",
  "data": {
    "errors": {
      "filters.city_id": [
        "The filters.city_id field must be a valid UUID."
      ]
    }
  }
}
```

```json
{
  "status": "fail",
  "message": "The selected sort.completed_trips_count is invalid.",
  "data": {
    "errors": {
      "sort.completed_trips_count": [
        "The selected sort.completed_trips_count is invalid."
      ]
    }
  }
}
```

### 6.7 Set city and coverage

`PUT /drivers/{id}/work-area` · **admin.drivers.work-area**, drivers · **update**. ADMIN-021: the admin links the driver to her city and coverage areas (TD-83). The account owner can also set them from her own app (`profile/work-area`); the admin may do it **in any account status**, because she is the reviewer — the owner's "changing a saved value after approval needs review" block (BR-04, TD-76) does not apply to the admin. The change is audited (action `drivers.work-area.updated`, field names only) and takes effect at once for eligibility (BR-08) and for which trip requests she is offered. The owner is not notified in-app (limitation, TD-83).

| Field | Type | Validation | Description |
|-------|------|------------|-------------|
| `city_id` | uuid | optional, enabled city | Omit to keep the saved city |
| `district_ids` | uuid[] | optional | Full list; replaces the saved one, `[]` clears it; each enabled and in the resulting city |

Success returns the saved work area (same body as her own `GET /profile/work-area`; `locked_fields` lists what **she** can no longer change herself). Postman: `admin.drivers.work-area` › "200 Driver Work Area Set By Admin".

```json
{
  "status": "success",
  "message": "Data updated successfully",
  "data": {
    "city": {
      "id": "01a17247-37bf-721a-bbf1-b0fc9a1a3087",
      "name": "Sample-vkjy79831042"
    },
    "districts": [
      {
        "id": "01a17247-37c3-730f-bafe-f2a9fb3a3acb",
        "name": "Sample-qzyp38409991"
      }
    ],
    "locked_fields": [
      "city_id",
      "district_ids"
    ]
  }
}
```

| HTTP | `error_code` | When | Dashboard behavior |
|------|--------------|------|--------------------|
| 403 | `PERMISSION_DENIED` | Missing `admin.drivers.work-area` | Hide the editor |
| 404 | — | Unknown, integer or wrong-type id; administrator or deleted account | Toast, reload |
| 422 | `BUSINESS_RULE_VIOLATED` | A district does not belong to the resulting city | Toast `message` |
| 422 | `SERVICE_AREA_UNAVAILABLE` | The city or a district is disabled | Toast `message` |
| 422 | — | Shape: bad uuid, `base_lat` without `base_lng`, radius out of range → `data.errors` | Mark the fields |

## 7. Statuses and enums

| account_status value | Label en / ar | Badge | Admin actions |
|----------------------|---------------|-------|---------------|
| incomplete | Incomplete / غير مكتمل | neutral | None |
| pending_review | Pending review / قيد المراجعة | amber | Approve/reject/request-changes |
| needs_changes | Changes required / تعديلات مطلوبة | orange | None, user resubmits |
| approved | Approved / معتمدة | green | Suspend |
| rejected | Rejected / مرفوضة | red | None, user resubmission policy DRIV-003 |
| suspended | Suspended / موقوفة | red | Reactivate |

Enum objects localize with Accept-Language; branch on value. Vehicle types: car / Car / سيارة, motorbike / Motorbike / دراجة نارية. Document statuses: pending, verified, rejected, expired, replaced. Payout statuses: pending, verified, rejected, inactive. Review events: submitted, approved, rejected, needs_changes, suspended, reactivated. Change types: profile, work_area, identity, bank_account, vehicle, document, new_category; pending metadata only. Booleans remain booleans.

## Data model

| Tables | Read / write | Notes |
|--------|--------------|-------|
| users, drivers | Read; driver review/approval fields write | No users.is_active write; UUID-only public identity |
| vehicles, driver_districts, governorates/regions/translations | Read only | Actual vehicle/coverage data |
| user_documents | Read + qualifying approval verification | Driver role; only status/reviewed_by/reviewed_at change on qualifying pending rows (TD-50) |
| payout_accounts, profile_change_requests, media | Read only | Driver role scope, IBAN masked, request payload omitted |
| account_review_events | Read + insert | Existing nullable from/to ENUMs, actor/reason preserved |
| audit_logs, notifications | Insert | Same review transaction; identity reads audit only |

No new migration/schema change. Requires the previously shipped additive transition-status migration (TD-43). No trip/earning/payout aggregates or fabricated amounts are queried.

## 8. TypeScript types

Use shared ApiResponse, Paginated, EnumValue and Money helpers; date-times are ISO 8601 (`new Date(value)`). Money fields are intentionally absent from this release's driver profile.

```ts
export type Uuid = string;
export type IsoDateTime = string; // ISO 8601 with offset, e.g. 2026-10-22T06:57:15+00:00
export type AccountStatus = 'incomplete' | 'pending_review' | 'needs_changes' | 'approved' | 'rejected' | 'suspended';
export type ReviewAction = 'submitted' | 'approved' | 'rejected' | 'needs_changes' | 'suspended' | 'reactivated';
export type DocumentType = 'national_id' | 'professional_certificate' | 'health_certificate' | 'security_check' | 'commercial_register' | 'business_license' | 'staff_health_certificates' | 'driving_license' | 'vehicle_registration' | 'other';
export type ChangeType = 'profile' | 'work_area' | 'identity' | 'bank_account' | 'vehicle' | 'document' | 'new_category';
export interface GeoSummary { id: Uuid; name: string | null }
export interface VehicleSummary {
  id: Uuid; type: EnumValue<'car' | 'motorbike'>; make_model: string;
  model_year: number | null; color: string | null; plate_number: string; is_active: boolean; // plate_number is public, unmasked
}
export interface Driver {
  id: Uuid; full_name: string; phone_code: string | null; phone: string | null;
  nationality: string | null; city: GeoSummary | null; districts_count: number;
  vehicle: VehicleSummary | null; account_status: EnumValue<AccountStatus>; is_online: boolean;
  location_updated_at: IsoDateTime | null; rating_avg: string | null; rating_count: number;
  completed_trips_count: number; submitted_at: IsoDateTime | null; created_at: IsoDateTime;
}
export interface IdentityDocument { id: Uuid; type: EnumValue<DocumentType>; doc_number: string | null } // full, identity endpoint only
export interface DriverIdentity { id: Uuid; national_id: string | null; documents: IdentityDocument[] }
export interface ReviewEvent {
  id: Uuid; action: EnumValue<ReviewAction>; from_status: EnumValue<AccountStatus> | null;
  to_status: EnumValue<AccountStatus> | null; reason: string | null;
  actor: { id: Uuid; full_name: string } | null; created_at: IsoDateTime;
}
export interface DriverDetails extends Driver {
  user_id: Uuid; birth_date: string | null; national_id: string | null; // masked
  status_reason: string | null;
  consents: { female_declared_at: IsoDateTime | null; terms_accepted_at: IsoDateTime | null; privacy_accepted_at: IsoDateTime | null };
  approved_at: IsoDateTime | null; approved_by: Uuid | null; image: string | null;
  districts: GeoSummary[]; vehicles: VehicleSummary[];
  current_location: { lat: number | null; lng: number | null; updated_at: IsoDateTime | null } | null;
  documents: { id: Uuid; type: EnumValue<DocumentType>; status: EnumValue<'pending' | 'verified' | 'rejected' | 'expired' | 'replaced'>; doc_number: string | null; expires_at: string | null; file_url: string | null }[];
  payout_account: { id: Uuid; holder_name: string; bank_name: string; iban: string; status: EnumValue<'pending' | 'verified' | 'rejected' | 'inactive'> } | null;
  change_requests: { id: Uuid; change_type: EnumValue<ChangeType>; status: EnumValue<'pending'>; created_at: IsoDateTime }[];
  review_history: ReviewEvent[]; updated_at: IsoDateTime;
}
export type DriverList = Paginated<'drivers', Driver>;
export interface DriverListQuery {
  search?: string | null; page?: number; per_page?: number; paginate?: 'true' | 'false' | '1' | '0';
  filters?: { notification_reachable?: 0 | 1; account_status?: AccountStatus; city_id?: Uuid; is_online?: 0 | 1; created_from?: string; created_to?: string; submitted_from?: string; submitted_to?: string };
  sort?: Partial<Record<'created_at' | 'submitted_at' | 'rating_avg' | 'completed_trips_count', 'asc' | 'desc'>>;
}
export interface DriverStatus { id: Uuid; account_status: EnumValue<AccountStatus>; status_reason: string | null; approved_at: IsoDateTime | null }
export type AdverseReviewBody = { reason: string }; // nonblank max500
export type ReactivateBody = { note?: string | null }; // max500
// GET /drivers: ApiResponse<DriverList>; paginate=false: ApiResponse<Driver[]>
// POST /drivers/{id}/reject: ApiResponse<DriverStatus>, body {reason}
// GET /drivers/{id}/identity: ApiResponse<DriverIdentity>, explicit review-panel interaction
```

## 9. Business rules enforced by the backend

| Rule | Enforcement | Dashboard consequence |
|------|-------------|-----------------------|
| BR-01 | Shows saved female/terms/privacy timestamps | Never invent acceptance evidence |
| BR-03 / TD-35 | Shared explicit review transitions and approval prerequisites | Show only valid buttons; unapproved profile has no operational approval |
| BR-04 / TD-44 / TD-184 | Important changes retain old data and stay pending until approval | Decided in [profile-change-requests.md](profile-change-requests.md): approve exactly as submitted, or reject with a reason |
| BR-29 / TD-27/28/45/51 | Exact route authorization, dedicated audited identity/document-number read under approve | View is insufficient for identity; plates are public, IBAN never revealed |
| TD-50/52 | Initial approval verifies qualifying documents and requires an active vehicle | Refetch document statuses/history; show localized prerequisites |
| TD-19/03/47 | UUIDs, ISO 8601 date-times, concrete validation paths/localized messages |
| TD-45/53 | National ID/iqama is never searchable | Search matches name, phone and plate only; compare numbers through the audited identity panel | Correct filter input; preserve route identity boundary |
| TD-36/43/48 | Profile-only suspension, atomic history/audit/inbox | Other apps unaffected; confirmed-trip/financial effects deferred |

## 10. Notifications triggered

| Event catalog key | Recipient / app | Channel | Payload |
|-------------------|-----------------|---------|---------|
| account_review_status_changed | Reviewed user / driver | Existing shared in-app inbox | profile_id UUID, account_status, action, reason/null |

Every successful state change creates one notice inside the review transaction. Stored title/body use the recipient's en/ar locale (Arabic fallback), independently of push preferences. Use a delivery/driver-app JWT for `/api/v1/notifications`; provider/customer inboxes cannot see it. Push/SMS/email/template/preferences remain ADMIN-020; ADMIN-007 will reuse the review service.

## 11. Integration checklist

- [ ] Gate sidebar/details/each action by exact route; identity uses drivers approve permission.
- [ ] Use drivers.uuid paths and city UUID filters; treat bad city as 422, bad path identity as 404.
- [ ] Wire name/phone/plate search (no ID search), nested filters/sorts/page and 500-item dropdown cap.
- [ ] Render enum labels/badges; saved is_online is not operational eligibility.
- [ ] Preserve nullable vehicle/rating/location/expiry/consent/media values; show actual counters only.
- [ ] Mask default identity/document numbers and every IBAN; reveal document numbers only in the audited identity panel. Show full saved vehicle plates in list/details.
- [ ] Show current coordinates only when current_location is present; distinguish stale timestamps.
- [ ] Keep vehicles/documents/coverage/payout read-only here; change requests are decided through [profile-change-requests.md](profile-change-requests.md); no trip/money/chat placeholders.
- [ ] Validate reasons and optional reactivation note, max500; prevent repeated submissions.
- [ ] Refetch document statuses/history after approval and after timeouts/409; handle 401/403/404/422/500 with shared behavior.
- [ ] Parse ISO 8601 date-times (with offset), calendar birth/expiry unchanged; check Arabic labels/RTL.
- [ ] Sync RBAC and run opt-in dev fixture seeder for manual QA; add no dev seeder to tests.

## 12. Acceptance scenarios

Version 1.1.0 verification on MySQL `joshen_testing`: **603 tests / 5,154 assertions passed** (236.94 seconds), including all five schema lifecycle tests. Focused provider/driver HTTP run: **81 tests / 1,014 assertions passed**. RBAC synchronized locally: **89 routes, 0 changed, 0 removed**; Postman regenerated: **145 routes**. Pint, captured JSON, all 13 guide sections, documentation file links and diff checks passed. This is local backend verification; deployed/browser business-cycle acceptance is not claimed.

| AC ID | Scenario | Evidence / status |
|-------|----------|-------------------|
| AC-ADMIN-006 / BR-29 | Every route 200, 401, 403 and exact-route grant | endpoint_authentication_and_exact_route_permissions |
| AC-ADMIN-006 / TD-19 | Unknown/integer/users/customer/provider-only/admin/deleted identities | uuid_and_profile_scope, 404 and no writes |
| AC-ADMIN-006 | Name/phone/plate search (identity numbers not searchable, TD-54), filters/sorts/pagination | list_search_filters_sort_and_pagination; dropdown_is_capped_at_500 |
| AC-ADMIN-006 | Actual driver-owned details, masked fields and offline location | details_and_masking; provider-role data excluded |
| AC-ADMIN-006 / TD-45/51 | Audited full identity/document-number read and fail-closed audit failure | identity_read_is_audited_and_not_granted_by_view; identity_is_not_revealed_when_auditing_fails |
| AC-ADMIN-006 / TD-35 | All five legal actions; representative illegal transitions | review_cycle_is_profile_scoped_and_audited; reject_and_request_changes; illegal_transition_returns_409 |
| AC-ADMIN-006 / TD-50 | Only qualifying pending documents verified; reviewer/time retained on previous verification | approval_verifies_only_qualifying_documents; missing_prerequisites_are_localized |
| AC-ADMIN-006 / DRIV-002/003 / TD-52 | Active vehicle required for initial approval; absent/inactive fail in en/ar | approve_requires_an_active_vehicle; no-vehicle manual fixture |
| AC-ADMIN-006 / AC-ADMIN-007 | Submission + identity/licence/registration approval prerequisites | approval_prerequisites, expiry/status/role/attachment failures and today-valid success |
| AC-ADMIN-006 | Atomic review event/audit/inbox rollback | atomic_rollback_when_audit_write_fails; atomic_rollback_when_inbox_write_fails |
| AC-ADMIN-006 / TD-47 | Bad city/sort paths, reason/note validation, bilingual messages | invalid_queries_and_reasons; localized_transition_and_concrete_sort_errors |
| AC-ADMIN-006 | No create/edit/delete | no_profile_mutation_crud |
| AC-ADMIN-006 / BR-04 | Important-change review | **Covered (TD-184):** list, approve, reject in [profile-change-requests.md](profile-change-requests.md) |
| AC-ADMIN-006 | Trips/amounts/commission/net earnings | **Not on this screen (§13).** They are shown in their own sections: trips in [orders](orders.md) (trip summary) and [transportation-monitoring](transportation-monitoring.md), amounts and net earnings in [payouts](payouts.md) and [commissions](commissions.md), ratings in [ratings](ratings.md) |
| AC-ADMIN-007 | Combined registration-review screen | **Ready:** [registration-review.md](registration-review.md) delegates to this review |

Test names have prefix test_ac_admin_006_ in tests/Feature/Api/V1/Admin/DriverManagementTest.php. Local tests/fixtures are not deployed/browser business-cycle acceptance.

## 13. Open decisions and known limitations

| ID | Impact | Current behavior |
|----|--------|------------------|
| TD-44 / DEC-25 / ADMIN-006 | Allowed fields, validation, stale check and apply | **Resolved (TD-184, DEC-25 resolved 2026-11-10):** see [profile-change-requests.md](profile-change-requests.md) |
| TD-36 | Confirmed trips when driver is suspended | Approval gate changes immediately; trip execution/cancellation/reassignment/refund handling deferred to transport |
| ADMIN-006 / Phase 3 payouts | Amounts due, gross/net earnings, platform commission and payout history | Omitted, no fabricated zero/amounts. Only existing rating/completed_trips_count columns read |
| ADMIN-006 / transportation | Current/past trip detail and dispatch eligibility | No trip/dispatch screen or query. is_online is a saved flag; location only online, last update can be stale |
| ADMIN-007 | Combined provider/driver registration review | Built (TD-85); suspend / reactivate stay on this page |
| ADMIN-020 | External notifications/templates | Only actual in-app inbox rows implemented |
| DRIV-003 / TD-35 | User resubmission policy/settings | Admin cannot move incomplete/needs_changes/rejected to review; user onboarding owns resubmission |
| TD-50 | Earlier approvals did not verify document rows | New approvals verify qualifying documents; no historical reviews or document verification metadata are backfilled |
| TD-43 | Historical event states | Existing rows may have null from/to; no invented backfill |
| TD-47 | Old UserSeeder bypass | New default seeds are incomplete. Existing legacy/live-reviewed rows preserved. No app registration auto-approval found |
| TD-40 | Deployment of UTC database sessions | Values are UTC and now emitted with `+00:00` (2.0.0); old provenance caveat remains; no historical time rewrite |

### Local manual review fixtures (TD-47/48)

Opt-in command: `php artisan db:seed --class=AdminReviewDevSeeder --no-interaction`. Runs only for APP_ENV=local/dev, is absent from DatabaseSeeder/tests, and preserves existing profiles/admin decisions on rerun. No approval is seeded. Each has submitted_at, consent timestamps, a submitted event and attached valid synthetic PDFs with future expiry. Payout fixtures contain synthetic IBANs, always masked in API.

| Label / phone (+966) | State | Documents | Expected manual test |
|----------------------|-------|-----------|----------------------|
| DEV-REVIEW Driver Car / 500009201 | pending_review | national_id, driving_license, vehicle_registration | Approve succeeds |
| DEV-REVIEW Driver Motorbike / 500009202 | pending_review | Same complete set | Approve succeeds |
| DEV-REVIEW Driver Missing document / 500009203 | pending_review | Missing driving_license | Approve →422 names Driving license / رخصة قيادة; correction/rejection allowed |
| DEV-REVIEW Driver No active vehicle / 500009204 | pending_review | All three documents; no vehicle | Approve →422 names Active vehicle / مركبة نشطة; documents remain pending |

The table describes initial seed states; existing manual decisions are preserved on rerun. Car/motorbike vehicles are actual saved fixtures; no real trip/earning data. Documents are visibly development-only, not real identity evidence. Existing temporary OTP policy TD-24 applies. Provider scenarios are documented in [providers.md](providers.md#local-manual-review-fixtures-td-47).
