- Added a new column `national_code_verified` to the `users` table. - Normalized the `bank_account` field in the `representations` table from a single object to an array of IBANs with a default `verified` status of false. feat(ApiIrService): implement identity verification client for api.ir - Created `ApiIrService` to handle identity verification via api.ir. - Implemented methods for matching national code with mobile and IBAN with national code and birth date. - Added error handling and logging for external API requests.
20 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": [
{ "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
{ "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
{ "iban": "IR000000000000000000000000" }
| Field | Type | Required | Description |
|---|---|---|---|
iban |
string | ✅ | شماره شبا (با/بدون IR و فاصله؛ نرمالسازی میشود) |
Response 200
آبجکت پروفایل نماینده با bank_account بهروزشده.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_IDENTITY_004 |
409 | کد ملی هنوز تأیید نشده |
ERR_IDENTITY_003 |
409 | سقف ۲ شبا پر است |
ERR_VALIDATION_001 |
422 | شبا نامعتبر (field: iban) |
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
{ "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 دیده شود. پس از ثبت موفق، یک پیامک خوشآمد (تگ welcome) بهصورت async به موبایل مالک کلینیک ارسال میشود.
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) همان نماینده محاسبه میشود.