feat: Enhance insurance billing system to support supplementary insurance
- 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.
This commit is contained in:
@@ -36,6 +36,7 @@ interface AppointmentLike {
|
||||
patient_name?: string | null;
|
||||
insurance_service_category?: string | null;
|
||||
insurance_base_id?: number | null;
|
||||
insurance_supplementary_id?: number | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -148,12 +149,14 @@ export default function ConfirmAppointmentModal({
|
||||
const total = visitPrice + servicesTotal;
|
||||
const [serviceCategory, setServiceCategory] = useState<string>('');
|
||||
const [insuranceId, setInsuranceId] = useState<string>('');
|
||||
const [supplementaryId, setSupplementaryId] = useState<string>('');
|
||||
|
||||
// مقدارِ نوبت مبنا است؛ در نبودش نوع پیشفرضِ tenant.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setServiceCategory(appt?.insurance_service_category ?? insurance.defaultCategory ?? '');
|
||||
setInsuranceId(appt?.insurance_base_id ? String(appt.insurance_base_id) : '');
|
||||
setSupplementaryId(appt?.insurance_supplementary_id ? String(appt.insurance_supplementary_id) : '');
|
||||
}, [open, appt?.uuid, insurance.defaultCategory]);
|
||||
|
||||
const effectiveCategory = serviceCategory || insurance.defaultCategory || DEFAULT_SERVICE_CATEGORY;
|
||||
@@ -166,9 +169,10 @@ export default function ConfirmAppointmentModal({
|
||||
category: s.service_category ?? DEFAULT_SERVICE_CATEGORY,
|
||||
insured: s.insurance_covered !== false,
|
||||
})),
|
||||
], insuranceId), [visitPrice, services, effectiveCategory, insuranceId, insurance.breakdown]);
|
||||
], insuranceId, supplementaryId), [visitPrice, services, effectiveCategory, insuranceId, supplementaryId, insurance.breakdown]);
|
||||
|
||||
const payable = insuranceId ? shares.patient : total;
|
||||
const hasInsurance = !!insuranceId || !!supplementaryId;
|
||||
const payable = hasInsurance ? shares.patient : total;
|
||||
|
||||
// ردیفِ اول تا لحظهای که کاربر مبلغ را دستی تغییر ندهد پیشفرضِ «پرداخت کامل» است؛
|
||||
// نوبت هنوز session ندارد، پس باقیماندهاش برابر مبلغِ قابل پرداخت است.
|
||||
@@ -196,6 +200,7 @@ export default function ConfirmAppointmentModal({
|
||||
version: appt?.version,
|
||||
...(serviceCategory ? { insurance_service_category: serviceCategory } : {}),
|
||||
...(insuranceId ? { insurance_base_id: Number(insuranceId) } : {}),
|
||||
...(supplementaryId ? { insurance_supplementary_id: Number(supplementaryId) } : {}),
|
||||
payments: rows
|
||||
.filter(r => tomanToRial(r.amountToman) > 0)
|
||||
.map(r => ({
|
||||
@@ -305,7 +310,7 @@ export default function ConfirmAppointmentModal({
|
||||
</div>
|
||||
)}
|
||||
<div className="field-block" style={{ flex: 1, minWidth: 180 }}>
|
||||
<label>بیمه</label>
|
||||
<label>بیمه پایه</label>
|
||||
<SearchableSelect
|
||||
value={insuranceId}
|
||||
onChange={(v) => setInsuranceId(v == null ? '' : String(v))}
|
||||
@@ -316,6 +321,19 @@ export default function ConfirmAppointmentModal({
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
{insurance.hasSupplementary && (
|
||||
<div className="field-block" style={{ flex: 1, minWidth: 180 }}>
|
||||
<label>بیمه تکمیلی</label>
|
||||
<SearchableSelect
|
||||
value={supplementaryId}
|
||||
onChange={(v) => setSupplementaryId(v == null ? '' : String(v))}
|
||||
options={insurance.supplementaryOptions}
|
||||
placeholder="بدون بیمه تکمیلی"
|
||||
isClearable
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* هزینهها */}
|
||||
@@ -339,10 +357,26 @@ export default function ConfirmAppointmentModal({
|
||||
<span>جمع کل</span>
|
||||
<strong style={{ color: 'var(--text)' }}>{formatRial(total)}</strong>
|
||||
</div>
|
||||
{/* سهمها زنجیرهایاند: پایه روی کل، تکمیلی روی باقیمانده — جدا نشان داده
|
||||
میشوند تا معلوم باشد هرکدام چقدر برداشتهاند. */}
|
||||
{insuranceId !== '' && (
|
||||
<div style={{ ...rowStyle, borderTop: '1px solid var(--border)' }}>
|
||||
<span>سهم بیمه{insurance.categoryLabelOf(effectiveCategory) ? ` (${insurance.categoryLabelOf(effectiveCategory)})` : ''}</span>
|
||||
<strong style={{ color: 'var(--success)' }}>{formatRial(shares.insurance)}</strong>
|
||||
<span>
|
||||
سهم بیمه پایه
|
||||
{insurance.insuranceNameOf(insuranceId) ? ` — ${insurance.insuranceNameOf(insuranceId)}` : ''}
|
||||
{insurance.categoryLabelOf(effectiveCategory) ? ` (${insurance.categoryLabelOf(effectiveCategory)})` : ''}
|
||||
</span>
|
||||
<strong style={{ color: 'var(--success)' }}>{formatRial(shares.base)}</strong>
|
||||
</div>
|
||||
)}
|
||||
{supplementaryId !== '' && (
|
||||
<div style={{ ...rowStyle, borderTop: '1px solid var(--border)' }}>
|
||||
<span>
|
||||
سهم بیمه تکمیلی
|
||||
{insurance.insuranceNameOf(supplementaryId) ? ` — ${insurance.insuranceNameOf(supplementaryId)}` : ''}
|
||||
<span style={{ color: 'var(--text-3)', fontSize: 11.5 }}> (روی باقیمانده)</span>
|
||||
</span>
|
||||
<strong style={{ color: 'var(--success)' }}>{formatRial(shares.supplementary)}</strong>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
@@ -351,7 +385,7 @@ export default function ConfirmAppointmentModal({
|
||||
paddingTop: 12, fontSize: 14, fontWeight: 700, color: 'var(--text)',
|
||||
}}
|
||||
>
|
||||
<span>{insuranceId !== '' ? 'سهم بیمار (قابل پرداخت)' : 'مبلغ قابل پرداخت'}</span>
|
||||
<span>{hasInsurance ? 'سهم بیمار (قابل پرداخت)' : 'مبلغ قابل پرداخت'}</span>
|
||||
<strong style={{ fontSize: 16, color: 'var(--primary)' }}>{formatRial(payable)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user