Add API documentation for Representation, Secretary, Settlement, SMS, Specialty, Tag, and User Profile endpoints

This commit is contained in:
hamed
2026-06-11 10:27:27 +03:30
parent e88ae9bf9c
commit cced85456a
22 changed files with 5691 additions and 0 deletions
+23
View File
@@ -134,3 +134,26 @@ City IDs (integer FK to `categories.id` where `bundle='city'`) are stored on ent
5. Add UI components: `<DataTable>`, `<Pagination>`, `<Modal>`, `<ConfirmDialog>` 5. Add UI components: `<DataTable>`, `<Pagination>`, `<Modal>`, `<ConfirmDialog>`
**Category API endpoint pattern:** `GET /api/v1/categorys/{bundle}` (note: typo `categorys` is intentional — existing route). Response is double-nested: extract array with `data?.data?.data ?? []`. **Category API endpoint pattern:** `GET /api/v1/categorys/{bundle}` (note: typo `categorys` is intentional — existing route). Response is double-nested: extract array with `data?.data?.data ?? []`.
---
## Standing Rule — API Documentation
**Whenever any API endpoint is created or modified** (controller file, route, request/response structure, error code, permission), the corresponding file in `docs/api/` **must be updated in the same session**.
| Changed file | Doc to update |
|---|---|
| `src/Auth/*` | `docs/api/auth.md` |
| `src/Doctor/*` | `docs/api/doctor.md` |
| `src/Clinic/*` | `docs/api/clinic.md` + `docs/api/clinic-invitation.md` |
| `src/Appointment/Controller/AppointmentController.php` | `docs/api/appointment.md` |
| `src/Appointment/Controller/AppointmentSettings*` | `docs/api/appointment-settings.md` |
| `src/Payment/*` | `docs/api/payment.md` |
| `src/Settlement/*` | `docs/api/settlement.md` |
| `src/Rating/*` | `docs/api/rating.md` |
| `src/Secretary/*` | `docs/api/secretary.md` |
| `src/Representation/*` | `docs/api/representation.md` |
| `src/Sms/*` | `docs/api/sms.md` |
| `src/Blog/*` | `docs/api/blog.md` |
| `src/Admin/*` | `docs/api/admin.md` |
| Category/Province/City controllers | `docs/api/location.md`, `docs/api/specialty.md`, `docs/api/insurance.md`, `docs/api/doctor-service.md`, `docs/api/tag.md` |
+90
View File
@@ -0,0 +1,90 @@
# ClinicPro — API Documentation Index
> **Base URL:** `https://clinic-pro.ddev.site`
> **API Prefix:** `/api/v1`
> **Swagger UI:** `https://clinic-pro.ddev.site/api/doc` — user: `admin` / pass: `clinic123`
---
## Authentication
All protected endpoints require:
```
Authorization: Bearer <JWT_TOKEN>
```
| Role | Description |
|------|-------------|
| `PUBLIC` | No token required |
| `AUTH` | Any valid JWT |
| `ROLE_ADMIN` | Admin user |
| `ROLE_DOCTOR` | Doctor user |
| `ROLE_CLINIC` | Clinic owner |
| `ROLE_SECRETARY` | Secretary |
---
## Standard Response Envelope
```json
// Success
{ "success": true, "data": { ... } }
// Paginated
{ "success": true, "data": [...], "meta": { "totalRecords": 100, "totalPages": 5, "currentPage": 1 } }
// Error
{ "success": false, "data": null, "errors": [{ "code": "ERR_XXX_000", "message": "..." }] }
```
---
## Modules
| File | Domain | Endpoints |
|------|--------|-----------|
| [auth.md](auth.md) | Authentication — OTP, Login, JWT | 8 |
| [doctor.md](doctor.md) | Doctor profile & addresses | 11 |
| [clinic.md](clinic.md) | Clinics | 7 |
| [clinic-invitation.md](clinic-invitation.md) | Doctor invitations to clinics | 8 |
| [appointment.md](appointment.md) | Appointments & slot booking | 6 |
| [appointment-settings.md](appointment-settings.md) | Weekly schedule, date overrides, holidays | 14 |
| [payment.md](payment.md) | Payments (Mellat / Sep) | 5 |
| [settlement.md](settlement.md) | Wallet & settlement requests | 7 |
| [rating.md](rating.md) | Ratings, comments, likes | 9 |
| [secretary.md](secretary.md) | Doctor secretaries | 5 |
| [representation.md](representation.md) | Representations (agents) | 6 |
| [sms.md](sms.md) | SMS send & templates | 10 |
| [blog.md](blog.md) | Blog posts | 6 |
| [specialty.md](specialty.md) | Medical specialties | 5 |
| [insurance.md](insurance.md) | Insurances & doctor-insurance links | 10 |
| [doctor-service.md](doctor-service.md) | Doctor services | 5 |
| [tag.md](tag.md) | Blog tags | 5 |
| [location.md](location.md) | Provinces & cities | 10 |
| [user-profile.md](user-profile.md) | User medical profile | 4 |
| [admin.md](admin.md) | Admin dashboard & management | 25+ |
---
## Error Code Reference
| Code | Message (FA) | HTTP |
|------|--------------|------|
| `ERR_AUTH_001` | توکن JWT منقضی یا نامعتبر | 401 |
| `ERR_AUTH_002` | کد OTP نامعتبر | 401 |
| `ERR_AUTH_003` | کد OTP منقضی شده | 401 |
| `ERR_AUTH_004` | تعداد تلاش‌های OTP به حد مجاز رسیده | 429 |
| `ERR_AUTH_005` | نام کاربری یا رمز عبور اشتباه | 401 |
| `ERR_AUTH_006` | دسترسی ممنوع | 403 |
| `ERR_VALIDATION_001` | ورودی نامعتبر | 422 |
| `ERR_VALIDATION_002` | فیلد الزامی وارد نشده | 422 |
| `ERR_NOT_FOUND_001` | منبع درخواستی یافت نشد | 404 |
| `ERR_CONFLICT_001` | تداخل: منبع در حال استفاده | 409 |
| `ERR_FORBIDDEN_001` | دسترسی به این منبع مجاز نیست | 403 |
| `ERR_PAYMENT_001` | درگاه پرداخت در دسترس نیست | 503 |
| `ERR_PAYMENT_002` | مبلغ پرداخت نامعتبر | 422 |
| `ERR_PAYMENT_003` | وضعیت نوبت برای پرداخت مناسب نیست | 422 |
| `ERR_FILE_001` | فرمت فایل مجاز نیست | 422 |
| `ERR_SMS_003` | تمپلیت قبلاً ارسال شده | 422 |
| `ERR_SECRETARY_001` | پلن فعلی اجازه منشی بیشتر نمی‌دهد | 422 |
| `ERR_RATE_LIMIT_001` | درخواست‌های زیاد، بعداً تلاش کنید | 429 |
+679
View File
@@ -0,0 +1,679 @@
# Admin API
> **Prefix:** `/api/v1/admin`
> **Permission:** ALL endpoints in this file require `ROLE_ADMIN`
> **Headers:** `Authorization: Bearer <admin_jwt_token>`
---
## Dashboard
### GET `/api/v1/admin/dashboard/stats`
Get key performance indicators (KPIs) for the dashboard.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": {
"total_users": 1200,
"active_doctors": 85,
"total_doctors": 92,
"total_clinics": 34,
"today_appointments": 47,
"total_appointments": 8540,
"today_payments_count": 30,
"today_payments_amount": 15000000,
"total_payments_amount": 425000000,
"pending_comments": 12,
"pending_settlements": 5,
"this_month_revenue": 52000000,
"this_month_appointments": 620
}
}
```
---
### GET `/api/v1/admin/dashboard/charts`
Get chart data for the last 30 days.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": {
"appointments_30d": [
{ "date": "2024-06-01", "count": 42 }
],
"revenue_30d": [
{ "date": "2024-06-01", "amount_rials": 21000000 }
],
"appointment_status": {
"confirmed": 350,
"completed": 180,
"cancelled": 45,
"pending": 20,
"no_show": 25
},
"top_specialties": [
{ "name": "قلب و عروق", "count": 120 }
]
}
}
```
---
### GET `/api/v1/admin/dashboard/recent`
Get recent activity (last 10 of each type).
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": {
"appointments": [
{
"uuid": "...",
"doctor_title": "دکتر علی احمدی",
"patient_name": "محمد رضایی",
"slot_start": 1718438400,
"status": "confirmed"
}
],
"payments": [
{
"uuid": "...",
"amount_rials": 500000,
"gateway": "mellat",
"status": "paid",
"created_at": 1717000000
}
],
"users": [
{
"uuid": "...",
"real_name": "محمد رضایی",
"mobile_number": "09...",
"roles": ["ROLE_USER"],
"created_at": 1717000000
}
]
}
}
```
---
## User Management
### GET `/api/v1/admin/users`
List all users with pagination and filters.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search by name or mobile |
| `role` | string | ❌ | Filter: `ROLE_USER`, `ROLE_DOCTOR`, `ROLE_ADMIN`, etc. |
| `status` | string | ❌ | `"active"` or `"inactive"` |
| `sort` | string | ❌ | `"created_at"` (default desc) |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"real_name": "علی احمدی",
"mobile_number": "09123456789",
"roles": ["ROLE_USER"],
"status": "active",
"created_at": 1717000000
}
],
"meta": { "totalRecords": 1200, "totalPages": 60, "currentPage": 1 }
}
```
---
### GET `/api/v1/admin/users/{uuid}`
Get detailed user info.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": {
"uuid": "...",
"real_name": "علی احمدی",
"mobile_number": "09123456789",
"roles": ["ROLE_USER"],
"status": "active",
"wallet_balance_rials": 500000,
"appointments_count": 5,
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | User not found |
---
### GET `/api/v1/admin/users/stats`
Get user statistics.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": {
"total": 1200,
"active": 1150,
"inactive": 50,
"admins": 3,
"doctors": 92,
"patients": 1100
}
}
```
---
### PUT `/api/v1/admin/users/{uuid}`
Update user info (name, email, password).
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"real_name": "علی احمدی جدید",
"password": "newPassword123"
}
```
### Response `200`
Updated user object.
---
### PUT `/api/v1/admin/users/{uuid}/role`
Change a user's role.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"role": "ROLE_DOCTOR"
}
```
| Field | Type | Required | Allowed Values |
|-------|------|----------|----------------|
| `role` | string | ✅ | `ROLE_USER`, `ROLE_DOCTOR`, `ROLE_CLINIC`, `ROLE_SECRETARY`, `ROLE_ADMIN` |
### Response `200`
```json
{ "success": true, "data": { "message": "نقش کاربر تغییر کرد", "roles": ["ROLE_DOCTOR"] } }
```
---
### POST `/api/v1/admin/users/{uuid}/status`
Toggle user active/inactive status.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "status": "inactive" } }
```
---
### DELETE `/api/v1/admin/users/{uuid}`
Delete a user.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "کاربر حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | User not found |
---
## Doctor Management
### GET `/api/v1/admin/doctors`
List all doctors with pagination.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in title |
| `status` | string | ❌ | `"active"` or `"inactive"` |
| `gender` | string | ❌ | `"male"` or `"female"` |
| `specialty_id` | integer | ❌ | Filter by specialty |
| `sort` | string | ❌ | Sort field |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"title": "دکتر علی احمدی",
"degree": "متخصص",
"gender": "male",
"doctor_rate": 4.5,
"active_doctor_appointment": true
}
],
"meta": { "totalRecords": 92, "totalPages": 5, "currentPage": 1 }
}
```
---
### GET `/api/v1/admin/doctors/stats`
Get doctor statistics.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": {
"total": 92,
"active": 85,
"inactive": 7,
"male": 60,
"female": 32,
"top_specialty": "قلب و عروق"
}
}
```
---
### POST `/api/v1/admin/doctors/{uuid}/status`
Toggle doctor active status.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "active": false } }
```
---
## Clinic Management
### GET `/api/v1/admin/clinics`
List all clinics with pagination.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search by clinic name |
| `status` | string | ❌ | `"active"` or `"inactive"` |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"name": "کلینیک الوند",
"city": "تهران",
"telephone": "02112345678",
"is_active": true,
"created_at": 1717000000
}
],
"meta": { "totalRecords": 34, "totalPages": 2, "currentPage": 1 }
}
```
---
### PATCH `/api/v1/admin/clinic/{uuid}/status`
Toggle clinic active/inactive.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "is_active": false } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Clinic not found |
---
### DELETE `/api/v1/admin/clinic/{uuid}`
Delete a clinic.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "کلینیک حذف شد" } }
```
---
## Appointment Management
### GET `/api/v1/admin/appointments`
List all appointments.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search by doctor/patient name |
| `status` | string | ❌ | Filter by status |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"doctor_title": "دکتر علی احمدی",
"patient_name": "محمد رضایی",
"slot_start": 1718438400,
"status": "confirmed",
"price": 500000
}
],
"meta": { "totalRecords": 8540, "totalPages": 427, "currentPage": 1 }
}
```
---
## Payment Management
### GET `/api/v1/admin/payments`
List all payments.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `status` | string | ❌ | `"pending"`, `"paid"`, `"failed"`, `"cancelled"` |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"order_id": "CLINICPRO-...",
"amount_rials": 500000,
"status": "paid",
"gateway": "mellat",
"created_at": 1717000000
}
],
"meta": { "totalRecords": 7800, "totalPages": 390, "currentPage": 1 }
}
```
---
## Settlement Management
### GET `/api/v1/admin/settlements`
List all settlement requests.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `status` | string | ❌ | `"pending"`, `"approved"`, `"rejected"` |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"user": { "uuid": "...", "real_name": "دکتر علی احمدی" },
"amount_rials": 1000000,
"status": "pending",
"bank_account": { "bank_name": "بانک ملت", "owner_name": "..." },
"created_at": 1717000000
}
],
"meta": { "totalRecords": 45, "totalPages": 3, "currentPage": 1 }
}
```
> To approve or reject, use the Settlement API: `POST /api/v1/settlement/{uuid}/approve` or `/reject`
---
## Representation Management
### GET `/api/v1/admin/representations`
List all representations.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search by name |
| `city_id` | integer | ❌ | Filter by city |
### Response `200`
Paginated representation list.
---
## Secretary Management
### GET `/api/v1/admin/secretaries`
List all secretaries.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search by mobile |
### Response `200`
Paginated secretary list with linked doctor info.
---
## Rating & Comment Management
### GET `/api/v1/admin/rates`
List all ratings.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search by doctor/patient |
---
### GET `/api/v1/admin/comments`
List all comments (all statuses).
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in body |
| `status` | string | ❌ | `"pending"`, `"approved"`, `"rejected"` |
> To approve/reject comments, use the Rating API: `POST /api/v1/admin/comment/{uuid}/approve` or `/reject`
---
## SMS Management (Admin)
### GET `/api/v1/admin/sms/logs`
List SMS send logs.
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
### Response `200`
```json
{
"success": true,
"data": [
{
"id": 1,
"mobile": "09123456789",
"message": "کد تأیید: 123456",
"provider": "kavenegar",
"success": true,
"created_at": 1717000000
}
],
"meta": { "totalRecords": 5000, "totalPages": 250, "currentPage": 1 }
}
```
---
### GET `/api/v1/admin/sms/templates`
List all SMS templates.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"name": "تأیید نوبت",
"status": "approved",
"provider_code": "verify_appointment",
"created_at": 1717000000
}
]
}
```
> To create/approve/reject templates, see [sms.md](sms.md)
---
## Clinic Invitation Management
> See [clinic-invitation.md](clinic-invitation.md) for full endpoint details.
| Endpoint | Description |
|----------|-------------|
| `POST /api/v1/admin/clinic/{uuid}/invite-doctor` | Send invitation |
| `GET /api/v1/admin/clinic/{uuid}/invitations` | List invitations |
| `POST /api/v1/admin/clinic/invitation/{invUuid}/resend` | Resend SMS |
| `PATCH /api/v1/admin/clinic/invitation/{invUuid}/status` | Change status |
| `DELETE /api/v1/admin/clinic/invitation/{invUuid}` | Delete |
+540
View File
@@ -0,0 +1,540 @@
# Appointment Settings API
> **Prefix:** `/api/v1/appointment-settings`
> **Permission:** All write endpoints require `AUTH` — must be the doctor owner or `ROLE_ADMIN`
Doctors configure their availability via three resources: **weekly schedule**, **date overrides**, and **holidays**.
---
## Weekly Schedule
Each doctor has **one** weekly schedule (upsert). The schedule is keyed by **day index** (0=Saturday ... 6=Friday), each day containing a `sessions` array.
### Day Index Convention
| Index | Day (EN) | Day (FA) |
|-------|----------|----------|
| `"0"` | Saturday | شنبه |
| `"1"` | Sunday | یکشنبه |
| `"2"` | Monday | دوشنبه |
| `"3"` | Tuesday | سه‌شنبه |
| `"4"` | Wednesday | چهارشنبه |
| `"5"` | Thursday | پنجشنبه |
| `"6"` | Friday | جمعه |
---
### POST `/api/v1/appointment-settings/weekly-schedule`
Create or update the weekly schedule for a doctor (upsert).
**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"doctor_uuid": "550e8400-e29b-41d4-a716-446655440000",
"schedule": {
"0": {
"sessions": [
{
"active": true,
"location_id": 1973,
"start_time": "09:00",
"end_time": "13:00",
"duration_per_patient": 20,
"has_rest": true,
"rest_interval": 60,
"time_to_rest": 10,
"patient_limit": null
},
{
"active": true,
"location_id": 1973,
"start_time": "15:00",
"end_time": "18:00",
"duration_per_patient": 20,
"has_rest": false,
"rest_interval": 0,
"time_to_rest": 0,
"patient_limit": 5
}
]
},
"1": { "sessions": [] },
"2": { "sessions": [] },
"3": { "sessions": [] },
"4": { "sessions": [] },
"5": { "sessions": [] },
"6": { "sessions": [] }
}
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `schedule` | object | ✅ | Keys `"0"` through `"6"` (day indices) |
| `schedule.{n}.sessions` | array | ✅ | Array of session config objects |
**Session Config Object:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `active` | boolean | ✅ | Whether this session is active |
| `location_id` | integer\|null | ❌ | Doctor address/location ID |
| `start_time` | string | ✅ | Session start `"HH:MM"` |
| `end_time` | string | ✅ | Session end `"HH:MM"` |
| `duration_per_patient` | integer | ✅ | Minutes per appointment slot |
| `has_rest` | boolean | ❌ | Whether to insert rest breaks |
| `rest_interval` | integer | ❌ | Work minutes before taking a rest break |
| `time_to_rest` | integer | ❌ | Duration of each rest break (minutes) |
| `patient_limit` | integer\|null | ❌ | Max patients per session (`null` = unlimited) |
> Multiple sessions per day are supported (e.g., morning + afternoon). Sessions are sorted by `start_time` and overlapping ones are skipped.
### Response `201`
```json
{
"success": true,
"data": {
"data": {
"uuid": "sched-uuid-...",
"doctor_uuid": "550e8400-...",
"schedule": {
"0": { "sessions": [ { "active": true, "start_time": "09:00", ... } ] },
"1": { "sessions": [] },
"2": { "sessions": [] },
"3": { "sessions": [] },
"4": { "sessions": [] },
"5": { "sessions": [] },
"6": { "sessions": [] }
},
"created_at": 1717000000,
"updated_at": 1717000000
}
}
}
```
> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data`
> This is an **upsert** — if a schedule already exists for the doctor, it is overwritten.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_VALIDATION_002` | 404 | Doctor not found |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
---
### GET `/api/v1/appointment-settings/weekly-schedule/{uuid}`
Get weekly schedule. `{uuid}` can be either the **schedule UUID** or the **doctor UUID** — the controller tries both.
**Permission:** `AUTH` (class-level `IS_AUTHENTICATED_FULLY`)
### Response `200`
Same structure as POST response.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_VALIDATION_002` | 404 | Schedule not found |
---
### PATCH `/api/v1/appointment-settings/weekly-schedule/{uuid}`
Update weekly schedule. `{uuid}` can be schedule UUID or doctor UUID.
**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN`
### Request Body
```json
{
"schedule": {
"0": {
"sessions": [
{
"active": true,
"location_id": 1973,
"start_time": "10:00",
"end_time": "14:00",
"duration_per_patient": 30,
"has_rest": false,
"rest_interval": 0,
"time_to_rest": 0,
"patient_limit": null
}
]
}
}
}
```
> Replaces the entire `schedule` object if provided.
### Response `200`
Updated schedule object (same structure as POST).
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Schedule not found |
---
### DELETE `/api/v1/booking-setting/{uuid}`
Delete a weekly schedule.
**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN`
> Note: route is `/booking-setting/`, not `/appointment-settings/`
### Response `200`
```json
{ "success": true, "data": { "message": "برنامه هفتگی با موفقیت حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Schedule not found |
---
## Date Overrides
Override a specific date — mark it inactive (day off) or give it custom sessions.
### GET `/api/v1/appointment-settings/date-override/list/{doctorUuid}`
Get all date overrides for a doctor.
**Permission:** `AUTH` (class-level)
### Response `200`
```json
{
"success": true,
"data": {
"data": [
{
"uuid": "...",
"doctor_uuid": "...",
"date": 1718476800,
"active": false,
"reason": "تعطیل خاص",
"custom_slots": [],
"created_at": 1717000000
}
]
}
}
```
> `date` is a Unix timestamp. `active: false` = entire day blocked. `active: true` with `custom_slots` = custom session schedule.
---
### POST `/api/v1/appointment-settings/date-override`
Create a date override.
**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN`
### Request Body
```json
{
"doctor_uuid": "550e8400-...",
"date": "2024-06-20",
"active": false,
"reason": "تعطیل رسمی",
"custom_slots": null
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `date` | string | ✅ | Date in `Y-m-d` format (e.g. `"2024-06-20"`) |
| `active` | boolean | ❌ | `false` = full day off (default); `true` = use custom_slots |
| `reason` | string | ❌ | Reason for override |
| `custom_slots` | array\|null | ❌ | Custom sessions (same SessionConfig format as weekly schedule, see below) |
**`custom_slots` format when `active: true`:**
```json
{
"custom_slots": [
{
"start_time": "14:00",
"end_time": "18:00",
"duration_per_patient": 20,
"location_id": 1973,
"has_rest": false,
"rest_interval": 0,
"time_to_rest": 0,
"patient_limit": null
}
]
}
```
**Legacy format (backward compatible):**
```json
{
"custom_slots": [
{ "start": "14:00", "end": "18:00", "duration": 20 }
]
}
```
### Response `201`
```json
{
"success": true,
"data": {
"data": {
"uuid": "override-uuid-...",
"doctor_uuid": "...",
"date": 1718476800,
"active": false,
"reason": "تعطیل رسمی",
"custom_slots": [],
"created_at": 1717000000
}
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Doctor not found |
| `ERR_VALIDATION_001` | 422 | Invalid date format |
---
### GET `/api/v1/appointment-settings/date-override/{uuid}`
Get a single date override.
**Permission:** `AUTH` (class-level)
### Response `200`
Override object (same structure as above).
---
### PATCH `/api/v1/appointment-settings/date-override/{uuid}`
Update a date override.
**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN`
### Request Body (all optional)
```json
{
"active": true,
"reason": "جبران مرخصی",
"custom_slots": [
{
"start_time": "14:00",
"end_time": "18:00",
"duration_per_patient": 20,
"location_id": 1973,
"has_rest": false,
"rest_interval": 0,
"time_to_rest": 0,
"patient_limit": null
}
],
"date": "2024-06-21"
}
```
### Response `200`
Updated override object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Override not found |
---
### DELETE `/api/v1/appointment-settings/date-override/{uuid}`
Delete a date override.
**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "Override با موفقیت حذف شد" } }
```
---
## Holidays
Mark a date range as holiday — all slots blocked, no overrides apply.
### GET `/api/v1/appointment-settings/holidays/list/{doctorUuid}`
Get all holidays for a doctor.
**Permission:** `AUTH` (class-level)
### Response `200`
```json
{
"success": true,
"data": {
"data": [
{
"uuid": "...",
"doctor_uuid": "...",
"start_date": 1719792000,
"end_date": 1720656000,
"reason": "تعطیلات تابستانی",
"active": true,
"created_at": 1717000000
}
]
}
}
```
> `start_date` and `end_date` are Unix timestamps. Holidays take **highest priority** — they block the day even if a date override exists.
---
### POST `/api/v1/appointment-settings/holidays`
Create a holiday range.
**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN`
### Request Body
```json
{
"doctor_uuid": "550e8400-...",
"start_date": "2024-07-01",
"end_date": "2024-07-10",
"reason": "تعطیلات تابستانی"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `start_date` | string | ✅ | Start date `Y-m-d` |
| `end_date` | string | ✅ | End date `Y-m-d` (must be ≥ start_date) |
| `reason` | string | ❌ | Holiday reason |
### Response `201`
```json
{
"success": true,
"data": {
"data": {
"uuid": "holiday-uuid-...",
"doctor_uuid": "...",
"start_date": 1719792000,
"end_date": 1720656000,
"reason": "تعطیلات تابستانی",
"active": true,
"created_at": 1717000000
}
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not the doctor owner |
| `ERR_VALIDATION_002` | 404 | Doctor not found |
| `ERR_VALIDATION_001` | 422 | end_date before start_date or invalid format |
---
### GET `/api/v1/appointment-settings/holidays/{uuid}`
Get a single holiday.
**Permission:** `AUTH` (class-level)
---
### PATCH `/api/v1/appointment-settings/holidays/{uuid}`
Update a holiday.
**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN`
### Request Body (all optional)
```json
{
"start_date": "2024-07-02",
"end_date": "2024-07-12",
"reason": "تمدید تعطیلات",
"active": false
}
```
### Response `200`
Updated holiday object.
---
### DELETE `/api/v1/appointment-settings/holidays/{uuid}`
Delete a holiday.
**Permission:** `AUTH` — must be the doctor owner or `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "تعطیلات حذف شد" } }
```
---
## Slot Calculation Logic (Reference)
The `SlotCalculatorService` calculates available slots in this priority order:
1. **Holiday** — if date falls in a holiday range → return empty (no slots)
2. **Date Override** — if a date override exists for this date:
- `active: false` → return empty
- `active: true` → use `custom_slots` sessions
3. **Weekly Schedule** — use the day's `sessions` array (only `active: true` sessions, sorted by `start_time`, overlapping sessions skipped)
**Slot output format:**
```json
[
{
"start": 1718438400,
"end": 1718439600,
"start_time": "09:00",
"end_time": "09:20",
"location_id": 1973
}
]
```
+277
View File
@@ -0,0 +1,277 @@
# Appointment API
> **Prefix:** `/api/v1/appointment*`
---
## GET `/api/v1/appointment-slots`
Get available appointment slots for a doctor on a specific date.
**Permission:** `PUBLIC`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `date` | string | ✅ | Date in `Y-m-d` format (e.g. `2024-06-15`) |
### Response `200`
```json
{
"success": true,
"data": {
"doctor_uuid": "550e8400-...",
"date": "2024-06-15",
"slots": [
{
"start": 1718438400,
"end": 1718439600,
"available": true
},
{
"start": 1718439600,
"end": 1718440800,
"available": false
}
]
}
}
```
> All times are **Unix timestamps**. Slots are calculated from `WeeklySchedule` minus booked appointments, date overrides, and holidays.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
| `ERR_VALIDATION_001` | 422 | Missing or invalid date/doctor_uuid |
---
## POST `/api/v1/appointment`
Book an appointment slot.
**Permission:** `AUTH` — any authenticated user
### Request Body (`application/json`)
```json
{
"doctor_uuid": "550e8400-e29b-41d4-a716-446655440000",
"slot_start": 1718438400,
"slot_end": 1718439600,
"note": "لطفاً سریع ویزیت شوم"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor UUID |
| `slot_start` | integer | ✅ | Slot start (Unix timestamp) |
| `slot_end` | integer | ✅ | Slot end (Unix timestamp) |
| `note` | string | ❌ | Patient note |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "appt-uuid-...",
"doctor": { "uuid": "...", "title": "دکتر علی احمدی" },
"user": { "uuid": "...", "real_name": "..." },
"slot_start": 1718438400,
"slot_end": 1718439600,
"status": "pending",
"note": "...",
"price": 500000,
"created_at": 1717000000
}
}
```
**Appointment Status Values:**
| Value | Description |
|-------|-------------|
| `pending` | Awaiting payment |
| `confirmed` | Paid and confirmed |
| `cancelled` | Cancelled |
| `completed` | Visit completed |
| `no_show` | Patient did not show |
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
| `ERR_CONFLICT_001` | 409 | Slot already booked |
| `ERR_VALIDATION_001` | 422 | Invalid slot times |
---
## GET `/api/v1/appointment/{uuid}`
Get appointment detail.
**Permission:** `AUTH` — must be the patient, the doctor, or `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Appointment UUID |
### Response `200`
```json
{
"success": true,
"data": {
"uuid": "appt-uuid-...",
"doctor": {
"uuid": "...",
"title": "دکتر علی احمدی",
"image": "https://..."
},
"user": {
"uuid": "...",
"real_name": "کاربر",
"mobile_number": "09..."
},
"slot_start": 1718438400,
"slot_end": 1718439600,
"status": "confirmed",
"note": "...",
"price": 500000,
"payment_uuid": "pay-uuid-...",
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the patient/doctor/admin |
| `ERR_NOT_FOUND_001` | 404 | Appointment not found |
---
## GET `/api/v1/appointments/doctor/{doctorUuid}`
Get all appointments for a specific doctor.
**Permission:** `AUTH` — must be the doctor, their secretary, or `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `doctorUuid` | string (UUID) | Doctor UUID |
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | ❌ | Filter: `pending`, `confirmed`, `cancelled`, `completed`, `no_show` |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"user": { "uuid": "...", "real_name": "..." },
"slot_start": 1718438400,
"slot_end": 1718439600,
"status": "confirmed",
"price": 500000
}
]
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not authorized to view this doctor's appointments |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
---
## GET `/api/v1/appointments/user`
Get all appointments for the authenticated user.
**Permission:** `AUTH`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | ❌ | Filter by status |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"doctor": { "uuid": "...", "title": "دکتر علی احمدی" },
"slot_start": 1718438400,
"slot_end": 1718439600,
"status": "confirmed",
"price": 500000
}
]
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
---
## PATCH `/api/v1/appointment/{uuid}/status`
Change appointment status.
**Permission:** `AUTH` — patient can cancel; doctor/secretary can confirm/complete/no_show; admin can do all
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Appointment UUID |
### Request Body
```json
{
"status": "cancelled",
"version": 3
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | ✅ | New status value |
| `version` | integer | ❌ | Optimistic lock version (prevents double-submit) |
**Allowed Transitions by Role:**
| Actor | Allowed transitions |
|-------|---------------------|
| Patient | `pending → cancelled` |
| Doctor / Secretary | `pending → confirmed`, `confirmed → completed`, `confirmed → no_show` |
| Admin | Any transition |
### Response `200`
Updated appointment object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not authorized for this transition |
| `ERR_NOT_FOUND_001` | 404 | Appointment not found |
| `ERR_CONFLICT_001` | 409 | Version mismatch (optimistic lock) |
| `ERR_VALIDATION_001` | 422 | Invalid status value |
+284
View File
@@ -0,0 +1,284 @@
# Authentication API
> **Prefix:** `/api/v1/user` and `/oauth`
> **Permission:** All endpoints in this module are **PUBLIC** (no JWT required) except `userinfo` and `logout`
---
## POST `/api/v1/user/send-code`
Send OTP code to mobile number.
**Permission:** `PUBLIC`
### Request Body
```json
{
"mobile": "09123456789"
}
```
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `mobile` | string | ✅ | Format: `09XXXXXXXXX` (11 digits) |
### Response `200`
```json
{
"success": true,
"data": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"message": "کد تأیید ارسال شد"
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | Invalid mobile format |
| `ERR_AUTH_004` | 429 | OTP rate limit exceeded |
---
## POST `/api/v1/user/verify-code`
Verify OTP code. Returns whether this is a new or existing user.
**Permission:** `PUBLIC`
### Request Body
```json
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"code": "123456"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `uuid` | string | ✅ | UUID returned from `send-code` |
| `code` | string | ✅ | 6-digit OTP |
### Response `200`
```json
{
"success": true,
"data": {
"message": "کد تأیید شد",
"is_new_user": false,
"uuid": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_002` | 401 | Invalid OTP code |
| `ERR_AUTH_003` | 401 | OTP expired |
| `ERR_VALIDATION_002` | 422 | Missing required field |
---
## POST `/api/v1/user/register`
Complete registration for new users (called only when `is_new_user: true`).
**Permission:** `PUBLIC`
### Request Body
```json
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"real_name": "علی احمدی"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `uuid` | string | ✅ | Verified UUID from `verify-code` |
| `real_name` | string | ❌ | User's full name |
### Response `201`
```json
{
"success": true,
"data": {
"message": "ثبت‌نام با موفقیت انجام شد",
"uuid": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 422 | Missing uuid |
| `ERR_CONFLICT_001` | 409 | User already registered |
---
## POST `/api/v1/user/login`
Login with mobile number and password (for users who set a password).
**Permission:** `PUBLIC`
### Request Body
```json
{
"mobile_number": "09123456789",
"password": "mypassword"
}
```
| Field | Type | Required |
|-------|------|----------|
| `mobile_number` | string | ✅ |
| `password` | string | ✅ |
### Response `200`
```json
{
"success": true,
"data": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200..."
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_005` | 401 | Wrong credentials |
| `ERR_AUTH_006` | 403 | Account suspended |
| `ERR_AUTH_004` | 429 | Too many attempts |
---
## POST `/oauth/token`
Exchange verified UUID for JWT access token.
**Permission:** `PUBLIC`
### Request Body
```json
{
"grant_type": "mobile",
"uuid": "550e8400-e29b-41d4-a716-446655440000"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `grant_type` | string | ✅ | Must be `"mobile"` |
| `uuid` | string | ✅ | UUID from verified OTP flow |
### Response `200`
```json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200..."
}
```
> JWT payload: `{ username: mobile_number, roles: [...], iat, exp }`
> Access token TTL: **1 hour** | Refresh token TTL: **30 days** (stored in Redis)
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_002` | 400 | Invalid or expired UUID |
---
## POST `/oauth/token/refresh`
Refresh expired JWT using refresh token.
**Permission:** `PUBLIC`
### Request Body
```json
{
"refresh_token": "def50200..."
}
```
### Response `200`
```json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200..."
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Invalid or expired refresh token |
---
## GET `/oauth/userinfo`
Get authenticated user info.
**Permission:** `AUTH` — requires valid JWT
### Headers
```
Authorization: Bearer <token>
```
### Response `200`
```json
{
"success": true,
"data": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"mobile_number": "09123456789",
"real_name": "علی احمدی",
"roles": ["ROLE_USER"],
"status": "active",
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Invalid or missing token |
---
## POST `/oauth/logout`
Invalidate the refresh token (stored in Redis).
**Permission:** `AUTH`
### Request Body
```json
{
"refresh_token": "def50200..."
}
```
| Field | Type | Required |
|-------|------|----------|
| `refresh_token` | string | ❌ |
### Response `200`
```json
{
"success": true,
"data": {
"message": "با موفقیت خارج شدید"
}
}
```
+232
View File
@@ -0,0 +1,232 @@
# Blog API
> **Prefix:** `/api/v1/blog`, `/api/v1/blogs`
---
## GET `/api/v1/blogs`
List published blog posts.
**Permission:** `PUBLIC`
### Query Parameters
| Param | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `page` | integer | ❌ | 1 | Page number |
| `limit` | integer | ❌ | 20 | Items per page |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"title": "آشنایی با بیماری دیابت",
"slug": "ashnayi-ba-bimari-diabat",
"summary": "خلاصه مطلب...",
"image": "https://...",
"author": { "uuid": "...", "real_name": "دکتر احمدی" },
"tags": [{ "id": 1, "name": "دیابت" }],
"status": "published",
"created_at": 1717000000
}
],
"meta": {
"totalRecords": 25,
"totalPages": 2,
"currentPage": 1
}
}
```
---
## GET `/api/v1/blog/{slug}`
Get a single blog post by slug.
**Permission:** `PUBLIC`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `slug` | string | URL slug (e.g. `ashnayi-ba-bimari-diabat`) |
### Response `200`
```json
{
"success": true,
"data": {
"uuid": "...",
"title": "آشنایی با بیماری دیابت",
"slug": "ashnayi-ba-bimari-diabat",
"summary": "خلاصه...",
"body": "<p>محتوای کامل...</p>",
"image": "https://...",
"author": { "uuid": "...", "real_name": "دکتر احمدی" },
"tags": [{ "id": 1, "name": "دیابت" }],
"status": "published",
"created_at": 1717000000,
"updated_at": 1717100000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Blog not found or not published |
---
## POST `/api/v1/blog`
Create a new blog post.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"title": "آشنایی با بیماری دیابت",
"body": "<p>محتوای کامل مقاله...</p>",
"summary": "خلاصه کوتاه از مقاله",
"tags": [1, 2],
"status": "draft"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | ✅ | Post title (slug auto-generated) |
| `body` | string | ✅ | Full HTML body |
| `summary` | string | ❌ | Short excerpt |
| `tags` | integer[] | ❌ | Array of tag IDs |
| `status` | string | ❌ | `"draft"` (default) or `"published"` |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "blog-uuid-...",
"title": "آشنایی با بیماری دیابت",
"slug": "ashnayi-ba-bimari-diabat",
"summary": "...",
"body": "...",
"image": null,
"tags": [],
"status": "draft",
"created_at": 1717000000
}
}
```
**Blog Status Values:**
| Value | Description |
|-------|-------------|
| `draft` | Not visible to public |
| `published` | Visible in public listing |
| `archived` | Hidden from listing |
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing required fields |
---
## PATCH `/api/v1/blog/{uuid}`
Update a blog post.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Blog UUID |
### Request Body (`application/json`)
```json
{
"title": "عنوان جدید",
"body": "<p>محتوای جدید</p>",
"summary": "خلاصه جدید",
"tags": [1, 3],
"status": "published"
}
```
All fields optional.
### Response `200`
Updated blog object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Blog not found |
---
## DELETE `/api/v1/blog/{uuid}`
Delete a blog post.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "مقاله حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Blog not found |
---
## POST `/file/upload/clinic_pro/blog/field_image`
Upload blog post header image.
**Permission:** `ROLE_ADMIN`
### Request
`Content-Type: multipart/form-data`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file` | binary | ✅ | Image (max 5MB) |
| `blog_uuid` | string (UUID) | ❌ | Auto-associate with blog post |
### Response `200`
```json
{
"success": true,
"data": {
"url": "https://clinic-pro.ddev.site/uploads/blog/post_abc.jpg",
"uuid": "...",
"filename": "post_abc.jpg",
"filemime": "image/jpeg",
"filesize": 204800
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_FILE_001` | 422 | Invalid file type |
+312
View File
@@ -0,0 +1,312 @@
# Clinic Doctor Invitation API
> **Prefix:** `/api/v1/admin/clinic/...` (admin) and `/api/v1/clinic-invitation/...` (public)
Admins invite doctors to clinics via SMS. The doctor receives a secure 96-char token link valid for 72 hours.
---
## POST `/api/v1/admin/clinic/{uuid}/invite-doctor`
Send an invitation to a doctor (by mobile number) to join a clinic.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Clinic UUID |
### Request Body (`application/json`)
```json
{
"mobile": "09123456789",
"name": "دکتر علی احمدی",
"specialty": "قلب و عروق"
}
```
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `mobile` | string | ✅ | Format: `09XXXXXXXXX` |
| `name` | string | ❌ | Doctor's display name |
| `specialty` | string | ❌ | Specialty label for SMS |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "inv-uuid-...",
"mobile": "09123456789",
"invited_name": "دکتر علی احمدی",
"invited_specialty": "قلب و عروق",
"status": "pending",
"invited_at": 1717000000,
"expires_at": 1717259200,
"token_used": false,
"clinic": { "uuid": "...", "name": "کلینیک الوند" }
}
}
```
> SMS is dispatched **asynchronously** via Symfony Messenger → Redis queue.
> SMS text: `"دکتر گرامی، کلینیک {name} شما را برای همکاری دعوت کرده است.\nبرای بررسی: {link}\nاین لینک تا ۷۲ ساعت معتبر است."`
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing or invalid token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Clinic not found |
| `ERR_VALIDATION_001` | 422 | Invalid mobile format |
---
## GET `/api/v1/admin/clinic/{uuid}/invitations`
List all invitations for a clinic.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Clinic UUID |
### Query Parameters
| Param | Type | Required | Default |
|-------|------|----------|---------|
| `page` | integer | ❌ | 1 |
| `limit` | integer | ❌ | 50 |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"mobile": "09123456789",
"invited_name": "دکتر علی احمدی",
"invited_specialty": "قلب و عروق",
"status": "pending",
"invited_at": 1717000000,
"expires_at": 1717259200,
"responded_at": null,
"token_used": false,
"doctor": null
}
],
"meta": {
"totalRecords": 5,
"totalPages": 1,
"currentPage": 1
}
}
```
**Invitation Status Values:**
| Value | Description |
|-------|-------------|
| `pending` | Sent, awaiting response |
| `accepted` | Doctor accepted |
| `rejected` | Doctor rejected |
| `suspended` | Suspended by admin |
| `removed` | Removed |
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Clinic not found |
---
## POST `/api/v1/admin/clinic/invitation/{invUuid}/resend`
Resend the invitation SMS with a fresh token and reset expiry to +72 hours.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `invUuid` | string (UUID) | Invitation UUID |
### Response `200`
```json
{
"success": true,
"data": { "message": "دعوتنامه مجدداً ارسال شد" }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Invitation not found |
---
## PATCH `/api/v1/admin/clinic/invitation/{invUuid}/status`
Change the status of an invitation (e.g., suspend or remove).
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `invUuid` | string (UUID) | Invitation UUID |
### Request Body
```json
{
"status": "suspended"
}
```
| Field | Type | Required | Allowed Values |
|-------|------|----------|----------------|
| `status` | string | ✅ | `pending`, `suspended`, `removed` |
### Response `200`
```json
{
"success": true,
"data": { "status": "suspended" }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Invitation not found |
| `ERR_VALIDATION_001` | 422 | Invalid status value |
---
## DELETE `/api/v1/admin/clinic/invitation/{invUuid}`
Delete an invitation.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `invUuid` | string (UUID) | Invitation UUID |
### Response `204`
Empty body.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Invitation not found |
---
## GET `/api/v1/clinic-invitation/{token}`
View invitation details by token (used on the doctor-facing landing page).
**Permission:** `PUBLIC`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `token` | string | 96-char hex token from SMS link |
### Response `200`
```json
{
"success": true,
"data": {
"invitation": {
"uuid": "...",
"mobile": "09123456789",
"invited_name": "دکتر علی احمدی",
"invited_specialty": "قلب و عروق",
"status": "pending",
"expires_at": 1717259200
},
"clinic": {
"uuid": "...",
"name": "کلینیک الوند",
"city": "تهران",
"clinic_logo": "https://..."
},
"is_usable": true
}
}
```
> `is_usable: false` when: token already used, expired, or status is not `pending`
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Token not found |
---
## POST `/api/v1/clinic-invitation/{token}/accept`
Doctor accepts the invitation. If a doctor profile exists for this mobile, they are automatically linked to the clinic.
**Permission:** `PUBLIC`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `token` | string | 96-char hex token |
### Response `200`
```json
{
"success": true,
"data": { "message": "دعوتنامه پذیرفته شد" }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Token not found |
| `ERR_VALIDATION_001` | 422 | Token expired or already used |
---
## POST `/api/v1/clinic-invitation/{token}/reject`
Doctor rejects the invitation.
**Permission:** `PUBLIC`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `token` | string | 96-char hex token |
### Response `200`
```json
{
"success": true,
"data": { "message": "دعوتنامه رد شد" }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Token not found |
| `ERR_VALIDATION_001` | 422 | Token expired or already used |
+310
View File
@@ -0,0 +1,310 @@
# 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: "..."}]` |
| `clinic_logo` | string | ❌ | Logo URL |
| `doctors` | string[] | ❌ | Doctor UUIDs to associate |
| `specialties` | integer[] | ❌ | Specialty IDs |
| `doctor_services` | integer[] | ❌ | Service IDs |
| `insurance` | integer[] | ❌ | Insurance IDs |
### 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": {
"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": [{ "uuid": "...", "title": "دکتر..." }],
"specialties": [{ "id": 1, "name": "قلب" }],
"doctor_services": [],
"insurance": [],
"tags": [],
"owner": { "uuid": "...", "real_name": "..." }
}
}
}
```
### 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` | string | ❌ | Filter by city |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"name": "کلینیک الوند",
"city": "تهران",
"clinic_logo": "https://...",
"is_active": true
}
],
"meta": {
"totalRecords": 30,
"totalPages": 2,
"currentPage": 1
}
}
```
---
## 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 |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"title": "دکتر علی احمدی",
"degree": "متخصص",
"doctor_rate": 4.5,
"image": "https://..."
}
]
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Clinic not found |
---
## 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 |
+146
View File
@@ -0,0 +1,146 @@
# Doctor Service API
> **Prefix:** `/api/v1/doctor-services`, `/api/v1/admin/doctor-service`
---
## GET `/api/v1/doctor-services`
List all active doctor services.
**Permission:** `PUBLIC`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `specialty_id` | integer | ❌ | Filter by parent specialty |
### Response `200`
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "نوار قلب (ECG)",
"slug": "navar-ghalb",
"specialty_id": 1,
"status": "active",
"weight": 5
}
]
}
```
---
## GET `/api/v1/admin/doctor-services`
List all services with pagination (admin view).
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in name |
| `specialty_id` | integer | ❌ | Filter by specialty |
### Response `200`
```json
{
"success": true,
"data": [ ... ],
"meta": { "totalRecords": 30, "totalPages": 2, "currentPage": 1 }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
---
## POST `/api/v1/admin/doctor-service`
Create a new doctor service.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"name": "نوار قلب (ECG)",
"slug": "navar-ghalb",
"specialty_id": 1,
"status": "active",
"weight": 5
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Service name |
| `slug` | string | ❌ | Auto-generated from name if omitted |
| `specialty_id` | integer | ❌ | Link to specialty |
| `status` | string | ❌ | `"active"` or `"inactive"` |
| `weight` | integer | ❌ | Sort weight |
### Response `201`
Service object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing name |
---
## PATCH `/api/v1/admin/doctor-service/{id}`
Update a doctor service.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `id` | integer | Service ID |
All body fields optional.
### Response `200`
Updated service object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Service not found |
---
## DELETE `/api/v1/admin/doctor-service/{id}`
Delete a doctor service.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "خدمت پزشکی حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Service not found |
+380
View File
@@ -0,0 +1,380 @@
# Doctor API
> **Prefix:** `/api/v1/doctor`, `/api/v1/doctors`, `/api/v1/clinic-pro/doctor-address*`
---
## POST `/api/v1/doctor`
Create a doctor profile for the authenticated user.
**Permission:** `AUTH` — any authenticated user
### Request Body (`application/json`)
```json
{
"title": "دکتر علی احمدی",
"gender": "male",
"medical_system_code": "12345",
"degree": "متخصص",
"info": "توضیحات درباره پزشک",
"specialties": [1, 2],
"doctor_services": [3, 4]
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | ✅ | Full name with title |
| `gender` | string | ❌ | `"male"` or `"female"` |
| `medical_system_code` | string | ❌ | Nظام پزشکی code |
| `degree` | string | ❌ | Academic degree |
| `info` | string | ❌ | Bio/description |
| `specialties` | integer[] | ❌ | Array of specialty IDs |
| `doctor_services` | integer[] | ❌ | Array of doctor service IDs |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"title": "دکتر علی احمدی",
"gender": "male",
"medical_system_code": "12345",
"degree": "متخصص",
"info": "...",
"doctor_rate": null,
"active_doctor_appointment": false,
"specialties": [],
"doctor_services": [],
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing or invalid token |
| `ERR_CONFLICT_001` | 409 | Doctor profile already exists for this user |
| `ERR_VALIDATION_002` | 422 | Missing required field |
---
## GET `/api/v1/doctor/{uuid}`
Get doctor detail with clinics.
**Permission:** `PUBLIC`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Doctor UUID |
### Response `200`
```json
{
"success": true,
"data": {
"data": {
"uuid": "550e8400-...",
"title": "دکتر علی احمدی",
"gender": "male",
"medical_system_code": "12345",
"degree": "متخصص",
"info": "...",
"image": "https://...",
"doctor_rate": 4.5,
"active_doctor_appointment": true,
"specialties": [{ "id": 1, "name": "قلب و عروق" }],
"doctor_services": [{ "id": 3, "name": "نوار قلب" }],
"clinics": [{ "uuid": "...", "name": "کلینیک الوند" }],
"created_at": 1717000000
}
}
}
```
> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data`
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
---
## GET `/api/v1/doctors`
List doctors with pagination and filters.
**Permission:** `PUBLIC`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in title |
| `specialty_id` | integer | ❌ | Filter by specialty |
| `city_id` | integer | ❌ | Filter by city |
| `state_id` | integer | ❌ | Filter by province |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"title": "دکتر علی احمدی",
"degree": "متخصص",
"doctor_rate": 4.5,
"image": "https://..."
}
],
"meta": {
"totalRecords": 50,
"totalPages": 3,
"currentPage": 1
}
}
```
---
## PATCH `/api/v1/doctor/{uuid}`
Update doctor profile.
**Permission:** `AUTH` — must be the owner (or `ROLE_ADMIN`)
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Doctor UUID |
### Request Body (`application/json`)
Same fields as POST — all optional.
### Response `200`
Updated doctor object (same structure as GET single).
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the owner |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
---
## DELETE `/api/v1/doctor/{uuid}`
Delete a doctor profile.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Doctor UUID |
### Response `200`
```json
{
"success": true,
"data": { "message": "پزشک با موفقیت حذف شد" }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
---
## POST `/file/upload/clinic_pro/doctor/field_image`
Upload doctor profile image.
**Permission:** `AUTH`
### Request
`Content-Type: multipart/form-data`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file` | binary | ✅ | Image file (max 5MB) |
### Response `200`
```json
{
"success": true,
"data": {
"url": "https://clinic-pro.ddev.site/uploads/doctor/abc123.jpg",
"uuid": "...",
"filename": "abc123.jpg",
"filemime": "image/jpeg",
"filesize": 204800
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_FILE_001` | 422 | Invalid file type |
| `ERR_AUTH_001` | 401 | Missing token |
---
## GET `/api/v1/clinic-pro/doctor-addresses/{doctorId}`
Get all practice addresses for a doctor.
**Permission:** `PUBLIC`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `doctorId` | integer | Doctor's numeric ID |
### Response `200`
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "مطب تهران",
"address": "تهران، خیابان...",
"telephone": "02112345678",
"latitude": 35.6892,
"longitude": 51.3890
}
]
}
```
---
## POST `/api/v1/clinic-pro/doctor-address`
Add a new practice address.
**Permission:** `AUTH` — must own the doctor profile
### Request Body
```json
{
"name": "مطب تهران",
"address": "تهران، خیابان ولیعصر",
"telephone": "02112345678",
"latitude": 35.6892,
"longitude": 51.3890
}
```
| Field | Type | Required |
|-------|------|----------|
| `name` | string | ❌ |
| `address` | string | ❌ |
| `telephone` | string | ❌ |
| `latitude` | float | ❌ |
| `longitude` | float | ❌ |
### Response `201`
```json
{
"success": true,
"data": {
"id": 1,
"name": "مطب تهران",
"address": "تهران، خیابان ولیعصر",
"telephone": "02112345678",
"latitude": 35.6892,
"longitude": 51.3890
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the doctor owner |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
---
## PATCH `/api/v1/clinic-pro/doctor-address/{id}`
Update a practice address.
**Permission:** `AUTH` — must own the doctor profile
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `id` | integer | Address ID |
### Request Body
Same fields as POST — all optional.
### Response `200`
Updated address object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the owner |
| `ERR_NOT_FOUND_001` | 404 | Address not found |
---
## DELETE `/api/v1/clinic-pro/doctor-address/{id}`
Delete a practice address.
**Permission:** `AUTH` — must own the doctor profile
### Response `200`
```json
{ "success": true, "data": { "message": "آدرس حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the owner |
| `ERR_NOT_FOUND_001` | 404 | Address not found |
---
## POST `/api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}`
Create a doctor address automatically from a clinic's location.
**Permission:** `AUTH` — must own the doctor profile and be associated with the clinic
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `clinicUuid` | string (UUID) | Clinic UUID |
### Response `201`
Address object created from clinic data.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_FORBIDDEN_001` | 403 | Not associated with this clinic |
| `ERR_NOT_FOUND_001` | 404 | Clinic not found |
+267
View File
@@ -0,0 +1,267 @@
# Insurance API
> **Prefix:** `/api/v1/insurances`, `/api/v1/insurance`, `/api/v1/admin/insurance`
Two resource types:
1. **Insurance** — master list of insurance companies managed by admin
2. **DoctorInsurance** — a doctor's acceptance of a specific insurance (with optional price)
---
## GET `/api/v1/insurances`
List all active insurances.
**Permission:** `PUBLIC`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | ❌ | `"basic"` or `"supplementary"` |
### Response `200`
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "بیمه تأمین اجتماعی",
"type": "basic",
"logo_url": "https://...",
"status": "active"
},
{
"id": 2,
"name": "بیمه ایران",
"type": "supplementary",
"logo_url": "https://...",
"status": "active"
}
]
}
```
---
## GET `/api/v1/admin/insurances`
List all insurances with pagination (admin view — includes inactive).
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in name |
| `type` | string | ❌ | `"basic"` or `"supplementary"` |
### Response `200`
```json
{
"success": true,
"data": [ ... ],
"meta": { "totalRecords": 15, "totalPages": 1, "currentPage": 1 }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
---
## POST `/api/v1/admin/insurance`
Create a new insurance.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"name": "بیمه تأمین اجتماعی",
"type": "basic",
"logo_url": "https://...",
"status": "active"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Insurance name |
| `type` | string | ✅ | `"basic"` or `"supplementary"` |
| `logo_url` | string | ❌ | Logo image URL |
| `status` | string | ❌ | `"active"` (default) or `"inactive"` |
### Response `201`
Insurance object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing required field |
---
## PATCH `/api/v1/admin/insurance/{id}`
Update an insurance.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `id` | integer | Insurance ID |
All body fields optional.
### Response `200`
Updated insurance object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Insurance not found |
---
## DELETE `/api/v1/admin/insurance/{id}`
Delete an insurance.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "بیمه حذف شد" } }
```
---
## POST `/api/v1/admin/insurance/{id}/upload-logo`
Upload insurance logo.
**Permission:** `ROLE_ADMIN`
### Request
`Content-Type: multipart/form-data`
| Field | Type | Required |
|-------|------|----------|
| `file` | binary | ✅ |
### Response `200`
```json
{
"success": true,
"data": {
"url": "https://...",
"uuid": "...",
"filename": "insurance_logo.png",
"filemime": "image/png",
"filesize": 51200
}
}
```
---
## POST `/api/v1/insurance/`
Add an insurance to a doctor's accepted list.
**Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.create` permission)
### Request Body (`application/json`)
```json
{
"doctor_id": 42,
"insurance_id": 1,
"price": 150000
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_id` | integer | ✅ | Doctor's numeric ID |
| `insurance_id` | integer | ✅ | Insurance ID |
| `price` | integer | ❌ | Visit price for this insurance (Rials) |
### Response `201`
```json
{
"success": true,
"data": {
"id": 10,
"doctor_id": 42,
"insurance": { "id": 1, "name": "بیمه تأمین اجتماعی", "type": "basic" },
"price": 150000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the doctor |
| `ERR_NOT_FOUND_001` | 404 | Doctor or insurance not found |
| `ERR_CONFLICT_001` | 409 | Insurance already added to doctor |
---
## GET `/api/v1/insurance/{id}`
Get a doctor-insurance link.
**Permission:** `PUBLIC`
### Response `200`
DoctorInsurance object.
---
## PATCH `/api/v1/insurance/{id}`
Update a doctor-insurance (e.g., change price).
**Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.update` permission)
### Request Body
```json
{ "price": 200000 }
```
### Response `200`
Updated DoctorInsurance object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the doctor |
| `ERR_NOT_FOUND_001` | 404 | Link not found |
---
## DELETE `/api/v1/insurance/{id}`
Remove an insurance from a doctor's list.
**Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.delete` permission)
### Response `200`
```json
{ "success": true, "data": { "message": "بیمه از لیست حذف شد" } }
```
+249
View File
@@ -0,0 +1,249 @@
# Location API (Province & City)
> **Prefix:** `/api/v1/provinces`, `/api/v1/cities`, `/api/v1/admin/province`, `/api/v1/admin/city`
---
## GET `/api/v1/provinces`
List all active provinces.
**Permission:** `PUBLIC`
### Response `200`
```json
{
"success": true,
"data": [
{ "id": 1, "name": "تهران", "status": "active", "weight": 1 },
{ "id": 2, "name": "اصفهان", "status": "active", "weight": 2 }
]
}
```
---
## GET `/api/v1/cities`
List cities, optionally filtered by province.
**Permission:** `PUBLIC`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `province_id` | integer | ❌ | Filter cities by province |
### Response `200`
```json
{
"success": true,
"data": [
{ "id": 10, "name": "تهران", "province_id": 1, "status": "active", "weight": 1 },
{ "id": 11, "name": "کرج", "province_id": 1, "status": "active", "weight": 2 }
]
}
```
---
## GET `/api/v1/admin/provinces`
List all provinces with pagination (admin view — includes inactive).
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in name |
### Response `200`
```json
{
"success": true,
"data": [ ... ],
"meta": { "totalRecords": 31, "totalPages": 2, "currentPage": 1 }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
---
## GET `/api/v1/admin/cities`
List all cities with pagination (admin view).
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in name |
| `province_id` | integer | ❌ | Filter by province |
### Response `200`
Paginated city list.
---
## POST `/api/v1/admin/province`
Create a new province.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"name": "تهران",
"status": "active",
"weight": 1
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Province name |
| `status` | string | ❌ | `"active"` (default) or `"inactive"` |
| `weight` | integer | ❌ | Sort weight |
### Response `201`
Province object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing name |
---
## PATCH `/api/v1/admin/province/{id}`
Update a province.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `id` | integer | Province ID |
All fields optional.
### Response `200`
Updated province object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Province not found |
---
## DELETE `/api/v1/admin/province/{id}`
Delete a province.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "استان حذف شد" } }
```
---
## POST `/api/v1/admin/city`
Create a new city.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"name": "تهران",
"province_id": 1,
"status": "active",
"weight": 1
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | City name |
| `province_id` | integer | ❌ | Parent province ID |
| `status` | string | ❌ | `"active"` or `"inactive"` |
| `weight` | integer | ❌ | Sort weight |
### Response `201`
City object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing name |
---
## PATCH `/api/v1/admin/city/{id}`
Update a city.
**Permission:** `ROLE_ADMIN`
All body fields optional.
### Response `200`
Updated city object.
---
## DELETE `/api/v1/admin/city/{id}`
Delete a city.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "شهر حذف شد" } }
```
---
## GET `/api/v1/categorys/{bundle}` *(Legacy)*
Legacy endpoint that proxies to the new endpoints.
**Permission:** `PUBLIC`
### Path Parameters
| Bundle | Maps to |
|--------|---------|
| `state` | `/api/v1/provinces` |
| `city` | `/api/v1/cities` |
| `specially_doctor` | `/api/v1/specialties` |
| `doctor_services` | `/api/v1/doctor-services` |
| `insurance_type` | `/api/v1/insurances?type=basic` |
| `supplementary_insurance` | `/api/v1/insurances?type=supplementary` |
| `tag` | `/api/v1/tags` |
> ⚠️ Response is **triple-nested**: `data?.data?.data ?? []`
> Note: `categorys` (not `categories`) is intentional — legacy route name.
+178
View File
@@ -0,0 +1,178 @@
# Payment API
> **Prefix:** `/api/v1/payment`, `/api/v1/subscription-payment`
> **Supported Gateways:** `mellat` (Mellat Bank SOAP) | `sep` (SEP REST)
---
## POST `/api/v1/payment/appointment`
Initiate payment for an appointment. Returns a redirect URL to the payment gateway.
**Permission:** `AUTH` — must be the appointment owner (patient)
### Request Body (`application/json`)
```json
{
"appointment_uuid": "appt-uuid-...",
"gateway": "mellat",
"frontend_address": "https://myapp.com/payment/result"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `appointment_uuid` | string (UUID) | ✅ | Appointment to pay for |
| `gateway` | string | ✅ | `"mellat"` or `"sep"` |
| `frontend_address` | string | ❌ | Redirect URL after payment (overrides default) |
### Response `200`
```json
{
"success": true,
"data": {
"payment_uuid": "pay-uuid-...",
"redirect_url": "https://bpm.shaparak.ir/pgwchannel/...",
"order_id": "CLINICPRO-1717000000-ABC123"
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the patient |
| `ERR_PAYMENT_003` | 422 | Appointment not in payable state |
| `ERR_PAYMENT_002` | 422 | Invalid amount |
| `ERR_PAYMENT_001` | 503 | Payment gateway unavailable |
---
## POST `/api/v1/payment/callback/{gateway}`
## GET `/api/v1/payment/callback/{gateway}`
Payment gateway callback. Called by the bank after user completes (or cancels) payment.
**Permission:** `PUBLIC` — called by the gateway, not the user
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `gateway` | string | `mellat` or `sep` |
### Request (varies by gateway)
**Mellat POST fields:**
```
ResCode=0&SaleOrderId=...&SaleReferenceId=...&RefId=...
```
**SEP POST fields:**
```
Status=2&RRN=...&RefNum=...&TerminalId=...&TraceNo=...
```
### Response
- If `ResCode=0` (success): appointment confirmed, redirect to `frontend_address?success=1&uuid=...`
- If failed: redirect to `frontend_address?success=0&error=...`
### Notes
- On success: appointment status → `confirmed`, wallet credited with doctor's share
- Payment record stored with: `order_id`, `amount_rials`, `status`, `gateway`, `ref_id`
---
## POST `/api/v1/subscription-payment`
Initiate a subscription / wallet top-up payment (not tied to a specific appointment).
**Permission:** `AUTH`
### Request Body
```json
{
"gateway": "mellat",
"amount_rials": 1000000,
"frontend_address": "https://myapp.com/wallet/result"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `gateway` | string | ✅ | `"mellat"` or `"sep"` |
| `amount_rials` | integer | ✅ | Amount in Rials (min: 10,000) |
| `frontend_address` | string | ❌ | Redirect URL after payment |
### Response `200`
```json
{
"success": true,
"data": {
"payment_uuid": "pay-uuid-...",
"redirect_url": "https://bpm.shaparak.ir/pgwchannel/...",
"order_id": "CLINICPRO-SUB-1717000000-XYZ"
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_PAYMENT_002` | 422 | Invalid amount |
| `ERR_PAYMENT_001` | 503 | Gateway unavailable |
---
## POST/GET `/api/v1/subscription-payment/callback/{gateway}`
Callback for subscription payments. Same behavior as appointment callback but credits wallet instead.
**Permission:** `PUBLIC`
---
## GET `/api/v1/payment/{uuid}`
Get payment status and details.
**Permission:** `AUTH` — must be the payment owner or `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Payment UUID |
### Response `200`
```json
{
"success": true,
"data": {
"uuid": "pay-uuid-...",
"order_id": "CLINICPRO-1717000000-ABC123",
"amount_rials": 500000,
"status": "paid",
"gateway": "mellat",
"ref_id": "123456789",
"appointment_uuid": "appt-uuid-...",
"created_at": 1717000000,
"paid_at": 1717000120
}
}
```
**Payment Status Values:**
| Value | Description |
|-------|-------------|
| `pending` | Created, not paid yet |
| `paid` | Successfully paid |
| `failed` | Gateway returned failure |
| `cancelled` | User cancelled at gateway |
| `refunded` | Refunded |
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the owner |
| `ERR_NOT_FOUND_001` | 404 | Payment not found |
+275
View File
@@ -0,0 +1,275 @@
# Rating & Comments API
> **Prefix:** `/api/v1/rate`, `/api/v1/comment`, `/api/v1/like`
---
## POST `/api/v1/rate`
Submit a rating for a doctor.
**Permission:** `AUTH` — any authenticated user (typically after a completed appointment)
### Request Body (`application/json`)
```json
{
"doctor_uuid": "550e8400-...",
"score": 5
}
```
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `doctor_uuid` | string (UUID) | ✅ | Must exist |
| `score` | integer | ✅ | 15 |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "rate-uuid-...",
"doctor_uuid": "...",
"score": 5,
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
| `ERR_VALIDATION_001` | 422 | Score out of range |
---
## GET `/api/v1/rate/{doctorUuid}`
Get average rating for a doctor.
**Permission:** `PUBLIC`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `doctorUuid` | string (UUID) | Doctor UUID |
### Response `200`
```json
{
"success": true,
"data": {
"average": 4.3,
"total": 47
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
---
## POST `/api/v1/comment`
Submit a comment/review for a doctor.
**Permission:** `AUTH`
> Comments require admin approval before appearing publicly.
### Request Body (`application/json`)
```json
{
"doctor_uuid": "550e8400-...",
"body": "پزشک بسیار مؤدب و متخصص بودند"
}
```
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `doctor_uuid` | string (UUID) | ✅ | Must exist |
| `body` | string | ✅ | Min 10 chars |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "comment-uuid-...",
"body": "پزشک بسیار مؤدب و متخصص بودند",
"status": "pending",
"created_at": 1717000000
}
}
```
**Comment Status Values:**
| Value | Description |
|-------|-------------|
| `pending` | Awaiting admin review |
| `approved` | Visible to public |
| `rejected` | Not visible |
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
| `ERR_VALIDATION_001` | 422 | Body too short |
---
## GET `/api/v1/comments/{doctorUuid}`
Get approved comments for a doctor.
**Permission:** `PUBLIC`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `doctorUuid` | string (UUID) | Doctor UUID |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"body": "پزشک بسیار مؤدب...",
"user": { "uuid": "...", "real_name": "علی" },
"likes": 3,
"status": "approved",
"created_at": 1717000000
}
]
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
---
## DELETE `/api/v1/comment/{uuid}`
Delete a comment.
**Permission:** `AUTH` — must be the comment author or `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "نظر حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the author |
| `ERR_NOT_FOUND_001` | 404 | Comment not found |
---
## GET `/api/v1/admin/comments/pending`
Get all pending comments waiting for review.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"body": "...",
"doctor": { "uuid": "...", "title": "دکتر علی احمدی" },
"user": { "uuid": "...", "real_name": "..." },
"status": "pending",
"created_at": 1717000000
}
]
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
---
## POST `/api/v1/admin/comment/{uuid}/approve`
Approve a pending comment (makes it public).
**Permission:** `ROLE_ADMIN`
### Response `200`
Updated comment object with `status: "approved"`.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Comment not found |
---
## POST `/api/v1/admin/comment/{uuid}/reject`
Reject a pending comment.
**Permission:** `ROLE_ADMIN`
### Response `200`
Updated comment object with `status: "rejected"`.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Comment not found |
---
## POST `/api/v1/like/{commentUuid}`
Toggle like on a comment (like if not liked, unlike if already liked).
**Permission:** `AUTH`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `commentUuid` | string (UUID) | Comment UUID |
### Response `200` (unlike) or `201` (new like)
```json
{
"success": true,
"data": {
"liked": true,
"likes": 4
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_NOT_FOUND_001` | 404 | Comment not found |
+227
View File
@@ -0,0 +1,227 @@
# Representation (Agent) API
> **Prefix:** `/api/v1/representation`
Representations are sales agents who earn commission on appointments booked through their referral.
---
## POST `/api/v1/representation`
Create a new representation.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"full_name": "علی احمدی",
"mobile_number": "09123456789",
"city_id": 42,
"commission_percent": 10,
"bank_account": {
"iban": "IR...",
"account_number": "1234567890",
"bank_name": "بانک ملت",
"owner_name": "علی احمدی"
}
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `full_name` | string | ✅ | Agent full name |
| `mobile_number` | string | ✅ | Login mobile (creates a User account) |
| `city_id` | integer | ❌ | City ID (FK to categories where bundle=city) |
| `commission_percent` | float | ❌ | Commission rate (0100) |
| `bank_account` | object | ❌ | Bank details for settlements |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "rep-uuid-...",
"full_name": "علی احمدی",
"mobile_number": "09123456789",
"city_id": 42,
"commission_percent": 10,
"active": true,
"bank_account": { ... },
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_CONFLICT_001` | 409 | Mobile number already in use |
| `ERR_VALIDATION_001` | 422 | Invalid input |
---
## GET `/api/v1/representation/{uuid}`
Get representation detail.
**Permission:** `AUTH` — must be the representation's user or `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Representation UUID |
### Response `200`
```json
{
"success": true,
"data": {
"uuid": "...",
"full_name": "علی احمدی",
"mobile_number": "09123456789",
"city_id": 42,
"city_name": "تهران",
"commission_percent": 10,
"active": true,
"bank_account": { ... },
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not owner or admin |
| `ERR_NOT_FOUND_001` | 404 | Representation not found |
---
## PATCH `/api/v1/representation/{uuid}`
Update representation.
**Permission:** `AUTH` — must be the representation's user or `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"full_name": "علی احمدی جدید",
"city_id": 50,
"commission_percent": 12,
"bank_account": { ... },
"active": true
}
```
All fields optional.
### Response `200`
Updated representation object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not owner or admin |
| `ERR_NOT_FOUND_001` | 404 | Representation not found |
---
## DELETE `/api/v1/representation/{uuid}`
Delete a representation.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "نماینده حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Representation not found |
---
## GET `/api/v1/representation/{uuid}/dashboard/monthly`
Get monthly earnings dashboard for a representation.
**Permission:** `AUTH` — must be the representation's user or `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Representation UUID |
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `year` | integer | ✅ | e.g. `2024` |
| `month` | integer | ✅ | 112 |
### Response `200`
```json
{
"success": true,
"data": {
"period": { "year": 2024, "month": 6 },
"stats": {
"total_appointments": 15,
"total_revenue_rials": 7500000,
"commission_rials": 750000,
"daily": [
{ "date": "2024-06-01", "appointments": 2, "revenue": 1000000, "commission": 100000 }
]
}
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not owner or admin |
| `ERR_NOT_FOUND_001` | 404 | Representation not found |
---
## GET `/api/v1/representation/{uuid}/dashboard/yearly`
Get yearly earnings dashboard for a representation.
**Permission:** `AUTH` — must be the representation's user or `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `year` | integer | ✅ | e.g. `2024` |
### Response `200`
```json
{
"success": true,
"data": {
"period": { "year": 2024 },
"months": [
{ "month": 1, "appointments": 10, "revenue_rials": 5000000, "commission_rials": 500000 },
{ "month": 2, "appointments": 8, "revenue_rials": 4000000, "commission_rials": 400000 }
],
"totals": {
"appointments": 97,
"revenue_rials": 48500000,
"commission_rials": 4850000
}
}
}
```
+220
View File
@@ -0,0 +1,220 @@
# Secretary API
> **Prefix:** `/api/v1/secretary`, `/api/v1/secretaries`
Secretaries are linked to a doctor and have granular permissions controlling what they can do on behalf of the doctor.
---
## POST `/api/v1/secretary`
Create a secretary for a doctor.
**Permission:** `ROLE_DOCTOR` — must own the doctor profile
### Request Body (`application/json`)
```json
{
"doctor_uuid": "550e8400-...",
"mobile_number": "09123456789",
"password": "secretaryPass123",
"permissions": {
"version": 1,
"resources": {
"appointments": { "view": true, "create": true, "cancel": false, "update_status": true },
"addresses": { "view": true, "create": false, "update": false, "delete": false },
"clinic_info": { "view": true, "update": false },
"insurances": { "view": true, "create": false, "update": false, "delete": false }
}
}
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_uuid` | string (UUID) | ✅ | Doctor to assign secretary to |
| `mobile_number` | string | ✅ | Secretary's login mobile |
| `password` | string | ❌ | Initial password (auto-generated if omitted) |
| `permissions` | object | ❌ | Permission set (see structure below) |
**Permissions Structure:**
```json
{
"version": 1,
"resources": {
"appointments": {
"view": true, // Can view appointments list
"create": true, // Can book appointments
"cancel": false, // Can cancel appointments
"update_status": true // Can mark as completed/no_show
},
"addresses": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"clinic_info": {
"view": true,
"update": false
},
"insurances": {
"view": true,
"create": false,
"update": false,
"delete": false
}
}
}
```
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "sec-uuid-...",
"mobile_number": "09123456789",
"active": true,
"permissions": { ... },
"doctor": { "uuid": "...", "title": "دکتر علی احمدی" },
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not a doctor or not the doctor's owner |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
| `ERR_CONFLICT_001` | 409 | Mobile number already in use |
| `ERR_SECRETARY_001` | 422 | Plan limit for secretaries reached |
---
## GET `/api/v1/secretary/{uuid}`
Get secretary detail.
**Permission:** `AUTH` — must be the linked doctor or `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Secretary UUID |
### Response `200`
```json
{
"success": true,
"data": {
"uuid": "...",
"mobile_number": "09123456789",
"active": true,
"permissions": { ... },
"doctor": { "uuid": "...", "title": "دکتر علی احمدی" },
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not authorized |
| `ERR_NOT_FOUND_001` | 404 | Secretary not found |
---
## PATCH `/api/v1/secretary/{uuid}`
Update secretary active status or permissions.
**Permission:** `ROLE_DOCTOR` — must be the linked doctor
### Request Body (`application/json`)
```json
{
"active": false,
"permissions": {
"version": 1,
"resources": {
"appointments": { "view": true, "create": false, "cancel": false, "update_status": false }
}
}
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `active` | boolean | ❌ | Enable/disable secretary |
| `permissions` | object | ❌ | New permissions object |
### Response `200`
Updated secretary object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the linked doctor |
| `ERR_NOT_FOUND_001` | 404 | Secretary not found |
---
## DELETE `/api/v1/secretary/{uuid}`
Delete a secretary.
**Permission:** `ROLE_DOCTOR` — must be the linked doctor
### Response `200`
```json
{ "success": true, "data": { "message": "منشی حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the linked doctor |
| `ERR_NOT_FOUND_001` | 404 | Secretary not found |
---
## GET `/api/v1/secretaries/{doctorUuid}`
Get all secretaries for a specific doctor.
**Permission:** `AUTH` — must be the doctor or `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `doctorUuid` | string (UUID) | Doctor UUID |
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"mobile_number": "09...",
"active": true,
"permissions": { ... },
"created_at": 1717000000
}
]
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the doctor |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
+254
View File
@@ -0,0 +1,254 @@
# Settlement & Wallet API
> **Prefix:** `/api/v1/wallet`, `/api/v1/settlement`
---
## GET `/api/v1/wallet/balance`
Get authenticated user's wallet balance.
**Permission:** `AUTH`
### Response `200`
```json
{
"success": true,
"data": {
"balance_rials": 2500000,
"recent_transactions": [
{
"uuid": "...",
"type": "credit",
"amount_rials": 500000,
"balance_after": 2500000,
"description": "دریافت از نوبت شماره ...",
"created_at": 1717000000
}
]
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
---
## GET `/api/v1/wallet/transactions`
Get wallet transaction history for the authenticated user.
**Permission:** `AUTH`
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"type": "credit",
"amount_rials": 500000,
"balance_after": 2500000,
"description": "دریافت از نوبت",
"created_at": 1717000000
},
{
"uuid": "...",
"type": "debit",
"amount_rials": 200000,
"balance_after": 2300000,
"description": "تسویه‌حساب",
"created_at": 1716900000
}
]
}
```
**Transaction Type Values:**
| Value | Description |
|-------|-------------|
| `credit` | Money added to wallet |
| `debit` | Money removed from wallet |
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
---
## POST `/api/v1/settlement`
Request a settlement (withdrawal from wallet to bank account).
**Permission:** `AUTH`
### Request Body (`application/json`)
```json
{
"amount_rials": 1000000,
"bank_account": {
"iban": "IR...",
"account_number": "1234567890",
"bank_name": "بانک ملت",
"owner_name": "علی احمدی"
}
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `amount_rials` | integer | ✅ | Amount to withdraw (must be ≤ wallet balance) |
| `bank_account` | object | ❌ | Bank account details (saved if not previously set) |
| `bank_account.iban` | string | ❌ | IBAN (شبا) |
| `bank_account.account_number` | string | ❌ | Account number |
| `bank_account.bank_name` | string | ❌ | Bank name |
| `bank_account.owner_name` | string | ❌ | Account owner name |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "settle-uuid-...",
"amount_rials": 1000000,
"status": "pending",
"bank_account": {
"iban": "IR...",
"bank_name": "بانک ملت",
"owner_name": "علی احمدی"
},
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_VALIDATION_001` | 422 | Amount exceeds balance or invalid amount |
---
## GET `/api/v1/settlement`
Get authenticated user's settlement requests.
**Permission:** `AUTH`
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"amount_rials": 1000000,
"status": "pending",
"note": null,
"created_at": 1717000000,
"processed_at": null
}
]
}
```
**Settlement Status Values:**
| Value | Description |
|-------|-------------|
| `pending` | Awaiting admin review |
| `approved` | Approved, payment sent |
| `rejected` | Rejected by admin |
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
---
## GET `/api/v1/settlement/{uuid}`
Get a single settlement.
**Permission:** `AUTH` — must be the owner or `ROLE_ADMIN`
### Response `200`
Settlement object with full details including `bank_account`.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the owner |
| `ERR_NOT_FOUND_001` | 404 | Settlement not found |
---
## POST `/api/v1/settlement/{uuid}/approve`
Approve a settlement request. Marks it as paid and debits the wallet.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Settlement UUID |
### Request Body (`application/json`)
```json
{
"note": "پرداخت شد — شناسه پیگیری: 123456"
}
```
| Field | Type | Required |
|-------|------|----------|
| `note` | string | ❌ |
### Response `200`
Updated settlement object with `status: "approved"`.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Settlement not found |
| `ERR_VALIDATION_001` | 422 | Already processed |
---
## POST `/api/v1/settlement/{uuid}/reject`
Reject a settlement request. Returns the amount back to wallet.
**Permission:** `ROLE_ADMIN`
### Request Body
```json
{
"note": "حساب بانکی نادرست است"
}
```
| Field | Type | Required |
|-------|------|----------|
| `note` | string | ✅ | Rejection reason (required) |
### Response `200`
Updated settlement object with `status: "rejected"`.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Settlement not found |
| `ERR_VALIDATION_002` | 422 | Missing note |
+300
View File
@@ -0,0 +1,300 @@
# SMS API
> **Prefix:** `/api/v1/sms`
> **Providers:** `kavenegar` (default) | `rangineh`
> All send operations are dispatched **asynchronously** via Symfony Messenger → Redis queue.
---
## POST `/api/v1/sms/send`
Send a direct SMS message (free text).
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"mobile": "09123456789",
"message": "سلام، پیام آزمایشی",
"provider": "kavenegar"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mobile` | string | ✅ | Recipient mobile (`09XXXXXXXXX`) |
| `message` | string | ✅ | Message text |
| `provider` | string | ❌ | `"kavenegar"` or `"rangineh"` (default from env) |
### Response `200`
```json
{
"success": true,
"data": { "message": "پیامک با موفقیت ارسال شد" }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_001` | 422 | Invalid mobile format |
---
## POST `/api/v1/sms/send-template`
Send an SMS using an approved template.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"mobile": "09123456789",
"template_uuid": "tmpl-uuid-...",
"vars": {
"name": "دکتر علی احمدی",
"date": "۱۵ خرداد ۱۴۰۴"
},
"provider": "kavenegar"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mobile` | string | ✅ | Recipient mobile |
| `template_uuid` | string (UUID) | ✅ | UUID of an approved template |
| `vars` | object | ❌ | Key-value substitutions for template placeholders |
| `provider` | string | ❌ | Override provider |
### Response `200`
```json
{
"success": true,
"data": { "message": "پیامک با موفقیت ارسال شد" }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Template not found |
| `ERR_VALIDATION_001` | 422 | Template not approved |
---
## POST `/api/v1/sms/template`
Create a new SMS template.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"name": "تأیید نوبت",
"body": "دکتر گرامی ${name}، نوبت شما در تاریخ ${date} تأیید شد.",
"variables": ["name", "date"]
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Template display name |
| `body` | string | ✅ | Template text with `${variable}` placeholders |
| `variables` | string[] | ❌ | List of expected variable names |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "tmpl-uuid-...",
"name": "تأیید نوبت",
"body": "دکتر گرامی ${name}...",
"variables": ["name", "date"],
"status": "draft",
"created_at": 1717000000
}
}
```
**Template Status Values:**
| Value | Description |
|-------|-------------|
| `draft` | Created, not submitted |
| `pending` | Submitted for review |
| `approved` | Ready to use |
| `rejected` | Rejected |
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing required field |
---
## GET `/api/v1/sms/template/{uuid}`
Get template detail.
**Permission:** `AUTH`
### Response `200`
Template object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_NOT_FOUND_001` | 404 | Template not found |
---
## PATCH `/api/v1/sms/template/{uuid}`
Update a template (only allowed in `draft` or `rejected` status).
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"name": "تأیید نوبت - ویرایش",
"body": "نوبت شما در ${date} تأیید شد.",
"variables": ["date"]
}
```
All fields optional.
### Response `200`
Updated template object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Template not found |
| `ERR_SMS_003` | 422 | Template already submitted/approved |
---
## POST `/api/v1/sms/template/{uuid}/submit`
Submit template for admin review (moves status from `draft` to `pending`).
**Permission:** `ROLE_ADMIN`
### Response `200`
Updated template with `status: "pending"`.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Template not found |
| `ERR_SMS_003` | 422 | Already submitted |
---
## DELETE `/api/v1/sms/template/{uuid}`
Delete a template.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "قالب پیامک حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Template not found |
---
## GET `/api/v1/admin/sms/templates`
List all templates (admin view with all statuses).
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"name": "تأیید نوبت",
"status": "approved",
"created_at": 1717000000
}
]
}
```
---
## POST `/api/v1/admin/sms/template/{uuid}/approve`
Approve a pending template.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"note": "تأیید شد",
"provider_code": "verify_appointment"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `note` | string | ❌ | Admin note |
| `provider_code` | string | ❌ | Provider-side template code |
### Response `200`
Updated template with `status: "approved"`.
---
## POST `/api/v1/admin/sms/template/{uuid}/reject`
Reject a pending template.
**Permission:** `ROLE_ADMIN`
### Request Body
```json
{
"note": "متن قالب نامناسب است"
}
```
| Field | Type | Required |
|-------|------|----------|
| `note` | string | ✅ |
### Response `200`
Updated template with `status: "rejected"`.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 422 | Missing note |
+162
View File
@@ -0,0 +1,162 @@
# Specialty API
> **Prefix:** `/api/v1/specialties`, `/api/v1/admin/specialty`
---
## GET `/api/v1/specialties`
List all medical specialties.
**Permission:** `PUBLIC`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `parent_id` | integer | ❌ | Filter to sub-specialties of a parent |
### Response `200`
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "قلب و عروق",
"slug": "ghalb-va-oroug",
"parent_id": null,
"status": "active",
"weight": 10
},
{
"id": 5,
"name": "فوق تخصص قلب",
"slug": "fowgh-takhassos-ghalb",
"parent_id": 1,
"status": "active",
"weight": 5
}
]
}
```
---
## GET `/api/v1/admin/specialties`
List all specialties with pagination (admin view — includes inactive).
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in name |
### Response `200`
```json
{
"success": true,
"data": [ ... ],
"meta": { "totalRecords": 50, "totalPages": 3, "currentPage": 1 }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
---
## POST `/api/v1/admin/specialty`
Create a new specialty.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"name": "قلب و عروق",
"slug": "ghalb-va-oroug",
"parent_id": null,
"status": "active",
"weight": 10
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Specialty name |
| `slug` | string | ❌ | Auto-generated from name if omitted |
| `parent_id` | integer | ❌ | Parent specialty ID (for sub-specialties) |
| `status` | string | ❌ | `"active"` (default) or `"inactive"` |
| `weight` | integer | ❌ | Sort weight |
### Response `201`
Specialty object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing name |
---
## PATCH `/api/v1/admin/specialty/{id}`
Update a specialty.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `id` | integer | Specialty ID |
### Request Body (all optional)
```json
{
"name": "قلب و عروق ویرایش‌شده",
"status": "inactive",
"weight": 20,
"parent_id": null,
"slug": "new-slug"
}
```
### Response `200`
Updated specialty object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Specialty not found |
---
## DELETE `/api/v1/admin/specialty/{id}`
Delete a specialty.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "تخصص حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Specialty not found |
+132
View File
@@ -0,0 +1,132 @@
# Tag API
> **Prefix:** `/api/v1/tags`, `/api/v1/admin/tag`
Tags are used for categorizing blog posts.
---
## GET `/api/v1/tags`
List all active tags.
**Permission:** `PUBLIC`
### Response `200`
```json
{
"success": true,
"data": [
{ "id": 1, "name": "دیابت", "slug": "diabat", "status": "active" },
{ "id": 2, "name": "قلب", "slug": "ghalb", "status": "active" }
]
}
```
---
## GET `/api/v1/admin/tags`
List all tags with pagination (admin view — includes inactive).
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in name |
### Response `200`
```json
{
"success": true,
"data": [ ... ],
"meta": { "totalRecords": 10, "totalPages": 1, "currentPage": 1 }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
---
## POST `/api/v1/admin/tag`
Create a new tag.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"name": "دیابت",
"slug": "diabat",
"status": "active"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Tag name |
| `slug` | string | ❌ | Auto-generated from name if omitted |
| `status` | string | ❌ | `"active"` (default) or `"inactive"` |
### Response `201`
Tag object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing name |
---
## PATCH `/api/v1/admin/tag/{id}`
Update a tag.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `id` | integer | Tag ID |
All fields optional.
### Response `200`
Updated tag object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Tag not found |
---
## DELETE `/api/v1/admin/tag/{id}`
Delete a tag.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "تگ حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Tag not found |
+154
View File
@@ -0,0 +1,154 @@
# User Profile API
> **Prefix:** `/api/v1/user-profile`
User medical profiles store health information that can be shared with doctors.
---
## POST `/api/v1/user-profile`
Create a medical profile for the authenticated user.
**Permission:** `AUTH`
### Request Body (`application/json`)
```json
{
"name": "علی",
"family": "احمدی",
"fathers_name": "محمد",
"national_code": "0012345678",
"gender": "male",
"blood_type": "A+",
"marital_status": "married",
"education": "لیسانس",
"job": "مهندس",
"address": "تهران، خیابان ولیعصر",
"home_phone": "02112345678",
"work_phone": "02198765432",
"insurance_id": 1,
"description": "توضیحات بیشتر",
"sharing_with_user": true,
"birthday": "1370-06-15",
"date_of_birth": 648172800,
"basic_insurance": "بیمه تأمین اجتماعی",
"supplementary_insurance": "بیمه ایران",
"other": {}
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ❌ | First name |
| `family` | string | ❌ | Last name |
| `fathers_name` | string | ❌ | Father's name |
| `national_code` | string | ❌ | National ID (10 digits) |
| `gender` | string | ❌ | `"male"` or `"female"` |
| `blood_type` | string | ❌ | `"A+"`, `"A-"`, `"B+"`, `"B-"`, `"AB+"`, `"AB-"`, `"O+"`, `"O-"` |
| `marital_status` | string | ❌ | `"single"` or `"married"` |
| `education` | string | ❌ | Education level |
| `job` | string | ❌ | Occupation |
| `address` | string | ❌ | Home address |
| `home_phone` | string | ❌ | Home phone number |
| `work_phone` | string | ❌ | Work phone number |
| `insurance_id` | integer | ❌ | Insurance ID |
| `description` | string | ❌ | Additional notes |
| `sharing_with_user` | boolean | ❌ | Allow doctors to view this profile |
| `birthday` | string | ❌ | Birthday in Jalali format `YYYY-MM-DD` |
| `date_of_birth` | integer | ❌ | Birthday as Unix timestamp |
| `basic_insurance` | string | ❌ | Basic insurance name |
| `supplementary_insurance` | string | ❌ | Supplementary insurance name |
| `other` | object | ❌ | Extra metadata |
### Response `201`
```json
{
"success": true,
"data": {
"uuid": "profile-uuid-...",
"name": "علی",
"family": "احمدی",
"gender": "male",
"blood_type": "A+",
"national_code": "0012345678",
"sharing_with_user": true,
"created_at": 1717000000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_CONFLICT_001` | 409 | Profile already exists for this user |
---
## GET `/api/v1/user-profile/{uuid}`
Get a user profile.
**Permission:** `AUTH` — must be the profile owner, or a doctor if `sharing_with_user: true`, or `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Profile UUID |
### Response `200`
Full profile object including all fields.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not authorized to view this profile |
| `ERR_NOT_FOUND_001` | 404 | Profile not found |
---
## PATCH `/api/v1/user-profile/{uuid}`
Update a user profile.
**Permission:** `AUTH` — must be the profile owner
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string (UUID) | Profile UUID |
### Request Body
Same fields as POST — all optional.
### Response `200`
Updated profile object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the profile owner |
| `ERR_NOT_FOUND_001` | 404 | Profile not found |
---
## DELETE `/api/v1/user-profile/{uuid}`
Delete a user profile.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "پروفایل حذف شد" } }
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Profile not found |