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:
+25
-10
@@ -367,6 +367,7 @@ Get appointment detail.
|
||||
| `insurance_service_category` | string \| null | نوع خدمتِ بیمهایِ این نوبت — یکی از مقادیر [`GET /api/v1/service-categories`](clinic-services.md#get-apiv1service-categories). `null` = انتخاب نشده؛ محاسبه به نوع پیشفرضِ tenant برمیگردد (`default_service_category` در [insurance.md](insurance.md)) |
|
||||
| `insurance_service_category_label` | string \| null | برچسب فارسی همان نوع |
|
||||
| `insurance_base_id` | int \| null | بیمهٔ **پایهٔ** انتخابشده؛ باید قرارداد فعال روی همان پزشک/کلینیک داشته باشد |
|
||||
| `insurance_supplementary_id` | int \| null | بیمهٔ **تکمیلیِ** انتخابشده؛ قرارداد فعال لازم دارد و روی **باقیماندهٔ بعد از بیمهٔ پایه** محاسبه میشود ([فرمول زنجیرهای](billing.md)) |
|
||||
|
||||
نام بیمه در این پاسخ نیست؛ پنل آن را از `GET /api/v1/billing/tenant-insurances` (که کش میشود) مپ میکند تا لیستهای نوبت به N+1 نیفتند.
|
||||
|
||||
@@ -557,6 +558,7 @@ transaction**. If any step fails nothing is committed.
|
||||
| `version` | integer | ❌ | Optimistic lock version; defaults to the stored one |
|
||||
| `insurance_service_category` | string | ❌ | نوع خدمتِ بیمهای، همان قواعد و خطاهای `PATCH /api/v1/appointment/{uuid}`. قبل از ساخت مراجعه روی نوبت مینشیند تا سهمها با همان نوع محاسبه شوند. |
|
||||
| `insurance_base_id` | integer | ❌ | بیمهٔ پایه، همان قواعد و خطاهای `PATCH`. |
|
||||
| `insurance_supplementary_id` | integer | ❌ | بیمهٔ تکمیلی، همان قواعد `PATCH`. روی باقیماندهٔ بعد از بیمهٔ پایه اعمال میشود. |
|
||||
| `payments` | array | ❌ | Empty/absent = confirm without payment. Several rows allowed (split payment). |
|
||||
| `payments[].method` | string | ✅ | ∈ `wallet\|pos\|cash\|card` |
|
||||
| `payments[].amount_rials` | integer | ✅ | > 0 |
|
||||
@@ -584,8 +586,10 @@ collected later through `POST /api/v1/session/{uuid}/payments`.
|
||||
A `PatientSession` is opened with the visit price and one line per attached service.
|
||||
4. **تفکیک بیمه** روی همان مراجعه محاسبه میشود (`BillingCalculator`): درصد پوشش از
|
||||
نوع خدمتِ نوبت — یا تنها نوع فعالِ tenant، وگرنه سرپایی — و زنجیرهٔ resolve
|
||||
([insurance.md](insurance.md#قاعدهٔ-درصد-پوشش-coverage-percent-model)). نوبتِ بدون بیمه
|
||||
مثل قبل کاملاً سهم بیمار میماند.
|
||||
([insurance.md](insurance.md#قاعدهٔ-درصد-پوشش-coverage-percent-model)). محاسبه زنجیرهای
|
||||
است: پایه روی کل، تکمیلی روی باقیمانده. نوبتِ بدون بیمه مثل قبل کاملاً سهم بیمار میماند.
|
||||
مراجعهٔ بیمهدار همینجا صورتحساب نهایی و مطالبهٔ بیمه هم میگیرد
|
||||
(`SessionBillingService` → رویداد `InvoiceFinalized`؛ [billing.md](billing.md)).
|
||||
5. Each payment row is registered on that visit (`wallet` also debits the patient wallet).
|
||||
|
||||
> پیش از این، پروندهای که با قطعیکردن ساخته میشد همیشه کل مبلغ را سهم بیمار میگذاشت
|
||||
@@ -593,7 +597,7 @@ collected later through `POST /api/v1/session/{uuid}/payments`.
|
||||
|
||||
پاسخ، `session` را با تفکیک بیمه برمیگرداند: `gross_total_rials`، `base_insurance_rials`،
|
||||
`supplementary_insurance_rials`، `patient_share_rials`، `insurance_service_category`،
|
||||
`insurance_base_id` — تا مودال همان مبلغی را نشان دهد که ثبت شده است.
|
||||
`insurance_base_id`، `insurance_supplementary_id` — تا مودال همان مبلغی را نشان دهد که ثبت شده است.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
@@ -603,18 +607,28 @@ collected later through `POST /api/v1/session/{uuid}/payments`.
|
||||
"appointment": { "uuid": "…", "status": "confirmed", "version": 4 },
|
||||
"session": {
|
||||
"uuid": "…",
|
||||
"visit_price_rials": 5000000,
|
||||
"services_total_rials": 1500000,
|
||||
"final_price_rials": 6500000,
|
||||
"visit_price_rials": 5950000,
|
||||
"services_total_rials": 0,
|
||||
"final_price_rials": 833000,
|
||||
"discount_rials": 0,
|
||||
"paid_total_rials": 5000000,
|
||||
"remaining_rials": 1500000,
|
||||
"is_paid": false
|
||||
"paid_total_rials": 0,
|
||||
"remaining_rials": 833000,
|
||||
"is_paid": false,
|
||||
"insurance_service_category": "outpatient",
|
||||
"insurance_base_id": 176,
|
||||
"insurance_supplementary_id": 182,
|
||||
"gross_total_rials": 5950000,
|
||||
"base_insurance_rials": 1785000,
|
||||
"supplementary_insurance_rials": 3332000,
|
||||
"patient_share_rials": 833000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> نمونهٔ بالا خروجی واقعیِ همان مسیر است: ویزیت ۵٬۹۵۰٬۰۰۰ · پایه ۳۰٪ سرپایی → ۱٬۷۸۵٬۰۰۰ ·
|
||||
> تکمیلیِ ۹۰٪ با فرانشیز ۱۰٪ روی باقیماندهٔ ۴٬۱۶۵٬۰۰۰ → ۳٬۳۳۲٬۰۰۰ · سهم بیمار ۸۳۳٬۰۰۰.
|
||||
|
||||
`session` is `null` when the tenant does not have the `patient_records` subscription
|
||||
feature — the appointment is still confirmed, it simply has no case file. **Sending
|
||||
`payments` in that situation fails with `403 ERR_SUBSCRIPTION_REQUIRED` and confirms
|
||||
@@ -876,7 +890,8 @@ General update (ویرایش / جا به جایی / انتقال به رزرو /
|
||||
- `slot_start`/`slot_end` must be sent together; moving to an occupied slot → `409`.
|
||||
- Relation uuids: empty string clears; unknown uuid → `422`.
|
||||
- `insurance_service_category` — نوعِ خدمت باید در تنظیمات بیمهٔ همان tenant **فعال** باشد؛ `null`/`""` انتخاب را پاک میکند. نوع نامعتبر یا غیرفعال → `422 ERR_VALIDATION_001` با فیلد `insurance_service_category`.
|
||||
- `insurance_base_id` — بیمه باید قرارداد فعال روی همان پزشک/کلینیک داشته باشد و **پایه** باشد؛ `null`/`0` انتخاب را پاک میکند. بیمهٔ بدون قرارداد فعال یا بیمهٔ تکمیلی → `422 ERR_VALIDATION_001` با فیلد `insurance_base_id`.
|
||||
- `insurance_base_id` — بیمه باید قرارداد فعال روی همان پزشک/کلینیک داشته باشد و **پایه** باشد؛ `null`/`0` انتخاب را پاک میکند. بیمهٔ بدون قرارداد فعال → `422 ERR_VALIDATION_001` («این بیمه برای این پزشک/کلینیک قرارداد فعال ندارد»)؛ فرستادن بیمهٔ تکمیلی در این فیلد → `422` («اینجا فقط بیمهٔ پایه قابل انتخاب است»)، هر دو با فیلد `insurance_base_id`.
|
||||
- `insurance_supplementary_id` — همان قواعد، برعکس: فقط قرارداد فعالِ **تکمیلی** پذیرفته میشود؛ فرستادن بیمهٔ پایه → `422` («اینجا فقط بیمهٔ تکمیلی قابل انتخاب است») با فیلد `insurance_supplementary_id`.
|
||||
- `status` follows the same transition rules as `PATCH /appointment/{uuid}/status`. A transition to `cancelled_by_doctor`/`cancelled_by_user` records a cancellation event (Timeline) + `app_log` warning; an optional `cancel_reason` body field is stored on the event. An inline cancellation is gated on `appointments.cancel` exactly like the dedicated status endpoint, so it cannot be used to bypass a secretary's missing cancel permission.
|
||||
- Optimistic lock via `version` → `409` on concurrent edit.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user