# Ripple API — Adding Bills: Technical Blueprint

> **Scope**: Complete backend flow for adding bills (inpatient & outpatient). Excludes HMO/multiplier logic.  
> **Generated from**: `dev` branch, full code audit.

---

## Table of Contents

1. [Architecture Overview](#1-architecture-overview)
2. [Database Schema](#2-database-schema)
3. [Bill Item Price List (Admin CRUD)](#3-bill-item-price-list-admin-crud)
4. [Entry Points: How Bills Are Created](#4-entry-points-how-bills-are-created)
5. [Outpatient Billing Flow](#5-outpatient-billing-flow)
6. [Inpatient Billing Flow](#6-inpatient-billing-flow)
7. [Manual Bill Addition](#7-manual-bill-addition)
8. [Discount / Waiver Approval Workflow](#8-discount--waiver-approval-workflow)
9. [Invoice Generation](#9-invoice-generation)
10. [Invoice Settlement (Payment)](#10-invoice-settlement-payment)
11. [Pharmacy Dispensation Billing](#11-pharmacy-dispensation-billing)
12. [Wallet System](#12-wallet-system)
13. [External / Mobile API](#13-external--mobile-api)
14. [Route Map](#14-route-map)
15. [Complete File Index](#15-complete-file-index)

---

## 1. Architecture Overview

```
┌─────────────────────────────────────────────────────────────────┐
│                        CONTROLLERS                              │
│  AppointmentController  PatientInvoiceController  InvoiceCtrl   │
│  Doctor/AdmissionCtrl   PharmacistController      BillCtrl      │
│  WalletController       PaymentController         MobileCtrl    │
└────────────┬──────────────────┬─────────────────────┬───────────┘
             │                  │                     │
             ▼                  ▼                     ▼
┌─────────────────┐  ┌──────────────────┐  ┌──────────────────────┐
│ DiagnosticService│  │  PatientService  │  │   PaymentService     │
│ ConsultationSvc  │  │  WalletService   │  │   BillingEngine      │
└────────┬────────┘  └────────┬─────────┘  └────────┬─────────────┘
         │                    │                      │
         ▼                    ▼                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                          MODELS                                 │
│  BillGroupCategory → BillGroup → BillItem (price catalog)       │
│  PatientBillItem (patient↔bill junction, THE billing pivot)     │
│  Invoice → InvoiceItem → Settlement (payment settlement)        │
│  PaymentHistory, Wallet, WalletTransaction                      │
│  PatientPharmacyDiagnostic, Diagnostic, RadiologyDiagnostic     │
│  BillDiscountApproval → BillDiscountApprovalItem                │
└─────────────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────┐
│  CBS (Monnify)  │ ← External payment gateway
└─────────────────┘
```

### Core Pipeline

```
Doctor orders test/medication        Admin adds bill manually
         │                                    │
         ▼                                    ▼
    DiagnosticService              PatientInvoiceController
    creates records +              addBillItem() / addMultipleBillItem()
    PatientBillItem                     │
         │                              │
         └─────────┬───────────────────┘
                   ▼
           PatientBillItem (CENTRAL TABLE)
           payment_status = UNSETTLED
           invoice_generated = false
                   │
                   ▼
        PatientInvoiceController::generateInvoice()
        → PatientService::generateInternalInvoice()
        → Creates Invoice + InvoiceItems
        → Sets invoice_generated = true
                   │
                   ▼
        InvoiceController::settleInvoice()
        → Accepts CASH / Bank Transfer / POS / Wallet / CBS
        → Updates InvoiceItem.amount_paid, payment_status
        → Updates PatientBillItem.payment_status
        → Creates PaymentHistory + Settlement records
        → For pharmacy: updates qty_pending_dispensation
```

---

## 2. Database Schema

### 2.1 `bill_group_categories` — Top-level service classification

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| hospital_id | bigint | FK (indexed) |
| bank_id | int | |
| name | string | e.g. "Clinic", "Laboratory", "X-Ray" |
| type | enum | `radiology`, `laboratory`, `clinic`, `pharmacy`, `procedure` |
| bank_name | string | Revenue account |
| account_number | string | Revenue account |
| account_name | string | Revenue account |
| is_active | boolean | Default true |

**Migration**: `2025_06_25_084628_create_bill_group_categories_table.php`  
**Model**: `app/Models/BillGroupCategory.php`

### 2.2 `bill_groups` — Revenue groupings with bank accounts for CBS split

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| hospital_id | bigint | FK (indexed) |
| bill_group_category_id | int | Added by `2025_07_25_120000` |
| bank_id | int | |
| name | string | e.g. "Pharmacy", "Laboratory" |
| bank_name | string | For CBS revenue splitting |
| account_number | string | For CBS revenue splitting |
| account_name | string | For CBS revenue splitting |
| is_active | boolean | Default true |

**Migrations**: `2025_04_15_212059_create_bill_groups_table.php`, `2025_07_25_120000_add_bill_group_category_id_to_bill_groups.php`  
**Model**: `app/Models/BillGroup.php`

### 2.3 `bill_items` — The price list / catalog

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| hospital_id | bigint | |
| branch_id | bigint | nullable |
| bill_group_id | int | nullable (added by `2025_04_15_212452`) |
| name | string | Item name |
| purchase_price | double | Default 0.0 |
| selling_price | double | Default 0.0 |
| status | int | Default `STATUS_ACTIVE` (1) |
| qty | – | Added later for pharmacy items |
| qty_pending_dispensation | – | Added by `2025_10_13_125125` — tracks paid-but-not-dispensed pharmacy items |
| sku | – | Added later |
| batch_no | – | Added later |
| production_batch_number | – | Added by `2025_10_23_021227` |
| last_edit | timestamp | nullable |
| last_edit_by_id | bigint | nullable |

**Migration**: `2022_10_28_102431_create_bill_items_table.php` + multiple ALTER migrations  
**Model**: `app/Models/BillItem.php` — `guarded = ['id']`, relationships: `hospital`, `billGroup`, `manufacturer`, `supplier`, `category`, `subCategory`, `composition`, `strength`, `inventory`, `labInvestigation`, `revenueSubHead`

### 2.4 `patient_bill_items` — **THE CENTRAL BILLING TABLE**

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| patient_id | bigint | |
| consultation_id | bigint | nullable (added `2025_07_18_120004`) |
| appointment_record_id | bigint | nullable (added `2025_09_15_140000`) |
| invoice_item_id | bigint | nullable (added `2025_07_18_120005`) |
| bill_item_id | bigint | FK to bill_items |
| quantity | decimal(8,2) | Default 1.00 (changed from int by `2025_07_10_120000`) |
| custom_amount | decimal(10,2) | nullable (added `2025_07_11_120003`) |
| discount_amount | decimal(10,2) | Default 0 (added `2025_07_11_120003`) |
| waiver_amount | decimal(15,2) | Default 0 (added `2026_02_18_120002`) |
| final_amount | decimal(10,2) | nullable (added `2025_07_11_120003`) |
| invoice_generated | boolean | Default false |
| approval_status | string | nullable (added `2026_02_18_120002`): null/pending/approved/disapproved |
| payment_status | int | Default `UNSETTLED` (1) |
| cbs_settlement | boolean | Default false (added `2025_08_12_120000`) |

**Migrations**: `2025_02_08_074226_create_patient_bill_items_table.php` + 6 ALTER migrations  
**Model**: `app/Models/PatientBillItem.php`

```php
// Key relationships
public function patient()           → belongsTo(Patient)
public function billItem()          → belongsTo(BillItem)
public function consultation()      → belongsTo(Consultation)
public function appointmentRecord() → belongsTo(AppointmentRecord)
public function invoiceItem()       → belongsTo(InvoiceItem)
public function discountApprovalItems() → hasMany(BillDiscountApprovalItem)
```

### 2.5 `invoices` — Generated from grouped PatientBillItems

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| patient_id | bigint | |
| hospital_id | bigint | nullable |
| reference | string | unique, nullable |
| payment_status | int | Default `UNSETTLED` (1) |
| cbs_settlement | boolean | Default false |
| total | double | Default 0.0 |
| original_total | double | Added by `2026_01_12_000001` |
| amount_due | double | Default 0.0 |
| amount_paid | double | Default 0.0 |
| invoice_number | string | Generated by `TokenGenerator::generateInvoiceID()` |
| description | text | nullable |
| payment_url | string | nullable — CBS payment link |
| payment_method | string | Default 'WEB' (added `2025_03_13_023332`) |
| email | string | nullable |
| phone | string | nullable |
| request_reference | string | nullable, unique (added `2025_11_27`) |
| processed_by_id | bigint | nullable (added `2025_03_28_132909`) |
| paid_at | datetime | nullable (added `2025_12_18_000003`) |
| patient_billing_category_id | bigint | nullable (added `2025_11_27`) |
| contract_multiplier | decimal(5,2) | nullable (added `2025_11_27`) |
| hmo_requires_preauth | boolean | Default false |
| hmo_preauth_status | enum | pending/approved/rejected/not_required |
| hmo_settlement_status | string | pending/settled/not_applicable |
| hmo_settlement_batch_id | bigint | nullable |
| linked_entity_type | string | nullable — polymorphic (Employer, HmoProvider, etc.) |
| linked_entity_id | bigint | nullable |
| status | string | nullable — OVERDUE etc. |

**Migrations**: `2022_11_15_150233_create_invoices_table.php` + 8 ALTER migrations  
**Model**: `app/Models/Invoice.php`

### 2.6 `invoice_items` — Individual line items on an invoice

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| invoice_id | bigint | FK to invoices |
| hospital_id | bigint | |
| bill_item_id | bigint | FK to patient_bill_items.id (NOT bill_items.id) |
| payment_status | int | Default `UNSETTLED` (1) |
| payment_channel | string | nullable |
| name | string | Copy of bill item name |
| amount | double | Default 0.0 |
| amount_paid | double | Default 0.0 |
| base_amount | decimal(15,2) | nullable — before multiplier (added `2025_11_27`) |
| multiplier | decimal(5,2) | Default 1.0 (added `2025_11_27`) |
| service_coverage_status | enum | covered/not_covered/partial (added `2025_11_27`) |
| year | mediumint | nullable |
| waiting | boolean | Default false |

**Migrations**: `2022_10_28_102503_create_invoice_items_table.php`, `2025_11_27_add_billing_fields_to_invoices_and_invoice_items.php`  
**Model**: `app/Models/InvoiceItem.php`

**Important**: `InvoiceItem.bill_item_id` points to `patient_bill_items.id`, NOT `bill_items.id`. The `billItem()` relationship returns `PatientBillItem`:

```php
public function billItem(): BelongsTo {
    return $this->belongsTo(PatientBillItem::class, 'bill_item_id');
}
```

### 2.7 `settlements` — Per-item payment records

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| payment_history_id | bigint | nullable |
| identifier | string | |
| reference | string | nullable |
| invoice_id | bigint | |
| invoice_item_id | bigint | |
| total_fee | double | Default 0.0 |
| total_paid | double | Default 0.0 |
| total_cost_price | double | Default 0.0 |
| total_selling_price | double | Default 0.0 |
| item_total_profit | double | Default 0.0 |
| item_paid_profit | double | Default 0.0 |
| narration | text | nullable |
| tx_date | timestamp | nullable |

**Migration**: `2022_11_03_104608_create_settlements_table.php`  
**Model**: `app/Models/Settlement.php`

### 2.8 `payment_histories` — Transaction-level payment records

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| invoice_id | bigint | |
| patient_id | bigint | nullable |
| transaction_id | string | |
| request_reference | string | |
| reference | string | |
| total_cost_price | double | |
| total_bill_amount | double | |
| amount_paid | double | |
| total_profit | double | |
| paid_profit | double | |
| provider | double | |
| collection_bal | double | |
| hospital_collection | double | |
| igr_collection | double | |
| hospital_id | bigint | nullable |
| amount | double | |
| payment_link | text | |
| tx_date | timestamp | |
| payment_method | string | nullable (added `2026_02_19`) |
| pos_type | string | nullable (added `2026_02_19`) |
| bank_name | string | nullable (added `2026_02_19`) |
| account_number | string | nullable (added `2026_02_19`) |
| processed_by_id | bigint | nullable (added `2026_02_19`) |

**Migration**: `2023_04_27_214650_create_payment_histories_table.php`, `2026_02_19_000001_add_audit_fields_to_payment_histories_table.php`  
**Model**: `app/Models/PaymentHistory.php`

### 2.9 `wallets` / `wallet_transactions`

**wallets**:

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| patient_id | bigint | unique, FK |
| hospital_id | bigint | FK |
| balance | decimal(14,2) | Default 0 |
| currency_code | string | Default 'NGN' |
| status | enum | ACTIVE/FROZEN/CLOSED |

**wallet_transactions**:

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| wallet_id | bigint | FK |
| transaction_type | enum | CREDIT/DEBIT |
| amount | decimal(14,2) | |
| reference | string | unique |
| reason | string | DEPOSIT/PAYMENT/REFUND/ADJUSTMENT |
| related_entity_type | string | nullable |
| related_entity_id | bigint | nullable |
| balance_before | decimal(14,2) | |
| balance_after | decimal(14,2) | |
| initiated_by_id | bigint | nullable FK |
| processed_by_id | bigint | nullable FK |
| status | enum | PENDING/COMPLETED/FAILED/REVERSED |

**Migrations**: `2025_11_25_100008`, `2025_11_25_100009`  
**Models**: `app/Models/Wallet.php`, `app/Models/WalletTransaction.php`

### 2.10 `patient_pharmacy_diagnostics` — Pharmacy orders (parallel to PatientBillItem)

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| consultation_id | int | nullable |
| appointment_record_id | bigint | nullable (added `2025_09_14`) |
| patient_id | int | |
| fee_id | string | FK to bill_items.id (the medication) |
| quantity | int | Default 1 |
| invoice_generated | int | Default 0 |
| amount | double | Default 0 |
| payment_status | int | Default UNSETTLED |
| cbs_settlement | boolean | Default false (added `2025_08_12`) |
| prescription_status | int | Default 0 (0=pending, 1=dispensed) |
| comment | text | nullable |

**Migration**: `2024_07_25_154641_create_patient_pharmacy_diagnostics_table.php`  
**Model**: `app/Models/PatientPharmacyDiagnostic.php`

### 2.11 `patient_admissions`

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| patient_id | bigint | |
| consultation_id | bigint | nullable — original consultation |
| ward_id, room_id, bed_id | bigint | |
| doctor_id | bigint | Admitting doctor |
| hospital_id, branch_id | bigint | |
| admission_date | datetime | |
| discharge_date | datetime | nullable |
| admission_reason | text | |
| status | enum | admitted/discharged/transferred/deceased |
| admission_fee_charged | boolean | Default false |
| admission_fee_amount | decimal(10,2) | nullable |

**Migration**: `2025_09_01_120003_create_patient_admissions_table.php`  
**Model**: `app/Models/PatientAdmission.php`

### 2.12 `bill_discount_approvals` / `bill_discount_approval_items`

**bill_discount_approvals**:

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| hospital_id | bigint | |
| patient_id | bigint | |
| reference | string | unique, generated by `BillDiscountApproval::generateReference()` |
| request_type | string | "discount" or "waiver" |
| status | string | pending/approved/disapproved |
| requested_by_id | bigint | |
| assigned_to_id | bigint | nullable |
| approved_by_id | bigint | nullable |
| reason | text | |
| total_original_amount | decimal(15,2) | |
| total_discount_amount | decimal(15,2) | |
| total_waiver_amount | decimal(15,2) | |
| total_new_amount | decimal(15,2) | |

**bill_discount_approval_items**:

| Column | Type | Notes |
|--------|------|-------|
| id | bigint PK | |
| bill_discount_approval_id | bigint | FK |
| patient_bill_item_id | bigint | FK |
| original_amount | decimal(15,2) | |
| discount_amount | decimal(15,2) | |
| waiver_amount | decimal(15,2) | |
| new_amount | decimal(15,2) | |

**Migrations**: `2026_02_18_120000`, `2026_02_18_120001`  
**Models**: `app/Models/BillDiscountApproval.php`, `app/Models/BillDiscountApprovalItem.php`

### 2.13 `payment_statuses` — Reference table

| ID | Name |
|----|------|
| 1 | UNSETTLED |
| 2 | PARTIAL |
| 3 | PAID |

**Model**: `app/Models/PaymentStatus.php` — constants `UNSETTLED = 1`, `PARTIAL = 2`, `PAID = 3`

---

## 3. Bill Item Price List (Admin CRUD)

**Controller**: `app/Http/Controllers/Admin/BillController.php` (383 lines)

### 3.1 Add Bill Item

**Route**: `POST /api/admin/bills/add`

```
Request:
  - bill_group_id: required|exists:bill_groups,id
  - name: required|string
  - selling_price: required
  - purchase_price: nullable
  - description: nullable

Logic:
  1. Find BillGroup → check if type is 'Pharmacy'
  2. If Pharmacy:
     → BillItem::firstOrCreate (name + hospital_id)
     → PharmacyItem::firstOrCreate (name + bill_group_id + hospital_id)
     → Generates SKU and batch_no
  3. If NOT Pharmacy:
     → BillItem::firstOrCreate with all fields
  4. Returns created BillItem
```

### 3.2 Update / List / Show / Search

- `POST /api/admin/bills/update` — updates BillItem fields
- `GET /api/admin/bills/list?type=&per_page=` — paginated list, filterable by BillGroupCategory type
- `GET /api/admin/bills/show/{bill}` — single item with billGroup relationship
- `GET /api/admin/bills/search?keyword=&type=` — Spatie Searchable + optional type filter

---

## 4. Entry Points: How Bills Are Created

Bills enter `patient_bill_items` through **4 distinct channels**:

| # | Channel | Who Triggers | Service/Method | Creates PatientBillItem? |
|---|---------|-------------|----------------|--------------------------|
| 1 | Doctor orders lab test | Doctor | `DiagnosticService::createLabDiagnostic()` | ✅ Yes |
| 2 | Doctor orders radiology | Doctor | `DiagnosticService::createRadiologyDiagnostic()` | ✅ Yes |
| 3 | Doctor orders medication | Doctor | `DiagnosticService::createPharmacyDiagnostic()` | ✅ Yes |
| 4 | Admin adds bill manually | Admin/Receptionist | `PatientInvoiceController::addBillItem()` | ✅ Yes |
| 5 | Doctor admits patient | Doctor | `Doctor/AdmissionController::admitPatient()` | ✅ Yes (if fee) |
| 6 | Pharmacist adds brand | Pharmacist | `PharmacistController::storeBrand()` | ❌ No (only BillItem) |

---

## 5. Outpatient Billing Flow

### Step 1: Consultation Booking

**File**: `app/Services/ConsultationService.php`  
**Method**: `ConsultationService::bookAppointment($data)`

```php
// Creates or updates Consultation record
$consultation = Consultation::updateOrCreate(
    ['hospital_id' => ..., 'patient_id' => ...],
    ['doctor_id' => ..., 'name' => ..., 'scheduled_date' => ...]
);
// Creates AppointmentRecord
$appointmentRecord = AppointmentRecord::create([
    'appointment_id' => $consultation->id,
    ...
]);
```

### Step 2: Doctor Orders Tests/Medications

**File**: `app/Http/Controllers/Admin/AppointmentController.php`

#### 2a. Send to Laboratory

**Route**: `POST /api/admin/patient/send-to-laboratory`

```
Request:
  - consultation_id (or patient_id + extra='Outpatient consultation')
  - test_ids: array of bill_item IDs
  - lab_note: optional

Logic:
  1. If outpatient without consultation → auto-creates dummy consultation via ConsultationService
  2. Gets current AppointmentRecord
  3. For each test_id:
     → Checks DiagnosticService::diagnosticExists() to avoid duplicates
     → Calls DiagnosticService::createLabDiagnosticForAppointment()
  4. Updates consultation with lab_test_ids JSON
```

#### 2b. Send to Radiology

**Route**: `POST /api/admin/patient/send-to-radiology`

Same pattern as laboratory, calls `DiagnosticService::createRadiologyDiagnosticForAppointment()`.

#### 2c. Send to Pharmacy

**Route**: `POST /api/admin/patient/send-to-pharmacy`

```
Request:
  - consultation_id (or patient_id + extra='Outpatient consultation')
  - medications: array of {medication_id, quantity, dosage, administration_mode, ...}
  - pharmacy_note: optional

Logic:
  1. Auto-creates consultation if needed (outpatient flow)
  2. For each medication:
     → Checks DiagnosticService::medicationExists()
     → Calls DiagnosticService::createPharmacyDiagnosticForAppointment()
     → Creates PharmacistPrescription record if dosage/administration provided
```

### Step 3: DiagnosticService Creates PatientBillItem

**File**: `app/Services/DiagnosticService.php` (~280 lines)

#### Lab Diagnostic

```php
public static function createLabDiagnostic($consultation, $patient, $testId)
{
    $billItem = BillItem::find($testId);
    
    // 1. Create Diagnostic record
    Diagnostic::create([
        'consultation_id' => $consultation->id,
        'patient_id' => $patient->id,
        'fee_id' => $testId,
        'amount' => $billItem->selling_price
    ]);
    
    // 2. Create PatientBillItem
    PatientBillItem::create([
        'patient_id' => $patient->id,
        'consultation_id' => $consultation->id,
        'bill_item_id' => $testId,
        'quantity' => 1,
        'final_amount' => $billItem->selling_price
    ]);
}
```

#### Radiology Diagnostic

Same pattern. Creates `RadiologyDiagnostic` + `PatientBillItem`.

#### Pharmacy Diagnostic

```php
public static function createPharmacyDiagnostic($consultation, $patient, $medication)
{
    $billItem = BillItem::find($medication['medication_id']);
    $pharmacyItem = PharmacyItem::where('name', $billItem->name)->first();
    
    // Uses PharmacyItem.selling_price as authoritative price
    $amount = $pharmacyItem 
        ? $pharmacyItem->selling_price * $medication['quantity']
        : $billItem->selling_price * $medication['quantity'];
    
    // 1. Create PatientPharmacyDiagnostic record
    $ppd = PatientPharmacyDiagnostic::create([
        'consultation_id' => $consultation->id,
        'patient_id' => $patient->id,
        'fee_id' => $medication['medication_id'],
        'quantity' => $medication['quantity'],
        'amount' => $amount
    ]);
    
    // 2. Create PatientBillItem
    PatientBillItem::create([
        'patient_id' => $patient->id,
        'consultation_id' => $consultation->id,
        'bill_item_id' => $medication['medication_id'],
        'quantity' => $medication['quantity'],
        'final_amount' => $amount
    ]);
    
    return $ppd;
}
```

**Key insight**: Every diagnostic order creates **both** a domain-specific record (Diagnostic, RadiologyDiagnostic, PatientPharmacyDiagnostic) **and** a `PatientBillItem` record. Billing flows through `PatientBillItem`; domain records are for clinical tracking.

---

## 6. Inpatient Billing Flow

### Step 1: Admit Patient

**File**: `app/Http/Controllers/Doctor/AdmissionController.php`  
**Route**: `POST /api/admin/doctor/admit-patient`

```
Request:
  - patient_id: required
  - consultation_id: required
  - ward_id, room_id, bed_id: required
  - admission_reason: required
  - admission_fee_amount: nullable|numeric|min:0
  - medical_notes, treatment_plan: optional

Logic (inside DB::beginTransaction):
  1. Find consultation, verify matching patient
  2. Check no active admission exists
  3. Create PatientAdmission record
  4. If admission_fee_amount provided:
     a. Find or create "Admission Fee" BillItem:
        BillItem::firstOrCreate(
          ['name' => 'Admission Fee', 'hospital_id' => ...],
          ['selling_price' => $feeAmount, ...]
        )
     b. Create PatientBillItem:
        PatientBillItem::create([
          'patient_id' => ...,
          'consultation_id' => ...,
          'bill_item_id' => $admissionBillItem->id,
          'quantity' => 1,
          'custom_amount' => $feeAmount,
          'final_amount' => $feeAmount,
          'payment_status' => PaymentStatus::UNSETTLED
        ])
     c. Mark admission: admission_fee_charged = true
  5. Update bed status to 'occupied'
  6. Commit transaction
```

### Step 2: During Admission

While a patient is admitted:
- Doctors can order **lab tests**, **radiology**, and **medications** using the same `sendToLaboratory`, `sendToRadiology`, `sendToPharmacy` routes
- These all tie to the patient's consultation, creating PatientBillItems
- Admin can add additional bills manually via `PatientInvoiceController`

### Step 3: Discharge

**Route**: `POST /api/admin/doctor/discharge-patient`

```
Logic:
  1. Find admission, verify doctor match
  2. Update admission: status='discharged', discharge_date=now()
  3. Update bed: status='available'
  4. (No automatic billing settlement at discharge — outstanding bills remain)
```

---

## 7. Manual Bill Addition

**File**: `app/Http/Controllers/Admin/PatientInvoiceController.php` (1086 lines)

### 7.1 Add Single Bill Item

**Route**: `POST /api/admin/patient-bills/item/add`

```
Request:
  - patient_id: required|exists:patients,id
  - bill_item_id: required|exists:bill_items,id
  - quantity: nullable|numeric|min:0.01
  - custom_amount: nullable|numeric|min:0
  - discount_amount: nullable|numeric|min:0
  - waiver_amount: nullable|numeric|min:0
  - appointment_record_id: nullable

Logic:
  1. Get BillItem → selling_price
  2. Calculate amounts:
     - base = custom_amount ?? selling_price
     - unitAmount = base * quantity
     - discountAmount + waiverAmount
     - finalAmount = unitAmount - discountAmount - waiverAmount (min 0)
  3. Determine if discount/waiver needs approval:
     - needsApproval = (discountAmount > 0 || waiverAmount > 0)
  4. If needs approval:
     → Save PatientBillItem at FULL price (finalAmount = unitAmount, no discount applied)
     → Set approval_status = 'pending'
     → Create BillDiscountApproval record (status='pending')
     → Create BillDiscountApprovalItem linking to the PatientBillItem
  5. If no approval needed:
     → Save PatientBillItem with discounts applied
     → approval_status = null
  6. Return created PatientBillItem
```

### 7.2 Add Multiple Bill Items (Batch)

**Route**: `POST /api/admin/patient-bills/item/add-multiple`

```
Request:
  - patient_id: required
  - items: required|array|min:1
  - items.*.bill_item_id: required|exists:bill_items,id
  - items.*.quantity: nullable|numeric|min:0.01
  - items.*.custom_amount: nullable|numeric|min:0
  - items.*.discount_amount: nullable|numeric|min:0
  - items.*.waiver_amount: nullable|numeric|min:0
  - discount_reason: required_if any item has discount/waiver
  - appointment_record_id: nullable

Logic:
  1. Iterates each item
  2. Same amount calculation as single add
  3. Collects all items needing approval
  4. Creates single BillDiscountApproval for ALL items needing approval (batch)
  5. Creates BillDiscountApprovalItem for each item
  6. Items without discount saved with final amounts applied
```

### 7.3 Update Bill Item

**Route**: `POST /api/admin/patient-bills/item/update`

```
Request:
  - patient_bill_item_id: required
  - quantity: required|numeric|min:0.01

Logic:
  1. Find PatientBillItem
  2. Verify payment_status == UNSETTLED
  3. Recalculate final_amount based on new quantity
  4. Update quantity and final_amount
```

### 7.4 Delete Bill Item

**Route**: `POST /api/admin/patient-bills/item/delete`

```
Request:
  - patient_bill_item_id: required

Logic:
  1. Find PatientBillItem
  2. Verify invoice_generated == false (can't delete if invoice exists)
  3. Delete the record
```

### 7.5 List Patient Bill Items

**Route**: `POST /api/admin/patient-bills/item/all`

```
Request:
  - patient_id: required

Logic:
  Returns PatientBillItems where:
    - invoice_generated = false
    - payment_status = UNSETTLED
  With: billItem relationship (name, selling_price)
```

### 7.6 List Patients with Pending Bills

**Route**: `GET /api/admin/patient-bills`

Returns patients who have PatientBillItems where `invoice_generated = false` and `payment_status = UNSETTLED`.

---

## 8. Discount / Waiver Approval Workflow

### Overview

When a bill item is added with a `discount_amount` or `waiver_amount`:

1. The `PatientBillItem` is saved at **full price** (no discount applied yet)
2. `approval_status` is set to `'pending'`
3. A `BillDiscountApproval` record is created with `status = 'pending'`
4. A `BillDiscountApprovalItem` links the approval to the PatientBillItem
5. An admin/Medical Director reviews and approves/disapproves

### On Approval

When approved (handled separately — likely a discount approval controller):
- `BillDiscountApproval.status` → `'approved'`
- Each `BillDiscountApprovalItem`:
  - Its linked `PatientBillItem.final_amount` gets reduced by discount + waiver
  - `PatientBillItem.discount_amount` and `waiver_amount` get set
  - `PatientBillItem.approval_status` → `'approved'`

### On Disapproval

- The PatientBillItem stays at full price
- `approval_status` → `'disapproved'`

**Models**:
- `BillDiscountApproval` (`app/Models/BillDiscountApproval.php`)
  - Constants: `STATUS_PENDING`, `STATUS_APPROVED`, `STATUS_DISAPPROVED`
  - `generateReference()` → `"DWA-{yymmddHHiiss}-{random}"`
- `BillDiscountApprovalItem` (`app/Models/BillDiscountApprovalItem.php`)

---

## 9. Invoice Generation

**Controller**: `app/Http/Controllers/Admin/PatientInvoiceController.php`  
**Route**: `POST /api/admin/patient/invoice/generate`

```
Request:
  - patient_id: required|exists:patients,id
  - patient_bill_item_ids: required|array|min:1
  - description: nullable|string

Logic:
  1. Validate patient_id
  2. Fetch PatientBillItems by IDs (that belong to this patient)
  3. Call PatientService::generateInternalInvoice($patient, $bills, $user, $description)
```

### PatientService::generateInternalInvoice()

**File**: `app/Services/PatientService.php` (lines 254–445)

```php
public static function generateInternalInvoice($patient, $bills, $user, $description = null): array
{
    // 1. Generate unique invoice number
    $invoiceNumber = TokenGenerator::generateInvoiceID(); // random 10-digit int

    // 2. Create Invoice record (starts with total=0)
    $invoice = $patient->invoices()->create([
        'invoice_number' => $invoiceNumber,
        'email' => $patient->email ?? dummy_email,
        'phone' => $patient->phone,
        'description' => $description ?? "Medical Bill Payment",
        'hospital_id' => $patient->hospital_id,
        'amount_due' => 0,
        'total' => 0,
        'original_total' => 0,
        'processed_by_id' => $user->id,
        // ...billing profile fields (HMO/employer — excluded from scope)
    ]);

    // 3. Create InvoiceItem for each PatientBillItem
    $total = 0;
    foreach ($bills as $bill) {
        $baseAmount = $bill->final_amount;
        $finalAmount = $baseAmount; // * contractMultiplier (excluded from scope)
        $total += $finalAmount;

        $createdBill = InvoiceItem::create([
            'invoice_id' => $invoice->id,
            'hospital_id' => $patient->hospital_id,
            'bill_item_id' => $bill->id,        // ← PatientBillItem.id
            'amount' => $finalAmount,
            'base_amount' => $baseAmount,
            'multiplier' => 1.0,                 // contractMultiplier
            'name' => $bill->billItem->name,     // BillItem name
            'year' => now()->format('Y')
        ]);

        // 4. Link PatientBillItem → InvoiceItem
        $bill->update([
            'invoice_generated' => 1,
            'invoice_item_id' => $createdBill->id
        ]);
    }

    // 5. Update invoice totals
    $invoice->total = $total;
    $invoice->original_total = $total; // before multiplier
    $invoice->amount_due = $total;
    $invoice->save();

    return ['success' => true, 'invoice' => $invoice, ...];
}
```

**Key relationships created**:
- `Invoice` → `InvoiceItem` (hasMany)
- `InvoiceItem.bill_item_id` → `PatientBillItem.id`
- `PatientBillItem.invoice_item_id` → `InvoiceItem.id`
- `PatientBillItem.invoice_generated` = `true`

### Delete Invoice

**Route**: `POST /api/admin/patient/invoice/delete`

```
Logic:
  1. Find Invoice, verify payment_status == UNSETTLED
  2. For each InvoiceItem:
     → Find linked PatientBillItem (via patientBillItem relationship)
     → Set invoice_generated = false, invoice_item_id = null
  3. Delete all InvoiceItems
  4. Delete Invoice
```

---

## 10. Invoice Settlement (Payment)

**Controller**: `app/Http/Controllers/Admin/InvoiceController.php` (1152 lines)  
**Route**: `POST /api/admin/settle-invoice`

### 10.1 Settlement Flow

```
Request:
  - invoice_id: required|exists:invoices,id
  - payment_method: required|string (CASH, Bank Transfer, POS, wallet, CBS)
  - amount: required|numeric|min:0
  - invoice_items: required|array
  - invoice_items.*.invoice_item_id: required
  - invoice_items.*.amount: required|numeric|min:0
  - pos_type: nullable (for POS)
  - bank_name: nullable (for Bank Transfer)
  - account_number: nullable (for Bank Transfer)

Logic:
  1. Validate inputs
  2. Calculate totalPaymentAmount from invoice_items
  3. If totalPaymentAmount == 0 → settle items with zero amount locally
  
  4. WALLET payment:
     a. Find patient's Wallet
     b. Check wallet.canDeduct(totalPaymentAmount)
     c. Deduct from wallet balance
     d. Create WalletTransaction (type=DEBIT, reason=PAYMENT)
  
  5. CBS payment:
     a. If invoice has no request_reference:
        → Call PaymentService::generateCBSPaymentReturningPatient()
        → Gets payment URL from CBS
     b. Call CBS 'settle_invoice' endpoint with:
        - invoice_number
        - transaction_id
        - amount
        - payment_method
     c. If CBS returns 402 → "Invoice already settled on CBS"
  
  6. For each invoice_item in request:
     a. Find InvoiceItem
     b. Calculate new amount_paid = existing + payment
     c. Determine payment_status:
        - If new_amount_paid >= amount → PAID
        - If new_amount_paid > 0 → PARTIAL
        - Else → UNSETTLED
     d. Update InvoiceItem: amount_paid, payment_status, payment_channel
     e. Find linked PatientBillItem → update payment_status
     f. Create Settlement record per item:
        Settlement::create([
          'payment_history_id' => ...,
          'identifier' => transaction_id,
          'invoice_id' => ...,
          'invoice_item_id' => ...,
          'total_fee' => item_amount,
          'total_paid' => payment_for_this_item,
          'total_cost_price' => ...,
          'total_selling_price' => ...,
          'item_total_profit' => ...,
          'item_paid_profit' => ...
        ])
     g. For pharmacy items (BillGroup name = 'Pharmacy'):
        → Update BillItem.qty_pending_dispensation += quantity
  
  7. Create PaymentHistory record:
     PaymentHistory::create([
       'invoice_id' => ...,
       'patient_id' => ...,
       'hospital_id' => ...,
       'transaction_id' => unique MSS + random,
       'request_reference' => invoice.request_reference,
       'amount' => totalPaymentAmount,
       'amount_paid' => totalPaymentAmount,
       'total_bill_amount' => invoice.total,
       'payment_link' => invoice.payment_url,
       'payment_method' => ...,
       'processed_by_id' => user.id,
       ...profit calculations...
     ])
  
  8. Update Invoice totals:
     - amount_paid += totalPaymentAmount
     - amount_due = total - amount_paid
     - payment_status: PAID if amount_due <= 0, PARTIAL if amount_paid > 0
     - payment_method
     - paid_at (if fully paid)
```

### 10.2 Reducing Balance Support

The settlement supports **partial payments**. Multiple payments can be made against the same invoice:

- `InvoiceItem.amount_paid` accumulates across payments
- Status transitions: `UNSETTLED → PARTIAL → PAID`
- Each payment creates a new `PaymentHistory` + `Settlement` records
- `Invoice.amount_due` decreases with each payment

### 10.3 Resolve Payment (Manual CBS Reconciliation)

**Route**: `POST /api/admin/invoices/resolve-payment`

```
Request: invoice_number, dry_run (optional)

Logic:
  1. Find invoice by invoice_number
  2. Use CbsPaymentFetcherService to check CBS for payments
  3. Reconcile CBS payments with local records
  4. Returns updated invoice state
```

### 10.4 Additional Invoice Endpoints

| Route | Method | Description |
|-------|--------|-------------|
| `GET /api/admin/invoices` | `index()` | List invoices with search, date filters, pagination |
| `GET /api/admin/invoices/{number}` | `singleInvoice()` | Detailed view with billing profile |
| `GET /api/admin/invoices/{id}/items` | `invoiceItems()` | Invoice line items |
| `GET /api/admin/invoices/{id}/items-paid` | `invoiceItemsPaid()` | Paid items only |
| `GET /api/admin/invoices/overdue` | `overdueInvoices()` | Overdue invoice list |
| `POST /api/admin/invoices/reconciliation` | `reconciliationReport()` | Period reconciliation |
| `GET /api/admin/invoices/{invoice}/reminders` | `reminderHistory()` | Reminder tracking |

---

## 11. Pharmacy Dispensation Billing

**File**: `app/Http/Controllers/Admin/PharmacistController.php`

### 11.1 Add Pharmacy Item (Inventory)

**Route**: `POST /api/admin/pharmacy-items/add`

```
Logic:
  1. Creates/updates BillItem with pharmacy fields
  2. Creates PharmacyItem if bill group is 'Pharmacy'
  3. Creates InventoryManagement record for batch tracking
  4. Generates SKU, batch_no
```

### 11.2 Store Brand

**Route**: `POST /api/admin/pharmacy-brand-inventory/store`

```
Logic:
  1. Creates BillItem + PharmacyItem pair
  2. Links via PharmacyItem.bill_item_id
  3. Sets initial qty to 0
```

### 11.3 Dispensation Flow

**Route**: `POST /api/admin/pharmacist/dispense-medication`

```
Request:
  - medication_id: required (patient_pharmacy_diagnostics.id)
  - dispensed_quantity: nullable|integer|min:1
  - dispensing_notes: nullable

Logic:
  1. Verify payment_status == PAID (unless user is NURSE)
  2. Verify not already dispensed (prescription_status != 1)
  3. Update PatientPharmacyDiagnostic: prescription_status = 1
  4. Update PharmacistPrescription quantity if provided
  5. Reduce stock:
     a. BillItem: qty -= dispensed_quantity, 
        qty_pending_dispensation = max(qty_pending_dispensation - dispensed_quantity, 0)
     b. PharmacyItem: qty -= dispensed_quantity, 
        qty_pending_dispensation -= dispensed_quantity
     c. InventoryManagement: deducts from earliest-expiring batches first (FEFO)
```

### 11.4 qty_pending_dispensation Lifecycle

```
1. Patient pays for medication (InvoiceController::settleInvoice):
   → BillItem.qty_pending_dispensation += quantity
   (This flags that meds are paid but not yet dispensed)

2. Pharmacist dispenses (PharmacistController::dispenseMedication):
   → BillItem.qty_pending_dispensation -= dispensed_quantity
   → BillItem.qty -= dispensed_quantity
   (Removes from pending and actual stock)
```

---

## 12. Wallet System

**Controller**: `app/Http/Controllers/Admin/WalletController.php`  
**Service**: `app/Services/WalletService.php`

### Routes

| Route | Method | Description |
|-------|--------|-------------|
| `GET /api/admin/billing/wallets/{patientId}/balance` | `getBalance()` | Current wallet balance |
| `GET /api/admin/billing/wallets/{patientId}/transactions` | `getTransactions()` | Transaction history |
| `POST /api/admin/billing/wallets/{patientId}/fund` | `fundWallet()` | Credit wallet |
| `POST /api/admin/billing/wallets/{patientId}/deduct` | `deductFromWallet()` | Debit wallet |
| `POST /api/admin/billing/wallets/{patientId}/freeze` | `freezeWallet()` | Freeze wallet |
| `POST /api/admin/billing/wallets/{patientId}/unfreeze` | `unfreezeWallet()` | Unfreeze wallet |

### Fund Wallet

```
Request: amount, reason, reference (optional)

Logic:
  1. Find or create Wallet for patient
  2. Record balance_before
  3. Increase wallet.balance
  4. Create WalletTransaction (type=CREDIT, status=COMPLETED)
  5. Return updated balance
```

### Wallet Integration with Invoice Settlement

In `InvoiceController::settleInvoice()`, when `payment_method == 'wallet'`:
1. Finds patient wallet
2. Checks `wallet.canDeduct(amount)` — validates balance >= amount and status == ACTIVE
3. Deducts balance
4. Creates WalletTransaction (type=DEBIT, reason=PAYMENT)
5. Proceeds with normal settlement flow

---

## 13. External / Mobile API

**Routes prefix**: `/api/external`  
**Auth**: `auth:merchant` middleware

| Route | Method | Description |
|-------|--------|-------------|
| `POST /external/search-patient` | Search patients |
| `POST /external/patient-bills` | Get patient's pending bills |
| `POST /external/search-bill-item` | Search bill items catalog |
| `POST /external/patient-bills/add` | Add single bill to patient |
| `POST /external/patient-bills/add-bulk` | Add multiple bills |
| `POST /external/patient-bills/update` | Update bill item qty |
| `POST /external/patient/invoice/generate` | Generate invoice |
| `POST /external/patient/invoice/pay` | Create payment |
| `POST /external/patient/invoice/search` | Search invoices |

These endpoints mirror the admin functionality via `MobileController`.

---

## 14. Route Map

### Bill Items (Price List CRUD)

| HTTP | Path | Controller@Method |
|------|------|-------------------|
| POST | `/api/admin/bills/add` | `BillController@add` |
| POST | `/api/admin/bills/update` | `BillController@update` |
| GET | `/api/admin/bills/list` | `BillController@index` |
| GET | `/api/admin/bills/show/{bill}` | `BillController@show` |
| GET | `/api/admin/bills/search` | `BillController@search` |

### Patient Bills (Adding bills to patients)

| HTTP | Path | Controller@Method |
|------|------|-------------------|
| GET | `/api/admin/patient-bills` | `PatientInvoiceController@getPatients` |
| POST | `/api/admin/patient-bills/item/all` | `PatientInvoiceController@getBillItems` |
| POST | `/api/admin/patient-bills/item/add` | `PatientInvoiceController@addBillItem` |
| POST | `/api/admin/patient-bills/item/add-multiple` | `PatientInvoiceController@addMultipleBillItem` |
| POST | `/api/admin/patient-bills/item/update` | `PatientInvoiceController@updateBillItem` |
| POST | `/api/admin/patient-bills/item/delete` | `PatientInvoiceController@deleteBillItem` |

### Patient Invoices

| HTTP | Path | Controller@Method |
|------|------|-------------------|
| POST | `/api/admin/patient/invoice/all` | `PatientInvoiceController@getInvoices` |
| POST | `/api/admin/patient/invoice/single` | `PatientInvoiceController@getSingleInvoice` |
| POST | `/api/admin/patient/invoice/generate` | `PatientInvoiceController@generateInvoice` |
| POST | `/api/admin/patient/invoice/delete` | `PatientInvoiceController@deleteInvoice` |
| POST | `/api/admin/patient/invoice/pay` | `PatientInvoiceController@makePayment` |

### Invoice Settlement

| HTTP | Path | Controller@Method |
|------|------|-------------------|
| GET | `/api/admin/invoices` | `InvoiceController@index` |
| GET | `/api/admin/invoices/{number}` | `InvoiceController@singleInvoice` |
| POST | `/api/admin/settle-invoice` | `InvoiceController@settleInvoice` |
| POST | `/api/admin/invoices/resolve-payment` | `InvoiceController@resolvePayment` |
| GET | `/api/admin/invoices/overdue` | `InvoiceController@overdueInvoices` |
| POST | `/api/admin/invoices/reconciliation` | `InvoiceController@reconciliationReport` |

### Doctor / Referral Routes

| HTTP | Path | Controller@Method |
|------|------|-------------------|
| POST | `/api/admin/patient/send-to-laboratory` | `AppointmentController@sendToLaboratory` |
| POST | `/api/admin/patient/send-to-radiology` | `AppointmentController@sendToRadiology` |
| POST | `/api/admin/patient/send-to-pharmacy` | `AppointmentController@sendToPharmacy` |
| POST | `/api/admin/doctor/admit-patient` | `Doctor/AdmissionController@admitPatient` |
| POST | `/api/admin/doctor/discharge-patient` | `Doctor/AdmissionController@dischargePatient` |

### Pharmacy

| HTTP | Path | Controller@Method |
|------|------|-------------------|
| POST | `/api/admin/pharmacy-items/add` | `PharmacistController@addItem` |
| GET | `/api/admin/pharmacist/appointments` | `PharmacistController@getPharmacyAppointments` |
| GET | `/api/admin/pharmacist/patient-medications` | `PharmacistController@getPatientMedications` |
| POST | `/api/admin/pharmacist/dispense-medication` | `PharmacistController@dispenseMedication` |

### Wallets

| HTTP | Path | Controller@Method |
|------|------|-------------------|
| GET | `/api/admin/billing/wallets/{id}/balance` | `WalletController@getBalance` |
| POST | `/api/admin/billing/wallets/{id}/fund` | `WalletController@fundWallet` |
| POST | `/api/admin/billing/wallets/{id}/deduct` | `WalletController@deductFromWallet` |

### Bulk Payment

| HTTP | Path | Controller@Method |
|------|------|-------------------|
| POST | `/api/admin/bulk-payment` | `PaymentController@makeBulkPayment` |

---

## 15. Complete File Index

### Controllers

| File | Lines | Role |
|------|-------|------|
| `app/Http/Controllers/Admin/BillController.php` | 383 | Bill item CRUD (price list) |
| `app/Http/Controllers/Admin/PatientInvoiceController.php` | 1086 | Add bills to patients, generate/delete invoices, make payments |
| `app/Http/Controllers/Admin/InvoiceController.php` | 1152 | Invoice listing, settlement, reconciliation, overdue |
| `app/Http/Controllers/Admin/AppointmentController.php` | 2523 | sendToLaboratory/Radiology/Pharmacy (creates PatientBillItems via DiagnosticService) |
| `app/Http/Controllers/Admin/PharmacistController.php` | 1658 | Pharmacy inventory, dispensation, brand management |
| `app/Http/Controllers/Admin/PaymentController.php` | ~55 | Bulk payment processing |
| `app/Http/Controllers/Admin/WalletController.php` | ~200 | Wallet CRUD |
| `app/Http/Controllers/Admin/BillingEngineController.php` | ~150 | Price calculation engine |
| `app/Http/Controllers/Admin/PatientAdmissionController.php` | ~200 | Admin admission views (read-only) |
| `app/Http/Controllers/Doctor/AdmissionController.php` | ~300 | Admit/discharge patients (creates admission fee bill) |

### Services

| File | Lines | Role |
|------|-------|------|
| `app/Services/DiagnosticService.php` | ~280 | Creates Diagnostic/Radiology/Pharmacy records + PatientBillItems |
| `app/Services/PatientService.php` | 586 | `generateInternalInvoice()`, `generateRegistrationInvoice()`, `generatePharmacyMedicationInvoice()` |
| `app/Services/PaymentService.php` | ~400 | CBS payment integration, revenue splitting, transaction IDs |
| `app/Services/ConsultationService.php` | ~200 | Consultation/appointment booking |
| `app/Services/WalletService.php` | ~200 | Wallet fund/deduct/freeze operations |
| `app/Services/BillingEngine.php` | ~300 | Contract/multiplier-based pricing |
| `app/Services/TokenGenerator.php` | 213 | `generateInvoiceID()`, `generateTransactionReference()` |

### Models

| File | Table | Role |
|------|-------|------|
| `app/Models/BillGroupCategory.php` | bill_group_categories | Top-level service classification |
| `app/Models/BillGroup.php` | bill_groups | Revenue group with bank account |
| `app/Models/BillItem.php` | bill_items | Price catalog item |
| `app/Models/PatientBillItem.php` | patient_bill_items | **CENTRAL** — patient↔bill junction |
| `app/Models/Invoice.php` | invoices | Invoice header |
| `app/Models/InvoiceItem.php` | invoice_items | Invoice line item |
| `app/Models/Settlement.php` | settlements | Per-item payment record |
| `app/Models/PaymentHistory.php` | payment_histories | Transaction-level payment record |
| `app/Models/Wallet.php` | wallets | Patient wallet |
| `app/Models/WalletTransaction.php` | wallet_transactions | Wallet ledger entry |
| `app/Models/PatientPharmacyDiagnostic.php` | patient_pharmacy_diagnostics | Pharmacy orders (clinical) |
| `app/Models/Consultation.php` | consultations | Patient consultation/appointment |
| `app/Models/PatientBillingProfile.php` | patient_billing_profiles | Patient↔billing category link |
| `app/Models/BillingCategory.php` | billing_categories | Category: Regular/HMO/Employer |
| `app/Models/PaymentStatus.php` | payment_statuses | UNSETTLED=1, PARTIAL=2, PAID=3 |
| `app/Models/Patient.php` | patients | Patient record |
| `app/Models/PatientAdmission.php` | patient_admissions | Inpatient admission |
| `app/Models/BillDiscountApproval.php` | bill_discount_approvals | Discount/waiver requests |
| `app/Models/BillDiscountApprovalItem.php` | bill_discount_approval_items | Per-item discount detail |

### Migrations (Billing-Related)

| File | What It Creates/Modifies |
|------|--------------------------|
| `2022_10_28_102431_create_bill_items_table.php` | bill_items (base) |
| `2022_10_28_102503_create_invoice_items_table.php` | invoice_items (base) |
| `2022_11_02_085711_create_payment_statuses_table.php` | payment_statuses |
| `2022_11_03_104608_create_settlements_table.php` | settlements |
| `2022_11_15_150233_create_invoices_table.php` | invoices (base) |
| `2023_04_27_214650_create_payment_histories_table.php` | payment_histories |
| `2024_02_22_162611_create_consultations_table.php` | consultations |
| `2024_07_25_154641_create_patient_pharmacy_diagnostics_table.php` | patient_pharmacy_diagnostics |
| `2025_02_08_074226_create_patient_bill_items_table.php` | patient_bill_items (base) |
| `2025_03_13_023332_add_payment_method_to_invoices_table.php` | +invoices.payment_method |
| `2025_03_28_132909_add_processed_by_to_invoices_table.php` | +invoices.processed_by_id |
| `2025_04_15_212059_create_bill_groups_table.php` | bill_groups |
| `2025_04_15_212452_add_bill_group_id_to_bill_items_table.php` | +bill_items.bill_group_id |
| `2025_06_25_084628_create_bill_group_categories_table.php` | bill_group_categories |
| `2025_07_10_120000_alter_patient_bill_items_quantity_to_decimal.php` | patient_bill_items.quantity → decimal |
| `2025_07_11_120003_add_discount_fields_to_patient_bill_items_table.php` | +custom_amount, discount_amount, final_amount |
| `2025_07_18_120004_add_consultation_id_to_patient_bill_items.php` | +consultation_id |
| `2025_07_18_120005_add_invoice_item_id_to_patient_bill_items.php` | +invoice_item_id |
| `2025_07_25_120000_add_bill_group_category_id_to_bill_groups.php` | +bill_group_category_id |
| `2025_08_12_120000_add_cbs_settlement_to_payment_tables.php` | +cbs_settlement to 6 tables |
| `2025_09_01_120003_create_patient_admissions_table.php` | patient_admissions |
| `2025_09_15_140000_add_appointment_record_id_to_patient_bill_items.php` | +appointment_record_id |
| `2025_10_13_125125_add_qty_pending_dispensation_to_bill_items_table.php` | +qty_pending_dispensation |
| `2025_11_25_100008_create_wallets_table.php` | wallets |
| `2025_11_25_100009_create_wallet_transactions_table.php` | wallet_transactions |
| `2025_11_27_add_billing_fields_to_invoices_and_invoice_items.php` | +base_amount, multiplier, billing category, HMO fields |
| `2026_01_12_000001_add_original_total_to_invoices_table.php` | +original_total |
| `2026_02_18_120000_create_bill_discount_approvals_table.php` | bill_discount_approvals |
| `2026_02_18_120001_create_bill_discount_approval_items_table.php` | bill_discount_approval_items |
| `2026_02_18_120002_add_waiver_and_approval_to_patient_bill_items.php` | +waiver_amount, approval_status |
| `2026_02_19_000001_add_audit_fields_to_payment_histories_table.php` | +payment_method, pos_type, bank_name, etc. |

### Integrations

| File | Role |
|------|------|
| `app/Integrations/Cbs/Cbs.php` | CBS (Monnify) API client — create_invoice, settle_invoice, fetch_payments, process_refund |

### Other

| File | Role |
|------|------|
| `app/Helpers/AppConstant.php` | Constants: CONSULTATION_ACTIVE=0, CONSULTATION_CLOSED=1, STATUS_ACTIVE=1 |
| `app/Models/PaymentStatus.php` | UNSETTLED=1, PARTIAL=2, PAID=3 |
| `routes/api.php` | All API route definitions (880 lines) |

---

## Summary of Data Flow

```
                   ┌──────────────────┐
                   │ BillGroupCategory │
                   │  (radiology,     │
                   │   laboratory,    │
                   │   pharmacy, etc) │
                   └────────┬─────────┘
                            │ 1:N
                   ┌────────▼─────────┐
                   │    BillGroup      │
                   │  (bank account   │
                   │   for revenue    │
                   │   splitting)     │
                   └────────┬─────────┘
                            │ 1:N
                   ┌────────▼─────────┐
                   │    BillItem       │◄── Price Catalog
                   │  (selling_price) │
                   └────────┬─────────┘
                            │
            ┌───────────────┼────────────────────┐
            │               │                    │
    DiagnosticService    Manual Add        Admission Fee
            │               │                    │
            ▼               ▼                    ▼
     ┌──────────────────────────────────────────────┐
     │           PatientBillItem                     │
     │  patient_id, bill_item_id, quantity           │
     │  final_amount, payment_status=UNSETTLED       │
     │  invoice_generated=false                      │
     └─────────────────┬────────────────────────────┘
                       │
          generateInternalInvoice()
                       │
           ┌───────────▼────────────┐
           │       Invoice          │
           │  total, amount_due,    │
           │  payment_status        │
           └───────────┬────────────┘
                       │ 1:N
           ┌───────────▼────────────┐
           │     InvoiceItem        │
           │  amount, amount_paid,  │
           │  payment_status        │
           └───────────┬────────────┘
                       │
             settleInvoice()
             (CASH/POS/Bank/Wallet/CBS)
                       │
           ┌───────────▼────────────┐
           │   PaymentHistory       │  (transaction record)
           │   Settlement           │  (per-item record)
           │   WalletTransaction    │  (if wallet payment)
           └────────────────────────┘
```
