- 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.
101 lines
4.0 KiB
TypeScript
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 });
|
|
}
|