Files
clinicpro/docs/api/billing.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

26 KiB
Raw Blame History

Billing API — صورتحساب (فاز ۴ سیستم صورتحساب/بیمه)

Prefix: /api/v1/billing دامنه: App\Billing. مرجع معماری: docs/architecture/insurance-billing-system.md. tenant از #[CurrentUser] با App\Patient\Security\PatientRecordScopeResolver resolve می‌شود — همان رزولوری که پرونده‌های بیمار استفاده می‌کنند، چون صورتحساب و مطالبه از دل مراجعه بیرون می‌آیند و باید در همان محیط دیده شوند. محیط فعال (UserActiveContext) تعیین‌کننده است، نه صرفاً ترتیب نقش‌ها. منشیِ فعال هم می‌تواند صورتحساب‌های همان tenant را ببیند/بسازد. جزئیات جدول محیط‌ها: patient.md.

پیش از این، این دامنه ترتیب نقش‌ها را خودش پیاده کرده بود و اول ROLE_DOCTOR را می‌گرفت؛ در نتیجه مالک کلینیکی که خودش پزشک هم هست به مطب شخصی‌اش نگاشت می‌شد و صورتحساب/مطالبه‌ی کلینیک خودش را 404 می‌گرفت. همین رزولور در InsuranceController هم استفاده می‌شود تا قرارداد بیمه و صورتحساب هرگز به دو محیط متفاوت نیفتند.

صورتحساب (Invoice) از یک Encounter (PatientSession) ساخته می‌شود. برای هر آیتم سهم بیمه‌ی پایه، بیمه‌ی مکمل و بیمار با BillingCalculator محاسبه می‌شود:

  • تعرفه‌ی خدمت از Tariff سال جاری (با fallback به ServiceItem.priceRials).
  • قانون پوشش از قرارداد بیمه‌ی tenant (TenantInsurance) + override خدمت (TenantServiceCoverage).
  • درصد پوشش به تفکیک نوع خدمت و از زنجیرهٔ resolve توضیح‌داده‌شده در insurance.md گرفته می‌شود: هر خدمت با ServiceItem.service_category خودش، و ویزیت با PatientSession.insurance_service_category (نوعی که سرِ پذیرش انتخاب شده؛ در نبودش سرپایی).
  • invoices.service_category همان نوع را snapshot می‌کند و در toArray() به‌صورت service_category / service_category_label برمی‌گردد.
  • ترتیب محاسبه: کل → پوشش پایه (با سقف) → باقیمانده → پوشش مکمل (با سقف) → فرانشیزِ تکمیلی روی سهم بیمار.

بیمهٔ پایه صرفاً درصدی است:

سهم بیمهٔ پایه = round(کل × درصد پوشش پایه ÷ 100)
سهم بیمار      = کل − سهم بیمهٔ پایه

franchise_rials قرارداد پایه در محاسبه بی‌اثر است (ستون برای سازگاری و قراردادهای تکمیلی می‌ماند).

نمونه‌ها:

  • کل ۶۰۰٬۰۰۰ · پایه ۷۰٪ → ۴۲۰٬۰۰۰ · مکمل روی باقیمانده → ۱۲۰٬۰۰۰ · بیمار ۶۰٬۰۰۰.
  • ویزیت ۵٬۹۵۲٬۰۰۰ ریال · پایهٔ بستری ۳۰٪ → سهم پایه ۱٬۷۸۵٬۶۰۰ · سهم بیمار ۴٬۱۶۶٬۴۰۰.

POST /api/v1/billing/invoices

ساخت صورتحساب از یک مراجعه. اگر صورتحساب برای آن مراجعه قبلاً ساخته شده، همان برگردانده می‌شود (idempotent).

Permission: AUTH (مالک مراجعه)

Body:

{ "session_uuid": "…" }

Response 201:

{
  "success": true,
  "data": {
    "data": {
      "uuid": "…",
      "entity_type": "doctor",
      "entity_id": 7,
      "patient_session_id": 33,
      "base_insurance_id": 3,
      "supplementary_insurance_id": 9,
      "total_rials": 600000,
      "base_insurance_rials": 420000,
      "supplementary_rials": 120000,
      "patient_rials": 60000,
      "status": "draft",
      "issued_at": 1718900000,
      "items": [
        {
          "uuid": "…",
          "service_item_id": 12,
          "title": "ویزیت",
          "tariff_rials": 600000,
          "quantity": 1,
          "total_rials": 600000,
          "base_insurance_rials": 420000,
          "supplementary_rials": 120000,
          "patient_rials": 60000
        }
      ]
    }
  }
}

Errors: 422 ERR_VALIDATION_001 session_uuid الزامی · 404 ERR_NOT_FOUND_001 مراجعه یافت نشد · 403 ERR_FORBIDDEN_001 پروفایل یافت نشد.


GET /api/v1/billing/invoices/{uuid}

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

Response 200: همان ساختار بالا + کلید session — خروجی کامل PatientSession.toArray() مراجعه‌ای که صورتحساب از آن ساخته شده (برای نمایش «خلاصه فاکتور»: پرداختی‌ها، کالای مصرفی، تخفیف، مبالغ پرداخت‌شده). اگر صورتحساب از session ساخته نشده باشد session: null.

همچنین برای درج روی فاکتور: service_category / service_category_label (نوع خدمتِ بیمه‌ای) و base_insurance_name / supplementary_insurance_name (نام بیمه‌ها؛ فاکتور خودش فقط شناسه را نگه می‌دارد).

{
  "success": true,
  "data": {
    "data": {
      "uuid": "…",
      "total_rials": 600000,
      "status": "finalized",
      "items": [  ],
      "session": {
        "uuid": "…",
        "session_at": 1718900000,
        "paid_at": 1718990000,
        "services_total_rials": 2400000,
        "consumables_total_rials": 40000,
        "discount_rials": 200000,
        "final_price_rials": 2240000,
        "paid_total_rials": 1500000,
        "payments": [
          { "uuid": "…", "method": "wallet", "amount_rials": 1500000, "paid_at": 1718990000, "created_by_name": "منشی تست", "created_at": 1718990000 }
        ],
        "consumables": [
          { "uuid": "…", "item_name": "عینک", "quantity": 2, "price_rials": 20000, "line_total_rials": 40000 }
        ]
      }
    }
  }
}

Errors: 404 ERR_NOT_FOUND_001.


POST /api/v1/billing/invoices/{uuid}/finalize

نهایی‌سازی صورتحساب (draftfinalized). صورتحساب نهایی‌شده مبنای ساخت Claim (فاز ۵) است.

Response 200: صورتحساب با status: "finalized". Errors: 404 ERR_NOT_FOUND_001.


وضعیت‌های Invoice

draft (پیش‌نویس، قابل بازسازی) → finalized (نهایی) → paid (پرداخت‌شده) · void (باطل).

نکات

  • پول: integer ریال. تاریخ: Unix timestamp.
  • BillingCalculator خالص و واحد-تست‌شده است (tests/Billing/BillingCalculatorTest.php).
  • این فاز جایگزین تدریجی PatientService::calculateFinalPrice است؛ آن متد فعلاً برای سازگاری باقی مانده.

Claims — مطالبات بیمه (فاز ۵)

مطالبه (Claim) از یک صورتحساب نهایی‌شده ساخته می‌شود: یک Claim برای بیمه‌ی پایه و یک Claim برای بیمه‌ی مکمل (فقط اگر سهم بیمه > ۰). هر ClaimItem به یک InvoiceItem ارجاع می‌دهد. چرخه‌ی وضعیت با state machine.

ساخت خودکار: هنگام ثبت session بیمه‌دار (POST /api/v1/patient/{uuid}/session)، زنجیره‌ی صورتحساب→نهایی‌سازی→مطالبه به‌صورت خودکار اجرا می‌شود؛ نیازی به فراخوانی دستی POST /billing/claims نیست. برای هر صورتحساب تنها یک‌بار مطالبه ساخته می‌شود (تلاش مجدد 422).

وضعیت‌ها: pending → submitted → {approved → paid | rejected}. از paid/rejected خروجی ندارد.

POST /api/v1/billing/claims

ساخت مطالبات از یک صورتحساب نهایی‌شده.

Body: { "invoice_uuid": "…" } Response 201: { success, data: [ …claims ] } (یک یا دو مطالبه: پایه/مکمل). Errors: 422 صورتحساب نهایی نشده / سهم بیمه ندارد / مطالبه از قبل ثبت شده · 404 صورتحساب یافت نشد.

هر مطالبه در پاسخ علاوه بر insurance_id، فیلد insurance_name (نام بیمه، یا null اگر بیمه حذف شده باشد) را نیز دارد. در POST و transition و GET یکسان است.

GET /api/v1/billing/claims

لیست مطالبات tenant با فیلترهای اختیاری (query params):

پارامتر نوع توضیح
status string pending|submitted|approved|rejected|paid
insurance_id int فقط مطالبات یک بیمه‌گر
from int (Unix) مطالبات با created_at >= from (شروع بازه)
to int (Unix) مطالبات با created_at <= to (پایان بازه)
q string جستجوی بیمار: موبایل، کدملی یا نام (LIKE)
page int شماره صفحه (پیش‌فرض ۱)
limit int تعداد در هر صفحه (پیش‌فرض ۵۰، حداکثر ۱۰۰)

صفحه‌بندی: پاسخ علاوه بر data.data (آرایه‌ی مطالبات) یک data.meta با totalRecords/totalPages/currentPage دارد. پاکت قبلی (data.data) دست‌نخورده است؛ کلاینت‌های موجود بدون تغییر کار می‌کنند.

بازه‌ی تاریخ شمسی (سال/ماه/روز خاص) در سمت کلاینت به from/to یونیکس تبدیل می‌شود؛ backend فقط بازه‌ی یونیکس می‌گیرد. فیلتر q از طریق زنجیره claim → claim_item → invoice_item → invoice → patient_record → user با EXISTS اعمال می‌شود.

هر مطالبه شامل insurance_name، patient_name، patient_mobile و آرایه‌ی items با جزئیات هر ردیف است.

هر عضو items علاوه بر invoice_item_id، claimed_rials، approved_rials، این فیلدهای نمایشی را دارد (برای جدول صفحه‌ی مطالبات):

فیلد نوع توضیح
title string | null شرح ردیف؛ «ویزیت» یا نام خدمت
is_visit bool true اگر ردیف ویزیت باشد (service_item_id تهی)
quantity int تعداد
total_rials int | null مبلغ کل آن ردیف (قیمت کامل، قبل از بیمه)
visit_date int | null تاریخ ثبت مراجعه (Unix؛ نمایش شمسی)
{
  "success": true,
  "data": { "data": [
    {
      "uuid": "…", "insurance_id": 3, "insurance_name": "تأمین اجتماعی", "insurance_kind": "base",
      "total_claimed_rials": 240000, "status": "pending",
      "items": [
        { "invoice_item_id": 12, "title": "ویزیت", "is_visit": true, "quantity": 1, "total_rials": 300000, "claimed_rials": 240000, "approved_rials": null, "visit_date": 1718000000 },
        { "invoice_item_id": 13, "title": "سرم ۵۰۰cc", "is_visit": false, "quantity": 2, "total_rials": 170000, "claimed_rials": 0, "approved_rials": null, "visit_date": 1718000000 }
      ]
    }
  ] }
}

این فیلدها با یک کوئری گروهی (InvoiceItemRepository::detailsForIds + PatientSessionRepository::datesForIds) پر می‌شوند تا N+1 رخ ندهد. همان enrichment روی پاسخ POST /claims و POST /claims/{uuid}/{action} هم اعمال می‌شود.

GET /api/v1/billing/claims/by-patient

نمای سطح‌اول داشبورد مطالبات: یک ردیف به‌ازای هر بیمار (نه هر مطالبه)، با جمع‌های تجمیعی.

Query params:

Param Type Default توضیح
page int 1 شماره صفحه
limit int 20 حداکثر ۱۰۰
sort string last_activity_at full_name | claims_count | total_services_rials | total_insurance_rials | last_activity_at
dir string desc asc | desc
search string نام، موبایل یا کد ملی بیمار
status string pending | submitted | approved | rejected | paid
insurance_id int شناسه بیمه
doctor_id int پزشکِ نوبتِ مراجعه
payment_status string paid (وصول‌شده) | unpaid
from / to int بازه‌ی claims.created_at (unix ثانیه)

پاسخ paginated استاندارد ({ success, data: [], meta }):

{
  "success": true,
  "data": [
    {
      "patient_uuid": "45064492-...",
      "record_uuid": "ad0a3d0e-...",
      "full_name": "تست جراحی بینی",
      "mobile": "09370671756",
      "national_code": null,
      "claims_count": 2,
      "total_services_rials": 81500000,
      "total_insurance_rials": 57050000,
      "total_patient_rials": 24450000,
      "total_approved_rials": 28000000,
      "total_paid_rials": 28000000,
      "overall_status": "mixed",
      "last_activity_at": 1784404115
    }
  ],
  "meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 }
}
  • overall_status: اگر همه‌ی مطالبات بیمار یک وضعیت داشته باشند همان؛ وگرنه mixed.
  • ثابت: total_services_rials = total_insurance_rials + total_patient_rials.
  • ضدِ double-counting: مبالغ خدمات/سهم بیمار از صورتحساب‌های یکتا جمع می‌شوند، نه از مطالبات. یک صورتحساب می‌تواند هم‌زمان مطالبه‌ی پایه و مکمل داشته باشد؛ جمع‌زدن از سمت مطالبه مبلغ خدمات را دوبار می‌شمرد.
  • مطالبه لینک مستقیم به بیمار ندارد؛ زنجیره‌ی claim → claim_item → invoice_item → invoice → patient_record است.

GET /api/v1/billing/claims/by-patient/{patientUuid}

جزئیات کامل مطالبات یک بیمار (patientUuid = uuid پرونده، همان record_uuid نمای سطح‌اول). فیلترها همان فیلترهای بالا.

{
  "success": true,
  "data": {
    "patient": { "uuid": "...", "record_uuid": "...", "full_name": "...", "mobile": "...", "national_code": null },
    "claims": [
      {
        "uuid": "b244b506-...",
        "invoice_uuid": "15498c14-...",
        "visit_date": 1784440200,
        "doctor_name": "تست",
        "insurance_id": 176,
        "insurance_name": "تامین اجتماعی",
        "insurance_kind": "base",
        "service_base_rials": 41000000,
        "coverage_percent": 70,
        "insurance_share_rials": 28700000,
        "patient_share_rials": 12300000,
        "total_approved_rials": 28000000,
        "total_paid_rials": 28000000,
        "status": "paid",
        "tracking_number": "TM-4419-88",
        "reject_reason": null,
        "submitted_at": 1784404200,
        "settled_at": 1784404300,
        "created_at": 1784404115,
        "allowed_transitions": [],
        "logs": [
          { "uuid": "...", "from_status": null, "to_status": "pending", "note": "ایجاد مطالبه", "by": null, "at": 1784404115 },
          { "uuid": "...", "from_status": "pending", "to_status": "submitted", "note": null, "by": "علی بهروزی", "at": 1784404200 }
        ]
      }
    ]
  }
}
  • coverage_percent محاسبه‌شده است: insurance_share_rials / service_base_rials × 100.
  • allowed_transitions از Claim::TRANSITIONS می‌آید؛ پنل دکمه‌ها را از همین می‌سازد و فهرست مجاز را hardcode نمی‌کند.
  • logs تاریخچه‌ی کامل تغییر وضعیت از جدول claim_status_logs است (به‌ترتیب زمانی صعودی).

Errors: 404 پرونده بیمار یافت نشد یا متعلق به tenant دیگری است · 403 پروفایل یافت نشد.

POST /api/v1/billing/claims/{uuid}/{action}

انتقال وضعیت. actionsubmit|approve|reject|pay. هر انتقال یک ردیف در claim_status_logs ثبت می‌کند (وضعیت مبدأ/مقصد، توضیح، کاربر، زمان).

action body اثر
submit tracking_number (اختیاری) pending → submitted؛ شماره پرونده/پیگیری بیمه روی مطالبه ذخیره می‌شود
approve approved_rials (اختیاری) submitted → approved (پیش‌فرض = کل ادعا) — باید 0 ≤ approved_rials ≤ total_claimed_rials
reject reason (الزامی) submitted → rejected
pay paid_rials (اختیاری) approved → paid (پیش‌فرض = approved) — باید 0 ≤ paid_rials ≤ total_approved_rials

note (اختیاری) روی همه‌ی اکشن‌ها پذیرفته می‌شود و در تاریخچه ثبت می‌گردد؛ برای reject در نبودِ note خودِ reason ثبت می‌شود.

Errors: 422 انتقال نامعتبر، دلیل رد خالی، یا مبلغ approved_rials/paid_rials خارج از بازه (field در پاسخ) · 404 مطالبه یافت نشد.

اعتبارسنجی انتقال سمت سرور انجام می‌شود (Claim::TRANSITIONS منبع حقیقت است)؛ مخفی‌کردن دکمه در UI کافی نیست.

GET /api/v1/billing/reports/insurance-debt

گزارش بدهی بیمه‌ها برای tenant (group بر اساس بیمه).

Response 200:

{
  "success": true,
  "data": {
    "data": [
      { "insurance_id": 3, "insurance_name": "تأمین اجتماعی", "claimed": 840000, "approved": 800000, "paid": 500000, "debt": 340000 }
    ]
  }
}

debt = claimed - paid (حداقل صفر).

GET /api/v1/my/billing/payments

«لیست پرداخت‌ها» — فهرست مسطح صورتحساب‌های ثبت‌شده‌ی همان tenant (هر ردیف یک صورتحساب)، جدیدترین اول. فقط finalized/paid؛ draft/void نادیده گرفته می‌شوند.

Query params:

param توضیح
national_code جست‌وجوی جزئی روی کد ملی بیمار (LIKE) — روی profiles.national_code و در نبود آن users.national_code
status وضعیت مشتق‌شده‌ی پرداخت: paid · partial · unsettled
from / to بازه‌ی issued_at بر حسب ثانیه‌ی Unix
page / limit صفحه‌بندی (پیش‌فرض ۱ / ۲۰، سقف ۱۰۰)

Response 200 (صفحه‌بندی‌شده‌ی مسطح):

{
  "success": true,
  "data": [
    { "invoice_uuid": "…", "patient_uuid": "…", "patient_name": "دنیا خلیلی",
      "national_code": "1744023654", "issued_at": 1717000000,
      "amount_rials": 2350000, "paid_rials": 2350000, "status": "paid" }
  ],
  "meta": { "totalRecords": 12, "totalPages": 1, "currentPage": 1 }
}

amount_rials = سهم بیمار (patient_rials) همان صورتحساب · paid_rials = مجموع پرداخت‌های ثبت‌شده‌ی مراجعه‌ی متناظر.

national_code از profiles.national_code خوانده می‌شود (منبعِ اصلیِ کد ملی؛ بیمارِ ساخته‌شده در نوبت‌دهی کد ملی را آنجا دارد نه روی users)، و در نبودِ آن به users.national_code برمی‌گردد.

وضعیت پرداخت مشتق است، ذخیره نمی‌شود. ستون invoices.status فقط چرخه‌ی حیات صورتحساب را نگه می‌دارد (draftfinalizedvoid) و هرگز paid نمی‌شود؛ پول واقعی در session_payments ثبت می‌شود. قاعده در App\Billing\Service\InvoicePaymentStatus است:

حالت شرط
paid paid_rials >= amount_rials (شامل صورتحساب صفرریالی)
partial 0 < paid_rials < amount_rials
unsettled paid_rials = 0 و amount_rials > 0

صورتحساب بدون مراجعه (patient_session_id = null) هیچ پرداختی ندارد، پس unsettled می‌ماند.

Errors: 403 (ERR_FORBIDDEN_001) پروفایل tenant یافت نشد.

GET /api/v1/my/billing/payments/summary

خلاصه‌ی مالی همان مجموعه‌ی فیلترشده‌ی GET /api/v1/my/billing/payments — برای کارت‌های آمار بالای صفحه‌ی «لیست پرداخت‌ها». همان دامنه‌ی رکوردها (فقط finalized/paid همان tenant).

Query params: دقیقاً national_code، status، from، to مثل اندپوینت لیست (بدون page/limit).

Response 200:

{
  "success": true,
  "data": {
    "total_rials": 8350000,
    "paid_rials": 2350000,
    "unsettled_rials": 6000000,
    "invoices_count": 2
  }
}

مبالغ جمع patient_rials هستند. paid_rials = جمع صورتحساب‌هایی که کاملاً وصول شده‌اند؛ صورتحساب نیمه‌پرداخت (partial) کامل در unsettled_rials می‌نشیند. unsettled_rials = total_rials - paid_rials. با فیلتر status=paid مقدار unsettled_rials صفر می‌شود (رفتار درست، نه باگ). وقتی هیچ رکوردی مطابقت ندارد، همه‌ی مقادیر 0 برمی‌گردند.

Errors: 403 (ERR_FORBIDDEN_001) پروفایل tenant یافت نشد.

GET /api/v1/my/billing/patients/{patientUuid}/invoices

«پرداخت‌های ثبت‌شده» — سربرگ بیمار + فهرست صفحه‌بندی‌شده‌ی صورتحساب‌های finalized/paid او (جدیدترین اول). فقط مالک رکورد (همان tenant) دسترسی دارد.

Response 200:

{
  "success": true,
  "data": {
    "patient": { "uuid": "…", "name": "دنیا خلیلی", "national_code": "1744023654" },
    "data": [
      { "uuid": "…", "number": 12345, "issued_at": 1717000000, "total_rials": 2350000,
        "patient_rials": 2350000, "paid_rials": 2350000,
        "status": "paid", "service_title": "روکش دندان",
        "items": [ { "uuid": "…", "title": "روکش دندان", "quantity": 1, "total_rials": 2350000, "patient_rials": 2350000 } ],
        "payments": [
          { "method": "cash", "amount_rials": 350000, "paid_at": 1717000000, "created_by_name": "منشی" },
          { "method": "pos",  "amount_rials": 2000000, "paid_at": 1717000500, "created_by_name": null }
        ] }
    ],
    "summary": {
      "total_rials": 8350000,
      "paid_rials": 2350000,
      "unsettled_rials": 6000000,
      "invoices_count": 3
    },
    "meta": { "totalRecords": 3, "totalPages": 1, "currentPage": 1 }
  }
}

payments پرداخت‌های ثبت‌شده‌ی مراجعه‌ی همان صورتحساب است (قدیمی‌ترین اول)؛ method یکی از wallet · pos · cash · card — برچسب فارسی سمت کلاینت در assets/admin/lib/paymentMethods.ts. صورتحساب بدون مراجعه آرایه‌ی خالی می‌گیرد.

status همان وضعیت مشتق‌شده‌ی بالاست (paid · partial · unsettled) و همیشه بر مبنای patient_rials سنجیده می‌شود، حتی در این نما که ستون مبلغش total_rials است. service_title عنوان اولین آیتم است (+ «و موارد دیگر» اگر بیش از یک آیتم باشد).

summary روی همه‌ی صورتحساب‌های همان بیمار محاسبه می‌شود (نه فقط صفحه‌ی جاری) و جمع total_rials صورتحساب‌هاست — بر خلاف summary اندپوینت لیست پرداخت‌ها که سهم بیمار (patient_rials) را جمع می‌زند. invoices_count همان meta.totalRecords است.

Errors: 403 پروفایل tenant یافت نشد · 404 (ERR_NOT_FOUND_001) بیمار متعلق به این tenant نیست/یافت نشد.


ارسال مطالبه (ClaimSubmitter)

عملِ submit از طریق interface App\Billing\Contract\ClaimSubmitterInterface انجام می‌شود. پیاده‌سازی پیش‌فرض ManualClaimSubmitter است (ارسال دستی/آفلاین — همیشه موفق). برای اتصال آینده به API شرکت‌های بیمه‌ی ایران کافی است یک پیاده‌سازی جدید از این interface ساخته و در config/services.yaml bind شود؛ ClaimService تغییر نمی‌کند (Dependency Inversion). اگر submit ناموفق باشد، انتقال وضعیت با 422 متوقف می‌شود.

جریان صدور فاکتور در پنل ادمین

«صدور فاکتور» (گام «جزییات» ویزارد مراجعه) و «مشاهده فاکتور» (کارت مراجعه / مودال جزئیات) هر دو از useIssueInvoice استفاده می‌کنند: POST /billing/invoices (idempotent) و سپس finalize اگر draft باشد. یعنی session بدون فاکتور، در اولین مشاهده صاحب فاکتور نهایی‌شده می‌شود.