Files
clinicpro/docs/api/appointment.md
T
hamedandClaude Fable 5 4678739d15 feat(appointments): backend for clinic workflow (Figma نوبت‌ها) — phase A
Extend Appointment for the clinic-facing appointments area:

- New nullable relations service_section/service_item/staff (بخش/سرویس/پرسنل)
  plus deposit_required/deposit_amount_rials (بیعانه) and is_reserve.
- New statuses following_up (در حال پیگیری) and salon (سالن) with day-of
  transition rules; reserve entries never occupy a slot (several reserves may
  share one day), enforced in refreshActiveSlotKey.
- rescheduleTo(slotStart, slotEnd, isReserve) keeps active_slot_key consistent
  for جا به جایی and reserve transfers.
- New PATCH /api/v1/appointment/{uuid}: partial update covering edit, slot
  move (409 on taken slot, race backstop on the unique key), reserve toggle,
  patient swap (جایگزینی) and optional status transition; optimistic lock via
  version like the status endpoint.
- POST /my/appointment now accepts the workflow fields and is_reserve
  (day-level entry: no past-slot rule, no atomic slot booking); GET
  /my/appointments gains reserve=1 and returns the new fields per row.

Migration Version20260713195434 (+ mirrored on db_test). Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 23:37:32 +03:30

19 KiB
Raw Blame History

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

{
  "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 112

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

Response 200

{
  "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)

{
  "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": "man",
  "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 کد ملی بیمار — همیشه الزامی (هر دو حالت for_self). باید ۱۰ رقم معتبر باشد (isValidIranNationalCode)؛ ارقام فارسی به انگلیسی تبدیل می‌شوند
patient_gender string جنسیت بیمار — همیشه الزامی. ورودی man/male یا woman/female پذیرفته می‌شود و به فرمِ متعارف man/woman ذخیره می‌گردد
patient_reason string Reason for visit
note string Patient note
city_id integer شناسه‌ی شهرِ دامنه‌ی جاری (از city.json سایت). برای گاردِ پورسانت نماینده: اگر شهر نماینده‌ی فعال داشته باشد، booking_representation_id نوبت ست می‌شود. پورسانت فقط وقتی واریز می‌شود که این نماینده با نماینده‌ی پزشک یکی باشد. خالی/ناموجود ⇒ بدون پورسانت

آدرس نوبت: آدرس (address_id) ارسالی نیست؛ سرور آن را از روی location_id همان session در برنامه‌ی هفتگی که اسلات در آن قرار دارد، خودکار تعیین و ذخیره می‌کند. در پاسخ به‌صورت address_id برمی‌گردد. همه‌ی مسیرهای رزرو (آنلاین POST /api/v1/appointment، منشی POST /api/v1/my/appointment، ادمین) آدرس را به همین شکل ست می‌کنند.

تضمین عدم رزرو دوگانه: هر سه مسیر رزرو از AppointmentRepository::bookAtomically() عبور می‌کنند و یک قید یکتای دیتابیسی (active_slot_key) پشت آن قرار دارد؛ بنابراین حتی در شرایط رقابتی (race) فقط یک نوبتِ زنده روی هر (doctor, slot_start) ممکن است و درخواست بازنده 409 SLOT_TAKEN می‌گیرد. نوبت‌های لغو/منقضی اسلات را آزاد می‌کنند (کلید NULL).

Auto-add to clinic: هنگام تأیید نوبت، اگر آدرس نوبت متعلق به یک کلینیک باشد (DoctorAddress.clinic_id)، بیمار علاوه بر پرونده‌ی پزشک، به پرونده‌های آن کلینیک هم اضافه می‌شود. اگر آدرس کلینیک نداشت ولی دکتر فقط عضو یک کلینیک بود، به همان کلینیک اضافه می‌شود. هر شاخه مشروط به فعال‌بودن patient_records. جزئیات در docs/api/patient.md.

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

{
  "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": "man",
    "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, missing patient name/mobile when for_self=false, missing/invalid patient_national_code, or patient_gender not in man/male/woman/female

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

{
  "success": true,
  "data": {
    "uuid": "appt-uuid-...",
    "doctor": {
      "uuid": "...",
      "name": "دکتر علی احمدی",
      "specialties": [
        { "uuid": "...", "name": "اورولوژی عمومی" }
      ]
    },
    "address": {
      "uuid": "...",
      "name": "مطب دکتر علی احمدی",
      "address": "یزد، خیابان ...",
      "telephone": "035...",
      "map": { "latitude": "31.8", "longitude": "54.3" },
      "city": { "id": "132", "name": "یزد" },
      "province": { "id": "100", "name": "یزد" }
    },
    "user": { "uuid": "...", "mobile": "09..." },
    "slot_start": 1718438400,
    "slot_end": 1718439600,
    "status": "confirmed",
    "note": "...",
    "patient_name": "...",
    "patient_mobile": "...",
    "created_at": 1717000000
  }
}

doctor.specialties آرایه (ممکن است خالی)؛ address اولین آدرس پزشک است (ممکن است null اگر پزشک آدرسی ندارد). address.map.latitude/longitude رشته یا null. تاریخ‌ها Unix.

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

{
  "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

{
  "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

{
  "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

Scope enforced: the caller must be related to the target doctor_uuid, not merely hold an allowed role. A doctor may book only onto their own calendar; a clinic only onto doctors that belong to it; a secretary only within their active clinic/doctor scope and with the appointments.create permission; admin onto any. Otherwise 403 FORBIDDEN.

Request Body

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

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

Response 201

{
  "success": true,
  "data": {
    "uuid": "appt-uuid",
    "slot_start": 1718438400,
    "slot_end": 1718439600,
    "status": "pending"
  }
}

Error Responses

Code HTTP Description
FORBIDDEN 403 Role not allowed, or caller not scoped to this doctor
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)
(plain patient ROLE_USER) The patient's own appointments (a.user = current user)

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

{
  "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
  }
}

---

## Clinic workflow extensions (نوبت‌ها  Figma)

New optional fields on `Appointment` (all backward-compatible): `service_section` (بخش), `service_item` (سرویس), `staff` (پرسنل), `deposit_required` / `deposit_amount_rials` (بیعانه), `is_reserve` (نوبت رزرو  day-level, never occupies a slot).

New statuses: `following_up` (در حال پیگیری), `salon` (سالن). Transitions:
`pending  confirmed|following_up|cancelled_*|expired` · `confirmed  completed|following_up|salon|cancelled_*|no_show` · `following_up  confirmed|salon|completed|cancelled_*|no_show` · `salon  completed|following_up|cancelled_*|no_show`

### PATCH `/api/v1/appointment/{uuid}`
General update (ویرایش / جا به جایی / انتقال به رزرو / جایگزینی). All body fields optional; only present keys change. **Permission:** appointment's patient, owning doctor, or admin.

```json
{
  "slot_start": 1731000000, "slot_end": 1731001800,
  "is_reserve": false,
  "service_section_uuid": "…", "service_item_uuid": "…", "staff_uuid": "…",
  "deposit_required": true, "deposit_amount_rials": 5000000,
  "note": "…", "patient_name": "…", "patient_mobile": "…",
  "status": "confirmed", "version": 3
}
  • slot_start/slot_end must be sent together; moving to an occupied slot → 409.
  • Relation uuids: empty string clears; unknown uuid → 422.
  • status follows the same transition rules as PATCH /appointment/{uuid}/status.
  • Optimistic lock via version409 on concurrent edit.

Response 200: { success, data: { data: <appointment.toArray()> } }

HTTP Description
404 نوبت یافت نشد
403 not patient/doctor/admin
422 half slot pair, end < start, unknown relation uuid, invalid transition
409 slot taken or version conflict

POST /api/v1/my/appointment (extended)

Extra optional body fields: service_section_uuid, service_item_uuid, staff_uuid, deposit_required, deposit_amount_rials, is_reserve. is_reserve: true → day-level reserve entry: slot_end may equal slot_start, the past-slot rule is skipped, and the entry never occupies a slot (several reserves may share a day). Response 201 now also returns is_reserve.

GET /api/v1/my/appointments (extended)

New query param reserve=1 → returns only reserve-list entries; without it only regular slot bookings are returned. Each row now also includes: patient_uuid, is_reserve, deposit_required, deposit_amount_rials, note, service_section, service_item, staff (each {uuid, name|full_name} or null).