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

> **Prefix:** `/api/v1/billing`
> دامنه: `App\Billing`. مرجع معماری: `docs/architecture/insurance-billing-system.md`.
> tenant از `#[CurrentUser]` resolve می‌شود (`ROLE_DOCTOR`→doctor، `ROLE_CLINIC`→clinic، و `ROLE_SECRETARY`→ همان مطب/کلینیکِ فعال بر اساس `db_uuid` و رابطه‌ی فعالِ منشی). یعنی منشیِ فعال هم می‌تواند صورتحساب‌های همان tenant را ببیند/بسازد.

صورتحساب (`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 (پیش‌فرض = کل ادعا) — باید `0 ≤ approved_rials ≤ total_claimed_rials` |
| reject | `reason` (الزامی) | submitted → rejected |
| pay | `paid_rials` | approved → paid (پیش‌فرض = approved) — باید `0 ≤ paid_rials ≤ total_approved_rials` |

**Errors:** `422` انتقال نامعتبر، دلیل رد خالی، یا مبلغ `approved_rials`/`paid_rials` خارج از بازه (`field` در پاسخ) · `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` (حداقل صفر).

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

**Query params:**

| param | توضیح |
|-------|-------|
| `national_code` | جست‌وجوی جزئی روی کد ملی بیمار (`LIKE`) |
| `status` | `paid` (پرداخت‌شده) · `unsettled` (تسویه‌نشده = `finalized`) |
| `from` / `to` | بازه‌ی `issued_at` بر حسب ثانیه‌ی Unix |
| `page` / `limit` | صفحه‌بندی (پیش‌فرض ۱ / ۲۰، سقف ۱۰۰) |

**Response 200** (صفحه‌بندی‌شده‌ی مسطح):
```json
{
  "success": true,
  "data": [
    { "invoice_uuid": "…", "patient_uuid": "…", "patient_name": "دنیا خلیلی",
      "national_code": "1744023654", "issued_at": 1717000000,
      "amount_rials": 2350000, "status": "paid" }
  ],
  "meta": { "totalRecords": 12, "totalPages": 1, "currentPage": 1 }
}
```
> `amount_rials` = سهم بیمار (`patient_rials`) همان صورتحساب. `status` دو حالته: `paid` یا `unsettled`.

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

## GET /api/v1/my/billing/patients/{patientUuid}/invoices
«پرداخت‌های ثبت‌شده» — سربرگ بیمار + فهرست صفحه‌بندی‌شده‌ی صورتحساب‌های `finalized`/`paid` او (جدیدترین اول). فقط مالک رکورد (همان tenant) دسترسی دارد.

**Response 200:**
```json
{
  "success": true,
  "data": {
    "patient": { "uuid": "…", "name": "دنیا خلیلی", "national_code": "1744023654" },
    "data": [
      { "uuid": "…", "number": 12345, "issued_at": 1717000000, "total_rials": 2350000,
        "status": "paid", "service_title": "روکش دندان",
        "items": [ { "uuid": "…", "title": "روکش دندان", "quantity": 1, "total_rials": 2350000, "patient_rials": 2350000 } ] }
    ],
    "meta": { "totalRecords": 3, "totalPages": 1, "currentPage": 1 }
  }
}
```
> `status` دو حالته: `paid` (پرداخت‌شده) یا `unsettled` (تسویه‌نشده = `finalized`). `service_title` عنوان اولین آیتم است (+ «و موارد دیگر» اگر بیش از یک آیتم باشد).

**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` متوقف می‌شود.