# 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`) ```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` ```json { "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` ```json { "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.js` → `isThinClinic`). - اگر روزی معنای «حذف‌شده» لازم شد، باید فیلد جداگانه‌ای اضافه شود — نه بازتعریف این یکی. ### نام کلینیک `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](./practice-domain.md) | ### Response `200` Updated clinic object (same structure as GET). Carries `practice_domain` — the full domain object, or `null` when unset: ```json {"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): ```json { "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` ```json { "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` ```json { "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:00–13: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:00–13: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](doctor.md#city--state-در-پاسخ-لیست) | > `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` ```json { "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](permission.md). این کلاس با منشی مشترک است، پس هر دو نقش دقیقاً یک فهرست از منابع و اکشن‌ها دارند؛ فقط **پیش‌فرض‌ها** فرق می‌کنند. تا پیش از این `services` برای پزشکِ عضو فقط `view/update` داشت و `create`/`delete` اصلاً قابل ذخیره نبود. منبعی که بعد از ساختِ یک ردیف به رجیستری اضافه شود، هنگام خواندن **پیش‌فرضِ نقش** را می‌گیرد نه `false`، پس migration داده لازم نیست. توجه: حذفِ یک کلید از JSON یعنی «پیش‌فرض را بگیر»، نه «ممنوع» — برای ممنوع‌کردن باید `false` صریح ذخیره شود. The envelope is always returned in full (`{version, resources}`); it is never flattened. ```json { "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` ```json { "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 ```json { "permissions": { "resources": { "payments": { "create": true } } }, "active": true } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `permissions` | object | ❌ | `{resources: {: {: bool}}}`. The bare `{: {...}}` 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 | --- ## POST `/file/upload/clinic_pro/clinic/field_clinic_logo` Upload clinic logo. **Permission:** `AUTH` ### Request `Content-Type: multipart/form-data` | Field | Type | Required | Max Size | |-------|------|----------|----------| | `file` | binary | ✅ | 5MB | ### Response `200` ```json { "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` ```json { "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` ```json { "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 ```json { "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` ```json { "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` ```json { "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.