Files
clinicpro/docs/api/settlement.md
T
hamed 8d2b0d908a feat: Add online share functionality for secretaries
- 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.
2026-07-25 18:34:18 +03:30

344 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```json
{
"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`
```json
{
"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`)
```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`
```json
{
"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`
```json
{
"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`)
```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
```json
{
"note": "حساب بانکی نادرست است"
}
```
| Field | Type | Required |
|-------|------|----------|
| `note` | string | ✅ | Rejection reason (required) |
### 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](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`:**
```json
{ "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:**
```json
{ "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:**
```json
{ "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`) |