# 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:** ```json { "session_uuid": "…" } ``` **Response 201:** ```json { "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 نهایی‌سازی صورتحساب (`draft` → `finalized`). صورتحساب نهایی‌شده مبنای ساخت 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) | > بازه‌ی تاریخ شمسی (سال/ماه/روز خاص) در سمت کلاینت به `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؛ نمایش شمسی) | ```json { "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} انتقال وضعیت. `action` ∈ `submit|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:** ```json { "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` متوقف می‌شود.