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.
This commit is contained in:
hamed
2026-07-29 13:28:59 +03:30
parent 11b4dcdd34
commit 4f4bce9fe2
31 changed files with 1497 additions and 137 deletions
+53 -12
View File
@@ -8,7 +8,7 @@ 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,
version: 1, is_active: true, coverage_percent: 70, franchise_percent: 15,
annual_ceiling_rials: 20_000_000, kind: 'basic', effective_from: 1_700_000_000,
effective_to: null,
category_coverages: { outpatient: 70, inpatient: 30 },
@@ -27,11 +27,11 @@ const categories: ServiceCategoryOption[] = [
];
describe('buildInsurancePayload', () => {
it('converts toman → rials, per-category percents, and Y-m-d → unix', () => {
it('keeps the franchise a percent, converts ceiling toman → rials and Y-m-d → unix', () => {
const payload = buildInsurancePayload({
...EMPTY_FORM, insuranceId: '3', kind: 'supplementary',
categoryPercents: { outpatient: '80', inpatient: '30' },
franchise: '50000', ceiling: '2000000',
franchise: '10', ceiling: '2000000',
effectiveFrom: '2024-01-01', effectiveTo: '2025-01-01',
});
expect(payload.insurance_id).toBe(3);
@@ -41,7 +41,7 @@ describe('buildInsurancePayload', () => {
{ key: 'outpatient', coverage_percent: 80 },
{ key: 'inpatient', coverage_percent: 30 },
]);
expect(payload.franchise_rials).toBe(500_000); // 50000 toman × 10
expect(payload.franchise_percent).toBe(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!);
@@ -58,10 +58,10 @@ describe('buildInsurancePayload', () => {
it('forces franchise to zero on a basic contract', () => {
const payload = buildInsurancePayload({
...EMPTY_FORM, insuranceId: '3', kind: 'basic', franchise: '50000',
...EMPTY_FORM, insuranceId: '3', kind: 'basic', franchise: '10',
categoryPercents: { outpatient: '70' },
});
expect(payload.franchise_rials).toBe(0);
expect(payload.franchise_percent).toBe(0);
});
it('omits category_coverages when the user may not override them', () => {
@@ -75,9 +75,9 @@ describe('buildInsurancePayload', () => {
});
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');
it('maps the franchise percent, contract kind, and effective category percents', () => {
const form = contractToForm(mkContract({ franchise_percent: 30, kind: 'supplementary' }));
expect(form.franchise).toBe('30');
expect(form.kind).toBe('supplementary');
expect(form.categoryPercents).toEqual({ outpatient: '70', inpatient: '30' });
});
@@ -95,7 +95,7 @@ describe('InsuranceModal', () => {
expect(screen.getByText('پایه')).toBeInTheDocument();
expect(screen.getByText('درصد پوشش — خدمات سرپایی')).toBeInTheDocument();
expect(screen.getByText('درصد پوشش — خدمات بستری')).toBeInTheDocument();
expect(screen.queryByText('فرانشیز (تومان)')).not.toBeInTheDocument();
expect(screen.queryByText('فرانشیز (درصد)')).not.toBeInTheDocument();
expect(screen.getByText('سقف تعهد (تومان)')).toBeInTheDocument();
expect(screen.getByText('ثبت بیمه')).toBeInTheDocument();
});
@@ -105,7 +105,48 @@ describe('InsuranceModal', () => {
<InsuranceModal open editContract={null} options={options} categories={categories} kind="supplementary" onClose={() => {}} onSubmit={() => {}} />,
);
expect(screen.getByText('تکمیلی')).toBeInTheDocument();
expect(screen.getByText('فرانشیز (تومان)')).toBeInTheDocument();
expect(screen.getByText('فرانشیز (درصد)')).toBeInTheDocument();
});
it('blocks submit until every rendered category has a valid percent', () => {
const onSubmit = vi.fn();
renderWithProviders(
<InsuranceModal
open
editContract={mkContract({ category_coverages: { outpatient: 70 } })}
options={options}
categories={categories}
kind="basic"
onClose={() => {}}
onSubmit={onSubmit}
/>,
);
// بستری بدون درصد مانده: ثبت باید بسته باشد و پیام الزامی دیده شود.
expect(screen.getByText('ثبت بیمه')).toBeDisabled();
expect(screen.getByText('درصد پوشش الزامی است (۱ تا ۱۰۰)')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('درصد پوشش خدمات بستری'), { target: { value: '30' } });
expect(screen.getByText('ثبت بیمه')).not.toBeDisabled();
fireEvent.click(screen.getByText('ثبت بیمه'));
expect(onSubmit).toHaveBeenCalled();
});
it('renders only the service kinds it is given', () => {
renderWithProviders(
<InsuranceModal
open
editContract={null}
options={options}
categories={[{ key: 'outpatient', label: 'خدمات سرپایی' }]}
kind="basic"
onClose={() => {}}
onSubmit={() => {}}
/>,
);
expect(screen.getByText('درصد پوشش — خدمات سرپایی')).toBeInTheDocument();
expect(screen.queryByText('درصد پوشش — خدمات بستری')).not.toBeInTheDocument();
});
it('prefills the percents of an edited contract and marks admin defaults', () => {
@@ -132,7 +173,7 @@ describe('InsuranceModal', () => {
);
fireEvent.click(screen.getByText('ثبت بیمه'));
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
insurance_id: 3, coverage_percent: 70, franchise_rials: 0, kind: 'basic',
insurance_id: 3, coverage_percent: 70, franchise_percent: 0, kind: 'basic',
category_coverages: [
{ key: 'outpatient', coverage_percent: 70 },
{ key: 'inpatient', coverage_percent: 30 },