Files
clinicpro/assets/admin/hooks/useAppointmentInsurance.ts
T
hamed e6267080b2 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.
2026-07-29 19:57:02 +03:30

105 lines
4.7 KiB
TypeScript

import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import { breakdownOf, type BillableLine, type ShareBreakdown, type TenantContract } from '../lib/insuranceShares';
interface ServiceCategoryRow {
key: string;
label: string;
enabled: boolean;
}
interface PricingPayload {
service_categories?: ServiceCategoryRow[];
default_service_category?: string | null;
/** «قیمت ویزیت آزاد» tenant — سرور وقتی نوبت قیمت ندارد همین را می‌گذارد. */
free_visit_price_rials?: number;
}
/**
* انتخاب بیمهٔ یک نوبت: نوع خدماتِ فعالِ همین پزشک/کلینیک و قراردادهای بیمهٔ پایهٔ فعال،
* به‌همراه محاسبهٔ سهم — مشترک بین مودال «قطعی کردن نوبت» و صفحهٔ ویرایش نوبت تا
* هر دو یک قاعده را نشان دهند.
*/
export function useAppointmentInsurance(enabled: boolean) {
const pricingQuery = useQuery<ApiResponse<PricingPayload>>({
queryKey: ['insurance-pricing'],
queryFn: () => api.get('/api/v1/insurance-pricing'),
enabled,
});
const contractsQuery = useQuery<ApiResponse<{ data: TenantContract[] }>>({
queryKey: ['tenant-insurances'],
queryFn: () => api.get('/api/v1/billing/tenant-insurances'),
enabled,
});
const pricing = (pricingQuery.data?.data ?? {}) as PricingPayload;
const enabledCategories = useMemo(
() => (pricing.service_categories ?? []).filter((c) => c.enabled),
[pricing.service_categories],
);
const contracts: TenantContract[] = useMemo(
() => ((contractsQuery.data?.data as any)?.data ?? []) as TenantContract[],
[contractsQuery.data],
);
const activeContracts = useMemo(() => contracts.filter((c) => c.is_active !== false), [contracts]);
const basicContracts = useMemo(
() => 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 {
/**
* هزینهٔ ویزیتِ مؤثر — آینهٔ همان fallback سرور: قیمت خودِ نوبت، در نبودش
* «قیمت ویزیت آزاد» تنظیمات همین tenant.
*/
visitPriceOf: (appointmentVisitPrice: number | null | undefined): number =>
Number(appointmentVisitPrice ?? 0) > 0
? Number(appointmentVisitPrice)
: Number(pricing.free_visit_price_rials ?? 0),
/** فقط وقتی بیش از یک نوع فعال است، انتخاب از کاربر پرسیده می‌شود. */
needsCategoryChoice: enabledCategories.length > 1,
categoryOptions: enabledCategories.map((c) => ({ value: c.key, label: c.label })),
/** تنها نوع فعال (یا null اگر چند نوع فعال باشد) — همان قاعدهٔ سرور. */
defaultCategory: pricing.default_service_category ?? enabledCategories[0]?.key ?? null,
categoryLabelOf: (key: string | null | undefined) =>
(pricing.service_categories ?? []).find((c) => c.key === key)?.label ?? null,
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,
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,
};
}