Three things kept a city-site booking from ever reaching its representative. The domain never resolved. City sites carry their own domain on cities.domain while a representative's coverage is a set of cities, and representations.domain is normally only filled for a global agent. The resolver looked at that column alone, so bookingRepId was always null and the commission guard rejected every booking made through a city site. It now falls back to the active representative covering that city, and stays null when two of them cover it — an ambiguous money assignment has to be resolved in the data, not guessed. Commission waited for confirmation. The money has already arrived when the gateway callback succeeds; confirming the appointment is the doctor's or secretary's job and may happen days later or never. It is now recorded on payment, with the appointment still pending. Recording is idempotent, so the confirmation path stays and creates nothing twice. The dashboard counted every appointment of the representative's doctors, including the ones a secretary typed into the panel. It now counts only bookings that came from the representative's own site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
667 lines
25 KiB
Markdown
667 lines
25 KiB
Markdown
# 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 (0–100) |
|
||
| `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` باید در بازه `0–100` باشد. خطاهای `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 | ✅ | 1–12 |
|
||
|
||
### 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)
|
||
|
||
این 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) همان نماینده محاسبه میشود.
|