feat: Implement financial engine for commission and tax calculations

- Added new configuration keys for appointment and upgrade commissions, tax settings, and SMS panel fee in SiteConfigController and SiteConfigRepository.
- Introduced CommissionService to handle commission calculations for appointments and subscriptions, including tax deductions and SMS fees.
- Created FinancialBreakdown entity and repository to log financial transactions.
- Updated PaymentController to process commissions upon successful payments for appointments and subscriptions.
- Developed FinancialReportPage in the admin panel to display financial breakdowns and summaries.
- Added database migration for the new financial_breakdowns table.
This commit is contained in:
hamed
2026-06-24 13:06:17 +03:30
parent e0abaf5c0c
commit 148d033114
16 changed files with 1119 additions and 0 deletions
+69
View File
@@ -966,3 +966,72 @@ Reject a pending request. **Permission:** `ROLE_ADMIN`
```json
{ "success": true, "data": { "message": "درخواست رد شد" } }
```
---
## موتور مالی نمایندگی
تنظیمات مالی از طریق `GET`/`PATCH /api/v1/admin/settings` کنترل می‌شوند (کلیدها در whitelist `SiteConfigController::ALLOWED_KEYS`):
| کلید | پیش‌فرض | شرح |
|------|---------|-----|
| `appointment_commission_enabled` | `0` | فعال‌سازی پورسانت نوبت (درصد از `Representation.commission_percent` هر نماینده) |
| `upgrade_commission_enabled` | `0` | فعال‌سازی پورسانت ارتقاء اشتراک |
| `upgrade_commission_percent` | `20` | درصد پورسانت ارتقاء (سراسری) |
| `tax_enabled` | `0` | فعال‌سازی مالیات بر ارزش افزوده |
| `tax_percent` | `10` | درصد مالیات |
| `sms_panel_fee_rials` | `1500000` | هزینه ثابت پنل پیامک به ریال (از نوبت و اشتراک کسر می‌شود) |
**ترتیب محاسبه** (در `CommissionService`): ۱) کسر `sms_panel_fee_rials` ۲) مالیاتِ استخراجی `afterSms × tax/(100+tax)` ۳) پورسانت = `netAfterTax × percent/100`. سهم نماینده به کیف‌پولش (`WalletTransaction` credit) واریز و یک ردیف `FinancialBreakdown` ثبت می‌شود (idempotent بر اساس `payment_id`).
### GET `/api/v1/admin/financial-breakdowns`
لیست تفکیک مالی تراکنش‌ها (paginated). **Permission:** `ROLE_ADMIN`
**Query Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `page` | integer | پیش‌فرض 1 |
| `limit` | integer | پیش‌فرض 15، حداکثر 100 |
| `representation_id` | integer | فیلتر نماینده |
| `source` | string | `appointment` یا `subscription` |
| `from` | integer | Unix timestamp شروع بازه |
| `to` | integer | Unix timestamp پایان بازه |
**Response `200` (paginated):**
```json
{
"success": true,
"data": [
{
"uuid": "…", "order_id": "ORD-…", "source": "appointment",
"gross_rials": 2000000, "sms_fee_rials": 1500000,
"tax_percent": 10, "tax_rials": 45455, "net_after_tax_rials": 454545,
"commission_percent": 20, "representation_share_rials": 90909,
"system_share_rials": 363636,
"representation_id": 3, "representation_name": "نماینده یزد",
"doctor_id": 12, "clinic_id": null, "created_at": "2026-06-24T…"
}
],
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}
```
### GET `/api/v1/admin/financial-summary`
جمع کل مبالغ. **Permission:** `ROLE_ADMIN`
**Response `200`:**
```json
{
"success": true,
"data": {
"total_gross": 2000000,
"total_representation_income": 90909,
"total_tax_collected": 45455,
"total_sms_fee": 1500000,
"total_system_share": 363636
}
}
```
+2
View File
@@ -106,6 +106,8 @@ Initiate payment for an appointment. Returns a redirect URL to the payment gatew
```
> **On successful callback** for an appointment payment, the booking is transitioned `pending → confirmed` (its 15-minute `expires_at` is cleared) and a confirmation SMS is dispatched to the patient's mobile. If the booking already lapsed to `expired` before payment confirmed, it is **not** re-confirmed (the transition is rejected) — handle refund out of band.
>
> **پورسانت نماینده:** اگر پزشک نوبت `representation_id` داشته باشد و `appointment_commission_enabled=1` باشد، پس از confirm شدن `CommissionService` هزینه پنل پیامک و مالیات را کسر و سهم نماینده را به کیف‌پولش واریز می‌کند (ردیف `FinancialBreakdown` ثبت می‌شود). برای پرداخت اشتراک هم اگر `upgrade_commission_enabled=1` و پزشک/کلینیک `representation_id` داشته باشد همین منطق با درصد `upgrade_commission_percent` اعمال می‌شود. کلیدهای تنظیمات و ترتیب محاسبه در `docs/api/admin.md`.
### Errors
| Code | HTTP | Description |
+6
View File
@@ -252,3 +252,9 @@ Updated settlement object with `status: "rejected"`.
| `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`.