Files
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

29 KiB
Raw Permalink Blame History

Clinic API

Prefix: /api/v1/clinic, /api/v1/clinics


POST /api/v1/clinic

Create a new clinic.

Permission: AUTH — any authenticated user becomes the clinic owner, and is granted ROLE_CLINIC on success.

One clinic per user. ClinicRepository::findByUser() (which resolves the caller's working context) is a findOneBy, so a second clinic would be unreachable data — the request is rejected instead.

Errors:

Code HTTP توضیح
ERR_VALIDATION_001 422 name خالی است (field: name)
ERR_CONFLICT_001 409 این کاربر از قبل مالک یک کلینیک است

Request Body (application/json)

{
  "name": "کلینیک الوند",
  "info": "توضیحات کلینیک",
  "address": "تهران، خیابان ولیعصر",
  "telephone": "02112345678",
  "working_days": "شنبه تا چهارشنبه",
  "is_24_7": false,
  "latitude": 35.6892,
  "longitude": 51.3890,
  "state": "تهران",
  "city": "تهران",
  "image_clinic": [
    { "url": "https://..." }
  ],
  "clinic_logo": "https://...",
  "doctors": ["uuid1", "uuid2"],
  "specialties": [1, 2],
  "doctor_services": [3, 4],
  "insurance": [5, 6]
}
Field Type Required Description
name string Clinic name
info string Description
address string Full address
telephone string Contact number
working_days string Working days description
is_24_7 boolean Open 24/7 flag
latitude float Latitude for map
longitude float Longitude for map
state string Province name
city string City name
image_clinic object[] Gallery images [{url: "..."}]max 5; more returns ERR_VALIDATION_001 (422)
clinic_logo string Logo URL
doctors string[] Doctor UUIDs to associate
specialties integer[] Specialty IDs
doctor_services integer[] Service IDs
insurance integer[] Insurance IDs
social_media object Social media URLs — keys: instagram, telegram, aparat, youtube, linkedin. Values are validated as URLs; invalid/empty values are stored as null.

Response 201

{
  "success": true,
  "data": {
    "data": {
      "uuid": "550e8400-...",
      "name": "کلینیک الوند",
      "info": "...",
      "address": "...",
      "telephone": "02112345678",
      "working_days": "...",
      "is_24_7": false,
      "latitude": 35.6892,
      "longitude": 51.3890,
      "state": "تهران",
      "city": "تهران",
      "images_clinic": [{ "url": "https://..." }],
      "clinic_logo": "https://...",
      "is_active": true,
      "doctors": [],
      "specialties": [],
      "doctor_services": [],
      "insurance": [],
      "tags": [],
      "created_at": 1717000000
    }
  }
}

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

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing or invalid token

GET /api/v1/clinic/{uuid}

Get clinic detail.

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

Path Parameters

Param Type Description
uuid string (UUID) Clinic UUID

can_edit

The payload carries can_edit: boolean — whether the caller may PATCH this clinic. It is true for the owner, ROLE_ADMIN, a member doctor holding clinic_info.update, and the registering representative; false for everyone else and always false without a token. It is computed by the same checks the PATCH gate uses, so clients must read it rather than re-deriving the rule.

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/clinic/{uuid}.

Response 200

{
  "success": true,
  "data": {
    "data": {
      "id": "233",
      "uuid": "550e8400-...",
      "name": "کلینیک الوند",
      "title": "کلینیک الوند",
      "is_active": true,
      "phone": "02112345678",
      "phone_number": "02112345678",
      "logo": "/uploads/clinics/logo/...",
      "clinic_logo": "/uploads/clinics/logo/...",
      "images_clinic": [{ "url": "/uploads/clinics/gallery/..." }],
      "social_media": {
        "instagram": "https://instagram.com/clinic.example",
        "telegram": "https://t.me/clinic_example",
        "aparat": null,
        "youtube": null,
        "linkedin": null
      },
      "caption": "توضیحات کلینیک",
      "list_bime": [],
      "specialties": [{ "uuid": "...", "id": "1", "name": "قلب", "parent": null }],
      "services": [],
      "clinic_specialty": [{ "uuid": "...", "id": "1", "name": "قلب", "parent": null }],
      "doctors": 5,
      "doctor_list": null,
      "city": [{ "uuid": "...", "id": "132", "name": "یزد", "parent": "100" }],
      "state": [{ "uuid": "...", "id": "100", "name": "یزد" }],
      "location": "یزد، خیابان اصلی، پلاک 101",
      "map": { "latitude": "31.868", "longitude": "54.330" },
      "24_7": false,
      "field_working_days": "شنبه تا پنجشنبه ۸ تا ۱۸"
    }
  }
}

city/state/map/location/phone/phone_number are all resolved from the clinic's address (DoctorAddress linked by clinic_id), not from columns on the clinic. location and phone/phone_number fall back to the deprecated clinics.address / clinics.telephone columns only when the address record has no value — reading them from different rows made one response describe two different places. Each is an array with a single object (or empty [] if the clinic has no address). doctors is a count; the actual doctor list comes from GET /api/v1/clinic/doctor-list/{clinicUuid} (doctor_list here is always null).

معنای is_active

is_active: false یعنی «موقتاً غیرفعال»، نه «حذف‌شده». تصمیم صریح، چون رکورد و نوبت‌های تاریخی‌اش باقی می‌مانند و کلینیک ممکن است دوباره فعال شود.

پیامدها:

  • کلینیک غیرفعال همچنان از API برمی‌گردد و لینک مستقیمش ۲۰۰ می‌دهد (نه ۴۰۴/۴۱۰) تا لینک‌های موجود نشکنند.
  • سایت عمومی همان صفحه را noindex می‌کند و از sitemap بیرون می‌گذارد (nobat724_front/lib/entityQuality.jsisThinClinic).
  • اگر روزی معنای «حذف‌شده» لازم شد، باید فیلد جداگانه‌ای اضافه شود — نه بازتعریف این یکی.

نام کلینیک

name نمی‌تواند شماره‌تلفن یا مقدار آزمایشی (test، تست، -) باشد؛ این مقادیر با 422 رد می‌شوند (App\Shared\Util\DisplayName). null مجاز است و یعنی «هنوز نام‌گذاری نشده».

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 Clinic not found
ERR_VALIDATION_001 422 نام کلینیک شماره‌تلفن یا مقدار آزمایشی است

PATCH /api/v1/clinic/{uuid}

Update a clinic.

Permission: AUTH — the clinic owner, ROLE_ADMIN, a member doctor holding clinic_info.update (see Clinic Doctor Permissions), or the registering representative (a ROLE_REPRESENTATION user whose Representation.id equals the clinic'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. Owner, admin and member-doctor access is unchanged and unrestricted. The representative path deliberately bypasses ClinicDoctorPermissionChecker: that class answers "is this doctor a member of this clinic", and a representative is not a member at all.

Path Parameters

Param Type Description
uuid string (UUID) Clinic 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.

name · info · address · telephone · working_days · 24_7 · latitude · longitude · practice_domain_uuid · state · city · social_media · image_clinic · clinic_logo · specialties · doctor_services · insurance

Notably excluded: doctors. Which doctors belong to a clinic is a membership decision for the owner, not for the representative who registered it. specialties / doctor_services / insurance are allowed — they are the clinic's public-facing catalogue, the mirror of the same keys on the doctor whitelist, and are not membership.

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 owner, a member doctor, or an admin write no such row.

Request Body

Same fields as POST — all optional — plus:

Field Type Description
practice_domain_uuid string (UUID) | "" | null حوزهٔ فعالیت کلینیک. رشتهٔ خالی یا null یعنی «پاک کن»؛ نبودنِ کلید یعنی «دست نزن». uuid ناشناس ۴۲۲ می‌گیرد، نه رد شدن بی‌صدا. ← practice-domain.md

Response 200

Updated clinic object (same structure as GET). Carries practice_domain — the full domain object, or null when unset:

{"uuid":"8c7bfd18-9159-11f1-b98b-f28fd8aa5db5","code":"beauty","name":"کلینیک زیبایی","sort_order":0,"active":true}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_FORBIDDEN_001 403 Secretary without clinic_info.update — thrown by the pre-check, before the clinic is even loaded
ERR_AUTH_006 403 Not the owner, not an admin, not a permitted member doctor, 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 Clinic not found
ERR_VALIDATION_001 422 image_clinic بیش از ۵ عکس دارد
ERR_VALIDATION_002 422 practice_domain_uuid به هیچ حوزه‌ای اشاره نمی‌کند

Real response for a whitelist violation (captured from a functional test run, not hand-written):

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

GET /api/v1/clinics

List clinics with pagination.

Permission: PUBLIC

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 20
name string Search by clinic name
city integer City id — filters by the clinic address's city
state integer Province id — filters by the clinic address's province
specialty integer Specialty id
domain string دامنه‌ی سایتِ درخواست‌کننده. اگر دامنه‌ی یک نماینده سراسری باشد، فقط کلینیک‌های همان نماینده برمی‌گردند و city/state نادیده گرفته می‌شوند؛ دامنه شهری/ناشناخته اثری ندارد

city/state are matched against the clinic's address (DoctorAddress linked by clinic_id), not a field on the clinic itself.

Response 200

{
  "success": true,
  "data": [
    {
      "id": "3426",
      "uuid": "...",
      "name": "کلینیک الوند",
      "title": "کلینیک الوند",
      "phone": "02112345678",
      "phone_number": "02112345678",
      "logo": "/uploads/clinics/logo/...",
      "clinic_logo": "/uploads/clinics/logo/...",
      "images_clinic": [{ "url": "/uploads/clinics/gallery/..." }],
      "doctors_count": 4,
      "is_active": true,
      "created_at": 1781762386,
      "city": "تهران",
      "state": "تهران",
      "specialties": [{ "uuid": "...", "id": "7", "name": "..." }],
      "24_7": false,
      "field_working_days": "شنبه تا پنجشنبه ۸ تا ۲۰"
    }
  ],
  "meta": {
    "totalRecords": 30,
    "totalPages": 2,
    "currentPage": 1
  }
}
Field Type Description
clinic_logo / logo string|null Logo path (relative /uploads/... or absolute URL)
doctors_count integer Number of doctors linked to the clinic
city string|null City name, resolved from the clinic's address (DoctorAddress)
state string|null Province name, resolved from the clinic's address (DoctorAddress)
phone / phone_number string|null Contact number from the clinic's address (DoctorAddress), falling back to the deprecated clinics.telephone column
24_7 boolean Open 24/7 flag
field_working_days string|null Working days/hours description

GET /api/v1/clinic/doctor-list/{clinicUuid}

Get doctors associated with a clinic.

Permission: PUBLIC

Path Parameters

Param Type Description
clinicUuid string (UUID) Clinic UUID

Query Parameters

Param Type Required Description
page integer Default: 1
limit integer Default: 10, max 50
name string Filter by doctor name (LIKE)
specialty integer Specialty id
gender string man / woman
degree string expert / general / specialist / subspecialistplus
active 0|1 Only doctors with appointments enabled
sort string ASC / DESC by rating (default DESC)

Filters apply only within this clinic's linked doctors.

⚠️ Double-nested: the doctors array is at data.data (extract with data?.data?.data); pagination is at data.meta.

Response 200

{
  "success": true,
  "data": {
    "data": [
      {
        "id": "1207",
        "uuid": "...",
        "name": "آرمان رضایی",
        "gender": "man",
        "degree": "specialist",
        "img": [],
        "specialties": [{ "uuid": "...", "id": "2", "name": "داخلی عمومی" }],
        "satisfaction": "96",
        "point": "4.8",
        "free_turn": "پنجشنبه 09:0013:00",
        "hours_of_work": "شنبه تا چهارشنبه | پنجشنبه",
        "active": true,
        "city": [
          { "uuid": "7bfb989e-...", "id": "123", "name": "یاسوج", "parent": "23" }
        ],
        "state": [
          { "uuid": "7bfb5705-...", "id": "23", "name": "کهگیلویه و بویراحمد" }
        ]
      }
    ],
    "meta": { "totalRecords": 3, "totalPages": 1, "currentPage": 1 }
  }
}
Field Type Description
free_turn string Next available appointment (e.g. پنجشنبه 09:0013:00), or نوبت آزادی موجود نیست if the doctor has no active weekly schedule
hours_of_work string Working-days summary, or برنامه کاری تنظیم نشده when unscheduled
active boolean true only when appointments are enabled and the doctor has an active schedule
city / state array مکان خودِ پزشک (آدرس شخصی، و در نبودش آدرس کلینیک). آرایه با حداکثر یک عضو؛ پزشک بدون آدرس []. جزئیات و قاعدهٔ انتخاب در doctor.md

free_turn/hours_of_work/active are computed from each doctor's WeeklySchedule (loaded in bulk by the endpoint). Without a schedule they fall back to the "not set" values.

Errors

Code HTTP Description
ERR_NOT_FOUND_001 404 Clinic not found

DELETE /api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}

Detach a doctor from a clinic. This removes the clinic↔doctor link (the clinic_doctors association) and the doctor's clinic_doctor_permissions row; it does not delete the doctor or change the doctor's own active appointment flag.

Permission: AUTH — the caller must be ROLE_ADMIN or the owner of this clinic (ROLE_CLINIC whose user owns clinicUuid). Any other authenticated user gets 403.

Path Parameters

Param Type Description
clinicUuid string (UUID) Clinic UUID
doctorUuid string (UUID) Doctor UUID

Response 200

{ "success": true, "data": { "message": "پزشک از کلینیک جدا شد" } }

Errors

Code HTTP Description
ERR_ACCESS_DENIED 403 Caller is neither an admin nor the clinic owner
ERR_VALIDATION_002 404 Clinic not found
ERR_NOT_FOUND_001 404 Doctor not found, or doctor not linked to this clinic

Clinic Doctor Permissions

Each doctor attached to a clinic has a permission envelope scoped to that clinic only — the doctor's own practice is never affected. Rows live in clinic_doctor_permissions (one per clinic+doctor) and are created lazily with defaults for doctors who joined before this feature existed.

مجموعهٔ منابع را App\Shared\Security\PermissionCatalog تعیین می‌کند و از GET /api/v1/permission-catalog هم خوانده می‌شود — permission.md. این کلاس با منشی مشترک است، پس هر دو نقش دقیقاً یک فهرست از منابع و اکشن‌ها دارند؛ فقط پیش‌فرض‌ها فرق می‌کنند. تا پیش از این services برای پزشکِ عضو فقط view/update داشت و create/delete اصلاً قابل ذخیره نبود.

منبعی که بعد از ساختِ یک ردیف به رجیستری اضافه شود، هنگام خواندن پیش‌فرضِ نقش را می‌گیرد نه false، پس migration داده لازم نیست. توجه: حذفِ یک کلید از JSON یعنی «پیش‌فرض را بگیر»، نه «ممنوع» — برای ممنوع‌کردن باید false صریح ذخیره شود.

The envelope is always returned in full ({version, resources}); it is never flattened.

{
  "version": 1,
  "resources": {
    "appointments":         { "view": true, "create": true, "cancel": true, "update_status": true },
    "patients":             { "view": true, "create": true, "update": true, "delete": false },
    "treatment":            { "view": true, "update": true },
    "payments":             { "view": true, "create": false, "update": false, "delete": false },
    "insurances":           { "view": true, "create": false, "update": false, "delete": false },
    "addresses":            { "view": true },
    "clinic_info":          { "view": true, "update": false },
    "services":             { "view": true, "create": false, "update": false, "delete": false },
    "inventory":            { "view": false, "create": false, "update": false, "delete": false },
    "staff":                { "view": false, "create": false, "update": false, "delete": false },
    "tags":                 { "view": false, "create": false, "update": false, "delete": false },
    "discounts":            { "view": false, "create": false, "update": false, "delete": false },
    "sms":                  { "view": false, "create": false, "update": false, "delete": false },
    "appointment_settings": { "view": true, "update": true },
    "resources":            { "view": true, "create": true, "update": true, "delete": true },
    "clinic_doctors":       { "view": false, "create": false, "update": false, "delete": false },
    "subscription":         { "view": false, "create": false }
  }
}

active: false revokes everything at once regardless of the individual flags. The clinic owner and ROLE_ADMIN bypass all checks and can never be locked out.

Unknown resources and unknown actions in a PATCH body are silently ignored (اعتبارسنجی از PermissionCatalog::filterPatch)، so a client cannot invent permission keys. subscription و clinic_doctors حالا در رجیستری هستند ولی پیش‌فرضشان برای پزشکِ عضو خاموش است — عملیاتِ مالکِ کلینیک‌اند.

خواندنِ پرونده و تگ. PatientController::resolveScope و TenantTagController::guardTagView تا پیش از این فقط منشی را بررسی می‌کردند، پس پزشکِ عضو با patients.view خاموش به‌جای 403 یک 200 با فهرست خالی می‌گرفت (نشتی نبود — tenant scoping خالی‌اش می‌کرد — ولی یک مجوز دو رفتار داشت). حالا هر دو نقش قرینه‌اند. تگ‌ها مثل قبل با tags.view یا patients.view باز می‌شوند، برای هر دو نقش.

اعمال (enforcement): همهٔ منابع در بک‌اند enforce می‌شوند. نقطهٔ واحد App\Clinic\Security\ClinicDoctorAccessChecker (denyUnlessGranted / memberClinicId) که فقط پزشکِ عضوِ کلینیک در محیطِ فعالِ کلینیک را محدود می‌کند؛ مالک/ادمین/منشی/پزشکِ مطبِ شخصی دست‌نخورده عبور می‌کنند. کنترلرهایی که tenant را نقش‌محور حل می‌کنند (Inventory/Tag/Staff/Discount/Sms) با memberClinicId پزشکِ عضو را به دادهٔ کلینیک می‌برند (نه مطبِ شخصی). نبودِ مجوز → 403. در پنل، سایدبار/Route/دکمه‌های CRUD با usePermissions().can برای محیطِ scope=clinic گِیت می‌شوند.


GET /api/v1/admin/clinic/{clinicUuid}/doctor-permissions

List the permission rows of every doctor in the clinic.

Permission: AUTH — clinic owner or ROLE_ADMIN

Response 200

{
  "success": true,
  "data": [
    {
      "uuid": "ce200cde-826d-11f1-b923-c282b864cdcc",
      "clinic_uuid": "41e325c4-e825-4067-8438-5d828ecaee09",
      "doctor_uuid": "bcabb3a8-cae3-45ec-876c-548f9c1e1569",
      "doctor_name": "تست",
      "active": true,
      "permissions": { "version": 1, "resources": { "...": {} } },
      "created_at": 1784352916,
      "updated_at": 1784352916
    }
  ]
}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_ACCESS_DENIED 403 Neither admin nor the clinic owner
ERR_NOT_FOUND_001 404 Clinic not found

GET /api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}/permissions

Read one doctor's permissions. Creates the row with defaults if it does not exist yet.

Permission: AUTH — clinic owner or ROLE_ADMIN

Path Parameters

Param Type Description
clinicUuid string (UUID) Clinic UUID
doctorUuid string (UUID) Doctor UUID — must already be attached to this clinic

Response 200

Single permission object (same shape as one item of the list above).

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_ACCESS_DENIED 403 Neither admin nor the clinic owner
ERR_NOT_FOUND_001 404 Clinic not found, or doctor not attached to this clinic

PATCH /api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}/permissions

Update one doctor's permissions. Deep merge — only the resources/actions present in the body change; everything else keeps its current value.

Permission: AUTH — clinic owner or ROLE_ADMIN

Request Body

{
  "permissions": { "resources": { "payments": { "create": true } } },
  "active": true
}
Field Type Required Description
permissions object {resources: {<resource>: {<action>: bool}}}. The bare {<resource>: {...}} form is also accepted.
active bool false revokes all access to this clinic

Response 200

Updated permission object.

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_ACCESS_DENIED 403 Neither admin nor the clinic owner
ERR_NOT_FOUND_001 404 Clinic not found, or doctor not attached to this clinic
ERR_VALIDATION_001 422 permissions is not an object

Upload clinic logo.

Permission: AUTH

Request

Content-Type: multipart/form-data

Field Type Required Max Size
file binary 5MB

Response 200

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

Errors

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

POST /file/upload/clinic_pro/clinic/field_image_clinic

Upload clinic gallery image.

Permission: AUTH

Request

Content-Type: multipart/form-data

Field Type Required Max Size
file binary 5MB

Response 200

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

After uploading, use the returned url inside image_clinic: [{ "url": "..." }] when calling PATCH clinic.

Errors

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

Clinic Address Management

GET /api/v1/clinic/{clinicUuid}/addresses

Permission: Public

Returns all addresses registered for a clinic (type=clinic entries).

Response 200

{
  "success": true,
  "data": [
    {
      "id": "12",
      "uuid": "abc-123",
      "type": "clinic",
      "clinic_id": "5",
      "name": "شعبه مرکزی",
      "address": "تهران، خیابان ولیعصر...",
      "telephone": "02112345678",
      "map": { "latitude": "35.699", "longitude": "51.337" },
      "city": { "id": "1", "name": "تهران" },
      "province": { "id": "8", "name": "تهران" }
    }
  ]
}

POST /api/v1/clinic/{clinicUuid}/address

Permission: Clinic owner, ROLE_ADMIN, or the clinic's registering representative

Creates a new address for the clinic. The address will appear in available-locations for doctors belonging to this clinic.

A clinic may hold only one address — posting a second one returns 409 (ERR_CONFLICT_001). Use PATCH to change it.

Request

{
  "name": "شعبه مرکزی",
  "address": "تهران، خیابان ولیعصر...",
  "telephone": "02112345678",
  "latitude": 35.699,
  "longitude": 51.337,
  "city_id": 123,
  "province_id": 7
}
Field Type Required
name string
address string
telephone string
latitude float
longitude float
city_id integer
province_id integer

Response 201

{ "success": true, "data": { "id": "12", "uuid": "...", "type": "clinic", ... } }

Errors

Code HTTP Description
ERR_AUTH_006 403 Not the clinic owner
ERR_VALIDATION_002 404 Clinic not found

PATCH /api/v1/clinic/{clinicUuid}/address/{addressUuid}

Permission: Clinic owner, ROLE_ADMIN, or the clinic's registering representative

Updates an existing clinic address. Same body fields as POST (all optional). The address is content, not membership, so no field whitelist applies to representatives here.

Response 200

{ "success": true, "data": { ... } }

DELETE /api/v1/clinic/{clinicUuid}/address/{addressUuid}

Permission: Clinic owner, ROLE_ADMIN, or the clinic's registering representative

Deletes a clinic address. There is no "must keep at least one" guard — the last address can be deleted.

Errors

Code HTTP Description
ERR_VALIDATION_002 404 Address or clinic not found
ERR_AUTH_006 403 Not the owner, not an admin, and not the registering representative

Removed endpoint

POST /api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}removed. Use clinic address management endpoints instead.