feat: insurance & medical billing system (6 phases)
Multi-tenant insurance contracts, service coverage, versioned tariffs, invoice calculation, and insurance claims with debt reporting. - TenantInsurance: per-tenant insurance contracts (coverage/franchise/ceiling, versioning, soft-deactivate) + active guard - ServiceItem.insuranceCovered + TenantServiceCoverage per-service overrides - Tariff: versioned yearly tariffs with fallback to ServiceItem price - Billing domain: Money/ShareBreakdown VOs, BillingCalculator (unit-tested), Invoice/InvoiceItem aggregate, InvoiceService.createFromSession - Claim/ClaimItem with state machine (pending->submitted->approved/rejected->paid), ClaimService, insurance-debt report - ClaimSubmitterInterface + ManualClaimSubmitter (future insurance API ready) - Admin UI: insurance-pricing page, claims page, service tariff modal, service insurance toggle; routes + sidebar entries - Architecture doc + billing/insurance/clinic-services API docs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -265,3 +265,190 @@ Remove an insurance from a doctor's list.
|
||||
```json
|
||||
{ "success": true, "data": { "message": "بیمه از لیست حذف شد" } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EntityInsurancePricing — قیمتگذاری ویزیت بر اساس بیمه
|
||||
|
||||
قیمتگذاری ویزیت برای entity جاری (پزشک یا کلینیک)، با تفکیک:
|
||||
- **ویزیت آزاد** (بدون بیمه) — یک مبلغ پایه (ردیفی با `insurance_id = null`)
|
||||
- **سهم بیمار به ازای هر بیمه** پایه/مکمل
|
||||
|
||||
entity جاری از `#[CurrentUser]` resolve میشود: نقش `ROLE_DOCTOR` → `doctor`، نقش `ROLE_CLINIC` → `clinic`. ذخیرهسازی polymorphic در جدول `entity_insurance_pricing` (`entity_type`, `entity_id`, `insurance_id` nullable, `patient_share_rials`).
|
||||
|
||||
---
|
||||
|
||||
## GET `/api/v1/insurance-pricing`
|
||||
|
||||
قیمتگذاری بیمهی entity جاری + لیست همهی بیمههای فعال (با سهم بیمار اگر تعیین شده).
|
||||
|
||||
**Permission:** `AUTH` (`ROLE_DOCTOR` یا `ROLE_CLINIC`)
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"entity_type": "doctor",
|
||||
"entity_id": 7,
|
||||
"free_visit_price_rials": 5000000,
|
||||
"insurances": [
|
||||
{
|
||||
"insurance_id": 3,
|
||||
"insurance_name": "تأمین اجتماعی",
|
||||
"type": "basic",
|
||||
"patient_share_rials": 1500000
|
||||
},
|
||||
{
|
||||
"insurance_id": 9,
|
||||
"insurance_name": "دانا",
|
||||
"type": "supplementary",
|
||||
"patient_share_rials": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `patient_share_rials = null` یعنی این بیمه پذیرفته نمیشود (قیمتگذاری ندارد).
|
||||
|
||||
### خطاها
|
||||
- `403` `ERR_FORBIDDEN_001` — پروفایل (doctor/clinic) برای کاربر یافت نشد.
|
||||
|
||||
---
|
||||
|
||||
## PUT `/api/v1/insurance-pricing`
|
||||
|
||||
ذخیره/بهروزرسانی قیمت ویزیت آزاد و سهم بیمار هر بیمه. عملیات upsert؛ ردیفی که `patient_share_rials = null` بفرستد حذف میشود.
|
||||
|
||||
**Permission:** `AUTH` (`ROLE_DOCTOR` یا `ROLE_CLINIC`)
|
||||
|
||||
### Request Body
|
||||
```json
|
||||
{
|
||||
"free_visit_price_rials": 5000000,
|
||||
"insurances": [
|
||||
{ "insurance_id": 3, "patient_share_rials": 1500000 },
|
||||
{ "insurance_id": 9, "patient_share_rials": null }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `free_visit_price_rials` | int | مبلغ ویزیت آزاد (ریال). اختیاری؛ اگر نباشد تغییر نمیکند. |
|
||||
| `insurances[].insurance_id` | int | شناسهی بیمه (الزامی برای هر ردیف). |
|
||||
| `insurances[].patient_share_rials` | int \| null | سهم بیمار با این بیمه. `null` → ردیف حذف میشود. |
|
||||
|
||||
### Response `200`
|
||||
همان ساختار `GET /api/v1/insurance-pricing` (وضعیت پس از ذخیره).
|
||||
|
||||
### خطاها
|
||||
- `403` `ERR_FORBIDDEN_001` — پروفایل یافت نشد.
|
||||
|
||||
---
|
||||
|
||||
## TenantInsurance — قراردادهای بیمهی tenant (فاز ۱ سیستم صورتحساب)
|
||||
|
||||
قرارداد یک پزشک/کلینیک با یک بیمه: درصد پوشش، فرانشیز، سقف تعهد سالانه، نسخهبندی و وضعیت فعال. مبنای محاسبهی سهم در سیستم صورتحساب (`docs/architecture/insurance-billing-system.md`). tenant از `#[CurrentUser]` (`ROLE_DOCTOR`→doctor، `ROLE_CLINIC`→clinic). جدول `tenant_insurances`.
|
||||
|
||||
### GET `/api/v1/billing/tenant-insurances`
|
||||
لیست قراردادهای فعال tenant جاری.
|
||||
|
||||
**Permission:** `AUTH` (doctor/clinic)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"uuid": "…",
|
||||
"insurance_id": 3,
|
||||
"insurance_name": "تأمین اجتماعی",
|
||||
"insurance_kind": "basic",
|
||||
"version": 1,
|
||||
"is_active": true,
|
||||
"coverage_percent": 70,
|
||||
"franchise_rials": 0,
|
||||
"annual_ceiling_rials": null,
|
||||
"effective_from": 1718900000,
|
||||
"effective_to": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/v1/billing/tenant-insurances`
|
||||
فعالسازی/بهروزرسانی قرارداد. اگر قرارداد فعالی برای آن بیمه باشد ویرایش میشود، وگرنه نسخهی جدید.
|
||||
|
||||
**Body:**
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `insurance_id` | int | الزامی |
|
||||
| `coverage_percent` | float | درصد پوشش (۰–۱۰۰) |
|
||||
| `franchise_rials` | int | فرانشیز ثابت سهم بیمار |
|
||||
| `annual_ceiling_rials` | int \| null | سقف تعهد (null = بینهایت) |
|
||||
|
||||
پاسخ `201`: `{ success, data: { …contract } }`.
|
||||
خطاها: `404 ERR_NOT_FOUND_001` بیمه یافت نشد · `422 ERR_VALIDATION_001` insurance_id الزامی · `403 ERR_FORBIDDEN_001` پروفایل یافت نشد.
|
||||
|
||||
### PATCH `/api/v1/billing/tenant-insurances/{uuid}`
|
||||
ویرایش `coverage_percent` / `franchise_rials` / `annual_ceiling_rials`. فقط قرارداد متعلق به tenant جاری.
|
||||
|
||||
### DELETE `/api/v1/billing/tenant-insurances/{uuid}`
|
||||
غیرفعالسازی نرم (soft) — `is_active=false` و `effective_to=now`. داده حذف نمیشود.
|
||||
|
||||
```json
|
||||
{ "success": true, "data": { "message": "قرارداد بیمه غیرفعال شد" } }
|
||||
```
|
||||
|
||||
> **Guard:** `TenantInsuranceService::assertActive()` هنگام پذیرش/صورتحساب فقط بیمههای فعالِ همان tenant را مجاز میداند؛ در غیر این صورت `422 ERR_VALIDATION_001` («این بیمه برای این کلینیک/پزشک فعال نیست»).
|
||||
|
||||
---
|
||||
|
||||
## TenantServiceCoverage — پوشش خدمت تحت یک قرارداد بیمه (فاز ۲)
|
||||
|
||||
override پوشش یک خدمت خاص تحت قرارداد یک بیمه. فیلدهای `null` از خود قرارداد ارث میبرند. اگر `covered=false` → آن خدمت تحت آن بیمه پوشش ندارد. جدول `tenant_service_coverage`.
|
||||
|
||||
### GET `/api/v1/billing/tenant-insurances/{uuid}/service-coverage`
|
||||
لیست overrideهای پوشش خدمات یک قرارداد.
|
||||
|
||||
**Permission:** `AUTH` (مالک قرارداد)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"uuid": "…",
|
||||
"tenant_insurance_id": 4,
|
||||
"service_item_id": 12,
|
||||
"covered": true,
|
||||
"coverage_percent": 80,
|
||||
"franchise_rials": null,
|
||||
"ceiling_rials": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PUT `/api/v1/billing/tenant-insurances/{uuid}/service-coverage`
|
||||
تنظیم/بهروزرسانی پوشش یک خدمت (upsert).
|
||||
|
||||
**Body:**
|
||||
| فیلد | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| `service_item_id` | int | الزامی |
|
||||
| `covered` | bool | پیشفرض true |
|
||||
| `coverage_percent` | float \| null | null = ارث از قرارداد |
|
||||
| `franchise_rials` | int \| null | null = ارث از قرارداد |
|
||||
| `ceiling_rials` | int \| null | null = ارث از قرارداد |
|
||||
|
||||
پاسخ `200`: `{ success, data: { message } }`.
|
||||
خطاها: `404 ERR_NOT_FOUND_001` قرارداد یافت نشد · `422 ERR_VALIDATION_001` service_item_id الزامی.
|
||||
|
||||
> منطق resolve: `TenantInsuranceService::coverageRuleForService()` ابتدا override خدمت را بررسی میکند؛ اگر `covered=false` → `CoverageRule::notCovered()`؛ در غیر این صورت فیلدهای null از قرارداد پر میشوند. این `CoverageRule` در فاز ۴ ورودی `BillingCalculator` است.
|
||||
|
||||
Reference in New Issue
Block a user