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:
hamed
2026-07-29 19:57:02 +03:30
parent 9b05c6d1ff
commit e6267080b2
25 changed files with 876 additions and 126 deletions
+25 -9
View File
@@ -47,10 +47,18 @@ export function useAppointmentInsurance(enabled: boolean) {
[contractsQuery.data],
);
const activeContracts = useMemo(() => contracts.filter((c) => c.is_active !== false), [contracts]);
const basicContracts = useMemo(
() => contracts.filter((c) => c.is_active !== false && c.insurance_kind !== 'supplementary'),
[contracts],
() => activeContracts.filter((c) => c.insurance_kind !== 'supplementary'),
[activeContracts],
);
const supplementaryContracts = useMemo(
() => activeContracts.filter((c) => c.insurance_kind === 'supplementary'),
[activeContracts],
);
const optionsOf = (list: TenantContract[]) =>
list.map((c) => ({ value: String(c.insurance_id), label: c.insurance_name ?? `#${c.insurance_id}` }));
return {
/**
@@ -70,18 +78,26 @@ export function useAppointmentInsurance(enabled: boolean) {
categoryLabelOf: (key: string | null | undefined) =>
(pricing.service_categories ?? []).find((c) => c.key === key)?.label ?? null,
insuranceOptions: basicContracts.map((c) => ({
value: String(c.insurance_id),
label: c.insurance_name ?? `#${c.insurance_id}`,
})),
insuranceOptions: optionsOf(basicContracts),
/** قراردادهای تکمیلیِ فعال — روی باقیماندهٔ بعد از بیمهٔ پایه اعمال می‌شوند. */
supplementaryOptions: optionsOf(supplementaryContracts),
hasSupplementary: supplementaryContracts.length > 0,
contractOf: (insuranceId: string | number | null | undefined): TenantContract | null =>
basicContracts.find((c) => String(c.insurance_id) === String(insuranceId)) ?? null,
insuranceNameOf: (insuranceId: string | number | null | undefined): string | null =>
contracts.find((c) => String(c.insurance_id) === String(insuranceId))?.insurance_name ?? null,
/** تفکیک سهم بیمه/بیمار برای ردیف‌های داده‌شده تحت قرارداد یک بیمه. */
breakdown: (lines: BillableLine[], insuranceId: string | number | null | undefined): ShareBreakdown =>
breakdownOf(lines, basicContracts.find((c) => String(c.insurance_id) === String(insuranceId)) ?? null),
/** تفکیک زنجیره‌ای سهم‌ها: پایه روی کل، تکمیلی روی باقیمانده. */
breakdown: (
lines: BillableLine[],
insuranceId: string | number | null | undefined,
supplementaryId: string | number | null | undefined = null,
): ShareBreakdown =>
breakdownOf(
lines,
basicContracts.find((c) => String(c.insurance_id) === String(insuranceId)) ?? null,
supplementaryContracts.find((c) => String(c.insurance_id) === String(supplementaryId)) ?? null,
),
isLoading: pricingQuery.isLoading || contractsQuery.isLoading,
};