# Appointment API

> **Prefix:** `/api/v1/appointment*`

---

## GET `/api/v1/appointment-slots`

Get all appointment slots (available and booked) for a doctor on a specific date.

**Permission:** `PUBLIC`

### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `date` | string | ✅ | Date in `Y-m-d` format (e.g. `2024-06-15`) |

### Response `200`
```json
{
  "success": true,
  "data": {
    "doctor_uuid": "550e8400-...",
    "date": "2024-06-15",
    "sessions": [
      {
        "start_time": "09:00",
        "end_time": "13:00",
        "slots": [
          {
            "start": 1718438400,
            "end": 1718439600,
            "start_time": "09:00",
            "end_time": "09:20",
            "location_id": null,
            "is_available": true
          },
          {
            "start": 1718439600,
            "end": 1718440800,
            "start_time": "09:20",
            "end_time": "09:40",
            "location_id": null,
            "is_available": false
          }
        ]
      },
      {
        "start_time": "15:00",
        "end_time": "17:00",
        "slots": [...]
      }
    ]
  }
}
```

> Returns **all** slots grouped by work shift. `is_available: false` means the slot is either already booked (active pending/confirmed appointment) **or** its start time has already passed (for today's date). Session boundaries match the doctor's `WeeklySchedule` or date override config.
>
> Returns an **empty** `sessions` array when the date is a holiday, a closed date override, in the past, beyond the doctor's booking window, or when online booking is disabled (see `meta` in `appointment-settings.md`).

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
| `ERR_VALIDATION_001` | 422 | Missing or invalid date/doctor_uuid |

---

## GET `/api/v1/appointment-settings/month-availability/{doctorUuid}`

Which days of a month are bookable — used by the public calendar to grey out unavailable days.

**Permission:** `PUBLIC`

### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `doctorUuid` | string (UUID) | Doctor UUID |

### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `year` | integer | ✅ | **Gregorian** year (e.g. `2026`) |
| `month` | integer | ✅ | Gregorian month `1`–`12` |

> Input is Gregorian. A Jalali (Shamsi) front-end must convert the displayed month to the Gregorian month(s) it spans before calling.

### Response `200`
```json
{
  "success": true,
  "data": {
    "year": 2026,
    "month": 6,
    "disabled_dates": ["2026-06-01", "2026-06-17", "2026-06-26"],
    "enabled_dates": ["2026-06-15", "2026-06-16", "2026-06-18"],
    "online_booking_enabled": true,
    "booking_window": { "value": 1, "unit": "month" }
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `disabled_dates` | string[] | `Y-m-d` days with no bookable slot (holiday / closed override / non-working / past / out-of-window) |
| `enabled_dates` | string[] | `Y-m-d` days with at least one slot |
| `online_booking_enabled` | boolean | Doctor's online-booking flag |
| `booking_window` | object | `{ value, unit }` — `unit` is `week` or `month` |

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 404 | Doctor not found |
| `ERR_VALIDATION_001` | 422 | Invalid year/month |

---

## POST `/api/v1/appointment`

Book an appointment slot.

**Permission:** `AUTH` — any authenticated user

### Request Body (`application/json`)
```json
{
  "doctor_uuid": "550e8400-e29b-41d4-a716-446655440000",
  "slot_start": 1718438400,
  "slot_end": 1718439600,
  "for_self": false,
  "patient_name": "علی احمدی",
  "patient_mobile": "09120000000",
  "patient_national_code": "0012345678",
  "patient_gender": "male",
  "patient_reason": "چکاپ",
  "note": "لطفاً سریع ویزیت شوم"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `slot_start` | integer | ✅ | Slot start (Unix timestamp) |
| `slot_end` | integer | ✅ | Slot end (Unix timestamp) |
| `for_self` | boolean | ❌ | `true` (default) = patient is the logged-in payer; `false` = booking for someone else |
| `patient_name` | string | ⚠️ | Required when `for_self=false`; otherwise filled from the payer's profile |
| `patient_mobile` | string | ⚠️ | Required when `for_self=false`; otherwise the payer's mobile |
| `patient_national_code` | string | ❌ | Patient national code (only when for another person) |
| `patient_gender` | string | ❌ | `male` / `female` |
| `patient_reason` | string | ❌ | Reason for visit |
| `note` | string | ❌ | Patient note |

> **Payer vs patient:** the authenticated user (`user`) is always the payer; the `patient_*` fields describe who the visit is for and are stored separately. **Temporary lock:** the slot is held by the new `pending` booking for **15 minutes** (`expires_at = created_at + 900`). If payment is not completed in time, the booking is moved to `expired` and the slot is freed (see `app:cancel-expired-appointments`). An expired pending booking no longer blocks the slot even before the cron runs.

### Response `201`
```json
{
  "success": true,
  "data": {
    "uuid": "appt-uuid-...",
    "doctor": { "uuid": "...", "name": "دکتر علی احمدی" },
    "user": { "uuid": "...", "mobile": "..." },
    "slot_start": 1718438400,
    "slot_end": 1718439600,
    "status": "pending",
    "note": "...",
    "expires_at": 1718438100,
    "patient_name": "علی احمدی",
    "patient_mobile": "09120000000",
    "patient_national_code": "0012345678",
    "patient_gender": "male",
    "patient_reason": "چکاپ",
    "version": 1,
    "created_at": 1717000000
  }
}
```

**Appointment Status Values:**
| Value | Description |
|-------|-------------|
| `pending` | Awaiting payment |
| `confirmed` | Paid and confirmed |
| `cancelled` | Cancelled |
| `completed` | Visit completed |
| `no_show` | Patient did not show |

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_VALIDATION_002` | 404 | Doctor not found |
| `ERR_CONFLICT_001` | 409 | Slot already booked (incl. concurrent booking — the booking is atomic) |
| `ERR_VALIDATION_001` | 422 | Invalid slot times, past slot, or missing patient name/mobile when `for_self=false` |

---

## GET `/api/v1/appointment/{uuid}`

Get appointment detail.

**Permission:** `AUTH` — must be the patient, the doctor, or `ROLE_ADMIN`

### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Appointment UUID |

### Response `200`
```json
{
  "success": true,
  "data": {
    "uuid": "appt-uuid-...",
    "doctor": {
      "uuid": "...",
      "title": "دکتر علی احمدی",
      "image": "https://..."
    },
    "user": {
      "uuid": "...",
      "real_name": "کاربر",
      "mobile_number": "09..."
    },
    "slot_start": 1718438400,
    "slot_end": 1718439600,
    "status": "confirmed",
    "note": "...",
    "price": 500000,
    "payment_uuid": "pay-uuid-...",
    "created_at": 1717000000
  }
}
```

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the patient/doctor/admin |
| `ERR_NOT_FOUND_001` | 404 | Appointment not found |

---

## GET `/api/v1/appointments/doctor/{doctorUuid}`

Get all appointments for a specific doctor.

**Permission:** `AUTH` — must be the doctor, their secretary, or `ROLE_ADMIN`

### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `doctorUuid` | string (UUID) | Doctor UUID |

### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | ❌ | Filter: `pending`, `confirmed`, `cancelled`, `completed`, `no_show` |

### Response `200`
```json
{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "user": { "uuid": "...", "real_name": "..." },
      "slot_start": 1718438400,
      "slot_end": 1718439600,
      "status": "confirmed",
      "price": 500000
    }
  ]
}
```

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not authorized to view this doctor's appointments |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |

---

## GET `/api/v1/appointments/user`

Get all appointments for the authenticated user.

**Permission:** `AUTH`

### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | ❌ | Filter by status |

### Response `200`
```json
{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "doctor": { "uuid": "...", "title": "دکتر علی احمدی" },
      "slot_start": 1718438400,
      "slot_end": 1718439600,
      "status": "confirmed",
      "price": 500000
    }
  ]
}
```

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |

---

## PATCH `/api/v1/appointment/{uuid}/status`

Change appointment status.

**Permission:** `AUTH` — patient can cancel; doctor/secretary can confirm/complete/no_show; admin can do all

### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Appointment UUID |

### Request Body
```json
{
  "status": "cancelled",
  "version": 3
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | ✅ | New status value |
| `version` | integer | ❌ | Optimistic lock version (prevents double-submit) |

**Allowed Transitions by Role:**
| Actor | Allowed transitions |
|-------|---------------------|
| Patient | `pending → cancelled` |
| Doctor / Secretary | `pending → confirmed`, `confirmed → completed`, `confirmed → no_show` |
| Admin | Any transition |

### Response `200`
Updated appointment object.

### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not authorized for this transition |
| `ERR_NOT_FOUND_001` | 404 | Appointment not found |
| `ERR_CONFLICT_001` | 409 | Version mismatch (optimistic lock) |
| `ERR_VALIDATION_001` | 422 | Invalid status value |

---

## POST `/api/v1/my/appointment`

Create a new appointment for a patient. Used by doctor/clinic/secretary to book appointments on behalf of patients. If no user exists with the given mobile, a new user account is created automatically.

**Auth:** `IS_AUTHENTICATED_FULLY` — Roles: `ROLE_DOCTOR`, `ROLE_CLINIC`, `ROLE_SECRETARY`, `ROLE_ADMIN`

### Request Body
```json
{
  "doctor_uuid": "doctor-uuid",
  "slot_start": 1718438400,
  "slot_end": 1718439600,
  "patient_mobile": "09123456789",
  "patient_name": "علی محمدی",
  "note": "optional note"
}
```

> اگر کاربری با این شماره موبایل وجود نداشته باشد، یک کاربر جدید با نقش `ROLE_USER` ساخته می‌شود.

### Response `201`
```json
{
  "success": true,
  "data": {
    "uuid": "appt-uuid",
    "slot_start": 1718438400,
    "slot_end": 1718439600,
    "status": "pending"
  }
}
```

### Error Responses
| Code | HTTP | Description |
|------|------|-------------|
| `FORBIDDEN` | 403 | Role not allowed |
| `VALIDATION` | 422 | Missing required fields |
| `DOCTOR_NOT_FOUND` | 404 | Doctor UUID not found |
| `SLOT_TAKEN` | 409 | Slot already booked |

---

## GET /api/v1/my/appointments

Role-aware paginated list of appointments. Returns only what the authenticated user is authorized to see.

**Auth:** `IS_AUTHENTICATED_FULLY` (any role)

**Role behavior:**
| Role | Scope |
|------|-------|
| `ROLE_ADMIN` | All appointments |
| `ROLE_CLINIC` | Appointments for doctors in this clinic |
| `ROLE_DOCTOR` | Appointments for this doctor |
| `ROLE_SECRETARY` | Appointments for the linked doctor (empty if `appointments.view` permission is false) |

### Query Parameters
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `page` | int | 1 | Page number |
| `limit` | int | 15 | Items per page (max 100) |
| `search` | string | — | Search by mobile, real name, or doctor name |
| `status` | string | — | Filter by appointment status |
| `date` | string | — | Filter by date in `Y-m-d` format |

### Response `200`
```json
{
  "success": true,
  "data": [
    {
      "uuid": "string",
      "patient_name": "string",
      "patient_mobile": "string",
      "doctor_name": "string",
      "clinic_name": "string | null",
      "appointment_date": "2026-07-25",
      "appointment_time": "14:30",
      "slot_start": 1700000000,
      "status": "reserved",
      "amount": 0,
      "created_at": "ISO 8601 string"
    }
  ],
  "meta": {
    "totalRecords": 8000,
    "totalPages": 533,
    "currentPage": 1
  }
}
