Files
clinicpro/docs/api/insurance.md
T
hamed 22937dfa56 feat: add CategoryImportController for bulk JSON import and export of categories
- Implemented export functionality to retrieve all rows from specified category tables.
- Developed import functionality with strict validation and referential integrity checks.
- Added error handling for various import scenarios including invalid formats and duplicate entries.
- Introduced tests for import functionality to ensure correct behavior and validation.
2026-06-30 21:51:06 +03:30

14 KiB

Insurance API

Prefix: /api/v1/insurances, /api/v1/insurance, /api/v1/admin/insurance

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)

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"
    },
    {
      "id": 2,
      "name": "بیمه ایران",
      "type": "supplementary",
      "logo_url": "https://...",
      "status": "active"
    }
  ]
}

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

{
  "success": true,
  "data": [ ... ],
  "meta": { "totalRecords": 15, "totalPages": 1, "currentPage": 1 }
}

Errors

Code HTTP Description
ERR_AUTH_001 401 Missing token
ERR_AUTH_006 403 Not admin

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)

Response 200

{
  "success": true,
  "data": {
    "entity_type": "doctor",
    "entity_id": 7,
    "free_visit_price_rials": 5000000,
    "insurances": [
      {
        "insurance_id": 3,
        "insurance_name": "تأمین اجتماعی",
        "type": "basic",
        "patient_share_rials": 1500000
      },
      {
        "insurance_id": 9,
        "insurance_name": "دانا",
        "type": "supplementary",
        "patient_share_rials": null
      }
    ]
  }
}
  • patient_share_rials = null یعنی این بیمه پذیرفته نمی‌شود (قیمت‌گذاری ندارد).

خطاها

  • 403 ERR_FORBIDDEN_001 — پروفایل (doctor/clinic) برای کاربر یافت نشد.

PUT /api/v1/insurance-pricing

ذخیره/به‌روزرسانی قیمت ویزیت آزاد و سهم بیمار هر بیمه. عملیات upsert؛ ردیفی که patient_share_rials = null بفرستد حذف می‌شود.

Permission: AUTH (ROLE_DOCTOR یا ROLE_CLINIC)

Request Body

{
  "free_visit_price_rials": 5000000,
  "insurances": [
    { "insurance_id": 3, "patient_share_rials": 1500000 },
    { "insurance_id": 9, "patient_share_rials": null }
  ]
}
فیلد نوع توضیح
free_visit_price_rials int مبلغ ویزیت آزاد (ریال). اختیاری؛ اگر نباشد تغییر نمی‌کند.
insurances[].insurance_id int شناسه‌ی بیمه (الزامی برای هر ردیف).
insurances[].patient_share_rials int | null سهم بیمار با این بیمه. null → ردیف حذف می‌شود.

Response 200

همان ساختار GET /api/v1/insurance-pricing (وضعیت پس از ذخیره).

خطاها

  • 403 ERR_FORBIDDEN_001 — پروفایل یافت نشد.

TenantInsurance — قراردادهای بیمه‌ی tenant (فاز ۱ سیستم صورتحساب)

قرارداد یک پزشک/کلینیک با یک بیمه: درصد پوشش، فرانشیز، سقف تعهد سالانه، نسخه‌بندی و وضعیت فعال. مبنای محاسبه‌ی سهم در سیستم صورتحساب (docs/architecture/insurance-billing-system.md). tenant از #[CurrentUser] (ROLE_DOCTOR→doctor، ROLE_CLINIC→clinic). جدول tenant_insurances.

GET /api/v1/billing/tenant-insurances

لیست قراردادهای فعال tenant جاری.

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,
        "effective_from": 1718900000,
        "effective_to": null
      }
    ]
  }
}

POST /api/v1/billing/tenant-insurances

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

Body:

فیلد نوع توضیح
insurance_id int الزامی
coverage_percent float درصد پوشش (۰–۱۰۰)
franchise_rials int فرانشیز ثابت سهم بیمار
annual_ceiling_rials int | null سقف تعهد (null = بی‌نهایت)

پاسخ 201: { success, data: { …contract } }. خطاها: 404 ERR_NOT_FOUND_001 بیمه یافت نشد · 422 ERR_VALIDATION_001 insurance_id الزامی · 403 ERR_FORBIDDEN_001 پروفایل یافت نشد.

PATCH /api/v1/billing/tenant-insurances/{uuid}

ویرایش coverage_percent / franchise_rials / annual_ceiling_rials. فقط قرارداد متعلق به tenant جاری.

DELETE /api/v1/billing/tenant-insurances/{uuid}

غیرفعال‌سازی نرم (soft) — is_active=false و effective_to=now. داده حذف نمی‌شود.

{ "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های پوشش خدمات یک قرارداد.

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 = ارث از قرارداد

سرویس باید متعلق به همان مطب/کلینیکِ قرارداد باشد (ServiceItem→section→entity_type/entity_id).

پاسخ 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.