# Insurance API > **Prefix:** `/api/v1/insurances`, `/api/v1/insurance`, `/api/v1/admin/insurance` > **دسترسی منشی:** endpointهای غیرادمینِ بیمه (insurance-pricing, billing/tenant-insurances, service-coverage, `/api/v1/insurance/*`) برای `ROLE_SECRETARY` روی منبع `insurances` اعمال می‌شوند (`SecretaryAccessChecker`): GET→`view`, POST→`create`, PATCH/PUT→`update`, DELETE→`delete`؛ نبودِ مجوز → `403`. جزئیات: [secretary.md](secretary.md). Two resource types: 1. **Insurance** — master list of insurance companies managed by admin 2. **DoctorInsurance** — a doctor's acceptance of a specific insurance (with optional price) --- ## قاعدهٔ درصد پوشش (Coverage percent model) سهم بیمهٔ پایه فقط درصدی است: ``` سهم بیمهٔ پایه = round(مبلغ کل × درصد پوشش ÷ 100) سهم بیمار = مبلغ کل − سهم بیمهٔ پایه (فرانشیز در بیمهٔ پایه دخالت ندارد) ``` درصد پوشش به تفکیک **نوع خدمت** تعیین می‌شود. لیست انواع از `GET /api/v1/service-categories` می‌آید (فعلاً `outpatient` = خدمات سرپایی و `inpatient` = خدمات بستری) و هرگز در کلاینت hardcode نمی‌شود. ویزیت همیشه `outpatient` است. درصد مؤثر به این ترتیب resolve می‌شود (اولین مقدار موجود برنده است): | اولویت | منبع | جدول | |---|---|---| | ۱ | override همان خدمت | `tenant_service_coverage.coverage_percent` | | ۲ | override قرارداد برای نوع خدمت | `tenant_insurance_category_coverage` | | ۳ | پیش‌فرض مرکزی ادمین (اگر > ۰ باشد) | `insurance_coverage_defaults` | | ۴ | `coverage_percent` قرارداد (سازگاری با ردیف‌های قدیمی) | `tenant_insurances` | **fallback زنده است، نه کپی:** قراردادی که ردیف سطح ۲ ندارد، با تغییر پیش‌فرض ادمین خودبه‌خود به‌روز می‌شود. `franchise_percent` فقط در قراردادهای `supplementary` اثر دارد. --- ## GET `/api/v1/insurances` List all active insurances. **Permission:** `PUBLIC` ### Query Parameters | Param | Type | Required | Description | |-------|------|----------|-------------| | `type` | string | ❌ | `"basic"` or `"supplementary"` | ### Response `200` ```json { "success": true, "data": [ { "id": 1, "name": "بیمه تأمین اجتماعی", "type": "basic", "logo_url": "https://...", "status": "active", "coverage_defaults": { "outpatient": 70, "inpatient": 30 } }, { "id": 2, "name": "بیمه ایران", "type": "supplementary", "logo_url": "https://...", "status": "active", "coverage_defaults": { "outpatient": 0, "inpatient": 0 } } ] } ``` `coverage_defaults` درصدهای مرکزی ادمین به تفکیک نوع خدمت است؛ همیشه همهٔ نوع‌ها حاضرند (نبودِ ردیف = `0`). پنل پزشک هنگام ساخت قرارداد همین مقادیر را پیش‌فرض بار می‌کند. --- ## GET `/api/v1/admin/insurances` List all insurances with pagination (admin view — includes inactive). **Permission:** `ROLE_ADMIN` ### Query Parameters | Param | Type | Required | Description | |-------|------|----------|-------------| | `page` | integer | ❌ | Default: 1 | | `limit` | integer | ❌ | Default: 20 | | `search` | string | ❌ | Search in name | | `type` | string | ❌ | `"basic"` or `"supplementary"` | ### Response `200` هر ردیف علاوه بر فیلدهای بیمه، `coverage_defaults` خود را هم دارد (یک کوئری برای کل صفحه، بدون N+1). ```json { "success": true, "data": [ { "id": 1, "name": "بیمه تأمین اجتماعی", "type": "basic", "logo_url": "https://...", "status": "active", "coverage_defaults": { "outpatient": 70, "inpatient": 30 } } ], "meta": { "totalRecords": 15, "totalPages": 1, "currentPage": 1 } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | --- ## GET `/api/v1/admin/insurance/{id}/coverage-defaults` درصدهای پوشش مرکزی یک بیمه به تفکیک نوع خدمت. همیشه **همهٔ** نوع‌ها برمی‌گردند (ردیف نداشته = `0`)، تا پنل ادمین جدول کامل نشان دهد. **Permission:** `ROLE_ADMIN` ### Response `200` ```json { "success": true, "data": { "insurance_id": 3, "categories": [ { "key": "outpatient", "label": "خدمات سرپایی", "coverage_percent": 70 }, { "key": "inpatient", "label": "خدمات بستری", "coverage_percent": 30 } ] } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_VALIDATION_002` | 404 | بیمه یافت نشد | --- ## PUT `/api/v1/admin/insurance/{id}/coverage-defaults` ذخیرهٔ درصدهای مرکزی. تغییر این مقادیر بی‌درنگ روی همهٔ قراردادهایی که برای همان نوع خدمت override ندارند اثر می‌گذارد. **Permission:** `ROLE_ADMIN` ### Request Body (`application/json`) ```json { "categories": [ { "key": "outpatient", "coverage_percent": 70 }, { "key": "inpatient", "coverage_percent": 30 } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `categories` | array | ✅ | ردیف‌هایی که باید ذخیره شوند؛ ردیف‌های نیامده دست‌نخورده می‌مانند | | `categories[].key` | string | ✅ | یکی از مقادیر `GET /api/v1/service-categories` | | `categories[].coverage_percent` | number | ✅ | ۰ تا ۱۰۰ | ### Response `200` همان ساختار پاسخِ `GET` (وضعیت پس از ذخیره). ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_VALIDATION_002` | 404 | بیمه یافت نشد | | `ERR_VALIDATION_001` | 422 | `key` نامعتبر یا درصد خارج از بازهٔ ۰ تا ۱۰۰ | --- ## POST `/api/v1/admin/insurance` Create a new insurance. **Permission:** `ROLE_ADMIN` ### Request Body (`application/json`) ```json { "name": "بیمه تأمین اجتماعی", "type": "basic", "logo_url": "https://...", "status": "active" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | ✅ | Insurance name | | `type` | string | ✅ | `"basic"` or `"supplementary"` | | `logo_url` | string | ❌ | Logo image URL | | `status` | string | ❌ | `"active"` (default) or `"inactive"` | ### Response `201` Insurance object. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_VALIDATION_002` | 422 | Missing required field | --- ## PATCH `/api/v1/admin/insurance/{id}` Update an insurance. **Permission:** `ROLE_ADMIN` ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `id` | integer | Insurance ID | All body fields optional. ### Response `200` Updated insurance object. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Insurance not found | --- ## DELETE `/api/v1/admin/insurance/{id}` Delete an insurance. **Permission:** `ROLE_ADMIN` ### Response `200` ```json { "success": true, "data": { "message": "بیمه حذف شد" } } ``` --- ## POST `/api/v1/admin/insurance/{id}/upload-logo` Upload insurance logo. **Permission:** `ROLE_ADMIN` ### Request `Content-Type: multipart/form-data` | Field | Type | Required | |-------|------|----------| | `file` | binary | ✅ | ### Response `200` ```json { "success": true, "data": { "url": "https://...", "uuid": "...", "filename": "insurance_logo.png", "filemime": "image/png", "filesize": 51200 } } ``` --- ## POST `/api/v1/insurance/` Add an insurance to a doctor's accepted list. **Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.create` permission) ### Request Body (`application/json`) ```json { "doctor_id": 42, "insurance_id": 1, "price": 150000 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `doctor_id` | integer | ✅ | Doctor's numeric ID | | `insurance_id` | integer | ✅ | Insurance ID | | `price` | integer | ❌ | Visit price for this insurance (Rials) | ### Response `201` ```json { "success": true, "data": { "id": 10, "doctor_id": 42, "insurance": { "id": 1, "name": "بیمه تأمین اجتماعی", "type": "basic" }, "price": 150000 } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_FORBIDDEN_001` | 403 | Not the doctor | | `ERR_NOT_FOUND_001` | 404 | Doctor or insurance not found | | `ERR_CONFLICT_001` | 409 | Insurance already added to doctor | --- ## GET `/api/v1/insurance/{id}` Get a doctor-insurance link. **Permission:** `AUTH` — must be the owning doctor or `ROLE_ADMIN` (otherwise `403 ERR_AUTH_006`). Prevents reading another doctor's negotiated price by id enumeration. ### Response `200` DoctorInsurance object. --- ## PATCH `/api/v1/insurance/{id}` Update a doctor-insurance (e.g., change price). **Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.update` permission) ### Request Body ```json { "price": 200000 } ``` ### Response `200` Updated DoctorInsurance object. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_FORBIDDEN_001` | 403 | Not the doctor | | `ERR_NOT_FOUND_001` | 404 | Link not found | --- ## DELETE `/api/v1/insurance/{id}` Remove an insurance from a doctor's list. **Permission:** `AUTH` — must be the doctor (or their secretary with `insurances.delete` permission) ### Response `200` ```json { "success": true, "data": { "message": "بیمه از لیست حذف شد" } } ``` --- ## EntityInsurancePricing — قیمت‌گذاری ویزیت بر اساس بیمه قیمت‌گذاری ویزیت برای entity جاری (پزشک یا کلینیک)، با تفکیک: - **ویزیت آزاد** (بدون بیمه) — یک مبلغ پایه (ردیفی با `insurance_id = null`) - **سهم بیمار به ازای هر بیمه** پایه/مکمل entity جاری از `#[CurrentUser]` resolve می‌شود: نقش `ROLE_DOCTOR` → `doctor`، نقش `ROLE_CLINIC` → `clinic`. ذخیره‌سازی polymorphic در جدول `entity_insurance_pricing` (`entity_type`, `entity_id`, `insurance_id` nullable, `patient_share_rials`). --- ## GET `/api/v1/insurance-pricing` قیمت‌گذاری بیمه‌ی entity جاری + لیست همه‌ی بیمه‌های فعال (با سهم بیمار اگر تعیین شده). **Permission:** `AUTH` (`ROLE_DOCTOR` یا `ROLE_CLINIC`) ### Query Parameters | Param | Type | Required | Description | |-------|------|----------|-------------| | `doctor_uuid` | string (UUID) | ❌ | قیمت‌گذاری همان پزشک را برمی‌گرداند به‌جای موجودیت کاربر جاری. برای تب‌های نوبت‌دهی پنل کلینیک. | با `doctor_uuid`، دسترسی این‌گونه بررسی می‌شود: `ROLE_ADMIN`، خودِ پزشک، مالک کلینیکی که پزشک عضو آن است، یا پزشکِ عضو همان کلینیک با مجوز `services.view` (برای `PUT`: `services.update`). در غیر این صورت `403 ERR_ACCESS_DENIED`؛ پزشکِ ناموجود `404 ERR_NOT_FOUND_001`. بدون این پارامتر رفتار قبلی (موجودیت کاربر جاری) دست‌نخورده است. ### Response `200` ```json { "success": true, "data": { "entity_type": "doctor", "entity_id": 7, "free_visit_price_rials": 5000000, "require_visit_price": false, "insurances": [ { "insurance_id": 3, "insurance_name": "تأمین اجتماعی", "type": "basic", "patient_share_rials": 1500000, "coverage_defaults": { "outpatient": 70, "inpatient": 30 } }, { "insurance_id": 9, "insurance_name": "دانا", "type": "supplementary", "patient_share_rials": null, "coverage_defaults": { "outpatient": 0, "inpatient": 0 } } ] } } ``` همچنین دو کلید سراسریِ tenant: ```json { "service_categories": [ { "key": "outpatient", "label": "خدمات سرپایی", "enabled": true }, { "key": "inpatient", "label": "خدمات بستری", "enabled": false } ], "default_service_category": "outpatient" } ``` | فیلد | توضیح | |------|-------| | `service_categories` | نوع خدماتی که بیمه‌های این پزشک/کلینیک پوشش می‌دهند — **سراسری برای همهٔ بیمه‌های همان tenant**، نه per-insurance. همیشه همهٔ نوع‌ها برمی‌گردند؛ نبودِ ردیف در DB = `enabled: true`. پنل: کارت «نوع خدمات بیمه» در تنظیمات ← مدیریت بیمه (`/admin/insurance-pricing`) | | `default_service_category` | اگر فقط یک نوع فعال باشد همان (مبنای خودکار محاسبه)؛ اگر بیش از یکی فعال باشد `null` و پنل باید سرِ پذیرش بپرسد | - `coverage_defaults` — درصدهای مرکزی ادمین؛ پنل پزشک هنگام افزودن قرارداد از همین پر می‌کند. - `patient_share_rials = null` یعنی این بیمه پذیرفته نمی‌شود (قیمت‌گذاری ندارد). این مقدار **ورودی هیچ محاسبه‌ای نیست**؛ محاسبهٔ سهم فقط از درصد پوشش انجام می‌شود. - `require_visit_price` — فلگ «الزامی کردن هزینه ویزیت». وقتی `true` باشد، ثبت مراجعه (session)، فاکتور سرویس و ثبت نوبت بدون هزینه ویزیت (`> 0`) رد می‌شوند. ### خطاها - `403` `ERR_FORBIDDEN_001` — پروفایل (doctor/clinic) برای کاربر یافت نشد. --- ## PUT `/api/v1/insurance-pricing` ذخیره/به‌روزرسانی قیمت ویزیت آزاد و سهم بیمار هر بیمه. عملیات upsert؛ ردیفی که `patient_share_rials = null` بفرستد حذف می‌شود. **Permission:** `AUTH` (`ROLE_DOCTOR` یا `ROLE_CLINIC`) ### Request Body > `doctor_uuid` (اختیاری) در بدنه پذیرفته می‌شود و مثل نسخهٔ `GET` عمل می‌کند — همان قواعد دسترسی، با اکشن `services.update`. ```json { "doctor_uuid": "550e8400-e29b-41d4-a716-446655440000", "free_visit_price_rials": 5000000, "require_visit_price": true, "insurances": [ { "insurance_id": 3, "patient_share_rials": 1500000 }, { "insurance_id": 9, "patient_share_rials": null } ] } ``` | فیلد | نوع | توضیح | |------|-----|-------| | `free_visit_price_rials` | int | مبلغ ویزیت آزاد (ریال). اختیاری؛ اگر نباشد تغییر نمی‌کند. | | `require_visit_price` | bool | فلگ «الزامی کردن هزینه ویزیت». اختیاری؛ اگر نباشد مقدار ذخیره‌شده حفظ می‌شود. | | `insurances[].insurance_id` | int | شناسه‌ی بیمه (الزامی برای هر ردیف). | | `insurances[].patient_share_rials` | int \| null | سهم بیمار با این بیمه. `null` → ردیف حذف می‌شود. | | `service_categories` | array | اختیاری — `[{ "key": "inpatient", "enabled": false }]`. فقط نوع‌های ارسالی تغییر می‌کنند؛ نیامدنِ کلید یعنی تنظیمات دست‌نخورده. | اعتبارسنجی: اگر فلگ مؤثر (ارسالی یا ذخیره‌شده) `true` باشد و قیمت مؤثر (ارسالی یا ذخیره‌شده) `<= 0`، درخواست رد می‌شود. اعتبارسنجی `service_categories`: `key ∈ ServiceCategory::values()` و پس از اعمالِ تغییر **حداقل یک نوع فعال بماند** — وگرنه `422 ERR_VALIDATION_001` با فیلد `service_categories`. ### Response `200` همان ساختار `GET /api/v1/insurance-pricing` (وضعیت پس از ذخیره). ### خطاها - `403` `ERR_FORBIDDEN_001` — پروفایل یافت نشد. - `422` `ERR_VALIDATION_001` (field: `free_visit_price_rials`) — فلگ الزامی فعال است ولی قیمت ویزیت آزاد `<= 0`. - `422` `ERR_VALIDATION_001` (field: `service_categories`) — نوع خدمت نامعتبر، یا غیرفعال‌کردن همهٔ نوع‌ها. --- ## TenantInsurance — قراردادهای بیمه‌ی tenant (فاز ۱ سیستم صورتحساب) قرارداد یک پزشک/کلینیک با یک بیمه: درصد پوشش، فرانشیز، سقف تعهد سالانه، نسخه‌بندی و وضعیت فعال. مبنای محاسبه‌ی سهم در سیستم صورتحساب (`docs/architecture/insurance-billing-system.md`). جدول `tenant_insurances`. tenant از `#[CurrentUser]` با `App\Patient\Security\PatientRecordScopeResolver` resolve می‌شود — همان رزولور پرونده‌ها و صورتحساب‌ها، تا قرارداد بیمه و صورتحسابی که از آن ساخته می‌شود هرگز به دو محیط متفاوت نیفتند. محیط فعال (`UserActiveContext`) تعیین‌کننده است، نه صرفاً ترتیب نقش‌ها؛ مالک کلینیکی که خودش پزشک هم هست، قراردادهای **کلینیک** خود را می‌بیند. **تنظیمات per-doctor در کلینیک چندپزشکه:** درصد و شرایط هر بیمه می‌تواند برای هر پزشک متفاوت باشد. همهٔ اندپوینت‌های زیر یک پارامتر اختیاری `doctor_uuid` می‌پذیرند (در `GET`/`DELETE` از query، در `POST`/`PATCH`/`PUT` از بدنه). با آن، قرارداد به‌جای موجودیتِ tenantِ کاربر جاری، به‌ازای پزشک هدف (`entity_type='doctor'`) خوانده/نوشته می‌شود — دقیقاً مثل `insurance-pricing`. **بدون** آن، رفتار قبلی (tenant کاربر جاری) دست‌نخورده می‌ماند (سازگاری عقب‌رو). دسترسی با `doctor_uuid` هم مثل `insurance-pricing` بررسی می‌شود: `ROLE_ADMIN`، خودِ پزشک، یا کاربرِ عضو/مالکِ کلینیکِ آن پزشک با مجوز `services.view` (برای نوشتن `services.update`)؛ در غیر این صورت `403 ERR_ACCESS_DENIED`، و پزشکِ ناموجود `404 ERR_NOT_FOUND_001`. ### GET `/api/v1/billing/tenant-insurances` لیست قراردادهای tenant جاری — **آخرین نسخهٔ هر بیمه، فعال یا غیرفعال** (برای toggle فعال/غیرفعال در UI مدیریت بیمه). `insurance_kind` = `kind` قرارداد در صورت تعیین، وگرنه نوع بیمه از کاتالوگ. **Query:** `doctor_uuid` (اختیاری) — قراردادهای همان پزشک را برمی‌گرداند (نگاه کنید به «تنظیمات per-doctor» بالا). **Permission:** `AUTH` (doctor/clinic) ```json { "success": true, "data": { "data": [ { "uuid": "…", "insurance_id": 3, "insurance_name": "تأمین اجتماعی", "insurance_kind": "basic", "version": 1, "is_active": true, "coverage_percent": 70, "franchise_percent": 0, "annual_ceiling_rials": null, "kind": "basic", "effective_from": 1718900000, "effective_to": null, "category_coverages": { "outpatient": 70, "inpatient": 30 }, "category_coverage_source": { "outpatient": "override", "inpatient": "admin_default" } } ] } } ``` | فیلد | توضیح | |------|-------| | `category_coverages` | درصد **مؤثر** هر نوع خدمت پس از اجرای زنجیرهٔ resolve | | `category_coverage_source` | منبع هر درصد: `override` (خودِ قرارداد) · `admin_default` (تنظیمات مرکزی) · `contract` (ستون قدیمی `coverage_percent`) | | `coverage_percent` | ستون قدیمی قرارداد؛ فقط آخرین سطح fallback است | | `franchise_percent` | فقط در قرارداد `supplementary` معنا دارد | ### POST `/api/v1/billing/tenant-insurances` فعال‌سازی/به‌روزرسانی قرارداد. اگر قرارداد فعالی برای آن بیمه باشد ویرایش می‌شود، وگرنه نسخه‌ی جدید. **Body:** | فیلد | نوع | توضیح | |------|-----|-------| | `insurance_id` | int | الزامی | | `coverage_percent` | float | ستون قدیمی قرارداد (آخرین سطح fallback)؛ پنل آن را با درصد سرپایی همگام می‌فرستد | | `franchise_percent` | float | فرانشیز **درصدی** (۰ تا ۱۰۰) — سهم اجباری بیمار از مبلغ تحت پوشش؛ فقط در قرارداد `supplementary` اثر دارد. خارج از بازه → `422` با `field: franchise_percent` | | `annual_ceiling_rials` | int \| null | سقف تعهد (null = بی‌نهایت) | | `kind` | string \| null | نوع بیمه قرارداد (`basic`/`supplementary`); خالی → پیش‌فرض نوع کاتالوگ | | `effective_from` | int \| null | تاریخ شروع قرارداد (Unix)؛ null → اکنون | | `effective_to` | int \| null | تاریخ پایان قرارداد (Unix)؛ null → نامحدود | | `category_coverages` | array \| null | اختیاری — override درصد به تفکیک نوع خدمت. **نیامدنش** یعنی قرارداد روی پیش‌فرض مرکزی ادمین می‌ماند (fallback زنده) | | `category_coverages[].key` | string | یکی از مقادیر `GET /api/v1/service-categories` | | `category_coverages[].coverage_percent` | number \| null | ۰ تا ۱۰۰؛ `null` → override آن نوع حذف و به پیش‌فرض ادمین برمی‌گردد | **اجبار درصد برای نوع خدمتِ فعال:** با ارسال `category_coverages`، هر نوع خدمتی که در تنظیمات همین tenant فعال است باید درصد مؤثر بزرگ‌تر از صفر داشته باشد — از خود payload، از override قبلی، یا از پیش‌فرض مرکزی ادمین. ستون قدیمی `coverage_percent` قرارداد اینجا fallback حساب نمی‌شود، وگرنه نوع خدمتی که درصدش نیامده بی‌صدا نرخ نوع دیگر را ارث می‌برد. ```json { "success": false, "errors": [ { "code": "ERR_VALIDATION_001", "field": "category_coverages", "message": "درصد پوشش خدمات بستری الزامی است" } ] } ``` | `doctor_uuid` | string (UUID) \| null | اختیاری — قرارداد را به‌ازای پزشک هدف ذخیره می‌کند (نگاه کنید به «تنظیمات per-doctor» بالا) | ```json { "insurance_id": 3, "kind": "basic", "category_coverages": [ { "key": "outpatient", "coverage_percent": 70 }, { "key": "inpatient", "coverage_percent": 30 } ] } ``` پاسخ `201`: `{ success, data: { …contract, category_coverages, category_coverage_source } }`. خطاها: `404 ERR_NOT_FOUND_001` بیمه یافت نشد · `422 ERR_VALIDATION_001` insurance_id الزامی، یا `key` نامعتبر / درصد خارج از ۰–۱۰۰ · `403 ERR_FORBIDDEN_001` پروفایل یافت نشد، یا ارسال `category_coverages` بدون مجوز `insurances.update`. ### PATCH `/api/v1/billing/tenant-insurances/{uuid}` ویرایش فیلدهای قرارداد (همه اختیاری، فقط کلیدهای موجود اعمال می‌شوند). فقط قرارداد متعلق به tenant جاری. **Body:** `coverage_percent` · `franchise_percent` · `annual_ceiling_rials` · `kind` · `effective_from` · `effective_to` · `is_active` · `category_coverages` (همان ساختار `POST`؛ ارسالش نیازمند مجوز `insurances.update` است وگرنه `403 ERR_FORBIDDEN_001`) · `doctor_uuid` (اختیاری، برای هدف‌گیری پزشک — نگاه کنید به «تنظیمات per-doctor» بالا). - `is_active` (bool): toggle فعال/غیرفعال. برخلاف `DELETE`، مقدار `effective_to`ِ تعیین‌شدهٔ کاربر را دست‌نخورده نگه می‌دارد (برای reactivate). - قرارداد باید به همان موجودیتِ resolve‌شده (پزشک هدف یا tenant کاربر) تعلق داشته باشد، وگرنه `404`. ### DELETE `/api/v1/billing/tenant-insurances/{uuid}` غیرفعال‌سازی نرم (soft) — `is_active=false` و `effective_to=now`. داده حذف نمی‌شود. **Query:** `doctor_uuid` (اختیاری) — برای غیرفعال‌سازی قرارداد یک پزشک خاص. ```json { "success": true, "data": { "message": "قرارداد بیمه غیرفعال شد" } } ``` > **Guard:** `TenantInsuranceService::assertActive()` هنگام پذیرش/صورتحساب فقط بیمه‌های فعالِ همان tenant را مجاز می‌داند؛ در غیر این صورت `422 ERR_VALIDATION_001` («این بیمه برای این کلینیک/پزشک فعال نیست»). --- ## TenantServiceCoverage — پوشش خدمت تحت یک قرارداد بیمه (فاز ۲) override پوشش یک خدمت خاص تحت قرارداد یک بیمه. فیلدهای `null` از خود قرارداد ارث می‌برند. اگر `covered=false` → آن خدمت تحت آن بیمه پوشش ندارد. جدول `tenant_service_coverage`. ### GET `/api/v1/billing/tenant-insurances/{uuid}/service-coverage` لیست overrideهای پوشش خدمات یک قرارداد. **Query:** `doctor_uuid` (اختیاری) — برای قراردادِ متعلق به پزشک هدف در کلینیک چندپزشکه. **Permission:** `AUTH` (مالک قرارداد) ```json { "success": true, "data": { "data": [ { "uuid": "…", "tenant_insurance_id": 4, "service_item_id": 12, "service_item_uuid": "…", "covered": true, "coverage_percent": 80, "franchise_percent": null, "ceiling_rials": null } ] } } ``` ### PUT `/api/v1/billing/tenant-insurances/{uuid}/service-coverage` تنظیم/به‌روزرسانی پوشش یک خدمت (upsert). **Body:** | فیلد | نوع | توضیح | |------|-----|-------| | `service_item_uuid` | string | شناسه‌ی خدمت (ترجیحی؛ پنل ادمین فقط uuid دارد) | | `service_item_id` | int | جایگزین `service_item_uuid` (id داخلی) — یکی از این دو الزامی | | `covered` | bool | پیش‌فرض true | | `coverage_percent` | float \| null | null = ارث از قرارداد | | `franchise_percent` | int \| null | null = ارث از قرارداد | | `ceiling_rials` | int \| null | null = ارث از قرارداد | | `doctor_uuid` | string (UUID) \| null | اختیاری — قراردادِ متعلق به پزشک هدف (نگاه کنید به «تنظیمات per-doctor» بالا) | سرویس باید متعلق به همان مطب/کلینیکِ قرارداد باشد (`ServiceItem→section→entity_type/entity_id`). با `doctor_uuid`، موجودیت هدف پزشک است، پس سرویس هم باید متعلق به همان پزشک باشد. **اثر جانبی — همگام‌سازی `ServiceItem.insurance_covered`:** پس از ذخیره‌ی ردیف پوشش، پرچم `insurance_covered` همان خدمت بازمحاسبه می‌شود: اگر زیر **هر** قرارداد بیمه‌ای دست‌کم یک ردیف با `covered=true` بماند ⇒ `true`، وگرنه `false`. پنل ادمین دیگر این پرچم را دستی نمی‌فرستد (سوییچ «این خدمت شامل بیمه می‌شود» از فرم سرویس حذف شد)، پس این endpoint تنها منبع حقیقت آن است. پیاده‌سازی: `TenantInsuranceService::syncServiceItemInsuranceFlag()` + `TenantServiceCoverageRepository::hasActiveCoverage()`. پاسخ `200`: `{ success, data: { message } }`. خطاها: `404 ERR_NOT_FOUND_001` قرارداد یافت نشد · `422 ERR_VALIDATION_001` سرویس یافت نشد · `403 ERR_FORBIDDEN_001` سرویس متعلق به شما نیست. > منطق resolve: `TenantInsuranceService::coverageRuleForService()` ابتدا **پرچم `ServiceItem.insurance_covered`** را چک می‌کند؛ اگر این خدمت «شامل بیمه» نباشد، بدون توجه به override یا قرارداد، `CoverageRule::notCovered()` برمی‌گردد (gate نهایی). سپس override خدمت بررسی می‌شود؛ اگر `covered=false` → `notCovered()`؛ در غیر این صورت فیلدهای null از قرارداد پر می‌شوند. این `CoverageRule` ورودی `BillingCalculator` است و سهم بیمه‌ی هر `InvoiceItem` را تعیین می‌کند؛ همان سهم‌ها در `ClaimService::createFromInvoice()` به `ClaimItem` (مطالبات بیمه) تبدیل می‌شوند. پنل ادمین این endpoint را از مودال «پوشش بیمه» در صفحه سرویس‌های کلینیک فراخوانی می‌کند. --- ## Bulk import / export Full-table JSON export and strict wipe+replace import for this category live under `/api/v1/admin/categories/{bundle}/{export|import}` — see [category-import.md](category-import.md). ### Sorting by id The admin list endpoint accepts `sort=id&order=asc|desc` to order by `id` (used by the admin «دسته‌بندی‌ها» page when clicking the «شناسه» column). Without `sort`, the default ordering (weight/name) is unchanged.