# بیمهٔ تکمیلی: فرانشیز درصدی، درصد پوشش الزامی برای نوع خدمت فعال، و شناسایی بیمه در مطالبات ## پروژه `clinicpro` (بکاند Symfony + پنل ادمین React). تغییرات فقط داخل همین ریپو است؛ کلاینت بیرونی ندارد (`nobat724_front` و `clinic-pro-tauri` هیچکدام `/api/v1/billing/tenant-insurances` یا `/api/v1/billing/claims/*` را مصرف نمیکنند — قبل از شروع با grep تأیید کن). ## زمینه مدل بیمهٔ tenant امروز این است: - کاتالوگ بیمه: `insurances` (`type` = `basic` | `supplementary`) — ۶ بیمهٔ پایه و ۱۰ بیمهٔ تکمیلی seed شدهاند. - قرارداد tenant: `TenantInsurance` (`tenant_insurances`) با `coverage_percent`، `franchise_rials`، `annual_ceiling_rials`، `kind`. - درصد پوشش به تفکیک نوع خدمت: `TenantInsuranceCategoryCoverage` (override قرارداد) → `InsuranceCoverageDefault` (پیشفرض مرکزی ادمین) → `coverage_percent` قرارداد (fallback قدیمی). - نوع خدمت: enum `ServiceCategory` = `outpatient` (خدمات سرپایی) / `inpatient` (خدمات بستری). - «کدام نوع خدمت اصلاً بیمهای است» یک سوییچ سراسریِ tenant است: `TenantServiceCategorySetting` (`tenant_service_category_settings`, ردیف نداشته = فعال) و از `GET /api/v1/insurance-pricing` در کلید `service_categories` بیرون میآید. - محاسبهٔ سهم: `BillingCalculator::calculateItem()` روی `CoverageRule` — فرانشیز فقط در قرارداد تکمیلی اثر دارد و بهصورت **مبلغ ریالی** به سهم بیمار اضافه میشود. - مطالبات: `claims` با `insurance_id` و `insurance_kind` (`base` | `supplementary` — دقت کن، واژگان مطالبه `base` است نه `basic`). داشبورد سطحاول `ClaimsPage` یک ردیف بهازای هر **بیمار** میدهد. سه ایراد واقعی در همین مسیر وجود دارد: 1. مودال «افزودن بیمه» ورودی «درصد پوشش» را برای **هر دو** نوع خدمت رندر میکند، بیتوجه به اینکه tenant کدام نوع را فعال کرده، و خالیماندنِ آنها هیچ خطایی نمیدهد → قرارداد با پوشش صفر ثبت میشود. 2. فرانشیز بهصورت «تومان» گرفته و ذخیره میشود (`franchise_rials`) در حالی که فرانشیز در بیمهٔ تکمیلی یک **درصد** است. 3. در داشبورد مطالبات (سطح اول) هیچجا معلوم نیست هر ردیف مربوط به کدام بیمه است، و نه فیلتر «نوع بیمه» دارد و نه جستجو روی نام بیمه کار میکند. (سطح دومِ `ClaimPatientDetailPage` نام و نوع بیمه را دارد.) ## مشکل / هدف ۱. **درصد پوشش فقط برای نوع خدمتِ فعال، و الزامی.** در مودال افزودن/ویرایش بیمه، بهجای «همهٔ ServiceCategoryها»، فقط نوعهای خدمتی رندر شوند که برای همان tenant فعالاند (`service_categories[].enabled === true` از `GET /api/v1/insurance-pricing`)، و ثبت فرم بدون درصدِ معتبر (۱ تا ۱۰۰) برای هر نوع خدمتِ رندرشده مجاز نباشد. بکاند هم همین را اجبار کند (نه فقط UI). ۲. **فرانشیز درصد است، نه تومان.** `franchise_rials` → `franchise_percent` در هر دو سطح (قرارداد `TenantInsurance` و override خدمت `TenantServiceCoverage`)، در `CoverageRule`، در `BillingCalculator`، در API، در پنل، و در آینهٔ سمتکلاینت `assets/admin/lib/insuranceShares.ts`. **معنای انتخابشده (این را در کد کامنت کن):** فرانشیز درصدی است از مبلغی که بیمهٔ تکمیلی روی آن کار میکند — یعنی «باقیماندهٔ بعد از بیمهٔ پایه» — و سهم بیمار است: ``` remainingAfterBase = total − baseShare(سقفخورده) suppShare = remainingAfterBase × coveragePercent٪ (سقفخورده) franchiseAmount = remainingAfterBase × franchisePercent٪ ← جدید patient = min(total, remainingAfterBase − suppShare + franchiseAmount) ``` این با واقعیت بازار میخواند («تکمیلی ۹۰٪ میدهد، فرانشیز ۱۰٪») و ساختار فعلی فرمول را حفظ میکند؛ تنها تفاوت این است که مبلغ فرانشیز بهجای عدد ثابت، از درصد ساخته میشود. فرانشیز در بیمهٔ **پایه** همچنان بیاثر است. ۳. **مطالبات: کدام بیمه.** در `ClaimsPage` (سطح اول) ستون «بیمه» اضافه شود (یک بیمار میتواند مطالبه زیر چند بیمه داشته باشد → لیست بیمههای آن بیمار با نوعشان)، فیلتر «نوع بیمه» (پایه/تکمیلی) اضافه شود، و جستجوی متنی علاوه بر نام/موبایل/کد ملی بیمار، **نام بیمه** را هم پوشش دهد. ۴. **سناریوی واقعی + تست.** برای پزشک `09389388131` (کاربر `users.id = 11685`، پزشک `doctors.id = 11548`، `entity_type='doctor'`, `entity_id=11548`) چند بیمهٔ تکمیلی و چند بیمار ساخته شود که از این بیمهها استفاده میکنند، مطالبات تولید و مسیر وضعیتها تست شود. ## معیار پذیرش **درصد پوشش / نوع خدمت فعال** - ✅ موفق: tenantـی که فقط `outpatient` را فعال دارد، مودال «افزودن بیمه» تنها یک ورودی «درصد پوشش — خدمات سرپایی» نشان میدهد؛ با مقدار ۷۰ ثبت میشود و `GET /api/v1/billing/tenant-insurances` برای آن قرارداد `category_coverages.outpatient = 70` میدهد. - ❌ خطا: `POST /api/v1/billing/tenant-insurances` با `category_coverages: [{key:'outpatient', coverage_percent: null}]` در حالی که `outpatient` فعال است → HTTP 422 با `{ success:false, errors:[{ code:'ERR_VALIDATION_001', field:'category_coverages', message:'درصد پوشش خدمات سرپایی الزامی است' }] }`. دکمهٔ «ثبت بیمه» هم در UI تا پرشدن همهٔ درصدهای فعال غیرفعال است. - ⚠️ مرزی: نوع خدمتی که در تنظیمات tenant **غیرفعال** است نه رندر میشود و نه در payload میرود؛ قراردادی که از قبل برای آن نوع override داشت، آن override دستنخورده در DB میماند (ارسالنشدن ≠ حذف؛ حذف فقط با `coverage_percent: null` صریح انجام میشود). **فرانشیز درصدی** - ✅ موفق: قرارداد تکمیلی با `coverage_percent = 90` و `franchise_percent = 10` روی خدمتی ۱٬۰۰۰٬۰۰۰ ریالی بدون بیمهٔ پایه → `supplementary = 900,000`، `patient = 100,000 + 100,000 = 200,000`، و `total = base + supp + patient` همچنان برقرار است (تست موجود `tests/Patient/SessionInsuranceShareTest::testTheBreakdownAlwaysSumsBackToTheGrossTotal` باید سبز بماند). - ❌ خطا: `franchise_percent: 150` → HTTP 422 «فرانشیز باید بین ۰ تا ۱۰۰ باشد» (`field: 'franchise_percent'`). همچنین `franchise_percent` منفی → همان خطا. - ⚠️ مرزی: `franchise_percent = 100` روی قرارداد تکمیلی → سهم بیمار از `total` بیشتر نمیشود (`min(total)` نگهدارنده است) و سهم بیمه منفی نمیشود؛ در بیمهٔ **پایه** هر مقدار فرانشیز بیاثر است. **مطالبات** - ✅ موفق: `GET /api/v1/billing/claims/by-patient` برای بیماری با دو مطالبه زیر دو بیمه، آرایهٔ `insurances: [{insurance_id, insurance_name, kind}, …]` میدهد و ستون «بیمه» در جدول هر دو را نشان میدهد. - ❌ خطا: `?kind=bogus` → HTTP 422 «نوع بیمه نامعتبر است» (نه ۵۰۰ و نه لیست خالیِ بیصدا). - ⚠️ مرزی: `?search=آسیا` فقط بیمارانی را برمیگرداند که مطالبهای زیر «بیمه آسیا» دارند، و `meta.totalRecords` با همان فیلتر همخوان است (شمارش و لیست از یک WHERE میآیند)؛ `?kind=supplementary&insurance_id=<یک بیمهٔ پایه>` → صفر ردیف، بدون خطا. **سناریو** - ✅ موفق: بعد از اجرای دستور سناریو، ورود به پنل با `09389388131` → صفحهٔ «پروندههای بیمه» حداقل ۴ بیمار با مطالبات زیر ۳ بیمهٔ تکمیلی مختلف نشان میدهد؛ کارتهای آمار (ادعا/وصول/مانده) با جمع ستونهای جدول همخواناند. - ⚠️ مرزی: اجرای دوبارهٔ دستور، داده را دوباره نمیسازد (idempotent) یا با `--purge` تمیز و بازسازی میکند. ## فایلهای مرتبط | فایل | نقش | |------|-----| | `clinicpro/src/Insurance/Entity/TenantInsurance.php` | ستون `franchise_rials` → `franchise_percent` | | `clinicpro/src/Insurance/Entity/TenantServiceCoverage.php` | همان تغییر در override سطح خدمت | | `clinicpro/src/Insurance/ValueObject/CoverageRule.php` | `franchiseRials` → `franchisePercent` | | `clinicpro/src/Insurance/Service/TenantInsuranceService.php` | `activate()`، `buildRule()`، `setServiceCoverage()`، اعتبارسنجی درصد | | `clinicpro/src/Insurance/Controller/InsuranceController.php` | خواندن/نوشتن `franchise_percent`، اجبار درصد برای نوع خدمت فعال (خطوط ۴۷۶–۵۹۲ و ۶۴۷–۶۹۳) | | `clinicpro/src/Billing/Service/BillingCalculator.php` | فرمول فرانشیز درصدی | | `clinicpro/src/Billing/Repository/ClaimRepository.php` | `patientAggregateSql()` + `patientAggregateFilters()` — بیمهها، فیلتر `kind`، جستجوی نام بیمه | | `clinicpro/src/Billing/Controller/BillingController.php` | `claimsByPatient()`، `claimFilters()` — پارامتر `kind` و نگاشت نام بیمه | | `clinicpro/assets/admin/components/InsuranceModal.tsx` | ورودی فرانشیز درصدی + درصد الزامی + فقط نوع خدمت فعال | | `clinicpro/assets/admin/components/TenantInsuranceContracts.tsx` | تغذیهٔ مودال با نوع خدمتِ فعال، خلاصه و جزئیات فرانشیز درصدی | | `clinicpro/assets/admin/lib/insuranceShares.ts` | آینهٔ فرمول سمت کلاینت | | `clinicpro/assets/admin/components/session/CreateStep.tsx` | مصرفکنندهٔ `franchise_rials` در پذیرش | | `clinicpro/assets/admin/components/ServiceInsuranceModal.tsx`، `pages/ServiceDetailPage.tsx` | override فرانشیز سطح خدمت | | `clinicpro/assets/admin/pages/ClaimsPage.tsx` | ستون بیمه + فیلتر نوع بیمه | | `clinicpro/assets/admin/types/index.ts` | `ClaimPatientRow` | | `clinicpro/migrations/` | migration تغییر نام و نوع ستونها | | `clinicpro/docs/api/insurance.md`، `docs/api/billing.md` | مستندسازی قرارداد جدید | ## وضعیت فعلی `clinicpro/src/Billing/Service/BillingCalculator.php` (خطوط ۳۱–۴۳) — فرانشیز مبلغ ثابت: ```php $suppShare = Money::zero(); 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); } // بیمهٔ پایه صرفاً درصدی است: سهم بیمار = کل − سهم پایه. فرانشیز فقط در بیمهٔ // تکمیلی معنا دارد و سهم بیمار را از کل بیشتر نمیکند. $franchise = new Money($supplementary?->franchiseRials ?? 0); $patient = $remaining->add($franchise)->min($total); ``` `clinicpro/assets/admin/components/InsuranceModal.tsx` (خطوط ۲۱۹–۲۵۵) — همهٔ categoryها رندر میشوند، هیچ اجباری روی مقدار نیست، و فرانشیز تومانی است: ```tsx