# 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 ### 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` ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `uuid` | string (UUID) | 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` are resolved from the clinic's **address** (`DoctorAddress` linked by `clinic_id`), not from columns on the clinic. 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`, or a member doctor holding `clinic_info.update` (see **Clinic Doctor Permissions**) ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `uuid` | string (UUID) | Clinic UUID | ### Request Body Same fields as POST — all optional. ### Response `200` Updated clinic object (same structure as GET). ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_FORBIDDEN_001` | 403 | Not the owner | | `ERR_NOT_FOUND_001` | 404 | Clinic not found | --- ## 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`) | | `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. 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 }, "appointment_settings": { "view": true, "update": true }, "patients": { "view": true, "create": true, "update": true, "delete": false }, "payments": { "view": true, "create": false, "update": false, "delete": false }, "services": { "view": true, "update": false }, "clinic_info": { "view": true, "update": 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, so a client cannot invent permission keys. --- ## 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 or `ROLE_ADMIN` Creates a new address for the clinic. The address will appear in `available-locations` for doctors belonging to this clinic. #### 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 or `ROLE_ADMIN` Updates an existing clinic address. Same body fields as POST (all optional). #### Response `200` ```json { "success": true, "data": { ... } } ``` --- ### `DELETE /api/v1/clinic/{clinicUuid}/address/{addressUuid}` **Permission:** Clinic owner or `ROLE_ADMIN` Deletes a clinic address. > A clinic must retain at least one address — attempting to delete the last address returns `409`. #### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_CONFLICT_001` | 409 | Cannot delete the last address | | `ERR_VALIDATION_002` | 404 | Address or clinic not found | | `ERR_AUTH_006` | 403 | Not the clinic owner | --- ### Removed endpoint `POST /api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}` — **removed**. Use clinic address management endpoints instead.