# Location API (Province & City)

> **Prefix:** `/api/v1/provinces`, `/api/v1/cities`, `/api/v1/admin/province`, `/api/v1/admin/city`

---

## GET `/api/v1/provinces`

List all active provinces.

**Permission:** `PUBLIC`

### Response `200`
```json
{
  "success": true,
  "data": [
    { "id": 1, "name": "تهران", "status": "active", "weight": 1 },
    { "id": 2, "name": "اصفهان", "status": "active", "weight": 2 }
  ]
}
```

---

## GET `/api/v1/cities`

List cities, optionally filtered by province.

**Permission:** `PUBLIC`

### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `province_id` | integer | ❌ | Filter cities by province |

### Response `200`
```json
{
  "success": true,
  "data": [
    { "id": 10, "name": "تهران", "province_id": 1, "status": "active", "weight": 1 },
    { "id": 11, "name": "کرج", "province_id": 1, "status": "active", "weight": 2 }
  ]
}
```

---

## GET `/api/v1/admin/provinces`

List all provinces with pagination (admin view — includes inactive).

**Permission:** `ROLE_ADMIN`

### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in name |

### Response `200`
```json
{
  "success": true,
  "data": [ ... ],
  "meta": { "totalRecords": 31, "totalPages": 2, "currentPage": 1 }
}
```

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |

---

## GET `/api/v1/admin/cities`

List all cities with pagination (admin view).

**Permission:** `ROLE_ADMIN`

### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Matches `name`, `site_name`, `domain`, **or the parent province's name** (case-insensitive `LIKE`) |
| `province_id` | integer | ❌ | Filter by province |

### Response `200`
Paginated city list. Each row already carries `province_name` alongside `province_id`.

---

## POST `/api/v1/admin/province`

Create a new province.

**Permission:** `ROLE_ADMIN`

### Request Body (`application/json`)
```json
{
  "name": "تهران",
  "status": "active",
  "weight": 1
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Province name |
| `status` | string | ❌ | `"active"` (default) or `"inactive"` |
| `weight` | integer | ❌ | Sort weight |

### Response `201`
Province object.

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing name |

---

## PATCH `/api/v1/admin/province/{id}`

Update a province.

**Permission:** `ROLE_ADMIN`

### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `id` | integer | Province ID |

All fields optional.

### Response `200`
Updated province object.

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Province not found |

---

## DELETE `/api/v1/admin/province/{id}`

Delete a province.

**Permission:** `ROLE_ADMIN`

### Response `200`
```json
{ "success": true, "data": { "message": "استان حذف شد" } }
```

---

## POST `/api/v1/admin/city`

Create a new city.

**Permission:** `ROLE_ADMIN`

### Request Body (`application/json`)
```json
{
  "name": "تهران",
  "site_name": "تهران نوبت",
  "province_id": 1,
  "status": "active",
  "weight": 1
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | City name |
| `site_name` | string | ❌ | Public site name shown on the city's booking website (e.g. `"یزد نوبت"`) |
| `title` | string | ❌ | SEO meta title for the city's booking website (e.g. `"نوبت‌دهی آنلاین پزشکان یزد | یزد نوبت"`) |
| `province_id` | integer | ❌ | Parent province ID |
| `status` | string | ❌ | `"active"` or `"inactive"` |
| `weight` | integer | ❌ | Sort weight |
| `contact_phone`, `email`, `description`, `slogan`, `domain`, `keywords`, `footer_description`, `social_media`, `logo_url` | mixed | ❌ | SEO / branding fields returned in the city object |

### Response `201`
City object.

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing name |

---

## PATCH `/api/v1/admin/city/{id}`

Update a city.

**Permission:** `ROLE_ADMIN`

All body fields optional.

### Response `200`
Updated city object.

---

## DELETE `/api/v1/admin/city/{id}`

Delete a city.

**Permission:** `ROLE_ADMIN`

### Response `200`
```json
{ "success": true, "data": { "message": "شهر حذف شد" } }
```

---

## GET `/api/v1/categorys/{bundle}` *(Legacy)*

Legacy endpoint that proxies to the new endpoints.

**Permission:** `PUBLIC`

### Path Parameters
| Bundle | Maps to |
|--------|---------|
| `state` | `/api/v1/provinces` |
| `city` | `/api/v1/cities` |
| `specially_doctor` | `/api/v1/specialties` |
| `doctor_services` | `/api/v1/doctor-services` |
| `insurance_type` | `/api/v1/insurances?type=basic` |
| `supplementary_insurance` | `/api/v1/insurances?type=supplementary` |
| `tag` | `/api/v1/tags` |

> ⚠️ Response is **triple-nested**: `data?.data?.data ?? []`  
> Note: `categorys` (not `categories`) is intentional — legacy route name.

---

## Bulk import / export

Full-table JSON export and strict wipe+replace import for this category live under
`/api/v1/admin/categories/{bundle}/{export|import}` — see [category-import.md](category-import.md).


### Sorting by id

The admin list endpoint accepts `sort=id&order=asc|desc` to order by `id`
(used by the admin «دسته‌بندی‌ها» page when clicking the «شناسه» column).
Without `sort`, the default ordering (weight/name) is unchanged.
