- 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.
105 lines
4.7 KiB
TypeScript
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,
|
|
};
|
|
}
|