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
+16 -4
View File
@@ -40,6 +40,7 @@ class SmsWalletController extends BaseController
private readonly \App\Config\Repository\SiteConfigRepository $configRepo,
private readonly \App\Secretary\Security\SecretaryAccessChecker $secretaryAccess,
private readonly \App\Clinic\Security\ClinicDoctorAccessChecker $clinicDoctorAccess,
private readonly \App\Payment\Service\PaymentTaxCalculator $tax,
private readonly string $appBaseUrl,
) {}
@@ -82,12 +83,18 @@ class SmsWalletController extends BaseController
$data = json_decode($request->getContent(), true) ?? [];
$gatewayName = trim($data['gateway'] ?? 'mellat');
$amountRials = (int) ($data['amount_rials'] ?? 0);
// `amount_rials` مبلغی است که به کیف پول می‌نشیند — خالص، بدون مالیات.
$netRials = (int) ($data['amount_rials'] ?? 0);
if ($amountRials <= 0) {
if ($netRials <= 0) {
return $this->error(ErrorCodes::ERR_PAYMENT_002, 'مبلغ شارژ نامعتبر است', 422);
}
// مالیات روی مبلغ شارژ اضافه می‌شود؛ اعتبارِ کیف پول همان مبلغ خالص می‌ماند،
// وگرنه کاربر مالیات را هم به‌صورت اعتبار پیامک پس می‌گرفت.
$breakdown = $this->tax->breakdown($netRials);
$amountRials = $breakdown['payable_rials'];
// فقط اعتبارسنجی درگاه؛ ارتباط با بانک در flow واحد (GET /payment/pay) انجام می‌شود.
if ($this->gateways->resolve($gatewayName) === null) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'درگاه پرداخت نامعتبر است', 422);
@@ -96,14 +103,19 @@ class SmsWalletController extends BaseController
$frontendAddress = trim($data['frontend_address'] ?? '');
$payment = new Payment($user, $amountRials, $gatewayName, Payment::TYPE_SMS_WALLET, $frontendAddress);
$payment->assignTenantPair($entityType, $entityId);
$payment->setMetadata(['entity_type' => $entityType, 'entity_id' => $entityId]);
$payment->setMetadata([
'entity_type' => $entityType,
'entity_id' => $entityId,
// اعتباری که بعد از پرداخت موفق به کیف پول می‌نشیند — بدون مالیات.
'net_rials' => $netRials,
]);
$this->paymentRepo->save($payment);
return $this->success([
'payment_uuid' => $payment->getUuid(),
'pay_url' => $this->appBaseUrl . '/api/v1/payment/pay/' . $payment->getOrderId(),
'order_id' => $payment->getOrderId(),
]);
] + $breakdown);
}
#[Route('/api/v1/sms/wallet/logs', methods: ['GET'])]