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:
hamed
2026-06-23 15:05:24 +03:30
co-authored by Claude Opus 4.8
parent 5b1dfe9b40
commit 89191eee57
54 changed files with 4233 additions and 10 deletions
+110
View File
@@ -0,0 +1,110 @@
# قیمت‌گذاری ویزیت بر اساس بیمه در پروفایل کلینیک/مطب
## پروژه
`clinicpro` (backend + admin frontend)
## زمینه
در حال حاضر قیمت ویزیت پزشک نسبت به بیمه فقط با موجودیت `DoctorInsurance` ذخیره می‌شود که **یک فیلد قیمت تکی** (`price`) دارد:
```php
// src/Insurance/Entity/DoctorInsurance.php
#[ORM\Column(type: 'integer', nullable: true)]
private ?int $price = null;
```
این کافی نیست. کاربر (کلینیک یا مطب شخصی) باید بتواند در پروفایل خود مشخص کند:
- مبلغ **ویزیت آزاد** (بدون بیمه) چقدر است
- اگر **بیمه پایه** اعمال شود، مبلغ نهایی/سهم بیمار چقدر می‌شود
- اگر علاوه بر آن **بیمه مکمل** هم اضافه شود، مبلغ نهایی چقدر می‌شود
## مشکل / هدف
افزودن یک ساختار قیمت‌گذاری ویزیت بر اساس بیمه در پروفایل entity (هم `doctor` و هم `clinic`)، با سه لایه:
1. **قیمت پایه آزاد** (free / بدون بیمه)
2. **سهم/مبلغ با بیمه پایه** — به ازای هر بیمه پایه‌ای که entity می‌پذیرد
3. **سهم/مبلغ با بیمه مکمل** — به ازای هر بیمه مکمل
این مقادیر بعداً در «ثبت مراجعه جدید» (پرامپت `new-visit-modal-ux.md`) برای پر کردن خودکار قیمت ویزیت و درصد/مبلغ تخفیف استفاده می‌شوند.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/Insurance/Entity/DoctorInsurance.php` | رابطه فعلی پزشک↔بیمه با یک قیمت |
| `src/Insurance/Entity/Insurance.php` | موجودیت بیمه؛ دارای `type` (enum `InsuranceType`) |
| `src/Insurance/Enum/InsuranceType.php` | نوع بیمه (پایه/مکمل) — بخوان و مقادیر را تأیید کن |
| `src/Insurance/Controller/InsuranceController.php` | CRUD بیمه + `DoctorInsurance` (از خط ۱۸۶) |
| `src/Insurance/Repository/DoctorInsuranceRepository.php` | کوئری‌ها |
| `assets/admin/pages/MyClinicPage.tsx` و `DoctorProfilePage.tsx` | پروفایل کلینیک/مطب در پنل |
| `docs/api/insurance.md` | مستندات API بیمه |
## وضعیت فعلی
`DoctorInsurance.toArray()`:
```php
return [
'id' => $this->id,
'doctor_id' => $this->doctor->getId(),
'insurance_id' => $this->insurance->getId(),
'insurance_name' => $this->insurance->getName(),
'type' => $this->insurance->getType()->value,
'price' => $this->price,
];
```
- فقط برای `doctor` است؛ معادل `clinic` وجود ندارد.
- فقط یک `price` تکی دارد؛ تفکیک آزاد/پایه/مکمل ندارد.
## وظایف
### ۱. تحلیل و طراحی مدل قیمت‌گذاری
ابتدا `InsuranceType` و `DoctorInsurance` و `InsuranceController` (بخش DoctorInsurance) را کامل بخوان. سپس تصمیم بگیر:
- آیا «ویزیت آزاد» یک فیلد روی پروفایل entity است (یک مقدار) و هر `DoctorInsurance` فقط مبلغ سهم بیمار با آن بیمه را نگه می‌دارد؟ (پیشنهاد: بله)
- آیا باید برای `clinic` هم معادل `DoctorInsurance` ساخت (مثلاً عمومی‌سازی به entity-type/entity-id مثل `PatientRecord`)، یا یک موجودیت جدید `EntityInsurancePricing(entityType, entityId, insuranceId, patientShareRials)` ساخت؟
> توصیه: یک موجودیت polymorphic جدید `EntityInsurancePricing` با کلیدهای `entity_type` (`doctor|clinic`)، `entity_id`، `insurance_id`، `patient_share_rials` بساز و `DoctorInsurance` را به‌مرور کنار بگذار (اما داده‌ی فعلی را migrate کن). قیمت ویزیت آزاد را روی پروفایل entity (یک کلید config یا ستون) نگه‌دار.
طرح نهایی را قبل از پیاده‌سازی به‌صورت یک پاراگراف توضیح بده.
### ۲. Entity + Migration
- موجودیت/فیلدهای جدید را بساز (طبق طرح مرحله ۱).
- مقدار «ویزیت آزاد» را برای entity ذخیره کن.
- `doctrine:migrations:diff` سپس `migrate`.
- داده‌ی موجود `doctor_insurances.price` را به مدل جدید migrate کن (در همان migration یا یک migration داده‌ای).
### ۳. API
Endpointها برای خواندن/ذخیره قیمت‌گذاری بیمه‌ی entity جاری (از `#[CurrentUser]` → resolve به `doctor` یا `clinic` مثل الگوی `PatientController::resolveEntity`):
- `GET /api/v1/insurance-pricing` — لیست قیمت‌گذاری entity جاری + قیمت ویزیت آزاد
- `PUT|PATCH /api/v1/insurance-pricing` — ذخیره‌ی قیمت آزاد + آرایه‌ی سهم هر بیمه
از `BaseController` و helperهای `$this->success()` / `$this->error()` استفاده کن.
### ۴. Admin Frontend
در `MyClinicPage.tsx` و `DoctorProfilePage.tsx` یک بخش «قیمت‌گذاری ویزیت بر اساس بیمه» اضافه کن:
- ورودی «مبلغ ویزیت آزاد (ریال)»
- جدول/لیست بیمه‌های پایه و مکمل (از `GET /api/v1/insurances`) با یک ورودی مبلغ سهم بیمار به ازای هر کدام
- پیش‌نمایش: «آزاد: X — با بیمه پایه Y: Z — با بیمه مکمل W: …»
- ذخیره با TanStack Query `useMutation`
### ۵. مستندات
`docs/api/insurance.md` را با endpointهای جدید (method/path/permission، body کامل، نمونه پاسخ JSON واقعی، error codeها) به‌روز کن.
## نکات مهم
- entity جاری از `#[CurrentUser]` resolve می‌شود؛ الگو را از `PatientController::resolveEntity` بگیر (نقش `ROLE_DOCTOR` → doctor، `ROLE_CLINIC` → clinic).
- تاریخ‌ها/قیمت‌ها همه integer ریال هستند.
- این پرامپت پیش‌نیاز `new-visit-modal-ux.md` است؛ خروجی قیمت‌ها آنجا مصرف می‌شود.
- مهاجرت داده‌ی `DoctorInsurance` نباید قیمت‌های موجود را از بین ببرد.
@@ -0,0 +1,98 @@
# مشخص‌کردن شمول بیمه برای هر خدمت
## پروژه
`clinicpro` (backend + admin frontend)
## زمینه
هر خدمت کلینیک با موجودیت `ServiceItem` نگه‌داری می‌شود. در حال حاضر هیچ فیلدی ندارد که بگوید این خدمت **شامل بیمه می‌شود یا نه**.
## مشکل / هدف
افزودن قابلیت تعیین «شمول بیمه» به هر `ServiceItem`:
- یک پرچم: آیا این خدمت شامل بیمه می‌شود؟
- اگر شامل می‌شود، اطلاعات لازم برای محاسبه‌ی درست (مثلاً سهم بیمار / درصد پوشش / لیست بیمه‌های پذیرفته‌شده) ذخیره و نمایش داده شود.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/ClinicService/Entity/ServiceItem.php` | موجودیت خدمت |
| `src/ClinicService/Controller/ClinicServiceController.php` | CRUD خدمات/بخش‌ها |
| `src/ClinicService/Repository/ServiceItemRepository.php` | کوئری‌ها |
| `assets/admin/pages/ClinicServicesPage.tsx` | صفحه‌ی مدیریت خدمات در پنل |
| `src/Patient/Service/PatientService.php` | محاسبه‌ی قیمت مراجعه (مصرف‌کننده‌ی خدمت) |
| `docs/api/clinic-services.md` | مستندات API خدمات |
## وضعیت فعلی
`ServiceItem` فیلدهای موجود:
```php
private string $name;
#[ORM\Column(name: 'price_rials', type: 'integer')]
private int $priceRials = 0;
#[ORM\Column(type: 'boolean')]
private bool $active = true;
```
`toArray()`:
```php
return [
'uuid' => $this->uuid,
'section_uuid' => $this->section->getUuid(),
'staff_uuid' => $this->staff?->getUuid(),
'staff_name' => $this->staff?->getFullName(),
'name' => $this->name,
'price_rials' => $this->priceRials,
'active' => $this->active,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt,
];
```
هیچ مفهوم بیمه‌ای ندارد.
## وظایف
### ۱. طراحی فیلد شمول بیمه
تصمیم بگیر مدل داده چه باشد:
- ساده: یک `bool $insuranceCovered = false`.
- کامل‌تر: `bool $insuranceCovered` + `?int $patientShareRials` (سهم بیمار وقتی بیمه اعمال شود) یا `?float $coveragePercent`.
> توصیه: `insuranceCovered` (bool) + `?int $insurancePriceRials` (قیمت/سهم بیمار با بیمه). اگر `insuranceCovered=false`، فیلد دوم نادیده گرفته شود.
طرح را قبل از پیاده‌سازی توضیح بده.
### ۲. Entity + Migration
- فیلد(ها) را به `ServiceItem` اضافه کن + getter/setter.
- در `toArray()` اضافه کن.
- `doctrine:migrations:diff` سپس `migrate`.
### ۳. API
`ClinicServiceController` (create/update خدمت) باید فیلدهای جدید را از body بپذیرد و ذخیره کند. validation مناسب (اگر `insurance_covered=true` و قیمت بیمه خالی، خطا یا صفر منطقی).
### ۴. Admin Frontend
در `ClinicServicesPage.tsx` فرم خدمت:
- یک toggle/checkbox «شامل بیمه می‌شود»
- وقتی روشن شد، ورودی «قیمت با بیمه (ریال)» نمایش داده شود
- در لیست خدمات، یک badge نشان دهد خدمت شامل بیمه است یا خیر
### ۵. مستندات
`docs/api/clinic-services.md` را با فیلدهای جدید در request/response به‌روز کن.
## نکات مهم
- قیمت‌ها integer ریال.
- این فیلد بعداً در محاسبه‌ی قیمت مراجعه (`PatientService::createSession`) قابل استفاده است؛ در این پرامپت فقط ذخیره/نمایش کافی است مگر اینکه ساده باشد همان‌جا هم اعمال شود.
- الگوی frontend خدمات موجود را رعایت کن (همان فرم/modal فعلی).