feat(insurance): bill an appointment with a chosen service kind and insurance
An appointment can now carry the insurance it is billed with: the service kind (outpatient/inpatient) and the basic insurance. Confirming it no longer hands the whole amount to the patient — the visit is split through BillingCalculator with the coverage percent of that service kind, and the choice travels to the encounter and the invoice built from it. The enabled service kinds are a tenant-wide setting (all of that tenant's insurances share it), so a tenant covering only one kind is never asked which one: the panel resolves it the same way the server does. - add tenant_service_category_settings + TenantServiceCategoryService, exposed on the existing insurance-pricing endpoint (service_categories, default_service_category); at least one kind must stay enabled - add appointments.insurance_service_category / insurance_base_id with AppointmentInsuranceService validating them against the tenant's own settings and active contracts (basic only), accepted by PATCH and by confirm - snapshot the kind on patient_sessions and invoices; the visit's coverage rule is resolved per kind (services keep using their own ServiceItem.service_category) - lib/insuranceShares becomes the single client-side mirror of BillingCalculator, shared by the confirm modal, the appointment edit page and the session form - surface the selection: confirm modal (with live shares), turns timeline chip, appointment edit page, patient record service card and invoice summary - the session form shows the insurance block whenever the tenant has an active contract and prefills the patient's own insurance, so it can be changed - fix: the confirm modal showed a zero visit price when the appointment had none — it now falls back to the tenant's free-visit price like the server - fix: useServiceCategories read one level too shallow, so Persian labels never arrived and raw enum keys leaked into the contract summary - fix: BlogsPage test asserted the public blogs endpoint after the page moved to the admin one Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -504,6 +504,8 @@ Creates a new visit session for a patient record.
|
||||
- `inventory_package_uuid` (اختیاری): مرجع پکیج مصرفی ([inventory](inventory.md))؛ فقط پکیج متعلق به همان tenant پذیرفته میشود، وگرنه بیصدا نادیده گرفته میشود. روی قیمت اثری ندارد (فقط مرجع).
|
||||
- `consumables` (اختیاری): کالاهای مصرفی از انبار ([inventory](inventory.md)). `price_rials` snapshot از `InventoryItem.price`؛ `quantity` (پیشفرض ۱، حداقل ۱). کالاها **پوشش بیمه ندارند** و مبلغ کاملشان به `final_price_rials` (سهم بیمار) اضافه میشود. آیتم ناموجود یا متعلق به tenant دیگر بیصدا رد میشود (همرفتار با `services`). پاسخ شامل `consumables[]` (با `line_total_rials`) و `consumables_total_rials` است.
|
||||
- `services`: array of service items to attach; `price_rials` snapshot از ServiceItem؛ `quantity` (پیشفرض ۱) → `line_total_rials = price_rials × quantity`. هر `SessionService` در پاسخ `quantity` و `line_total_rials` دارد.
|
||||
- پاسخهای مراجعه (لیست، ایجاد، ویرایش) علاوه بر شناسهها، `insurance_base_name` و `insurance_supplementary_name` را هم دارند (نامها با کش دروندرخواستی حل میشوند، بدون N+1) تا کارت «سرویسها» در پرونده بیمار بتواند نوع خدمت و بیمه را نشان دهد.
|
||||
- `insurance_service_category` (اختیاری): نوع خدمتِ بیمهایِ این مراجعه — یکی از مقادیر [`GET /api/v1/service-categories`](clinic-services.md#get-apiv1service-categories). درصد پوششِ **ویزیت** از همین نوع resolve میشود؛ سهم هر خدمت همچنان از `service_category` خودِ خدمت میآید. `null` → سرپایی. در پاسخ بههمراه `insurance_service_category_label` برمیگردد و در `PATCH /api/v1/session/{uuid}` هم پذیرفته میشود.
|
||||
- `base_insurance_discount_percent` / `supplementary_discount_percent`: **ورودی محاسبه نیستند.** هر مقداری که ارسال شود نادیده گرفته و از درصد مؤثر قرارداد فعال (زنجیرهٔ resolve — [insurance.md](insurance.md#قاعدهٔ-درصد-پوشش-coverage-percent-model)، با نوع خدمتِ `outpatient` برای ویزیت) بازنویسی میشود؛ صرفاً snapshot برای نمایش/گزارشاند.
|
||||
- `final_price_rials` (سهم بیمار) به این صورت محاسبه میشود:
|
||||
- **ویزیت:** خدمتِ سرپایی است و با قاعدهی پوشش قرارداد (`TenantInsuranceService::coverageRule`) از طریق `BillingCalculator` حساب میشود — همان مسیری که `InvoiceService` برای صدور فاکتور میرود. سهم بیمهٔ پایه = `round(کل × درصد ÷ 100)` و سهم بیمار = `کل − سهم پایه`؛ فرانشیزِ قرارداد پایه بیاثر است.
|
||||
@@ -687,6 +689,10 @@ GET /api/v1/session/{uuid}/audit-log
|
||||
1. اگر بیمار در آن محیط پرونده نداشته باشد، یک `PatientRecord` میسازد
|
||||
2. یک `PatientSession` گرهخورده به همان نوبت میسازد (زمان مراجعه = زمان نوبت، هزینه ویزیت
|
||||
و خطوط سرویس از خود نوبت snapshot میشوند)
|
||||
3. **بیمهٔ نوبت** (`appointments.insurance_base_id` + `insurance_service_category`) روی مراجعه
|
||||
snapshot میشود و سهم بیمه/بیمار با `BillingCalculator` محاسبه میگردد. نوع خدمتِ مؤثر:
|
||||
انتخابِ نوبت → تنها نوع فعالِ tenant → سرپایی. نوبتِ بدون بیمه کاملاً سهم بیمار میماند
|
||||
(رفتار قبلی).
|
||||
|
||||
### محیط پرونده — یکی، نه هر دو (2026-07)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user