- Introduced `online_share_enabled` and `online_share_percent` fields in the `doctor_secretaries` table to manage secretary shares from online appointments. - Added `bank_account` field in the `profiles` table to store user-level IBANs for settlements. - Created `secretary_earnings` table to track earnings per secretary from online appointments, including a foreign key relationship with `financial_breakdowns`. - Implemented `SecretaryEarning` entity and repository for managing secretary earnings. - Developed `SecretaryShareResolver` service to determine which secretaries earn from online payments. - Added `UserIbanResolver` service to handle user IBAN retrieval and management. - Created `HasIbansTrait` for entities to manage IBANs in a JSON format. - Implemented tests for secretary earnings and API endpoints for managing secretary shares and IBANs.
12 KiB
Settlement & Wallet API
Prefix:
/api/v1/wallet,/api/v1/settlement
GET /api/v1/wallet/balance
Get authenticated user's wallet balance.
Permission: AUTH
Response 200
{
"success": true,
"data": {
"balance_rials": 2500000,
"recent_transactions": [
{
"uuid": "...",
"type": "credit",
"amount_rials": 500000,
"balance_after": 2500000,
"description": "دریافت از نوبت شماره ...",
"created_at": 1717000000
}
]
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
GET /api/v1/wallet/transactions
Get wallet transaction history for the authenticated user (paginated, newest first).
Permission: AUTH
Query Parameters
| پارامتر | نوع | توضیح |
|---|---|---|
page |
int | شماره صفحه (پیشفرض ۱) |
limit |
int | تعداد در هر صفحه (پیشفرض ۵۰، حداکثر ۱۰۰) |
Response 200
{
"success": true,
"data": {
"data": [
{
"uuid": "...",
"type": "credit",
"amount_rials": 500000,
"balance_after": 2500000,
"description": "دریافت از نوبت",
"created_at": 1717000000
},
{
"uuid": "...",
"type": "debit",
"amount_rials": 200000,
"balance_after": 2300000,
"description": "تسویهحساب",
"created_at": 1716900000
}
],
"meta": { "totalRecords": 124, "totalPages": 3, "currentPage": 1 }
}
}
Transaction Type Values:
| Value | Description |
|---|---|
credit |
Money added to wallet |
debit |
Money removed from wallet |
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
POST /api/v1/settlement
Request a settlement (withdrawal from wallet to bank account).
Permission: AUTH
Request Body (application/json)
{
"amount_rials": 1000000,
"iban_id": "iban-uuid-..."
}
| Field | Type | Required | Description |
|---|---|---|---|
amount_rials |
integer | ✅ | Amount to withdraw (must be ≤ wallet balance) |
iban_id |
string | ✅ | شناسهی یکی از شباهای تأییدشدهی همان کاربر |
شبا از کجا خوانده میشود؟ تسویه دیگر مخصوص نماینده نیست: UserIbanResolver ابتدا شبای
نماینده (GET /api/v1/representation/me → bank_account[].id) و در نبودش شبای پروفایل
کاربر (GET /api/v1/secretary/me → bank_account[].id) را بررسی میکند. بنابراین هر نقشی
که موجودی کیف پول دارد — از جمله منشی با سهم نوبتهای آنلاین — میتواند برداشت کند.
شبای انتخابی بهصورت snapshot (
iban,bank_name,owner_name) داخل خود رکورد تسویه ذخیره میشود؛ حذف بعدی شبا در پروفایل، این رکورد را تغییر نمیدهد. مبلغ همان لحظهی ثبت از کیفپول کسر (debit) میشود.
Response 201
{
"success": true,
"data": {
"uuid": "settle-uuid-...",
"amount_rials": 1000000,
"status": "pending",
"bank_account": {
"iban": "IR...",
"bank_name": "بانک ملت",
"owner_name": "علی احمدی"
},
"created_at": 1717000000
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_VALIDATION_001 |
422 | مبلغ نامعتبر/بیش از موجودی، یا شبا تأییدنشده/نامعتبر (field: iban_id) |
ERR_VALIDATION_002 |
422 | iban_id ارسال نشده (field: iban_id) |
ERR_CONFLICT_001 |
422 | موجودی کافی نیست |
GET /api/v1/settlement
Get authenticated user's settlement requests (paginated, newest first).
Permission: AUTH
صفحهبندی:
?pageو?limit(پیشفرض ۵۰، حداکثر ۱۰۰). پاسخ علاوه برdata.dataیکdata.meta(totalRecords/totalPages/currentPage) دارد؛ پاکت قبلی دستنخورده است.
Response 200
{
"success": true,
"data": [
{
"uuid": "...",
"amount_rials": 1000000,
"status": "pending",
"note": null,
"created_at": 1717000000,
"processed_at": null
}
]
}
Settlement Status Values:
| Value | Description |
|---|---|
pending |
Awaiting admin review |
approved |
Approved, payment sent |
rejected |
Rejected by admin |
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
GET /api/v1/settlement/{uuid}
Get a single settlement.
Permission: AUTH — must be the owner or ROLE_ADMIN
Response 200
Settlement object with full details including bank_account.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_FORBIDDEN_001 |
403 | Not the owner |
ERR_NOT_FOUND_001 |
404 | Settlement not found |
POST /api/v1/settlement/{uuid}/approve
Approve a settlement request. Marks it as paid and debits the wallet.
Permission: ROLE_ADMIN
Path Parameters
| Param | Type | Description |
|---|---|---|
uuid |
string (UUID) | Settlement UUID |
Request Body (application/json)
{
"note": "پرداخت شد — شناسه پیگیری: 123456"
}
| Field | Type | Required |
|---|---|---|
note |
string | ❌ |
Response 200
Updated settlement object with status: "approved".
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not admin |
ERR_NOT_FOUND_001 |
404 | Settlement not found |
ERR_VALIDATION_001 |
422 | Already processed |
POST /api/v1/settlement/{uuid}/reject
Reject a settlement request. Returns the amount back to wallet.
Permission: ROLE_ADMIN
Request Body
{
"note": "حساب بانکی نادرست است"
}
| Field | Type | Required |
|---|---|---|
note |
string | ✅ |
Response 200
Updated settlement object with status: "rejected".
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not admin |
ERR_NOT_FOUND_001 |
404 | Settlement not found |
ERR_VALIDATION_002 |
422 | Missing note |
FinancialBreakdown (لاگ مالی)
علاوه بر تسویهحساب دستی، کیفپول نماینده بهصورت خودکار از طریق CommissionService هنگام پرداخت موفقِ نوبت/اشتراک شارژ میشود (WalletTransaction credit). هر واریز یک ردیف FinancialBreakdown ثبت میکند که تفکیک کامل تراکنش (ناخالص، هزینه پیامک، مالیات، خالص، درصد و سهم پورسانت، سهم منشی، سهم سیستم) را نگه میدارد. ثبت idempotent است (بر اساس payment_id). گزارشها از طریق GET /api/v1/admin/financial-breakdowns و GET /api/v1/admin/financial-summary در دسترساند — جزئیات در docs/api/admin.md.
ترتیب تقسیم و سهم منشی
۱) هزینهٔ پنل پیامک ← از ناخالص کم میشود
۲) مالیات ← استخراجی از باقیمانده: tax = amount × p/(100+p)
۳) سهمها، همه از «خالصِ پس از مالیات»:
پورسانت نماینده = netAfterTax × commission_percent / 100
سهم هر منشی = netAfterTax × online_share_percent / 100
سهم سیستم = ناخالص − پیامک − مالیات − پورسانت − مجموع سهم منشیها
- سهم منشی مستقل از نماینده است: نوبتِ بدون نمایندهٔ منطبق هم اگر منشیِ سهمبر داشته باشد، تفکیک مالی میسازد.
financial_breakdowns.secretary_share_rialsمجموع سهم منشیهای همان پرداخت است؛ تفکیک هر منشی در جدولsecretary_earnings(باshare_percentوrelation_uuid) ذخیره میشود و گزارش پنل منشی از همان خوانده میشود (secretary.md).- اگر مجموع درصدها (پورسانت + سهم منشیها) از ۱۰۰ بگذرد، به نسبت کلیپ و یک هشدار با
payment_uuidلاگ میشود تا سهم سیستم منفی نشود.
رسید پرداخت و ثبت پرداخت نهایی (ROLE_ADMIN)
منطق کیفپول: مبلغ هنگام ثبت درخواست از کیفپول نماینده کسر (reserve/debit) میشود؛ رد آن را برمیگرداند (credit). تأیید و پرداختِ نهایی کیفپول را دوباره دست نمیزنند (جلوگیری از double-debit).
POST /file/upload/clinic_pro/settlement/receipt
آپلود تصویر رسید پرداخت. بدنه = محتوای خام فایل؛ هدر Content-Disposition: filename="...". Permission: ROLE_ADMIN
Response 200:
{ "success": true, "data": { "uuid": "...", "url": "/uploads/settlements/receipts/2026-06/...", "filename": "...", "filemime": "image/jpeg" } }
POST /api/v1/settlement/{uuid}/receipt
ذخیرهی رسید پرداخت بدون نهاییسازی. فقط روی تسویهی approved؛ وضعیت تغییر نمیکند (همچنان approved). پس از این مرحله دکمهی «تکمیل» در پنل فعال میشود. Permission: ROLE_ADMIN
Request Body:
{ "receipt": "/uploads/settlements/receipts/2026-06/..." }
| Field | Type | Required | Description |
|---|---|---|---|
receipt |
string | ✅ | URL رسیدِ آپلودشده |
Response 200: آبجکت تسویه (با receipt ذخیرهشده، status: "approved").
Errors:
| Code | HTTP | Description |
|---|---|---|
ERR_NOT_FOUND_001 |
404 | درخواست یافت نشد |
ERR_VALIDATION_001 |
422 | تسویه approved نیست |
ERR_VALIDATION_002 |
422 | receipt خالی (field: receipt) |
POST /api/v1/settlement/{uuid}/paid
«تکمیل» — ثبت پرداخت نهایی. فقط روی تسویهی approved. وضعیت → paid و receipt ذخیره میشود. کیفپول تغییر نمیکند (مبلغ هنگام ثبت درخواست کسر شده). پس از تکمیل، آن مبلغ دیگر قابل درخواست تسویه نیست. Permission: ROLE_ADMIN
Request Body:
{ "receipt": "/uploads/settlements/receipts/2026-06/..." }
| Field | Type | Required | Description |
|---|---|---|---|
receipt |
string | ✅ | URL رسیدِ آپلودشده |
Response 200: آبجکت تسویه (شامل receipt و status: "paid").
Errors:
| Code | HTTP | Description |
|---|---|---|
ERR_NOT_FOUND_001 |
404 | درخواست یافت نشد |
ERR_VALIDATION_001 |
422 | تسویه approved نیست |
ERR_VALIDATION_002 |
422 | receipt خالی (field: receipt) |