Files
clinicpro/docs/api/doctor.md
T
hamedandClaude Opus 5 2da5b5188c feat(doctors): search every specialty a doctor has, and expose the tree
`GET /api/v1/doctors` could not answer either question the public search box
asks. Typing a specialty name returned nothing, because `name` only matched
`d.name`. And `specialty_id` matched one id exactly, so a parent group only
found doctors who happened to carry the parent — which they usually do, but
only as a side effect of `expandWithAncestors` running on save. A doctor
imported through any other path has no denormalised parent, and a search
guarantee resting on a save-time side effect is not a guarantee.

`expandWithDescendants` mirrors the existing ancestor walk over the same cached
parentMap, so no extra query. It deliberately keeps unknown ids instead of
dropping them like its mirror does: the result feeds an `IN (...)`, and an empty
array turns the filter into a no-op that returns every doctor — an unknown id
must mean "nothing", never "everything".

Both specialty filters use their own EXISTS alias rather than the shared `s`
join. Two conditions on one alias force a single join row to satisfy both, so a
doctor filtered by specialty A while searching the name of specialty B was
silently dropped. Verified by reverting to the shared alias and watching
testFilterOnOneSpecialtyWhileSearchingTheNameOfAnother fail.

toListArray now carries specialties[].parent_id so a client can tell the main
specialty from a sub-specialty instead of printing all of them. It is a string,
matching toDetailArray and the sibling `id` key — one concept should not have
two types across two endpoints. Reading the id off the parent proxy costs no
query; measured 6→11 queries with four more doctors both with and without the
field. That growth is a pre-existing N+1 (findWithFilters does not fetch-join
specialties, unlike findByClinic) and is left untouched here.

Also drops the phantom `search` parameter from the OpenAPI annotation — it was
advertised but never read, so a client sending it got an unfiltered list — and
documents the six live parameters that were missing.

Note for deploy: DoctorRepository gained a constructor argument, so a stale
container fails with ArgumentCountError until cache:clear runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 16:44:32 +03:30

31 KiB
Raw Blame History

Doctor API

name در برابر display_name: name نامِ خام است (بدون عنوان — برای فرم ویرایش). display_name نامِ آمادهٔ نمایش با پیشوند «دکتر» است که بک‌اند تولید می‌کند (PersianText::withDoctorTitle) تا همهٔ کلاینت‌ها یکسان و بدون «دکتر دکتر …» نشان دهند؛ در لیست و جزئیات پزشک هر دو حاضرند. برای نمایش از display_name، برای ویرایش از name استفاده کنید.

فیلد 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 نام پزشک بدون عنوان. پیشوند «دکتر» سمت سرور با PersianText::stripDoctorTitle() حذف می‌شود؛ نمایش عنوان کار لایهٔ UI است.
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
ERR_VALIDATION_001 422 نام پزشک شماره‌تلفن یا مقدار آزمایشی است

GET /api/v1/doctor/{uuid}

Get doctor detail with clinics.

Permission: PUBLIC — a token is optional and only affects can_edit.

Path Parameters

Param Type Description
uuid string (UUID) Doctor UUID

can_edit

The payload carries can_edit: boolean — whether the caller may PATCH this doctor. It is true for the doctor themselves, ROLE_ADMIN, and the registering representative; false for everyone else and always false without a token. It is computed by the same policy the PATCH gate uses, so clients must read it rather than re-deriving the rule; a client that recomputes will drift.

It says nothing about which fields are writable — a representative sees can_edit: true yet is still limited to the whitelist under PATCH /api/v1/doctor/{uuid}.

Response 200

{
  "success": true,
  "data": {
    "data": {
      "uuid": "550e8400-...",
      "name": "علی احمدی",
      "display_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 در صورت وجود برنامه حفظ می‌شود. تا وقتی حتی یک برنامه روشن و دارای روز فعال باشد، همان مبنا قرار می‌گیرد.

چنین پزشکی (همه برنامه‌ها خاموش، ولی فلگ active_doctor_appointment روشن) در لیست عمومی GET /api/v1/doctors نمایش داده می‌شود — چون هنوز فعال است — ولی با bookableRank پایین‌تر از پزشکان دارای نوبت مرتب می‌شود و تنها با active=1 از نتایج حذف می‌گردد.

پزشک غیرفعال (active_doctor_appointment خاموش): با غیرفعال‌کردن پزشک از پنل ادمین، او دیگر در لیست عمومی GET /api/v1/doctors ظاهر نمی‌شود (پیش‌فرض این اندپوینت پزشکان غیرفعال را کنار می‌گذارد). صفحهٔ تکی GET /api/v1/doctor/{slug} همچنان پاسخ می‌دهد (ادمین/کلینیک از همین اندپوینت برای مشاهده/ویرایش استفاده می‌کنند) و در بدنهٔ پاسخ فیلد خام is_active (= active_doctor_appointment، مستقل از داشتن برنامه) را برمی‌گرداند؛ کلاینت عمومی مثل nobat724 با is_active === false صفحهٔ پزشک را 404 می‌کند. این با فیلد active (که active_doctor_appointment && has_schedule است) فرق دارد.


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 درخواستی»
specialty_id integer Filter by specialty ID — این تخصص و همهٔ زیرشاخه‌هایش. 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 نادیده گرفته می‌شوند؛ دامنه شهری/ناشناخته اثری ندارد
gender string man یا woman
degree string expert, general, specialist, subspecialistplus
name string جستجوی LIKE روی نام پزشک یا نام هر یک از تخصص‌های او. کادر جستجوی سایت عمومی یک فیلد بیشتر ندارد و کاربر در آن هر دو را تایپ می‌کند. با specialty_id مستقل ترکیب می‌شود: پزشکی که با یک تخصص فیلتر را پاس می‌کند و نامِ تخصص دیگرش را می‌جویند، حذف نمی‌شود
sort string ASC یا DESC (پیش‌فرض DESC) — مرتب‌سازی ثانویه بر اساس doctorRate
active 0/1 1 → فقط پزشکان دارای نوبت (تعریف پایین). 0 → فقط پزشکانی که فلگ active_doctor_appointment آن‌ها خاموش است (کاربرد ادمین). بدون این پارامتر → فقط پزشکان فعال (active_doctor_appointment روشن)؛ پزشکان غیرفعال هرگز در لیست عمومی نمی‌آیند

مرتب‌سازی و تعریف «دارای نوبت»

پزشک دارای نوبت یعنی هر سه شرط برقرار باشد (همان تعریفی که فیلد active هر آیتم پاسخ را می‌سازد):

  1. فلگ active_doctor_appointment روشن،
  2. WeeklySchedule ثبت‌شده با حداقل یک سشن active: true،
  3. meta.online_booking_enabled برابر false نباشد.

لیست همیشه اول پزشکان دارای نوبت را نشان می‌دهد و بعد بقیه را؛ داخل هر گروه بر اساس doctorRate و پارامتر sort مرتب می‌شود.

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "name": "علی احمدی",
      "display_name": "دکتر علی احمدی",
      "gender": "man",
      "degree": "specialist",
      "img": [],
      "specialties": [
        { "uuid": "277d0e01-...", "id": "5179", "name": "جراحی عمومی", "parent_id": null },
        { "uuid": "460bd820-...", "id": "5180", "name": "جراح گوارش", "parent_id": "5179" }
      ],
      "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 هستند.

هر آیتم specialties[] کلید parent_id دارد — رشته یا null برای ریشه، هم‌شکل با id در همان آرایه و با پاسخ جزئیات پزشک. کلاینت با همین تشخیص می‌دهد کدام تخصص «اصلی» است؛ پزشک چندتخصصی معمولاً هم والد را دارد و هم زیرشاخه‌هایش، چون ذخیرهٔ یک زیرتخصص والدهایش را هم می‌نشاند.

اعتبارسنجی نام پزشک

name نمی‌تواند شماره‌تلفن (^0?9\d{9}$) یا مقدار آزمایشی (test، تست، -، null) باشد. این مقادیر در هر مسیر نوشتن با 422 رد می‌شوند — API عمومی، پنل ادمین، import و دعوت کلینیک — چون گارد روی خودِ Entity نشسته است (App\Shared\Util\DisplayName).

دلیل: نام پزشک در <title> و نتایج جست‌وجوی سایت عمومی رندر می‌شود؛ رکوردی با نام «09390039833» یک صفحهٔ بی‌ارزش ایندکس‌شدنی می‌سازد.

دعوت پزشک توسط کلینیک، اگر نام ارسال نشود، دیگر شمارهٔ موبایل را به‌عنوان نام نمی‌نشاند — برچسب خنثای «پزشک دعوت‌شده» می‌گیرد تا خود پزشک پروفایلش را claim کند. (ریشهٔ آلودگی تولیدی همین بود.)

فرمان ممیزی رکوردهای موجود:

php bin/console app:audit-polluted-records          # فقط گزارش
php bin/console app:audit-polluted-records --force  # خارج‌کردن از انتشار (بدون حذف)

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 — the doctor themselves, ROLE_ADMIN, or the registering representative (a ROLE_REPRESENTATION user whose Representation.id equals the doctor's representation_id).

The representative's grant is permanent for as long as representation_id points at them, but it is restricted to content fields — see Representative field whitelist below. The doctor's and the admin's own access is unchanged and unrestricted.

Path Parameters

Param Type Description
uuid string (UUID) Doctor UUID

Representative field whitelist

A representative may send only these keys. Any other key aborts the whole request with 403 and nothing is saved — the payload is not silently filtered.

title · gender · degree · info · detail · mobile_number · activity_time · images · image_data · social_media · specialties · doctor_services · expertise · states · cities

Notably excluded: medical_system_code (professional credential) and active. A representative activates or deactivates their own doctor through POST /api/v1/representation/doctors/{uuid}/status instead.

Every successful representative edit writes one app_log row with channel = 'representation_edit', recording the representative id, the target uuid, and the names of the changed fields (never their values). Edits by the doctor or an admin write no such row.

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), wrapped as { "success": true, "data": { ... } }.

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_AUTH_006 403 Not the doctor, not an admin, and not the registering representative
ERR_AUTH_006 403 Representative sent a field outside the whitelist — the offending key is in errors[0].field
ERR_VALIDATION_002 404 Doctor not found

Real responses (captured from a functional test run, not hand-written):

{
  "success": false,
  "data": null,
  "errors": [
    {
      "code": "ERR_AUTH_006",
      "message": "نماینده اجازهٔ تغییر این فیلد را ندارد",
      "field": "medical_system_code"
    }
  ]
}
{
  "success": false,
  "data": null,
  "errors": [
    { "code": "ERR_AUTH_006", "message": "دسترسی ممنوع" }
  ]
}

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",
      "active": true,
      "timezone": "Asia/Tehran",
      "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",
      "active": true,
      "timezone": "Asia/Tehran",
      "map": { "latitude": null, "longitude": null },
      "city": { "id": "3", "name": "اصفهان" },
      "province": { "id": "2", "name": "اصفهان" }
    }
  ]
}

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

active و timezone (افزوده‌شده در تسک شعبه): هر آدرس یک «شعبه» است و این دو ویژگی روی خروجی همهٔ اندپوینت‌های آدرس ظاهر می‌شوند، چون از DoctorAddress::toArray() می‌آیند. هر دو ستون NOT NULL DEFAULT دارند، پس ردیف‌های قدیمی هم active: true و timezone: "Asia/Tehran" می‌دهند؛ تغییر additive است.

active فقط ذخیره می‌شود و اثرش بر دسترس‌پذیری فقط همین است که منبعِ روی یک آدرسِ غیرفعال، روزش با دلیل address_inactive خالی برمی‌گردد. اندپوینت اختصاصیِ نوشتنِ این دو فیلد با حذف دامنهٔ شعبه برداشته شد.


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

Add a new practice address.

Permission: AUTH — the doctor themselves, ROLE_ADMIN, or the registering representative.

Target resolution. When doctor_uuid is present it is the target, and the caller must be that doctor, an admin, or that doctor's registering representative. When it is absent the target is the caller's own doctor profile. A representative who also happens to have a doctor profile therefore no longer silently writes the address onto their own profile — the explicit uuid always wins.

Caller doctor_uuid present doctor_uuid absent
The doctor must be their own profile, else 403 their own profile
ROLE_ADMIN any doctor 422doctor_uuid required
Registering representative their own doctors only, else 403 422doctor_uuid required
Anyone else 403 403

Request Body

{
  "doctor_uuid": "9d5f94ba-322d-4735-88e1-b15e8447a0fa",
  "name": "مطب تهران",
  "address": "تهران، خیابان ولیعصر",
  "telephone": "02112345678",
  "province_id": 1,
  "city_id": 3,
  "latitude": 35.6892,
  "longitude": 51.3890
}
Field Type Required
doctor_uuid string (UUID) for admins and representatives; optional for the doctor themselves
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 — the doctor themselves, ROLE_ADMIN, or the registering representative.

Only type = personal addresses are reachable here. A clinic address returns 403 regardless of caller, including the representative — clinic addresses are edited through the clinic routes.

Path Parameters

Param Type Description
id integer Address ID

Request Body

Same fields as POST — all optional. doctor_uuid is ignored; the target comes from the address itself.

Response 200

Updated address object, double-wrapped as { "success": true, "data": { "data": { … } } }.

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_AUTH_006 403 Address is a clinic address, or the caller is none of: the doctor, an admin, the registering representative
ERR_VALIDATION_002 404 Address not found

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

Delete a practice address.

Permission: AUTH — the doctor themselves, ROLE_ADMIN, or the registering representative. Clinic addresses return 403 here, same as PATCH.

Response 200

{ "success": true, "data": { "message": "آدرس با موفقیت حذف شد" } }

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_AUTH_006 403 Address is a clinic address, or the caller is none of: the doctor, an admin, the registering representative
ERR_VALIDATION_002 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

GET /api/v1/addresses

مجوز: appointment_settings.view. فهرست محل‌های نوبت‌دهی محیط جاری، فقط برای انتخاب.

جانشین GET /api/v1/branches است که با حذف دامنهٔ شعبه برداشته شد. خودِ آدرس نمی‌رود: هر منبع، لیست قیمت و نوبت به یکی از این‌ها بسته است. ساخت و ویرایش آدرس همان‌جایی است که همیشه بود (ClinicController و AppointmentSettingsController).

{
  "success": true,
  "data": [
    { "uuid": "…", "name": "کلینیک تخصصی مهر", "active": true }
  ]
}

۴۰۳: منشیِ بدون مجوز appointment_settings.view.