# Admin API Endpoints Documentation

> **Base URL**: `/api/dashboard/admin`
> **Auth Header**: `Authorization: Bearer {token}`
> **Response Envelope**: All responses return `{ status, message, data }`
> **Pagination**: All list endpoints support `?page=1&per_page=15`. Pass `?paginate=false` to get all records.
> **Translatable fields**: Send as `{ ar: { field }, en: { field } }`. Index returns the current locale flat; Show returns a `translations` object.

---

## 🔐 Auth

### POST `/auth/login` — Public (no token)

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `email` | string | required, valid email |
| `password` | string | required |

**Response**
| Field | Type |
|-------|------|
| `id` | integer |
| `uuid` | string |
| `full_name` | string |
| `email` | string |
| `phone` | string |
| `phone_code` | string |
| `user_type` | string |
| `locale` | string |
| `allow_notifications` | boolean |
| `is_active` | boolean |
| `is_admin_active` | boolean |
| `avatar` | string (URL) |
| `token` | string |
| `is_super_admin` | boolean |
| `role` | `{ id, name }` \| null |

---

### POST `/auth/logout`
No body. Invalidates token.

---

### GET `/auth/profile`
Returns same fields as login response (token will be null).

---

### PUT `/auth/profile`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `image` | file | nullable |
| `full_name` | string | sometimes, 2–50 chars, no digits |
| `email` | string | sometimes, nullable, unique, max 255 |
| `phone_code` | string | sometimes, nullable, numeric |
| `phone` | string | sometimes, nullable, numeric, unique |

---

### PUT `/auth/change-password`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `old_password` | string | required |
| `password` | string | required, min 8, confirmed |
| `password_confirmation` | string | required |

---

## 👥 Admins

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`
**Base**: `/admins`

### POST / PUT `/admins`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `image` | file | nullable |
| `full_name` | string | required, 2–25 chars, no digits |
| `email` | string | required, email, unique |
| `phone` | string | nullable, unique |
| `phone_code` | string | nullable, max 10 |
| `password` | string | required on create (min 6), nullable on update |
| `role_id` | integer | nullable, exists in roles |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `full_name` | string |
| `email` | string |
| `phone` | string |
| `phone_code` | string |
| `avatar` | string |
| `is_admin_active` | boolean |
| `is_banned` | boolean |
| `role` | `{ id, name }` \| null |
| `created_at` | string |

**Response (Show)** — adds to above
| Field | Type |
|-------|------|
| `is_super_admin` | boolean |
| `role_id` | integer\|null |
| `last_login_at` | timestamp |
| `updated_at` | string |

---

## 🎯 Services

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`
**Base**: `/services`

> ⚠️ **Change**: `hex_color` is now **required** on create/update. Must be a valid 6-digit hex color with `#` prefix (e.g. `#FF5733`).

### POST / PUT `/services`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `is_active` | boolean | optional |
| `hex_color` | string | **required**, format `#RRGGBB` (e.g. `#A3B4C5`) |
| `ar.title` | string | required, max 255 |
| `ar.desc` | string | required |
| `ar.showcase_text` | string | required, max 500 |
| `en.title` | string | required, max 255 |
| `en.desc` | string | required |
| `en.showcase_text` | string | required, max 500 |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `title` | string |
| `desc` | string |
| `showcase_text` | string |
| `is_active` | boolean |
| `hex_color` | string |
| `image` | string |
| `background` | string |
| `created_at` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `is_active` | boolean |
| `hex_color` | string |
| `image` | string |
| `background` | string |
| `translations` | `{ ar: { title, desc, showcase_text }, en: { ... } }` |
| `created_at` | string |
| `updated_at` | string |

---

## 👤 Clients

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`
**Base**: `/clients`

### POST / PUT `/clients`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `full_name` | string | required, 2–25 chars, no digits |
| `phone_code` | string | required, exists in countries |
| `phone` | string | required, unique per phone_code |
| `email` | string | nullable, email, unique |
| `password` | string | required on create (min 8, confirmed), nullable on update |
| `password_confirmation` | string | required on create |

**Response (Index & Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `full_name` | string |
| `email` | string |
| `phone_code` | string |
| `phone` | string |
| `is_admin_active_user` | boolean |
| `avatar` | string |
| `created_at` | string |

---

## 🏢 Providers

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`
**Base**: `/providers`

### POST / PUT `/providers`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `full_name` | string | required, 2–25 chars, no digits |
| `service_id` | integer | required, exists in services |
| `phone_code` | string | required, max 10, exists in countries |
| `phone` | string | required, unique per phone_code |
| `email` | string | required, email, unique |
| `brief.ar` | string | required, 10–1000 chars |
| `brief.en` | string | required, 10–1000 chars |
| `password` | string | required on create (min 8, confirmed), nullable on update |
| `password_confirmation` | string | required on create |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `full_name` | string |
| `email` | string |
| `phone_code` | string |
| `phone` | string |
| `is_admin_active_user` | boolean |
| `service_data` | `{ id, title, desc, showcase_text, image, background }` |
| `brief` | `{ ar, en }` |
| `avatar` | string |
| `created_at` | string |

**Response (Show)** — same + `updated_at`

---

## 🌍 Countries

**Endpoints**: GET, GET `{id}`, GET `/countries/list`
**Base**: `/countries`
**Note**: Countries are **read-only** — no create, update, delete, or status toggle. They are seeded/managed at the infrastructure level.

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `name` | string |
| `slug` | string |
| `phone_code` | string |
| `currency` | string |
| `status` | boolean |
| `flag` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `slug` | string |
| `phone_code` | string |
| `phone_limit` | integer |
| `currency` | string |
| `status` | boolean |
| `flag` | string |
| `translations` | `{ ar: { name, slug, currency }, en: { ... } }` |
| `created_at` | string |

---

## 🏙️ Cities

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`, GET `/cities/list`
**Base**: `/cities`

### POST / PUT `/cities`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `status` | boolean | optional |
| `ar.name` | string | required, max 255, unique |
| `ar.slug` | string | sometimes, nullable, 2–40 chars, unique |
| `en.name` | string | required, max 255, unique |
| `en.slug` | string | sometimes, nullable, 2–40 chars, unique |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `name` | string |
| `status` | boolean |
| `country` | `{ id, name }` |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `country_id` | integer |
| `status` | boolean |
| `translations` | `{ ar: { name, slug }, en: { ... } }` |
| `country` | `{ id, name }` |
| `created_at` | string |

---

## 📍 Districts

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`
**Base**: `/districts`

### POST / PUT `/districts`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `city_id` | integer | required, exists in cities |
| `status` | boolean | optional |
| `ar.name` | string | required, max 255 |
| `en.name` | string | required, max 255 |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `name` | string |
| `status` | boolean |
| `city_data` | `{ id, name }` |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `status` | boolean |
| `translations` | `{ ar: { name }, en: { name } }` |
| `city_data` | `{ id, name }` |
| `created_at` | string |

---

## 📧 Contacts

**Endpoints**: GET, GET `{id}`, DELETE `{id}`, POST `{id}/reply`
**Base**: `/contacts`
**Note**: Read-only — contacts are submitted by users, not created by admin.

### POST `/contacts/{id}/reply`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `reply` | string | required, max 5000 |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `full_name` | string |
| `phone_code` | string |
| `phone` | string |
| `message` | string |
| `is_read` | boolean |
| `created_at` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `full_name` | string |
| `phone_code` | string |
| `phone` | string |
| `message` | string |
| `read_at` | string\|null |
| `user` | `{ id, full_name }` |
| `created_at` | string |

---

## 📂 Categories

**Endpoints**: GET, GET `{id}`, PATCH `{id}/status`, GET `/categories/list`
**Base**: `/categories`
**Note**: Categories are **read-only** — no create or delete. Admin can only toggle status. Images are updated via the general attachment API (see below).

### PATCH `/categories/{id}/status`
Toggles `status` boolean. Returns the updated category (Show shape).

### Updating a category image

Category images are managed through the **general attachment API**, not a dedicated endpoint.

**POST `/api/attachment`** — `multipart/form-data`

| Field | Type | Value for category image |
|-------|------|--------------------------|
| `file` | file | The image file |
| `media_type` | string | `image` |
| `model` | string | `Category` |
| `model_id` | integer | The category's ID |
| `option` | string | `category_image` |
| `is_single` | boolean | `true` (replaces previous image) |
| `id` | integer | nullable — pass existing media ID to replace in-place |

**Response**
| Field | Type |
|-------|------|
| `id` | integer (save this for future `id` replacement calls) |
| `path` | string (full URL to uploaded image) |
| `type` | string |
| `option` | string |
| `model` | string |
| `model_id` | integer |
| `model_type` | string |
| `is_single` | boolean |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `name` | string |
| `key` | string (e.g. `cars`, `real_estate`, `plates`, `business`) |
| `status` | boolean |
| `image` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `name` | string |
| `key` | string |
| `status` | boolean |
| `image` | string |
| `translations` | `{ ar: { name }, en: { name } }` |
| `created_at` | string |

---

## 📄 Static Pages

**Endpoints**: GET `/types`, GET `/list/{page?}`, GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`
**Base**: `/pages`
**Page Types**: `about`, `terms`, `privacy`, `faq`

### POST / PUT `/pages`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `type` | string | required, one of: `about`, `terms`, `privacy`, `faq` |
| `ar.title` | string | required, max 255 |
| `ar.desc` | string | nullable |
| `en.title` | string | required, max 255 |
| `en.desc` | string | nullable |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `type` | string |
| `title` | string |
| `is_active` | boolean |
| `image` | string |
| `created_at` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `type` | string |
| `is_active` | boolean |
| `image` | string |
| `translations` | `{ ar: { title, desc }, en: { ... } }` |
| `created_at` | string |
| `updated_at` | string |

---

## 📚 Knowledge Center

All endpoints prefixed with `/knowledge-center/`

### Icons — `/knowledge-center/icons`

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `is_active` | boolean | optional |
| `ar.title` | string | required, max 255 |
| `ar.subtitle` | string | nullable, max 255 |
| `en.title` | string | required, max 255 |
| `en.subtitle` | string | nullable, max 255 |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `title` | string |
| `subtitle` | string |
| `icon` | string |
| `background` | string |
| `is_active` | boolean |
| `created_at` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `is_active` | boolean |
| `icon` | string |
| `background` | string |
| `translations` | `{ ar: { title, subtitle }, en: { ... } }` |
| `created_at` | string |
| `updated_at` | string |

---

### Articles — `/knowledge-center/articles`

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `is_active` | boolean | optional |
| `reading_time` | integer | nullable, min 1 |
| `ar.title` | string | required, max 255 |
| `ar.description` | string | nullable |
| `en.title` | string | required, max 255 |
| `en.description` | string | nullable |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `title` | string |
| `description` | string |
| `reading_time` | integer |
| `image` | string |
| `is_active` | boolean |
| `created_at` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `is_active` | boolean |
| `reading_time` | integer |
| `image` | string |
| `translations` | `{ ar: { title, description }, en: { ... } }` |
| `created_at` | string |
| `updated_at` | string |

---

### FAQs — `/knowledge-center/faqs`

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `is_active` | boolean | optional |
| `order` | integer | required, min 1, unique ordering |
| `ar.question` | string | required, max 1000 |
| `ar.answer` | string | required, max 10000 |
| `en.question` | string | required, max 1000 |
| `en.answer` | string | required, max 10000 |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `question` | string |
| `answer` | string |
| `is_active` | boolean |
| `order` | integer |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `is_active` | boolean |
| `order` | integer |
| `translations` | `{ ar: { question, answer }, en: { ... } }` |
| `created_at` | string |

---

### Videos — `/knowledge-center/videos`

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `is_active` | boolean | optional |
| `duration` | integer | required, min 1 |
| `ar.title` | string | required, max 255 |
| `ar.description` | string | nullable |
| `en.title` | string | required, max 255 |
| `en.description` | string | nullable |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `title` | string |
| `description` | string |
| `duration` | integer (seconds) |
| `video` | string (URL) |
| `is_active` | boolean |
| `created_at` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `is_active` | boolean |
| `duration` | integer |
| `video` | string (URL) |
| `translations` | `{ ar: { title, description }, en: { ... } }` |
| `created_at` | string |
| `updated_at` | string |

---

### Contracts — `/knowledge-center/contracts`

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `is_active` | boolean | optional |
| `ar.title` | string | required, max 255 |
| `ar.description` | string | nullable |
| `en.title` | string | required, max 255 |
| `en.description` | string | nullable |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `title` | string |
| `description` | string |
| `pdf` | string (URL) |
| `is_active` | boolean |
| `created_at` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `is_active` | boolean |
| `pdf` | string (URL) |
| `translations` | `{ ar: { title, description }, en: { ... } }` |
| `created_at` | string |
| `updated_at` | string |

---

## 🚗 Brands

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`
**Base**: `/brands`

> **Note**: Brand has only `name` as its translatable field. There is no `desc` or other text fields.

### POST / PUT `/brands`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `is_active` | boolean | optional |
| `ar.name` | string | required, max 255 |
| `en.name` | string | required, max 255 |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `name` | string (current locale) |
| `created_at` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `translations` | `{ ar: { name }, en: { name } }` |
| `created_at` | string |
| `updated_at` | string |

---

## 🚘 Car Models

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`
**Base**: `/models`

### POST / PUT `/models`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `brand_id` | integer | required, exists in brands |
| `ar.name` | string | required, max 255 |
| `en.name` | string | required, max 255 |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `name` | string (current locale) |
| `brand_data` | `{ id, name, created_at }` |
| `created_at` | string |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `brand_data` | `{ id, name, created_at }` |
| `translations` | `{ ar: { name }, en: { name } }` |
| `created_at` | string |
| `updated_at` | string |

---

## 🔤 Plate Distinctions

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`
**Base**: `/plate-distinctions`

**Response (Index & Show)**: standard translatable resource with `name` field.

---

## 🏭 Business Activity Types

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`
**Base**: `/business-activity-types`

**Response (Index & Show)**: standard translatable resource with `name` field.

---

## 🏗️ Building Types

**Endpoints**: GET, POST, GET `{id}`, PUT `{id}`, DELETE `{id}`, PATCH `{id}/status`, GET `/building-types/list`
**Base**: `/building-types`
**Note**: Soft-deleted. When a building type is soft-deleted, existing ads keep the FK (set null only on hard-delete). Toggle `is_active` to hide from the public dropdown without deleting.

### POST / PUT `/building-types`

**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `is_active` | boolean | optional |
| `ar.name` | string | required, max 255 |
| `en.name` | string | required, max 255 |

**Response (Index)**
| Field | Type |
|-------|------|
| `id` | integer |
| `name` | string (current locale) |
| `is_active` | boolean |
| `deleted_at` | string\|null |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `name` | string (current locale) |
| `is_active` | boolean |
| `translations` | `{ ar: { name }, en: { name } }` |
| `deleted_at` | string\|null |
| `created_at` | string |

### PATCH `/building-types/{id}/status`
Toggles `is_active`. Returns the updated Show resource.

### GET `/building-types/list`
Returns all active, non-deleted building types (no pagination). Same shape as Index.

---

---

## 📢 Ads Management

All ad endpoints are under `/ads`.

### GET `/ads` — List all ads

**Query Filters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `category_id` | integer | Filter by category. Also unlocks category-specific filters below. |
| `status` | string | `pending` / `active` / `rejected` / `expired` / `sold` |
| `ad_type` | string | `sell` / `exchange` / `partner` |
| `city_id` | integer | Filter by city |
| `district_id` | integer | Filter by district |
| `is_promoted` | boolean | Promoted ads only |
| `keyword` | string | Name search (LIKE) |
| `min_price` | numeric | Min price (searches across all detail types) |
| `max_price` | numeric | Max price |
| `per_page` | integer | Default 15 |

**Cars category-specific filters** (pass `category_id` for cars category):
| Parameter | Description |
|-----------|-------------|
| `brand_id` | Brand ID |
| `model_id` | Model ID |
| `manufacture_year` | Year |
| `car_status` | `new` / `used` |
| `transmission` | `manual` / `automatic` |
| `body_condition` | `agency` / `cosmetic_paint` / `accident_paint` / `damaged` |
| `engine_condition` | `excellent` / `good` / `needs_maintenance` / `needs_restoration` |
| `fuel_type` | `petrol` / `diesel` / `hybrid` / `electric` |
| `min_kilometers` / `max_kilometers` | Mileage range |

**Plates category-specific filters**:
| Parameter | Description |
|-----------|-------------|
| `plate_type` | `private` / `private_transport` / `public_transport` / `motorcycle` |
| `plate_distinction_id` | Distinction ID |
| `arabic_numbers` | Arabic numbers (LIKE search) |
| `english_numbers` | English numbers (LIKE search) |
| `ready_for_transfer_ownership` | boolean |

**Real Estate category-specific filters**:
| Parameter | Description |
|-----------|-------------|
| `property_type_id` | Property type (see static types above) |
| `advertiser_role` | `owner` / `broker` |
| `purpose` | `residential` / `commercial` / `residential_commercial` |
| `rooms` | Number of rooms |
| `bathrooms` | Number of bathrooms |
| `facade` | `north` / `south` / `east` / `west` / `multiple` |
| `has_mortgage` | boolean |
| `has_legal_dispute` | boolean |
| `min_area` / `max_area` | Area in m² |
| `vacancy_readiness` | `immediate` / `bank_mortgage` / `has_heirs` |

**Business category-specific filters**:
| Parameter | Description |
|-----------|-------------|
| `business_activity_type_id` | Activity type ID |
| `ownership_nature` | `rent` / `owned` / `equipment_liquidation` |
| `operation_status` | `active` / `under_establishment` / `struggling` |
| `brand_included` | `included` / `not_included` |
| `licenses_status` | `valid` / `expired` / `needs_transfer` |

---

### GET `/ads/{ad}` — Show ad detail
Returns category-specific show resource (see Ad Creation section for shape).

### PATCH `/ads/{ad}/approve`
Transitions ad from `pending` → `active`. Returns updated ad resource.

### PATCH `/ads/{ad}/reject`
**Request Body**
| Field | Type | Rules |
|-------|------|-------|
| `rejection_reason` | string | required |

Transitions ad from `pending` → `rejected`. Returns updated ad resource.

### DELETE `/ads/{ad}`
Hard deletes the ad. Returns null data.

### GET `/ads/{ad}/comments`
Returns paginated comments on an ad.

**Query Params**
| Parameter | Type | Description |
|-----------|------|-------------|
| `per_page` | integer | Default 15 |

**Response (each item)**
| Field | Type |
|-------|------|
| `id` | integer |
| `comment` | string |
| `created_at` | timestamp |
| `client_data` | `{ id, full_name, avatar }` |

### DELETE `/ads/comments/{comment}`
Hard deletes a comment without ownership check. Returns null data.

---

## 📝 Ad Creation (by Category)

Ad creation goes through **category-specific** endpoints. Each has its own request shape.  
Admin endpoints add one extra field: `client_id` (required) — the user the ad is being created for.

> **Media fields**: All images/files are sent as **media IDs** (integers) — upload the file first via the media upload endpoint to get an ID, then pass that ID here.

---

### POST `/ads/cars` — Create Car Ad
### PUT `/ads/cars/{ad}` — Update Car Ad

> **Supported `ad_type` values**: `sell`, `exchange`

#### General Fields (always required on create, `sometimes|required` on update)

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `client_id` | integer | required, exists:users | **Admin only** — which client owns this ad |
| `ad_type` | string | required, `sell` or `exchange` | Determines which type-specific fields are required |
| `name` | string | required, max 255 | Ad title |
| `description` | string | nullable, max 2000 | Free text description |
| `city_id` | integer | required, exists:cities (active) | |
| `district_id` | integer | required, exists:districts (active) | |
| `data_accuracy_confirmed` | boolean | required, must be `true` | Accuracy declaration checkbox |

#### Car Detail Fields (always required on create)

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `brand_id` | integer | required, exists:brands | Car brand |
| `model_id` | integer | required, exists:models | Car model (must belong to brand) |
| `manufacture_year` | integer | required, 1990–current year | |
| `kilometers` | integer | required, min 0 | Odometer reading |
| `car_status` | string | required, `new` or `used` | |
| `transmission` | string | required, `manual` or `automatic` | |
| `body_condition` | string | required, `agency` / `cosmetic_paint` / `accident_paint` / `damaged` | |
| `engine_condition` | string | required, `excellent` / `good` / `needs_maintenance` / `needs_restoration` | |
| `fuel_type` | string | required, `petrol` / `diesel` / `hybrid` / `electric` | |
| `inspection_date` | date | nullable, format `Y-m-d` | Periodic inspection date |
| `inspection_file` | integer | nullable, exists:media | PDF inspection report media ID |

#### Sell-Specific Fields (`ad_type = sell`)

| Field | Type | Rules |
|-------|------|-------|
| `final_price` | numeric | **required when `ad_type=sell`**, min 0 |
| `includes_transfer_fees` | boolean | **required when `ad_type=sell`** |

#### Exchange-Specific Fields (`ad_type = exchange`)

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `exchange_for` | integer | **required when `ad_type=exchange`**, exists:categories (active) | Category ID of what they want in exchange |
| `required_description` | string | **required when `ad_type=exchange`**, max 2000 | Description of desired asset |
| `price_difference_type` | string | **required when `ad_type=exchange`**, `i_pay` / `i_request` / `even` | Financial difference direction |
| `max_difference_amount` | numeric | nullable, min 0 | Max acceptable difference amount |

#### Media Fields

| Field | Type | Rules |
|-------|------|-------|
| `cover_image` | integer | required on create, exists:media |
| `images` | array | nullable, max 19 items |
| `images.*` | integer | exists:media |

---

### POST `/ads/real-estate` — Create Real Estate Ad
### PUT `/ads/real-estate/{ad}` — Update Real Estate Ad

> **Supported `ad_type` values**: `sell`, `exchange`, `partner`

#### General Fields (always required on create)

| Field | Type | Rules |
|-------|------|-------|
| `client_id` | integer | required, exists:users |
| `ad_type` | string | required, `sell` / `exchange` / `partner` |
| `name` | string | required, max 255 |
| `description` | string | nullable, max 2000 |
| `city_id` | integer | required, exists:cities |
| `district_id` | integer | required, exists:districts |
| `data_accuracy_confirmed` | boolean | required, must be `true` |

#### Advertiser Info (always required)

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `advertiser_role` | string | required, `owner` or `broker` | |
| `fal_number` | string | **required when `advertiser_role=broker`**, max 50 | FAL license number |
| `has_commission` | boolean | nullable | Broker commission — broker only |
| `owner_id_number` | string | required, max 20 | Owner national ID |
| `birth_date` | date | required, format `Y-m-d` | Owner birth date |

#### Ad Licensing (always required)

| Field | Type | Rules |
|-------|------|-------|
| `ad_license_number` | string | required, max 50 |
| `ad_license_url` | string | required, valid URL, max 500 |
| `license_issue_date` | date | required, format `Y-m-d` |
| `license_expiry_date` | date | required, format `Y-m-d`, must be after or equal to issue date |

#### Property Identity (always required)

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `property_type` | string | required, one of: `land` / `villa` / `apartment` / `complex` / `chalet` / `commercial` / `agricultural` | Determines which field group is used |
| `purpose` | string | required, `residential` / `commercial` / `residential_commercial` | |
| `deed_number` | string | nullable, max 50 | Property deed / registration number |
| `has_mortgage` | boolean | nullable | |
| `obligations` | string | nullable, max 2000 | Undocumented obligations |
| `has_legal_dispute` | boolean | nullable | |

#### Building Fields — only when `property_type = building`

> `area_m2` and `building_type_id` are **required** for building types. Land fields are **rejected** if sent.

| Field | Type | Rules |
|-------|------|-------|
| `building_type_id` | integer | **required for building types**, exists:building_types (active, not soft-deleted) |
| `building_purpose` | string | nullable, `residential` / `commercial` / `residential_commercial` |
| `area_m2` | numeric | **required for building types**, min 1 |
| `rooms` | integer | nullable, min 0 |
| `bathrooms` | integer | nullable, min 0 |
| `floor` | integer | nullable, min 0 |
| `floors_count` | integer | nullable, min 0 |
| `property_age` | integer | nullable, min 0 (years) |
| `facade` | string | nullable, `north` / `south` / `east` / `west` / `multiple` |
| `utilities` | array | nullable, values: `electricity` / `water` / `sewage` / `internet` |
| `building_description` | string | nullable, max 3000 |

#### Land Fields — only when `property_type = land`

> `area_m2` is **required** for land types. Building fields are **rejected** if sent.

| Field | Type | Rules |
|-------|------|-------|
| `area_m2` | numeric | **required for land types**, min 1 |
| `land_type` | string | nullable, max 100 |
| `land_purpose` | string | nullable, `residential` / `commercial` / `residential_commercial` |
| `land_plan_number` | string | nullable, max 100 |
| `land_piece_number` | string | nullable, max 100 |
| `land_facade` | string | nullable, `north` / `south` / `east` / `west` / `multiple` |
| `land_facade_length` | numeric | nullable, min 0, max 99,999 |
| `street_width` | numeric | nullable, min 0, max 9999 |
| `streets_count` | integer | nullable, min 0, max 99 |
| `land_description` | string | nullable, max 3000 |

#### Sell-Specific Fields (`ad_type = sell`)

| Field | Type | Rules |
|-------|------|-------|
| `final_price` | numeric | **required when `ad_type=sell`**, min 0 |
| `vacancy_readiness` | string | **required when `ad_type=sell`**, `immediate` / `bank_mortgage` / `has_heirs` |
#### Exchange-Specific Fields (`ad_type = exchange`)

| Field | Type | Rules |
|-------|------|-------|
| `exchange_for` | integer | **required when `ad_type=exchange`**, exists:categories |
| `required_description` | string | **required when `ad_type=exchange`**, max 2000 |
| `price_difference_type` | string | **required when `ad_type=exchange`**, `i_pay` / `i_request` / `even` |
| `max_difference_amount` | numeric | nullable, min 0 |

#### Partner-Specific Fields (`ad_type = partner`)

| Field | Type | Rules |
|-------|------|-------|
| `partnership_type` | string | **required when `ad_type=partner`**, `build_and_sell` / `build_and_rent` / `long_term_investment` |
| `required_amount` | numeric | **required when `ad_type=partner`**, min 0 |
| `offered_share_percentage` | numeric | **required when `ad_type=partner`**, 0–100 |

#### Media Fields

| Field | Type | Rules |
|-------|------|-------|
| `cover_image` | integer | required on create, exists:media |
| `images` | array | nullable, max 19 items |
| `images.*` | integer | exists:media |

---

### POST `/ads/plates` — Create Plate Ad
### PUT `/ads/plates/{ad}` — Update Plate Ad

> **Supported `ad_type` values**: `sell`, `exchange`

#### General Fields (always required on create)

| Field | Type | Rules |
|-------|------|-------|
| `client_id` | integer | required, exists:users |
| `ad_type` | string | required, `sell` or `exchange` |
| `name` | string | required, max 255 |
| `description` | string | nullable, max 2000 |
| `city_id` | integer | required, exists:cities (active) |
| `district_id` | integer | required, exists:districts (active) |
| `data_accuracy_confirmed` | boolean | required, must be `true` |

#### Plate Detail Fields

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `plate_type` | string | required, `private` / `private_transport` / `public_transport` / `motorcycle` | |
| `arabic_chars` | string | nullable, max 4 | Arabic letters on plate |
| `english_chars` | string | nullable, max 4 | English letters on plate |
| `arabic_numbers` | string | required, max 10 | Arabic numbers on plate |
| `english_numbers` | string | nullable, max 10 | English numbers on plate |
| `plate_distinction_id` | integer | nullable, exists:plate_distinctions | From Plate Distinctions CRUD |
| `ready_for_transfer_ownership` | boolean | required | Ownership transfer readiness |

#### Sell-Specific Fields (`ad_type = sell`)

| Field | Type | Rules |
|-------|------|-------|
| `final_price` | numeric | **required when `ad_type=sell`**, min 0 |
| `includes_transfer_fees` | boolean | **required when `ad_type=sell`** |

#### Exchange-Specific Fields (`ad_type = exchange`)

| Field | Type | Rules |
|-------|------|-------|
| `exchange_for` | integer | **required when `ad_type=exchange`**, exists:categories (active) |
| `required_description` | string | nullable, max 2000 |
| `price_difference_type` | string | **required when `ad_type=exchange`**, `i_pay` / `i_request` / `even` |
| `max_difference_amount` | numeric | nullable, min 0 |

#### Media Fields

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `cover_image` | integer | required on create, exists:media | |
| `images` | array | nullable, max 19 items | |
| `images.*` | integer | exists:media | |

---

### POST `/ads/business` — Create Business Ad
### PUT `/ads/business/{ad}` — Update Business Ad

> **Supported `ad_type` values**: `sell`, `exchange`, `partner`

#### General Fields (always required on create)

| Field | Type | Rules |
|-------|------|-------|
| `client_id` | integer | required, exists:users |
| `ad_type` | string | required, `sell` / `exchange` / `partner` |
| `name` | string | required, max 255 |
| `description` | string | nullable, max 2000 |
| `city_id` | integer | required, exists:cities |
| `district_id` | integer | required, exists:districts |
| `data_accuracy_confirmed` | boolean | required, must be `true` |

#### Business Detail Fields (always required on create)

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `business_activity_type_id` | integer | required, exists:business_activity_types | From Business Activity Types CRUD |
| `ownership_nature` | string | required, `rent` / `owned` / `equipment_liquidation` | |
| `operation_status` | string | required, `active` / `under_establishment` / `struggling` | |
| `brand_included` | string | required, `included` / `not_included` | Whether brand/trademark is part of deal |
| `licenses_status` | string | required, `valid` / `expired` / `needs_transfer` | |
| `id_number` | string | required, exactly 10 digits | رقم الهوية الوطنية أو السجل التجاري |

#### Sell-Specific Fields (`ad_type = sell`)

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `final_price` | numeric | **required when `ad_type=sell`**, min 0 | |
| `financial_obligations` | numeric | nullable, min 0 | Supplier debts, salaries, rent arrears |

#### Exchange-Specific Fields (`ad_type = exchange`)

| Field | Type | Rules |
|-------|------|-------|
| `exchange_for` | integer | **required when `ad_type=exchange`**, exists:categories |
| `required_description` | string | **required when `ad_type=exchange`**, max 2000 |
| `price_difference_type` | string | **required when `ad_type=exchange`**, `i_pay` / `i_request` / `even` |
| `max_difference_amount` | numeric | nullable, min 0 |

#### Partner-Specific Fields (`ad_type = partner`)

| Field | Type | Rules | Notes |
|-------|------|-------|-------|
| `partnership_type` | string | **required when `ad_type=partner`**, `build_and_sell` / `build_and_rent` / `long_term_investment` | نوع الشراكة — deal structure |
| `partner_type` | string | **required when `ad_type=partner`**, `financier` / `operator` / `technical` / `effort_partner` | نوع الشريك — partner role |
| `offered_share_percentage` | numeric | **required when `ad_type=partner`**, 0–100 | |
| `required_amount` | numeric | **required when `ad_type=partner`**, min 0 | |

#### Media Fields

| Field | Type | Rules |
|-------|------|-------|
| `cover_image` | integer | required on create, exists:media |
| `images` | array | nullable, max 19 items |
| `images.*` | integer | exists:media |
| `inspection_file` | integer | nullable, exists:media — business inspection document (PDF) |

---

## 🚨 Ad Reports

**Base**: `/ad-reports`
**Endpoints**: GET, GET `{id}`, DELETE `{id}`
**Note**: Read-only listing — reports are created by clients, not admin.

### GET `/ad-reports` — List reports

**Query Filters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `ad_id` | integer | Filter reports for a single ad |
| `ad_ids[]` | integer[] | Filter reports for multiple ads (e.g. `?ad_ids[]=1&ad_ids[]=2`) |
| `per_page` | integer | Default 15 |

**Response (Index — each item)**
| Field | Type |
|-------|------|
| `id` | integer |
| `reason` | string |
| `created_at` | timestamp |
| `ad_data` | `{ id, name, cover_image }` |
| `reporter_data` | `{ id, full_name, phone, avatar }` |

**Response (Show)**
| Field | Type |
|-------|------|
| `id` | integer |
| `reason` | string |
| `created_at` | timestamp |
| `ad_data` | `{ id, name, cover_image }` |
| `reporter_data` | `{ id, full_name, phone, email, avatar }` |

---

## 🔍 General Lookup APIs (Public — no auth required)

These endpoints serve dropdown data for ad creation forms. All return `{ id, name }` arrays.

| Endpoint | Description | Filter |
|----------|-------------|--------|
| `GET /general/brands` | All car brands | — |
| `GET /general/car-models` | Car models | `?brand_id=` to filter by brand |
| `GET /general/plate-distinctions` | Active plate distinctions | — |
| `GET /general/business-activity-types` | Active business activity types | — |
| `GET /general/cities` | Active cities | — |
| `GET /general/districts` | Active districts | — |
| `GET /general/categories` | Active categories | — |

> **Note:** `GET /general/car-models?brand_id=5` returns only models belonging to brand 5. Without the filter, all models are returned.

---

## 📋 Quick Reference Table

| Resource | Base Path | CRUD | Status | Extra |
|----------|-----------|------|--------|-------|
| **Auth** | `/auth` | login, logout, profile, change-password | — | — |
| **Admins** | `/admins` | ✅ | ✅ | — |
| **Services** | `/services` | ✅ | ✅ | `hex_color` now required |
| **Clients** | `/clients` | ✅ | ✅ | — |
| **Providers** | `/providers` | ✅ | ✅ | — |
| **Countries** | `/countries` | List, Show only | — | `/countries/list` |
| **Cities** | `/cities` | ✅ | ✅ | `/cities/list` |
| **Districts** | `/districts` | ✅ | ✅ | — |
| **Contacts** | `/contacts` | List, Show, Delete only | — | Reply endpoint |
| **Categories** | `/categories` | List, Show only | ✅ | `/categories/list`, image via attachment API |
| **Pages** | `/pages` | ✅ | — | `/types`, `/list/{page?}` |
| **KC Icons** | `/knowledge-center/icons` | ✅ | ✅ | — |
| **KC Articles** | `/knowledge-center/articles` | ✅ | ✅ | — |
| **KC FAQs** | `/knowledge-center/faqs` | ✅ | ✅ | — |
| **KC Videos** | `/knowledge-center/videos` | ✅ | ✅ | — |
| **KC Contracts** | `/knowledge-center/contracts` | ✅ | ✅ | — |
| **Brands** | `/brands` | ✅ | — | `name` only, no desc |
| **Car Models** | `/models` | ✅ | — | Requires `brand_id` |
| **Property Types** | — | Static enum values, not exposed via admin API | — | Used in real estate ad `property_type` field |
| **Plate Distinctions** | `/plate-distinctions` | ✅ | ✅ | — |
| **Business Activity Types** | `/business-activity-types` | ✅ | ✅ | — |
| **Ads** | `/ads` | List, Show, Delete | — | approve, reject, comments |
| **Ad Creation — Cars** | `/ads/cars` | Create/Update only | — | No index/show (use `/ads`) |
| **Ad Creation — Real Estate** | `/ads/real-estate` | Create/Update only | — | No index/show (use `/ads`) |
| **Ad Creation — Plates** | `/ads/plates` | Create/Update only | — | No index/show (use `/ads`) |
| **Ad Creation — Business** | `/ads/business` | Create/Update only | — | No index/show (use `/ads`) |
| **Ad Reports** | `/ad-reports` | List, Show, Delete only | — | Filter by `ad_id` or `ad_ids[]` |

---

## 🔧 Global Notes

1. **Authentication**: All protected endpoints require `Authorization: Bearer {token}` header.
2. **Pagination**: All list endpoints support `?page=1&per_page=15`. Use `?paginate=false` to disable.
3. **Translatable Fields**: On create/update, send as `{ ar: { field }, en: { field } }`. On index, returns current locale flat. On show, returns both a flat value and a `translations` object.
4. **Status Toggle**: `PATCH /{resource}/{id}/status` returns the updated resource.
5. **Media Upload**: For fields that accept media IDs (`cover_image`, `images`, `plate_image`, etc.) — upload the file first to get a media ID, then pass that ID in the ad request.
6. **Update (PUT) behavior**: All required fields become `sometimes|required` on update — you only need to send fields you want to change.
7. **Response Envelope**:
   ```json
   {
     "status": "success",
     "message": "Operation completed",
     "data": { ... }
   }
   ```
8. **Validation Errors**: `422` status, Laravel standard format.

---

*Last Updated: July 7, 2026*
*For questions, contact the backend team.*
