feat(insurance): resolve coverage percent per service category
Base insurance is a percentage-only rule: patient share is now total minus the base share, and the contract franchise no longer inflates it (franchise stays meaningful for supplementary contracts only). Coverage percentages are managed centrally by admin per service category (outpatient/inpatient, extensible via the ServiceCategory enum). A tenant contract may override a category, otherwise it follows the admin default live — changing the central value immediately applies to every contract that did not override it. - add ServiceCategory enum + GET /api/v1/service-categories as the single source of the category list for every client - add insurance_coverage_defaults (+ GET/PUT admin coverage-defaults endpoints) and expose coverage_defaults on the insurance list and insurance-pricing - add tenant_insurance_category_coverage; tenant-insurances accepts optional category_coverages (needs insurances.update) and returns the effective percentages with their source - add service_items.service_category; visits always resolve as outpatient - drop the reverse-engineered percent from patient_share_rials in MyPatientsPage and align the client-side BillingCalculator mirror in CreateStep Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+14
-2
@@ -10,9 +10,21 @@
|
||||
|
||||
- تعرفهی خدمت از `Tariff` سال جاری (با fallback به `ServiceItem.priceRials`).
|
||||
- قانون پوشش از قرارداد بیمهی tenant (`TenantInsurance`) + override خدمت (`TenantServiceCoverage`).
|
||||
- ترتیب محاسبه: کل → پوشش پایه (با سقف) → باقیمانده → پوشش مکمل → فرانشیز سهم بیمار.
|
||||
- درصد پوشش به تفکیک **نوع خدمت** (`ServiceItem.service_category`؛ ویزیت همیشه `outpatient`) و از زنجیرهٔ resolve توضیحدادهشده در [insurance.md](insurance.md#قاعدهٔ-درصد-پوشش-coverage-percent-model) گرفته میشود.
|
||||
- ترتیب محاسبه: کل → پوشش پایه (با سقف) → باقیمانده → پوشش مکمل (با سقف) → فرانشیزِ **تکمیلی** روی سهم بیمار.
|
||||
|
||||
نمونه: کل ۶۰۰٬۰۰۰ · پایه ۷۰٪ → ۴۲۰٬۰۰۰ · مکمل روی باقیمانده → ۱۲۰٬۰۰۰ · بیمار ۶۰٬۰۰۰.
|
||||
**بیمهٔ پایه صرفاً درصدی است:**
|
||||
|
||||
```
|
||||
سهم بیمهٔ پایه = round(کل × درصد پوشش پایه ÷ 100)
|
||||
سهم بیمار = کل − سهم بیمهٔ پایه
|
||||
```
|
||||
|
||||
`franchise_rials` قرارداد پایه در محاسبه **بیاثر** است (ستون برای سازگاری و قراردادهای تکمیلی میماند).
|
||||
|
||||
نمونهها:
|
||||
- کل ۶۰۰٬۰۰۰ · پایه ۷۰٪ → ۴۲۰٬۰۰۰ · مکمل روی باقیمانده → ۱۲۰٬۰۰۰ · بیمار ۶۰٬۰۰۰.
|
||||
- ویزیت ۵٬۹۵۲٬۰۰۰ ریال · پایهٔ بستری ۳۰٪ → سهم پایه ۱٬۷۸۵٬۶۰۰ · سهم بیمار ۴٬۱۶۶٬۴۰۰.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -8,6 +8,27 @@
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/service-categories
|
||||
|
||||
لیست انواع خدمت (سرپایی/بستری/…). **تنها منبع** این لیست برای کلاینتها؛ افزودن نوع تازه در
|
||||
بکاند یک `case` است و بدون تغییر فرانت اینجا ظاهر میشود. درصد پوشش بیمه به ازای همین
|
||||
نوعها تعیین میشود ([insurance.md](insurance.md#قاعدهٔ-درصد-پوشش-coverage-percent-model)).
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY`
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "key": "outpatient", "label": "خدمات سرپایی" },
|
||||
{ "key": "inpatient", "label": "خدمات بستری" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/service-sections
|
||||
|
||||
لیست بخشهای سرویس entity جاری.
|
||||
@@ -112,6 +133,8 @@
|
||||
"price_rials": 500000,
|
||||
"active": true,
|
||||
"insurance_covered": false,
|
||||
"service_category": "outpatient",
|
||||
"service_category_label": "خدمات سرپایی",
|
||||
"duration_minutes": 50,
|
||||
"bookable": true,
|
||||
"created_at": 1718000000,
|
||||
@@ -220,6 +243,7 @@ refresh مستقیم هم کار کند، بنابراین فیلترکردن س
|
||||
"price_rials": 500000,
|
||||
"staff_uuid": "...",
|
||||
"insurance_covered": true,
|
||||
"service_category": "outpatient",
|
||||
"duration_minutes": 50,
|
||||
"bookable": true
|
||||
}
|
||||
@@ -233,12 +257,13 @@ refresh مستقیم هم کار کند، بنابراین فیلترکردن س
|
||||
| staff_uuids | UUID[] | ❌ — پرسنل مسئول (چند نفر). ترجیح داده میشود |
|
||||
| staff_uuid | UUID | ❌ — legacy تکپرسنل (اگر `staff_uuids` نباشد استفاده میشود) |
|
||||
| insurance_covered | boolean | ❌ (پیشفرض false) — **deprecated برای نوشتن.** پنل ادمین دیگر این فیلد را نمیفرستد؛ مقدارش بهصورت خودکار از ردیفهای پوشش بیمه همگام میشود (به [insurance.md](insurance.md#put-apiv1billingtenant-insurancesuuidservice-coverage) نگاه کن). endpoint هنوز آن را میپذیرد تا کلاینتهای قدیمی نشکنند، ولی ذخیرهی پوشش بعداً آن را بازنویسی میکند |
|
||||
| service_category | string | ❌ (پیشفرض `outpatient`) — «نوع خدمت»؛ یکی از مقادیر [`GET /api/v1/service-categories`](#get-apiv1service-categories). درصد پوشش بیمهٔ این خدمت از همین نوع resolve میشود. مقدار نامعتبر → `422 ERR_VALIDATION_001` با فیلد `service_category` |
|
||||
| duration_minutes | integer\|null | ❌ — «زمان متوسط» انجام خدمت به دقیقه (`""`/`null` = بدون مقدار) |
|
||||
| bookable | boolean | ❌ (پیشفرض false) — «نمایش در نوبتدهی». فقط سرویسهای `bookable=true` در حالت نوبتدهی سرویسی قابلانتخاباند |
|
||||
| inventory_package_uuid | UUID\|null | ❌ — پکیج کالای مصرفی این خدمت ([inventory.md](inventory.md)). `null`/`""` یعنی قطع اتصال. پکیج باید متعلق به همان مطب/کلینیک باشد وگرنه `422 ERR_VALIDATION_001` با فیلد `inventory_package_uuid` |
|
||||
| consumables | array\|null | ❌ — کالاهای **تکی** این خدمت: `[{ "item_uuid": "…", "amount": 2 }]`. **مکمل پکیج است، نه جایگزین آن** — یک خدمت میتواند همزمان پکیج و کالای تکی داشته باشد. ارسال این فیلد کل فهرست را **جایگزین** میکند (`[]` = حذف همه). هر کالا باید متعلق به همان مطب/کلینیک باشد وگرنه `422 ERR_VALIDATION_001` با فیلد `consumables`. `amount` حداقل ۱ است |
|
||||
|
||||
> `bookable` در `PATCH /api/v1/service-item/{uuid}` هم به همین شکل پذیرفته میشود.
|
||||
> `bookable` و `service_category` در `PATCH /api/v1/service-item/{uuid}` هم به همین شکل پذیرفته میشوند؛ تغییر نوع خدمت در audit-log با برچسب «نوع خدمت» ثبت میشود.
|
||||
|
||||
**Response 201:** ServiceItem object (شامل `insurance_covered`)
|
||||
|
||||
|
||||
+147
-12
@@ -10,6 +10,33 @@ Two resource types:
|
||||
|
||||
---
|
||||
|
||||
## قاعدهٔ درصد پوشش (Coverage percent model)
|
||||
|
||||
سهم بیمهٔ پایه فقط درصدی است:
|
||||
|
||||
```
|
||||
سهم بیمهٔ پایه = round(مبلغ کل × درصد پوشش ÷ 100)
|
||||
سهم بیمار = مبلغ کل − سهم بیمهٔ پایه (فرانشیز در بیمهٔ پایه دخالت ندارد)
|
||||
```
|
||||
|
||||
درصد پوشش به تفکیک **نوع خدمت** تعیین میشود. لیست انواع از `GET /api/v1/service-categories`
|
||||
میآید (فعلاً `outpatient` = خدمات سرپایی و `inpatient` = خدمات بستری) و هرگز در کلاینت
|
||||
hardcode نمیشود. ویزیت همیشه `outpatient` است.
|
||||
|
||||
درصد مؤثر به این ترتیب resolve میشود (اولین مقدار موجود برنده است):
|
||||
|
||||
| اولویت | منبع | جدول |
|
||||
|---|---|---|
|
||||
| ۱ | override همان خدمت | `tenant_service_coverage.coverage_percent` |
|
||||
| ۲ | override قرارداد برای نوع خدمت | `tenant_insurance_category_coverage` |
|
||||
| ۳ | پیشفرض مرکزی ادمین (اگر > ۰ باشد) | `insurance_coverage_defaults` |
|
||||
| ۴ | `coverage_percent` قرارداد (سازگاری با ردیفهای قدیمی) | `tenant_insurances` |
|
||||
|
||||
**fallback زنده است، نه کپی:** قراردادی که ردیف سطح ۲ ندارد، با تغییر پیشفرض ادمین
|
||||
خودبهخود بهروز میشود. `franchise_rials` فقط در قراردادهای `supplementary` اثر دارد.
|
||||
|
||||
---
|
||||
|
||||
## GET `/api/v1/insurances`
|
||||
|
||||
List all active insurances.
|
||||
@@ -31,19 +58,24 @@ List all active insurances.
|
||||
"name": "بیمه تأمین اجتماعی",
|
||||
"type": "basic",
|
||||
"logo_url": "https://...",
|
||||
"status": "active"
|
||||
"status": "active",
|
||||
"coverage_defaults": { "outpatient": 70, "inpatient": 30 }
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "بیمه ایران",
|
||||
"type": "supplementary",
|
||||
"logo_url": "https://...",
|
||||
"status": "active"
|
||||
"status": "active",
|
||||
"coverage_defaults": { "outpatient": 0, "inpatient": 0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`coverage_defaults` درصدهای مرکزی ادمین به تفکیک نوع خدمت است؛ همیشه همهٔ نوعها حاضرند
|
||||
(نبودِ ردیف = `0`). پنل پزشک هنگام ساخت قرارداد همین مقادیر را پیشفرض بار میکند.
|
||||
|
||||
---
|
||||
|
||||
## GET `/api/v1/admin/insurances`
|
||||
@@ -61,10 +93,21 @@ List all insurances with pagination (admin view — includes inactive).
|
||||
| `type` | string | ❌ | `"basic"` or `"supplementary"` |
|
||||
|
||||
### Response `200`
|
||||
هر ردیف علاوه بر فیلدهای بیمه، `coverage_defaults` خود را هم دارد (یک کوئری برای کل صفحه، بدون N+1).
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [ ... ],
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "بیمه تأمین اجتماعی",
|
||||
"type": "basic",
|
||||
"logo_url": "https://...",
|
||||
"status": "active",
|
||||
"coverage_defaults": { "outpatient": 70, "inpatient": 30 }
|
||||
}
|
||||
],
|
||||
"meta": { "totalRecords": 15, "totalPages": 1, "currentPage": 1 }
|
||||
}
|
||||
```
|
||||
@@ -77,6 +120,72 @@ List all insurances with pagination (admin view — includes inactive).
|
||||
|
||||
---
|
||||
|
||||
## GET `/api/v1/admin/insurance/{id}/coverage-defaults`
|
||||
|
||||
درصدهای پوشش مرکزی یک بیمه به تفکیک نوع خدمت. همیشه **همهٔ** نوعها برمیگردند
|
||||
(ردیف نداشته = `0`)، تا پنل ادمین جدول کامل نشان دهد.
|
||||
|
||||
**Permission:** `ROLE_ADMIN`
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"insurance_id": 3,
|
||||
"categories": [
|
||||
{ "key": "outpatient", "label": "خدمات سرپایی", "coverage_percent": 70 },
|
||||
{ "key": "inpatient", "label": "خدمات بستری", "coverage_percent": 30 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_AUTH_001` | 401 | Missing token |
|
||||
| `ERR_AUTH_006` | 403 | Not admin |
|
||||
| `ERR_VALIDATION_002` | 404 | بیمه یافت نشد |
|
||||
|
||||
---
|
||||
|
||||
## PUT `/api/v1/admin/insurance/{id}/coverage-defaults`
|
||||
|
||||
ذخیرهٔ درصدهای مرکزی. تغییر این مقادیر بیدرنگ روی همهٔ قراردادهایی که برای همان نوع خدمت
|
||||
override ندارند اثر میگذارد.
|
||||
|
||||
**Permission:** `ROLE_ADMIN`
|
||||
|
||||
### Request Body (`application/json`)
|
||||
```json
|
||||
{
|
||||
"categories": [
|
||||
{ "key": "outpatient", "coverage_percent": 70 },
|
||||
{ "key": "inpatient", "coverage_percent": 30 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `categories` | array | ✅ | ردیفهایی که باید ذخیره شوند؛ ردیفهای نیامده دستنخورده میمانند |
|
||||
| `categories[].key` | string | ✅ | یکی از مقادیر `GET /api/v1/service-categories` |
|
||||
| `categories[].coverage_percent` | number | ✅ | ۰ تا ۱۰۰ |
|
||||
|
||||
### Response `200`
|
||||
همان ساختار پاسخِ `GET` (وضعیت پس از ذخیره).
|
||||
|
||||
### Errors
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `ERR_AUTH_001` | 401 | Missing token |
|
||||
| `ERR_AUTH_006` | 403 | Not admin |
|
||||
| `ERR_VALIDATION_002` | 404 | بیمه یافت نشد |
|
||||
| `ERR_VALIDATION_001` | 422 | `key` نامعتبر یا درصد خارج از بازهٔ ۰ تا ۱۰۰ |
|
||||
|
||||
---
|
||||
|
||||
## POST `/api/v1/admin/insurance`
|
||||
|
||||
Create a new insurance.
|
||||
@@ -307,20 +416,23 @@ entity جاری از `#[CurrentUser]` resolve میشود: نقش `ROLE_DOCTOR
|
||||
"insurance_id": 3,
|
||||
"insurance_name": "تأمین اجتماعی",
|
||||
"type": "basic",
|
||||
"patient_share_rials": 1500000
|
||||
"patient_share_rials": 1500000,
|
||||
"coverage_defaults": { "outpatient": 70, "inpatient": 30 }
|
||||
},
|
||||
{
|
||||
"insurance_id": 9,
|
||||
"insurance_name": "دانا",
|
||||
"type": "supplementary",
|
||||
"patient_share_rials": null
|
||||
"patient_share_rials": null,
|
||||
"coverage_defaults": { "outpatient": 0, "inpatient": 0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `patient_share_rials = null` یعنی این بیمه پذیرفته نمیشود (قیمتگذاری ندارد).
|
||||
- `coverage_defaults` — درصدهای مرکزی ادمین؛ پنل پزشک هنگام افزودن قرارداد از همین پر میکند.
|
||||
- `patient_share_rials = null` یعنی این بیمه پذیرفته نمیشود (قیمتگذاری ندارد). این مقدار **ورودی هیچ محاسبهای نیست**؛ محاسبهٔ سهم فقط از درصد پوشش انجام میشود.
|
||||
- `require_visit_price` — فلگ «الزامی کردن هزینه ویزیت». وقتی `true` باشد، ثبت مراجعه (session)، فاکتور سرویس و ثبت نوبت بدون هزینه ویزیت (`> 0`) رد میشوند.
|
||||
|
||||
### خطاها
|
||||
@@ -399,13 +511,22 @@ tenant از `#[CurrentUser]` با `App\Patient\Security\PatientRecordScopeResolv
|
||||
"annual_ceiling_rials": null,
|
||||
"kind": "basic",
|
||||
"effective_from": 1718900000,
|
||||
"effective_to": null
|
||||
"effective_to": null,
|
||||
"category_coverages": { "outpatient": 70, "inpatient": 30 },
|
||||
"category_coverage_source": { "outpatient": "override", "inpatient": "admin_default" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | توضیح |
|
||||
|------|-------|
|
||||
| `category_coverages` | درصد **مؤثر** هر نوع خدمت پس از اجرای زنجیرهٔ resolve |
|
||||
| `category_coverage_source` | منبع هر درصد: `override` (خودِ قرارداد) · `admin_default` (تنظیمات مرکزی) · `contract` (ستون قدیمی `coverage_percent`) |
|
||||
| `coverage_percent` | ستون قدیمی قرارداد؛ فقط آخرین سطح fallback است |
|
||||
| `franchise_rials` | فقط در قرارداد `supplementary` معنا دارد |
|
||||
|
||||
### POST `/api/v1/billing/tenant-insurances`
|
||||
فعالسازی/بهروزرسانی قرارداد. اگر قرارداد فعالی برای آن بیمه باشد ویرایش میشود، وگرنه نسخهی جدید.
|
||||
|
||||
@@ -413,21 +534,35 @@ tenant از `#[CurrentUser]` با `App\Patient\Security\PatientRecordScopeResolv
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `insurance_id` | int | الزامی |
|
||||
| `coverage_percent` | float | درصد پوشش (۰–۱۰۰) |
|
||||
| `franchise_rials` | int | فرانشیز ثابت سهم بیمار |
|
||||
| `coverage_percent` | float | ستون قدیمی قرارداد (آخرین سطح fallback)؛ پنل آن را با درصد سرپایی همگام میفرستد |
|
||||
| `franchise_rials` | int | فرانشیز — فقط در قرارداد `supplementary` اثر دارد |
|
||||
| `annual_ceiling_rials` | int \| null | سقف تعهد (null = بینهایت) |
|
||||
| `kind` | string \| null | نوع بیمه قرارداد (`basic`/`supplementary`); خالی → پیشفرض نوع کاتالوگ |
|
||||
| `effective_from` | int \| null | تاریخ شروع قرارداد (Unix)؛ null → اکنون |
|
||||
| `effective_to` | int \| null | تاریخ پایان قرارداد (Unix)؛ null → نامحدود |
|
||||
| `category_coverages` | array \| null | اختیاری — override درصد به تفکیک نوع خدمت. **نیامدنش** یعنی قرارداد روی پیشفرض مرکزی ادمین میماند (fallback زنده) |
|
||||
| `category_coverages[].key` | string | یکی از مقادیر `GET /api/v1/service-categories` |
|
||||
| `category_coverages[].coverage_percent` | number \| null | ۰ تا ۱۰۰؛ `null` → override آن نوع حذف و به پیشفرض ادمین برمیگردد |
|
||||
| `doctor_uuid` | string (UUID) \| null | اختیاری — قرارداد را بهازای پزشک هدف ذخیره میکند (نگاه کنید به «تنظیمات per-doctor» بالا) |
|
||||
|
||||
پاسخ `201`: `{ success, data: { …contract } }`.
|
||||
خطاها: `404 ERR_NOT_FOUND_001` بیمه یافت نشد · `422 ERR_VALIDATION_001` insurance_id الزامی · `403 ERR_FORBIDDEN_001` پروفایل یافت نشد.
|
||||
```json
|
||||
{
|
||||
"insurance_id": 3,
|
||||
"kind": "basic",
|
||||
"category_coverages": [
|
||||
{ "key": "outpatient", "coverage_percent": 70 },
|
||||
{ "key": "inpatient", "coverage_percent": 30 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
پاسخ `201`: `{ success, data: { …contract, category_coverages, category_coverage_source } }`.
|
||||
خطاها: `404 ERR_NOT_FOUND_001` بیمه یافت نشد · `422 ERR_VALIDATION_001` insurance_id الزامی، یا `key` نامعتبر / درصد خارج از ۰–۱۰۰ · `403 ERR_FORBIDDEN_001` پروفایل یافت نشد، یا ارسال `category_coverages` بدون مجوز `insurances.update`.
|
||||
|
||||
### PATCH `/api/v1/billing/tenant-insurances/{uuid}`
|
||||
ویرایش فیلدهای قرارداد (همه اختیاری، فقط کلیدهای موجود اعمال میشوند). فقط قرارداد متعلق به tenant جاری.
|
||||
|
||||
**Body:** `coverage_percent` · `franchise_rials` · `annual_ceiling_rials` · `kind` · `effective_from` · `effective_to` · `is_active` · `doctor_uuid` (اختیاری، برای هدفگیری پزشک — نگاه کنید به «تنظیمات per-doctor» بالا).
|
||||
**Body:** `coverage_percent` · `franchise_rials` · `annual_ceiling_rials` · `kind` · `effective_from` · `effective_to` · `is_active` · `category_coverages` (همان ساختار `POST`؛ ارسالش نیازمند مجوز `insurances.update` است وگرنه `403 ERR_FORBIDDEN_001`) · `doctor_uuid` (اختیاری، برای هدفگیری پزشک — نگاه کنید به «تنظیمات per-doctor» بالا).
|
||||
|
||||
- `is_active` (bool): toggle فعال/غیرفعال. برخلاف `DELETE`، مقدار `effective_to`ِ تعیینشدهٔ کاربر را دستنخورده نگه میدارد (برای reactivate).
|
||||
- قرارداد باید به همان موجودیتِ resolveشده (پزشک هدف یا tenant کاربر) تعلق داشته باشد، وگرنه `404`.
|
||||
|
||||
+3
-3
@@ -504,10 +504,10 @@ 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` دارد.
|
||||
- `base_insurance_discount_percent` / `supplementary_discount_percent`: **ورودی محاسبه نیستند.** هر مقداری که ارسال شود نادیده گرفته و از درصد قرارداد فعال (`TenantInsurance.coveragePercent`) بازنویسی میشود؛ صرفاً snapshot برای نمایش/گزارشاند.
|
||||
- `base_insurance_discount_percent` / `supplementary_discount_percent`: **ورودی محاسبه نیستند.** هر مقداری که ارسال شود نادیده گرفته و از درصد مؤثر قرارداد فعال (زنجیرهٔ resolve — [insurance.md](insurance.md#قاعدهٔ-درصد-پوشش-coverage-percent-model)، با نوع خدمتِ `outpatient` برای ویزیت) بازنویسی میشود؛ صرفاً snapshot برای نمایش/گزارشاند.
|
||||
- `final_price_rials` (سهم بیمار) به این صورت محاسبه میشود:
|
||||
- **ویزیت:** با قاعدهی پوشش قرارداد (`TenantInsuranceService::coverageRule`) از طریق `BillingCalculator` — همان مسیری که `InvoiceService` برای صدور فاکتور میرود. (تا پیش از این، ویزیت با فرمول درصدی جدا و inline حساب میشد و با فاکتور واگرا میشد.)
|
||||
- **هر خدمت:** سهم بیمار با قاعدهی پوشش همان بیمهگر برای همان خدمت (`TenantServiceCoverage` از طریق `BillingCalculator`) محاسبه میشود؛ یعنی فقط خدمتی که بیمهی انتخابشده آن را پوشش میدهد تخفیف میگیرد (درصد/فرانشیز/سقف؛ مقدار نبودِ override از قرارداد ارث میبرد). خدمتِ بدون پوشش، کامل بر عهدهی بیمار است.
|
||||
- **ویزیت:** خدمتِ سرپایی است و با قاعدهی پوشش قرارداد (`TenantInsuranceService::coverageRule`) از طریق `BillingCalculator` حساب میشود — همان مسیری که `InvoiceService` برای صدور فاکتور میرود. سهم بیمهٔ پایه = `round(کل × درصد ÷ 100)` و سهم بیمار = `کل − سهم پایه`؛ فرانشیزِ قرارداد پایه بیاثر است.
|
||||
- **هر خدمت:** سهم بیمار با قاعدهی پوشش همان بیمهگر برای همان خدمت (`TenantServiceCoverage` از طریق `BillingCalculator`) محاسبه میشود و درصد از **نوع خدمت** (`ServiceItem.service_category`: سرپایی/بستری) گرفته میشود؛ یعنی فقط خدمتی که بیمهی انتخابشده آن را پوشش میدهد تخفیف میگیرد (درصد/سقف، و فرانشیز فقط در قرارداد تکمیلی؛ مقدار نبودِ override از قرارداد/پیشفرض مرکزی ارث میبرد). خدمتِ بدون پوشش، کامل بر عهدهی بیمار است.
|
||||
- `final_price_rials = سهم بیمار ویزیت + Σ(سهم بیمار هر خدمت) + Σ(کالاهای مصرفی)` و `services_total_rials = Σ(price × quantity)` (قیمت کامل خدمات، بدون بیمه). کالاهای مصرفی در `consumables_total_rials` جدا گزارش میشوند.
|
||||
- **گیت پوشش:** اگر `ServiceItem.insurance_covered` غیرفعال باشد یا برای tenant قرارداد فعالی نباشد، هیچ پوششی اعمال نمیشود و کل مبلغ سهم بیمار است. این پرچم دستی ست نمیشود؛ از ردیفهای `TenantServiceCoverage` سینک میشود ([insurance.md](insurance.md)).
|
||||
- **سقف:** `annual_ceiling_rials` با وجود نامش بهصورت **سقف هر قلم** اعمال میشود؛ انباشت سالانهای در کد وجود ندارد.
|
||||
|
||||
Reference in New Issue
Block a user