feat: update session payment logic to ensure accurate payable amounts and reflect consumables in cost breakdown

- Adjusted the calculation of payable amounts in PaymentStep to align with server logic, ensuring overpayments are handled correctly.
- Enhanced DetailsStep to include consumables in the itemized cost breakdown, ensuring consistency with patient share calculations.
- Updated tests for SessionPaymentPage to validate new behavior regarding overpayments and consumable listings.
- Modified PatientController to register SessionPayment correctly when settling sessions via wallet, preventing double charges.
- Refactored WalletService to remove outdated methods and ensure wallet transactions reflect the correct amounts after discounts.
- Improved accessibility in SearchableSelect component by adding aria labels and ensuring proper role attributes for screen readers.
- Updated styles to ensure minimum touch targets meet WCAG guidelines for mobile usability.
This commit is contained in:
hamed
2026-07-19 13:29:46 +03:30
parent 0d9784b03a
commit 773f9d4d16
22 changed files with 251 additions and 70 deletions
+50 -4
View File
@@ -93,10 +93,55 @@ describe('SessionPaymentPage (تکمیل پرداخت مراجعه)', () => {
await waitFor(() => expect(post).toHaveBeenCalledTimes(1));
const [url, body] = post.mock.calls[0];
expect(url).toBe('/api/v1/session/s1/payments');
expect(body).toMatchObject({ method: 'cash', amount_rials: 500000 });
// فیلد به تومان است و کامپوننت tomanToRial می‌کند: ۵۰۰,۰۰۰ تومان = ۵,۰۰۰,۰۰۰ ریال
expect(body).toMatchObject({ method: 'cash', amount_rials: 5_000_000 });
expect(typeof (body as any).paid_at).toBe('number');
});
it('does not inflate «مبلغ نهایی قابل پرداخت» when the patient overpaid', async () => {
// سرور remaining را در صفر کلمپ می‌کند. بازسازی از roo remaining+paid باعث
// می‌شد مبلغ نهایی به اندازه‌ی پرداختی بالا برود و بیش‌پرداخت دیده نشود.
mockGets([session({ remaining_rials: 0, paid_total_rials: 5_000_000 })]);
renderPage();
await screen.findByText('مبلغ نهایی قابل پرداخت:');
// ۲,۵۰۰,۰۰۰ − ۲۰۰,۰۰۰ تخفیف = ۲,۳۰۰,۰۰۰ — نه ۵,۰۰۰,۰۰۰
expect(screen.getAllByText(formatRial(2_300_000)).length).toBeGreaterThan(0);
expect(screen.queryByText(formatRial(5_000_000))).not.toBeInTheDocument();
});
it('lists consumables in the itemised cost breakdown', async () => {
// کالای مصرفی در سهم بیمار حساب می‌شود، پس باید در ریز هزینه‌ها هم بیاید
// وگرنه جمعِ خطوط با «جمع کل» نمی‌خواند.
mockGets([session({
visit_price_rials: 500_000,
consumables: [{ uuid: 'c1', item_name: 'عینک', quantity: 2, line_total_rials: 40_000 }],
payments: [{ uuid: 'p1', method: 'cash', amount_rials: 100_000, paid_at: 1_700_000_000 }],
})]);
renderPage();
fireEvent.click(await screen.findByText('ثبت و ادامه'));
expect(screen.getByText('عینک')).toBeInTheDocument();
expect(screen.getAllByText(formatRial(40_000)).length).toBeGreaterThan(0);
});
it('shows insurance shares so the breakdown reconciles with the patient share', async () => {
mockGets([session({
gross_total_rials: 4_000_000,
base_insurance_rials: 1_000_000,
supplementary_insurance_rials: 500_000,
final_price_rials: 2_500_000,
payments: [{ uuid: 'p1', method: 'cash', amount_rials: 100_000, paid_at: 1_700_000_000 }],
})]);
renderPage();
fireEvent.click(await screen.findByText('ثبت و ادامه'));
expect(screen.getByText('جمع کل خدمات')).toBeInTheDocument();
expect(screen.getByText('سهم بیمه پایه')).toBeInTheDocument();
expect(screen.getByText('سهم بیمه تکمیلی')).toBeInTheDocument();
expect(screen.getByText('سهم بیمار')).toBeInTheDocument();
});
it('prefills the amount field with the remaining balance when a method is opened', async () => {
// مانده ۲,۳۰۰,۰۰۰ ریال → ۲۳۰,۰۰۰ تومان در فیلد مبلغ
mockGets([session({ remaining_rials: 2_300_000 })]);
@@ -121,9 +166,9 @@ describe('SessionPaymentPage (تکمیل پرداخت مراجعه)', () => {
renderPage();
await screen.findByText('هزینه سرویس:');
// حذف تخفیف → discount_type: null
// حذف تخفیف → discount_rule_uuid: null (تخفیف قاعده‌محور را هم جدا می‌کند)
fireEvent.click(screen.getByText('حذف تخفیف'));
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/session/s1', { discount_type: null }));
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/session/s1', { discount_rule_uuid: null }));
});
it('shows registered payments and moves to details step', async () => {
@@ -144,7 +189,8 @@ describe('SessionPaymentPage (تکمیل پرداخت مراجعه)', () => {
// گام جزییات
fireEvent.click(screen.getByText('ثبت و ادامه'));
expect(screen.getByText('صدور فاکتور')).toBeInTheDocument();
expect(screen.getByText('کاشت مو')).toBeInTheDocument();
// نام سرویس هم در سرتیتر و هم در ریز هزینه‌ها می‌آید
expect(screen.getAllByText('کاشت مو').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('دکتر فتحی')).toBeInTheDocument();
expect(screen.getByText('مبلغ باقی مانده:')).toBeInTheDocument();
});