Files
clinicpro/docs/api/insurance.md
T
hamedandClaude Opus 5 1f58b1b9b3 feat(insurance): bill an appointment with a chosen service kind and insurance
An appointment can now carry the insurance it is billed with: the service kind
(outpatient/inpatient) and the basic insurance. Confirming it no longer hands the
whole amount to the patient — the visit is split through BillingCalculator with the
coverage percent of that service kind, and the choice travels to the encounter and
the invoice built from it.

The enabled service kinds are a tenant-wide setting (all of that tenant's
insurances share it), so a tenant covering only one kind is never asked which one:
the panel resolves it the same way the server does.

- add tenant_service_category_settings + TenantServiceCategoryService, exposed on
  the existing insurance-pricing endpoint (service_categories,
  default_service_category); at least one kind must stay enabled
- add appointments.insurance_service_category / insurance_base_id with
  AppointmentInsuranceService validating them against the tenant's own settings
  and active contracts (basic only), accepted by PATCH and by confirm
- snapshot the kind on patient_sessions and invoices; the visit's coverage rule is
  resolved per kind (services keep using their own ServiceItem.service_category)
- lib/insuranceShares becomes the single client-side mirror of BillingCalculator,
  shared by the confirm modal, the appointment edit page and the session form
- surface the selection: confirm modal (with live shares), turns timeline chip,
  appointment edit page, patient record service card and invoice summary
- the session form shows the insurance block whenever the tenant has an active
  contract and prefills the patient's own insurance, so it can be changed
- fix: the confirm modal showed a zero visit price when the appointment had none —
  it now falls back to the tenant's free-visit price like the server
- fix: useServiceCategories read one level too shallow, so Persian labels never
  arrived and raw enum keys leaked into the contract summary
- fix: BlogsPage test asserted the public blogs endpoint after the page moved to
  the admin one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 17:50:14 +03:30

29 KiB
Raw Blame History

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.

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_rials فقط در قراردادهای supplementary اثر دارد.


GET /api/v1/insurances

List all active insurances.

Permission: PUBLIC

Query Parameters

Param Type Required Description
type string "basic" or "supplementary"

Response 200

{
  "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).

{
  "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

{
  "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)

{
  "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)

{
  "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

{ "success": true, "data": { "message": "بیمه حذف شد" } }

Upload insurance logo.

Permission: ROLE_ADMIN

Request

Content-Type: multipart/form-data

Field Type Required
file binary

Response 200

{
  "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)

{
  "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

{
  "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

{ "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

{ "success": true, "data": { "message": "بیمه از لیست حذف شد" } }

EntityInsurancePricing — قیمت‌گذاری ویزیت بر اساس بیمه

قیمت‌گذاری ویزیت برای entity جاری (پزشک یا کلینیک)، با تفکیک:

  • ویزیت آزاد (بدون بیمه) — یک مبلغ پایه (ردیفی با insurance_id = null)
  • سهم بیمار به ازای هر بیمه پایه/مکمل

entity جاری از #[CurrentUser] resolve می‌شود: نقش ROLE_DOCTORdoctor، نقش ROLE_CLINICclinic. ذخیره‌سازی 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

{
  "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:

{
  "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.

{
  "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)

{
  "success": true,
  "data": {
    "data": [
      {
        "uuid": "…",
        "insurance_id": 3,
        "insurance_name": "تأمین اجتماعی",
        "insurance_kind": "basic",
        "version": 1,
        "is_active": true,
        "coverage_percent": 70,
        "franchise_rials": 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_rials فقط در قرارداد supplementary معنا دارد

POST /api/v1/billing/tenant-insurances

فعال‌سازی/به‌روزرسانی قرارداد. اگر قرارداد فعالی برای آن بیمه باشد ویرایش می‌شود، وگرنه نسخه‌ی جدید.

Body:

فیلد نوع توضیح
insurance_id int الزامی
coverage_percent float ستون قدیمی قرارداد (آخرین سطح fallback)؛ پنل آن را با درصد سرپایی همگام می‌فرستد
franchise_rials int فرانشیز — فقط در قرارداد supplementary اثر دارد
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 آن نوع حذف و به پیش‌فرض ادمین برمی‌گردد
doctor_uuid string (UUID) | null اختیاری — قرارداد را به‌ازای پزشک هدف ذخیره می‌کند (نگاه کنید به «تنظیمات per-doctor» بالا)
{
  "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_rials · 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 (اختیاری) — برای غیرفعال‌سازی قرارداد یک پزشک خاص.

{ "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 (مالک قرارداد)

{
  "success": true,
  "data": {
    "data": [
      {
        "uuid": "…",
        "tenant_insurance_id": 4,
        "service_item_id": 12,
        "service_item_uuid": "…",
        "covered": true,
        "coverage_percent": 80,
        "franchise_rials": 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_rials 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=falsenotCovered()؛ در غیر این صورت فیلدهای 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.

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.