Files
clinicpro/docs/api/doctor.md
T
hamedandClaude Opus 4.8 3363dfbf22 feat(doctor): expose city/state in public doctor list
The public doctor list had no location field, so multi-domain consumers
could not tell which city domain owns a doctor. nobat724_front's sitemap
worked around this by fetching the list once per city (35 sweeps) and
subtracting, costing ~13s to build the root sitemap.

Location is resolved in bulk by DoctorRepository::findLocationsByDoctors
using the same rule the city_id/state_id filter applies: the doctor's own
address first, falling back to the address of a clinic they belong to.
Without the clinic fallback a doctor could match city_id=X yet report no
city, which would break the sitemap's per-domain partitioning.

city/state are arrays with at most one entry, matching the shape already
used by the doctor detail response and the clinic list. A doctor with no
address reports [] rather than null. Multi-location doctors get a single
primary city, mirroring the canonical rule on the public site.

Also surface the applied page size as meta.limit. Repositories silently
clamp limit to 50, which previously made clients believe pagination had
ended early — this is what truncated the sitemap to 50 doctors.

The clinic doctor-list endpoint gets the same location data so both
endpoints agree.

Location resolution costs at most 2 queries regardless of page size,
asserted directly against the repository rather than through the
endpoint, since the endpoint carries a pre-existing specialties N+1 in
findWithFilters that is unrelated to this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 08:08:48 +03:30

19 KiB
Raw Blame History

Doctor API

فیلد owner_status (claimed | unclaimed | pending_transfer) به خروجی لیست و جزئیات پزشک اضافه شده است — پروفایل unclaimed (ایمپورت نظام پزشکی) در سایت دکمهٔ «تصاحب پروفایل» می‌گیرد (docs/api/doctor-claim.md) و نوبت‌دهی آنلاینش غیرفعال است.

امتیاز فقط برای پروفایل claimed منتشر می‌شود. برای هر owner_status غیر از claimed (یعنی unclaimed / pending_transfer) فیلدهای point و satisfaction مقدار null برمی‌گردند — مقدار پیش‌فرض انتیتی (3.5 / 60) نشتی نمی‌کند تا امتیاز جعلی/AggregateRating جعلی ساخته نشود. بعد از تصاحب و claimed شدن، امتیاز واقعی به‌صورت خودکار برمی‌گردد.

Prefix: /api/v1/doctor, /api/v1/doctors, /api/v1/clinic-pro/doctor-address*

Numeric path params on the address routes (doctor-address/{id}, doctor-addresses/{doctorId}) require \d+; a non-numeric value returns a clean 404 instead of a 500.


POST /api/v1/doctor

Create a doctor profile for the authenticated user.

Permission: AUTH — any authenticated user

Request Body (application/json)

{
  "title": "دکتر علی احمدی",
  "gender": "male",
  "medical_system_code": "12345",
  "degree": "متخصص",
  "info": "توضیحات درباره پزشک",
  "specialties": [1, 2],
  "doctor_services": [3, 4]
}
Field Type Required Description
title string Full name with title
gender string "male" or "female"
medical_system_code string Nظام پزشکی code
degree string Academic degree
info string Bio/description
specialties integer[] Array of specialty IDs
doctor_services integer[] Array of doctor service IDs
activity_time integer Unix timestamp (ثانیه) تاریخ شروع فعالیت؛ مبنای محاسبهٔ experience (سال تجربه) در پاسخ

Response 201

{
  "success": true,
  "data": {
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "title": "دکتر علی احمدی",
    "gender": "male",
    "medical_system_code": "12345",
    "degree": "متخصص",
    "info": "...",
    "doctor_rate": null,
    "active_doctor_appointment": false,
    "specialties": [],
    "doctor_services": [],
    "created_at": 1717000000
  }
}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing or invalid token
ERR_CONFLICT_001 409 Doctor profile already exists for this user
ERR_VALIDATION_002 422 Missing required field

GET /api/v1/doctor/{uuid}

Get doctor detail with clinics.

Permission: PUBLIC

Path Parameters

Param Type Description
uuid string (UUID) Doctor UUID

Response 200

{
  "success": true,
  "data": {
    "data": {
      "uuid": "550e8400-...",
      "name": "دکتر علی احمدی",
      "gender": "man",
      "medical_system_code": "12345",
      "degree": "specialist",
      "detail": "...",
      "img": [],
      "social_media": {
        "instagram": "https://instagram.com/dr.example",
        "telegram": "https://t.me/dr_example",
        "aparat": null,
        "youtube": null,
        "linkedin": null
      },
      "satisfaction": "60",
      "point": "3.5",
      "free_turn": "دوشنبه 09:0013:00",
      "hours_of_work": "شنبه: 09:0013:00 و 14:0018:00 | یکشنبه: 09:0013:00",
      "active": true,
      "owner_status": "claimed",
      "specialties": [{ "uuid": "...", "id": "1", "name": "قلب و عروق", "parent_id": null }],
      "expertise": [{ "uuid": "...", "id": "3", "name": "نوار قلب" }],
      "address": [],
      "state": [],
      "city": [],
      "clinics": [{ "uuid": "...", "name": "کلینیک الوند", "address": "...", "telephone": "..." }],
      "representation": { "id": 12, "uuid": "9c1...", "full_name": "علی محمدی" }
    }
  }
}

⚠️ Double-nested: Frontend extracts with data?.data?.data

representation نماینده‌ی مالکِ پزشک است؛ برای پزشکِ بدون نماینده null.

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 Doctor not found

GET /api/v1/clinic/my-doctor/{doctorUuid}

Get doctor detail for clinic owner — only doctors who are members of the authenticated clinic.

Permission: ROLE_CLINIC

Path Parameters

Param Type Description
doctorUuid string (UUID) Doctor UUID

Response 200

{
  "success": true,
  "data": {
    "data": {
      "uuid": "...",
      "title": "دکتر علی احمدی",
      "specialties": [...],
      "clinics": [{ "uuid": "...", "name": "کلینیک نور", "address": "...", "telephone": "..." }]
    }
  }
}

⚠️ Double-nested: Frontend extracts with data?.data?.data

Side note: Returns only the authenticated clinic's data in the clinics array (not all clinics of the doctor).

Errors

Code HTTP Description
ERR_VALIDATION_002 404 Clinic or doctor not found
ERR_AUTH_006 403 Doctor is not a member of this clinic

Schedule Fields Notes

تجمیع همهٔ برنامه‌ها (2026-07): این فیلدها روی همهٔ برنامه‌های هفتگی پزشک محاسبه می‌شوند — برنامهٔ مطب شخصی (clinic_id IS NULL) به‌علاوهٔ یک برنامه به ازای هر کلینیک. پزشکی که برنامهٔ شخصی خالی/خاموش ولی برنامهٔ کلینیکیِ فعال دارد، active=true می‌گیرد؛ برنامهٔ یک محیط هرگز محیط دیگر را نمی‌پوشاند.

Field When at least one schedule is bookable When none
free_turn نزدیک‌ترین روز/ساعت کاری از امروز، بین همهٔ برنامه‌های روشن (مثلاً «دوشنبه ۹:۰۰–۱۳:۰۰») «نوبت آزادی موجود نیست»
hours_of_work خلاصه ساعت‌های همان برنامه‌ای که free_turn را داده (ساعت‌های دو محل با هم ترکیب نمی‌شوند) «برنامه کاری تنظیم نشده»
active activeDoctorAppointment && (∃ schedule: online_booking_enabled && has_active_sessions) false — نوبت‌دهی غیرفعال

نوبت‌دهی آنلاین غیرفعال: اگر meta.online_booking_enabled در همهٔ برنامه‌های پزشک false باشد، free_turn برابر "نوبت‌دهی آنلاین غیرفعال است" و active برابر false برمی‌گردد؛ hours_of_work در صورت وجود برنامه حفظ می‌شود. تا وقتی حتی یک برنامه روشن و دارای روز فعال باشد، همان مبنا قرار می‌گیرد. چنین پزشکی (همه خاموش) در لیست عمومی GET /api/v1/doctors (پیش‌فرض active=true) نمایش داده نمی‌شود، ولی صفحه‌ی تکی GET /api/v1/doctor/{slug} همچنان قابل دسترسی است.


GET /api/v1/doctors

List doctors with pagination and filters.

Permission: PUBLIC

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 10. حداکثر ۵۰ — مقادیر بزرگ‌تر بی‌صدا به ۵۰ کاهش می‌یابند. مقدار واقعاً اعمال‌شده در meta.limit برمی‌گردد؛ برای پیمایش کامل به meta.totalPages تکیه کن، نه به «تعداد آیتم کمتر از limit درخواستی»
search string Search in title
specialty_id integer Filter by specialty ID
city_id integer Filter by city ID — شامل دکترهایی که آدرس شخصی‌شان (doctor_addresses.city_id, با doctor_id مقداردار) در آن شهر است یا از طریق کلینیکی که آدرس آن در آن شهر است (doctor_addresses.clinic_id)
state_id integer Filter by province ID — بر اساس آدرس شخصی پزشک (doctor_addresses.province_id) یا آدرس کلینیک
domain string دامنه‌ی سایتِ درخواست‌کننده. اگر دامنه‌ی یک نماینده سراسری باشد، فقط پزشکانِ همان نماینده برمی‌گردند و city_id/state_id نادیده گرفته می‌شوند؛ دامنه شهری/ناشناخته اثری ندارد

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "name": "دکتر علی احمدی",
      "gender": "man",
      "degree": "specialist",
      "img": [],
      "specialties": [{ "uuid": "...", "id": "1", "name": "قلب و عروق" }],
      "satisfaction": "60",
      "point": "3.5",
      "free_turn": "دوشنبه 09:0013:00",
      "hours_of_work": "شنبه: 09:0013:00 و 14:0018:00 | یکشنبه: 09:0013:00",
      "active": true,
      "owner_status": "claimed",
      "city": [
        { "uuid": "7bfb989e-...", "id": "123", "name": "یاسوج", "parent": "23" }
      ],
      "state": [
        { "uuid": "7bfb5705-...", "id": "23", "name": "کهگیلویه و بویراحمد" }
      ]
    }
  ],
  "meta": {
    "totalRecords": 50,
    "totalPages": 3,
    "currentPage": 1,
    "limit": 50
  }
}

point و satisfaction فقط برای owner_status="claimed" مقدار دارند؛ برای unclaimed/pending_transfer هر دو null هستند.

city / state در پاسخ لیست

آرایه با حداکثر یک عضو — هم‌شکل با city/state در پاسخ جزئیات پزشک و پاسخ لیست کلینیک‌ها.

  • منبع مکان دقیقاً همان قاعده‌ای است که فیلتر city_id/state_id اعمال می‌کند: اول آدرس شخصی پزشک (doctor_addresses با doctor_id مقداردار)، و اگر نداشت آدرس کلینیکی که عضو آن است (doctor_addresses با clinic_id مقداردار و doctor_id تهی). یعنی هر پزشکی که با city_id=X برگردد، در پاسخ هم همان شهر را اعلام می‌کند.
  • پزشک چند-مطبی یک شهر اصلی می‌گیرد (اولین مکان یافت‌شده) — نه فهرست همهٔ شهرها.
  • پزشک بدون هیچ آدرس: "city": [] و "state": [] (آرایهٔ خالی، نه null).
  • city[].parent شناسهٔ استان است.
  • استخراج مکان دسته‌ای انجام می‌شود (DoctorRepository::findLocationsByDoctors) — حداکثر دو کوئری ثابت، مستقل از تعداد پزشکان در صفحه.

🔗 مصرف‌کننده: nobat724_front/app/sitemap.js با این فیلد تشخیص می‌دهد هر پزشک به کدام دامنهٔ شهری تعلق دارد (canonical). تغییر شکل این فیلد قرارداد آن را می‌شکند.


PATCH /api/v1/doctor/{uuid}

Update doctor profile.

Permission: AUTH — must be the owner (or ROLE_ADMIN)

Path Parameters

Param Type Description
uuid string (UUID) Doctor UUID

Request Body (application/json)

Same fields as POST (all optional), plus:

Field Type Description
social_media object Keys: instagram, telegram, aparat, youtube, linkedin. Each value must be a full valid URL or null. Any value that fails FILTER_VALIDATE_URL is silently stored as null.
{
  "social_media": {
    "instagram": "https://instagram.com/dr.example",
    "telegram": "https://t.me/dr_example",
    "aparat": null,
    "youtube": null,
    "linkedin": null
  }
}

Response 200

Updated doctor object (same structure as GET single).

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not the owner
ERR_NOT_FOUND_001 404 Doctor not found

DELETE /api/v1/doctor/{uuid}

Delete a doctor profile.

Permission: IS_AUTHENTICATED_FULLYadmin (any profile) or the owner of a claimed profile (doctor.user === current user). Other users get 403.

Side effect: the doctor's insurance configuration (tenant_insurances, entity_insurance_pricing, and their tenant_service_coverages) is purged in the same request — these reference the doctor via a polymorphic entity_id with no DB FK, so the cleanup is enforced in the application.

Guard: a doctor with existing appointments cannot be deleted (the appointments.doctor_id FK would otherwise raise a 500). The endpoint pre-checks and returns 409 ERR_CONFLICT_001 instead.

Path Parameters

Param Type Description
uuid string (UUID) Doctor UUID

Response 200

{
  "success": true,
  "data": { "message": "پزشک با موفقیت حذف شد" }
}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not admin and not the owner of this claimed profile
ERR_VALIDATION_002 404 Doctor not found
ERR_CONFLICT_001 409 Doctor has existing appointments and cannot be deleted

POST /file/upload/clinic_pro/doctor/field_image

Upload doctor profile image.

Permission: AUTH

Request

Content-Type: multipart/form-data

Field Type Required Description
file binary Image file (max 5MB)

Response 200

{
  "success": true,
  "data": {
    "url": "https://clinic-pro.ddev.site/uploads/doctor/abc123.jpg",
    "uuid": "...",
    "filename": "abc123.jpg",
    "filemime": "image/jpeg",
    "filesize": 204800
  }
}

Errors

Code HTTP Description
ERR_FILE_001 422 Invalid file type
ERR_AUTH_001 401 Missing token

GET /api/v1/clinic-pro/doctor-addresses/{doctorId}

Get all practice addresses for a doctor, including addresses of clinics the doctor is a member of.

Permission: PUBLIC

Path Parameters

Param Type Description
doctorId integer Doctor's numeric ID (route requires \d+; non-numeric → 404)

Response 200

{
  "success": true,
  "data": [
    {
      "id": "1",
      "uuid": "...",
      "type": "personal",
      "clinic_id": null,
      "clinic_name": null,
      "name": "مطب تهران",
      "address": "تهران، خیابان...",
      "telephone": "02112345678",
      "map": { "latitude": "35.6892", "longitude": "51.3890" },
      "city": { "id": "1", "name": "تهران" },
      "province": { "id": "1", "name": "تهران" }
    },
    {
      "id": "5",
      "uuid": "...",
      "type": "clinic",
      "clinic_id": 12,
      "clinic_name": "کلینیک الوند",
      "name": null,
      "address": "اصفهان، خیابان...",
      "telephone": "03112345678",
      "map": { "latitude": null, "longitude": null },
      "city": { "id": "3", "name": "اصفهان" },
      "province": { "id": "2", "name": "اصفهان" }
    }
  ]
}

نکته: آدرس‌های با type: "clinic" از کلینیک‌هایی که پزشک عضو آن‌هاست می‌آیند و clinic_name نام کلینیک را نشان می‌دهد.


POST /api/v1/clinic-pro/doctor-address

Add a new practice address.

Permission: AUTH — must own the doctor profile

Request Body

{
  "name": "مطب تهران",
  "address": "تهران، خیابان ولیعصر",
  "telephone": "02112345678",
  "province_id": 1,
  "city_id": 3,
  "latitude": 35.6892,
  "longitude": 51.3890
}
Field Type Required
name string
address string (frontend validation)
telephone string (frontend validation)
province_id integer (frontend validation)
city_id integer (frontend validation)
latitude float
longitude float

Response 201

{
  "success": true,
  "data": {
    "id": 1,
    "name": "مطب تهران",
    "address": "تهران، خیابان ولیعصر",
    "telephone": "02112345678",
    "latitude": 35.6892,
    "longitude": 51.3890
  }
}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not the doctor owner
ERR_NOT_FOUND_001 404 Doctor not found

PATCH /api/v1/clinic-pro/doctor-address/{id}

Update a practice address.

Permission: AUTH — must own the doctor profile

Path Parameters

Param Type Description
id integer Address ID

Request Body

Same fields as POST — all optional.

Response 200

Updated address object.

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not the owner
ERR_NOT_FOUND_001 404 Address not found

DELETE /api/v1/clinic-pro/doctor-address/{id}

Delete a practice address.

Permission: AUTH — must own the doctor profile

Response 200

{ "success": true, "data": { "message": "آدرس حذف شد" } }

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Not the owner
ERR_NOT_FOUND_001 404 Address not found

POST /api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}

Create a doctor address automatically from a clinic's location.

Permission: AUTH — must own the doctor profile and be associated with the clinic

Path Parameters

Param Type Description
clinicUuid string (UUID) Clinic UUID

Response 201

Address object created from clinic data.

Errors

Code HTTP Description
ERR_FORBIDDEN_001 403 Not associated with this clinic
ERR_NOT_FOUND_001 404 Clinic not found