/** * آینهٔ سمت‌کلاینتِ `BillingCalculator` سرور. تنها منبع محاسبهٔ سهم‌ها در پنل است — * هیچ صفحه‌ای نباید فرمول درصدی جداگانه بنویسد، وگرنه مبلغِ نمایش‌داده‌شده با مبلغِ * ثبت‌شده واگرا می‌شود. */ export interface CoverageRule { covered: boolean; percent: number; /** فقط در بیمهٔ تکمیلی معنا دارد. */ franchise: number; ceiling: number | null; } /** قراردادِ بیمهٔ tenant، همان شکلی که `/api/v1/billing/tenant-insurances` می‌دهد. */ export interface TenantContract { insurance_id: number; insurance_name: string | null; insurance_kind: string | null; is_active?: boolean; coverage_percent: number; franchise_rials: number; annual_ceiling_rials: number | null; category_coverages?: Record; } /** ویزیت آیتم سرویس نیست؛ نوعِ پیش‌فرضش سرپایی است. */ export const DEFAULT_SERVICE_CATEGORY = 'outpatient'; /** * سهم بیمار یک ردیف: کل − سهم پایه (با سقف) − سهم تکمیلی (روی باقیمانده) + فرانشیزِ تکمیلی. * فرانشیزِ بیمهٔ پایه در محاسبه دخالت نمی‌کند. */ export function patientShareOf(total: number, base: CoverageRule | null, supp: CoverageRule | null): number { let baseShare = 0; let remaining = total; if (base && base.covered) { baseShare = Math.round(total * (base.percent / 100)); if (base.ceiling !== null) baseShare = Math.min(baseShare, base.ceiling); remaining = total - baseShare; } let suppShare = 0; if (supp && supp.covered) { suppShare = Math.round(remaining * (supp.percent / 100)); if (supp.ceiling !== null) suppShare = Math.min(suppShare, supp.ceiling); remaining = remaining - suppShare; } return Math.min(remaining + (supp?.franchise ?? 0), total); } /** درصد مؤثر قرارداد برای یک نوع خدمت؛ نبودِ ردیف → ستون قدیمی قرارداد. */ export function contractPercentFor(contract: TenantContract, category: string): number { return Number(contract.category_coverages?.[category] ?? contract.coverage_percent ?? 0); } /** قاعدهٔ پوشش یک ردیف تحت یک قرارداد. فرانشیز فقط از قرارداد تکمیلی خوانده می‌شود. */ export function ruleOf(contract: TenantContract | null, category: string): CoverageRule | null { if (!contract) return null; return { covered: true, percent: contractPercentFor(contract, category), franchise: contract.insurance_kind === 'supplementary' ? contract.franchise_rials : 0, ceiling: contract.annual_ceiling_rials, }; } export interface BillableLine { /** مبلغ کل ردیف (با احتساب تعداد). */ total: number; /** نوع خدمت؛ برای ویزیت نوعِ انتخاب‌شدهٔ مراجعه. */ category: string; /** خدمتی که پوشش بیمه ندارد کامل سهم بیمار است. */ insured: boolean; } export interface ShareBreakdown { total: number; insurance: number; patient: number; } /** تفکیک سهم بیمه/بیمار برای مجموعه‌ای از ردیف‌ها تحت یک قرارداد پایه. */ export function breakdownOf(lines: BillableLine[], base: TenantContract | null): ShareBreakdown { return lines.reduce((acc, line) => { const patient = line.insured ? patientShareOf(line.total, ruleOf(base, line.category), null) : line.total; return { total: acc.total + line.total, insurance: acc.insurance + (line.total - patient), patient: acc.patient + patient, }; }, { total: 0, insurance: 0, patient: 0 }); }