Files
clinicpro/docs/api/admin.md
T

1054 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | ✅ | نام پزشک |
#### 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`
### 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"` |
### 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: 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 0100 dimensions). Each row's `overall` is the mean of the five dimensions (`0100`) and `score` is that mean on a 05 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",
"payment_test_mode": "0",
"mellat_terminal_id": "",
"mellat_username": "",
"mellat_password": "",
"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
- `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` | هزینه ثابت پنل پیامک به ریال (از نوبت و اشتراک کسر می‌شود) |
**ترتیب محاسبه** (در `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 }
]
}
```