Refactor insurance share calculation logic in PatientService
- Consolidated the calculation of patient and insurance shares into a single method using BillingCalculator. - Introduced new fields in PatientSession to store breakdown of insurance shares and patient share. - Updated the API responses to include the new fields for consistency across payment, invoice, and claims dashboard. - Added migration to backfill existing sessions with appropriate values for the new fields. - Implemented tests to ensure the correctness of the new logic and verify that the breakdown sums to the gross total. - Redesigned the claims dashboard to provide a more user-friendly overview of patient claims and their statuses.
This commit is contained in:
@@ -12,6 +12,9 @@ interface SessionData {
|
||||
session_at: number | null; paid_at: number | null;
|
||||
services_total_rials: number; consumables_total_rials: number;
|
||||
discount_rials: number; final_price_rials: number; paid_total_rials: number;
|
||||
gross_total_rials?: number; base_insurance_rials?: number;
|
||||
supplementary_insurance_rials?: number; patient_share_rials?: number;
|
||||
remaining_rials?: number;
|
||||
payments: SessionPayment[]; consumables: SessionConsumable[];
|
||||
}
|
||||
interface Invoice {
|
||||
@@ -64,11 +67,22 @@ export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceU
|
||||
const session = inv?.session ?? null;
|
||||
|
||||
const paid = inv?.status === 'paid';
|
||||
// با session: مبالغ واقعی پرداخت (مبلغ نهایی منهای تخفیف و پرداختها)؛ بدون آن heuristic قبلی.
|
||||
// مانده را سرور میدهد (session.remaining_rials)؛ محاسبهی محلی با صفحهی پرداخت واگرا میشد.
|
||||
const remaining = session
|
||||
? Math.max(0, session.final_price_rials - (session.discount_rials ?? 0) - session.paid_total_rials)
|
||||
? session.remaining_rials ?? Math.max(0, session.final_price_rials - (session.discount_rials ?? 0) - session.paid_total_rials)
|
||||
: inv ? (paid ? 0 : inv.patient_rials) : 0;
|
||||
const paidAmount = session ? session.paid_total_rials : inv ? inv.total_rials - remaining : 0;
|
||||
|
||||
// یک شکل واحد برای خلاصهی مالی؛ با session از خود مراجعه، بدون آن از فاکتور.
|
||||
const summary = {
|
||||
services: session ? session.services_total_rials : inv?.total_rials ?? 0,
|
||||
consumables: session?.consumables_total_rials ?? 0,
|
||||
discount: session?.discount_rials ?? 0,
|
||||
baseInsurance: session?.base_insurance_rials ?? inv?.base_insurance_rials ?? 0,
|
||||
suppInsurance: session?.supplementary_insurance_rials ?? inv?.supplementary_rials ?? 0,
|
||||
patientShare: session?.patient_share_rials ?? session?.final_price_rials ?? inv?.patient_rials ?? 0,
|
||||
gross: session?.gross_total_rials ?? inv?.total_rials ?? 0,
|
||||
};
|
||||
// وضعیت واقعی پرداخت (مستقل از وضعیت فریزشدهی فاکتور): تسویهشده اگر مانده صفر.
|
||||
const statusLabel = session ? (remaining <= 0 ? 'تسویه شده' : 'بدهکار') : (inv ? (STATUS_LABEL[inv.status] ?? inv.status) : '');
|
||||
|
||||
@@ -101,27 +115,19 @@ export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceU
|
||||
: [['-', '-', '-']]}
|
||||
/>
|
||||
)}
|
||||
{session ? (
|
||||
<SectionTable
|
||||
title="خلاصه مالی"
|
||||
cols={['جمع مبلغ سرویس', 'جمع مبلغ کالا', 'تخفیف', 'سهم بیمه پایه', 'سهم بیمه تکمیلی', 'مبلغ کل']}
|
||||
rows={[[
|
||||
formatRial(session.services_total_rials), formatRial(session.consumables_total_rials),
|
||||
session.discount_rials > 0 ? formatRial(session.discount_rials) : '-',
|
||||
formatRial(inv.base_insurance_rials), formatRial(inv.supplementary_rials),
|
||||
formatRial(session.final_price_rials),
|
||||
]]}
|
||||
/>
|
||||
) : (
|
||||
<SectionTable
|
||||
title="خلاصه مالی"
|
||||
cols={['جمع مبلغ سرویس', 'سهم بیمه پایه', 'سهم بیمه تکمیلی', 'سهم بیمار', 'مبلغ کل']}
|
||||
rows={[[
|
||||
formatRial(inv.total_rials), formatRial(inv.base_insurance_rials),
|
||||
formatRial(inv.supplementary_rials), formatRial(inv.patient_rials), formatRial(inv.total_rials),
|
||||
]]}
|
||||
/>
|
||||
)}
|
||||
<SectionTable
|
||||
title="خلاصه مالی"
|
||||
cols={['جمع مبلغ سرویس', 'جمع مبلغ کالا', 'سهم بیمه پایه', 'سهم بیمه تکمیلی', 'سهم بیمار', 'تخفیف', 'مبلغ نهایی']}
|
||||
rows={[[
|
||||
formatRial(summary.services),
|
||||
summary.consumables > 0 ? formatRial(summary.consumables) : '-',
|
||||
formatRial(summary.baseInsurance),
|
||||
formatRial(summary.suppInsurance),
|
||||
formatRial(summary.patientShare),
|
||||
summary.discount > 0 ? formatRial(summary.discount) : '-',
|
||||
formatRial(Math.max(0, summary.patientShare - summary.discount)),
|
||||
]]}
|
||||
/>
|
||||
{session && (
|
||||
<SectionTable
|
||||
title="پرداختی ها"
|
||||
|
||||
@@ -24,6 +24,12 @@ export interface SessionCardData {
|
||||
supplementary_discount_percent?: number;
|
||||
doctor_name?: string | null;
|
||||
final_price_rials?: number;
|
||||
// تفکیک بیمه — سرور محاسبه میکند (PatientSession::applyShares)
|
||||
gross_total_rials?: number;
|
||||
base_insurance_rials?: number;
|
||||
supplementary_insurance_rials?: number;
|
||||
patient_share_rials?: number;
|
||||
remaining_rials?: number;
|
||||
patient_debt_rials?: number;
|
||||
is_paid?: boolean;
|
||||
invoice_uuid?: string | null;
|
||||
|
||||
@@ -116,10 +116,17 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
|
||||
const finalPrice = session.final_price_rials ?? 0;
|
||||
const discountRials = session.discount_rials ?? 0;
|
||||
const debt = session.patient_debt_rials ?? 0;
|
||||
const payments = session.payments ?? [];
|
||||
const appliedRuleLabel = session.applied_discount_rule_label ?? null;
|
||||
const payable = Math.max(0, finalPrice - discountRials);
|
||||
// تفکیک بیمه و مانده را سرور میدهد؛ محاسبهی محلی باعث واگرایی با فاکتور میشد.
|
||||
const grossTotal = session.gross_total_rials ?? finalPrice;
|
||||
const baseInsurance = session.base_insurance_rials ?? 0;
|
||||
const suppInsurance = session.supplementary_insurance_rials ?? 0;
|
||||
const hasInsurance = baseInsurance > 0 || suppInsurance > 0;
|
||||
const payable = session.remaining_rials !== undefined
|
||||
? session.remaining_rials + (session.paid_total_rials ?? 0)
|
||||
: Math.max(0, finalPrice - discountRials);
|
||||
const debt = session.remaining_rials ?? session.patient_debt_rials ?? 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -194,8 +201,28 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
|
||||
{/* خلاصهی مبلغ: قبل از تخفیف / تخفیف / نهایی / منبع */}
|
||||
<div className="dark:border-[#35343D]" style={{ border: '1px solid #e0e0e0', borderRadius: 8, padding: 12, marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{hasInsurance && (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
|
||||
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>هزینه کل خدمات:</span>
|
||||
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(grossTotal)}</span>
|
||||
</div>
|
||||
{baseInsurance > 0 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
|
||||
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>سهم بیمه پایه:</span>
|
||||
<span style={{ color: '#2E7D32' }}>{formatRial(baseInsurance)}</span>
|
||||
</div>
|
||||
)}
|
||||
{suppInsurance > 0 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
|
||||
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>سهم بیمه تکمیلی:</span>
|
||||
<span style={{ color: '#2E7D32' }}>{formatRial(suppInsurance)}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
|
||||
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>مبلغ قبل از تخفیف:</span>
|
||||
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>{hasInsurance ? 'سهم بیمار:' : 'مبلغ قبل از تخفیف:'}</span>
|
||||
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(finalPrice)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
|
||||
|
||||
Reference in New Issue
Block a user