feat: implement tax calculations for subscription and SMS wallet payments

- Updated SubscriptionPeriod interface to include tax-related fields: tax_percent, tax_rials, and payable_rials.
- Modified payment API documentation to reflect changes in tax handling for subscriptions and SMS wallet charges.
- Adjusted PaymentController to calculate payment amounts based on subscription period details instead of client input.
- Enhanced PaymentManager to handle net amounts for SMS wallet charges, ensuring tax is not credited to the wallet.
- Created PaymentTaxCalculator and SubscriptionTaxCalculator services to manage tax calculations consistently across payment types.
- Added tests for tax calculations in both subscription and SMS wallet contexts, ensuring correct behavior with and without tax enabled.
- Updated frontend components to display tax information appropriately during payment processes.
This commit is contained in:
hamed
2026-08-09 16:51:22 +03:30
parent 2471c90cbb
commit 7716b40f6a
18 changed files with 762 additions and 45 deletions
+10 -3
View File
@@ -106,7 +106,8 @@
"appointment_fee_rials": 150000,
"gateways": [
{ "name": "mellat", "label": "بانک ملت" }
]
],
"tax_percent": 10
}
}
```
@@ -116,6 +117,7 @@
| `test_mode` | boolean | `true` = درگاه آزمایشی فعال است — backend از MockGateway استفاده می‌کند و پول واقعی کسر نمی‌شود |
| `appointment_fee_rials` | integer | مبلغ هر نوبت به ریال (از تنظیمات سایت، کلید `appointment_fee_rials`). frontend برای نمایش «مبلغ قابل پرداخت» از این می‌خواند؛ مبلغِ واقعیِ تراکنش هم در backend از همین کلید خوانده می‌شود (نه از client) |
| `gateways` | array | فقط درگاه‌های **فعال** (اعتبارنامه‌شان در تنظیمات سایت یا env ست شده). هر عضو: `{ name, label }`. frontend فقط همین‌ها را برای انتخاب نمایش می‌دهد. در `test_mode` تنها `[{ "name": "mellat", "label": "بانک ملت (آزمایشی)" }]` برمی‌گردد. اگر هیچ درگاهی فعال نباشد آرایه خالی است و frontend باید پرداخت را غیرفعال کند. |
| `tax_percent` | number | نرخ مالیات بر ارزش افزوده برای **اشتراک** و **شارژ کیف پول پیامک**. صفر یعنی مالیات خاموش است (`tax_enabled=0`). frontend با این عدد جمع کل را پیش از ارسال درخواست نشان می‌دهد؛ مبلغ نهایی همیشه در backend دوباره حساب می‌شود. نوبت این نرخ را به این شکل به‌کار نمی‌برد — آنجا مبلغ شامل مالیات است. |
فعال‌بودن هر درگاه با `PaymentGatewayInterface::isConfigured()` **و** کلید فعال‌سازی در تنظیمات سایت تعیین می‌شود: `mellat` نیازمند `mellat_terminal_id` + `mellat_username` + `mellat_password`؛ `sep` نیازمند `sep_terminal_id`. علاوه بر این، اگر ادمین درگاه را در تنظیمات غیرفعال کند (`mellat_enabled` / `sep_enabled` = `"0"`)، آن درگاه از این لیست حذف می‌شود و در `initiate` نیز رد می‌شود (خطای ۴۲۲: «درگاه پرداخت نامعتبر یا غیرفعال است»). کلید تنظیم‌نشده = فعال (پیش‌فرض).
@@ -341,7 +343,7 @@ Initiate a subscription / wallet top-up payment (not tied to a specific appointm
```json
{
"gateway": "mellat",
"amount_rials": 1000000,
"period_uuid": "uuid-of-subscription-period",
"frontend_address": "https://myapp.com/wallet/result"
}
```
@@ -349,9 +351,14 @@ Initiate a subscription / wallet top-up payment (not tied to a specific appointm
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `gateway` | string | ✅ | `"mellat"` or `"sep"` |
| `amount_rials` | integer | ✅ | Amount in Rials (min: 10,000) |
| `period_uuid` | string | ✅ | دورهٔ اشتراک؛ مبلغ از آن محاسبه می‌شود |
| `frontend_address` | string | ❌ | Redirect URL after payment |
> `amount_rials` دیگر خوانده نمی‌شود. مبلغ = `price_rials + tax_rials` همان دوره، محاسبه‌شده در
> `SubscriptionTaxCalculator`. قیمت دوره خالص است و مالیات رویش اضافه می‌شود — جزئیات در
> [subscription.md](subscription.md#مالیات-دورهها). پاسخ هم هر چهار عدد را برمی‌گرداند:
> `price_rials`، `tax_percent`، `tax_rials`، `payable_rials`.
### Response `200`
```json
{