- Added domain guard in CommissionService to ensure commission is calculated only when the appointment is booked under the same representation as the doctor. - Updated RepresentationController to filter statistics by representation, ensuring accurate data is shown for each representative. - Introduced new endpoints for the representation dashboard to provide summary statistics, doctor performance, and financial reports. - Created new pages for RepresentationFinance and RepresentationSettlement to display financial data and allow for settlement requests. - Added migration to include booking_representation_id in appointments for tracking the representative under which the appointment was booked.
17 KiB
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)
{
"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_id |
integer | ❌ | City ID (FK to categories where bundle=city) |
commission_percent |
float | ❌ | Commission rate (0–100) |
bank_account |
object | ❌ | Bank details for settlements |
Response 201
{
"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
{
"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)
{
"full_name": "علی احمدی جدید",
"city_id": 50,
"commission_percent": 12,
"bank_account": { ... },
"active": true
}
All fields optional.
Response 200
Updated representation object.
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 |
DELETE /api/v1/representation/{uuid}
Delete a representation.
Permission: ROLE_ADMIN
Response 200
{ "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
{
"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
{
"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
}
}
}
پنل نماینده (ROLE_REPRESENTATION)
این endpointها برای کاربرِ دارای نقش ROLE_REPRESENTATION در پنل ادمین (/admin) هستند. مالکیت همیشه از کاربر جاری (#[CurrentUser] + findByUser) تعیین میشود؛ هیچ uuid/id ورودی برای تعیین مالکیت پذیرفته نمیشود.
Permission (همهی این بخش):
ROLE_REPRESENTATION
GET /api/v1/representation/me
پروفایل نمایندهی کاربر جاری.
Response 200
{
"success": true,
"data": {
"data": {
"uuid": "...",
"full_name": "حامد حسینی",
"mobile_number": "09120671756",
"city_id": 132,
"commission_percent": "10.00",
"bank_account": null,
"active": true,
"created_at": 1718000000
}
}
}
double-nested: مقدار با
data.dataاستخراج میشود.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_NOT_FOUND_001 |
404 | کاربر جاری نماینده نیست |
POST /api/v1/representation/doctor
افزودن پزشک توسط نماینده. representation_id پزشک بهصورت خودکار روی نمایندهی کاربر جاری ست میشود.
Request Body
{ "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 | ❌ |
specialties |
integer[] | ❌ |
Response 201
{ "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 دیده شود.
Request Body
{ "owner_mobile": "0935...", "name": "کلینیک ...", "telephone": "...", "address": "..." }
| Field | Type | Required |
|---|---|---|
owner_mobile |
string | ✅ |
name |
string | ✅ |
telephone / address / info |
string | ❌ |
Response 200
{ "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
{
"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
{
"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
{
"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
{ "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
{
"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:
{
"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):
{
"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):
{
"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) همان نماینده محاسبه میشود.