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,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 فعلی).
|
||||
Reference in New Issue
Block a user