Files
clinicpro/docs/api/representation.md
T
hamedandClaude Opus 5 f863b39a74 fix(representation): count only online bookings in the agent panel
The agent's own dashboard (dashboard/summary and doctors/performance) counted
every appointment belonging to their doctors, so the bookings a secretary types
into the panel — which earn the agent nothing — sat next to a commission column
that ignored them. Both now count only bookings that came from the agent's own
site, matching the monthly/yearly report. The per-doctor income column is also
scoped to this agent, since a doctor may have been under another one before.

Adds a backfill for the bookings paid before city domains resolved to an agent:
they carry neither booking_representation_id nor a FinancialBreakdown, and
neither can be recovered by replaying the request. Both are derived from
payments.frontend_address, the address the payment was started from. The
recovered breakdown is dated to the payment, not to the run, or a year of
commission would land in "today". Dry-run by default; re-running is a no-op,
and a payment whose domain does not match the doctor's owner is reported and
skipped rather than retried forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:24:48 +03:30

676 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.
# 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_ids` | integer[] | ❌ | شهرهای تحت پوشش (چند-شهری). `city_id` تکی هم برای BC پذیرفته می‌شود |
| `domain` | string | ❌ | دامنه اختصاصی نماینده (نرمال می‌شود: بدون scheme/www). یکتا؛ نباید با دامنه شهرها تداخل کند. **admin-only** |
| `is_global` | boolean | ❌ | نماینده سراسری — سایتِ دامنه‌اش فقط پزشکان/کلینیک‌های خودش را نشان می‌دهد. **admin-only** |
| `commission_percent` | float | ❌ | Commission rate (0100) |
| `bank_account` | object | ❌ | Bank details for settlements |
**پاسخ‌ها اکنون شامل:** `city_ids: int[]`، `cities: [{id, name}]`، `domain`، `is_global` (علاوه بر `city_id` قدیمی = اولین شهر).
### 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 | `mobile_number` فرمت معتبر موبایل ایران (`^09\d{9}$`) ندارد (`field: mobile_number`) |
| `ERR_VALIDATION_002` | 422 | `mobile_number` یا `full_name` خالی |
---
## 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. `city_ids: int[]` جایگزین `city_id` است (تکی هم پذیرفته می‌شود).
**Privileged fields:** `commission_percent`، `active`، `domain` و `is_global` **admin-only** هستند — نماینده روی رکورد خودش فقط `full_name`، `city_ids`، `bank_account` را می‌تواند تغییر دهد. `commission_percent` باید در بازه `0100` باشد. خطاهای `domain`: نامعتبر → 422، تکراری یا برخورد با دامنه شهر → 409.
### Response `200`
Updated representation object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not owner or admin |
| `ERR_AUTH_006` | 403 | Non-admin tried to change `commission_percent` or `active` |
| `ERR_VALIDATION_001` | 422 | `commission_percent` خارج از بازه ۰ تا ۱۰۰ (`field: commission_percent`) |
| `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.
`total_appointments` فقط نوبت‌های **آنلاین** را می‌شمارد — یعنی نوبت‌هایی که از سایتِ همین نماینده رزرو شده‌اند (`appointments.booking_representation_id` برابر همین نماینده). نوبتی که منشی در پنل ثبت می‌کند از سایت نیامده و در آمار نماینده نمی‌آید.
**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
}
}
}
```
---
## GET `/api/v1/site-context`
**عمومی (بدون auth).** نگاشت یک دامنه به زمینه‌ی سایت — مصرف‌کننده: سایت عمومی nobat724 برای دامنه‌های خارج از `data/city.json` (دامنه اختصاصی نمایندگان سراسری).
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `domain` | string | ✅ | host یا URL کامل؛ نرمال می‌شود (scheme/www/پورت حذف) |
### Response `200`
```json
{
"success": true,
"data": {
"type": "representation",
"city": null,
"representation": { "uuid": "...", "full_name": "نماینده الف", "is_global": true }
}
}
```
`type`: `city` (دامنه یکی از شهرها) | `representation` (دامنه اختصاصی نماینده فعال) | `unknown`. برای `city`، آبجکت `city: {id, name}` پر می‌شود.
---
## قانون کمیسیون دامنه‌محور
کمیسیون (نوبت **و** اشتراک) فقط وقتی ثبت می‌شود که **هر دو** شرط برقرار باشد:
1. دامنه‌ی مبدأ خرید (`payment.frontend_address`) به یک نماینده‌ی فعال برسد.
2. پزشک/کلینیکِ موضوع خرید، `representation_id` همان نماینده را داشته باشد.
دامنه به نماینده به این ترتیب می‌رسد:
- نماینده‌ای که همان دامنه را در `representations.domain` ثبت کرده (نماینده‌ی سراسری).
- وگرنه اگر دامنه، دامنه‌ی یک شهر باشد (`cities.domain`)، نماینده‌ی فعالِ همان شهر از `representation_cities`.
اگر دو نماینده‌ی فعال یک شهر را پوشش دهند، نماینده‌ای انتخاب نمی‌شود: انتساب پول مبهم است و باید در داده صریح شود.
در غیر این صورت هیچ کمیسیونی برای هیچ نماینده‌ای ثبت نمی‌شود (پرداخت بدون `frontend_address` هم کمیسیون ندارد). درصد: نوبت = `commission_percent` نماینده؛ اشتراک = تنظیم سراسری `upgrade_commission_percent`. نگاشت دامنه فقط از طریق `DomainContextResolver` انجام می‌شود.
**زمان ثبت:** کمیسیون نوبت در لحظه‌ی **پرداخت موفق** ثبت می‌شود، نه در لحظه‌ی تأیید نوبت. نوبتِ `pending` هم کمیسیون دارد؛ تأیید کارِ پزشک/منشی است و ممکن است هرگز انجام نشود. ثبت idempotent است و مسیر تأیید دوباره چیزی نمی‌سازد.
---
## پنل نماینده (ROLE_REPRESENTATION)
> **شمارش نوبت:** `dashboard/summary` و `doctors/performance` هم فقط نوبت‌های آنلاینِ همین نماینده را می‌شمارند (`appointments.booking_representation_id`). نوبتی که منشی در پنل ثبت می‌کند شمرده نمی‌شود. ستون درآمدِ هر پزشک هم فقط سهم همین نماینده است، نه سهم نمایندگان قبلیِ آن پزشک.
>
> **بازسازی گذشته:** نوبت‌های آنلاینی که پیش از نگاشت دامنه‌ی شهری پرداخت شده‌اند نه `booking_representation_id` دارند و نه ردیف `FinancialBreakdown`. دستور زیر هر دو را از روی `payments.frontend_address` می‌سازد؛ بدون `--force` فقط گزارش می‌دهد و تاریخ ردیف مالی روی لحظه‌ی پرداخت می‌نشیند، نه لحظه‌ی اجرا:
>
> ```
> php bin/console app:representation:backfill-online-commission [--force]
> ```
این endpointها برای کاربرِ دارای نقش `ROLE_REPRESENTATION` در پنل ادمین (`/admin`) هستند. مالکیت همیشه از کاربر جاری (`#[CurrentUser]` + `findByUser`) تعیین می‌شود؛ هیچ uuid/id ورودی برای تعیین مالکیت پذیرفته نمی‌شود.
> **Permission (همه‌ی این بخش):** `ROLE_REPRESENTATION`
### GET `/api/v1/representation/me`
پروفایل نماینده‌ی کاربر جاری.
#### Response `200`
```json
{
"success": true,
"data": {
"data": {
"uuid": "...",
"full_name": "حامد حسینی",
"mobile_number": "09120671756",
"city_id": 132,
"commission_percent": "10.00",
"bank_account": [
{ "id": "iban-uuid-1", "iban": "IR000000000000000000000000", "bank_name": "بانک ملت", "owner_name": "حامد حسینی", "verified": true, "created_at": 1718000000 }
],
"active": true,
"created_at": 1718000000,
"national_code": "0012345678",
"national_code_verified": true
}
}
}
```
> double-nested: مقدار با `data.data` استخراج می‌شود. `bank_account` آرایه‌ای از ۰ تا ۲ شبا است (`null` اگر هیچ شبایی ثبت نشده). `national_code`/`national_code_verified` از کاربرِ نماینده می‌آید.
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | کاربر جاری نماینده نیست |
---
### POST `/api/v1/representation/verify-national-code`
تأیید کد ملی نماینده با استعلام **شاهکار** (`s.api.ir` → ShahkarLite): تطبیق کد ملی با موبایلِ کاربر جاری. در صورت موفقیت، `national_code` ذخیره و `national_code_verified=true` می‌شود.
#### Request Body
```json
{ "national_code": "0012345678" }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `national_code` | string | ✅ | کد ملی ۱۰ رقمی |
#### Response `200`
آبجکت پروفایل نماینده (مثل `me`، با `national_code_verified: true`).
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | کد ملی ۱۰ رقم نیست (`field: national_code`) |
| `ERR_IDENTITY_001` | 422 | کد ملی متعلق به این موبایل نیست (`field: national_code`) |
| `ERR_EXTERNAL_001` | 502 | خطا در استعلام |
| `ERR_EXTERNAL_002` | 503 | سرویس استعلام پیکربندی نشده |
---
### POST `/api/v1/representation/iban`
افزودن یک شماره شبا. ابتدا با **IbanMatch** (`s.api.ir`) بررسی می‌شود شبا متعلق به کد ملیِ تأییدشده‌ی نماینده باشد. حداکثر ۲ شبا.
#### Request Body
```json
{ "iban": "IR000000000000000000000000", "birth_date": "1370/01/01" }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `iban` | string | ✅ | شماره شبا (با/بدون `IR` و فاصله؛ نرمال‌سازی می‌شود) |
| `birth_date` | string | ✅ | تاریخ تولد شمسی `Y/m/d` (نمونه `1370/01/01`). فقط برای استعلام IbanMatch؛ **ذخیره نمی‌شود.** |
#### Response `200`
آبجکت پروفایل نماینده با `bank_account` به‌روزشده.
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_IDENTITY_004` | 409 | کد ملی هنوز تأیید نشده |
| `ERR_IDENTITY_003` | 409 | سقف ۲ شبا پر است |
| `ERR_VALIDATION_001` | 422 | شبا نامعتبر (`field: iban`) یا تاریخ تولد نامعتبر (`field: birth_date`) |
| `ERR_IDENTITY_002` | 422 | شبا متعلق به نماینده نیست (`field: iban`) |
| `ERR_EXTERNAL_001` | 502 | خطا در استعلام |
| `ERR_EXTERNAL_002` | 503 | سرویس استعلام پیکربندی نشده |
---
### DELETE `/api/v1/representation/iban/{id}`
حذف یک شماره شبا با `id` آن (از `bank_account[].id`).
#### Response `200`
آبجکت پروفایل نماینده با `bank_account` به‌روزشده.
---
### POST `/api/v1/representation/doctor`
افزودن پزشک توسط نماینده. `representation_id` پزشک به‌صورت خودکار روی نماینده‌ی کاربر جاری ست می‌شود. پس از ثبت موفق، یک پیامک خوش‌آمد (تگ `welcome`) به‌صورت async به موبایل پزشک ارسال می‌شود.
#### Request Body
```json
{ "mobile": "0935...", "name": "...", "gender": "man", "degree": "...", "medical_system_code": "...", "specialties": [1,2] }
```
| Field | Type | Required |
|-------|------|----------|
| `mobile` | string | ✅ |
| `name` | string | ✅ |
| `gender` / `degree` / `medical_system_code` / `info` | string | ❌ |
| `activity_time` | integer (Unix ts، تاریخ شروع فعالیت) | ❌ |
| `specialties` | integer[] | ❌ — هر شناسهٔ فرزند با تمام والدهایش تا ریشهٔ درخت گسترش می‌یابد |
#### Response `201`
```json
{ "success": true, "data": { "uuid": "..." } }
```
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 422 | موبایل یا نام خالی |
| `ERR_VALIDATION_001` | 422 | `mobile` فرمت معتبر موبایل ایران ندارد (`field: mobile`) |
| `ERR_CONFLICT_001` | 409 | این کاربر قبلاً پزشک است |
---
### POST `/api/v1/representation/clinic`
افزودن کلینیک توسط نماینده. `representation_id` کلینیک خودکار روی نماینده‌ی کاربر جاری ست می‌شود (مثل createDoctor) تا در لیست‌های scoped دیده شود. پس از ثبت موفق، یک پیامک خوش‌آمد (تگ `welcome`) به‌صورت async به موبایل مالک کلینیک ارسال می‌شود.
#### Request Body
```json
{ "owner_mobile": "0935...", "name": "کلینیک ...", "telephone": "...", "address": "..." }
```
| Field | Type | Required |
|-------|------|----------|
| `owner_mobile` | string | ✅ |
| `name` | string | ✅ |
| `telephone` / `address` / `info` | string | ❌ |
#### Response `200`
```json
{ "success": true, "data": { "uuid": "...", "name": "...", "is_active": true } }
```
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 422 | موبایل یا نام خالی |
| `ERR_VALIDATION_001` | 422 | `owner_mobile` فرمت معتبر موبایل ایران ندارد (`field: owner_mobile`) |
---
### GET `/api/v1/representation/appointments`
نوبت‌های همه‌ی پزشکانی که `representation_id` آن‌ها = نماینده‌ی کاربر جاری است (paginated، با شکل آیتمِ یکسان با `/api/v1/admin/appointments`).
#### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | پیش‌فرض 1 |
| `limit` | integer | ❌ | پیش‌فرض 15، حداکثر 500 |
| `status` | string | ❌ | فیلتر وضعیت |
| `date` | string (YYYY-MM-DD) | ❌ | فیلتر تاریخِ نوبت |
| `search` | string | ❌ | جستجو در موبایل/نام بیمار یا نام پزشک |
#### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...",
"patient_name": "...",
"patient_mobile": "0912...",
"doctor_uuid": "...",
"doctor_name": "...",
"slot_start": 1718000000,
"slot_end": 1718001800,
"appointment_date": "2025-06-15",
"appointment_time": "10:00",
"end_time": "10:30",
"status": "confirmed",
"created_at": 1717900000
}
],
"meta": { "totalRecords": 12, "totalPages": 1, "currentPage": 1 }
}
```
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | کاربر جاری نماینده نیست |
---
### GET `/api/v1/representation/doctors`
پزشکانِ ثبت‌شده توسط نماینده‌ی جاری (فقط ردیف‌های `representation_id = نماینده‌ی کاربر جاری`). شکل آیتم یکسان با `GET /api/v1/admin/doctors` است.
> **Permission:** `ROLE_REPRESENTATION` — id نماینده از `#[CurrentUser]` تعیین می‌شود، نه از query (نماینده نمی‌تواند داده‌ی نماینده‌ی دیگر را ببیند).
#### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | پیش‌فرض 1 |
| `limit` | integer | ❌ | پیش‌فرض 15، حداکثر 100 |
| `search` | string | ❌ | جستجو در نام یا موبایل پزشک |
#### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...", "id": 12, "name": "...", "gender": "man", "degree": "...",
"medical_code": "...", "mobile": "0912...", "email": null,
"is_active": true, "rate": 3.5, "specialties": [],
"profile_image": null, "created_at": "2026-06-18T..."
}
],
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}
```
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | کاربر جاری نماینده نیست |
---
### GET `/api/v1/representation/doctors/stats`
آمار پزشکانِ ثبت‌شده توسط نماینده‌ی جاری (فقط `representation_id = نماینده‌ی کاربر جاری`). شکل پاسخ سازگار با `GET /api/v1/admin/doctors/stats` (بدون `top_specialty`). فرانت‌اند کارت‌های «کل پزشکان / فعال / غیرفعال / مرد / زن» را از این endpoint برای نقش نماینده پر می‌کند.
> **Permission:** `ROLE_REPRESENTATION` — id نماینده از `#[CurrentUser]`.
#### Response `200`
```json
{
"success": true,
"data": { "total": 12, "active": 9, "inactive": 3, "male": 7, "female": 5 }
}
```
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | کاربر جاری نماینده نیست |
---
### POST `/api/v1/representation/doctors/{uuid}/status`
فعال/غیرفعال کردن پزشکِ زیرمجموعه‌ی نماینده‌ی جاری (toggle `active_doctor_appointment`). فقط روی پزشکانی که `representation_id` آن‌ها برابر نماینده‌ی کاربر جاری است؛ در غیر این صورت 404.
> **Permission:** `ROLE_REPRESENTATION` — مالکیت از `#[CurrentUser]` چک می‌شود.
#### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string | uuid پزشک |
#### Response `200`
```json
{ "success": true, "data": { "is_active": false } }
```
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | کاربر جاری نماینده نیست، یا پزشک یافت نشد / متعلق به این نماینده نیست |
---
### GET `/api/v1/representation/clinics`
کلینیک‌های ثبت‌شده توسط نماینده‌ی جاری (فقط `representation_id = نماینده‌ی کاربر جاری`). شکل آیتم سازگار با `GET /api/v1/admin/clinics`.
> **Permission:** `ROLE_REPRESENTATION` — id نماینده از `#[CurrentUser]`.
#### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | پیش‌فرض 1 |
| `limit` | integer | ❌ | پیش‌فرض 15، حداکثر 100 |
| `search` | string | ❌ | جستجو در نام یا تلفن کلینیک |
#### Response `200`
```json
{
"success": true,
"data": [
{
"uuid": "...", "id": 5, "name": "کلینیک ...", "telephone": "...",
"logo": null, "clinic_logo": null, "is_active": true,
"doctors_count": 0, "created_at": "2026-06-18T..."
}
],
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}
```
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | کاربر جاری نماینده نیست |
---
## داشبورد، عملکرد و مالیِ نماینده‌ی جاری
> همه‌ی این endpointها `#[IsGranted('ROLE_REPRESENTATION')]` و scope بر اساس `#[CurrentUser]` (نه uuid مسیر). درآمد همیشه از `FinancialBreakdown.representation_share_rials` (پورسانت واقعیِ ثبت‌شده) محاسبه می‌شود، نه مبلغ کل نوبت. بازه‌ها: امروز=`strtotime('today')`, هفته=۷ روز اخیر, ماه=۳۰ روز اخیر.
### GET `/api/v1/representation/dashboard/summary`
خلاصه‌ی آمار نوبت و درآمد نماینده‌ی جاری.
**Response `200`:**
```json
{
"success": true,
"data": {
"appointments": { "today": 0, "week": 3, "month": 12, "total": 40 },
"income": {
"today": 0, "week": 270000, "month": 909090, "total": 3000000,
"settlable_rials": 2090910, "settled_rials": 500000, "pending_rials": 0
}
}
}
```
`settlable_rials` = موجودی کیف‌پول (`getWalletBalance``settled_rials` = جمع Settlementهای `paid`؛ `pending_rials` = جمع `pending`+`approved`.
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | کاربر جاری نماینده نیست |
### GET `/api/v1/representation/doctors/performance`
عملکرد پزشکانِ نماینده‌ی جاری (paginated). **Query:** `page`, `limit`.
**Response `200` (paginated):**
```json
{
"success": true,
"data": [
{
"uuid": "...", "name": "...",
"appointments": { "today": 0, "week": 1, "month": 4, "total": 18 },
"representation_income_rials": 363636,
"subscription_status": "active"
}
],
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}
```
`subscription_status`: `active` (اشتراک فعال دارد) یا `none`.
### GET `/api/v1/representation/finance/report`
گزارش مالی بازه‌ای از ردیف‌های `FinancialBreakdown` نماینده‌ی جاری (paginated). **Query:** `page`, `limit`, `from` (Unix ts), `to` (Unix ts).
**Response `200` (paginated):**
```json
{
"success": true,
"data": [
{
"uuid": "...", "appointment_uuid": "...", "doctor_name": "...",
"gross_rials": 2000000, "tax_rials": 45455, "sms_fee_rials": 1500000,
"commission_percent": 20, "representation_share_rials": 90909,
"created_at": "2026-06-24T..."
}
],
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}
```
> **اصلاح `buildStats`** (در `GET /api/v1/representation/{uuid}/dashboard/monthly|yearly`): قبلاً آمار را به نماینده فیلتر نمی‌کرد (کلِ پلتفرم). اکنون `total_appointments` فقط نوبت‌های پزشکانِ همان نماینده، `commission_rials` از `FinancialBreakdown.representation_share_rials`، و `total_revenue_rials` از `FinancialBreakdown.gross_rials` (source=appointment) همان نماینده محاسبه می‌شود.