Files
clinicpro/docs/api/insurance.md
T
hamed 20bdc49e89 feat(claims): add tracking number and status history for claims
- Introduced `tracking_number` field in the `claims` table to store the insurance tracking number.
- Created `claim_status_logs` table to maintain a history of status changes for claims, including who made the change and when.
- Implemented `ClaimStatusLog` entity and repository for managing status log entries.
- Updated `ClaimService` to log transitions and handle tracking numbers during claim submissions.
- Added new API endpoint for fetching claims by patient, including detailed claim history and status logs.
- Enhanced frontend with a new `ClaimPatientDetailPage` to display claims and their status history.
- Added tests to ensure correct aggregation of claims and proper handling of status transitions.
2026-07-18 23:38:02 +03:30

18 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)

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
      },
      {
        "insurance_id": 9,
        "insurance_name": "دانا",
        "type": "supplementary",
        "patient_share_rials": null
      }
    ]
  }
}
  • 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 → ردیف حذف می‌شود.

اعتبارسنجی: اگر فلگ مؤثر (ارسالی یا ذخیره‌شده) true باشد و قیمت مؤثر (ارسالی یا ذخیره‌شده) <= 0، درخواست رد می‌شود.

Response 200

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

خطاها

  • 403 ERR_FORBIDDEN_001 — پروفایل یافت نشد.
  • 422 ERR_VALIDATION_001 (field: free_visit_price_rials) — فلگ الزامی فعال است ولی قیمت ویزیت آزاد <= 0.

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

قرارداد یک پزشک/کلینیک با یک بیمه: درصد پوشش، فرانشیز، سقف تعهد سالانه، نسخه‌بندی و وضعیت فعال. مبنای محاسبه‌ی سهم در سیستم صورتحساب (docs/architecture/insurance-billing-system.md). جدول tenant_insurances.

tenant از #[CurrentUser] با App\Patient\Security\PatientRecordScopeResolver resolve می‌شود — همان رزولور پرونده‌ها و صورتحساب‌ها، تا قرارداد بیمه و صورتحسابی که از آن ساخته می‌شود هرگز به دو محیط متفاوت نیفتند. محیط فعال (UserActiveContext) تعیین‌کننده است، نه صرفاً ترتیب نقش‌ها؛ مالک کلینیکی که خودش پزشک هم هست، قراردادهای کلینیک خود را می‌بیند.

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

اثر جانبی — همگام‌سازی 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.