Files
clinicpro/docs/api/billing.md
T
hamedandClaude Opus 4.8 d8f7db0ada perf(billing,settlement): paginate claims and wallet transactions (H9, H10)
GET /billing/claims loaded every tenant claim with no limit. Add
findByTenant(page, limit) + countByTenant (shared query builder), default
limit 50 / max 100, and expose totals as data.meta — kept inside the existing
{ data: { data: [...] } } envelope so current clients are unaffected.

GET /wallet/transactions was already bounded (findByUser defaulted to limit 50)
but page-less; add page/offset + countByUser + the same additive meta.

Regression: tests/Billing/ClaimsListPaginationTest,
tests/Settlement/WalletTransactionsPaginationTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:42:04 +03:30

9.6 KiB

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

Prefix: /api/v1/billing دامنه: App\Billing. مرجع معماری: docs/architecture/insurance-billing-system.md. tenant از #[CurrentUser] resolve می‌شود (ROLE_DOCTOR→doctor، ROLE_CLINIC→clinic).

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

  • تعرفه‌ی خدمت از Tariff سال جاری (با fallback به ServiceItem.priceRials).
  • قانون پوشش از قرارداد بیمه‌ی tenant (TenantInsurance) + override خدمت (TenantServiceCoverage).
  • ترتیب محاسبه: کل → پوشش پایه (با سقف) → باقیمانده → پوشش مکمل → فرانشیز سهم بیمار.

نمونه: کل ۶۰۰٬۰۰۰ · پایه ۷۰٪ → ۴۲۰٬۰۰۰ · مکمل روی باقیمانده → ۱۲۰٬۰۰۰ · بیمار ۶۰٬۰۰۰.


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: همان ساختار بالا. 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} هم اعمال می‌شود.

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

انتقال وضعیت. actionsubmit|approve|reject|pay.

action body اختیاری اثر
submit pending → submitted
approve approved_rials submitted → approved (پیش‌فرض = کل ادعا)
reject reason (الزامی) submitted → rejected
pay paid_rials approved → paid (پیش‌فرض = approved)

Errors: 422 انتقال نامعتبر یا دلیل رد خالی · 404 مطالبه یافت نشد.

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 (حداقل صفر).


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

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