# MedBilling — Charge Capture + Insurance/Claims (SIFA plan P4-T1)

Medical billing for the Shifa One platform. **Ground rule: bind to the existing
ERP invoicing — extend, never rebuild.** MedBilling never re-implements invoices,
payments, taxes or price lists. It creates the ERP sell through the *same*
`app/Utils/TransactionUtil` path the POS uses, then layers a charge-capture
staging area, a payer + policy master, and a claim + pre-auth workflow on top.

Depends on **ClinicCore** (`module.json` → `requires: ["ClinicCore"]`).

---

## What it adds

| Concern | Where it lives |
|---|---|
| Charge capture (encounter → draft charges) | `medbill_charge_items` + `ChargeCaptureService` |
| Payer master (insurer / corporate / gov) | `medbill_payers` (audit decision D8 — a dedicated table, **not** contacts) |
| Patient coverage | `medbill_patient_policies` (links Wave-A `clc_patient_insurances`) |
| Claims + item-level adjudication | `medbill_claims` / `medbill_claim_items` + `ClaimService` |
| Pre-authorisation tracking | `medbill_pre_auths` |
| ERP invoice | reused — `transactions` / `transaction_sell_lines` / `transaction_payments` |
| Price lists | reused — `selling_price_groups` / `variation_group_prices` (per-payer) |

All tables are `business_id`-scoped and prefixed `medbill_`. Migrations dated
`2026_07_04_00xx`. References into ERP / ClinicCore tables are plain indexed
columns (no cross-DB FK constraints), matching the existing module convention.

---

## How `billCharges()` creates the ERP sell

`ChargeCaptureService::billCharges($chargeIds, $locationId, $policyId)` groups the
selected draft charges by patient and, per patient, creates **one** finalized ERP
sell — mirroring `SellPosController::store` **exactly** (raw floats, `$uf_data=false`):

```
$invoice_total = ProductUtil::calculateInvoiceTotal($products, null, $discount, false);
$txn = TransactionUtil::createSellTransaction($businessId, $input, $invoice_total, $userId, false);
        // $input: status=final, type=sell, is_direct_sale=1, contact_id=<patient contact>,
        //         final_total, discount 0, tax_rate_id=null, transaction_date=now
TransactionUtil::createOrUpdateSellLines($txn, $products, $locationId, false, null, [], false);
foreach ($products as $p) if ($p['enable_stock'])
    ProductUtil::decreaseProductQuantity($p['product_id'], $p['variation_id'], $locationId, $p['quantity']);
TransactionUtil::updatePaymentStatus($txn->id, $txn->final_total);      // stamps 'due' (unpaid invoice)
TransactionUtil::mapPurchaseSell([...], $txn->sell_lines, 'purchase');  // FIFO/LIFO stock mapping (fail-soft)
```

Each product line carries `product_id, variation_id, quantity, unit_price,
unit_price_inc_tax, item_tax(0), tax_id(null), enable_stock, product_type`. The
returned `transaction_id` / sell-line ids are written back onto every charge
(`status → billed`).

When a `policy_id` is supplied, `billCharges` splits the billed total via
`MedbillPatientPolicy::splitAmount()` (copay first, payer covers `coverage_pct`
of the remainder) and drafts **one claim per (sell, policy)** with item lines.

### Product resolution (a flagged deviation — Ground Rule 6)

The ERP has **no free-text service-line path** — `createOrUpdateSellLines`
requires a real `product_id` + `variation_id`, and ClinicCore itself bills every
service as a real ERP product. So MedBilling only pushes charges that resolve to a
product:

- **Consultation** → `clc_doctors.product_id` (unit price = `consultation_fee`).
- **Dispensed medicine** → `clc_medicines.product_id`.
- **Clinical orders** (`clc_order_items`) carry no product; best-effort matched to
  `clc_soap_options.product_id` by label, else left **non-billable + flagged** in
  the review screen (kept for reference, never invoiced).

Charges are billed as **tax-free** service lines (`item_tax=0`, `tax_id=null`) so
`claimed == charged`; per-payer tax rules are out of P4-T1 scope.

---

## Claim lifecycle (`ClaimService`)

```
draft ─submit→ submitted ─(acknowledge / under_review)→ … ─adjudicate→
      approved | partially_approved | denied ─markPaid→ paid
denied ─appeal→ appealed ─(re-adjudicate)→ …
```

- `adjudicate()` applies per-item approved amounts (clamped to claimed), rolls up
  totals and derives `approved` / `partially_approved` / `denied`.
- `markPaid()` optionally records the **payer share** as an ERP payment against the
  claim's transaction via `TransactionUtil::createOrUpdatePaymentLines` + refreshes
  the payment status — the same add-payment path the POS uses. **Fail-soft
  (Ground Rule 6):** if there's no linked transaction or the payment path throws,
  the claim is still marked paid and the ERP payment is left as a **manual step**,
  flagged in the returned payload and noted on the claim.
- Every transition fires MedNotify's **`claim.status_changed`** event (fail-soft;
  no-op without MedNotify) to notify billing users.

---

## Charge capture: two entry points (spec §C)

Cross-module model listeners are intentionally avoided. Charges are captured:

1. **On demand** — the "Capture charges" button on the billing worklist calls
   `ChargeCaptureService::captureForAppointment($id)`.
2. **Scheduled** — `php artisan medbill:capture-completed [--date=Y-m-d] [--business=ID]`
   drafts charges for appointments **completed** on the given day (default:
   yesterday). Scheduled daily at 02:00 by `MedBillingServiceProvider`.

Both are **idempotent** — a source row (`source_type` + `source_id`) is never
captured twice.

---

## UI map (routes under `med-billing`, `medbill.*`)

| Screen | Route name | Permission |
|---|---|---|
| Billing worklist (capture / review / bill + split preview) | `medbill.worklist.*` | `medbill.charges.manage` |
| Payers CRUD | `medbill.payers.*` | `medbill.payers.manage` |
| Policies CRUD (per patient) | `medbill.policies.*` | `medbill.payers.manage` |
| Claims (DataTable + detail modal + transitions + print) | `medbill.claims.*` | `medbill.claims.manage` (+ `medbill.claims.submit` to submit) |
| Pre-auth CRUD | `medbill.preauth.*` | `medbill.preauth.manage` |
| Reports (billed vs collected, claims by status, payer aging) | `medbill.reports.index` | `medbill.reports.view` |

Menu injected by `DataController::modifyAdminMenu()` under **"Medical Billing"**,
gated by the SaaS entitlement helper + the `medbill_module` subscription flag +
the `medbill.*` permissions — exactly like MedNotify / Vrc.

---

## Install / permissions

Standard superadmin install/update/repair endpoints
(`/med-billing/install|update|repair`), gated by the lowercase
`medbilling_version` System property (`ModuleUtil::isModuleInstalled`).

Permissions: `medbill.charges.manage`, `medbill.payers.manage`,
`medbill.claims.manage`, `medbill.claims.submit`, `medbill.preauth.manage`,
`medbill.reports.view` — registered by migration + seeder, granted to `Admin` on
install.

Seeders: `MedbillPermissionSeeder`, `MedbillNotifyEventSeeder` (registers
`claim.status_changed` with MedNotify, fail-soft), `MedbillDemoSeeder` (guarded on
the 'Shifa One' business; uses existing demo data only, idempotent).

---

## Deviations flagged (Ground Rule 6)

1. **Product-less services can't be ERP sell lines** — the ERP has no free-text
   line path (confirmed against ClinicCore's own billing). Non-resolvable charges
   are staged + flagged, never invoiced.
2. **Charges billed tax-free** — keeps `claimed == charged`; per-payer tax is out
   of P4-T1 scope.
3. **`markPaid` ERP payment is best-effort** — falls back to a flagged manual step
   if the transaction is missing or the payment path throws.
4. **`modules_statuses.json`** gains a `"MedBilling": true` entry — the standard
   nwidart module-registration requirement (peer modules MedNotify/MedRegistry are
   registered the same way). No other file outside `Modules/MedBilling` is touched.
