Files
clinicpro/assets/admin/lib/insuranceShares.ts
T
hamed 4f4bce9fe2 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.
2026-07-29 13:28:59 +03:30

101 lines
4.0 KiB
TypeScript

/**
* آینهٔ سمت‌کلاینتِ `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_percent: number;
annual_ceiling_rials: number | null;
category_coverages?: Record<string, number>;
}
/** ویزیت آیتم سرویس نیست؛ نوعِ پیش‌فرضش سرپایی است. */
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.max(
0,
Math.round(remaining * (supp.percent / 100)) - Math.round(remaining * (supp.franchise / 100)),
);
if (supp.ceiling !== null) suppShare = Math.min(suppShare, supp.ceiling);
}
return total - baseShare - suppShare;
}
/** درصد مؤثر قرارداد برای یک نوع خدمت؛ نبودِ ردیف → ستون قدیمی قرارداد. */
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_percent : 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<ShareBreakdown>((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 });
}