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:
hamed
2026-07-25 16:21:19 +03:30
co-authored by Claude Opus 5
parent 1a9eda3576
commit 58c6d9ac18
41 changed files with 2558 additions and 143 deletions
+26 -1
View File
@@ -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`)