Files
clinicpro/docs/api/insurance.md
T
hamedandClaude Opus 4.8 89191eee57 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>
2026-06-23 15:05:24 +03:30

455 lines
13 KiB
Markdown

# Insurance API
> **Prefix:** `/api/v1/insurances`, `/api/v1/insurance`, `/api/v1/admin/insurance`
Two resource types:
1. **Insurance** — master list of insurance companies managed by admin
2. **DoctorInsurance** — a doctor's acceptance of a specific insurance (with optional price)
---
## GET `/api/v1/insurances`
List all active insurances.
**Permission:** `PUBLIC`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | ❌ | `"basic"` or `"supplementary"` |
### Response `200`
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "بیمه تأمین اجتماعی",
"type": "basic",
"logo_url": "https://...",
"status": "active"
},
{
"id": 2,
"name": "بیمه ایران",
"type": "supplementary",
"logo_url": "https://...",
"status": "active"
}
]
}
```
---
## GET `/api/v1/admin/insurances`
List all insurances with pagination (admin view — includes inactive).
**Permission:** `ROLE_ADMIN`
### Query Parameters
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | ❌ | Default: 1 |
| `limit` | integer | ❌ | Default: 20 |
| `search` | string | ❌ | Search in name |
| `type` | string | ❌ | `"basic"` or `"supplementary"` |
### Response `200`
```json
{
"success": true,
"data": [ ... ],
"meta": { "totalRecords": 15, "totalPages": 1, "currentPage": 1 }
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
---
## POST `/api/v1/admin/insurance`
Create a new insurance.
**Permission:** `ROLE_ADMIN`
### Request Body (`application/json`)
```json
{
"name": "بیمه تأمین اجتماعی",
"type": "basic",
"logo_url": "https://...",
"status": "active"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | ✅ | Insurance name |
| `type` | string | ✅ | `"basic"` or `"supplementary"` |
| `logo_url` | string | ❌ | Logo image URL |
| `status` | string | ❌ | `"active"` (default) or `"inactive"` |
### Response `201`
Insurance object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_VALIDATION_002` | 422 | Missing required field |
---
## PATCH `/api/v1/admin/insurance/{id}`
Update an insurance.
**Permission:** `ROLE_ADMIN`
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `id` | integer | Insurance ID |
All body fields optional.
### Response `200`
Updated insurance object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Insurance not found |
---
## DELETE `/api/v1/admin/insurance/{id}`
Delete an insurance.
**Permission:** `ROLE_ADMIN`
### Response `200`
```json
{ "success": true, "data": { "message": "بیمه حذف شد" } }
```
---
## POST `/api/v1/admin/insurance/{id}/upload-logo`
Upload insurance logo.
**Permission:** `ROLE_ADMIN`
### Request
`Content-Type: multipart/form-data`
| Field | Type | Required |
|-------|------|----------|
| `file` | binary | ✅ |
### Response `200`
```json
{
"success": true,
"data": {
"url": "https://...",
"uuid": "...",
"filename": "insurance_logo.png",
"filemime": "image/png",
"filesize": 51200
}
}
```
---
## POST `/api/v1/insurance/`
Add an insurance to a doctor's accepted list.
**Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.create` permission)
### Request Body (`application/json`)
```json
{
"doctor_id": 42,
"insurance_id": 1,
"price": 150000
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `doctor_id` | integer | ✅ | Doctor's numeric ID |
| `insurance_id` | integer | ✅ | Insurance ID |
| `price` | integer | ❌ | Visit price for this insurance (Rials) |
### Response `201`
```json
{
"success": true,
"data": {
"id": 10,
"doctor_id": 42,
"insurance": { "id": 1, "name": "بیمه تأمین اجتماعی", "type": "basic" },
"price": 150000
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the doctor |
| `ERR_NOT_FOUND_001` | 404 | Doctor or insurance not found |
| `ERR_CONFLICT_001` | 409 | Insurance already added to doctor |
---
## GET `/api/v1/insurance/{id}`
Get a doctor-insurance link.
**Permission:** `PUBLIC`
### Response `200`
DoctorInsurance object.
---
## PATCH `/api/v1/insurance/{id}`
Update a doctor-insurance (e.g., change price).
**Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.update` permission)
### Request Body
```json
{ "price": 200000 }
```
### Response `200`
Updated DoctorInsurance object.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_FORBIDDEN_001` | 403 | Not the doctor |
| `ERR_NOT_FOUND_001` | 404 | Link not found |
---
## DELETE `/api/v1/insurance/{id}`
Remove an insurance from a doctor's list.
**Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.delete` permission)
### Response `200`
```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` است.