Files
clinicpro/docs/api/clinic.md
T
hamed 0750bc9812 feat: add multi-city representation support and domain context resolution
- Created migration to add representation_cities table and domain, is_global fields to representations.
- Implemented SiteContextController to resolve domain to site context (city | representation | unknown).
- Developed DomainContext and DomainContextResolver services for domain mapping.
- Added tests for DomainContextResolver and commission logic based on domain ownership.
2026-07-09 07:26:58 +03:30

517 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Clinic not found |
---
## PATCH `/api/v1/clinic/{uuid}`
Update a clinic.
**Permission:** `AUTH` — must be the clinic owner or `ROLE_ADMIN`
### 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:0013:00",
"hours_of_work": "شنبه تا چهارشنبه | پنجشنبه",
"active": true
}
],
"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 |
> `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 only (the `clinic_doctors` association); it does **not** delete the doctor or change the doctor's own `active` appointment flag.
**Permission:** `ROLE_ADMIN`
### 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_VALIDATION_002` | 404 | Clinic not found |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found, or doctor not linked to this clinic |
---
## 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.