Files
clinicpro/assets/admin/components/TenantInsuranceContracts.test.tsx
hamed 4f4bce9fe2 feat(migrations): update franchise to percentage in tenant_insurances and tenant_service_coverage
- Changed franchise_rials to franchise_percent in tenant_insurances and tenant_service_coverage tables.
- Reset old rial values to 0/NULL as they are not convertible to percentage.

feat(command): add SeedInsuranceScenarioCommand for seeding insurance data

- Implemented a command to seed supplementary insurance contracts, patients, and claims for a specified doctor.
- Includes functionality for purging existing scenario data and generating new entries with predefined contracts and patient scenarios.
2026-07-29 13:28:59 +03:30

170 lines
8.0 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { renderWithProviders } from '../test/utils';
vi.mock('../lib/api', () => ({
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
ApiError: class extends Error {},
}));
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
import { api } from '../lib/api';
import TenantInsuranceContracts, { filterInsurances, contractSummary } from './TenantInsuranceContracts';
import type { Contract } from './InsuranceModal';
const get = api.get as ReturnType<typeof vi.fn>;
const patch = api.patch as ReturnType<typeof vi.fn>;
const mk = (over: Partial<Contract>): Contract => ({
uuid: 'u', insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic',
version: 1, is_active: true, coverage_percent: 70, franchise_percent: 0,
annual_ceiling_rials: null, kind: 'basic', effective_from: 0, effective_to: null, ...over,
});
describe('filterInsurances', () => {
const list = [
mk({ uuid: 'a', insurance_id: 3, insurance_name: 'بیمه ایران' }),
mk({ uuid: 'b', insurance_id: 5, insurance_name: 'بیمه آسیا' }),
];
it('filters by name', () => {
expect(filterInsurances(list, 'آسیا')).toHaveLength(1);
expect(filterInsurances(list, 'آسیا')[0].uuid).toBe('b');
});
it('filters by code (insurance_id)', () => {
expect(filterInsurances(list, '5')).toHaveLength(1);
});
it('empty query returns all', () => {
expect(filterInsurances(list, ' ')).toHaveLength(2);
});
});
const categories = [
{ key: 'outpatient', label: 'سرپایی' },
{ key: 'inpatient', label: 'بستری' },
];
describe('contractSummary', () => {
it('breaks the coverage down per service category', () => {
const s = contractSummary(
mk({ category_coverages: { outpatient: 70, inpatient: 30 }, annual_ceiling_rials: 20_000_000 }),
categories,
);
expect(s).toContain('سرپایی ۷۰٪');
expect(s).toContain('بستری ۳۰٪');
expect(s).toContain('سقف پوشش');
});
it('shows the franchise only on a supplementary contract', () => {
const basic = contractSummary(mk({ franchise_percent: 20, insurance_kind: 'basic' }), categories);
expect(basic).not.toContain('فرانشیز');
const supp = contractSummary(mk({ franchise_percent: 20, insurance_kind: 'supplementary' }), categories);
expect(supp).toContain('فرانشیز');
});
it('marks an unlimited ceiling', () => {
const s = contractSummary(mk({ franchise_percent: 0, annual_ceiling_rials: null }), categories);
expect(s).toContain('سقف پوشش نامحدود');
});
it('falls back to the legacy contract percent with no category rows', () => {
const s = contractSummary(mk({ coverage_percent: 90, category_coverages: undefined }), categories);
expect(s).toContain('پوشش ۹۰٪');
});
/** تا برچسب‌های فارسی از سرور نرسیده‌اند، نباید کلید انگلیسی نشان داده شود. */
it('بدون برچسب‌های سرور، کلید انگلیسی نشان نمی‌دهد', () => {
const s = contractSummary(mk({ coverage_percent: 90, category_coverages: { outpatient: 10, inpatient: 30 } }), []);
expect(s).not.toContain('outpatient');
expect(s).not.toContain('inpatient');
expect(s).toContain('پوشش ۹۰٪');
});
});
describe('TenantInsuranceContracts', () => {
beforeEach(() => {
get.mockReset();
patch.mockReset();
patch.mockResolvedValue({ success: true, data: { data: {} } });
get.mockImplementation((path: string) => {
if (path.includes('tenant-insurances')) {
return Promise.resolve({ success: true, data: { data: [
mk({ uuid: 'u1', insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic', is_active: true }),
mk({ uuid: 'u2', insurance_id: 5, insurance_name: 'بیمه آسیا', insurance_kind: 'basic', is_active: false }),
mk({ uuid: 'u3', insurance_id: 7, insurance_name: 'بیمه دانا', insurance_kind: 'supplementary', is_active: true }),
] } });
}
return Promise.resolve({ success: true, data: { insurances: [] } });
});
});
// Desktop table and mobile cards both render in jsdom (CSS `hidden`/`md:` is inert),
// so each row's text appears twice — assertions use *AllBy* accordingly.
it('lists only the active tab (basic) contracts', async () => {
renderWithProviders(<TenantInsuranceContracts />);
expect((await screen.findAllByText('بیمه ایران')).length).toBeGreaterThan(0);
expect(screen.getAllByText('بیمه آسیا').length).toBeGreaterThan(0);
// Supplementary contract is hidden under the other tab.
expect(screen.queryByText('بیمه دانا')).not.toBeInTheDocument();
});
it('switching to the supplementary tab filters by kind', async () => {
renderWithProviders(<TenantInsuranceContracts />);
await screen.findAllByText('بیمه ایران');
fireEvent.click(screen.getByRole('tab', { name: /بیمه تکمیلی/ }));
expect((await screen.findAllByText('بیمه دانا')).length).toBeGreaterThan(0);
expect(screen.queryByText('بیمه ایران')).not.toBeInTheDocument();
});
it('classifies by catalog type, overriding a stale contract kind', async () => {
// آسیا is stored on the contract as basic (legacy manual pick) but the catalog
// marks it supplementary — catalog type wins, so it must leave the basic tab.
get.mockImplementation((path: string) => {
if (path.includes('tenant-insurances')) {
return Promise.resolve({ success: true, data: { data: [
mk({ uuid: 'u1', insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic' }),
mk({ uuid: 'u2', insurance_id: 5, insurance_name: 'بیمه آسیا', insurance_kind: 'basic' }),
] } });
}
return Promise.resolve({ success: true, data: { insurances: [
{ insurance_id: 3, insurance_name: 'بیمه ایران', type: 'basic' },
{ insurance_id: 5, insurance_name: 'بیمه آسیا', type: 'supplementary' },
] } });
});
renderWithProviders(<TenantInsuranceContracts />);
await screen.findByText('بیمه ایران');
expect(screen.queryByText('بیمه آسیا')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: /بیمه تکمیلی/ }));
expect(await screen.findByText('بیمه آسیا')).toBeInTheDocument();
});
it('search box filters rows by name', async () => {
renderWithProviders(<TenantInsuranceContracts />);
await screen.findAllByText('بیمه ایران');
fireEvent.change(screen.getByPlaceholderText('جستجو در بیمه ها...'), { target: { value: 'آسیا' } });
expect(screen.queryByText('بیمه ایران')).not.toBeInTheDocument();
expect(screen.getAllByText('بیمه آسیا').length).toBeGreaterThan(0);
});
it('clicking a row expands its detail, clicking again collapses', async () => {
renderWithProviders(<TenantInsuranceContracts />);
const names = await screen.findAllByText('بیمه ایران');
expect(screen.queryByText('نسخه قرارداد')).not.toBeInTheDocument();
fireEvent.click(names[0]);
expect(screen.getAllByText('نسخه قرارداد').length).toBeGreaterThan(0);
fireEvent.click(names[0]);
expect(screen.queryByText('نسخه قرارداد')).not.toBeInTheDocument();
});
it('clicking the status toggle does not expand the row (stopPropagation)', async () => {
renderWithProviders(<TenantInsuranceContracts />);
await screen.findAllByText('بیمه ایران');
const toggles = screen.getAllByLabelText('غیرفعال کردن');
fireEvent.click(toggles[0]);
expect(screen.queryByText('نسخه قرارداد')).not.toBeInTheDocument();
await waitFor(() =>
expect(patch).toHaveBeenCalledWith('/api/v1/billing/tenant-insurances/u1', { is_active: false }),
);
});
});