# اتصال «بیمه‌ی سرویس» در صفحه سرویس‌های کلینیک به سامانه‌ی واقعی پوشش بیمه و مطالبات

## پروژه

`clinicpro` (backend Symfony + admin React)

## زمینه

صفحه `https://clinic-pro.ddev.site/admin/clinic-services` هنگام تعریف سرویس فقط دو فیلد ساده‌ی روی خود `ServiceItem` را ست می‌کند (این بخش قبلاً با پرامپت `service-insurance-coverage.md` اضافه شده):

- `insurance_covered` (boolean) — «این خدمت شامل بیمه می‌شود»
- `insurance_price_rials` (int, nullable) — «قیمت با بیمه»

اما سامانه‌ی واقعی بیمه و مطالبات در پروژه چیز دیگری است و **به این دو فیلد وصل نیست**:

- `App\Insurance\Entity\TenantInsurance` — قرارداد بیمه‌ی فعال هر مطب/کلینیک با یک بیمه‌گر (پایه یا تکمیلی)، دارای `coverage_percent`، `franchise_rials`، `annual_ceiling_rials`.
- `App\Insurance\Entity\TenantServiceCoverage` — قاعده‌ی پوشش **به ازای هر بیمه‌گر × هر سرویس** (`covered`, `coverage_percent`, `franchise_rials`, `ceiling_rials`). همین جدول است که محاسبه‌ی سهم را تعیین می‌کند.
- `App\Insurance\ValueObject\CoverageRule` + `App\Billing\Service\BillingCalculator` — سهم بیمه‌ی پایه/مکمل و سهم بیمار را از روی همین قاعده محاسبه می‌کنند.
- `App\Billing\Service\ClaimService::createFromInvoice()` — **مطالبات بیمه** را از `Invoice` می‌سازد؛ هر `ClaimItem` فقط وقتی ساخته می‌شود که `InvoiceItem` سهم بیمه‌ی مثبت داشته باشد (`getBaseInsuranceRials()` / `getSupplementaryRials()`).

نتیجه: سرویسی که در صفحه‌ی clinic-services «شامل بیمه» علامت خورده، **هیچ ردیفی در `TenantServiceCoverage` ندارد**، پس در `BillingCalculator` سهم بیمه‌اش صفر می‌شود و در `ClaimService` هیچ `ClaimItem` نمی‌سازد ⇒ در **مطالبات بیمه ظاهر نمی‌شود**. این همان شکافی است که این پرامپت می‌بندد.

> بافت ایران: بیمه‌گرها دو دسته‌اند — **پایه** (تأمین اجتماعی، سلامت، نیروهای مسلح، …) و **تکمیلی/مکمل** (دانا، آسیا، …). هر سرویس می‌تواند با هر بیمه‌گرِ فعالِ مطب درصد پوشش، فرانشیز و سقف متفاوت داشته باشد. مدل تک‌فیلدیِ `insurance_price_rials` این تنوع را پوشش نمی‌دهد؛ منبعِ حقیقت باید `TenantServiceCoverage` باشد.

## مشکل / هدف

هنگام ویرایش یک سرویس در صفحه‌ی clinic-services، کاربر باید بتواند **پوشش بیمه‌ی آن سرویس را به ازای هر بیمه‌گرِ فعال مطب** تعریف کند (`covered` + درصد پوشش + فرانشیز + سقف). این داده باید در `TenantServiceCoverage` نوشته شود تا مستقیماً وارد چرخه‌ی `BillingCalculator → Invoice → ClaimService` (مطالبات بیمه) شود.

سه endpoint از قبل وجود دارند و باید **بازاستفاده** شوند (نه ساختن endpoint جدید):

- `GET  /api/v1/billing/tenant-insurances` → لیست قراردادهای بیمه‌ی فعال مطب.
- `GET  /api/v1/billing/tenant-insurances/{uuid}/service-coverage` → پوشش‌های یک قرارداد.
- `PUT  /api/v1/billing/tenant-insurances/{uuid}/service-coverage` → ست‌کردن پوشش یک سرویس برای آن قرارداد.

## فایل‌های مرتبط

| فایل | نقش |
|------|-----|
| `assets/admin/pages/ClinicServicesPage.tsx` | فرم ویرایش سرویس — محل افزودن بخش پوشش بیمه |
| `assets/admin/components/ServiceTariffModal.tsx` | الگوی موجود modalِ مرتبط با سرویس (برای سبک UI ردیف‌ها) |
| `src/Insurance/Controller/InsuranceController.php` | endpointهای `tenant-insurances` و `service-coverage` (خطوط ۲۹۹–۴۳۵) |
| `src/Insurance/Service/TenantInsuranceService.php` | `setServiceCoverage(...)` که ردیف `TenantServiceCoverage` را upsert می‌کند |
| `src/Insurance/Entity/TenantServiceCoverage.php` | موجودیت قاعده‌ی پوشش به‌ازای بیمه‌گر×سرویس |
| `src/Insurance/Entity/TenantInsurance.php` | قرارداد بیمه‌ی فعال مطب |
| `src/Billing/Service/BillingCalculator.php` | محاسبه‌ی سهم از روی `CoverageRule` |
| `src/Billing/Service/ClaimService.php` | ساخت مطالبات از Invoice (مصرف‌کننده‌ی نهاییِ این داده) |
| `src/ClinicService/Entity/ServiceItem.php` | فیلدهای ساده‌ی `insurance_covered` / `insurance_price_rials` و `toArray()` (فقط `uuid` برمی‌گرداند) |
| `docs/api/insurance.md` | مستند API که باید به‌روز شود |

## وضعیت فعلی

### فرم سرویس (frontend) — فقط دو فیلد ساده

`assets/admin/pages/ClinicServicesPage.tsx` خطوط ۳۹۷–۴۱۵:

```tsx
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13.5 }}>
  <input
    type="checkbox"
    checked={itemForm.watch('insurance_covered') ?? false}
    onChange={(e) => itemForm.setValue('insurance_covered', e.target.checked)}
  />
  این خدمت شامل بیمه می‌شود
</label>
{itemForm.watch('insurance_covered') && (
  <div className="field">
    <label>قیمت با بیمه (ریال)</label>
    <PriceInput
      value={itemForm.watch('insurance_price_rials') ?? 0}
      onChange={(v) => itemForm.setValue('insurance_price_rials', v)}
      placeholder="سهم بیمار با بیمه"
      min={0}
    />
  </div>
)}
```

این مقادیر فقط روی `ServiceItem` ذخیره می‌شوند و در `BillingCalculator`/`ClaimService` خوانده نمی‌شوند.

### endpoint موجود ست‌کردن پوشش (backend) — payload مورد انتظار

`src/Insurance/Controller/InsuranceController.php` خط ۴۰۹:

```php
#[Route('/api/v1/billing/tenant-insurances/{uuid}/service-coverage', methods: ['PUT'])]
public function setServiceCoverage(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
    // ... resolveEntity + بررسی مالکیت قرارداد ...
    $serviceItemId = (int) ($data['service_item_id'] ?? 0); // الزامی
    $this->tenantInsuranceService->setServiceCoverage(
        $contract,
        $serviceItemId,
        (bool)  ($data['covered']          ?? true),
        isset($data['coverage_percent']) ? (float) $data['coverage_percent'] : null,
        isset($data['franchise_rials'])  ? (int)   $data['franchise_rials']  : null,
        isset($data['ceiling_rials'])    ? (int)   $data['ceiling_rials']    : null,
    );
    return $this->success(['message' => 'پوشش خدمت ذخیره شد']);
}
```

> توجه: endpoint با `service_item_id` (id داخلی) کار می‌کند، اما frontend فقط `uuid` سرویس دارد — `ServiceItem::toArray()` فقط `uuid` می‌دهد. وظیفه ۳ این شکاف را حل می‌کند.

### موجودیت پوشش — قاعده‌ای که محاسبه را تعیین می‌کند

`TenantServiceCoverage`: `tenant_insurance_id`، `service_item_id`، `covered`، `coverage_percent` (decimal 5,2)، `franchise_rials`، `ceiling_rials`؛ یکتا روی `(tenant_insurance_id, service_item_id)`.

## وظایف

### ۱. افزودن «پوشش بیمه به ازای بیمه‌گر» به modal سرویس (frontend)

در `ClinicServicesPage.tsx`، داخل modal سرویس (بعد از فیلد «پرسنل مسئول»)، یک بخش جدید اضافه کن:

- با `useQuery` لیست قراردادهای فعال بیمه را از `GET /api/v1/billing/tenant-insurances` بگیر (نمایش نام بیمه‌گر + برچسب نوع پایه/تکمیلی).
- این بخش فقط در حالت **ویرایش سرویس** فعال باشد (در «سرویس جدید» چون هنوز id/uuid نیست، با پیام «ابتدا سرویس را ذخیره کنید» غیرفعال شود).
- برای هر قرارداد یک ردیف با کنترل‌ها: `covered` (سوییچ)، `coverage_percent` (٪، ۰ تا ۱۰۰)، `franchise_rials` (PriceInput)، `ceiling_rials` (PriceInput).
- مقدار اولیه‌ی هر ردیف از `GET /api/v1/billing/tenant-insurances/{uuid}/service-coverage` پر شود (ردیفِ متناظر با همین سرویس).
- ذخیره‌ی هر ردیف با `PUT /api/v1/billing/tenant-insurances/{uuid}/service-coverage` و body:

```ts
{
  service_item_uuid: string,          // طبق وظیفه ۳ (یا service_item_id اگر id را expose کردی)
  covered: boolean,
  coverage_percent: number | null,
  franchise_rials: number | null,
  ceiling_rials: number | null,
}
```

از الگوهای موجود استفاده کن: `api.get/put` از `lib/api.ts`، `useMutation` + `qc.invalidateQueries`، `toast`، `PriceInput` برای مبالغ ریالی، `SearchableSelect` در صورت نیاز. سبک ردیف‌ها را از `ServiceTariffModal.tsx` الگو بگیر.

```tsx
type CoverageRow = {
  contractUuid: string;
  insuranceName: string;
  type: 'basic' | 'supplementary';
  covered: boolean;
  coveragePercent: number | null;
  franchiseRials: number | null;
  ceilingRials: number | null;
};
```

### ۲. شفاف‌سازی نقش دو فیلد ساده‌ی قدیمی

`insurance_covered` / `insurance_price_rials` را حذف نکن، اما نقش‌شان را در UI روشن کن: چک‌باکس «این خدمت شامل بیمه می‌شود» به‌عنوان **پرچم نمایشی/سریع** بماند و بخش جدیدِ «پوشش به ازای بیمه‌گر» به‌عنوان **منبعِ واقعیِ محاسبه و مطالبات** معرفی شود (یک خط توضیح فارسی زیر بخش).

قبل از تصمیم، با grep بررسی کن `insurance_price_rials` و `isInsuranceCovered()` کجا مصرف می‌شوند (frontend و backend) تا چیزی نشکند.

### ۳. حل مشکل `service_item_id` در برابر `uuid` (backend)

frontend فقط `uuid` سرویس دارد ولی endpoint پوشش `service_item_id` می‌خواهد. گزینه‌ی کم‌ریسک را پیاده کن:

- **الف (ترجیح):** `setServiceCoverage` (controller + `TenantInsuranceService`) را طوری گسترش بده که اگر `service_item_id` نبود ولی `service_item_uuid` بود، id را از `ServiceItemRepository` پیدا کند و **اعتبارسنجی کند سرویس متعلق به همان مطب/کلینیکِ قرارداد است** (همان `resolveEntity`).
- خروجی `GET .../service-coverage` هم باید برای هر ردیف `service_item_uuid` بدهد (با join یا map از `service_item_id`) تا frontend ردیف درست را پیدا کند.

(اگر به‌جای آن `id` را به `ServiceItem::toArray()` افزودی، همه‌ی مصرف‌کننده‌های آن را چک کن — قرارداد API تغییر می‌کند.)

### ۴. اطمینان از جریان به مطالبات بیمه

تأیید کن (و در صورت گسست، اصلاح کن) که زنجیره کامل برقرار است:

`TenantServiceCoverage` → `CoverageRule` → `BillingCalculator::calculateItem()` → ست‌شدن `baseInsuranceRials` / `supplementaryRials` روی `InvoiceItem` → `ClaimService::buildClaim()` که فقط آیتم‌های دارای سهم مثبت را به `ClaimItem` تبدیل می‌کند.

اگر جایی این زنجیره به‌جای `TenantServiceCoverage` از `insurance_price_rials` می‌خواند، همان نقطه را اصلاح کن تا سرویسِ بیمه‌داری که اینجا تعریف می‌شود واقعاً در **مطالبات بیمه** ظاهر شود. این هسته‌ی خواسته‌ی کاربر است.

### ۵. مستندسازی

`docs/api/insurance.md` را به‌روز کن: payloadِ `PUT/GET .../service-coverage` (به‌خصوص افزودن `service_item_uuid`)، و توضیح اینکه پوشش سرویس از این مسیر روی محاسبه‌ی سهم و ساخت مطالبات اثر می‌گذارد.

## نکات مهم

- **معماری دوگانه را به‌هم نریز:** منبعِ حقیقتِ محاسبه `TenantServiceCoverage` است، نه `insurance_price_rials`.
- **پایه vs تکمیلی:** نوع بیمه‌گر از `Insurance` مرجع می‌آید (`InsuranceType: basic|supplementary`). در UI نوع را نشان بده؛ در `BillingCalculator` مکمل روی **باقیمانده‌ی بعد از پایه** اعمال می‌شود.
- **چندمستأجری (tenant):** endpointهای `service-coverage` با `resolveEntity($user)` مالکیت قرارداد را چک می‌کنند؛ گسترش backend باید همان مالکیت را برای سرویس هم اعمال کند.
- **حالت ساخت اولیه:** بخش پوشش فقط در حالت ویرایش (سرویس باید id داشته باشد). ساده‌ترین مسیر، یا بعد از ساختِ سرویس خودکار modal ویرایش باز شود.
- **مقادیر:** مبالغ با `PriceInput` و واحد ریال؛ `coverage_percent` عدد ۰ تا ۱۰۰.
- الگوهای پروژه: پاسخ‌ها با `$this->success()/$this->error()`؛ تاریخ‌ها Unix timestamp؛ admin با TanStack Query + RHF + Zod؛ JWT از `localStorage['clinicpro-auth']`.
- اگر `ServiceItem` تغییر کرد ⇒ migration با `doctrine:migrations:diff`.
- بعد از تغییر frontend: `ddev exec npx tsc --noEmit` و `ddev exec yarn dev`.
- پس از تغییر کد: `graphify update .`
