Base insurance is a percentage-only rule: patient share is now total minus the base share, and the contract franchise no longer inflates it (franchise stays meaningful for supplementary contracts only). Coverage percentages are managed centrally by admin per service category (outpatient/inpatient, extensible via the ServiceCategory enum). A tenant contract may override a category, otherwise it follows the admin default live — changing the central value immediately applies to every contract that did not override it. - add ServiceCategory enum + GET /api/v1/service-categories as the single source of the category list for every client - add insurance_coverage_defaults (+ GET/PUT admin coverage-defaults endpoints) and expose coverage_defaults on the insurance list and insurance-pricing - add tenant_insurance_category_coverage; tenant-insurances accepts optional category_coverages (needs insurances.update) and returns the effective percentages with their source - add service_items.service_category; visits always resolve as outpatient - drop the reverse-engineered percent from patient_share_rials in MyPatientsPage and align the client-side BillingCalculator mirror in CreateStep Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
143 lines
6.8 KiB
TypeScript
143 lines
6.8 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
||
import { screen, fireEvent } from '@testing-library/react';
|
||
import { renderWithProviders } from '../test/utils';
|
||
import InsuranceModal, {
|
||
buildInsurancePayload, contractToForm, EMPTY_FORM, type Contract, type InsuranceOption,
|
||
} from './InsuranceModal';
|
||
import type { ServiceCategoryOption } from '../hooks/useServiceCategories';
|
||
|
||
const mkContract = (over: Partial<Contract> = {}): Contract => ({
|
||
uuid: 'c-1', insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic',
|
||
version: 1, is_active: true, coverage_percent: 70, franchise_rials: 500_000,
|
||
annual_ceiling_rials: 20_000_000, kind: 'basic', effective_from: 1_700_000_000,
|
||
effective_to: null,
|
||
category_coverages: { outpatient: 70, inpatient: 30 },
|
||
category_coverage_source: { outpatient: 'override', inpatient: 'admin_default' },
|
||
...over,
|
||
});
|
||
|
||
const options: InsuranceOption[] = [
|
||
{ insurance_id: 3, insurance_name: 'بیمه ایران', type: 'basic', coverage_defaults: { outpatient: 70, inpatient: 30 } },
|
||
{ insurance_id: 5, insurance_name: 'بیمه آسیا', type: 'supplementary', coverage_defaults: { outpatient: 40, inpatient: 20 } },
|
||
];
|
||
|
||
const categories: ServiceCategoryOption[] = [
|
||
{ key: 'outpatient', label: 'خدمات سرپایی' },
|
||
{ key: 'inpatient', label: 'خدمات بستری' },
|
||
];
|
||
|
||
describe('buildInsurancePayload', () => {
|
||
it('converts toman → rials, per-category percents, and Y-m-d → unix', () => {
|
||
const payload = buildInsurancePayload({
|
||
...EMPTY_FORM, insuranceId: '3', kind: 'supplementary',
|
||
categoryPercents: { outpatient: '80', inpatient: '30' },
|
||
franchise: '50000', ceiling: '2000000',
|
||
effectiveFrom: '2024-01-01', effectiveTo: '2025-01-01',
|
||
});
|
||
expect(payload.insurance_id).toBe(3);
|
||
expect(payload.kind).toBe('supplementary');
|
||
expect(payload.coverage_percent).toBe(80); // ستون قدیمی = درصد سرپایی
|
||
expect(payload.category_coverages).toEqual([
|
||
{ key: 'outpatient', coverage_percent: 80 },
|
||
{ key: 'inpatient', coverage_percent: 30 },
|
||
]);
|
||
expect(payload.franchise_rials).toBe(500_000); // 50000 toman × 10
|
||
expect(payload.annual_ceiling_rials).toBe(20_000_000);
|
||
expect(typeof payload.effective_from).toBe('number');
|
||
expect(payload.effective_to).toBeGreaterThan(payload.effective_from!);
|
||
});
|
||
|
||
it('empty ceiling → null (بینهایت), empty dates → null', () => {
|
||
const payload = buildInsurancePayload({
|
||
...EMPTY_FORM, insuranceId: '3', categoryPercents: { outpatient: '50' },
|
||
});
|
||
expect(payload.annual_ceiling_rials).toBeNull();
|
||
expect(payload.effective_from).toBeNull();
|
||
expect(payload.effective_to).toBeNull();
|
||
});
|
||
|
||
it('forces franchise to zero on a basic contract', () => {
|
||
const payload = buildInsurancePayload({
|
||
...EMPTY_FORM, insuranceId: '3', kind: 'basic', franchise: '50000',
|
||
categoryPercents: { outpatient: '70' },
|
||
});
|
||
expect(payload.franchise_rials).toBe(0);
|
||
});
|
||
|
||
it('omits category_coverages when the user may not override them', () => {
|
||
const payload = buildInsurancePayload(
|
||
{ ...EMPTY_FORM, insuranceId: '3', categoryPercents: { outpatient: '70' } },
|
||
null,
|
||
false,
|
||
);
|
||
expect(payload).not.toHaveProperty('category_coverages');
|
||
});
|
||
});
|
||
|
||
describe('contractToForm', () => {
|
||
it('maps rials → toman, contract kind, and effective category percents', () => {
|
||
const form = contractToForm(mkContract({ franchise_rials: 300_000, kind: 'supplementary' }));
|
||
expect(form.franchise).toBe('30000');
|
||
expect(form.kind).toBe('supplementary');
|
||
expect(form.categoryPercents).toEqual({ outpatient: '70', inpatient: '30' });
|
||
});
|
||
});
|
||
|
||
describe('InsuranceModal', () => {
|
||
it('renders one percent input per category and hides franchise on basic', () => {
|
||
renderWithProviders(
|
||
<InsuranceModal open editContract={null} options={options} categories={categories} kind="basic" onClose={() => {}} onSubmit={() => {}} />,
|
||
);
|
||
expect(screen.getByText('افزودن بیمه')).toBeInTheDocument();
|
||
expect(screen.getByText('نام بیمه')).toBeInTheDocument();
|
||
// Manual "نوع بیمه" select is gone; kind is shown as a read-only chip from the tab.
|
||
expect(screen.queryByText('نوع بیمه')).not.toBeInTheDocument();
|
||
expect(screen.getByText('پایه')).toBeInTheDocument();
|
||
expect(screen.getByText('درصد پوشش — خدمات سرپایی')).toBeInTheDocument();
|
||
expect(screen.getByText('درصد پوشش — خدمات بستری')).toBeInTheDocument();
|
||
expect(screen.queryByText('فرانشیز (تومان)')).not.toBeInTheDocument();
|
||
expect(screen.getByText('سقف تعهد (تومان)')).toBeInTheDocument();
|
||
expect(screen.getByText('ثبت بیمه')).toBeInTheDocument();
|
||
});
|
||
|
||
it('shows the franchise field for a supplementary contract', () => {
|
||
renderWithProviders(
|
||
<InsuranceModal open editContract={null} options={options} categories={categories} kind="supplementary" onClose={() => {}} onSubmit={() => {}} />,
|
||
);
|
||
expect(screen.getByText('تکمیلی')).toBeInTheDocument();
|
||
expect(screen.getByText('فرانشیز (تومان)')).toBeInTheDocument();
|
||
});
|
||
|
||
it('prefills the percents of an edited contract and marks admin defaults', () => {
|
||
renderWithProviders(
|
||
<InsuranceModal open editContract={mkContract()} options={options} categories={categories} kind="basic" onClose={() => {}} onSubmit={() => {}} />,
|
||
);
|
||
expect(screen.getByLabelText('درصد پوشش خدمات سرپایی')).toHaveValue('70');
|
||
expect(screen.getByLabelText('درصد پوشش خدمات بستری')).toHaveValue('30');
|
||
expect(screen.getByText('پیشفرض ادمین')).toBeInTheDocument();
|
||
});
|
||
|
||
it('makes the percents read-only without the update permission', () => {
|
||
renderWithProviders(
|
||
<InsuranceModal open editContract={mkContract()} options={options} categories={categories} kind="basic" canUpdate={false} onClose={() => {}} onSubmit={() => {}} />,
|
||
);
|
||
expect(screen.getByLabelText('درصد پوشش خدمات سرپایی')).toHaveAttribute('readonly');
|
||
expect(screen.getAllByText('مقدار پیشفرض تنظیمات مرکزی').length).toBe(2);
|
||
});
|
||
|
||
it('submits the built payload for an edited contract', () => {
|
||
const onSubmit = vi.fn();
|
||
renderWithProviders(
|
||
<InsuranceModal open editContract={mkContract()} options={options} categories={categories} kind="basic" onClose={() => {}} onSubmit={onSubmit} />,
|
||
);
|
||
fireEvent.click(screen.getByText('ثبت بیمه'));
|
||
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
||
insurance_id: 3, coverage_percent: 70, franchise_rials: 0, kind: 'basic',
|
||
category_coverages: [
|
||
{ key: 'outpatient', coverage_percent: 70 },
|
||
{ key: 'inpatient', coverage_percent: 30 },
|
||
],
|
||
}));
|
||
});
|
||
});
|