- Created migration to add representation_cities table and domain, is_global fields to representations. - Implemented SiteContextController to resolve domain to site context (city | representation | unknown). - Developed DomainContext and DomainContextResolver services for domain mapping. - Added tests for DomainContextResolver and commission logic based on domain ownership.
656 lines
23 KiB
Markdown
656 lines
23 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.
|
||
|
||
**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`) متعلق به یک نمایندهی فعال باشد (`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) همان نماینده محاسبه میشود.
|