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:
@@ -0,0 +1,144 @@
|
||||
# Billing API — صورتحساب (فاز ۴ سیستم صورتحساب/بیمه)
|
||||
|
||||
> **Prefix:** `/api/v1/billing`
|
||||
> دامنه: `App\Billing`. مرجع معماری: `docs/architecture/insurance-billing-system.md`.
|
||||
> tenant از `#[CurrentUser]` resolve میشود (`ROLE_DOCTOR`→doctor، `ROLE_CLINIC`→clinic).
|
||||
|
||||
صورتحساب (`Invoice`) از یک Encounter (`PatientSession`) ساخته میشود. برای هر آیتم سهم بیمهی پایه، بیمهی مکمل و بیمار با `BillingCalculator` محاسبه میشود:
|
||||
|
||||
- تعرفهی خدمت از `Tariff` سال جاری (با fallback به `ServiceItem.priceRials`).
|
||||
- قانون پوشش از قرارداد بیمهی tenant (`TenantInsurance`) + override خدمت (`TenantServiceCoverage`).
|
||||
- ترتیب محاسبه: کل → پوشش پایه (با سقف) → باقیمانده → پوشش مکمل → فرانشیز سهم بیمار.
|
||||
|
||||
نمونه: کل ۶۰۰٬۰۰۰ · پایه ۷۰٪ → ۴۲۰٬۰۰۰ · مکمل روی باقیمانده → ۱۲۰٬۰۰۰ · بیمار ۶۰٬۰۰۰.
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/billing/invoices
|
||||
|
||||
ساخت صورتحساب از یک مراجعه. اگر صورتحساب برای آن مراجعه قبلاً ساخته شده، همان برگردانده میشود (idempotent).
|
||||
|
||||
**Permission:** `AUTH` (مالک مراجعه)
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{ "session_uuid": "…" }
|
||||
```
|
||||
|
||||
**Response 201:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": {
|
||||
"uuid": "…",
|
||||
"entity_type": "doctor",
|
||||
"entity_id": 7,
|
||||
"patient_session_id": 33,
|
||||
"base_insurance_id": 3,
|
||||
"supplementary_insurance_id": 9,
|
||||
"total_rials": 600000,
|
||||
"base_insurance_rials": 420000,
|
||||
"supplementary_rials": 120000,
|
||||
"patient_rials": 60000,
|
||||
"status": "draft",
|
||||
"issued_at": 1718900000,
|
||||
"items": [
|
||||
{
|
||||
"uuid": "…",
|
||||
"service_item_id": 12,
|
||||
"title": "ویزیت",
|
||||
"tariff_rials": 600000,
|
||||
"quantity": 1,
|
||||
"total_rials": 600000,
|
||||
"base_insurance_rials": 420000,
|
||||
"supplementary_rials": 120000,
|
||||
"patient_rials": 60000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:** `422 ERR_VALIDATION_001` session_uuid الزامی · `404 ERR_NOT_FOUND_001` مراجعه یافت نشد · `403 ERR_FORBIDDEN_001` پروفایل یافت نشد.
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/billing/invoices/{uuid}
|
||||
|
||||
دریافت صورتحساب (فقط مالک tenant).
|
||||
|
||||
**Response 200:** همان ساختار بالا.
|
||||
**Errors:** `404 ERR_NOT_FOUND_001`.
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/billing/invoices/{uuid}/finalize
|
||||
|
||||
نهاییسازی صورتحساب (`draft` → `finalized`). صورتحساب نهاییشده مبنای ساخت Claim (فاز ۵) است.
|
||||
|
||||
**Response 200:** صورتحساب با `status: "finalized"`.
|
||||
**Errors:** `404 ERR_NOT_FOUND_001`.
|
||||
|
||||
---
|
||||
|
||||
## وضعیتهای Invoice
|
||||
`draft` (پیشنویس، قابل بازسازی) → `finalized` (نهایی) → `paid` (پرداختشده) · `void` (باطل).
|
||||
|
||||
## نکات
|
||||
- پول: integer ریال. تاریخ: Unix timestamp.
|
||||
- `BillingCalculator` خالص و واحد-تستشده است (`tests/Billing/BillingCalculatorTest.php`).
|
||||
- این فاز جایگزین تدریجی `PatientService::calculateFinalPrice` است؛ آن متد فعلاً برای سازگاری باقی مانده.
|
||||
|
||||
---
|
||||
|
||||
# Claims — مطالبات بیمه (فاز ۵)
|
||||
|
||||
مطالبه (`Claim`) از یک صورتحساب **نهاییشده** ساخته میشود: یک Claim برای بیمهی پایه و یک Claim برای بیمهی مکمل (فقط اگر سهم بیمه > ۰). هر `ClaimItem` به یک `InvoiceItem` ارجاع میدهد. چرخهی وضعیت با state machine.
|
||||
|
||||
**وضعیتها:** `pending → submitted → {approved → paid | rejected}`. از `paid`/`rejected` خروجی ندارد.
|
||||
|
||||
## POST /api/v1/billing/claims
|
||||
ساخت مطالبات از یک صورتحساب نهاییشده.
|
||||
|
||||
**Body:** `{ "invoice_uuid": "…" }`
|
||||
**Response 201:** `{ success, data: [ …claims ] }` (یک یا دو مطالبه: پایه/مکمل).
|
||||
**Errors:** `422` صورتحساب نهایی نشده / سهم بیمه ندارد · `404` صورتحساب یافت نشد.
|
||||
|
||||
## GET /api/v1/billing/claims?status=
|
||||
لیست مطالبات tenant. فیلتر اختیاری `status` (`pending|submitted|approved|rejected|paid`).
|
||||
|
||||
## POST /api/v1/billing/claims/{uuid}/{action}
|
||||
انتقال وضعیت. `action` ∈ `submit|approve|reject|pay`.
|
||||
|
||||
| action | body اختیاری | اثر |
|
||||
|--------|--------------|-----|
|
||||
| submit | — | pending → submitted |
|
||||
| approve | `approved_rials` | submitted → approved (پیشفرض = کل ادعا) |
|
||||
| reject | `reason` (الزامی) | submitted → rejected |
|
||||
| pay | `paid_rials` | approved → paid (پیشفرض = approved) |
|
||||
|
||||
**Errors:** `422` انتقال نامعتبر یا دلیل رد خالی · `404` مطالبه یافت نشد.
|
||||
|
||||
## GET /api/v1/billing/reports/insurance-debt
|
||||
گزارش بدهی بیمهها برای tenant (group بر اساس بیمه).
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": [
|
||||
{ "insurance_id": 3, "claimed": 840000, "approved": 800000, "paid": 500000, "debt": 340000 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
`debt = claimed - paid` (حداقل صفر).
|
||||
|
||||
---
|
||||
|
||||
## ارسال مطالبه (ClaimSubmitter)
|
||||
|
||||
عملِ `submit` از طریق interface `App\Billing\Contract\ClaimSubmitterInterface` انجام میشود. پیادهسازی پیشفرض `ManualClaimSubmitter` است (ارسال دستی/آفلاین — همیشه موفق). برای اتصال آینده به API شرکتهای بیمهی ایران کافی است یک پیادهسازی جدید از این interface ساخته و در `config/services.yaml` bind شود؛ `ClaimService` تغییر نمیکند (Dependency Inversion). اگر `submit` ناموفق باشد، انتقال وضعیت با `422` متوقف میشود.
|
||||
@@ -111,7 +111,9 @@
|
||||
"section_uuid": "...",
|
||||
"name": "رادیوگرافی مستقیم",
|
||||
"price_rials": 500000,
|
||||
"staff_uuid": "..."
|
||||
"staff_uuid": "...",
|
||||
"insurance_covered": true,
|
||||
"insurance_price_rials": 200000
|
||||
}
|
||||
```
|
||||
|
||||
@@ -121,8 +123,10 @@
|
||||
| name | string | ✅ |
|
||||
| price_rials | integer | ❌ (پیشفرض 0) |
|
||||
| staff_uuid | UUID | ❌ |
|
||||
| insurance_covered | boolean | ❌ (پیشفرض false) — آیا خدمت شامل بیمه میشود |
|
||||
| insurance_price_rials | integer\|null | ❌ — سهم/قیمت بیمار با بیمه |
|
||||
|
||||
**Response 201:** ServiceItem object
|
||||
**Response 201:** ServiceItem object (شامل `insurance_covered` و `insurance_price_rials`)
|
||||
|
||||
---
|
||||
|
||||
@@ -135,7 +139,9 @@
|
||||
"name": "رادیوگرافی دیجیتال",
|
||||
"price_rials": 600000,
|
||||
"staff_uuid": null,
|
||||
"active": false
|
||||
"active": false,
|
||||
"insurance_covered": true,
|
||||
"insurance_price_rials": 250000
|
||||
}
|
||||
```
|
||||
|
||||
@@ -160,3 +166,46 @@
|
||||
| ERR_SUBSCRIPTION_REQUIRED | 403 | نیاز به پنل Basic+ |
|
||||
| ERR_SERVICE_NOT_FOUND | 404 | سرویس یافت نشد |
|
||||
| ERR_SERVICE_ITEM_IN_USE | 409 | سرویس در پرونده بیمار استفاده شده |
|
||||
|
||||
---
|
||||
|
||||
## تعرفهی نسخهدار سالانه (Tariff) — فاز ۳ سیستم صورتحساب
|
||||
|
||||
هر خدمت میتواند برای هر سال شمسی یک تعرفه داشته باشد. اگر تعرفهی سالی ثبت نشود، به `price_rials` خود خدمت fallback میشود (`TariffService::resolvePrice`). سال جاری شمسی سمت سرور با `IntlDateFormatter` (تقویم persian) محاسبه میشود.
|
||||
|
||||
### GET /api/v1/service-items/{uuid}/tariffs
|
||||
|
||||
لیست تعرفههای یک خدمت + قیمت پیشفرض + سال جاری.
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY` (مالک خدمت)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"current_year": 1405,
|
||||
"default_price_rials": 500000,
|
||||
"data": [
|
||||
{ "uuid": "…", "service_item_id": 12, "year": 1405, "price_rials": 600000, "is_active": true },
|
||||
{ "uuid": "…", "service_item_id": 12, "year": 1404, "price_rials": 500000, "is_active": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /api/v1/service-items/{uuid}/tariffs/{year}
|
||||
|
||||
ثبت/بهروزرسانی تعرفهی یک سال (upsert). `year` بین ۱۳۹۰ تا ۱۵۰۰.
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{ "price_rials": 600000 }
|
||||
```
|
||||
|
||||
**Response 200:** `{ success, data: { …tariff } }`
|
||||
|
||||
**Errors:**
|
||||
| Code | HTTP | توضیح |
|
||||
|------|------|-------|
|
||||
| ERR_SERVICE_NOT_FOUND | 404 | سرویس یافت نشد |
|
||||
| ERR_VALIDATION_001 | 422 | سال نامعتبر |
|
||||
|
||||
@@ -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` است.
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
# سیستم صورتحساب و بیمه (Insurance & Medical Billing) — سند معماری
|
||||
|
||||
> وضعیت: **طرح معماری (Design)** — هنوز پیادهسازی نشده. بر اساس کد فعلی Clinic Pro (Symfony 7.4 / PHP 8.2+ / Doctrine / MariaDB) و الگوهای موجود نوشته شده.
|
||||
> دامنههای جدید پیشنهادی: `App\Billing\*` (Invoice/InvoiceItem/Claim/Tariff) و توسعهی `App\Insurance\*` (TenantInsurance/Coverage).
|
||||
|
||||
---
|
||||
|
||||
## ۰. تطبیق با کد فعلی (مبنای واقعی، نه greenfield)
|
||||
|
||||
| مفهوم پرامپت | معادل واقعی در Clinic Pro | تصمیم |
|
||||
|---|---|---|
|
||||
| **Tenant** | موجودیت مستقل وجود ندارد. tenancy بهصورت polymorphic `(entityType ∈ {doctor, clinic}, entityId)` پیاده شده (الگوی `PatientRecord`, `EntityInsurancePricing`, `SmsSettings`, `SubscriptionService`). | یک **Value Object `TenantRef(type, id)`** معرفی میشود؛ موجودیت Tenant جدید **ساخته نمیشود**. resolve از `#[CurrentUser]` مثل `PatientController::resolveEntity`. |
|
||||
| **InsuranceCompany** | `App\Insurance\Entity\Insurance` (لیست master سراسری، مدیریتشده توسط admin، دارای `type: InsuranceType{basic,supplementary}`). | حفظ میشود بهعنوان **master سراسری**. tenant آن را «فعال» میکند، تعریف نمیکند. |
|
||||
| **TenantInsurance** | `EntityInsurancePricing` فعلی (polymorphic، فقط `patient_share_rials`) — **ناکافی**. | به **`TenantInsurance`** ارتقا مییابد (قرارداد tenant↔insurance با نسخه و وضعیت). `EntityInsurancePricing` migrate میشود. |
|
||||
| **BaseInsurance / SupplementaryInsurance** | `InsuranceType` enum (`basic`/`supplementary`). | همان enum؛ موجودیت جدا لازم نیست (نوع روی `Insurance` است). |
|
||||
| **Service** | `App\ClinicService\Entity\ServiceItem` (دارای `priceRials`, `active`, متعلق به `ServiceSection`). | حفظ + افزودن `insuranceCovered`. تعرفه به `Tariff` منتقل میشود. |
|
||||
| **Tariff** | وجود ندارد (قیمت مستقیم روی `ServiceItem.priceRials`). | موجودیت جدید `Tariff` با **نسخهبندی سالانه** (effective year/range). |
|
||||
| **Encounter** | `App\Patient\Entity\PatientSession` (مراجعه؛ دارای فیلدهای بیمه/قیمت ساده). | `PatientSession` نقش Encounter را دارد؛ **Invoice** به آن متصل میشود. |
|
||||
| **Invoice / InvoiceItem** | وجود ندارد (محاسبهی ساده در `PatientService::calculateFinalPrice`). | موجودیتهای جدید `Invoice`/`InvoiceItem` با تفکیک سهم بیمار/پایه/مکمل. |
|
||||
| **Claim** | وجود ندارد. | موجودیت جدید با state machine (الگوی `Appointment::transitionTo`). |
|
||||
| **Patient** | `PatientRecord` (+ `UserProfile` برای دموگرافیک/بیمهی بیمار). | حفظ. بیمهی بیمار از `UserProfile.basicInsuranceId/supplementaryInsuranceId`. |
|
||||
| **Payment** | `App\Payment\Entity\Payment` (درگاه پرداخت). | سهم بیمار از Invoice → میتواند به Payment وصل شود. |
|
||||
|
||||
**اصول حفظشده:** پول = `int` ریال؛ تاریخ = Unix timestamp (نه DateTime)؛ کنترلرها از `BaseController` (`success/paginated/error`)؛ لیست admin با DQL array hydration؛ خطاها در `ErrorCodes`؛ multi-tenant با `(entityType, entityId)`.
|
||||
|
||||
---
|
||||
|
||||
## ۱. Bounded Contexts
|
||||
|
||||
```
|
||||
Insurance (master + tenant contracts + coverage)
|
||||
Insurance (master, global, admin) ← موجود
|
||||
TenantInsurance (قرارداد tenant↔insurance) ← ارتقای EntityInsurancePricing
|
||||
InsuranceCoverage (قانون پوشش هر بیمه)
|
||||
TenantServiceCoverage (override پوشش یک خدمت خاص)
|
||||
|
||||
Billing (encounter → invoice → claim)
|
||||
Tariff (تعرفهی نسخهدار سالانه)
|
||||
Invoice / InvoiceItem
|
||||
Claim (چرخهی مطالبات بیمه)
|
||||
BillingCalculator (دامنهی محاسبه — Domain Service)
|
||||
|
||||
Patient (موجود) ClinicService (موجود + insuranceCovered)
|
||||
Subscription (موجود — gate ویژگی billing)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۲. Domain Model — موجودیتها و فیلدها
|
||||
|
||||
### 2.1 Value Objects
|
||||
|
||||
```php
|
||||
// App\Shared\ValueObject\TenantRef
|
||||
final readonly class TenantRef
|
||||
{
|
||||
public function __construct(public string $type, public int $id) {} // type: 'doctor'|'clinic'
|
||||
public static function doctor(int $id): self { return new self('doctor', $id); }
|
||||
public static function clinic(int $id): self { return new self('clinic', $id); }
|
||||
}
|
||||
|
||||
// App\Billing\ValueObject\Money (ریال، صحیح، بدون منفی)
|
||||
final readonly class Money
|
||||
{
|
||||
public function __construct(public int $rials) { if ($rials < 0) throw new \InvalidArgumentException('negative money'); }
|
||||
public function add(Money $o): self { return new self($this->rials + $o->rials); }
|
||||
public function sub(Money $o): self { return new self(max(0, $this->rials - $o->rials)); }
|
||||
public function percent(float $p): self { return new self((int) round($this->rials * $p / 100)); }
|
||||
public function min(Money $o): self { return new self(min($this->rials, $o->rials)); }
|
||||
}
|
||||
|
||||
// App\Billing\ValueObject\ShareBreakdown (خروجی محاسبهی یک آیتم)
|
||||
final readonly class ShareBreakdown
|
||||
{
|
||||
public function __construct(
|
||||
public int $totalRials,
|
||||
public int $baseInsuranceRials,
|
||||
public int $supplementaryRials,
|
||||
public int $patientRials,
|
||||
) {}
|
||||
}
|
||||
|
||||
// App\Insurance\ValueObject\CoverageRule
|
||||
final readonly class CoverageRule
|
||||
{
|
||||
public function __construct(
|
||||
public float $coveragePercent, // درصد پوشش این بیمه
|
||||
public int $franchiseRials, // فرانشیز ثابت سهم بیمار
|
||||
public ?int $ceilingRials, // سقف تعهد هر آیتم (null = بینهایت)
|
||||
public bool $covered = true,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Entities (جداول)
|
||||
|
||||
#### TenantInsurance (ارتقای EntityInsurancePricing)
|
||||
قرارداد یک tenant با یک بیمه. نسخهدار، قابل غیرفعالسازی بدون حذف.
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| id | int PK | |
|
||||
| uuid | string(36) unique | |
|
||||
| entity_type | string(10) | `doctor`/`clinic` |
|
||||
| entity_id | int | |
|
||||
| insurance_id | int FK→insurances | |
|
||||
| version | int | نسخهی قرارداد (۱، ۲، …) |
|
||||
| is_active | bool | غیرفعال بدون حذف |
|
||||
| coverage_percent | decimal(5,2) | پوشش پیشفرض قرارداد |
|
||||
| franchise_rials | int | فرانشیز پیشفرض |
|
||||
| annual_ceiling_rials | int null | سقف تعهد سالانهی بیمار/بیمه |
|
||||
| effective_from | int (unix) | شروع اعتبار قرارداد |
|
||||
| effective_to | int null | پایان (null = جاری) |
|
||||
| created_at / updated_at | int | |
|
||||
|
||||
UniqueConstraint: `(entity_type, entity_id, insurance_id, version)`. Index: `(entity_type, entity_id, is_active)`.
|
||||
|
||||
#### TenantServiceCoverage
|
||||
override پوشش یک خدمت خاص تحت یک بیمهی tenant (آیا پوشش دارد + درصد/فرانشیز/سقف اختصاصی).
|
||||
|
||||
| ستون | نوع |
|
||||
|---|---|
|
||||
| id, uuid | |
|
||||
| tenant_insurance_id | int FK→tenant_insurances |
|
||||
| service_item_id | int FK→service_items |
|
||||
| covered | bool |
|
||||
| coverage_percent | decimal(5,2) null (null=ارث از قرارداد) |
|
||||
| franchise_rials | int null |
|
||||
| ceiling_rials | int null |
|
||||
|
||||
UniqueConstraint: `(tenant_insurance_id, service_item_id)`.
|
||||
|
||||
#### Tariff (تعرفهی نسخهدار)
|
||||
تعرفهی یک خدمت برای یک سال. تاریخچه حفظ میشود.
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| id, uuid | | |
|
||||
| service_item_id | int FK | |
|
||||
| year | smallint | سال شمسی (۱۴۰۳ …) |
|
||||
| price_rials | int | تعرفهی آن سال |
|
||||
| effective_from / effective_to | int (unix) | بازهی اعتبار |
|
||||
| is_active | bool | |
|
||||
|
||||
UniqueConstraint: `(service_item_id, year)`. `ServiceItem.priceRials` بهعنوان تعرفهی پیشفرض/جاری باقی میماند (fallback).
|
||||
|
||||
#### Invoice
|
||||
صورتحساب یک Encounter (`PatientSession`).
|
||||
|
||||
| ستون | نوع |
|
||||
|---|---|
|
||||
| id, uuid | |
|
||||
| entity_type, entity_id | tenant |
|
||||
| patient_session_id | int FK→patient_sessions (Encounter) |
|
||||
| patient_record_id | int FK |
|
||||
| base_insurance_id | int null |
|
||||
| supplementary_insurance_id | int null |
|
||||
| total_rials | int |
|
||||
| base_insurance_rials | int |
|
||||
| supplementary_rials | int |
|
||||
| patient_rials | int |
|
||||
| status | string(15) `draft\|finalized\|paid\|void` |
|
||||
| issued_at | int (unix) |
|
||||
| created_at, updated_at | int |
|
||||
|
||||
#### InvoiceItem
|
||||
یک خط صورتحساب (خدمت/ویزیت).
|
||||
|
||||
| ستون | نوع |
|
||||
|---|---|
|
||||
| id, uuid | |
|
||||
| invoice_id | int FK |
|
||||
| service_item_id | int null (ویزیت میتواند null باشد) |
|
||||
| title | string |
|
||||
| service_code | string null |
|
||||
| tariff_rials | int (تعرفهی واحد) |
|
||||
| quantity | int |
|
||||
| total_rials | int (= tariff × qty) |
|
||||
| base_coverage_percent | decimal(5,2) |
|
||||
| supp_coverage_percent | decimal(5,2) |
|
||||
| franchise_rials | int |
|
||||
| ceiling_rials | int null |
|
||||
| base_insurance_rials | int |
|
||||
| supplementary_rials | int |
|
||||
| patient_rials | int |
|
||||
|
||||
#### Claim (مطالبهی بیمه)
|
||||
گروهی از Invoiceها/InvoiceItemها که به یک بیمه ارسال میشود.
|
||||
|
||||
| ستون | نوع |
|
||||
|---|---|
|
||||
| id, uuid | |
|
||||
| entity_type, entity_id | tenant |
|
||||
| insurance_id | int FK |
|
||||
| insurance_kind | string `base\|supplementary` |
|
||||
| total_claimed_rials | int |
|
||||
| total_approved_rials | int null |
|
||||
| total_paid_rials | int null |
|
||||
| status | string(15) `pending\|submitted\|approved\|rejected\|paid` |
|
||||
| reject_reason | text null |
|
||||
| submitted_at / settled_at | int null |
|
||||
| created_at, updated_at | int |
|
||||
|
||||
#### ClaimItem
|
||||
ارتباط Claim ↔ InvoiceItem (many-to-many با مبلغ ادعاشده per item).
|
||||
|
||||
| ستون | نوع |
|
||||
|---|---|
|
||||
| id | |
|
||||
| claim_id | int FK |
|
||||
| invoice_item_id | int FK |
|
||||
| claimed_rials | int |
|
||||
| approved_rials | int null |
|
||||
|
||||
---
|
||||
|
||||
## ۳. Entity Diagram (ERD متنی)
|
||||
|
||||
```
|
||||
Insurance (master, global)
|
||||
▲ insurance_id
|
||||
│
|
||||
TenantInsurance ──< TenantServiceCoverage >── ServiceItem ──< Tariff
|
||||
(entity_type,id) │ service_item_id
|
||||
│
|
||||
PatientRecord ──< PatientSession(Encounter) ──1:1 Invoice ──< InvoiceItem
|
||||
│ ▲ invoice_item_id
|
||||
│ │
|
||||
base/supp ins ClaimItem >── Claim ── Insurance
|
||||
(entity_type,id)
|
||||
UserProfile.basicInsuranceId / supplementaryInsuranceId → بیمهی پیشفرض بیمار
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۴. Aggregates (DDD)
|
||||
|
||||
| Aggregate Root | شامل | Invariantها |
|
||||
|---|---|---|
|
||||
| **Invoice** | InvoiceItem[] | جمع سهمها = total هر آیتم؛ مجموع سطرها = total فاکتور؛ تغییر فقط در `draft`. |
|
||||
| **Claim** | ClaimItem[] | فقط InvoiceItemهای finalized؛ transition وضعیت طبق state machine؛ `approved_rials ≤ claimed_rials`. |
|
||||
| **TenantInsurance** | TenantServiceCoverage[] | فقط بیمههای فعالِ همان tenant؛ نسخهی جدید قبلی را غیرفعال نمیکند مگر صریح. |
|
||||
|
||||
Encounter (`PatientSession`) خارج از Aggregate صورتحساب است؛ Invoice به آن **ارجاع** میدهد (نه ownership).
|
||||
|
||||
---
|
||||
|
||||
## ۵. منطق محاسبهی سهم (Domain Service)
|
||||
|
||||
```php
|
||||
// App\Billing\Service\BillingCalculator
|
||||
final class BillingCalculator
|
||||
{
|
||||
/**
|
||||
* محاسبهی سهم برای یک آیتم.
|
||||
* ترتیب: کل → پوشش پایه (با سقف) → باقیماندهی بیمار → پوشش مکمل روی باقیمانده → فرانشیز ثابت سهم بیمار.
|
||||
*/
|
||||
public function calculateItem(
|
||||
Money $total,
|
||||
?CoverageRule $base, // قانون بیمهی پایه (یا null)
|
||||
?CoverageRule $supplementary, // قانون بیمهی مکمل (یا null)
|
||||
): ShareBreakdown {
|
||||
$baseShare = new Money(0);
|
||||
$remaining = $total;
|
||||
|
||||
if ($base !== null && $base->covered) {
|
||||
$baseShare = $total->percent($base->coveragePercent);
|
||||
if ($base->ceilingRials !== null) {
|
||||
$baseShare = $baseShare->min(new Money($base->ceilingRials));
|
||||
}
|
||||
$remaining = $total->sub($baseShare);
|
||||
}
|
||||
|
||||
$suppShare = new Money(0);
|
||||
if ($supplementary !== null && $supplementary->covered) {
|
||||
$suppShare = $remaining->percent($supplementary->coveragePercent);
|
||||
if ($supplementary->ceilingRials !== null) {
|
||||
$suppShare = $suppShare->min(new Money($supplementary->ceilingRials));
|
||||
}
|
||||
$remaining = $remaining->sub($suppShare);
|
||||
}
|
||||
|
||||
// فرانشیز همیشه سهم بیمار است (به remaining اضافه میشود، از سهم بیمه کم نمیکند مگر طراحی دیگر)
|
||||
$franchise = new Money(($base?->franchiseRials ?? 0) + ($supplementary?->franchiseRials ?? 0));
|
||||
$patient = $remaining->add($franchise)->min($total); // سهم بیمار از کل بیشتر نشود
|
||||
|
||||
// اصلاح: اگر فرانشیز باعث شد جمع > total شود، سهم بیمهها کم نمیشود؛ این Invariant باید تست شود.
|
||||
return new ShareBreakdown(
|
||||
totalRials: $total->rials,
|
||||
baseInsuranceRials: $baseShare->rials,
|
||||
supplementaryRials: $suppShare->rials,
|
||||
patientRials: $patient->rials,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**نمونه (مطابق پرامپت):** کل 600,000؛ پایه 70% → 420,000؛ مکمل روی باقیماندهی 180,000 با ≈66.7% → 120,000؛ سهم بیمار 60,000.
|
||||
|
||||
> نکتهی طراحی: تعامل فرانشیز و سقف باید با Test پوشش داده شود؛ منطق بالا یک baseline است و قابل تنظیم per-insurance.
|
||||
|
||||
---
|
||||
|
||||
## ۶. Repository / Service / DTO
|
||||
|
||||
**Repositoryها** (الگوی `ServiceEntityRepository` + `save/remove`):
|
||||
`TenantInsuranceRepository` (findActiveByTenant, findContractFor), `TenantServiceCoverageRepository`, `TariffRepository` (findForServiceYear), `InvoiceRepository`, `ClaimRepository` (findByStatus, debtReport).
|
||||
|
||||
**Service Layer:**
|
||||
- `TenantInsuranceService` — فعالسازی/غیرفعال/نسخهبندی قرارداد، گرفتن `CoverageRule` برای (tenant, insurance, serviceItem).
|
||||
- `BillingCalculator` — محاسبهی خالص (بالا).
|
||||
- `InvoiceService` — ساخت Invoice از Encounter: برای هر خدمت → resolve تعرفه (Tariff سال) + CoverageRule بیمههای بیمار → `BillingCalculator` → InvoiceItem؛ finalize.
|
||||
- `ClaimService` — گروهبندی InvoiceItemها بر اساس بیمه، ساخت Claim، transition وضعیت، گزارش بدهی.
|
||||
|
||||
**DTOها:** `CreateInvoiceRequest`, `InvoiceItemDTO`, `TenantInsuranceDTO`, `CoverageRuleDTO`, `ClaimDTO`, `BillingPreviewResponse` (پیشنمایش زندهی محاسبه قبل از ثبت).
|
||||
|
||||
---
|
||||
|
||||
## ۷. REST API (تحت `/api/v1`, `BaseController`, tenant از `#[CurrentUser]`)
|
||||
|
||||
### TenantInsurance (قراردادهای بیمهی tenant)
|
||||
- `GET /api/v1/billing/tenant-insurances` — لیست قراردادهای فعال tenant
|
||||
- `POST /api/v1/billing/tenant-insurances` — فعالسازی بیمه برای tenant (با coverage_percent/franchise/ceiling)
|
||||
- `PATCH /api/v1/billing/tenant-insurances/{uuid}` — ویرایش (نسخهی جدید)
|
||||
- `DELETE /api/v1/billing/tenant-insurances/{uuid}` — غیرفعالسازی (soft)
|
||||
- `PUT /api/v1/billing/tenant-insurances/{uuid}/service-coverage` — تنظیم پوشش خدمات
|
||||
|
||||
### Tariff
|
||||
- `GET /api/v1/billing/services/{serviceUuid}/tariffs`
|
||||
- `PUT /api/v1/billing/services/{serviceUuid}/tariffs/{year}` — ثبت/ویرایش تعرفهی سال
|
||||
|
||||
### Invoice
|
||||
- `POST /api/v1/billing/invoices/preview` — پیشنمایش محاسبه (بدون ذخیره)
|
||||
- `POST /api/v1/billing/invoices` — ساخت از Encounter
|
||||
- `GET /api/v1/billing/invoices/{uuid}`
|
||||
- `POST /api/v1/billing/invoices/{uuid}/finalize`
|
||||
|
||||
### Claim
|
||||
- `POST /api/v1/billing/claims` — ساخت از InvoiceItemهای finalized یک بیمه
|
||||
- `POST /api/v1/billing/claims/{uuid}/submit|approve|reject|pay`
|
||||
- `GET /api/v1/billing/claims?status=...`
|
||||
- `GET /api/v1/billing/reports/insurance-debt` — گزارش بدهی بیمهها
|
||||
|
||||
**Guard مهم (نیازمندی ۱۴):** هنگام پذیرش بیمار و ساخت Invoice، فقط بیمههایی مجازند که `TenantInsurance.is_active = true` برای همان tenant. در غیر این صورت `ERR_VALIDATION` («این بیمه برای این کلینیک فعال نیست»).
|
||||
|
||||
---
|
||||
|
||||
## ۸. Workflow
|
||||
|
||||
### ثبت صورتحساب
|
||||
```
|
||||
Encounter(PatientSession) ثبت میشود
|
||||
→ بیمهی بیمار از UserProfile یا انتخاب منشی (محدود به TenantInsurance فعال)
|
||||
→ InvoiceService.createFromEncounter:
|
||||
برای هر خدمت:
|
||||
tariff = TariffRepository.findForServiceYear(service, سالِ جاری) ?? service.priceRials
|
||||
coverage_base = TenantInsuranceService.coverageRule(tenant, baseIns, service)
|
||||
coverage_supp = TenantInsuranceService.coverageRule(tenant, suppIns, service)
|
||||
breakdown = BillingCalculator.calculateItem(...)
|
||||
→ InvoiceItem
|
||||
→ Invoice (status=draft) → finalize (status=finalized)
|
||||
```
|
||||
|
||||
### Claim
|
||||
```
|
||||
InvoiceItemهای finalized یک بیمه → ClaimService.create (status=pending)
|
||||
→ submit (submitted) → [پاسخ بیمه] approve/reject → pay (paid)
|
||||
گزارش بدهی = جمع claimed - paid بر اساس بیمه
|
||||
```
|
||||
|
||||
state machine Claim (الگوی `Appointment::canTransitionTo`):
|
||||
`pending → submitted → {approved → paid | rejected}`؛ از `paid`/`rejected` خروج ندارد.
|
||||
|
||||
---
|
||||
|
||||
## ۹. Events / Use Cases
|
||||
|
||||
**Domain Events:** `InvoiceFinalized`, `ClaimSubmitted`, `ClaimApproved`, `ClaimRejected`, `ClaimPaid`, `TenantInsuranceDeactivated`.
|
||||
کاربرد: SMS/notification به بیمار، بهروزرسانی گزارش بدهی، آینده: ارسال خودکار به API بیمه.
|
||||
|
||||
**Use Cases اصلی:** فعالسازی بیمه برای tenant؛ تنظیم پوشش خدمت؛ ثبت تعرفهی سالانه؛ پیشنمایش محاسبهی سهم؛ ثبت Invoice از Encounter؛ ساخت/پیگیری Claim؛ گزارش بدهی.
|
||||
|
||||
---
|
||||
|
||||
## ۱۰. آینده: اتصال به API بیمه (نیازمندی ۱۰)
|
||||
`ClaimSubmitter` بهصورت interface طراحی شود؛ پیادهسازی فعلی manual، آینده `IranInsuranceApiSubmitter` بدون تغییر در دامنه (Dependency Inversion).
|
||||
|
||||
---
|
||||
|
||||
## ۱۱. فازبندی پیادهسازی (پیشنهادی)
|
||||
|
||||
1. **فاز ۱ — قرارداد بیمه:** ✅ **انجام شد** — `TenantInsurance` (entity/repo/service/API/UI) + جدول `tenant_insurances` + guard `assertActive()` + `CoverageRule` VO. `EntityInsurancePricing` فعلاً برای ویزیت آزاد/سهم ساده باقی ماند (فاز ۴ ادغام). API: `/api/v1/billing/tenant-insurances` (GET/POST/PATCH/DELETE). UI: صفحهی «بیمه و قیمتگذاری».
|
||||
2. **فاز ۲ — پوشش خدمت:** ✅ **انجام شد** — `ServiceItem.insuranceCovered` + `insurancePriceRials` + موجودیت `TenantServiceCoverage` (override per-service per-contract) + `coverageRuleForService()` (resolve با ارث از قرارداد) + API `…/service-coverage` (GET/PUT) + UI toggle «شامل بیمه» در صفحهی خدمات.
|
||||
3. **فاز ۳ — تعرفه:** ✅ **انجام شد** — موجودیت `Tariff` (service_item × سال شمسی) + `TariffService::resolvePrice()` با fallback به `ServiceItem.priceRials` + `currentJalaliYear()` (IntlDateFormatter، رقم لاتین) + API `…/tariffs` (GET) و `…/tariffs/{year}` (PUT) + UI «تعرفههای سالانه» (modal از ردیف خدمت).
|
||||
4. **فاز ۴ — محاسبه + Invoice:** ✅ **انجام شد** — VO `Money`/`ShareBreakdown` + `BillingCalculator` (۶ تست واحد سبز، نمونه ۶۰۰k) + `Invoice`/`InvoiceItem` (aggregate، جداول `invoices`/`invoice_items`) + `InvoiceService::createFromSession` (resolve تعرفهی Tariff + CoverageRule per-service + محاسبه) + API `/api/v1/billing/invoices` (POST/GET/finalize). `PatientService::calculateFinalPrice` فعلاً برای سازگاری باقی ماند (مصرف UI موجود).
|
||||
5. **فاز ۵ — Claim + گزارش:** ✅ **انجام شد** — `Claim`/`ClaimItem` (جداول `claims`/`claim_items`) + state machine (`pending→submitted→approved/rejected→paid`) + `ClaimService::createFromInvoice` (تفکیک پایه/مکمل از InvoiceItemها) + transitions + API `/api/v1/billing/claims` (POST/GET + `{uuid}/{action}`) + گزارش `…/reports/insurance-debt`.
|
||||
6. **فاز ۶ — Frontend + API بیمه:** ✅ **انجام شد** — صفحهی «مطالبات بیمه» (`ClaimsPage`: لیست + فیلتر وضعیت + transitions submit/approve/reject/pay + گزارش بدهی) + route `/admin/claims` + آیتم sidebar (doctor/clinic) + interface `ClaimSubmitterInterface` با پیادهسازی پیشفرض `ManualClaimSubmitter` (autowired؛ آمادهی جایگزینی با API بیمهی ایران بدون تغییر `ClaimService`). UI صورتحساب در فرم مراجعه به فاز بعدی موکول شد (نیاز به بازطراحی مودال مراجعه — پرامپت `new-visit-modal-ux.md`).
|
||||
|
||||
> توجه: `EntityInsurancePricing` فعلی (همین session ساخته شد) در فاز ۱ به `TenantInsurance` تبدیل میشود؛ تا آن زمان بهعنوان نسخهی سادهی موقت کار میکند.
|
||||
Reference in New Issue
Block a user