Files
clinicpro/docs/api/doctor.md
T
hamedandClaude Opus 5 fb1cb20c11 feat(representation): let registering reps edit their doctors and clinics
A representative could create a doctor or clinic but not finish its profile:
PATCH /api/v1/doctor/{uuid} accepted only the doctor or an admin, and the
clinic gate ran through ClinicDoctorPermissionChecker, which asks about clinic
membership — a representative is not a member. Onboarding stopped at an empty
public record.

Grant is permanent while representation_id points at the rep, and limited to
content: RepresentationEditPolicy holds ownership plus the field whitelist.
Sending a key outside it aborts the whole request with 403 and names the field,
rather than filtering the payload silently, so a rep never believes a change
saved when it did not. medical_system_code, `active` and clinic `doctors` stay
out — credential, and membership, belong to the record's owner. `active` already
has a dedicated rep endpoint.

ClinicDoctorPermissionChecker is untouched on purpose; folding a second concept
into it would give it two reasons to change.

Doctor/clinic detail responses now carry can_edit, computed by the same policy
the PATCH gate uses, so the panel reads authorization instead of re-deriving it
and drifting. Both endpoints stay public: no token means can_edit false and an
otherwise unchanged payload, which is what nobat724_front consumes.

Address endpoints follow the same policy. createAddress now resolves its target
from an explicit doctor_uuid instead of findByUser first — a representative who
also has a doctor profile was silently writing the address onto their own.

Every rep edit writes one app_log row (channel representation_edit) recording
who, what, and which field names — never values. Owner and admin edits write
nothing, keeping /admin/logs readable.

Docs corrected where they already disagreed with the code: 403/404 error codes
on both PATCH routes, a non-existent "cannot delete the last clinic address"
409, and the missing gallery-size 422.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 15:50:17 +03:30

30 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 درخواستی»
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 نادیده گرفته می‌شوند؛ دامنه شهری/ناشناخته اثری ندارد
gender string man یا woman
degree string expert, general, specialist, subspecialistplus
name string جستجوی LIKE روی نام پزشک
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": "...", "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 هستند.

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

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.