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>
13 KiB
Insurance API
Prefix:
/api/v1/insurances,/api/v1/insurance,/api/v1/admin/insurance
Two resource types:
- Insurance — master list of insurance companies managed by admin
- 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
{
"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
{
"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)
{
"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
{ "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
{
"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)
{
"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
{
"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
{ "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
{ "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
{
"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یعنی این بیمه پذیرفته نمیشود (قیمتگذاری ندارد).
خطاها
403ERR_FORBIDDEN_001— پروفایل (doctor/clinic) برای کاربر یافت نشد.
PUT /api/v1/insurance-pricing
ذخیره/بهروزرسانی قیمت ویزیت آزاد و سهم بیمار هر بیمه. عملیات upsert؛ ردیفی که patient_share_rials = null بفرستد حذف میشود.
Permission: AUTH (ROLE_DOCTOR یا ROLE_CLINIC)
Request Body
{
"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 (وضعیت پس از ذخیره).
خطاها
403ERR_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)
{
"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. داده حذف نمیشود.
{ "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 (مالک قرارداد)
{
"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است.