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
+34 -3
View File
@@ -66,7 +66,7 @@ function SmsWalletPageInner() {
queryFn: () => api.get('/api/v1/sms/settings'),
});
const { isTestMode } = usePaymentConfig();
const { isTestMode, taxPercent } = usePaymentConfig();
const balance = balanceData?.data;
const logs = logsData?.data ?? EMPTY_LOGS;
@@ -76,6 +76,12 @@ function SmsWalletPageInner() {
const chargeForm = useForm<ChargeForm>({ resolver: zodResolver(chargeSchema) });
const watchAmount = chargeForm.watch('amount_rials');
// مبلغِ واردشده خالص است — همان چیزی که به کیف پول می‌نشیند. مالیات رویش اضافه
// می‌شود، دقیقاً با همان فرمولِ بک‌اند، تا عددِ مودال با صفحهٔ بانک یکی باشد.
const chargeNet = tomanToRial(Number(watchAmount) || 0);
const chargeTax = Math.round(chargeNet * taxPercent / 100);
const chargePayable = chargeNet + chargeTax;
const chargeMutation = useMutation({
mutationFn: ({ amount_rials }: ChargeForm) =>
api.post<{ data: { payment_url: string } }>('/api/v1/sms/wallet/charge', {
@@ -548,6 +554,31 @@ function SmsWalletPageInner() {
)}
</div>
{watchAmount && Number(watchAmount) >= 1000 && taxPercent > 0 && (
<div style={{
display: 'flex', flexDirection: 'column', gap: 6,
background: 'var(--surface-2)', borderRadius: 8,
padding: '10px 14px', fontSize: 13, color: 'var(--text-2)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>اعتباری که به کیف پول اضافه میشود</span>
<span style={{ direction: 'ltr' }}>{formatRial(chargeNet)}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>مالیات بر ارزش افزوده {formatNumber(taxPercent)}٪</span>
<span style={{ direction: 'ltr' }}>{formatRial(chargeTax)}</span>
</div>
<div style={{
display: 'flex', justifyContent: 'space-between',
borderTop: '1px solid var(--border)', paddingTop: 6,
color: 'var(--text)', fontWeight: 700,
}}>
<span>مبلغ قابل پرداخت</span>
<span style={{ direction: 'ltr' }}>{formatRial(chargePayable)}</span>
</div>
</div>
)}
{watchAmount && Number(watchAmount) >= 1000 && (
<div style={{
background: isTestMode ? 'var(--warning-bg)' : 'var(--primary-soft)',
@@ -556,8 +587,8 @@ function SmsWalletPageInner() {
fontSize: 13, color: isTestMode ? 'var(--warning)' : 'var(--primary)', fontWeight: 500,
}}>
{isTestMode
? `پرداخت آزمایشی ${formatRial(tomanToRial(Number(watchAmount)))}`
: `پرداخت ${formatRial(tomanToRial(Number(watchAmount)))} از طریق ${gateway === 'mellat' ? 'بانک ملت' : 'سپ'}`
? `پرداخت آزمایشی ${formatRial(chargePayable)}`
: `پرداخت ${formatRial(chargePayable)} از طریق ${gateway === 'mellat' ? 'بانک ملت' : 'سپ'}`
}
</div>
)}