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:
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { screen, fireEvent } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../test/utils';
|
||||
import { formatRial, tomanToRial } from '../lib/utils';
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||
vi.mock('../lib/api', () => ({
|
||||
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
||||
ApiError: class extends Error {},
|
||||
}));
|
||||
|
||||
import { api } from '../lib/api';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import SmsWalletPage from './SmsWalletPage';
|
||||
|
||||
const get = api.get as ReturnType<typeof vi.fn>;
|
||||
|
||||
/**
|
||||
* مبلغِ واردشده اعتبارِ کیف پول است — خالص. مالیات رویش اضافه میشود، پس عددِ
|
||||
* دکمهٔ پرداخت باید بزرگتر از مبلغ واردشده باشد وگرنه کاربر سرِ درگاه غافلگیر
|
||||
* میشود.
|
||||
*/
|
||||
function mockApi(taxPercent: number) {
|
||||
get.mockImplementation((url: string) => {
|
||||
if (url.includes('/sms/wallet/balance')) {
|
||||
return Promise.resolve({ success: true, data: { balance_rials: 0, sms_price_rials: 5000 } });
|
||||
}
|
||||
if (url.includes('/sms/wallet/logs')) {
|
||||
return Promise.resolve({ success: true, data: [], meta: { totalRecords: 0, totalPages: 0, currentPage: 1 } });
|
||||
}
|
||||
if (url.includes('/sms/settings')) return Promise.resolve({ success: true, data: null });
|
||||
// FeatureGate صفحه را پشت قابلیت پنل پیامک نگه میدارد.
|
||||
if (url.includes('/subscription/my')) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
data: { subscription: null, used_trial: false, effective_plan: { features: { sms_panel: true } } },
|
||||
});
|
||||
}
|
||||
if (url.includes('/payment/config')) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
data: { test_mode: false, appointment_fee_rials: 0, tax_percent: taxPercent, gateways: [{ name: 'mellat', label: 'بانک ملت' }] },
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ success: true, data: null });
|
||||
});
|
||||
}
|
||||
|
||||
async function openChargeModalWith(amountToman: string) {
|
||||
renderWithProviders(<SmsWalletPage />, { route: '/admin/sms-wallet' });
|
||||
fireEvent.click(await screen.findByText('شارژ کیف پول'));
|
||||
|
||||
const input = await screen.findByLabelText('مبلغ شارژ (تومان)');
|
||||
fireEvent.change(input, { target: { value: amountToman } });
|
||||
}
|
||||
|
||||
describe('SmsWalletPage — tax', () => {
|
||||
beforeEach(() => {
|
||||
get.mockReset();
|
||||
// useSubscription فقط برای این نقشها کوئری میزند؛ بدونش FeatureGate صفحه را میبندد.
|
||||
useAuthStore.setState({ primaryRole: 'doctor', context: null } as any);
|
||||
});
|
||||
|
||||
it('adds tax on top of the credit and shows the split', async () => {
|
||||
mockApi(10);
|
||||
await openChargeModalWith('100000');
|
||||
|
||||
const net = tomanToRial(100000);
|
||||
expect(await screen.findByText('اعتباری که به کیف پول اضافه میشود')).toBeInTheDocument();
|
||||
expect(screen.getByText('مالیات بر ارزش افزوده ۱۰٪')).toBeInTheDocument();
|
||||
expect(screen.getByText(formatRial(net * 0.1))).toBeInTheDocument();
|
||||
expect(screen.getAllByText(formatRial(net * 1.1)).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('shows no tax row and charges the plain amount when tax is off', async () => {
|
||||
mockApi(0);
|
||||
await openChargeModalWith('100000');
|
||||
|
||||
expect(await screen.findByText(/پرداخت .* از طریق بانک ملت/)).toBeInTheDocument();
|
||||
expect(screen.queryByText('اعتباری که به کیف پول اضافه میشود')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(new RegExp(formatRial(tomanToRial(100000))))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user