# تنظیمات بیمه به‌ازای هر پزشک در کلینیک چندپزشکه

## زمینه

درصد پوشش و شرایط هر بیمه (فرانشیز، سقف تعهد سالانه، تاریخ اعتبار، پوشش خدمات) می‌تواند برای هر پزشک متفاوت باشد. در یک کلینیک با ۳ پزشک، بیمه‌ها باید برای هر پزشک جداگانه تنظیم شوند، نه یک‌بار در سطح کلینیک.

مدل داده **از قبل** این را پشتیبانی می‌کند: هم `TenantInsurance` و هم `EntityInsurancePricing` چندریختی (polymorphic) هستند و ستون‌های `entity_type` (`'doctor'` یا `'clinic'`) و `entity_id` دارند. **نیازی به تغییر Entity یا migration نیست.**

مشکل فقط در «رزولوِ کردن موجودیت هدف» و UI است:

- اندپوینت‌های **`insurance-pricing`** (`GET/PUT /api/v1/insurance-pricing`) **از قبل** پارامتر `doctor_uuid` را می‌پذیرند و از `resolveTargetEntity()` استفاده می‌کنند → قیمت‌گذاری ویزیت هم‌اکنون per-doctor کار می‌کند. ✅
- اما اندپوینت‌های **`tenant-insurances`** (که `coverage_percent`=درصد و `franchise_rials`/`annual_ceiling_rials`=شرایط را نگه می‌دارند) از `resolveEntity($user)` استفاده می‌کنند که برای مالک کلینیک همیشه **سطح کلینیک** (`entity_type='clinic'`, `entity_id=clinicId`) برمی‌گرداند → یک مجموعه قرارداد مشترک برای هر ۳ پزشک. ❌ **این باگ اصلی است.**

## مشکل / هدف

قراردادهای بیمه (`tenant-insurances`) و پوشش خدمات (`service-coverage`) را طوری کن که مالک کلینیک (یا کاربر با مجوز) بتواند برای **هر پزشکِ** کلینیک، بیمه‌ها را جداگانه تنظیم کند — دقیقاً با همان الگویی که برای `insurance-pricing` پیاده شده (`doctor_uuid` + `resolveTargetEntity`). سپس در پنل ادمین یک انتخاب‌گر پزشک اضافه شود تا کاربر پزشک هدف را برگزیند.

**Spec (EN):** In a multi-doctor clinic, insurance contracts and their coverage percent / franchise / ceiling / service-coverage must be stored per doctor, not once per clinic. Extend the six `tenant-insurances` endpoints to accept an optional `doctor_uuid` and resolve the target entity via the existing `resolveTargetEntity()` (falling back to today's behavior when absent). Add a doctor selector to the admin insurance page for clinic owners that have more than one doctor, threading `doctor_uuid` through every query and mutation.

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

| فایل | نقش |
|------|-----|
| `src/Insurance/Controller/InsuranceController.php` | ۶ اندپوینت `tenant-insurances` که باید `doctor_uuid` بپذیرند |
| `src/Patient/Security/PatientRecordScopeResolver.php` | چرا مالک کلینیک همیشه clinic-level می‌شود (فقط برای درک — تغییر نکند) |
| `src/Insurance/Service/TenantInsuranceService.php` | متدهای `activate` / `deactivate` / `setServiceCoverage` (بررسی امضاها) |
| `src/Insurance/Entity/TenantInsurance.php` | مدل چندریختی — بدون تغییر |
| `src/Insurance/Entity/EntityInsurancePricing.php` | مدل چندریختی — بدون تغییر |
| `assets/admin/components/TenantInsuranceContracts.tsx` | UI اصلی؛ باید انتخاب‌گر پزشک + پاس‌دادن `doctor_uuid` اضافه شود |
| `assets/admin/pages/InsurancePricingPage.tsx` | صفحه میزبان؛ محل قرارگرفتن انتخاب‌گر پزشک |
| `assets/admin/components/InsuranceModal.tsx` | `buildInsurancePayload` — باید `doctor_uuid` را در payload بگنجاند |
| `assets/admin/components/ServiceInsuranceModal.tsx` | پوشش خدمات per-contract — باید `doctor_uuid` را پاس دهد |
| `docs/api/insurance.md` | مستندسازی پارامتر جدید `doctor_uuid` روی اندپوینت‌های tenant-insurances |

## وضعیت فعلی

### الگوی مرجع که از قبل per-doctor است (باید تکرار شود)

`resolveTargetEntity()` هم‌اکنون در کنترلر موجود است و درست کار می‌کند:

```php
// src/Insurance/Controller/InsuranceController.php:57
private function resolveTargetEntity(User $user, ?string $doctorUuid, string $action): array
{
    if ($doctorUuid === null || $doctorUuid === '') {
        [$type, $id] = $this->resolveEntity($user);
        return [$type, $id, null];                 // رفتار قبلی حفظ می‌شود
    }

    $doctor = $this->doctorRepo->findByUuid($doctorUuid);
    if ($doctor === null) {
        return ['unknown', null, $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پزشک یافت نشد', 404)];
    }

    if ($user->hasRole('ROLE_ADMIN') || $doctor->getUser()->getId() === $user->getId()) {
        return [EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), null];
    }

    foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) {
        if ($this->permChecker->can($user, $clinic, 'services', $action)) {
            return [EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), null];
        }
    }

    return ['unknown', null, $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'دسترسی ممنوع', 403)];
}
```

و در `getInsurancePricing`/`saveInsurancePricing` این‌طور مصرف می‌شود:

```php
[$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'view');
if ($err !== null) { return $err; }
```

### اندپوینت‌هایی که هنوز clinic-only هستند (باید اصلاح شوند)

هر ۶ اندپوینت زیر از `resolveEntity($user)` استفاده می‌کنند و `doctor_uuid` را نادیده می‌گیرند:

```php
// src/Insurance/Controller/InsuranceController.php
#[Route('/api/v1/billing/tenant-insurances', methods: ['GET'])]
public function listTenantInsurances(#[CurrentUser] User $user): JsonResponse
{
    [$entityType, $entityId] = $this->resolveEntity($user);   // ← clinic-level برای مالک کلینیک
    ...
}

#[Route('/api/v1/billing/tenant-insurances', methods: ['POST'])]
public function activateTenantInsurance(Request $request, #[CurrentUser] User $user): JsonResponse
{
    [$entityType, $entityId] = $this->resolveEntity($user);   // ←
    ...
}

// همین‌طور:
//  updateTenantInsurance(string $uuid, ...)           PATCH  /tenant-insurances/{uuid}
//  deactivateTenantInsurance(string $uuid, ...)       DELETE /tenant-insurances/{uuid}
//  listServiceCoverage(string $uuid, ...)             GET    /tenant-insurances/{uuid}/service-coverage
//  setServiceCoverage(string $uuid, ...)              PUT    /tenant-insurances/{uuid}/service-coverage
```

### چرا مالک کلینیک clinic-level می‌شود

```php
// src/Patient/Security/PatientRecordScopeResolver.php:40
if ($user->hasRole('ROLE_CLINIC')) {
    $clinic = $this->clinicRepo->findByUser($user);
    return PatientRecordScope::forClinic($clinic?->getId());   // entity_type='clinic'
}
```
این رزولوِر عمداً برای پرونده‌ها clinic-level است و **نباید تغییر کند**؛ راه‌حل، عبور `doctor_uuid` از سمت کنترلر است (مثل insurance-pricing).

### فرانت‌اند فعلی — بدون انتخاب پزشک

```tsx
// assets/admin/components/TenantInsuranceContracts.tsx:46
const contractsQuery = useQuery({
  queryKey: ['tenant-insurances'],
  queryFn: () => api.get('/api/v1/billing/tenant-insurances'),
});
const pricingQuery = useQuery({
  queryKey: ['insurance-pricing'],
  queryFn: () => api.get('/api/v1/insurance-pricing'),
});
```
هیچ مفهومی از «پزشک انتخاب‌شده» وجود ندارد؛ mutationها هم `doctor_uuid` نمی‌فرستند.

## وظایف

### ۱. Backend — عبور `doctor_uuid` در اندپوینت‌های tenant-insurances

در `InsuranceController.php`، شش اندپوینت `tenant-insurances` را از `resolveEntity($user)` به `resolveTargetEntity(...)` تغییر بده — دقیقاً مثل الگوی `insurance-pricing`:

- **`listTenantInsurances`** (GET): `doctor_uuid` را از `$request->query->get('doctor_uuid')` بگیر، `action='view'`. امضای متد به `(Request $request, #[CurrentUser] User $user)` تغییر کند.
- **`activateTenantInsurance`** (POST): `doctor_uuid` را از بدنه (`$data['doctor_uuid'] ?? null`) بگیر، `action='update'`.
- **`updateTenantInsurance`** (PATCH): `doctor_uuid` از بدنه، `action='update'`.
- **`deactivateTenantInsurance`** (DELETE): `doctor_uuid` از query، `action='update'`.
- **`listServiceCoverage`** (GET): `doctor_uuid` از query، `action='view'`.
- **`setServiceCoverage`** (PUT): `doctor_uuid` از بدنه، `action='update'`.

```php
#[Route('/api/v1/billing/tenant-insurances', methods: ['GET'])]
public function listTenantInsurances(Request $request, #[CurrentUser] User $user): JsonResponse
{
    [$entityType, $entityId, $err] = $this->resolveTargetEntity($user, $request->query->get('doctor_uuid'), 'view');
    if ($err !== null) { return $err; }
    if ($entityId === null) {
        return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
    }
    // ... بقیه بدون تغییر
}
```

**نکته حیاتی درباره تطبیق مالکیت قرارداد:** در `update`/`deactivate`/`listServiceCoverage`/`setServiceCoverage` بررسی فعلی این است:

```php
if ($contract === null || $contract->getEntityType() !== $entityType || $contract->getEntityId() !== $entityId) {
    return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'قرارداد یافت نشد', 404);
}
```
این بررسی باید **حفظ** شود؛ چون `$entityType/$entityId` حالا از `resolveTargetEntity` می‌آید، وقتی `doctor_uuid` داده شود قرارداد باید `entity_type='doctor'` و همان `entity_id` را داشته باشد — یعنی کاربر نمی‌تواند با پاس‌دادن `doctor_uuid`ِ یک پزشک، قرارداد پزشک دیگری را دستکاری کند. این رفتار درست است، فقط مطمئن شو ترتیب صحیح است (اول resolve، بعد تطبیق).

- امضای `TenantInsuranceService::activate/deactivate/setServiceCoverage` را بررسی کن؛ چون `$entityType/$entityId` را همان کنترلر پاس می‌دهد، معمولاً نیازی به تغییر سرویس نیست. اگر جایی مستقیم `resolveEntity` صدا زده می‌شود، آن را هم اصلاح کن.

### ۲. Backend — یکدست‌سازی و اعتبارسنجی

- در `setServiceCoverage`، بعد از resolve، این بررسی موجود است که section سرویس به همان tenant تعلق دارد:
  ```php
  if ($section->getEntityType() !== $entityType || $section->getEntityId() !== $entityId) { ... 403 }
  ```
  مطمئن شو با entity هدفِ per-doctor سازگار می‌ماند (سرویس‌های آن پزشک باید `entity_type='doctor'` باشند).
- Edge case: اگر `doctor_uuid` متعلق به پزشکی باشد که عضو کلینیکِ این کاربر **نیست**، `resolveTargetEntity` باید 403 برگرداند (منطق `findByDoctor` + `permChecker` این را پوشش می‌دهد). تأیید کن.

### ۳. Frontend — انتخاب‌گر پزشک در صفحه بیمه

در `TenantInsuranceContracts.tsx`:

- یک state جدید `selectedDoctorUuid: string | null` اضافه کن.
- فهرست پزشکان کلینیک را با `GET /api/v1/clinic/doctor-list/{clinicUuid}` بگیر (clinicUuid کلینیکِ فعال کاربر — از همان منبعی که بقیه صفحات کلینیک استفاده می‌کنند، مثلاً authStore/context؛ الگوی موجود را پیدا و تکرار کن).
- **فقط وقتی کاربر مالک کلینیک است و کلینیک بیش از یک پزشک دارد**، انتخاب‌گر را نشان بده. از کامپوننت `SearchableSelect` استفاده کن (قانون پروژه: هرگز `<select>` بومی). برای مطب تک‌پزشکه یا وقتی یک پزشک است، انتخاب‌گر پنهان بماند و `doctor_uuid` ارسال نشود (رفتار قبلی).
- `doctor_uuid` را در **query string** هر دو query بگنجان و در **queryKey** بیاور تا cache به‌ازای پزشک تفکیک شود:

```tsx
const dq = selectedDoctorUuid ? `?doctor_uuid=${selectedDoctorUuid}` : '';

const contractsQuery = useQuery({
  queryKey: ['tenant-insurances', selectedDoctorUuid],
  queryFn: () => api.get(`/api/v1/billing/tenant-insurances${dq}`),
});
const pricingQuery = useQuery({
  queryKey: ['insurance-pricing', selectedDoctorUuid],
  queryFn: () => api.get(`/api/v1/insurance-pricing${dq}`),
});
```

- در `invalidate()` هم کلید را با `selectedDoctorUuid` هماهنگ کن.
- در mutationها:
  - **POST/PATCH** (`saveMut`): `doctor_uuid` را داخل بدنه بفرست. `buildInsurancePayload` در `InsuranceModal.tsx` باید `doctor_uuid` را به payload اضافه کند (پارامتر جدید بگیرد یا مقدار از props).
  - **DELETE / toggle status** (`toggleMut` از PATCH استفاده می‌کند → بدنه) و اگر مسیر DELETE هم زده می‌شود → query string.
  - `ServiceInsuranceModal` (پوشش خدمات) هم `doctor_uuid` را در GET/PUT خودش پاس دهد.

### ۴. مستندات

`docs/api/insurance.md` را به‌روزرسانی کن: برای هر شش اندپوینت `tenant-insurances` پارامتر اختیاری `doctor_uuid` (query یا body) را مستند کن، با توضیح: «در نبودش رفتار پیش‌فرض (tenant کاربر) حفظ می‌شود؛ با آن، تنظیمات به‌ازای پزشک هدف اعمال می‌شود و نیازمند مالکیت/مجوز `services` روی کلینیکِ آن پزشک است.» به هم‌ترازی با `insurance-pricing` اشاره کن.

### ۵. تست

- **Backend (`ddev exec php bin/phpunit`)**: تست‌های موفق/خطا/مرزی برای `listTenantInsurances` و `activateTenantInsurance` با و بدون `doctor_uuid`:
  - مالک کلینیک با `doctor_uuid` پزشک عضو → قرارداد با `entity_type='doctor'` ساخته/خوانده شود.
  - `doctor_uuid` پزشکِ غیرعضو → 403.
  - بدون `doctor_uuid` → رفتار قبلی (clinic-level) دست‌نخورده.
  - جداسازی: قرارداد پزشک A نباید در فهرست پزشک B ظاهر شود.
- **Frontend (`yarn test` / `npx tsc --noEmit`)**: بدون خطای تایپ؛ رفتار انتخاب‌گر برای کلینیک تک‌پزشکه (مخفی) و چندپزشکه (نمایش).

## نکات مهم

- **بدون migration**: `TenantInsurance` و `EntityInsurancePricing` از قبل `entity_type/entity_id` دارند و uniqueِ آن‌ها شامل این ستون‌هاست (`uniq_tenant_insurance_version` = entity_type+entity_id+insurance_id+version)، پس ردیف‌های per-doctor مستقل‌اند. هیچ Entity تغییر نکند.
- **سازگاری عقب‌رو**: نبودِ `doctor_uuid` باید دقیقاً رفتار امروز را بدهد (`resolveEntity` fallback). این برای مطب‌های شخصی و مصرف‌کننده‌های موجود (`nobat724_front`, `clinic-pro-tauri`) حیاتی است — قرارداد فعلی نباید بشکند.
- **الگوی موجود را کپی کن، ابداع نکن**: `insurance-pricing` نقشهٔ راه است؛ همان `resolveTargetEntity`, همان resource `'services'`, همان action `'view'|'update'`.
- **BaseController**: پاسخ‌ها با `$this->success()` / `$this->error()`؛ کدهای خطا از `ErrorCodes`.
- **Frontend**: TanStack Query؛ خواندن single از `data?.data`، اما این کامپوننت قرارداد‌ها را از `data?.data?.data` می‌خواند (double-nested به‌خاطر `$this->success(['data' => ...])`) — این الگوی موجود را حفظ کن. `SearchableSelect` به‌جای `<select>`. رشته‌های UI فارسی.
- **مجوز کاربر غیرمالک**: مدیر/پرسنل کلینیک با مجوز `services` روی کلینیک هم می‌تواند بیمهٔ پزشکان را تنظیم کند (منطق `permChecker` از قبل این را پوشش می‌دهد).
