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.
This commit is contained in:
+81
-1
@@ -940,7 +940,87 @@ List all secretaries.
|
||||
| `search` | string | ❌ | Search by mobile |
|
||||
|
||||
### Response `200`
|
||||
Paginated secretary list with linked doctor info.
|
||||
Paginated secretary list with linked doctor info. هر ردیف علاوه بر مجوزها،
|
||||
`online_share_enabled` و `online_share_percent` (سهم منشی از نوبتهای آنلاین) را هم دارد.
|
||||
|
||||
---
|
||||
|
||||
### GET `/api/v1/admin/secretary/{uuid}`
|
||||
|
||||
جزئیات یک **رابطهٔ** منشی–پزشک/کلینیک (`uuid` = `DoctorSecretary.uuid`، همان uuid لیست بالا) بههمراه تنظیمات سهم و خلاصهٔ درآمد.
|
||||
|
||||
**Permission:** `ROLE_ADMIN`
|
||||
|
||||
#### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": {
|
||||
"uuid": "rel-uuid-…",
|
||||
"secretary_uuid": "user-uuid-…",
|
||||
"user_name": "زهرا رضایی",
|
||||
"mobile_number": "0912…",
|
||||
"doctor_name": "دکتر احمدی",
|
||||
"doctor_uuid": "doc-uuid-…",
|
||||
"owner_type": "doctor",
|
||||
"clinic_uuid": null,
|
||||
"clinic_name": null,
|
||||
"is_active": true,
|
||||
"online_share_enabled": true,
|
||||
"online_share_percent": 5,
|
||||
"permissions": { "…": {} },
|
||||
"created_at": 1700000000,
|
||||
"earnings": {
|
||||
"total_rials": 4500000,
|
||||
"this_month_rials": 1500000,
|
||||
"appointments_count": 9
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`earnings` روی **کاربرِ منشی** جمع میشود (نه فقط این رابطه): مجموع همهٔ سهمهای ثبتشده در `secretary_earnings`. `this_month_rials` = ۳۰ روز گذشته.
|
||||
|
||||
#### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_AUTH_001` | 401 | Missing token |
|
||||
| `ERR_AUTH_006` | 403 | Not admin |
|
||||
| `ERR_NOT_FOUND_001` | 404 | منشی یافت نشد |
|
||||
|
||||
---
|
||||
|
||||
### PUT `/api/v1/admin/secretary/{uuid}/online-share`
|
||||
|
||||
فعال/غیرفعالکردن محاسبهٔ درآمد منشی از نوبتهای آنلاین و تعیین درصد سهم. تنظیم
|
||||
**per-relation** است: یک منشی میتواند برای یک پزشک سهم داشته باشد و برای دیگری نه.
|
||||
|
||||
**Permission:** `ROLE_ADMIN`
|
||||
|
||||
#### Request Body (`application/json`)
|
||||
```json
|
||||
{ "enabled": true, "percent": 5 }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `enabled` | boolean | ✅ | محاسبهٔ سهم برای این رابطه فعال باشد؟ |
|
||||
| `percent` | number | ✅ | درصد سهم از **مبلغ خالص** نوبت (۰ تا ۱۰۰) |
|
||||
|
||||
#### Response `200`
|
||||
همان شکل رابطه (`DoctorSecretary::toArray()`) پس از ذخیره.
|
||||
|
||||
#### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_AUTH_006` | 403 | Not admin |
|
||||
| `ERR_NOT_FOUND_001` | 404 | منشی یافت نشد |
|
||||
| `ERR_VALIDATION_001` | 422 | `percent` خارج از ۰–۱۰۰ (`field: percent`) |
|
||||
| `ERR_VALIDATION_001` | 422 | `enabled=true` با `percent=0` (`field: percent`) |
|
||||
|
||||
> **مبنای محاسبه:** سهم منشی مثل پورسانت نماینده از «خالصِ پس از مالیات» گرفته میشود — ابتدا هزینهٔ پنل پیامک، بعد مالیات، بعد سهمها. تنها نوبتهایی که **آنلاین** پرداخت میشوند سهم میسازند (نوبت ثبتشده در پنل از مسیر تقسیم مالی عبور نمیکند). جزئیات: [settlement.md](settlement.md) و [secretary.md](secretary.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -534,3 +534,176 @@ Get all secretaries across **all doctors** of a clinic.
|
||||
```
|
||||
|
||||
برای افزایش محدودیت، باید پنل را از `POST /api/v1/subscription/trial` (تریال) یا `POST /api/v1/subscription-payment` (پرداخت) ارتقاء داد.
|
||||
|
||||
---
|
||||
|
||||
## سهم منشی از نوبتهای آنلاین (درآمد و تسویه)
|
||||
|
||||
ادمین میتواند برای هر رابطهٔ منشی–پزشک/کلینیک، محاسبهٔ درآمد از نوبتهای آنلاین را
|
||||
فعال کند و درصد بدهد ([admin.md](admin.md#put-apiv1adminsecretaryuuidonline-share)).
|
||||
سهم از **مبلغ خالص** نوبت گرفته میشود: ابتدا هزینهٔ پنل پیامک، بعد مالیات، سپس درصدِ
|
||||
منشی روی «خالصِ پس از مالیات» — همان مبنایی که پورسانت نماینده از آن محاسبه میشود
|
||||
([settlement.md](settlement.md)).
|
||||
|
||||
**«آنلاین» یعنی چه؟** تقسیم مالی تنها پس از پرداخت موفق درگاه (`PaymentManager`) اجرا
|
||||
میشود؛ نوبتی که در پنل ثبت و «قطعی» میشود از این مسیر عبور نمیکند و سهمی نمیسازد.
|
||||
انتساب بر پایهٔ محیط نوبت است: کلینیکِ نوبت، وگرنه خودِ پزشک. اگر چند منشیِ سهمبر وجود
|
||||
داشته باشد، **هر کدام درصد خودش** را میگیرد (تقسیم نمیشود)؛ اگر مجموع درصدها از ۱۰۰
|
||||
بگذرد به نسبت کلیپ میشود و هشدار لاگ میگردد تا سهم سیستم منفی نشود.
|
||||
|
||||
سهم هر منشی در جدول `secretary_earnings` ثبت و بهصورت اعتبار در کیف پول همان کاربر
|
||||
منظور میشود؛ برداشت از طریق `POST /api/v1/settlement` انجام میگیرد.
|
||||
|
||||
---
|
||||
|
||||
### GET `/api/v1/secretary/earnings/summary`
|
||||
|
||||
خلاصهٔ درآمد منشیِ جاری.
|
||||
|
||||
**Permission:** `AUTH` (کاربر منشی)
|
||||
|
||||
#### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": {
|
||||
"enabled": true,
|
||||
"share_percent": 5,
|
||||
"relations": [
|
||||
{ "relation_uuid": "rel-…", "doctor_name": "دکتر احمدی", "clinic_name": null, "share_percent": 5 }
|
||||
],
|
||||
"today_rials": 500000,
|
||||
"this_month_rials": 3000000,
|
||||
"total_rials": 9000000,
|
||||
"appointments_count": 4,
|
||||
"wallet_balance_rials": 9000000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | توضیح |
|
||||
|------|-------|
|
||||
| `enabled` | `false` یعنی هیچ رابطهٔ فعالی با سهمِ روشن ندارد؛ پنل پیام «فعال نیست» نشان میدهد (خطا نمیدهیم) |
|
||||
| `share_percent` | درصد اولین رابطهٔ سهمبر؛ تفکیک کامل در `relations` |
|
||||
| `today_rials` | از نیمهشب امروز |
|
||||
| `this_month_rials` | ۳۰ روز گذشته |
|
||||
| `wallet_balance_rials` | موجودی کیف پول همان کاربر (مبنای تسویه) |
|
||||
|
||||
---
|
||||
|
||||
### GET `/api/v1/secretary/earnings/report`
|
||||
|
||||
گزارش سطر-به-سطر سهم منشی (paginated).
|
||||
|
||||
**Permission:** `AUTH` (کاربر منشی)
|
||||
|
||||
#### Query Parameters
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `page` | integer | ❌ | پیشفرض ۱ |
|
||||
| `limit` | integer | ❌ | پیشفرض ۱۵، حداکثر ۱۰۰ |
|
||||
| `from` | integer | ❌ | Unix — از تاریخ |
|
||||
| `to` | integer | ❌ | Unix — تا تاریخ |
|
||||
|
||||
#### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "earning-uuid-…",
|
||||
"appointment_uuid": "appt-uuid-…",
|
||||
"doctor_name": "دکتر احمدی",
|
||||
"gross_rials": 10000000,
|
||||
"sms_fee_rials": 1000000,
|
||||
"tax_rials": 818182,
|
||||
"net_after_tax_rials": 8181818,
|
||||
"share_percent": 5,
|
||||
"share_rials": 409091,
|
||||
"created_at": 1700000000
|
||||
}
|
||||
],
|
||||
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
منشیِ بدون سهم، پاسخ `200` با آرایهٔ خالی میگیرد (نه `403`).
|
||||
|
||||
---
|
||||
|
||||
### GET `/api/v1/secretary/me`
|
||||
|
||||
پروفایل منشیِ جاری: رابطهها با تنظیمات سهم + شمارههای شبا.
|
||||
|
||||
**Permission:** `AUTH` (کاربر منشی)
|
||||
|
||||
#### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": {
|
||||
"full_name": "زهرا رضایی",
|
||||
"mobile": "0912…",
|
||||
"bank_account": [
|
||||
{ "id": "iban-uuid-…", "iban": "IR…", "bank_name": "ملی", "owner_name": null, "verified": false, "created_at": 1700000000 }
|
||||
],
|
||||
"relations": [
|
||||
{ "relation_uuid": "rel-…", "doctor_name": "دکتر احمدی", "clinic_name": null, "online_share_enabled": true, "online_share_percent": 5 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST `/api/v1/secretary/iban`
|
||||
|
||||
افزودن شماره شبا (حداکثر ۲) به پروفایل کاربرِ منشی — مثل پنل نماینده.
|
||||
|
||||
**Permission:** `AUTH` (کاربر منشی)
|
||||
|
||||
#### Request Body (`application/json`)
|
||||
```json
|
||||
{ "iban": "IR123456789012345678901234", "bank_name": "ملی", "owner_name": "زهرا رضایی" }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `iban` | string | ✅ | الگوی `IR` + ۲۴ رقم (فاصلهها حذف میشود) |
|
||||
| `bank_name` | string | ❌ | نام بانک |
|
||||
| `owner_name` | string | ❌ | نام صاحب حساب |
|
||||
|
||||
#### Response `201`
|
||||
```json
|
||||
{ "success": true, "data": { "data": { "bank_account": [ { "id": "…", "iban": "IR…", "verified": false } ] } } }
|
||||
```
|
||||
|
||||
`verified` همیشه `false` ثبت میشود؛ **تأیید فقط از سمت ادمین** انجام میگیرد و تسویه تنها با شبای تأییدشده مجاز است.
|
||||
|
||||
#### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_VALIDATION_001` | 422 | شبا نامعتبر (`field: iban`) |
|
||||
| `ERR_VALIDATION_001` | 422 | بیش از دو شبا (`field: iban`) |
|
||||
|
||||
---
|
||||
|
||||
### DELETE `/api/v1/secretary/iban/{id}`
|
||||
|
||||
حذف یکی از شباهای منشیِ جاری.
|
||||
|
||||
**Permission:** `AUTH` (کاربر منشی)
|
||||
|
||||
#### Response `200`
|
||||
`{ success, data: { data: { bank_account: [...] } } }`
|
||||
|
||||
#### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_NOT_FOUND_001` | 404 | پروفایل/شبا یافت نشد |
|
||||
|
||||
> کیف پول و تسویه اندپوینت اختصاصی ندارند: `GET /api/v1/wallet/balance`، `GET /api/v1/wallet/transactions` و `POST /api/v1/settlement` کاربر-محورند ([settlement.md](settlement.md)).
|
||||
|
||||
+22
-2
@@ -107,7 +107,12 @@ Request a settlement (withdrawal from wallet to bank account).
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `amount_rials` | integer | ✅ | Amount to withdraw (must be ≤ wallet balance) |
|
||||
| `iban_id` | string | ✅ | شناسهی یکی از شباهای **تأییدشدهی** نماینده (از `GET /api/v1/representation/me` → `bank_account[].id`) |
|
||||
| `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) میشود.
|
||||
|
||||
@@ -263,7 +268,22 @@ Updated settlement object with `status: "rejected"`.
|
||||
|
||||
## FinancialBreakdown (لاگ مالی)
|
||||
|
||||
علاوه بر تسویهحساب دستی، کیفپول نماینده بهصورت خودکار از طریق `CommissionService` هنگام پرداخت موفقِ نوبت/اشتراک شارژ میشود (`WalletTransaction` credit). هر واریز یک ردیف `FinancialBreakdown` ثبت میکند که تفکیک کامل تراکنش (ناخالص، هزینه پیامک، مالیات، خالص، درصد و سهم پورسانت، سهم سیستم) را نگه میدارد. ثبت idempotent است (بر اساس `payment_id`). گزارشها از طریق `GET /api/v1/admin/financial-breakdowns` و `GET /api/v1/admin/financial-summary` در دسترساند — جزئیات در `docs/api/admin.md`.
|
||||
علاوه بر تسویهحساب دستی، کیفپول نماینده بهصورت خودکار از طریق `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` لاگ میشود تا سهم سیستم منفی نشود.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user