---
feature: Content — FAQs and static pages (base endpoints)
slug: content
surface: admin
consumer: react
version: 0.3.2
status: base (TD-32)
brd_version: "1.3"
brd_ids: [CUST-002]
business_rules: [BR-01, BR-29]
decisions: [DEC-17, DEC-18, TD-02, TD-03, TD-12, TD-27, TD-30, TD-32]
schema_tables: [faqs, faq_translations, static_pages, static_page_translations, onboarding_slides]
related_docs: [banners.md, ../client/auth.md, ../provider/auth.md, ../delivery/auth.md]
last_updated: 2026-11-11
---

# Content (FAQs, static pages) — 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)

> **Base endpoints (TD-32).** The FAQ and static-page CRUD comes from the base
> project and only partly matches the schema tables. This doc records what works
> and what does not today. **Static pages now follow the schema (0.2.0)**: create,
> edit and read use `slug`, `audience` and `body`. FAQs are still base. The
> content feature finishes the FAQs and moves this doc to `1.0.0`.

## Version history

Newest first. Never edit an old row.

| Version | Date | Type | Summary | Dashboard action required |
|---------|------|------|---------|---------------------------|
| 0.3.2 | 2026-11-11 | PATCH | Enum `label` values on the admin FAQ, banner (target app, placement, link type), static page (slug, audience) and onboarding-slide (app) resources are now translated to the request locale (Arabic by default) instead of the English title-cased case name; `value` is unchanged and English responses are identical (QA 2026-11-11). | None; render `label` as before. |
| 0.3.1 | 2026-11-08 | PATCH | Postman examples for FAQs and static pages; every FAQ body key described. | None. |
| 0.3.0 | 2026-11-08 | MINOR | Onboarding slides management: `/onboarding-slides` list, show, create, edit and delete (TD-176). | Add the Content → Intro slides page (§5.3); show it only to admins with `admin.onboarding-slides.index`. |
| 0.2.0 | 2026-10-25 | Minor (pages: breaking, provisional 0.x) | Static pages rebuilt on the `static_pages` schema: request `slug` + `audience` + `ar`/`en` `title` + `body`; responses return `slug`, `audience` (enum objects), `version`, `updated_at`, details add `body` and `updated_by`. `POST /pages` no longer returns 500 and page text is editable; each edit bumps `version`. | Build the static-pages screen against §3.2, §4.2, §5.2. Stop sending `type`, `content`, `is_active`, `image`. |
| 0.1.1 | 2026-10-22 | Patch | `GET /faqs` and `GET /pages` no longer return 500 when `sort[...]` is omitted: the default order is newest first by `id` (these tables have no `created_at`). | None. Sending an explicit `sort[...]` still works and is still recommended for a stable column order. |
| 0.1.0 | 2026-10-21 | Initial (base) | Documents the existing `/faqs` and `/pages` endpoints with real responses and their defects. | FAQs: usable with an explicit `sort` (§3.1). Static pages: wait for 1.0.0. |

## Changelog

### 0.3.2 — 2026-11-11

- **Fixed:** Enum `label` values on the admin FAQ, banner (target app, placement, link type), static page (slug, audience) and onboarding-slide (app) resources are now translated to the request locale (Arabic by default) instead of the English title-cased case name; `value` is unchanged and English responses are identical (QA 2026-11-11).
- **Breaking:** No. **App action required:** None; render `label` as before.

### 0.3.1 — 2026-11-08

- **Changed:** Postman examples for FAQs and static pages; every FAQ body key described.
- **Breaking:** No. **App action required:** None.

### 0.3.0 — 2026-11-08

- **Added:** `GET/POST /onboarding-slides`, `GET/PUT/DELETE /onboarding-slides/{id}` under section `banners_content`. The customer intro list now follows what the admin saves. The "no endpoint yet" lines are removed.
- **Breaking:** No. **App action required:** Add the Content → Intro slides page (§5.3); show it only to admins with `admin.onboarding-slides.index`.

### 0.2.0 — 2026-10-25
- **Added:** `slug` and `audience` (create; update optional) with a unique `(slug, audience)` check (422); `version` and `updated_by` in responses; `ar.title`/`en.title` max 200 and `ar.body`/`en.body`; English optional (TD-12), `en.title` and `en.body` go together.
- **Changed:** page list returns `{id, slug, audience, title, version, updated_at}`; details add `body`, `updated_by` and the `en`/`ar` objects. Every edit sets `updated_by` to the admin and increases `version` (schema: "bump when any locale changes").
- **Fixed:** `POST /pages` returned 500 `Duplicate entry 'terms-all'` because the request validated `type`/`content` while the table has `slug`/`audience`/`body`; the insert carried no columns, so the unique key hit the first `slug`/`audience` defaults. Text edits through `content` were ignored.
- **Removed / Deprecated:** `type`, `image`, `is_active`, `created_at`, `content` on pages (no such columns).
- **Breaking:** Yes for pages (the old payload was already non-functional; 0.x is provisional)
- **Endpoints affected:** `GET|POST /pages`, `GET|PUT /pages/{id}`
- **Dashboard action required:** Build the page form with the fields in §5.2.
- **BRD / decisions:** CUST-002, BR-01, DEC-18, TD-12, TD-32

### 0.1.1 — 2026-10-22
- **Added:** —
- **Changed:** —
- **Fixed:** the default list order. `faqs` and `static_pages` have no `created_at`, so the default `latest()` ordered by a missing column and returned 500 (`Unknown column 'created_at'`). The default is now `id` descending for tables without a creation timestamp (`ApplySorting`); the duplicate `latest()` in `BaseCRUDService::index` is removed. Applies to every admin list built on `BaseCRUDService`; only these two entities were affected.
- **Removed / Deprecated:** —
- **Breaking:** No
- **Endpoints affected:** `GET /faqs`, `GET /pages`
- **Dashboard action required:** None.
- **BRD / decisions:** CUST-002, TD-32

### 0.1.0 — 2026-10-21
- **Added:** documentation for `GET|POST /faqs`, `GET|PUT|DELETE /faqs/{id}`, `GET|POST /pages`, `GET|PUT|DELETE /pages/{id}`. No code change.
- **Changed:** —
- **Fixed:** —
- **Removed / Deprecated:** —
- **Breaking:** No
- **Endpoints affected:** the ten above
- **Dashboard action required:** always send `sort[...]` on the two lists; do not ship the static-page form.
- **BRD / decisions:** CUST-002 (consent pages), DEC-18, TD-12, TD-30, TD-32

---

## 1. Business summary

FAQs are question/answer pairs shown in an app's help screen; each FAQ belongs
to one audience (`customer`, `provider`, `driver`). Static pages hold the
policies users accept at registration — terms, privacy, cancellation policy,
about, and the female declaration (BR-01) — one page per `slug` and `audience`.
The registration consent screens link to them, so working static pages are
needed before launch (DEC-18). Text is translated: Arabic and English (TD-12).
Onboarding slides (the customer app's first-launch intro, SCR-08) are managed here too since 0.3.0 (TD-176): §5.3.

## 2. Pages and permissions

| Page / component | Purpose | Endpoint(s) | RBAC permission (route name) |
|------------------|---------|-------------|------------------------------|
| FaqsListPage | Table of FAQs | `GET /faqs` | `admin.faqs.index` |
| FaqForm (edit) | Load one FAQ | `GET /faqs/{id}` | `admin.faqs.show` |
| FaqForm (create) | Add an FAQ | `POST /faqs` | `admin.faqs.store` |
| FaqForm (save) | Edit an FAQ | `PUT /faqs/{id}` | `admin.faqs.update` |
| DeleteFaqDialog | Delete | `DELETE /faqs/{id}` | `admin.faqs.destroy` |
| StaticPagesListPage | Table of pages | `GET /pages` | `admin.pages.index` |
| StaticPageForm | Load / create / edit | `GET /pages/{id}`, `POST /pages`, `PUT /pages/{id}` | `admin.pages.show`, `admin.pages.store`, `admin.pages.update` |
| DeletePageDialog | Delete | `DELETE /pages/{id}` | `admin.pages.destroy` |
| OnboardingSlidesListPage | Table of intro slides | `GET /onboarding-slides` | `admin.onboarding-slides.index` |
| OnboardingSlideForm (edit) | Load one slide | `GET /onboarding-slides/{id}` | `admin.onboarding-slides.show` |
| OnboardingSlideForm (create) | Add a slide | `POST /onboarding-slides` | `admin.onboarding-slides.store` |
| OnboardingSlideForm (save) | Edit a slide | `PUT /onboarding-slides/{id}` | `admin.onboarding-slides.update` |
| DeleteOnboardingSlideDialog | Delete | `DELETE /onboarding-slides/{id}` | `admin.onboarding-slides.destroy` |

Section `banners_content` (TD-27): `index`/`show` → view, `store` → create,
`update` → update, `destroy` → delete. Sidebar entry: **Content → FAQs**,
**Content → Pages**, **Content → Intro slides**.

## 3. List / table

### 3.1 FAQs — `GET /api/v1/admin/faqs` — list key `faqs`

Without a `sort` the list is ordered newest first by `id` (the `faqs` table has
no `created_at`). Send an explicit `sort[...]` for any other order.

| Column | Field | Sortable (`sort[...]`) | Filter (`filters[...]`) | Notes |
|--------|-------|------------------------|-------------------------|-------|
| ID | `id` (uuid) | no | — | |
| Question | `question` | yes | `filters[question]` | Request locale |
| Answer | `answer` | yes | `filters[answer]` | |
| Active | `is_active` | yes | `filters[is_active]=1` | |
| Audience | — (not returned) | `sort[audience]` | `filters[audience]=customer` | See §13 |
| Order | — (not returned) | `sort[sort_order]` | `filters[sort_order]` | Recommended default sort |

`search` matches: question, answer (translations), audience, sort_order, is_active.

`GET /faqs?sort[sort_order]=asc&filters[audience]=customer`:

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "faqs": [
      {
        "id": "01a15dd8-ff5c-7010-8775-5133668c3a91",
        "type": null,
        "question": "How do I book?",
        "answer": "Pick a service, then a provider.",
        "is_active": true,
        "created_at": null
      }
    ],
    "links": {
      "first": "http://localhost/api/v1/admin/faqs?page=1",
      "last": "http://localhost/api/v1/admin/faqs?page=1",
      "prev": null,
      "next": null
    },
    "meta": {
      "current_page": 1,
      "from": 1,
      "last_page": 1,
      "path": "http://localhost/api/v1/admin/faqs",
      "per_page": 15,
      "to": 1,
      "total": 1
    }
  }
}
```

### 3.2 Static pages — `GET /api/v1/admin/pages` — list key `static_pages`

Same default (newest first by `id`); send `sort[slug]=asc` (or another field below) for any other order.

Sortable / filterable / searchable: `slug`, `audience`, `version`,
`updated_by`, `updated_at`, `title`, `body`.

`GET /pages?sort[slug]=asc`:

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "static_pages": [
      {
        "id": "01a15dd8-ff95-72c5-a62f-37f42040c725",
        "slug": {"value": "terms", "label": "Terms"},
        "audience": {"value": "all", "label": "All"},
        "title": "Terms and conditions",
        "version": 1,
        "updated_at": "2026-10-25 06:58:37"
      }
    ],
    "links": {
      "first": "http://localhost/api/v1/admin/pages?page=1",
      "last": "http://localhost/api/v1/admin/pages?page=1",
      "prev": null,
      "next": null
    },
    "meta": {
      "current_page": 1,
      "from": 1,
      "last_page": 1,
      "path": "http://localhost/api/v1/admin/pages",
      "per_page": 15,
      "to": 1,
      "total": 1
    }
  }
}
```

## 4. Details view

### 4.1 FAQ — `GET /api/v1/admin/faqs/{id}`

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "01a15dd8-ff5c-7010-8775-5133668c3a91",
    "type": null,
    "question": "How do I book?",
    "answer": "Pick a service, then a provider.",
    "is_active": true,
    "created_at": null,
    "updated_at": null,
    "en": {
      "question": "How do I book?",
      "answer": "Pick a service, then a provider."
    },
    "ar": {
      "question": "كيف أحجز؟",
      "answer": "اختاري الخدمة ثم المقدمة."
    }
  }
}
```

| Field | Type | Nullable | Description |
|-------|------|----------|-------------|
| `id` | uuid | no | |
| `type` | `{value,label}` | yes | Always `null` — the table has no `type` column |
| `question` / `answer` | string | no | Request locale |
| `is_active` | boolean | no | |
| `created_at` / `updated_at` | string | yes | Always `null` — the table has no timestamps |
| `en` / `ar` | `{question, answer}` | no | All locales, for the edit form |

### 4.2 Static page — `GET /api/v1/admin/pages/{id}`

Postman: `admin.pages.show` › "200 Page with both locales"

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "01a171da-a66d-7297-9e7b-20a277cb1265",
    "slug": {"value": "terms", "label": "Terms"},
    "audience": {"value": "all", "label": "All"},
    "title": "Terms and conditions",
    "version": 1,
    "updated_at": "2026-10-25 06:58:37",
    "body": "Terms text",
    "updated_by": null,
    "en": {"title": "Terms and conditions", "body": "Terms text"},
    "ar": {"title": "الشروط والأحكام", "body": "نص الشروط"}
  }
}
```

| Field | Type | Nullable | Description |
|-------|------|----------|-------------|
| `slug` | `{value,label}` | no | `terms`, `privacy`, `cancellation_policy`, `about`, `female_declaration` |
| `audience` | `{value,label}` | no | `all`, `customer`, `provider`, `driver` |
| `title` / `body` | string | no | Request locale |
| `version` | integer | no | Starts at 1, +1 on every edit |
| `updated_at` | string | no | `Y-m-d H:i:s` (TD-03) |
| `updated_by` | uuid | yes | Admin who last saved the page |
| `en` / `ar` | `{title, body}` | no | All locales, for the edit form (`null` values for a missing English text) |

## 5. Forms (create / edit)

### 5.1 FAQ — `POST /faqs` · `PUT /faqs/{id}`

Update accepts partial payloads (every field `sometimes`).

| Field | Input type | Required on create | Validation | Notes |
|-------|-----------|--------------------|-----------|-------|
| `ar.question` / `en.question` | text | **both** yes | string, max 500 | The column holds 300 characters: keep ≤ 300 (§13) |
| `ar.answer` / `en.answer` | textarea | **both** yes | string, max 5000 | |
| `is_active` | switch | no | boolean | |
| `type` | — | no | integer | Accepted and ignored — do not send |

`audience` and `sort_order` cannot be set: every new FAQ gets audience
`customer` and order `0`.

```json
{ "is_active": true, "ar": { "question": "هل الدفع آمن؟", "answer": "نعم." }, "en": { "question": "Is payment safe?", "answer": "Yes." } }
```

Create — 201:

```json
{
  "status": "success",
  "message": "Data saved successfully",
  "data": {
    "id": "01a15dd8-ff7c-71b8-870d-08562d3f24b7",
    "type": null,
    "question": "Is payment safe?",
    "answer": "Yes.",
    "is_active": true,
    "created_at": null,
    "updated_at": null
  }
}
```

Update `{"en": {"question": "How do I book a service?"}}` — 200:

```json
{
  "status": "success",
  "message": "Data saved successfully",
  "data": {
    "id": "01a15dd8-ff5c-7010-8775-5133668c3a91",
    "type": null,
    "question": "How do I book a service?",
    "answer": "Pick a service, then a provider.",
    "is_active": true,
    "created_at": null,
    "updated_at": null
  }
}
```

Validation error (empty body):

```json
{
  "status": "fail",
  "message": "The Question (en) field is required. (and 3 more errors)",
  "data": null
}
```

### 5.2 Static page — `POST /pages` · `PUT /pages/{id}`

Update accepts partial payloads; `ar` fields are `sometimes` on update.

| Field | Validation | Notes |
|-------|-----------|-------|
| `slug` | required on create, `terms` / `privacy` / `cancellation_policy` / `about` / `female_declaration` | With `audience` it must be unique |
| `audience` | optional, `all` / `customer` / `provider` / `driver` (default `all`) | |
| `ar.title` | required on create, string, max 200 | |
| `ar.body` | required on create, string | |
| `en.title` / `en.body` | optional, sent together, title max 200 | English is optional (TD-12) |

Request:

```json
{
  "slug": "terms",
  "audience": "customer",
  "ar": {"title": "الشروط والأحكام", "body": "نص الشروط"},
  "en": {"title": "Terms", "body": "Terms text"}
}
```

Create returns 201 (Postman: `admin.pages.store` › "201 Page created with both locales"):

```json
{
  "status": "success",
  "message": "Data saved successfully",
  "data": {
    "id": "01a171da-a578-7177-be8d-fa07bb5523a1",
    "slug": {"value": "terms", "label": "Terms"},
    "audience": {"value": "customer", "label": "Customer"},
    "title": "الشروط والأحكام",
    "version": 1,
    "updated_at": "2026-10-25 06:58:37",
    "body": "نص الشروط",
    "updated_by": "01a171da-a521-71d2-8da4-39c379387a0a"
  }
}
```

Update returns 200 with `version` increased (Postman: `admin.pages.update` › "200 English text updated and version bumped").

A second page for the same `slug` + `audience` returns 422 (Postman: `admin.pages.store` › "422 Slug already used for this audience"):

```json
{
  "status": "fail",
  "message": "The slug has already been taken. (and 1 more error)",
  "data": null
}
```

The old payload (`type`, `content`, `is_active`, `image`) returns 422 for the missing `slug` (Postman: `admin.pages.store` › "422 Legacy type and content fields").

### 5.3 Onboarding slides — `/onboarding-slides` (since 0.3.0, TD-176)

The intro slides the customer app shows on first launch (`GET /client/onboarding-slides`, see [client/home](../client/home.md)). The provider and driver apps do not read slides yet, but the `app` field is stored so they can later. Anything saved here shows in the customer list at once: only `is_active = true` slides of `app = customer` appear, ordered by `sort_order` then creation order.

| Method | Path | Permission | Purpose |
|--------|------|------------|---------|
| GET | `/onboarding-slides` | `admin.onboarding-slides.index` | List (paginated; `paginate=false` for all) |
| GET | `/onboarding-slides/<built-in function id>` | `admin.onboarding-slides.show` | One slide with `ar` and `en` |
| POST | `/onboarding-slides` | `admin.onboarding-slides.store` | Create |
| PUT | `/onboarding-slides/<built-in function id>` | `admin.onboarding-slides.update` | Edit (send only changed fields) |
| DELETE | `/onboarding-slides/<built-in function id>` | `admin.onboarding-slides.destroy` | Delete |

List query: `filters[app]` (`customer`, `provider`, `driver`), `filters[is_active]` (`0`/`1`), `search` (title and body, current locale), `sort[sort_order]=asc|desc`, `sort[app]`, `per_page`, `page`, `paginate`.

**Form fields** (`id` in the path is the slide uuid; an integer id is a 404)

| Field | Type | Required on create | Validation (mirror in React) | Description |
|-------|------|--------------------|------------------------------|-------------|
| `ar.title` | string | yes | max 200 | Arabic headline |
| `ar.body` | string | yes | max 500 | Arabic text under the headline |
| `en.title`, `en.body` | string | no | max 200 / max 500; when `en` is sent both are required | English text (optional) |
| `app` | string | yes | `customer`, `provider` or `driver` | App that will show the slide |
| `sort_order` | integer | no (0) | 32-bit integer | Lowest first |
| `is_active` | boolean | no (true) | boolean | Hide without deleting |
| `image` | string | yes | `hash` of an uploaded image (`POST /media`); jpg, jpeg, png, webp; up to 5120 KB | Slide illustration; on edit, send a new hash to replace it |

**Create — 201** (Postman: `admin.onboarding-slides.store` › "201 Slide created with both locales")

```json
{
    "status": "success",
    "message": "تم حفظ البيانات بنجاح",
    "data": {
        "id": "01a1bab6-0570-71d5-aa3d-ec446c2653e0",
        "app": {
            "value": "customer",
            "label": "Customer"
        },
        "title": "احجزي بسهولة",
        "body": "اختاري الخدمة والموعد.",
        "image": "http://localhost/storage/slides/slide-image-hash.png",
        "sort_order": 1,
        "is_active": true
    }
}
```

**Show — 200** returns the list fields plus `ar` and `en` objects (`title`, `body`); a missing English text is `null`-valued fields. Postman: `admin.onboarding-slides.show` › "200 Slide details with both locales".

```json
{
    "status": "success",
    "message": "تم جلب البيانات بنجاح",
    "data": {
        "id": "01a1bab6-0657-7144-8a2b-913056b02ebe",
        "app": {
            "value": "customer",
            "label": "Customer"
        },
        "title": "Sample-dlse42910128",
        "body": "Aut et ex sed modi error unde quisquam.",
        "image": "https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200",
        "sort_order": 0,
        "is_active": true,
        "en": {
            "title": "Sample-mmzx03180932",
            "body": "Eligendi et nesciunt sequi dolor."
        },
        "ar": {
            "title": "Sample-dlse42910128",
            "body": "Aut et ex sed modi error unde quisquam."
        }
    }
}
```

**Errors**

| HTTP | `error_code` | When | What the dashboard should do |
|------|--------------|------|------------------------------|
| 422 | — | Validation failed (`data.errors` keyed by field, e.g. `ar.title`, `image`) | Show each message next to its field. Postman: "422 Empty form", "422 Body over 500 characters and unknown app", "422 Unknown app and title over 200 characters" |
| 404 | — | Unknown or integer id | Go back to the list. Postman: "404 Integer id is not a slide" |
| 401 / 403 | `UNAUTHENTICATED` / `PERMISSION_DENIED` | No token, or the admin lacks the permission | Hide the menu entry without `admin.onboarding-slides.index` |

Delete is hard (a slide has no history). Postman: `admin.onboarding-slides.destroy` › "200 Slide deleted".

## 6. Actions (non-CRUD)

### 6.1 Delete

`DELETE /faqs/{id}` (`admin.faqs.destroy`) and `DELETE /pages/{id}`
(`admin.pages.destroy`) work. Translations are deleted with the row.

```json
{
  "status": "success",
  "message": "Data removed successfully",
  "data": null
}
```

### Errors (all endpoints in this doc)

| HTTP | `error_code` | When | Dashboard behavior |
|------|--------------|------|--------------------|
| 401 | `UNAUTHENTICATED` | Missing or expired token | Refresh, then retry |
| 403 | `PERMISSION_DENIED` | Missing route permission | Hide/disable the action |
| 404 | — | Unknown or integer id | Toast, reload the list |
| 422 | — | Validation failed (combined `message`, TD-02) | Show `message` |
| 500 | `INTERNAL_ERROR` | FAQ question > 300 chars | Avoid as described; log `correlation_id` |

Real 403 (supervisor without `banners_content · view`):

```json
{
  "status": "fail",
  "message": "You do not have permission to perform this action.",
  "data": {
    "error_code": "PERMISSION_DENIED",
    "permission": "admin.faqs.index"
  }
}
```

## 7. Statuses and enums

Schema values (not exposed by these endpoints yet):

| Enum | `value` | Meaning |
|------|---------|---------|
| FAQ `audience` | `customer`, `provider`, `driver` | App whose help screen shows it |
| page `slug` | `terms`, `privacy`, `cancellation_policy`, `about`, `female_declaration` | Which policy |
| page `audience` | `all`, `customer`, `provider`, `driver` | One page per slug + audience |

## Data model

| Table (`db-schema.sql`) | Read / write | Notes |
|-------------------------|--------------|-------|
| `faqs` | read + write | `audience`, `sort_order`, `is_active`; no timestamps |
| `faq_translations` | read + write | `question` (varchar 300), `answer` |
| `static_pages` | read + write | `slug`, `audience` (unique together), `version`, `updated_by`, `updated_at` |
| `static_page_translations` | read + write | `title` (varchar 200), `body` |
| `onboarding_slides` | read + write | `app`, `sort_order`, `is_active`; the image is a media file (TD-14); no timestamps |
| `onboarding_slide_translations` | read + write | `title` (varchar 200), `body` (varchar 500) |

## 8. TypeScript types

```ts
export interface Faq {
  id: string; // uuid
  type: null; // always null today
  question: string;
  answer: string;
  is_active: boolean;
  created_at: null;
}
export interface FaqDetails extends Faq {
  updated_at: null;
  en: { question: string; answer: string };
  ar: { question: string; answer: string };
}
export type FaqList = Paginated<'faqs', Faq>;

type EnumValue<T extends string> = { value: T; label: string };
export interface StaticPage {
  id: string;
  slug: EnumValue<'terms' | 'privacy' | 'cancellation_policy' | 'about' | 'female_declaration'>;
  audience: EnumValue<'all' | 'customer' | 'provider' | 'driver'>;
  title: string;
  version: number;
  updated_at: string;
}
export interface StaticPageDetails extends StaticPage {
  body: string;
  updated_by: string | null; // uuid
  en: { title: string | null; body: string | null };
  ar: { title: string; body: string };
}
export type StaticPageList = Paginated<'static_pages', StaticPage>;
```

## 9. Business rules enforced by the backend

| Rule | What the backend does | What the admin sees |
|------|-----------------------|---------------------|
| BR-29 | `rbac.auto` on every route | 403 `PERMISSION_DENIED` |
| BR-01 | Registration requires accepting terms, privacy and the female declaration; these pages hold the text | Pages can be created and edited; each edit bumps `version` so apps can detect changed consent text |
| TD-12 | Translated text in `*_translations` | FAQ create requires **both** `ar` and `en` (TD-12 says English optional) |

## 10. Notifications triggered

N/A.

## 11. Integration checklist

- [ ] FAQ list sends `sort[sort_order]=asc` (or another listed field) when a specific order is needed
- [ ] FAQ form sends `ar` and `en` question + answer; question ≤ 300 chars
- [ ] Static page form sends `slug`, `audience`, `ar` and optional `en` (`title` + `body`); 422 when the slug + audience pair exists
- [ ] Menu items/buttons gated by the listed permissions
- [ ] RTL (`ar`) and `en` verified

## 12. Acceptance scenarios

| AC ID | Scenario | Expected |
|-------|----------|----------|
| AC-CUST-002 | Customer opens the terms / privacy / female-declaration links on the consent screen | **Partly blocked:** admin can now create the pages; guest `/client/pages/{type}` is still unregistered (TD-30) |

## 13. Open decisions and known limitations

| ID | Impact on this feature | Current behavior |
|----|------------------------|------------------|
| TD-32 | Base endpoints, provisional contract | 1.0.0 comes with the content feature rebuild |
| — | FAQ `audience` and `sort_order` not in the request | Every FAQ is for `customer`, order `0` |
| — | FAQ `type` field has no column | Accepted and ignored; response `type` is always `null` |
| — | FAQ question: request allows 500, column holds 300 | Longer questions fail with 500 |
| — | Pages cannot be deleted safely while apps link to them | `DELETE /pages/{id}` still works; no in-use check yet |
| DEC-18 | Consent screens must link to terms and privacy pages | Admin side done (0.2.0); client read endpoint still needed before launch |
| TD-30 | Guest `/client/faqs`, `/client/pages/{type}` unregistered | Apps cannot read this content yet |
| TD-03 | Dates `Y-m-d H:i:s` without offset | Parse as local until TD-03 |
| TD-02 | 422 has one combined `message` | Show `message` |

## Postman examples (2026-11-08)

Real responses captured by `tests/Feature/Api/V1/Admin/ContentCatalogExamplesTest.php`. Note: `GET /faqs` and the FAQ forms still follow the base shape (§3.1, §5.1); the legacy `type` field is validated but not stored.

- `admin.faqs.destroy` › "200 Faqs Deleted"
- `admin.faqs.index` › "200 Faqs List"
- `admin.faqs.show` › "200 Faqs Details"
- `admin.faqs.store` › "201 Faqs Created", "422 Faqs Missing Text"
- `admin.faqs.update` › "200 Faqs Updated", "422 Faqs Update Answer Too Long"
- `admin.pages.destroy` › "200 Pages Deleted"
- `admin.pages.index` › "200 Pages List"
- `admin.pages.show` › "200 Page with both locales"
- `admin.pages.store` › "201 Page created with both locales", "422 Legacy type and content fields", "422 Slug already used for this audience"
- `admin.pages.update` › "200 English text updated and version bumped", "422 Pages Update Invalid Slug"
