# انتخاب نوع خدمت بیمه (سرپایی/بستری) در نوبت و انتشار آن تا فاکتور ## پروژه `clinicpro` (بک‌اند Symfony + پنل ادمین React). دامنه‌ها: `src/Insurance/`، `src/Appointment/`، `src/Patient/`، `src/Billing/`، `assets/admin/`. سایت عمومی `nobat724_front` هیچ‌جا بیمه را در فرآیند رزرو انتخاب نمی‌کند؛ فیلدهای جدید نوبت optional و nullable هستند → پرامپت همتا لازم نیست (فقط قرارداد پاسخ `GET /api/v1/appointment/{uuid}` دو کلید جدید می‌گیرد که مصرف‌کنندهٔ فعلی ندارد). ## زمینه در پرامپت قبلی (`insurance-coverage-percent-rework.md`) درصد پوشش هر بیمه به تفکیک **نوع خدمت** (`ServiceCategory`: `outpatient` / `inpatient`) پیاده شد: پیش‌فرض مرکزی ادمین (`insurance_coverage_defaults`)، override قرارداد (`tenant_insurance_category_coverage`) و زنجیرهٔ resolve در `TenantInsuranceService::resolvePercent`. نوع خدمتِ هر «آیتم سرویس» هم روی `ServiceItem.service_category` ذخیره می‌شود و **ویزیت تا الآن همیشه `outpatient` فرض شده است**. اما در جریان کاری واقعی، پزشک ممکن است اصلاً خدمات بستری ارائه نکند، یا هر دو را ارائه کند و لازم باشد سرِ پذیرش مشخص شود این مراجعه سرپایی است یا بستری. الآن هیچ‌کدام از این‌ها وجود ندارد: 1. پزشک/کلینیک نمی‌تواند بگوید بیمه‌هایش کدام نوع خدمات را پوشش می‌دهند. 2. نوبت هیچ فیلدی برای «بیمهٔ انتخابی» و «نوع خدمت» ندارد → مودال «قطعی کردن نوبت» فقط جمع کل را نشان می‌دهد و کل مبلغ سهم بیمار می‌شود. 3. `PatientService::autoCreateOnAppointmentConfirm` عامدانه بیمه را نادیده می‌گیرد: `applyShares($gross, 0, 0, $gross)`. 4. پرونده بیمار (بخش سرویس‌ها) و فاکتور، نوع خدمت و نام بیمه را نشان نمی‌دهند. ## هدف الف) در **تنظیمات ← مدیریت بیمه** (`/admin/insurance-pricing`) یک تنظیم **سراسری برای همهٔ بیمه‌های همان پزشک/کلینیک**: کدام نوع خدمات بیمه فعال است — «خدمات سرپایی» و/یا «خدمات بستری». (این تنظیم per-insurance نیست؛ یک بار برای کل tenant.) ب) اگر فقط یک نوع فعال باشد، همان به‌صورت خودکار مبنای محاسبه است و **هیچ انتخابی از کاربر پرسیده نمی‌شود**. اگر هر دو فعال باشند، انتخاب نوع خدمت نمایش داده شود در: - مودال «قطعی کردن نوبت» (که از تایم‌لاین نوبت‌ها و صفحهٔ جزئیات نوبت باز می‌شود) - صفحهٔ ویرایش نوبت (`/admin/appointments/{uuid}/edit`) ج) پس از انتخاب نوع خدمت، لیست **بیمه‌های پایهٔ فعالِ همان پزشک** نمایش داده شود؛ با انتخاب بیمه، سهم بیمه و مبلغ پرداختی بیمار خودکار محاسبه و نمایش داده شود (آینهٔ `BillingCalculator`، همان قاعدهٔ `round(کل × درصد ÷ 100)`). د) نوع خدمت + بیمهٔ انتخابی روی نوبت ذخیره شود، به مراجعه (`PatientSession`) منتقل شود، در **پرونده بیمار ← سرویس‌ها** نمایش داده شود و در **فاکتور** درج شود (نوع خدمت، بیمه، سهم بیمه، سهم بیمار). ## فایل‌های مرتبط | فایل | نقش | |---|---| | `src/Insurance/Enum/ServiceCategory.php` | enum موجود `outpatient`/`inpatient` (+ `values()`, `label()`, `tryFromValue()`) | | `src/Insurance/Entity/EntityInsurancePricing.php` | تنظیمات بیمهٔ tenant (ردیف `insurance_id = NULL` = قیمت ویزیت آزاد + `require_visit_price`) | | `src/Insurance/Controller/InsuranceController.php` | `GET/PUT /api/v1/insurance-pricing` (`pricingPayload()`) + قراردادهای tenant | | `src/Insurance/Service/TenantInsuranceService.php` | `coverageRule()` (ویزیت، الآن hardcode `Outpatient`)، `coverageRuleForService()`، `resolvePercent()` | | `src/Appointment/Entity/Appointment.php` | **فاقد** فیلد بیمه/نوع خدمت | | `src/Appointment/Controller/AppointmentController.php` | `PATCH /api/v1/appointment/{uuid}` (خط ۱۰۵۶) و `POST /api/v1/appointment/{uuid}/confirm` (خط ۹۸۵) | | `src/Appointment/Service/AppointmentConfirmationService.php` | `confirmWithPayments()` — تراکنش قطعی‌کردن + ساخت مراجعه + پرداخت‌ها | | `src/Patient/Service/PatientService.php` | `autoCreateOnAppointmentConfirm()` / `autoCreateForEntity()` / `calculateFinalPrice()` / `createSession()` | | `src/Patient/Entity/PatientSession.php` | `insurance_base_id`, `base_insurance_rials`, … — **فاقد** نوع خدمت | | `src/Billing/Service/InvoiceService.php` | `createFromSession()` — همان `CoverageRule` | | `src/Billing/Entity/Invoice.php` | `base_insurance_id`, `base_insurance_rials`, … — **فاقد** نوع خدمت | | `assets/admin/pages/InsurancePricingPage.tsx` | صفحهٔ «مدیریت بیمه» داخل `SettingsLayout active="insurance"` | | `assets/admin/components/TenantInsuranceContracts.tsx` | کارت قراردادهای بیمه + `useServiceCategories()` | | `assets/admin/components/appointments/ConfirmAppointmentModal.tsx` | مودال «قطعی کردن نوبت» | | `assets/admin/components/appointments/TurnsTimeline.tsx` | تایم‌لاین نوبت‌ها (خط ۱۴۵ مودال را باز می‌کند) | | `assets/admin/pages/AppointmentEditPage.tsx` | صفحهٔ ویرایش نوبت | | `assets/admin/components/SessionServiceCard.tsx` | کارت سرویس‌های مراجعه در پرونده بیمار | | `assets/admin/components/InvoiceSummaryModal.tsx` | نمایش فاکتور | | `assets/admin/hooks/useServiceCategories.ts` | لیست انواع خدمت از `GET /api/v1/service-categories` | | `docs/api/insurance.md`, `docs/api/appointment.md`, `docs/api/patient.md`, `docs/api/billing.md` | مستندات | ## وضعیت فعلی ### ۱) قطعی‌کردن نوبت، بیمه را کاملاً نادیده می‌گیرد — `src/Patient/Service/PatientService.php:212-226` ```php // خطوط هزینه‌ی سرویس (قیمت snapshot از خود سرویس، بدون ورود دستی). $servicesTotal = 0; $lines = []; foreach ($appointment->getServiceItems() as $item) { $line = new SessionService($session, $item, null, 1); $lines[] = $line; $servicesTotal += $line->getLineTotalRials(); } $session->setServicesTotalRials($servicesTotal); // پذیرش خودکار بیمه‌ای ندارد: تمام مبلغ سهم بیمار است. $gross = $servicesTotal + $visitPrice; $session->applyShares($gross, 0, 0, $gross); ``` ### ۲) قاعدهٔ ویزیت نوع خدمت را نمی‌پذیرد — `src/Insurance/Service/TenantInsuranceService.php` ```php public function coverageRule(string $entityType, int $entityId, ?int $insuranceId): CoverageRule { // ... return $this->buildRule($contract, ServiceCategory::Outpatient, null); } ``` ### ۳) مودال قطعی‌کردن فقط جمع کل را می‌شناسد — `assets/admin/components/appointments/ConfirmAppointmentModal.tsx:130-146` ```tsx const visitPrice = Number(appt?.visit_price_rials ?? 0); const services = appt?.service_items ?? []; const servicesTotal = useMemo( () => services.reduce((sum, s) => sum + Number(s.price_rials ?? 0), 0), [services], ); const total = visitPrice + servicesTotal; // ردیفِ اول تا لحظه‌ای که کاربر مبلغ را دستی تغییر ندهد پیش‌فرضِ «پرداخت کامل» است؛ useEffect(() => { if (!open || touched || total <= 0) return; setRows(prev => prev.map((r, i) => (i === 0 ? { ...r, amountToman: rialToToman(total) } : r))); }, [open, touched, total]); ``` و بدنهٔ ارسالی هیچ فیلد بیمه‌ای ندارد: ```tsx api.post(`/api/v1/appointment/${appointmentUuid}/confirm`, { version: appt?.version, payments: rows.filter(...).map(...), }) ``` ### ۴) صفحهٔ ویرایش نوبت — `assets/admin/pages/AppointmentEditPage.tsx:82-94` ```tsx const save = useMutation({ mutationFn: () => api.patch(`/api/v1/appointment/${uuid}`, { slot_start: toEpoch(date, start), slot_end: toEpoch(date, end), service_section_uuid: sectionUuid, service_item_uuid: itemUuid, staff_uuid: staffUuid, deposit_required: depositRequired, deposit_amount_rials: depositRequired ? tomanToRial(depositToman) : null, note, ...(status !== a?.status ? { status } : {}), version: a?.version, }), ``` ## وظایف > ترتیب: ۱ → ۹. بک‌اند اول، بعد فرانت، بعد مستندات/تست. هر گام مستقل تست‌شدنی باشد. ### ۱. تنظیم سراسریِ «نوع خدمات بیمه» برای هر tenant Entity جدید `src/Insurance/Entity/TenantServiceCategorySetting.php` (`tenant_service_category_settings`) + `src/Insurance/Repository/TenantServiceCategorySettingRepository.php`: ```php #[ORM\Entity(repositoryClass: TenantServiceCategorySettingRepository::class)] #[ORM\Table(name: 'tenant_service_category_settings')] #[ORM\UniqueConstraint(name: 'uniq_tenant_service_category_setting', columns: ['entity_type', 'entity_id', 'service_category'])] class TenantServiceCategorySetting { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private ?int $id = null; #[ORM\Column(name: 'entity_type', type: 'string', length: 10)] private string $entityType; // doctor|clinic — همان الگوی EntityInsurancePricing #[ORM\Column(name: 'entity_id', type: 'integer')] private int $entityId; #[ORM\Column(name: 'service_category', type: 'string', length: 30, enumType: ServiceCategory::class)] private ServiceCategory $serviceCategory; #[ORM\Column(type: 'boolean', options: ['default' => true])] private bool $enabled = true; #[ORM\Column(name: 'updated_at', type: 'integer')] private int $updatedAt; // getters/setters + toArray() } ``` Service جدید `src/Insurance/Service/TenantServiceCategoryService.php`: ```php /** @return list نوع‌های فعال؛ نبودِ ردیف = همهٔ نوع‌ها فعال (سازگاری عقب‌رو) */ public function enabledKeys(string $entityType, int $entityId): array; /** @return list برای UI — همیشه همهٔ caseها */ public function settingsRows(string $entityType, int $entityId): array; /** * نوع خدمتِ پیش‌فرض: اگر فقط یک نوع فعال باشد همان، وگرنه null * (یعنی کاربر باید انتخاب کند). */ public function defaultCategory(string $entityType, int $entityId): ?ServiceCategory; public function isEnabled(string $entityType, int $entityId, ServiceCategory $category): bool; /** @param list $rows */ public function save(string $entityType, int $entityId, array $rows): void; // حداقل یک نوع باید فعال بماند ``` migration + `ddev exec php bin/console doctrine:migrations:migrate --no-interaction` (روی `db` و `--env=test`). **backfill لازم نیست**: نبودِ ردیف = «همه فعال». ### ۲. انتشار تنظیم در API موجود (بدون endpoint جدید) `GET /api/v1/insurance-pricing` همین‌حالا تنظیماتِ سراسریِ بیمهٔ tenant را می‌دهد (`free_visit_price_rials`, `require_visit_price`, `insurances[]`), پس همان را توسعه بده — endpoint جدید نساز: - در `pricingPayload()` کلید تازه: ```json "service_categories": [ { "key": "outpatient", "label": "خدمات سرپایی", "enabled": true }, { "key": "inpatient", "label": "خدمات بستری", "enabled": false } ], "default_service_category": "outpatient" ``` `default_service_category` همان خروجی `defaultCategory()` است (`null` وقتی هر دو فعال‌اند). - `PUT /api/v1/insurance-pricing` بدنهٔ اختیاری `service_categories: [{ key, enabled }]` بپذیرد. اعتبارسنجی: `key ∈ ServiceCategory::values()` و **حداقل یک نوع فعال** → وگرنه `$this->error(ErrorCodes::ERR_VALIDATION_001, 'حداقل یک نوع خدمت باید فعال باشد', 422, 'service_categories')`. - همان قواعد دسترسیِ فعلیِ این endpoint (منشی: `insurances.view`/`update`، و `doctor_uuid` برای کلینیک چندپزشکه) بدون تغییر اعمال شود. ### ۳. فیلدهای بیمه روی نوبت `src/Appointment/Entity/Appointment.php` دو ستون جدید: ```php /** نوع خدمتِ بیمه‌ایِ این نوبت؛ null = هنوز انتخاب نشده. */ #[ORM\Column(name: 'insurance_service_category', type: 'string', length: 30, nullable: true, enumType: ServiceCategory::class)] private ?ServiceCategory $insuranceServiceCategory = null; /** بیمهٔ پایهٔ انتخاب‌شده (ارجاع خام int، مثل TenantInsurance/Tariff). */ #[ORM\Column(name: 'insurance_base_id', type: 'integer', nullable: true)] private ?int $insuranceBaseId = null; ``` - در `toArray()`: `insurance_service_category`, `insurance_service_category_label`, `insurance_base_id`, `insurance_base_name` (نام از `InsuranceRepository`؛ در لیست‌ها بدون N+1 — اگر لازم شد نام را در کنترلر با یک map تزریق کن، نه داخل entity). - migration. `PATCH /api/v1/appointment/{uuid}` (خط ۱۰۵۶) این دو فیلد را بپذیرد؛ اعتبارسنجی مشترک را در یک helper خصوصی بگذار تا `confirm` هم از آن استفاده کند: ```php /** * نوع خدمت و بیمهٔ پایه را روی نوبت می‌نشاند. * - نوع خدمت باید در تنظیمات همان tenant فعال باشد * - بیمه باید قرارداد فعال داشته باشد و پایه باشد */ private function applyAppointmentInsurance(Appointment $appointment, array $data): ?JsonResponse ``` خطاها: `422 ERR_VALIDATION_001` با فیلد `insurance_service_category` (نوع نامعتبر یا غیرفعال) · `422 ERR_VALIDATION_001` با فیلد `insurance_base_id` (بیمه بدون قرارداد فعال یا نوعش تکمیلی است). `POST /api/v1/appointment/{uuid}/confirm` هم همان دو فیلد را در بدنه بپذیرد (انتخاب سرِ پذیرش) و **قبل از** `confirmWithPayments` روی نوبت بنشاند تا مراجعه با بیمهٔ درست ساخته شود. ### ۴. نوع خدمت روی مراجعه + محاسبهٔ واقعیِ سهم در قطعی‌کردن `src/Patient/Entity/PatientSession.php`: ستون `insurance_service_category` (nullable, `enumType: ServiceCategory::class`) + getter/setter + کلید در `toArray()` (`insurance_service_category`, `insurance_service_category_label`). migration. `TenantInsuranceService::coverageRule()` یک پارامتر اختیاری بگیرد و پیش‌فرضش رفتار فعلی بماند: ```php public function coverageRule( string $entityType, int $entityId, ?int $insuranceId, ServiceCategory $category = ServiceCategory::Outpatient, ): CoverageRule ``` `PatientService::calculateFinalPrice()` هم پارامتر اختیاری `?ServiceCategory $visitCategory = null` بگیرد و برای ویزیت آن را (یا `Outpatient` در نبودش) به `coverageRule()` بدهد. سهم خدمات بدون تغییر از `service_category` خودِ `ServiceItem` می‌آید. `PatientService::autoCreateForEntity()` جای `applyShares($gross, 0, 0, $gross)`: ```php $session->setInsuranceBaseId($appointment->getInsuranceBaseId()); $session->setInsuranceServiceCategory($category); // از نوبت، یا defaultCategory() همان tenant $session->setBaseInsuranceDiscountPercent($this->contractPercent(...)); // snapshot نمایشی $calc = $this->calculateFinalPrice($visitPrice, $serviceLines, $entityType, $entityId, $appointment->getInsuranceBaseId(), null, $category); $session->applyShares( $calc['gross_total_rials'], $calc['base_insurance_rials'], $calc['supplementary_insurance_rials'], $calc['patient_share_rials'], ); ``` - اگر نوبت بیمه ندارد، رفتار فعلی (کل مبلغ سهم بیمار) دقیقاً حفظ شود. - نوع خدمتِ مؤثر: `appointment->getInsuranceServiceCategory() ?? $categoryService->defaultCategory(...) ?? ServiceCategory::Outpatient`. - `createSession()` و مسیر `PATCH /api/v1/session/{uuid}` هم `insurance_service_category` را بپذیرند و به `calculateFinalPrice()` بدهند (هم‌خوان با `MyPatientsPage`). ### ۵. مودال «قطعی کردن نوبت» `assets/admin/components/appointments/ConfirmAppointmentModal.tsx`: - `useQuery` روی `/api/v1/insurance-pricing` (برای `service_categories` + `default_service_category`) و `/api/v1/billing/tenant-insurances` (قراردادها). هر دو `enabled: open`. - state: `serviceCategory` (init: مقدار نوبت، وگرنه `default_service_category`)، `insuranceId` (init: `appt.insurance_base_id`). - `SearchableSelect` «نوع خدمت» **فقط** وقتی `service_categories.filter(c => c.enabled).length > 1` رندر شود؛ در غیر این صورت نوع فعال بی‌سروصدا استفاده شود (بدون UI). - `SearchableSelect` «بیمه» با قراردادهای `is_active === true` و `insurance_kind === 'basic'`. - محاسبهٔ زنده (آینهٔ `BillingCalculator`، همان `patientShareOf` که در `assets/admin/components/session/CreateStep.tsx` export شده — از همان استفاده کن، منطق تازه ننویس): - درصد از `contract.category_coverages[serviceCategory]` (fallback: `contract.coverage_percent`). - ویزیت با نوع خدمتِ انتخابی، هر سرویس با `service_category` خودش. - دو ردیف نمایشی جدید در خلاصهٔ مبالغ: «سهم بیمه» و «سهم بیمار (قابل پرداخت)» با `formatRial`. - پیش‌فرضِ ردیف اولِ پرداخت از `total` به **سهم بیمار** تغییر کند (وقتی بیمه انتخاب شده)؛ سقف `overpaid` هم بر همان مبنا. - بدنهٔ `confirm` دو کلید تازه بفرستد: ```ts api.post(`/api/v1/appointment/${appointmentUuid}/confirm`, { version: appt?.version, ...(serviceCategory ? { insurance_service_category: serviceCategory } : {}), ...(insuranceId ? { insurance_base_id: Number(insuranceId) } : {}), payments: [...], }) ``` ### ۶. تایم‌لاین نوبت‌ها `assets/admin/components/appointments/TurnsTimeline.tsx`: - کارت نوبت، وقتی `insurance_service_category` یا `insurance_base_name` دارد، یک چیپ کوچک نمایش دهد (`سرپایی · بیمه ایران`) با همان توکن‌های موجود (`--primary-soft` / `--text-2`)؛ طراحی تازه نساز. - `appointment` را به `ConfirmAppointmentModal` پاس بده (`appointment={a}`) تا درخواست جزئیات تکراری نزند و مقدارهای فعلی بیمه در مودال پیش‌پر شوند. ### ۷. صفحهٔ ویرایش نوبت `assets/admin/pages/AppointmentEditPage.tsx`: - `AppointmentDetail` دو فیلد جدید بگیرد؛ state + hydrate در همان `useEffect`. - دو `SearchableSelect` («نوع خدمت بیمه» با همان شرط «هر دو فعال»، و «بیمه») در بلوک مشخصات، با همان `label` استایل موجود. - پیش‌نمایش «سهم بیمه / سهم بیمار» زیر انتخاب بیمه (همان تابع مشترک محاسبه). - `save.mutationFn` دو کلید جدید را در بدنهٔ PATCH بفرستد. ### ۸. پرونده بیمار (سرویس‌ها) + فاکتور `assets/admin/components/SessionServiceCard.tsx`: - در بخش تفکیک بیمه، دو ردیف تازه: «نوع خدمت بیمه» (`insurance_service_category_label`) و «بیمه» (نام بیمه). داده از `PatientSession::toArray()`. - نام بیمه در پاسخ session لازم است: در `src/Patient/Controller/PatientController.php` (مسیر لیست/جزئیات مراجعه) نام‌ها را با یک کوئری map شده اضافه کن (`insurance_base_name`) — بدون N+1 و بدون گذاشتن ریپازیتوری داخل entity. `src/Billing/Entity/Invoice.php` + `src/Billing/Service/InvoiceService.php`: - ستون `service_category` روی `invoices` (nullable, enumType) که از `PatientSession::getInsuranceServiceCategory()` snapshot می‌شود؛ migration. - `Invoice::toArray()` → `service_category`, `service_category_label`؛ و `base_insurance_name` در پاسخ کنترلر فاکتور (همان الگوی map). `assets/admin/components/InvoiceSummaryModal.tsx`: - در هدر/خلاصهٔ فاکتور: «نوع خدمت بیمه»، «بیمه»، و ستون‌های موجود «سهم بیمه پایه» و «سهم بیمار» حفظ شوند (خط ۱۲۰ فعلی). ### ۹. مستندات و تست - `docs/api/insurance.md`: کلیدهای `service_categories` و `default_service_category` در `GET/PUT /api/v1/insurance-pricing` + قاعدهٔ «حداقل یک نوع فعال» + توضیح این‌که این تنظیم سراسری است نه per-insurance. - `docs/api/appointment.md`: فیلدهای `insurance_service_category` / `insurance_base_id` در `GET`, `PATCH`, و بدنهٔ `POST /confirm` با خطاهایشان. - `docs/api/patient.md`: `insurance_service_category` روی مراجعه + این‌که قطعی‌کردن نوبت دیگر کل مبلغ را سهم بیمار نمی‌گذارد. - `docs/api/billing.md`: `service_category` روی فاکتور. - PHPUnit: - `TenantServiceCategoryService`: نبودِ ردیف → همه فعال · یک نوع فعال → `defaultCategory()` همان · هر دو فعال → `null` · غیرفعال‌کردن همه → ۴۲۲. - `PUT /api/v1/insurance-pricing` با `service_categories` نامعتبر → ۴۲۲. - `PATCH /api/v1/appointment/{uuid}` با نوع غیرفعال → ۴۲۲؛ با بیمهٔ بدون قرارداد فعال → ۴۲۲. - `POST /confirm` با `insurance_base_id`: مراجعهٔ ساخته‌شده باید `base_insurance_rials` و `patient_share_rials` درست داشته باشد (سناریوی مرجع: ویزیت `5_952_000` ریال، پوشش بستری ۳۰٪ → `1_785_600` / `4_166_400`). - `InvoiceService`: `service_category` روی فاکتور snapshot شود. - Vitest: `ConfirmAppointmentModal` (پنهان‌بودن انتخاب نوع خدمت وقتی فقط یکی فعال است؛ محاسبهٔ سهم بیمه/بیمار؛ فیلدهای بدنهٔ confirm)، `AppointmentEditPage` (ارسال دو فیلد جدید). - اجرای واقعی: `ddev exec php bin/phpunit` · `ddev exec php vendor/bin/phpstan analyse` · `ddev exec npx tsc --noEmit --project tsconfig.json` · `ddev exec yarn dev` · `npx vitest run` (⚠️ vitest داخل ddev به‌خاطر معماری esbuild اجرا نمی‌شود؛ روی host اجرا کن). ## نکات مهم - **منبع واحد محاسبه:** فقط `BillingCalculator` (سرور) و `patientShareOf` (آینهٔ کلاینت در `CreateStep.tsx`). هیچ فرمول درصدیِ موازیِ جدیدی در مودال قطعی‌کردن یا صفحهٔ ویرایش نوبت ننویس؛ همان تابع را import کن. - **قاعدهٔ بیمهٔ پایه:** `سهم پایه = round(کل × درصد ÷ 100)` و `سهم بیمار = کل − سهم پایه`. فرانشیز فقط در قرارداد `supplementary`. - **زنجیرهٔ درصد** دست‌نخورده باقی بماند: service override → override قرارداد برای نوع خدمت → پیش‌فرض مرکزی ادمین → `coverage_percent` قرارداد. - **سازگاری عقب‌رو:** نبودِ ردیف در `tenant_service_category_settings` یعنی هر دو نوع فعال؛ نوبتِ بدون بیمه باید دقیقاً مثل امروز رفتار کند (کل مبلغ سهم بیمار). - **نوعِ غیرفعال روی دادهٔ قدیمی:** اگر نوبتی نوع خدمتی دارد که بعداً غیرفعال شده، محاسبه با همان مقدارِ ذخیره‌شده انجام شود (snapshot)، ولی در فرم فقط نوع‌های فعال قابل انتخاب باشند. - **بیمهٔ تکمیلی** در این پرامپت به نوبت اضافه نمی‌شود (فقط پایه) — `calculateFinalPrice` پارامتر تکمیلی را `null` می‌گیرد؛ ساختار را طوری بنویس که افزودنش بعداً یک فیلد باشد. - **الگوهای پروژه:** `$this->success()` / `$this->paginated()` / `$this->error()` · لیست‌های ادمین `getArrayResult()` · تاریخ‌ها Unix timestamp · رشته‌های UI فارسی · در فرانت همیشه `SearchableSelect` (هرگز `