# Appointment Settings API

> **Prefix:** `/api/v1/appointment-settings`  
> **Permission:** every endpoint requires `AUTH` and resolves access through one shared rule (below)

Doctors configure their availability via three resources: **weekly schedule**, **date overrides**, and **holidays**.

## Booking context (`clinic_uuid`)

Every endpoint in this file operates inside **one booking context**, selected by the optional
`clinic_uuid` parameter (query string on `GET`/`DELETE`, body field on `POST`/`PATCH`):

| `clinic_uuid` | Context | Services usable | Addresses selectable |
|---|---|---|---|
| omitted / `null` | the doctor's **personal practice** | `entity_type='doctor'` | the doctor's own `personal` addresses |
| a clinic uuid | that **doctor inside that clinic** | `entity_type='clinic'` | that clinic's addresses |

**Breaking change (2026-08): a doctor holds exactly ONE weekly schedule**, no matter how many clinics
they work at. What varies from day to day is the *place*: Saturday at the personal practice, Monday at
the clinic. The context of a shift is read from the `location_id` on that shift, not from the record it
lives in. Consequences:

- `clinic_uuid` no longer selects *which record* is read or written — every context reads the same one.
- It still selects **which addresses the caller may assign**, and **which days a booking context sees**:
  a personal-practice secretary never sees the clinic days and cannot book on them, and vice versa.
- A shift whose address belongs to another context is returned to the caller for display but is
  preserved verbatim on save — a clinic manager can neither edit nor delete the doctor's personal shifts.
- `booking_mode` and the rest of `meta` are now doctor-wide, because there is one record. The existing
  "mode is locked after the first save" rule therefore applies across contexts.
- Date overrides and holidays are unchanged and remain per-context.

Services never cross the boundary (they are polymorphic on `service_sections.entity_type`).

If the doctor is not a member of the given clinic → `422 ERR_VALIDATION_001`
(«این پزشک عضو کلینیک انتخاب‌شده نیست»). Unknown clinic → `404 ERR_VALIDATION_002`.

## Access rule

Given the target doctor and the resolved context, access is granted when the caller is:

1. `ROLE_ADMIN`, **or**
2. the doctor themselves, **or**
3. in a **clinic context only**, someone holding that clinic's `appointment_settings` permission —
   `view` for `GET`, `update` for `POST`/`PATCH`/`DELETE` (see `docs/api/clinic.md` →
   *Clinic Doctor Permissions*). The clinic owner always passes this check.

Anything else → `403 ERR_AUTH_006`.

> **Breaking change (2026-07):** a clinic owner can no longer read or write a member doctor's
> **personal** schedule. Without `clinic_uuid` the request targets the personal context, which only
> the doctor and an admin may touch. The clinic panel must send `clinic_uuid`; the admin SPA already
> does (`ScheduleSection` takes a `clinicUuid` prop).

---

## Weekly Schedule

Each doctor has **exactly one weekly schedule** (upsert keyed by `doctor_id`; the legacy `clinic_id`
column stays `NULL` on new rows). Rows created before 2026-08 were merged by
`migrations/Version20260820120000.php`, which appended each clinic record's sessions into the doctor's
single record — the `location_id` already on every shift carries the context.
The schedule is keyed by **day index** (0=Saturday ... 6=Friday), each day containing a `sessions`
array.

### Day Index Convention

| Index | Day (EN) | Day (FA) |
|-------|----------|----------|
| `"0"` | Saturday | شنبه |
| `"1"` | Sunday | یکشنبه |
| `"2"` | Monday | دوشنبه |
| `"3"` | Tuesday | سه‌شنبه |
| `"4"` | Wednesday | چهارشنبه |
| `"5"` | Thursday | پنجشنبه |
| `"6"` | Friday | جمعه |

---

### POST `/api/v1/appointment-settings/weekly-schedule`

Create or update the weekly schedule for a doctor (upsert).

**Permission:** `AUTH` — see [Access rule](#access-rule)

> **الزام آدرس:** هر session با `active=true` باید `location_id` (آدرس مطب/کلینیک) داشته باشد. در غیر این صورت `422 ERR_VALIDATION_001` («برای هر شیفت فعال باید آدرس انتخاب شود»). این آدرس هنگام رزرو خودکار روی نوبت ذخیره می‌شود.
>
> **الزام مکان:** آدرس هر شیفت باید در فهرست `available-locations` همان درخواست‌کننده باشد. پزشک هر دو محیط خودش را دارد؛ کلینیک فقط آدرس خودش. آدرس بیرون از این فهرست → `422 ERR_VALIDATION_001` («آدرس انتخاب‌شده متعلق به این کلینیک نیست»).
>
> **ادغام هنگام ذخیره:** شیفت‌هایی که آدرسشان بیرون از دسترس درخواست‌کننده است، از نسخهٔ ذخیره‌شده دست‌نخورده برمی‌گردند؛ ورودی نه می‌تواند حذفشان کند نه عوضشان.
>
> **نوبت‌دهی سرویسی:** با `meta.booking_mode = "service"` صاحبِ همان context باید حداقل یک سرویس با `bookable = true` داشته باشد؛ وگرنه `422 ERR_VALIDATION_001` روی فیلد `booking_mode`. پیام در محیط کلینیک به کلینیک اشاره می‌کند.

### Request Body (`application/json`)
```json
{
  "doctor_uuid": "550e8400-e29b-41d4-a716-446655440000",
  "clinic_uuid": null,
  "schedule": {
    "0": {
      "sessions": [
        {
          "active": true,
          "location_id": 1973,
          "start_time": "09:00",
          "end_time": "13:00",
          "duration_per_patient": 20,
          "has_rest": true,
          "rest_interval": 60,
          "time_to_rest": 10,
          "patient_limit": null
        },
        {
          "active": true,
          "location_id": 1973,
          "start_time": "15:00",
          "end_time": "18:00",
          "duration_per_patient": 20,
          "has_rest": false,
          "rest_interval": 0,
          "time_to_rest": 0,
          "patient_limit": 5
        }
      ]
    },
    "1": { "sessions": [] },
    "2": { "sessions": [] },
    "3": { "sessions": [] },
    "4": { "sessions": [] },
    "5": { "sessions": [] },
    "6": { "sessions": [] }
  }
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `schedule` | object | ✅ | Keys `"0"` through `"6"` (day indices) |
| `schedule.{n}.sessions` | array | ✅ | Array of session config objects |
| `meta` | object | ❌ | Online-booking settings (see below) |

**Online-booking `meta` object:**

```json
{
  "meta": {
    "online_booking_enabled": true,
    "booking_window_value": 2,
    "booking_window_unit": "month",
    "booking_mode": "service",
    "buffer_minutes": 5
  }
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `online_booking_enabled` | boolean | ❌ | `false` = no online booking; the public slot/month endpoints return no availability |
| `booking_window_value` | integer | ❌ | How far ahead patients may book (≥ 1) |
| `booking_window_unit` | string | ❌ | `"day"`, `"week"` or `"month"` (invalid value keeps the current one) |
| `booking_mode` | string | ❌ | `"slot"` (پیش‌فرض) = نوبت‌دهی اسلاتی با مدت ثابت (`duration_per_patient`). `"service"` = مدت هر نوبت از `duration_minutes` سرویسِ انتخاب‌شده؛ زمان‌ها با `GET /api/v1/appointment-service-slots` گرفته می‌شوند. مقدار نامعتبر نادیده گرفته می‌شود |
| `buffer_minutes` | integer | ❌ | فقط حالت سرویسی: فاصلهٔ بین نوبت‌ها (دقیقه، ≥ 0). در `slot_end` ذخیره نمی‌شود؛ فقط فاصلهٔ بین زمان‌های پیشنهادی |

> Defaults when `meta` is absent: `{ online_booking_enabled: true, booking_window_value: 3, booking_window_unit: "month", booking_mode: "slot", buffer_minutes: 0 }`. `meta` is stored inside the schedule `setting` JSON (no DB migration) and is **preserved** when only `schedule` is sent. `SlotCalculatorService` rejects any date in the past, beyond `today + value unit`, or when online booking is disabled — for the weekly schedule, date overrides, and `appointment-slots` alike.
>
> **اجبار حالت سرویسی:** اگر `booking_mode = service` ذخیره شود ولی پزشک هیچ سرویسِ «نمایش در نوبت‌دهی» (`bookable = true`) نداشته باشد، `POST`/`PATCH` برنامهٔ هفتگی با `422` (`ERR_VALIDATION_001`, field `booking_mode`) رد می‌شود.
>
> **غیرقابل‌تغییر پس از ثبت:** `booking_mode` فقط تا **اولین ثبت** قابل‌انتخاب است. پس از آنکه یک‌بار به‌صورت صریح ذخیره شد (در `setting.meta.booking_mode` نوشته شد)، هر `POST`/`PATCH` که آن را تغییر دهد با `422` («نوع نوبت‌دهی پس از ثبت قابل تغییر نیست»، field `booking_mode`) رد می‌شود. پاسخِ `toArray` فیلد boolean `booking_mode_locked` را برمی‌گرداند (`true` = قفل‌شده) تا پنل توگل را غیرفعال کند. رکوردهای قدیمی که هنوز mode صریح ندارند، `booking_mode_locked=false` و یک‌بار قابل‌انتخاب‌اند.

**Session Config Object:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `active` | boolean | ✅ | Whether this session is active |
| `location_id` | integer\|null | ❌ | Doctor address/location ID |
| `start_time` | string | ✅ | Session start `"HH:MM"` |
| `end_time` | string | ✅ | Session end `"HH:MM"` |
| `duration_per_patient` | integer | ✅ | Minutes per appointment slot |
| `has_rest` | boolean | ❌ | Whether to insert rest breaks |
| `rest_interval` | integer | ❌ | Work minutes before taking a rest break |
| `time_to_rest` | integer | ❌ | Duration of each rest break (minutes) |
| `patient_limit` | integer\|null | ❌ | Max patients per session (`null` = unlimited) |

> Multiple sessions per day are supported (e.g., morning + afternoon). Sessions are sorted by `start_time` and overlapping ones are skipped.

### Response `201`
```json
{
  "success": true,
  "data": {
    "data": {
      "uuid": "sched-uuid-...",
      "doctor_uuid": "550e8400-...",
      "schedule": {
        "0": { "sessions": [ { "active": true, "start_time": "09:00", ... } ] },
        "1": { "sessions": [] },
        "2": { "sessions": [] },
        "3": { "sessions": [] },
        "4": { "sessions": [] },
        "5": { "sessions": [] },
        "6": { "sessions": [] }
      },
      "meta": {
        "online_booking_enabled": true,
        "booking_window_value": 3,
        "booking_window_unit": "month",
        "booking_mode": "slot",
        "buffer_minutes": 0
      },
      "booking_mode_locked": true,
      "created_at": 1717000000,
      "updated_at": 1717000000
    }
  }
}
```

> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data`  
> This is an **upsert** — if a schedule already exists for the doctor, it is overwritten.

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_VALIDATION_002` | 404 | Doctor not found |
| `ERR_AUTH_006` | 403 | Not the doctor owner |

---

### GET `/api/v1/appointment-settings/weekly-schedule/{uuid}`

Get weekly schedule. `{uuid}` can be either the **schedule UUID** or the **doctor UUID** — the controller tries both.

**Permission:** `AUTH` (class-level `IS_AUTHENTICATED_FULLY`)

### Response `200`
Same structure as POST response.

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_VALIDATION_002` | 404 | Schedule not found |

---

### PATCH `/api/v1/appointment-settings/weekly-schedule/{uuid}`

Update weekly schedule. `{uuid}` can be schedule UUID or doctor UUID.

**Permission:** `AUTH` — see [Access rule](#access-rule)

### Request Body
```json
{
  "schedule": {
    "0": {
      "sessions": [
        {
          "active": true,
          "location_id": 1973,
          "start_time": "10:00",
          "end_time": "14:00",
          "duration_per_patient": 30,
          "has_rest": false,
          "rest_interval": 0,
          "time_to_rest": 0,
          "patient_limit": null
        }
      ]
    }
  }
}
```

> Replaces the entire `schedule` object if provided.

### Response `200`
Updated schedule object (same structure as POST).

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Schedule not found |

---

### DELETE `/api/v1/booking-setting/{uuid}`

Delete a weekly schedule.

**Permission:** `AUTH` — see [Access rule](#access-rule)

> Note: route is `/booking-setting/`, not `/appointment-settings/`

### Response `200`
```json
{ "success": true, "data": { "message": "برنامه هفتگی با موفقیت حذف شد" } }
```

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Schedule not found |

---

## Date Overrides

Override a specific date — mark it inactive (day off) or give it custom sessions.

### GET `/api/v1/appointment-settings/date-override/list/{doctorUuid}`

Get all date overrides for a doctor.

**Permission:** `AUTH` — see [Access rule](#access-rule) (`403 ERR_AUTH_006` otherwise).

### Response `200`
```json
{
  "success": true,
  "data": {
    "data": [
      {
        "uuid": "...",
        "doctor_uuid": "...",
        "date": 1718476800,
        "date_string": "2024-06-15",
        "active": false,
        "reason": "تعطیل خاص",
        "custom_slots": [],
        "created_at": 1717000000
      }
    ]
  }
}
```

> `date` is a Unix timestamp at **server-local midnight** (`Asia/Tehran`); `date_string` is the same day as `Y-m-d` and is what clients must render — converting the timestamp in a browser with `toISOString()` (UTC) shifts it one day back. `active: false` = entire day blocked. `active: true` with `custom_slots` = custom session schedule.

---

### POST `/api/v1/appointment-settings/date-override`

Create a date override.

**Permission:** `AUTH` — see [Access rule](#access-rule)

### Request Body
```json
{
  "doctor_uuid": "550e8400-...",
  "date": "2024-06-20",
  "active": false,
  "reason": "تعطیل رسمی",
  "custom_slots": null
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `date` | string | ✅ | Date in `Y-m-d` format (e.g. `"2024-06-20"`) |
| `active` | boolean | ❌ | `false` = full day off (default); `true` = use custom_slots |
| `reason` | string | ❌ | Reason for override |
| `custom_slots` | array\|null | ❌ | Custom sessions (same SessionConfig format as weekly schedule, see below) |

**`custom_slots` format when `active: true`:**
```json
{
  "custom_slots": [
    {
      "start_time": "14:00",
      "end_time": "18:00",
      "duration_per_patient": 20,
      "location_id": 1973,
      "has_rest": false,
      "rest_interval": 0,
      "time_to_rest": 0,
      "patient_limit": null
    }
  ]
}
```

**Legacy format (backward compatible):**
```json
{
  "custom_slots": [
    { "start": "14:00", "end": "18:00", "duration": 20 }
  ]
}
```

### Response `201`
```json
{
  "success": true,
  "data": {
    "data": {
      "uuid": "override-uuid-...",
      "doctor_uuid": "...",
      "date": 1718476800,
      "date_string": "2024-06-15",
      "active": false,
      "reason": "تعطیل رسمی",
      "custom_slots": [],
      "created_at": 1717000000
    }
  }
}
```

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Doctor not found |
| `ERR_VALIDATION_001` | 422 | Invalid date format |

---

### GET `/api/v1/appointment-settings/date-override/{uuid}`

Get a single date override.

**Permission:** `AUTH` (class-level)

### Response `200`
Override object (same structure as above).

---

### PATCH `/api/v1/appointment-settings/date-override/{uuid}`

Update a date override.

**Permission:** `AUTH` — see [Access rule](#access-rule)

### Request Body (all optional)
```json
{
  "active": true,
  "reason": "جبران مرخصی",
  "custom_slots": [
    {
      "start_time": "14:00",
      "end_time": "18:00",
      "duration_per_patient": 20,
      "location_id": 1973,
      "has_rest": false,
      "rest_interval": 0,
      "time_to_rest": 0,
      "patient_limit": null
    }
  ],
  "date": "2024-06-21"
}
```

### Response `200`
Updated override object.

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Override not found |

---

### DELETE `/api/v1/appointment-settings/date-override/{uuid}`

Delete a date override.

**Permission:** `AUTH` — see [Access rule](#access-rule)

### Response `200`
```json
{ "success": true, "data": { "message": "Override با موفقیت حذف شد" } }
```

---

## Holidays

Mark a date range as holiday — all slots blocked, no overrides apply.

### GET `/api/v1/appointment-settings/holidays/list/{doctorUuid}`

Get all holidays for a doctor.

**Permission:** `AUTH` — see [Access rule](#access-rule) (`403 ERR_AUTH_006` otherwise).

### Response `200`
```json
{
  "success": true,
  "data": {
    "data": [
      {
        "uuid": "...",
        "doctor_uuid": "...",
        "start_date": 1719792000,
        "end_date": 1720656000,
        "start_date_string": "2024-07-01",
        "end_date_string": "2024-07-11",
        "reason": "تعطیلات تابستانی",
        "active": true,
        "created_at": 1717000000
      }
    ]
  }
}
```

> `start_date` / `end_date` are Unix timestamps at **server-local midnight** (`Asia/Tehran`); `start_date_string` / `end_date_string` carry the same days as `Y-m-d` and are what clients must render (UTC conversion in the browser shifts them one day back). Holidays take **highest priority** — they block the day even if a date override exists.

---

### POST `/api/v1/appointment-settings/holidays`

Create a holiday range.

**Permission:** `AUTH` — see [Access rule](#access-rule)

### Request Body
```json
{
  "doctor_uuid": "550e8400-...",
  "start_date": "2024-07-01",
  "end_date": "2024-07-10",
  "reason": "تعطیلات تابستانی"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `start_date` | string | ✅ | Start date `Y-m-d` |
| `end_date` | string | ✅ | End date `Y-m-d` (must be ≥ start_date) |
| `reason` | string | ❌ | Holiday reason |

### Response `201`
```json
{
  "success": true,
  "data": {
    "data": {
      "uuid": "holiday-uuid-...",
      "doctor_uuid": "...",
      "start_date": 1719792000,
      "end_date": 1720656000,
      "start_date_string": "2024-07-01",
      "end_date_string": "2024-07-11",
      "reason": "تعطیلات تابستانی",
      "active": true,
      "created_at": 1717000000
    }
  }
}
```

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Doctor not found |
| `ERR_VALIDATION_001` | 422 | end_date before start_date or invalid format |

---

### GET `/api/v1/appointment-settings/holidays/{uuid}`

Get a single holiday.

**Permission:** `AUTH` (class-level)

---

### PATCH `/api/v1/appointment-settings/holidays/{uuid}`

Update a holiday.

**Permission:** `AUTH` — see [Access rule](#access-rule)

### Request Body (all optional)
```json
{
  "start_date": "2024-07-02",
  "end_date": "2024-07-12",
  "reason": "تمدید تعطیلات",
  "active": false
}
```

### Response `200`
Updated holiday object.

---

### DELETE `/api/v1/appointment-settings/holidays/{uuid}`

Delete a holiday.

**Permission:** `AUTH` — see [Access rule](#access-rule)

### Response `200`
```json
{ "success": true, "data": { "message": "تعطیلات حذف شد" } }
```

---

## Slot Calculation Logic (Reference)

The `SlotCalculatorService` calculates available slots in this priority order:

1. **Holiday** — if date falls in a holiday range → return empty (no slots)
2. **Date Override** — if a date override exists for this date:
   - `active: false` → return empty
   - `active: true` → use `custom_slots` sessions
3. **Weekly Schedule** — use the day's `sessions` array (only `active: true` sessions, sorted by `start_time`, overlapping sessions skipped)

**Slot output format:**
```json
[
  {
    "start": 1718438400,
    "end": 1718439600,
    "start_time": "09:00",
    "end_time": "09:20",
    "location_id": 1973
  }
]
```

---

## Available Locations

### `GET /api/v1/appointment-settings/available-locations/{doctorUuid}`

**Permission:** `AUTH` — see [Access rule](#access-rule) (`403 ERR_AUTH_006` otherwise).

Returns all locations a doctor can assign as `location_id` in their schedule sessions. Includes both the doctor's personal addresses and the addresses of all clinics they belong to.

#### Response `200`
```json
{
  "success": true,
  "data": [
    {
      "id": "5",
      "uuid": "...",
      "type": "personal",
      "clinic_id": null,
      "clinic_name": null,
      "name": "مطب شخصی",
      "address": "تهران، ...",
      "telephone": "09121234567",
      "map": { "latitude": "35.7", "longitude": "51.4" },
      "city": { "id": "1", "name": "تهران" },
      "province": { "id": "8", "name": "تهران" }
    },
    {
      "id": "12",
      "uuid": "...",
      "type": "clinic",
      "clinic_id": "3",
      "clinic_name": "کلینیک نور",
      "name": "شعبه مرکزی",
      "address": "تهران، خیابان ولیعصر...",
      "telephone": "02112345678",
      "map": { "latitude": "35.699", "longitude": "51.337" },
      "city": { "id": "1", "name": "تهران" },
      "province": { "id": "8", "name": "تهران" }
    }
  ]
}
```

| Field | Description |
|-------|-------------|
| `type` | `personal` = doctor's own address; `clinic` = clinic address |
| `clinic_id` | ID of the clinic (only for type=clinic) |
| `clinic_name` | Name of the clinic (only for type=clinic) |

> Use `id` as the `location_id` value in weekly schedule sessions or date override sessions.

#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 404 | Doctor not found |


---

## Context additions (2026-07)

### Response fields

`WeeklySchedule.toArray()` now also returns:

| Field | Type | Meaning |
|---|---|---|
| `clinic_uuid` | `string\|null` | **legacy**, always `null` on a weekly schedule — the record is no longer owned by one context |
| `context` | `"personal" \| "clinic"` | **legacy**, always `"personal"` for the same reason |
| `locations` | `array` | every address of this doctor (personal + each clinic they belong to), for labelling shifts the caller may not edit — `GET` only |
| `selectable_location_ids` | `int[]` | the subset of those the **caller** may assign — `GET` only |

`DateOverride.toArray()` returns the same two fields. `Holiday.toArray()` returns `clinic_uuid`
plus `scope` (`"global" | "clinic"`), and the list endpoint adds `editable` (see below).

### Holidays are global by default

A holiday means "the doctor is not there", which is a physical fact — so unlike schedules and date
overrides it is **not** per-context by default:

| `clinic_id` | Meaning |
|---|---|
| `NULL` | the doctor is absent **everywhere** — applies to the personal practice and every clinic |
| set | the doctor is absent in that clinic only |

`GET /holidays/list/{doctorUuid}?clinic_uuid=…` returns the **union**: the clinic's own holidays plus
the doctor's global ones. Global rows come back with `editable: false` — a clinic must see that the
doctor is away but may not delete that fact.

`POST /holidays` **without** `clinic_uuid` creates a global holiday and is restricted to the doctor
themselves and admins → otherwise `403 ERR_ACCESS_DENIED` («کلینیک فقط می‌تواند تعطیلی مخصوص خودش را
ثبت کند»). A clinic closing for *all* its doctors is not expressible in this model and needs a
separate `ClinicHoliday` entity — **not implemented**.

### Date overrides are always per-context

`GET /date-override/list/{doctorUuid}?clinic_uuid=…` returns only that context's overrides — no
union, because an override changes working hours and working hours are themselves per-context.

### `GET /available-locations/{doctorUuid}`

Returns the addresses the **caller** may assign, which is not the same as the addresses of the current
context:

| Caller | Returned |
|---|---|
| the doctor themselves, or `ROLE_ADMIN` | every address of theirs — personal **and** each clinic they belong to |
| anyone else (clinic manager, secretary) | only the addresses of the context in `?clinic_uuid=` |

The doctor gets the full set because their schedule is a single one and they move between places from
day to day; a clinic manager gets only its own so it cannot move a shift into the doctor's private
practice.

---

## حالت سوم: نوبت‌دهی منبع‌محور

`meta.booking_mode` مقدار سوم `resource` را هم می‌پذیرد (تسک ۰۶). در این حالت نوبت به
بخش‌ها تقسیم می‌شود و وقت آزاد از تقاطع تقویم منابع می‌آید.

| فیلد `meta` | معنی |
|---|---|
| `booking_mode: "resource"` | حالت منبع‌محور |
| `step_minutes` | گام جستجوی وقت؛ پیش‌فرض ۱۵، کمینه ۵ |

### شرط آمادگی

انتخاب این حالت **برگشت‌ناپذیر** است، پس پیش از ثبت سنجیده می‌شود: محیط باید حداقل یک
**منبع فعال** داشته باشد.

```json
{
  "success": false,
  "data": null,
  "errors": [{
    "code": "ERR_VALIDATION_001",
    "message": "برای نوبت‌دهی منبع‌محور حداقل یک منبع فعال لازم است؛ اول اتاق، اپراتور یا دستگاه تعریف کنید",
    "field": "booking_mode"
  }]
}
```

بدون این نگهبان، کلینیک حالتی را برای همیشه قفل می‌کرد که هیچ وقتی در آن محاسبه نمی‌شود.

پنل همین شرط‌ها را **پیش از** ثبت به‌صورت ✓/✗ نشان می‌دهد تا کاربر به ۴۲۲ نخورد.

### `has_segments` روی فهرست سرویس‌ها

`GET /api/v1/service-items` حالا فیلد `has_segments` هم می‌دهد — با یک کوئری تجمعی برای
کل فهرست، نه یکی per سرویس. تنظیمات نوبت‌دهی از همین می‌فهمد آمادگیِ حالت منبع‌محور
هست یا نه.

جزئیات بخش‌ها: [appointment-plan.md](appointment-plan.md) · منابع: [resource.md](resource.md)
