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:
+51
-21
@@ -12,20 +12,43 @@
|
||||
- قانون پوشش از قرارداد بیمهی tenant (`TenantInsurance`) + override خدمت (`TenantServiceCoverage`).
|
||||
- درصد پوشش به تفکیک **نوع خدمت** و از زنجیرهٔ resolve توضیحدادهشده در [insurance.md](insurance.md#قاعدهٔ-درصد-پوشش-coverage-percent-model) گرفته میشود: هر خدمت با `ServiceItem.service_category` خودش، و **ویزیت** با `PatientSession.insurance_service_category` (نوعی که سرِ پذیرش انتخاب شده؛ در نبودش سرپایی).
|
||||
- `invoices.service_category` همان نوع را snapshot میکند و در `toArray()` بهصورت `service_category` / `service_category_label` برمیگردد.
|
||||
- ترتیب محاسبه: کل → پوشش پایه (با سقف) → باقیمانده → پوشش مکمل (با سقف) → فرانشیزِ **تکمیلی** روی سهم بیمار.
|
||||
|
||||
**بیمهٔ پایه صرفاً درصدی است:**
|
||||
**محاسبه زنجیرهای است، نه همزمان:** اول بیمهٔ پایه روی کل مبلغ، بعد بیمهٔ تکمیلی روی
|
||||
**باقیماندهٔ سهم بیمار**، نه روی کل.
|
||||
|
||||
```
|
||||
سهم بیمهٔ پایه = round(کل × درصد پوشش پایه ÷ 100)
|
||||
سهم بیمار = کل − سهم بیمهٔ پایه
|
||||
سهم پایه = min(round(کل × درصد پایه ÷ 100), سقف پایه)
|
||||
باقیمانده = کل − سهم پایه
|
||||
سهم تکمیلی = min(max(0, round(باقیمانده × درصد تکمیلی ÷ 100) − round(باقیمانده × فرانشیز ÷ 100)), سقف تکمیلی)
|
||||
سهم بیمار = کل − سهم پایه − سهم تکمیلی
|
||||
```
|
||||
|
||||
`franchise_rials` قرارداد پایه در محاسبه **بیاثر** است (ستون برای سازگاری و قراردادهای تکمیلی میماند).
|
||||
- **فرانشیز درصد است، نه مبلغ**، و از تعهد بیمهٔ تکمیلی **کسر** میشود (نه اینکه روی سهم بیمار
|
||||
اضافه شود). با مدل قبلیِ «افزودن به سهم بیمار»، جمع سهمها از کل بیشتر میشد و مطالبهٔ
|
||||
ارسالی به بیمه بیش از سهم واقعیاش بود.
|
||||
- `franchise_percent` قرارداد **پایه** در محاسبه بیاثر است.
|
||||
- invariant همیشگی: `کل = سهم پایه + سهم تکمیلی + سهم بیمار`.
|
||||
|
||||
نمونهها:
|
||||
- کل ۶۰۰٬۰۰۰ · پایه ۷۰٪ → ۴۲۰٬۰۰۰ · مکمل روی باقیمانده → ۱۲۰٬۰۰۰ · بیمار ۶۰٬۰۰۰.
|
||||
- کل ۱۰٬۰۰۰٬۰۰۰ · پایه ۳۰٪ → ۳٬۰۰۰٬۰۰۰ · تکمیلی ۹۰٪ با فرانشیز ۱۰٪ روی باقیماندهٔ ۷٬۰۰۰٬۰۰۰ → ۵٬۶۰۰٬۰۰۰ · بیمار ۱٬۴۰۰٬۰۰۰.
|
||||
- ویزیت ۵٬۹۵۲٬۰۰۰ ریال · پایهٔ بستری ۳۰٪ → سهم پایه ۱٬۷۸۵٬۶۰۰ · سهم بیمار ۴٬۱۶۶٬۴۰۰.
|
||||
- بدون پایه · تکمیلی ۹۰٪ با فرانشیز ۱۰٪ روی ۱٬۰۰۰٬۰۰۰ → بیمه ۸۰۰٬۰۰۰ · بیمار ۲۰۰٬۰۰۰.
|
||||
|
||||
### مطالبهٔ خودکار
|
||||
|
||||
مطالبهٔ بیمه اثر جانبیِ **نهاییشدن صورتحساب** است، نه کارِ یک endpoint خاص: `InvoiceService::finalize()`
|
||||
رویداد `InvoiceFinalized` میفرستد و `CreateClaimsOnInvoiceFinalized` مطالبات جاافتاده را میسازد
|
||||
(idempotent — مطالبهٔ موجود دستنخورده میماند، و بیمهای که سهمی نبرده مطالبه نمیگیرد).
|
||||
|
||||
هر مراجعهٔ بیمهدار هم — چه از «ثبت مراجعه»، چه از ویرایش مراجعه، چه از **قطعیکردن نوبت** —
|
||||
با `SessionBillingService::ensureFinalizedInvoice()` صورتحساب نهایی میگیرد. پیش از این فقط
|
||||
مسیر «ثبت مراجعه» مطالبه میساخت و بقیهٔ مسیرها بیصدا بدون مطالبه میماندند.
|
||||
|
||||
جبران دادههای قدیمی:
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console app:billing:backfill-claims --dry-run # گزارش
|
||||
ddev exec php bin/console app:billing:backfill-claims # ساخت مطالبات جاافتاده
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -216,9 +239,10 @@
|
||||
| `limit` | int | 20 | حداکثر ۱۰۰ |
|
||||
| `sort` | string | `last_activity_at` | `full_name` \| `claims_count` \| `total_services_rials` \| `total_insurance_rials` \| `last_activity_at` |
|
||||
| `dir` | string | `desc` | `asc` \| `desc` |
|
||||
| `search` | string | — | نام، موبایل یا کد ملی بیمار |
|
||||
| `search` | string | — | نام، موبایل، کد ملی بیمار **یا نام بیمه** |
|
||||
| `status` | string | — | `pending` \| `submitted` \| `approved` \| `rejected` \| `paid` |
|
||||
| `insurance_id` | int | — | شناسه بیمه |
|
||||
| `kind` | string | — | نوع بیمه: `base` \| `supplementary` (واژگان مطالبه `base` است، نه `basic`). مقدار نامعتبر → `422 ERR_VALIDATION_001` با `field: kind` |
|
||||
| `doctor_id` | int | — | پزشکِ نوبتِ مراجعه |
|
||||
| `payment_status` | string | — | `paid` (وصولشده) \| `unpaid` |
|
||||
| `from` / `to` | int | — | بازهی `claims.created_at` (unix ثانیه) |
|
||||
@@ -230,25 +254,31 @@
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"patient_uuid": "45064492-...",
|
||||
"record_uuid": "ad0a3d0e-...",
|
||||
"full_name": "تست جراحی بینی",
|
||||
"mobile": "09370671756",
|
||||
"patient_uuid": "649e24ae-08e2-489d-b502-af07c59dfa2d",
|
||||
"record_uuid": "8e36035c-d234-42aa-858b-2427918e9c4a",
|
||||
"full_name": "تست بیمه ۱",
|
||||
"mobile": "09243243534",
|
||||
"national_code": null,
|
||||
"claims_count": 2,
|
||||
"total_services_rials": 81500000,
|
||||
"total_insurance_rials": 57050000,
|
||||
"total_patient_rials": 24450000,
|
||||
"total_approved_rials": 28000000,
|
||||
"total_paid_rials": 28000000,
|
||||
"overall_status": "mixed",
|
||||
"last_activity_at": 1784404115
|
||||
"claims_count": 3,
|
||||
"total_services_rials": 91900000,
|
||||
"total_insurance_rials": 9877000,
|
||||
"total_patient_rials": 82023000,
|
||||
"total_approved_rials": 0,
|
||||
"total_paid_rials": 0,
|
||||
"overall_status": "pending",
|
||||
"insurances": [
|
||||
{ "insurance_id": 176, "insurance_name": "تامین اجتماعی", "kind": "base" },
|
||||
{ "insurance_id": 182, "insurance_name": "بیمه ایران", "kind": "supplementary" }
|
||||
],
|
||||
"last_activity_at": 1785341593
|
||||
}
|
||||
],
|
||||
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
|
||||
"meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1, "limit": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
- `insurances`: بیمههایی که این بیمار زیرشان مطالبه دارد — یک بیمار میتواند همزمان پایه و تکمیلی داشته باشد، پس ستون «بیمه» یک لیست است نه یک مقدار.
|
||||
|
||||
- `overall_status`: اگر همهی مطالبات بیمار یک وضعیت داشته باشند همان؛ وگرنه `mixed`.
|
||||
- ثابت: `total_services_rials = total_insurance_rials + total_patient_rials`.
|
||||
- **ضدِ double-counting:** مبالغ خدمات/سهم بیمار از **صورتحسابهای یکتا** جمع میشوند، نه از مطالبات. یک صورتحساب میتواند همزمان مطالبهی پایه و مکمل داشته باشد؛ جمعزدن از سمت مطالبه مبلغ خدمات را دوبار میشمرد.
|
||||
|
||||
Reference in New Issue
Block a user