The clinic-member-doctor permission system (ClinicDoctorPermission) lagged the secretary system: only 6 resources, enforced in ~6 places, dead toggles (services.update never checked), and a sidebar showing just appointments+patients. Bring it to parity so a clinic owner can control exactly what each member doctor does — while an independent doctor stays completely unrestricted. Coverage: add insurances, addresses, inventory, tags, staff, discounts, sms to ClinicDoctorPermission::DEFAULT_PERMISSIONS + DoctorPermissionsModal (subscription/clinic_doctors stay owner-only by design). New App\Clinic\Security\ClinicDoctorAccessChecker (parallel to SecretaryAccessChecker): - denyUnlessGranted(user, resource, action): 403 only for a clinic-member doctor in the clinic context; owner/admin/secretary/independent-doctor pass through. - memberClinicId(user): resolves the member doctor to the CLINIC's tenant so the role-based controllers (Inventory/Tag/Staff/Discount/Sms) stop showing them their personal tenant in clinic context. Enforcement wired into 10 controllers alongside the existing secretary gates: ClinicService (services), Insurance (insurances), Patient (patients+payments), Staff, Discount, Inventory, Tag, SmsWallet, Payment, PaymentMethod. Frontend: the guest-doctor sidebar branch now exposes every permitted resource (gated by can()) plus a «تنظیمات» entry; both settings navs (PurchaseSubscription Sidebar + SETTINGS_MENU) are now permission-filtered for a scope=clinic doctor, not just secretaries; my-payments route gets the missing payments permission. CRUD-button gating already applies (usePermissions is role-agnostic). Tests: ClinicDoctorPermissionEnforcementTest (member denied/allowed + independent-doctor-unrestricted); guest-doctor sidebar gating. Backend 375 pass, frontend 503 pass. docs/api/clinic.md updated with the full resource set + enforcement notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
677 lines
23 KiB
Markdown
677 lines
23 KiB
Markdown
# 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`
|
||
|
||
### 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`/`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`, 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`) |
|
||
| `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.
|
||
|
||
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 },
|
||
"insurances": { "view": true, "create": false, "update": false, "delete": false },
|
||
"addresses": { "view": true, "create": false, "update": false, "delete": false },
|
||
"inventory": { "view": false, "create": false, "update": false, "delete": false },
|
||
"tags": { "view": false, "create": false, "update": false, "delete": false },
|
||
"staff": { "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 }
|
||
}
|
||
}
|
||
```
|
||
|
||
`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. `subscription` و `clinic_doctors` عمداً منبع نیستند — عملیاتِ مالکِ کلینیکاند، نه پزشکِ عضو.
|
||
|
||
**اعمال (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: {<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 |
|
||
|
||
---
|
||
|
||
## 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.
|