Files
clinicpro/docs/api/insurance.md
T
hamedandClaude Opus 4.8 d7cb9ea5a3 feat(insurance): redesign insurance management page to match Figma
Rebuild the /admin/insurance-pricing contracts UI to the Figma "مدیریت بیمه"
design and inject the coverage/franchise/ceiling fields the design omitted.

Backend:
- Add contract-level `kind` column to TenantInsurance (basic|supplementary),
  defaulting to the catalog type; migration Version20260715093358.
- POST/PATCH /billing/tenant-insurances now accept effective_from,
  effective_to, kind; PATCH also toggles is_active without clobbering the
  user-set effective_to (unlike DELETE/deactivate).
- List returns the latest version of every insurance (active + inactive) via
  TenantInsuranceRepository::findLatestByTenant, for the فعال/غیرفعال toggle.

Frontend:
- New InsuranceModal (ui/Modal + SearchableSelect + PersianDateInput) with the
  seven fields; submit "ثبت بیمه".
- TenantInsuranceContracts rebuilt: header + search box, desktop table
  (ردیف/نام/کد/نوع/وضعیت/عملیات) and mobile cards, status toggle -> PATCH.
- utils: isoToUnix/unixToIso helpers for contract dates.

Tests: TenantInsuranceContractApiTest (create/edit/toggle/list, 5 cases),
InsuranceModal + TenantInsuranceContracts vitest suites, docs/api updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:23:38 +03:30

15 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 جاری — آخرین نسخهٔ هر بیمه، فعال یا غیرفعال (برای toggle فعال/غیرفعال در UI مدیریت بیمه). insurance_kind = kind قرارداد در صورت تعیین، وگرنه نوع بیمه از کاتالوگ.

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
      }
    ]
  }
}

POST /api/v1/billing/tenant-insurances

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

Body:

فیلد نوع توضیح
insurance_id int الزامی
coverage_percent float درصد پوشش (۰–۱۰۰)
franchise_rials int فرانشیز ثابت سهم بیمار
annual_ceiling_rials int | null سقف تعهد (null = بی‌نهایت)
kind string | null نوع بیمه قرارداد (basic/supplementary); خالی → پیش‌فرض نوع کاتالوگ
effective_from int | null تاریخ شروع قرارداد (Unix)؛ null → اکنون
effective_to int | null تاریخ پایان قرارداد (Unix)؛ 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}

ویرایش فیلدهای قرارداد (همه اختیاری، فقط کلیدهای موجود اعمال می‌شوند). فقط قرارداد متعلق به tenant جاری.

Body: coverage_percent · franchise_rials · annual_ceiling_rials · kind · effective_from · effective_to · is_active.

  • is_active (bool): toggle فعال/غیرفعال. برخلاف DELETE، مقدار effective_toِ تعیین‌شدهٔ کاربر را دست‌نخورده نگه می‌دارد (برای reactivate).

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.

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.