1204 lines
32 KiB
Markdown
1204 lines
32 KiB
Markdown
# 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
|
||
|
||
> **نماینده (ROLE_REPRESENTATION):** افزودن پزشک و کلینیک برای نماینده از طریق endpointهای جدا انجام میشود — `POST /api/v1/representation/doctor` و `POST /api/v1/representation/clinic` (به `docs/api/representation.md` مراجعه کنید). در نسخهی نماینده، `representation_id` پزشک خودکار روی نمایندهی کاربر جاری ست میشود. endpointهای `/api/v1/admin/*` همچنان فقط `ROLE_ADMIN` هستند.
|
||
|
||
### POST `/api/v1/admin/doctors`
|
||
|
||
ساخت پزشک جدید (در صورت نبودِ کاربر با این موبایل، یک User هم ساخته میشود).
|
||
|
||
**Permission:** `ROLE_ADMIN`
|
||
|
||
#### Request Body (`application/json`)
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `mobile` | string | ✅ | موبایل ورود؛ باید فرمت معتبر موبایل ایران داشته باشد (`^09\d{9}$`) — ارقام فارسی/عربی به انگلیسی نرمال میشوند |
|
||
| `name` | string | ✅ | نام پزشک |
|
||
| `gender` / `degree` / `medical_system_code` / `info` | string | ❌ | اطلاعات حرفهای |
|
||
| `activity_time` | integer | ❌ | Unix timestamp (ثانیه) تاریخ شروع فعالیت؛ مبنای محاسبهٔ سال تجربه |
|
||
| `specialties` | integer[] | ❌ | آرایهٔ IDهای تخصص |
|
||
|
||
#### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `VALIDATION` | 422 | `mobile` یا `name` خالی |
|
||
| `VALIDATION` | 422 | `mobile` فرمت معتبر موبایل ایران ندارد (`field: mobile`) |
|
||
|
||
---
|
||
|
||
### POST `/api/v1/admin/clinic`
|
||
|
||
ساخت کلینیک جدید (در صورت نبودِ کاربرِ صاحب با این موبایل، یک User هم ساخته میشود).
|
||
|
||
**Permission:** `ROLE_ADMIN`
|
||
|
||
#### Request Body (`application/json`)
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `owner_mobile` | string | ✅ | موبایل صاحب کلینیک؛ باید فرمت معتبر موبایل ایران داشته باشد (`^09\d{9}$`) |
|
||
| `name` | string | ✅ | نام کلینیک |
|
||
|
||
#### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `VALIDATION` | 422 | `owner_mobile` خالی |
|
||
| `VALIDATION` | 422 | `owner_mobile` فرمت معتبر موبایل ایران ندارد (`field: owner_mobile`) |
|
||
| `VALIDATION` | 422 | `name` کلینیک خالی |
|
||
|
||
---
|
||
|
||
### 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`
|
||
|
||
> **Side effect:** the clinic's insurance configuration (`tenant_insurances`, `entity_insurance_pricing`, and their `tenant_service_coverages`) is purged in the same request — polymorphic `entity_id`, cleaned up at the application level.
|
||
|
||
### Response `200`
|
||
```json
|
||
{ "success": true, "data": { "message": "کلینیک حذف شد" } }
|
||
```
|
||
|
||
---
|
||
|
||
## Appointment Management
|
||
|
||
### GET `/api/v1/admin/appointments/today-stats`
|
||
|
||
Get appointment statistics for a specific date (defaults to today).
|
||
|
||
**Permission:** `ROLE_ADMIN`
|
||
|
||
### Query Parameters
|
||
| Param | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `date` | string (YYYY-MM-DD) | ❌ | Default: today |
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"total": 47,
|
||
"completed": 20,
|
||
"waiting": 18,
|
||
"cancelled": 9
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### GET `/api/v1/admin/appointments`
|
||
|
||
List appointments filtered by date and/or doctor. Sorted by `slot_start ASC`.
|
||
|
||
**Permission:** `ROLE_ADMIN`
|
||
|
||
### Query Parameters
|
||
| Param | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `page` | integer | ❌ | Default: 1 |
|
||
| `limit` | integer | ❌ | Default: 15, max: 500 |
|
||
| `search` | string | ❌ | Search by patient name/mobile or doctor name |
|
||
| `status` | string | ❌ | Filter by status |
|
||
| `date` | string (YYYY-MM-DD) | ❌ | Filter by slot date |
|
||
| `doctor_uuid` | string | ❌ | Filter by doctor UUID |
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "appt-uuid",
|
||
"patient_name": "محمد رضایی",
|
||
"patient_mobile": "09123456789",
|
||
"doctor_uuid": "doctor-uuid",
|
||
"doctor_name": "دکتر علی احمدی",
|
||
"slot_start": 1718438400,
|
||
"slot_end": 1718439600,
|
||
"appointment_date": "2025-06-15",
|
||
"appointment_time": "09:00",
|
||
"end_time": "09:20",
|
||
"status": "confirmed",
|
||
"version": 1,
|
||
"created_at": "2025-06-14T10:30:00+03:30"
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 47, "totalPages": 1, "currentPage": 1 }
|
||
}
|
||
```
|
||
|
||
**Status values:** `pending` | `confirmed` | `completed` | `cancelled_by_doctor` | `cancelled_by_user` | `no_show` | `expired`
|
||
|
||
---
|
||
|
||
### POST `/api/v1/admin/appointment`
|
||
|
||
Create a new appointment for a patient. If no user exists with the given mobile, a new user account is created automatically.
|
||
|
||
**Permission:** `ROLE_ADMIN`
|
||
|
||
### Request Body
|
||
```json
|
||
{
|
||
"doctor_uuid": "doctor-uuid",
|
||
"slot_start": 1718438400,
|
||
"slot_end": 1718439600,
|
||
"patient_mobile": "09123456789",
|
||
"patient_name": "علی محمدی",
|
||
"note": "optional note"
|
||
}
|
||
```
|
||
|
||
> `patient_mobile` و `patient_name` هر دو اجباری هستند. اگر کاربری با این شماره موبایل نداشته باشیم، یک کاربر جدید با نقش `ROLE_USER` ساخته میشود.
|
||
|
||
### Response `201`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"uuid": "appt-uuid",
|
||
"slot_start": 1718438400,
|
||
"slot_end": 1718439600,
|
||
"status": "pending"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Error Responses
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `VALIDATION` | 422 | Missing required fields (doctor_uuid, slot_start, slot_end, patient_mobile, patient_name) |
|
||
| `DOCTOR_NOT_FOUND` | 404 | Doctor UUID not found |
|
||
| `SLOT_TAKEN` | 409 | Slot already booked |
|
||
|
||
---
|
||
|
||
## 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"` |
|
||
|
||
### Query Parameters (تکمیل)
|
||
| Param | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `search` | string | ❌ | جستجو در موبایل کاربر، `reference_id` یا `order_id` |
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"amount": 500000,
|
||
"status": "success",
|
||
"gateway": "mellat",
|
||
"ref_id": "1234567",
|
||
"patient_mobile": "0912...",
|
||
"paid_at": "2026-07-02T09:00:00+03:30",
|
||
"created_at": "2026-07-02T08:55:00+03:30"
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 7800, "totalPages": 390, "currentPage": 1 }
|
||
}
|
||
```
|
||
|
||
> `amount` بر حسب ریال، `ref_id` همان `reference_id` درگاه، `paid_at` فقط برای پرداخت `success` (بر اساس `updated_at`) و در غیر اینصورت `null`. تاریخها ISO-8601.
|
||
|
||
---
|
||
|
||
### GET `/api/v1/admin/payments/{uuid}`
|
||
|
||
جزئیات یک پرداخت. **پاسخ تخت است** (`data` مستقیم آبجکت پرداخت، نه nested).
|
||
|
||
**Permission:** `ROLE_ADMIN`
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"uuid": "...",
|
||
"order_id": "ORD-XXXX",
|
||
"amount": 500000,
|
||
"status": "success",
|
||
"gateway": "mellat",
|
||
"type": "appointment",
|
||
"ref_id": "1234567",
|
||
"patient_mobile": "0912...",
|
||
"patient_name": "علی احمدی",
|
||
"appointment_uuid": "...",
|
||
"paid_at": "2026-07-02T09:00:00+03:30",
|
||
"created_at": "2026-07-02T08:55:00+03:30"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Errors
|
||
| Code | HTTP | Description |
|
||
|------|------|-------------|
|
||
| `ERR_NOT_FOUND_001` | 404 | پرداخت یافت نشد |
|
||
|
||
---
|
||
|
||
## 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: 15 |
|
||
| `search` | string | ❌ | Search by name or mobile (representation's or linked user's) |
|
||
| `city_id` | integer | ❌ | Filter by city |
|
||
|
||
### Response `200`
|
||
Paginated representation list. Each item:
|
||
```json
|
||
{
|
||
"id": 3,
|
||
"uuid": "...",
|
||
"full_name": "حامد حسینی",
|
||
"mobile_number": "09120671756",
|
||
"city_id": 132,
|
||
"city": "یزد",
|
||
"commission_percent": 10.0,
|
||
"wallet_balance": 0,
|
||
"is_active": true,
|
||
"created_at": "2026-06-18T..."
|
||
}
|
||
```
|
||
|
||
> `mobile_number` falls back to the linked user's mobile when the representation's own `mobile_number` column is empty.
|
||
|
||
> **Deactivation, not deletion:** the admin panel deactivates a representation via `PATCH /api/v1/representation/{uuid}` with `{ "active": false }` rather than calling `DELETE`.
|
||
|
||
---
|
||
|
||
## 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 |
|
||
|
||
> Ratings are multi-dimensional (five 0–100 dimensions). Each row's `overall` is the mean of the five dimensions (`0–100`) and `score` is that mean on a 0–5 scale (`overall / 20`).
|
||
|
||
---
|
||
|
||
### 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: 15, max 100 |
|
||
| `tag` | string | ❌ | فیلتر بر اساس تگ: `global` \| `otp` \| `payment` \| `clinic_invitation` \| `pre_registration` \| `notification_mobile` \| `user_template` |
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"recipient": "09123456789",
|
||
"message": "کد تأیید: 123456",
|
||
"status": "sent",
|
||
"provider": "kavenegar",
|
||
"tag": "otp",
|
||
"sent_at": "2026-06-19T...",
|
||
"created_at": "2026-06-19T..."
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 5000, "totalPages": 334, "currentPage": 1 }
|
||
}
|
||
```
|
||
> `tag` نوع پیامک را مشخص میکند؛ پیشفرض پیامکهای سیستمیِ بیبرچسب `global` است.
|
||
|
||
---
|
||
|
||
### 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 |
|
||
|
||
---
|
||
|
||
## Settings
|
||
|
||
### GET /api/v1/admin/settings
|
||
|
||
Returns all site configuration values.
|
||
|
||
**Response `200`**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"commission_enabled": "0",
|
||
"commission_percent": "0",
|
||
"site_name": "ClinicPro",
|
||
"support_phone": "",
|
||
"max_cancel_hours_before": "24",
|
||
"appointment_reminder_hours": "2",
|
||
"log_retention_days": "90",
|
||
"payment_test_mode": "0",
|
||
"mellat_enabled": "1",
|
||
"mellat_terminal_id": "",
|
||
"mellat_username": "",
|
||
"mellat_password": "",
|
||
"sep_enabled": "1",
|
||
"sep_terminal_id": "",
|
||
"sms_provider": "kavenegar",
|
||
"kavenegar_api_key": "",
|
||
"kavenegar_sender": "",
|
||
"sms_price_rials": "500"
|
||
}
|
||
}
|
||
```
|
||
|
||
All values are strings. Missing keys return their default values.
|
||
|
||
### PATCH /api/v1/admin/settings
|
||
|
||
Update one or more settings. Unknown keys are silently ignored.
|
||
|
||
**Request body** (partial update — send only keys to change):
|
||
```json
|
||
{
|
||
"commission_enabled": "1",
|
||
"commission_percent": "5",
|
||
"site_name": "کلینیکپرو",
|
||
"payment_test_mode": "1",
|
||
"mellat_terminal_id": "12345678",
|
||
"mellat_username": "user",
|
||
"mellat_password": "pass",
|
||
"sep_terminal_id": "87654321",
|
||
"sms_provider": "kavenegar",
|
||
"kavenegar_api_key": "your-api-key",
|
||
"kavenegar_sender": "10008664",
|
||
"sms_price_rials": "500"
|
||
}
|
||
```
|
||
|
||
**Response `200`** — same shape as GET, returns all settings after save.
|
||
|
||
**Commission rules:**
|
||
- `commission_enabled` — `"1"` = active, `"0"` = inactive
|
||
- `commission_percent` — integer string, `0`–`100`
|
||
- Commission applies only to regular users (`booked_by = user`); secretaries are exempt
|
||
|
||
**Payment gateway rules:**
|
||
- `payment_test_mode` — `"1"` = all payments use MockGateway (no real bank calls), `"0"` = real gateways
|
||
- `mellat_enabled` / `sep_enabled` — `"1"` = gateway selectable, `"0"` = gateway disabled (hidden from the payment selection list and rejected at `initiate`). Unset = enabled (default).
|
||
- `payment_allowed_frontend_hosts` — comma-separated hosts allowed as a payment `frontend_address` (origin site to return to). Falls back to the `ALLOWED_FRONTEND_HOSTS` env var when empty. Add a consumer site's host here to permit its payments.
|
||
- Gateway credentials (mellat/sep) read from DB first, fallback to env vars if DB value is empty
|
||
- MockGateway callback: same URL pattern + `&mock=1&ResCode=0&RefId=MOCK-{orderId}` (add `&cancel=1` to simulate a user cancellation → `canceled`)
|
||
|
||
**SMS provider rules:**
|
||
- `sms_provider` — `"kavenegar"` or `"rangineh"`
|
||
- Kavenegar API key and sender read from DB first, fallback to env vars `KAVENEGAR_API_KEY`, `KAVENEGAR_SENDER`
|
||
|
||
---
|
||
|
||
## Pre-Registration Management
|
||
|
||
### GET `/api/v1/admin/pre-registrations`
|
||
|
||
List pre-registration requests. **Permission:** `ROLE_ADMIN`
|
||
|
||
**Query params:**
|
||
|
||
| Param | Default | Notes |
|
||
|-------|---------|-------|
|
||
| `page` | 1 | |
|
||
| `limit` | 20 | max 50 |
|
||
| `status` | `pending` | `pending` \| `approved` \| `rejected` \| `all` |
|
||
|
||
**Response `200`** (paginated):
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"type": "independent_doctor",
|
||
"name": "دکتر احمدی",
|
||
"mobile": "09121234567",
|
||
"info": "متخصص داخلی",
|
||
"status": "pending",
|
||
"admin_note": null,
|
||
"created_at": 1718000000
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 5, "totalPages": 1, "currentPage": 1 }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### POST `/api/v1/admin/pre-registrations/{uuid}/approve`
|
||
|
||
Approve a pending request. Creates User + Doctor/Clinic entity based on `type`, resets password, sends SMS. **Permission:** `ROLE_ADMIN`
|
||
|
||
**Response `200`:**
|
||
```json
|
||
{ "success": true, "data": { "message": "تأیید شد و اطلاعات ورود ارسال گردید" } }
|
||
```
|
||
|
||
**Error Codes:**
|
||
|
||
| Code | HTTP | Meaning |
|
||
|------|------|---------|
|
||
| `NOT_FOUND` | 404 | UUID not found |
|
||
| `ALREADY_PROCESSED` | 409 | Status is not pending |
|
||
|
||
---
|
||
|
||
### POST `/api/v1/admin/pre-registrations/{uuid}/reject`
|
||
|
||
Reject a pending request. **Permission:** `ROLE_ADMIN`
|
||
|
||
**Request body** (optional):
|
||
```json
|
||
{ "note": "مدارک ناقص است" }
|
||
```
|
||
|
||
**Response `200`:**
|
||
```json
|
||
{ "success": true, "data": { "message": "درخواست رد شد" } }
|
||
```
|
||
|
||
---
|
||
|
||
## موتور مالی نمایندگی
|
||
|
||
تنظیمات مالی از طریق `GET`/`PATCH /api/v1/admin/settings` کنترل میشوند (کلیدها در whitelist `SiteConfigController::ALLOWED_KEYS`):
|
||
|
||
| کلید | پیشفرض | شرح |
|
||
|------|---------|-----|
|
||
| `appointment_commission_enabled` | `0` | فعالسازی پورسانت نوبت (درصد از `Representation.commission_percent` هر نماینده) |
|
||
| `upgrade_commission_enabled` | `0` | فعالسازی پورسانت ارتقاء اشتراک |
|
||
| `upgrade_commission_percent` | `20` | درصد پورسانت ارتقاء (سراسری) |
|
||
| `tax_enabled` | `0` | فعالسازی مالیات بر ارزش افزوده |
|
||
| `tax_percent` | `10` | درصد مالیات |
|
||
| `sms_panel_fee_rials` | `1500000` | هزینه ثابت پنل پیامک به ریال (از نوبت و اشتراک کسر میشود) |
|
||
| `sms_price_rials` | `500` | هزینه هر پیامک ارسالی به ریال؛ مبنای محاسبهٔ تعداد پیامک از موجودی کیفپول (`GET /api/v1/sms/wallet/balance`). قابل ویرایش در `/admin/settings` → بخش پیامک |
|
||
| `appointment_fee_rials` | `150000` | مبلغ هر نوبت به ریال؛ مبلغی که بیمار هنگام رزرو آنلاین پرداخت میکند. backend از همین کلید میخواند و در `GET /api/v1/payment/config` expose میشود |
|
||
| `log_retention_days` | `90` | مدت نگهداری لاگها (روز)؛ کاماند روزانه `app:prune-logs` لاگهای قدیمیتر را حذف میکند. `0` = نگهداری نامحدود |
|
||
|
||
**ترتیب محاسبه** (در `CommissionService`): ۱) کسر `sms_panel_fee_rials` ۲) مالیاتِ استخراجی `afterSms × tax/(100+tax)` ۳) پورسانت = `netAfterTax × percent/100`. سهم نماینده به کیفپولش (`WalletTransaction` credit) واریز و یک ردیف `FinancialBreakdown` ثبت میشود (idempotent بر اساس `payment_id`).
|
||
|
||
> هر تغییر `tax_percent`/`tax_enabled` در یک ردیف `TaxRateHistory` ثبت و از `GET /api/v1/admin/settings/tax-history` قابل مشاهده است.
|
||
|
||
### GET `/api/v1/admin/financial-breakdowns`
|
||
|
||
لیست تفکیک مالی تراکنشها (paginated). **Permission:** `ROLE_ADMIN`
|
||
|
||
**Query Parameters:**
|
||
|
||
| Param | Type | Description |
|
||
|-------|------|-------------|
|
||
| `page` | integer | پیشفرض 1 |
|
||
| `limit` | integer | پیشفرض 15، حداکثر 100 |
|
||
| `representation_id` | integer | فیلتر نماینده |
|
||
| `source` | string | `appointment` یا `subscription` |
|
||
| `from` | integer | Unix timestamp شروع بازه |
|
||
| `to` | integer | Unix timestamp پایان بازه |
|
||
|
||
**Response `200` (paginated):**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "…", "order_id": "ORD-…", "source": "appointment",
|
||
"gross_rials": 2000000, "sms_fee_rials": 1500000,
|
||
"tax_percent": 10, "tax_rials": 45455, "net_after_tax_rials": 454545,
|
||
"commission_percent": 20, "representation_share_rials": 90909,
|
||
"system_share_rials": 363636,
|
||
"representation_id": 3, "representation_name": "نماینده یزد",
|
||
"doctor_id": 12, "clinic_id": null, "created_at": "2026-06-24T…"
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
|
||
}
|
||
```
|
||
|
||
### GET `/api/v1/admin/financial-summary`
|
||
|
||
جمع کل مبالغ. **Permission:** `ROLE_ADMIN`
|
||
|
||
**Response `200`:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"total_gross": 2000000,
|
||
"total_representation_income": 90909,
|
||
"total_tax_collected": 45455,
|
||
"total_sms_fee": 1500000,
|
||
"total_system_share": 363636
|
||
}
|
||
}
|
||
```
|
||
|
||
### GET `/api/v1/admin/settings/tax-history`
|
||
|
||
تاریخچهی تغییرات مالیات بر ارزش افزوده (۵۰ ردیف آخر، نزولی). هر بار که `tax_percent` یا `tax_enabled` از طریق `PATCH /api/v1/admin/settings` تغییر کند، یک ردیف با کاربرِ تغییردهنده ثبت میشود. **Permission:** `ROLE_ADMIN`
|
||
|
||
**Response `200`:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{ "tax_percent": 10, "enabled": true, "changed_by_name": "مدیر سیستم", "changed_at": 1782800000 }
|
||
]
|
||
}
|
||
```
|
||
|
||
### GET `/api/v1/admin/settlement/{uuid}`
|
||
|
||
جزئیات یک درخواست تسویه (برای صفحهی `/admin/settlements/{uuid}`). **Permission:** `ROLE_ADMIN`
|
||
|
||
**Response `200`:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"uuid": "...",
|
||
"representation_name": "نماینده یزد",
|
||
"representation_mobile": "09390036732",
|
||
"amount": 500000,
|
||
"status": "pending",
|
||
"bank_card": "6037...", "bank_name": "ملت", "bank_iban": "IR...", "bank_owner": "...",
|
||
"reject_reason": null,
|
||
"requested_at": "2026-06-24T...", "processed_at": null
|
||
}
|
||
}
|
||
```
|
||
تأیید/رد از طریق `POST /api/v1/settlement/{uuid}/approve|reject` (در `docs/api/settlement.md`).
|
||
|
||
**Errors:** `NOT_FOUND` (404) — درخواست یافت نشد.
|
||
|
||
---
|
||
|
||
## Application Logs
|
||
|
||
Persisted application logs (`warning` level and above). Written by the `DbLogger`
|
||
decorator over the `logger` service into the `app_log` table — every
|
||
`LoggerInterface::warning()/error()/critical()/...` call across the backend lands
|
||
here, while `info`/`debug` go to stderr only.
|
||
|
||
### GET `/api/v1/admin/logs`
|
||
|
||
List persisted logs with pagination and filters.
|
||
|
||
**Permission:** `ROLE_ADMIN`
|
||
|
||
### Query Parameters
|
||
| Param | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `page` | integer | ❌ | Default: 1 |
|
||
| `limit` | integer | ❌ | Default: 25 (max 100) |
|
||
| `level` | string | ❌ | Exact PSR level: `warning`, `error`, `critical`, `alert`, `emergency` |
|
||
| `search` | string | ❌ | Substring match on the message |
|
||
| `from` | integer | ❌ | Unix timestamp lower bound (`created_at >=`) |
|
||
| `to` | integer | ❌ | Unix timestamp upper bound (`created_at <=`) |
|
||
|
||
Ordered by newest first (`id DESC`).
|
||
|
||
### Response `200`
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"id": 4213,
|
||
"level": "error",
|
||
"message": "Unhandled exception: RuntimeException: boom @ /var/www/html/src/Foo.php:42 [path=/oauth/userinfo]",
|
||
"context": "{\"exception\":\"RuntimeException: boom @ /var/www/html/src/Foo.php:42\"}",
|
||
"channel": "app",
|
||
"path": "/oauth/userinfo",
|
||
"created_at": 1717000000
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 137, "totalPages": 6, "currentPage": 1 }
|
||
}
|
||
```
|
||
|
||
Notes:
|
||
- `context` is a JSON string (or `null`); a `Throwable` in the context is stored as a compact `Class: message @ file:line` string, never the raw object.
|
||
- `created_at` is a Unix timestamp (integer).
|
||
|
||
### DELETE `/api/v1/admin/logs`
|
||
|
||
Delete **all** persisted logs (truncate the `app_log` table). Irreversible.
|
||
|
||
**Permission:** `ROLE_ADMIN`
|
||
|
||
#### Response `200`
|
||
```json
|
||
{ "success": true, "data": { "deleted": 137 } }
|
||
```
|
||
|
||
- `deleted` — number of rows removed.
|
||
|
||
### Log Retention
|
||
|
||
Logs are pruned automatically based on the `log_retention_days` setting (see [Site Settings](#) — `GET`/`PATCH /api/v1/admin/settings`, whitelisted key `log_retention_days`, default `90`, `0` = keep forever).
|
||
|
||
- A daily scheduled task (`App\Shared\Logging\Message\PruneLogsMessage`, registered in `src/Schedule.php`, routed to `scheduler_default`) deletes logs older than `log_retention_days`.
|
||
- Manual prune: `php bin/console app:prune-logs` (reads the same setting, deletes older-than-retention rows, prints the count).
|
||
- Requires the scheduler worker: `php bin/console messenger:consume scheduler_default`.
|