# Models: ClinicInvoice + ClinicInvoiceItem

**Tables**: `clinic_invoices`, `clinic_invoice_items` (new, Phase 11)

## Purpose

The clinic-side billing document that posts into Finance exactly the way `Sales\Invoice` does —
deliberately shaped to match it field-for-field so `ClinicPostingService` can mirror
`SalesPostingService::postInvoice()` almost line-for-line. See
`flows/12-billing-and-posting-flow.md` for the full posting sequence.

## Fields — `clinic_invoices`

| Column | Type | Constraints | Notes |
|---|---|---|---|
| `id` | bigint | PK | |
| `customer_id` | bigint | `foreignId('customer_id')->constrained('customers')->restrictOnDelete()` | `Sales\Customer`, auto-provisioned per patient by `ClinicPatientCustomerProvisioningService` — **never** an insurance company (out of scope, see `06-overview-medical-center-expansion.md`) |
| `patient_id` | bigint | `foreignId('patient_id')->constrained('users')->restrictOnDelete()` | kept alongside `customer_id` so clinical screens can query invoices by patient without joining through `Customer` |
| `encounter_id` | bigint | `foreignId('encounter_id')->nullable()->constrained('clinic_encounters')->nullOnDelete()` | the visit this invoice bills for, when applicable |
| `surgery_case_id` | bigint | `foreignId('surgery_case_id')->nullable()->constrained('clinic_surgery_cases')->nullOnDelete()` | set when the invoice is for a surgical procedure |
| `branch_id` | bigint | `foreignId('branch_id')->constrained('branches')->restrictOnDelete()` | for branch scoping |
| `subtotal` | decimal(12,2) | | |
| `tax_amount` | decimal(12,2) | default `0` | |
| `total_amount` | decimal(12,2) | | |
| `paid_amount` | decimal(12,2) | default `0` | updated by `ReceiptVoucherPostingService::post()` once the FK fix ships (see `07-database-schema-expansion.md` §Altered) |
| `status` | string | default `'draft'` | `draft` → `confirmed` → `posted` / `cancelled` — same shape as `Sales\Invoice.status` |
| `payment_status` | string | default `'unpaid'` | `unpaid` / `partial` / `paid` |
| `journal_entry_id` | bigint | `foreignId('journal_entry_id')->nullable()->constrained('finance_journal_entries')->nullOnDelete()` | set on posting |
| `posted_at` | datetime | nullable | |
| `created_by` | bigint | `foreignId(...)->nullable()->constrained('admins')->nullOnDelete()` | |
| `created_at` / `updated_at` | timestamp | | |

## Fields — `clinic_invoice_items`

| Column | Type | Constraints | Notes |
|---|---|---|---|
| `id` | bigint | PK | |
| `clinic_invoice_id` | bigint | `foreignId('clinic_invoice_id')->constrained('clinic_invoices')->cascadeOnDelete()` | |
| `service_type` | string | | `consultation` / `procedure` / `surgery` / `lab_test` / `other` — determines which revenue-account resolution rule applies (see Decisions) |
| `description` | string | | |
| `revenue_account_id` | bigint | `foreignId('revenue_account_id')->nullable()->constrained('finance_accounts')->nullOnDelete()` | line-level override, checked first in the resolution priority chain, same pattern as `Sales` line items |
| `quantity` | unsignedSmallInteger | default `1` | |
| `unit_price` | decimal(12,2) | | |
| `line_total` | decimal(12,2) | | |
| `created_at` / `updated_at` | timestamp | | |

## Relationships

| Relation | Type | Target |
|---|---|---|
| `ClinicInvoice::customer()` | `belongsTo` | `Sales\Customer` |
| `ClinicInvoice::patient()` | `belongsTo` | `User` |
| `ClinicInvoice::encounter()` | `belongsTo` | `Encounter` |
| `ClinicInvoice::surgeryCase()` | `belongsTo` | `SurgeryCase` |
| `ClinicInvoice::branch()` | `belongsTo` | `Core\Branch` |
| `ClinicInvoice::journalEntry()` | `belongsTo` | `Finance\JournalEntry` |
| `ClinicInvoice::items()` | `hasMany` | `ClinicInvoiceItem` |
| `ClinicInvoice::receiptVouchers()` | `hasMany` | `Finance\Vouchers\ReceiptVoucher` (via the new `clinic_invoice_id` FK) |
| `ClinicInvoiceItem::invoice()` | `belongsTo` | `ClinicInvoice` |
| `ClinicInvoiceItem::revenueAccount()` | `belongsTo` | `Finance\Account` |

## Decisions

- Field set intentionally mirrors `Sales\Invoice` (`subtotal`/`tax_amount`/`total_amount`/
  `paid_amount`/`status`/`payment_status`/`journal_entry_id`/`posted_at`) so
  `ClinicPostingService` can follow `SalesPostingService::postInvoice()`'s structure almost
  exactly, rather than inventing a parallel shape Finance has to special-case.
- `customer_id` FK to `Sales\Customer` (not directly to `User`) because Finance's account
  resolution, credit terms, and the entire posting pipeline are built around `Customer`, not
  `User` — see `06-overview-medical-center-expansion.md`'s "Patient identity for billing" reuse
  decision. `patient_id` is kept alongside it purely for clinical-side convenience queries.
- `service_type` on each line drives which revenue account gets resolved (e.g. `surgery` lines
  might default to a different revenue account than `consultation` lines) — this is the
  "category-level default" rung of the same priority chain `SalesPostingService::resolveRevenueAccount()`
  already uses (line-level → category/type default → global Setting → throw).
- The `finance_receipt_vouchers.clinic_invoice_id` FK (documented in
  `07-database-schema-expansion.md` §Altered) is what makes `paid_amount`/`payment_status` update
  automatically — without it, this table would inherit the same "payment tracked only via
  free-text reference" gap that exists for `Sales\Invoice` today.
