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:
@@ -14,6 +14,15 @@ import {
|
||||
SparkleIcon, PlanCardPerson, CloseCircleFilled, GiftIcon, InfoCircleRedFilled,
|
||||
} from './subscriptionIcons';
|
||||
|
||||
// ── Tax helpers ───────────────────────────────────────────────────────────
|
||||
// `price_rials` خالص است و مالیات رویش مینشیند. فیلدهای مالیاتی را بکاند حساب
|
||||
// میکند؛ fallback فقط برای پاسخِ کششدهٔ نسخهٔ قبلی است.
|
||||
|
||||
export const taxOf = (p: Pick<SubscriptionPeriod, 'tax_rials'>): number => p.tax_rials ?? 0;
|
||||
|
||||
export const payableOf = (p: Pick<SubscriptionPeriod, 'price_rials' | 'payable_rials'>): number =>
|
||||
p.payable_rials ?? p.price_rials;
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Shared feature labels (also consumed by PaymentSuccessPage). */
|
||||
@@ -92,9 +101,10 @@ export default function SubscriptionPage() {
|
||||
});
|
||||
|
||||
const purchaseMutation = useMutation({
|
||||
mutationFn: ({ period_uuid, gateway, amount_rials }: { period_uuid: string; gateway: string; amount_rials: number }) =>
|
||||
// مبلغ فرستاده نمیشود: بکاند خودش قیمت دوره + مالیات را حساب میکند.
|
||||
mutationFn: ({ period_uuid, gateway }: { period_uuid: string; gateway: string }) =>
|
||||
api.post<{ data: { pay_url?: string; redirect_url?: string; payment_url?: string } }>('/api/v1/subscription-payment', {
|
||||
period_uuid, gateway, amount_rials,
|
||||
period_uuid, gateway,
|
||||
// Gateway returns here (with ?payment_uuid&status); the success page reads them.
|
||||
frontend_address: `${window.location.origin}/admin/subscription/success`,
|
||||
}),
|
||||
@@ -191,11 +201,36 @@ export default function SubscriptionPage() {
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 2 }}>مبلغ قابل پرداخت</div>
|
||||
<div style={{ fontWeight: 800, fontSize: 20, color: 'var(--primary)' }}>
|
||||
{formatRial(purchaseTarget.period.price_rials)}
|
||||
{formatRial(payableOf(purchaseTarget.period))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{taxOf(purchaseTarget.period) > 0 && (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 6,
|
||||
background: 'var(--surface-2)', borderRadius: 'var(--r-sm)',
|
||||
padding: '10px 12px', fontSize: 13, color: 'var(--text-2)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>قیمت دوره</span>
|
||||
<span style={{ direction: 'ltr' }}>{formatRial(purchaseTarget.period.price_rials)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>مالیات بر ارزش افزوده {formatNumber(purchaseTarget.period.tax_percent)}٪</span>
|
||||
<span style={{ direction: 'ltr' }}>{formatRial(taxOf(purchaseTarget.period))}</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(payableOf(purchaseTarget.period))}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isTestMode ? (
|
||||
<div style={{
|
||||
background: 'var(--warning-bg)', border: '1px solid var(--warning)',
|
||||
@@ -248,12 +283,12 @@ export default function SubscriptionPage() {
|
||||
style={{ flex: 1, height: 44 }}
|
||||
disabled={purchaseMutation.isPending || (!isTestMode && (gateways.length === 0 || selectedGateway === ''))}
|
||||
onClick={() =>
|
||||
purchaseMutation.mutate({ period_uuid: purchaseTarget.period.uuid, gateway: selectedGateway, amount_rials: purchaseTarget.period.price_rials })
|
||||
purchaseMutation.mutate({ period_uuid: purchaseTarget.period.uuid, gateway: selectedGateway })
|
||||
}
|
||||
>
|
||||
{purchaseMutation.isPending
|
||||
? 'در حال انتقال...'
|
||||
: `پرداخت ${formatRial(purchaseTarget.period.price_rials)}`}
|
||||
: `پرداخت ${formatRial(payableOf(purchaseTarget.period))}`}
|
||||
</button>
|
||||
<button className="btn ghost" style={{ height: 44 }} onClick={() => setPurchaseTarget(null)}>
|
||||
انصراف
|
||||
@@ -478,11 +513,18 @@ function PlanCard({
|
||||
justifyContent: 'flex-end', width: '100%',
|
||||
}}>
|
||||
<span style={{ fontSize: 14, color: 'var(--text)', fontWeight: 600, direction: 'ltr' }}>
|
||||
{formatRial(selectedPeriod.price_rials)}
|
||||
{formatRial(payableOf(selectedPeriod))}
|
||||
</span>
|
||||
<span style={{ fontSize: 14, color: 'var(--text)', fontWeight: 500 }}>: قیمت</span>
|
||||
</div>
|
||||
)}
|
||||
{!isFree && selectedPeriod && taxOf(selectedPeriod) > 0 && (
|
||||
<div style={{
|
||||
fontSize: 11, color: 'var(--text-3)', textAlign: 'left', marginTop: -4,
|
||||
}}>
|
||||
شامل {formatNumber(selectedPeriod.tax_percent)}٪ مالیات بر ارزش افزوده
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user