Files
clinicpro/docs/api/representation.md
T

656 lines
24 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.
**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`) متعلق به یک نماینده‌ی فعال باشد (`representations.domain`).
2. پزشک/کلینیکِ موضوع خرید، `representation_id` همان نماینده را داشته باشد.
در غیر این صورت هیچ کمیسیونی برای هیچ نماینده‌ای ثبت نمی‌شود (پرداخت بدون `frontend_address` هم کمیسیون ندارد). درصد: نوبت = `commission_percent` نماینده؛ اشتراک = تنظیم سراسری `upgrade_commission_percent`. نگاشت دامنه فقط از طریق `DomainContextResolver` انجام می‌شود.
---
## پنل نماینده (ROLE_REPRESENTATION)
این 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) همان نماینده محاسبه می‌شود.