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>
192 lines
9.6 KiB
Markdown
192 lines
9.6 KiB
Markdown
# 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) |
|
|
| `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؛ نمایش شمسی) |
|
|
|
|
```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` متوقف میشود. |