feat: Enhance insurance billing system to support supplementary insurance
- Updated CoverageRule and related entities to include franchise_percent instead of franchise_rials. - Modified Appointment entity to carry supplementary insurance ID alongside base insurance. - Implemented SessionBillingService to ensure finalized invoices for insured patient sessions. - Created InvoiceFinalized event to trigger claims creation upon invoice finalization. - Added BackfillMissingClaimsCommand to generate claims for finalized invoices without existing claims. - Developed tests to validate the new functionality for supplementary insurance handling in appointments and claims.
This commit is contained in:
@@ -130,9 +130,16 @@ const referenceAppointment = {
|
||||
visit_price_rials: 5_952_000, service_items: [],
|
||||
};
|
||||
|
||||
const SUPP_CONTRACT = {
|
||||
uuid: 'c2', insurance_id: 9, insurance_name: 'بیمه آسیا', insurance_kind: 'supplementary',
|
||||
is_active: true, coverage_percent: 90, franchise_percent: 10, annual_ceiling_rials: null,
|
||||
category_coverages: { outpatient: 90, inpatient: 60 },
|
||||
};
|
||||
|
||||
function mockInsurance(
|
||||
categories: { key: string; label: string; enabled: boolean }[],
|
||||
freeVisitPriceRials = 0,
|
||||
contracts: unknown[] = [CONTRACT],
|
||||
) {
|
||||
get.mockImplementation((url: string) => {
|
||||
if (url === '/api/v1/insurance-pricing') {
|
||||
@@ -148,7 +155,7 @@ function mockInsurance(
|
||||
});
|
||||
}
|
||||
if (url === '/api/v1/billing/tenant-insurances') {
|
||||
return Promise.resolve({ success: true, data: { data: [CONTRACT] } });
|
||||
return Promise.resolve({ success: true, data: { data: contracts } });
|
||||
}
|
||||
return Promise.resolve({ success: true, data: [] });
|
||||
});
|
||||
@@ -180,7 +187,7 @@ describe('ConfirmAppointmentModal — انتخاب بیمه', () => {
|
||||
renderReference();
|
||||
|
||||
expect(await screen.findByText('نوع خدمت')).toBeInTheDocument();
|
||||
expect(screen.getByText('بیمه')).toBeInTheDocument();
|
||||
expect(screen.getByText('بیمه پایه')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('با فعال بودن فقط یک نوع، انتخاب نوع خدمت پنهان است', async () => {
|
||||
@@ -190,7 +197,7 @@ describe('ConfirmAppointmentModal — انتخاب بیمه', () => {
|
||||
]);
|
||||
renderReference();
|
||||
|
||||
expect(await screen.findByText('بیمه')).toBeInTheDocument();
|
||||
expect(await screen.findByText('بیمه پایه')).toBeInTheDocument();
|
||||
expect(screen.queryByText('نوع خدمت')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -224,6 +231,52 @@ describe('ConfirmAppointmentModal — انتخاب بیمه', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('بدون قرارداد تکمیلی، انتخاب بیمهٔ تکمیلی نمایش داده نمیشود', async () => {
|
||||
mockInsurance(BOTH);
|
||||
renderReference();
|
||||
|
||||
expect(await screen.findByText('بیمه پایه')).toBeInTheDocument();
|
||||
expect(screen.queryByText('بیمه تکمیلی')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('سهم پایه و تکمیلی جدا و زنجیرهای محاسبه میشوند و هر دو ارسال میگردند', async () => {
|
||||
mockInsurance(BOTH, 0, [CONTRACT, SUPP_CONTRACT]);
|
||||
renderReference();
|
||||
|
||||
await screen.findByText('نوع خدمت');
|
||||
await pick('انتخاب نوع خدمت', 'خدمات سرپایی');
|
||||
await pick('بدون بیمه', 'بیمه ایران');
|
||||
await pick('بدون بیمه تکمیلی', 'بیمه آسیا');
|
||||
|
||||
// پایه ۷۰٪ از ۵٬۹۵۲٬۰۰۰ → ۴٬۱۶۶٬۴۰۰؛ باقیمانده ۱٬۷۸۵٬۶۰۰؛
|
||||
// تکمیلی ۹۰٪ منهای فرانشیز ۱۰٪ → ۱٬۴۲۸٬۴۸۰؛ سهم بیمار ۳۵۷٬۱۲۰ ریال = ۳۵٬۷۱۲ تومان.
|
||||
expect(await screen.findByText(/سهم بیمه پایه/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/سهم بیمه تکمیلی/)).toBeInTheDocument();
|
||||
await waitFor(() => expect(amountInputs()[0]).toHaveValue('۳۵٬۷۱۲'));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'تأیید و قطعی کردن' }));
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/appointment/a1/confirm', {
|
||||
version: 1,
|
||||
insurance_service_category: 'outpatient',
|
||||
insurance_base_id: 3,
|
||||
insurance_supplementary_id: 9,
|
||||
payments: [{ method: 'cash', amount_rials: 357_120 }],
|
||||
}));
|
||||
});
|
||||
|
||||
it('بیمهٔ تکمیلیِ تنها هم روی کل مبلغ اعمال میشود', async () => {
|
||||
mockInsurance(BOTH, 0, [CONTRACT, SUPP_CONTRACT]);
|
||||
renderReference();
|
||||
|
||||
await screen.findByText('نوع خدمت');
|
||||
await pick('انتخاب نوع خدمت', 'خدمات سرپایی');
|
||||
await pick('بدون بیمه تکمیلی', 'بیمه آسیا');
|
||||
|
||||
// بدون پایه: ۹۰٪ منهای فرانشیز ۱۰٪ از ۵٬۹۵۲٬۰۰۰ → ۴٬۷۶۱٬۶۰۰؛ بیمار ۱٬۱۹۰٬۴۰۰.
|
||||
await waitFor(() => expect(amountInputs()[0]).toHaveValue('۱۱۹٬۰۴۰'));
|
||||
expect(screen.queryByText(/سهم بیمه پایه/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('نوبتِ بدون هزینهٔ ویزیت، «قیمت ویزیت آزاد» تنظیمات را نشان میدهد (نه صفر)', async () => {
|
||||
mockInsurance(BOTH, 5_952_000);
|
||||
renderWithProviders(
|
||||
|
||||
Reference in New Issue
Block a user