diff --git a/.claude/prompt/service-insurance-per-insurer-coverage.md b/.claude/prompt/service-insurance-per-insurer-coverage.md
new file mode 100644
index 00000000..65079f62
--- /dev/null
+++ b/.claude/prompt/service-insurance-per-insurer-coverage.md
@@ -0,0 +1,181 @@
+# اتصال «بیمهی سرویس» در صفحه سرویسهای کلینیک به سامانهی واقعی پوشش بیمه و مطالبات
+
+## پروژه
+
+`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
+
+{itemForm.watch('insurance_covered') && (
+
+)}
+```
+
+این مقادیر فقط روی `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 .`
diff --git a/assets/admin/components/ServiceInsuranceModal.tsx b/assets/admin/components/ServiceInsuranceModal.tsx
new file mode 100644
index 00000000..10a9ae98
--- /dev/null
+++ b/assets/admin/components/ServiceInsuranceModal.tsx
@@ -0,0 +1,221 @@
+import { useEffect, useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { ShieldCheckIcon, CheckIcon } from '@heroicons/react/24/outline';
+import { toast } from 'sonner';
+import { api } from '../lib/api';
+import Modal from './ui/Modal';
+import PriceInput from './ui/PriceInput';
+import type { ServiceItem } from '../types';
+
+interface TenantInsurance {
+ uuid: string;
+ insurance_name: string | null;
+ insurance_kind: 'basic' | 'supplementary' | null;
+ coverage_percent: number;
+}
+
+interface CoverageRow {
+ service_item_uuid: string | null;
+ covered: boolean;
+ coverage_percent: number | null;
+ franchise_rials: number | null;
+ ceiling_rials: number | null;
+}
+
+interface Draft {
+ covered: boolean;
+ coverage_percent: number | null;
+ franchise_rials: number | null;
+ ceiling_rials: number | null;
+}
+
+const KIND = {
+ basic: { label: 'پایه', cls: 'blue' },
+ supplementary: { label: 'تکمیلی', cls: 'violet' },
+} as const;
+
+function ContractCard({ contract, item }: { contract: TenantInsurance; item: ServiceItem }) {
+ const qc = useQueryClient();
+
+ const { data, isLoading } = useQuery<{ data: { data: CoverageRow[] } }>({
+ queryKey: ['service-coverage', contract.uuid],
+ queryFn: () => api.get(`/api/v1/billing/tenant-insurances/${contract.uuid}/service-coverage`),
+ });
+
+ const rows = (data as any)?.data?.data as CoverageRow[] | undefined;
+ const existing = rows?.find((r) => r.service_item_uuid === item.uuid);
+
+ const [draft, setDraft] = useState({ covered: true, coverage_percent: null, franchise_rials: null, ceiling_rials: null });
+
+ useEffect(() => {
+ setDraft(existing
+ ? {
+ covered: existing.covered,
+ coverage_percent: existing.coverage_percent,
+ franchise_rials: existing.franchise_rials,
+ ceiling_rials: existing.ceiling_rials,
+ }
+ : { covered: true, coverage_percent: null, franchise_rials: null, ceiling_rials: null });
+ }, [existing]);
+
+ const saveMut = useMutation({
+ mutationFn: () =>
+ api.put(`/api/v1/billing/tenant-insurances/${contract.uuid}/service-coverage`, {
+ service_item_uuid: item.uuid,
+ covered: draft.covered,
+ coverage_percent: draft.coverage_percent,
+ franchise_rials: draft.franchise_rials,
+ ceiling_rials: draft.ceiling_rials,
+ }),
+ onSuccess: () => {
+ toast.success('پوشش بیمه ذخیره شد');
+ qc.invalidateQueries({ queryKey: ['service-coverage', contract.uuid] });
+ },
+ onError: (e: Error) => toast.error(e.message),
+ });
+
+ const kind = contract.insurance_kind ? KIND[contract.insurance_kind] : null;
+
+ return (
+