# Phase 2 + 3 Gap-Fill Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Complete all remaining Phase 2 (Session Import) and Phase 3 (Financial Processing) features by filling gaps in the existing codebase.

**Architecture:** Vertical slices in dependency order. Each task is a complete feature with model/service/component/view/test. Existing Livewire patterns, Tailwind CSS styling, and Pest test conventions are followed exactly.

**Tech Stack:** Laravel 13, Livewire 4, Tailwind CSS 4, Alpine.js, Pest 4, MariaDB

---

## File Map

| File | Action | Responsibility |
|------|--------|----------------|
| `app/Livewire/Admin/AdminSettings.php` | Modify | Add insurance payers, charge prices, surcharge rate |
| `resources/views/livewire/admin/admin-settings.blade.php` | Modify | Add 3 new settings sections UI |
| `tests/Feature/Admin/AdminSettingsTest.php` | Create | Test all settings features |
| `app/Models/Charge.php` | Modify | Add `isSettled()` + immutability guards |
| `tests/Feature/Models/ChargeImmutabilityTest.php` | Create | Test settled charge protection |
| `app/Livewire/Admin/ImportAcceptanceSettings.php` | Create | Read-only import rules reference |
| `resources/views/livewire/admin/import-acceptance-settings.blade.php` | Create | Reference table view |
| `resources/views/admin/import-acceptance-settings.blade.php` | Create | Page wrapper |
| `tests/Feature/Admin/ImportAcceptanceSettingsTest.php` | Create | Test page renders with data |
| `app/Livewire/Admin/ClinicianSupportServices.php` | Create | Per-clinician services + charges page |
| `resources/views/livewire/admin/clinician-support-services.blade.php` | Create | Three-section services view |
| `resources/views/admin/clinician-support-services.blade.php` | Create | Page wrapper |
| `tests/Feature/Admin/ClinicianSupportServicesTest.php` | Create | Test service assignment + charge creation |
| `app/Console/Commands/GenerateMonthlyCharges.php` | Create | Monthly charge auto-generation |
| `tests/Feature/Commands/GenerateMonthlyChargesTest.php` | Create | Test full/prorated/idempotent |
| `app/Livewire/Admin/FeeProcessing.php` | Modify | Add selective processing + card surcharge |
| `resources/views/livewire/admin/fee-processing.blade.php` | Modify | Add checkboxes + surcharge display |
| `tests/Feature/Admin/FeeProcessingEnhancedTest.php` | Create | Test selective processing + surcharge |
| `database/migrations/2026_04_04_000001_add_payment_method_type_to_clinicians.php` | Create | payment_method_type column |
| `database/migrations/2026_04_04_000002_add_status_and_error_to_disbursements.php` | Create | status + error_message columns |
| `app/Jobs/ProcessDisbursementTransfer.php` | Create | Async disbursement job |
| `app/Jobs/ProcessFeeCharge.php` | Create | Async fee settlement job |
| `tests/Feature/Jobs/ProcessDisbursementTransferTest.php` | Create | Test status transitions |
| `tests/Feature/Jobs/ProcessFeeChargeTest.php` | Create | Test settlement flow |
| `app/Services/DisbursementService.php` | Modify | Dispatch jobs in createBatch |
| `routes/web.php` | Modify | Add new routes |
| `routes/console.php` | Modify | Schedule monthly charge command |
| `app/View/Components/Sidebar.php` | Modify | Add new nav entries |
| `resources/views/livewire/admin/sessions-database.blade.php` | Modify | ID click behavior |
| `resources/views/livewire/admin/fee-processing.blade.php` | Modify | Clinician name link |

---

## Task 1: Charge Immutability Guards

**Files:**
- Modify: `app/Models/Charge.php:10-70`
- Create: `tests/Feature/Models/ChargeImmutabilityTest.php`

- [ ] **Step 1: Write the failing tests**

Create `tests/Feature/Models/ChargeImmutabilityTest.php`:

```php
<?php

use App\Enums\ChargeStatus;
use App\Models\Charge;
use App\Models\Clinician;
use App\Models\ServiceCatalog;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

it('allows updating a pending charge', function () {
    $charge = Charge::factory()->create(['status' => ChargeStatus::Pending]);
    $charge->update(['notes' => 'updated']);
    expect($charge->fresh()->notes)->toBe('updated');
});

it('prevents updating a settled charge', function () {
    $charge = Charge::factory()->settled()->create();
    $charge->update(['notes' => 'should fail']);
})->throws(DomainException::class, 'Settled charges cannot be modified.');

it('prevents deleting a settled charge', function () {
    $charge = Charge::factory()->settled()->create();
    $charge->delete();
})->throws(DomainException::class, 'Settled charges cannot be deleted.');

it('allows deleting a pending charge', function () {
    $charge = Charge::factory()->create(['status' => ChargeStatus::Pending]);
    $charge->delete();
    expect(Charge::find($charge->id))->toBeNull();
});

it('reports isSettled correctly', function () {
    $pending = Charge::factory()->create(['status' => ChargeStatus::Pending]);
    $settled = Charge::factory()->settled()->create();
    expect($pending->isSettled())->toBeFalse();
    expect($settled->isSettled())->toBeTrue();
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `php artisan test --compact --filter=ChargeImmutabilityTest`
Expected: Failures on "prevents updating" and "prevents deleting" and "isSettled" tests.

- [ ] **Step 3: Implement immutability guards in Charge model**

Add to `app/Models/Charge.php` after the existing `isPending()` method (line 69):

```php
public function isSettled(): bool
{
    return $this->settlement_id !== null;
}

protected static function booted(): void
{
    static::updating(function (Charge $charge) {
        if ($charge->getOriginal('settlement_id') !== null) {
            throw new \DomainException('Settled charges cannot be modified.');
        }
    });

    static::deleting(function (Charge $charge) {
        if ($charge->settlement_id !== null) {
            throw new \DomainException('Settled charges cannot be deleted.');
        }
    });
}
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `php artisan test --compact --filter=ChargeImmutabilityTest`
Expected: All 5 tests pass.

- [ ] **Step 5: Run Pint and commit**

```bash
vendor/bin/pint --dirty --format agent
git add app/Models/Charge.php tests/Feature/Models/ChargeImmutabilityTest.php
git commit -m "feat: add immutability guards to settled charges"
```

---

## Task 2: Admin Settings — Insurance Payers Management

**Files:**
- Modify: `app/Livewire/Admin/AdminSettings.php:1-71`
- Modify: `resources/views/livewire/admin/admin-settings.blade.php:1-70`
- Create: `tests/Feature/Admin/AdminSettingsTest.php`

- [ ] **Step 1: Write the failing tests**

Create `tests/Feature/Admin/AdminSettingsTest.php`:

```php
<?php

use App\Livewire\Admin\AdminSettings;
use App\Models\Clinician;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

it('loads existing payers on mount', function () {
    Setting::set('insurance_payers', json_encode(['Aetna', 'Moda']));
    $admin = User::factory()->admin()->create();

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->assertSet('payers', ['Aetna', 'Moda']);
});

it('adds a new payer', function () {
    $admin = User::factory()->admin()->create();

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->set('newPayerName', 'Providence')
        ->call('addPayer')
        ->assertSet('newPayerName', '')
        ->assertSet('payers', ['Providence']);

    $stored = json_decode(Setting::get('insurance_payers'), true);
    expect($stored)->toBe(['Providence']);
});

it('prevents adding a duplicate payer', function () {
    Setting::set('insurance_payers', json_encode(['Aetna']));
    $admin = User::factory()->admin()->create();

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->set('newPayerName', 'Aetna')
        ->call('addPayer')
        ->assertHasErrors(['newPayerName']);
});

it('prevents adding an empty payer name', function () {
    $admin = User::factory()->admin()->create();

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->set('newPayerName', '')
        ->call('addPayer')
        ->assertHasErrors(['newPayerName']);
});

it('shows affected clinicians when removing a payer in use', function () {
    Setting::set('insurance_payers', json_encode(['Aetna', 'Moda']));
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();
    $clinician->user->update(['insurance_accepted' => json_encode(['Aetna'])]);

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->call('confirmRemovePayer', 'Aetna')
        ->assertSet('removingPayer', 'Aetna')
        ->assertNotEmpty('affectedClinicians');
});

it('removes a payer and clears it from clinician profiles', function () {
    Setting::set('insurance_payers', json_encode(['Aetna', 'Moda']));
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();
    $clinician->user->update(['insurance_accepted' => json_encode(['Aetna', 'Moda'])]);

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->call('confirmRemovePayer', 'Aetna')
        ->call('executeRemovePayer')
        ->assertSet('removingPayer', null)
        ->assertSet('payers', ['Moda']);

    $stored = json_decode(Setting::get('insurance_payers'), true);
    expect($stored)->toBe(['Moda']);
    expect(json_decode($clinician->user->fresh()->insurance_accepted, true))->toBe(['Moda']);
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `php artisan test --compact --filter=AdminSettingsTest`
Expected: Failures — payers properties and methods don't exist yet.

- [ ] **Step 3: Implement payer management in AdminSettings component**

Replace `app/Livewire/Admin/AdminSettings.php` with:

```php
<?php

namespace App\Livewire\Admin;

use App\Models\Setting;
use App\Models\User;
use App\Services\AuditLogService;
use Illuminate\Support\Facades\Hash;
use Illuminate\View\View;
use Livewire\Component;

class AdminSettings extends Component
{
    // Auth code properties
    public string $current_auth_code = '';
    public string $new_auth_code = '';
    public string $confirm_auth_code = '';
    public bool $saved = false;
    public bool $hasAuthCode = false;

    // Insurance payers properties
    /** @var string[] */
    public array $payers = [];
    public string $newPayerName = '';
    public ?string $removingPayer = null;
    /** @var array<int, string> */
    public array $affectedClinicians = [];

    // Session charge prices
    public string $ipPrice = '';
    public string $oopPrice = '';
    public bool $pricesSaved = false;

    // Card surcharge rate
    public string $surchargeRate = '';
    public bool $surchargeSaved = false;

    public function mount(): void
    {
        $this->hasAuthCode = Setting::get('payout_auth_code') !== null;

        $payersJson = Setting::get('insurance_payers');
        $this->payers = $payersJson ? json_decode($payersJson, true) : [];

        $ipService = \App\Models\ServiceCatalog::query()
            ->where('is_session_based', true)
            ->where('title', 'like', '%IP%')
            ->first();
        $oopService = \App\Models\ServiceCatalog::query()
            ->where('is_session_based', true)
            ->where('title', 'like', '%OOP%')
            ->first();

        $this->ipPrice = $ipService?->default_price ?? '15.00';
        $this->oopPrice = $oopService?->default_price ?? '6.00';

        $rate = Setting::get('card_surcharge_rate', '0.035');
        $this->surchargeRate = (string) round((float) $rate * 100, 2);
    }

    public function saveAuthCode(): void
    {
        $rules = [
            'new_auth_code' => ['required', 'string', 'size:4', 'regex:/^\d{4}$/'],
            'confirm_auth_code' => ['required', 'same:new_auth_code'],
        ];

        if ($this->hasAuthCode) {
            $rules['current_auth_code'] = ['required', 'string'];
        }

        $this->validate($rules, [
            'new_auth_code.size' => 'Must be exactly 4 digits.',
            'new_auth_code.regex' => 'Must be exactly 4 digits.',
            'confirm_auth_code.same' => 'Codes do not match.',
        ]);

        if ($this->hasAuthCode) {
            $stored = Setting::get('payout_auth_code');
            if (! Hash::check($this->current_auth_code, $stored)) {
                $this->addError('current_auth_code', 'Current code is incorrect.');

                return;
            }
        }

        Setting::set('payout_auth_code', Hash::make($this->new_auth_code));

        AuditLogService::log(
            action: 'setting.payout_auth_code_changed',
            actor: auth()->user(),
        );

        $this->reset(['current_auth_code', 'new_auth_code', 'confirm_auth_code']);
        $this->hasAuthCode = true;
        $this->saved = true;
    }

    public function addPayer(): void
    {
        $this->validate([
            'newPayerName' => ['required', 'string', 'max:100'],
        ]);

        $trimmed = trim($this->newPayerName);

        if (in_array($trimmed, $this->payers, true)) {
            $this->addError('newPayerName', 'This payer already exists.');

            return;
        }

        $this->payers[] = $trimmed;
        sort($this->payers);
        Setting::set('insurance_payers', json_encode($this->payers));
        $this->newPayerName = '';

        AuditLogService::log(
            action: 'setting.insurance_payer_added',
            actor: auth()->user(),
            metadata: ['payer' => $trimmed],
        );
    }

    public function confirmRemovePayer(string $name): void
    {
        $this->removingPayer = $name;
        $this->affectedClinicians = User::query()
            ->where('insurance_accepted', 'like', '%"' . $name . '"%')
            ->pluck('first_name')
            ->map(fn ($first, $key) => User::query()
                ->where('insurance_accepted', 'like', '%"' . $name . '"%')
                ->get()[$key]->full_name)
            ->toArray();

        // Simpler approach: query users whose JSON insurance_accepted contains the payer
        $this->affectedClinicians = User::query()
            ->whereNotNull('insurance_accepted')
            ->get()
            ->filter(function (User $user) use ($name) {
                $accepted = json_decode($user->insurance_accepted ?? '[]', true);

                return is_array($accepted) && in_array($name, $accepted, true);
            })
            ->map(fn (User $user) => $user->full_name)
            ->values()
            ->toArray();
    }

    public function cancelRemovePayer(): void
    {
        $this->removingPayer = null;
        $this->affectedClinicians = [];
    }

    public function executeRemovePayer(): void
    {
        if (! $this->removingPayer) {
            return;
        }

        $name = $this->removingPayer;

        // Remove from all clinician profiles
        User::query()
            ->whereNotNull('insurance_accepted')
            ->get()
            ->each(function (User $user) use ($name) {
                $accepted = json_decode($user->insurance_accepted ?? '[]', true);
                if (is_array($accepted) && in_array($name, $accepted, true)) {
                    $accepted = array_values(array_diff($accepted, [$name]));
                    $user->update(['insurance_accepted' => json_encode($accepted)]);
                }
            });

        // Remove from settings
        $this->payers = array_values(array_diff($this->payers, [$name]));
        Setting::set('insurance_payers', json_encode($this->payers));

        AuditLogService::log(
            action: 'setting.insurance_payer_removed',
            actor: auth()->user(),
            metadata: ['payer' => $name],
        );

        $this->removingPayer = null;
        $this->affectedClinicians = [];
    }

    public function saveChargesPrices(): void
    {
        $this->validate([
            'ipPrice' => ['required', 'numeric', 'min:0.01'],
            'oopPrice' => ['required', 'numeric', 'min:0.01'],
        ]);

        \App\Models\ServiceCatalog::query()
            ->where('is_session_based', true)
            ->where('title', 'like', '%IP%')
            ->update(['default_price' => $this->ipPrice]);

        \App\Models\ServiceCatalog::query()
            ->where('is_session_based', true)
            ->where('title', 'like', '%OOP%')
            ->update(['default_price' => $this->oopPrice]);

        AuditLogService::log(
            action: 'setting.session_charge_prices_updated',
            actor: auth()->user(),
            metadata: ['ip_price' => $this->ipPrice, 'oop_price' => $this->oopPrice],
        );

        $this->pricesSaved = true;
    }

    public function saveSurchargeRate(): void
    {
        $this->validate([
            'surchargeRate' => ['required', 'numeric', 'min:0', 'max:100'],
        ]);

        $decimal = round((float) $this->surchargeRate / 100, 4);
        Setting::set('card_surcharge_rate', (string) $decimal);

        AuditLogService::log(
            action: 'setting.card_surcharge_rate_updated',
            actor: auth()->user(),
            metadata: ['rate' => $decimal],
        );

        $this->surchargeSaved = true;
    }

    public function render(): View
    {
        return view('livewire.admin.admin-settings');
    }
}
```

- [ ] **Step 4: Update the Blade view**

Replace `resources/views/livewire/admin/admin-settings.blade.php` with:

```blade
<div class="max-w-2xl space-y-6">
    {{-- Payout Authorization Code --}}
    <div class="bg-white rounded-lg shadow overflow-hidden">
        <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
            <h2 class="text-base font-semibold text-navy">Payout Authorization Code</h2>
            <p class="text-xs text-gray-500 mt-1">Required to initiate disbursement payout batches. Stored securely (hashed).</p>
        </div>

        <div class="px-6 py-5 space-y-4">
            @if ($hasAuthCode)
                <div>
                    <label for="current_auth_code" class="block text-sm font-medium text-gray-700 mb-1">Current Code</label>
                    <input id="current_auth_code" type="password" wire:model="current_auth_code" maxlength="4"
                        class="w-full max-w-xs border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono tracking-widest focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold @error('current_auth_code') border-red-500 @enderror"
                        placeholder="****">
                    @error('current_auth_code')
                        <p class="mt-1 text-xs text-red-600">{{ $message }}</p>
                    @enderror
                </div>
            @endif

            <div>
                <label for="new_auth_code" class="block text-sm font-medium text-gray-700 mb-1">
                    {{ $hasAuthCode ? 'New Code' : 'Set Code' }}
                </label>
                <input id="new_auth_code" type="password" wire:model="new_auth_code" maxlength="4"
                    class="w-full max-w-xs border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono tracking-widest focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold @error('new_auth_code') border-red-500 @enderror"
                    placeholder="4 digits">
                @error('new_auth_code')
                    <p class="mt-1 text-xs text-red-600">{{ $message }}</p>
                @enderror
            </div>

            <div>
                <label for="confirm_auth_code" class="block text-sm font-medium text-gray-700 mb-1">Confirm Code</label>
                <input id="confirm_auth_code" type="password" wire:model="confirm_auth_code" maxlength="4"
                    class="w-full max-w-xs border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono tracking-widest focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold @error('confirm_auth_code') border-red-500 @enderror"
                    placeholder="****">
                @error('confirm_auth_code')
                    <p class="mt-1 text-xs text-red-600">{{ $message }}</p>
                @enderror
            </div>

            <div class="flex items-center gap-4 pt-2">
                <button wire:click="saveAuthCode" wire:loading.attr="disabled" class="px-4 py-2 text-sm font-medium text-white bg-gold rounded-lg hover:bg-gold-dark transition-colors">
                    <span wire:loading.remove wire:target="saveAuthCode">{{ $hasAuthCode ? 'Update Code' : 'Set Code' }}</span>
                    <span wire:loading wire:target="saveAuthCode">Saving...</span>
                </button>
                @if ($saved)
                    <span class="text-sm text-green-600 font-medium">Saved successfully.</span>
                @endif
            </div>
        </div>
    </div>

    {{-- Insurance Payers --}}
    <div class="bg-white rounded-lg shadow overflow-hidden">
        <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
            <h2 class="text-base font-semibold text-navy">Insurance Payers</h2>
            <p class="text-xs text-gray-500 mt-1">Master list of insurance payers. Clinicians select from this list for their profiles.</p>
        </div>

        <div class="px-6 py-5 space-y-4">
            {{-- Add payer --}}
            <div class="flex gap-2">
                <input type="text" wire:model="newPayerName" wire:keydown.enter="addPayer" placeholder="Payer name"
                    class="flex-1 max-w-sm border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold @error('newPayerName') border-red-500 @enderror">
                <button wire:click="addPayer" class="px-4 py-2 text-sm font-medium text-white bg-gold rounded-lg hover:bg-gold-dark transition-colors">Add</button>
            </div>
            @error('newPayerName')
                <p class="text-xs text-red-600">{{ $message }}</p>
            @enderror

            {{-- Payer list --}}
            @if (count($payers))
                <div class="flex flex-wrap gap-2">
                    @foreach ($payers as $payer)
                        <span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm bg-blue-50 text-blue-700 border border-blue-200">
                            {{ $payer }}
                            <button wire:click="confirmRemovePayer('{{ addslashes($payer) }}')" class="text-blue-400 hover:text-red-500 transition-colors" title="Remove">
                                <svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
                            </button>
                        </span>
                    @endforeach
                </div>
            @else
                <p class="text-sm text-gray-500">No insurance payers configured.</p>
            @endif

            {{-- Removal confirmation modal --}}
            @if ($removingPayer)
                <div class="rounded-lg border border-amber-200 bg-amber-50 p-4">
                    <p class="text-sm font-medium text-amber-800">Remove "{{ $removingPayer }}"?</p>
                    @if (count($affectedClinicians))
                        <p class="text-xs text-amber-700 mt-1">This payer will be removed from {{ count($affectedClinicians) }} clinician profile(s):</p>
                        <ul class="text-xs text-amber-700 mt-1 list-disc list-inside">
                            @foreach ($affectedClinicians as $name)
                                <li>{{ $name }}</li>
                            @endforeach
                        </ul>
                    @endif
                    <div class="flex gap-2 mt-3">
                        <button wire:click="executeRemovePayer" class="px-3 py-1.5 text-xs font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 transition-colors">Remove</button>
                        <button wire:click="cancelRemovePayer" class="px-3 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">Cancel</button>
                    </div>
                </div>
            @endif
        </div>
    </div>

    {{-- Session Charge Prices --}}
    <div class="bg-white rounded-lg shadow overflow-hidden">
        <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
            <h2 class="text-base font-semibold text-navy">Session Charge Prices</h2>
            <p class="text-xs text-gray-500 mt-1">Auto-generated charge amounts per session type. Changes apply to future imports only.</p>
        </div>

        <div class="px-6 py-5 space-y-4">
            <div class="grid grid-cols-2 gap-4 max-w-sm">
                <div>
                    <label for="ipPrice" class="block text-sm font-medium text-gray-700 mb-1">IP Session ($)</label>
                    <input id="ipPrice" type="number" step="0.01" min="0" wire:model="ipPrice"
                        class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold @error('ipPrice') border-red-500 @enderror">
                    @error('ipPrice')
                        <p class="mt-1 text-xs text-red-600">{{ $message }}</p>
                    @enderror
                </div>
                <div>
                    <label for="oopPrice" class="block text-sm font-medium text-gray-700 mb-1">OOP Session ($)</label>
                    <input id="oopPrice" type="number" step="0.01" min="0" wire:model="oopPrice"
                        class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold @error('oopPrice') border-red-500 @enderror">
                    @error('oopPrice')
                        <p class="mt-1 text-xs text-red-600">{{ $message }}</p>
                    @enderror
                </div>
            </div>
            <div class="flex items-center gap-4">
                <button wire:click="saveChargesPrices" class="px-4 py-2 text-sm font-medium text-white bg-gold rounded-lg hover:bg-gold-dark transition-colors">
                    <span wire:loading.remove wire:target="saveChargesPrices">Save Prices</span>
                    <span wire:loading wire:target="saveChargesPrices">Saving...</span>
                </button>
                @if ($pricesSaved)
                    <span class="text-sm text-green-600 font-medium">Prices updated.</span>
                @endif
            </div>
        </div>
    </div>

    {{-- Card Surcharge Rate --}}
    <div class="bg-white rounded-lg shadow overflow-hidden">
        <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
            <h2 class="text-base font-semibold text-navy">Card Surcharge Rate</h2>
            <p class="text-xs text-gray-500 mt-1">Percentage surcharge applied when clinician pays fees by card instead of ACH.</p>
        </div>

        <div class="px-6 py-5 space-y-4">
            <div class="flex items-center gap-2 max-w-xs">
                <input type="number" step="0.1" min="0" max="100" wire:model="surchargeRate"
                    class="w-24 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold @error('surchargeRate') border-red-500 @enderror">
                <span class="text-sm text-gray-500 font-medium">%</span>
            </div>
            @error('surchargeRate')
                <p class="text-xs text-red-600">{{ $message }}</p>
            @enderror
            <div class="flex items-center gap-4">
                <button wire:click="saveSurchargeRate" class="px-4 py-2 text-sm font-medium text-white bg-gold rounded-lg hover:bg-gold-dark transition-colors">
                    <span wire:loading.remove wire:target="saveSurchargeRate">Save Rate</span>
                    <span wire:loading wire:target="saveSurchargeRate">Saving...</span>
                </button>
                @if ($surchargeSaved)
                    <span class="text-sm text-green-600 font-medium">Rate updated.</span>
                @endif
            </div>
        </div>
    </div>
</div>
```

- [ ] **Step 5: Run tests to verify they pass**

Run: `php artisan test --compact --filter=AdminSettingsTest`
Expected: All tests pass.

- [ ] **Step 6: Run Pint and commit**

```bash
vendor/bin/pint --dirty --format agent
git add app/Livewire/Admin/AdminSettings.php resources/views/livewire/admin/admin-settings.blade.php tests/Feature/Admin/AdminSettingsTest.php
git commit -m "feat: complete admin settings with payers, charge prices, surcharge rate"
```

---

## Task 3: Admin Settings — Charge Prices & Surcharge Tests

**Files:**
- Modify: `tests/Feature/Admin/AdminSettingsTest.php`

- [ ] **Step 1: Add charge price and surcharge tests**

Append to `tests/Feature/Admin/AdminSettingsTest.php`:

```php
it('loads session charge prices from service catalog', function () {
    \App\Models\ServiceCatalog::query()->where('is_session_based', true)->delete();
    \App\Models\ServiceCatalog::factory()->sessionBased()->create([
        'title' => 'Billing and compliance services - IP',
        'default_price' => 15.00,
    ]);
    \App\Models\ServiceCatalog::factory()->sessionBased()->create([
        'title' => 'Billing and compliance services - OOP',
        'default_price' => 6.00,
    ]);

    $admin = User::factory()->admin()->create();

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->assertSet('ipPrice', '15.00')
        ->assertSet('oopPrice', '6.00');
});

it('updates session charge prices', function () {
    \App\Models\ServiceCatalog::query()->where('is_session_based', true)->delete();
    $ip = \App\Models\ServiceCatalog::factory()->sessionBased()->create([
        'title' => 'Billing and compliance services - IP',
        'default_price' => 15.00,
    ]);
    $oop = \App\Models\ServiceCatalog::factory()->sessionBased()->create([
        'title' => 'Billing and compliance services - OOP',
        'default_price' => 6.00,
    ]);

    $admin = User::factory()->admin()->create();

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->set('ipPrice', '18.00')
        ->set('oopPrice', '8.00')
        ->call('saveChargesPrices')
        ->assertSet('pricesSaved', true);

    expect($ip->fresh()->default_price)->toBe('18.00');
    expect($oop->fresh()->default_price)->toBe('8.00');
});

it('saves card surcharge rate as decimal', function () {
    $admin = User::factory()->admin()->create();

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->set('surchargeRate', '4.5')
        ->call('saveSurchargeRate')
        ->assertSet('surchargeSaved', true);

    expect(Setting::get('card_surcharge_rate'))->toBe('0.045');
});

it('loads existing surcharge rate as percentage', function () {
    Setting::set('card_surcharge_rate', '0.035');
    $admin = User::factory()->admin()->create();

    Livewire::actingAs($admin)
        ->test(AdminSettings::class)
        ->assertSet('surchargeRate', '3.5');
});
```

- [ ] **Step 2: Run all AdminSettings tests**

Run: `php artisan test --compact --filter=AdminSettingsTest`
Expected: All tests pass (implementation was done in Task 2).

- [ ] **Step 3: Commit**

```bash
vendor/bin/pint --dirty --format agent
git add tests/Feature/Admin/AdminSettingsTest.php
git commit -m "test: add charge price and surcharge rate tests for admin settings"
```

---

## Task 4: Import Acceptance Settings Page

**Files:**
- Create: `app/Livewire/Admin/ImportAcceptanceSettings.php`
- Create: `resources/views/livewire/admin/import-acceptance-settings.blade.php`
- Create: `resources/views/admin/import-acceptance-settings.blade.php`
- Modify: `routes/web.php:82-88`
- Modify: `app/View/Components/Sidebar.php:38`
- Create: `tests/Feature/Admin/ImportAcceptanceSettingsTest.php`

- [ ] **Step 1: Write the failing test**

Create `tests/Feature/Admin/ImportAcceptanceSettingsTest.php`:

```php
<?php

use App\Livewire\Admin\ImportAcceptanceSettings;
use App\Models\ImportFieldMapping;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

it('renders the import acceptance settings page', function () {
    $admin = User::factory()->admin()->create();

    $this->actingAs($admin)
        ->get(route('admin.import-acceptance-settings'))
        ->assertStatus(200)
        ->assertSeeLivewire(ImportAcceptanceSettings::class);
});

it('displays field mappings as reference rows', function () {
    $admin = User::factory()->admin()->create();

    ImportFieldMapping::create([
        'hub_field_name' => 'date_of_service',
        'hub_abbreviated_name' => 'DOS',
        'sp_import_header' => 'Date of Service',
        'example_value' => '2026-03-01 14:00:00',
        'sp_column_position' => 1,
        'import_action' => 'none',
        'import_action_detail' => null,
    ]);

    Livewire::actingAs($admin)
        ->test(ImportAcceptanceSettings::class)
        ->assertSee('Date of Service')
        ->assertSee('date_of_service');
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test --compact --filter=ImportAcceptanceSettingsTest`
Expected: Route not found / component not found.

- [ ] **Step 3: Create the Livewire component**

Create `app/Livewire/Admin/ImportAcceptanceSettings.php`:

```php
<?php

namespace App\Livewire\Admin;

use App\Models\ImportFieldMapping;
use Illuminate\View\View;
use Livewire\Component;

class ImportAcceptanceSettings extends Component
{
    public function render(): View
    {
        $mappings = ImportFieldMapping::query()
            ->orderBy('sp_column_position')
            ->get();

        return view('livewire.admin.import-acceptance-settings', [
            'mappings' => $mappings,
        ]);
    }
}
```

- [ ] **Step 4: Create the Blade views**

Create `resources/views/admin/import-acceptance-settings.blade.php`:

```blade
<x-layouts.app title="Import Acceptance Settings">
    <div class="space-y-6">
        <h1 class="text-2xl font-bold text-navy">Import Acceptance Settings</h1>
        <livewire:admin.import-acceptance-settings />
    </div>
</x-layouts.app>
```

Create `resources/views/livewire/admin/import-acceptance-settings.blade.php`:

```blade
<div>
    <p class="text-sm text-gray-500 mb-4">Reference table documenting the import validation rules. These rules are defined in code and cannot be edited here.</p>

    <div class="bg-white rounded-lg shadow overflow-x-auto">
        <table class="w-full text-sm text-left">
            <thead class="bg-gray-50 border-b border-gray-200">
                <tr>
                    <th class="px-4 py-3 font-semibold text-navy text-xs uppercase tracking-wider">Field Name</th>
                    <th class="px-4 py-3 font-semibold text-navy text-xs uppercase tracking-wider">SP Header</th>
                    <th class="px-4 py-3 font-semibold text-navy text-xs uppercase tracking-wider">Example</th>
                    <th class="px-4 py-3 font-semibold text-navy text-xs uppercase tracking-wider">Import Action</th>
                    <th class="px-4 py-3 font-semibold text-navy text-xs uppercase tracking-wider">Action Detail</th>
                </tr>
            </thead>
            <tbody class="divide-y divide-gray-100">
                @forelse ($mappings as $mapping)
                    <tr class="hover:bg-gray-50/50">
                        <td class="px-4 py-3 font-mono text-xs text-gray-700">{{ $mapping->hub_field_name }}</td>
                        <td class="px-4 py-3 text-gray-900">{{ $mapping->sp_import_header }}</td>
                        <td class="px-4 py-3 text-gray-500 text-xs">{{ $mapping->example_value ?? '—' }}</td>
                        <td class="px-4 py-3">
                            <span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium
                                @switch($mapping->import_action->badgeColor())
                                    @case('amber') bg-amber-100 text-amber-700 @break
                                    @case('red') bg-red-100 text-red-700 @break
                                    @case('green') bg-green-100 text-green-700 @break
                                    @case('blue') bg-blue-100 text-blue-700 @break
                                    @default bg-gray-100 text-gray-700
                                @endswitch
                            ">{{ $mapping->import_action->label() }}</span>
                        </td>
                        <td class="px-4 py-3 text-xs text-gray-500">{{ $mapping->import_action_detail ?? '—' }}</td>
                    </tr>
                @empty
                    <tr>
                        <td colspan="5" class="px-4 py-8 text-center text-gray-400">No field mappings configured.</td>
                    </tr>
                @endforelse
            </tbody>
        </table>
    </div>
</div>
```

- [ ] **Step 5: Add route and sidebar entry**

Add to `routes/web.php` inside the admin group (after line 84):

```php
Route::get('/import-acceptance-settings', fn () => view('admin.import-acceptance-settings'))->name('import-acceptance-settings');
```

Add to `app/View/Components/Sidebar.php` in the admin nav items array, after the 'Import Mappings' entry (line 38):

```php
['label' => 'Import Rules', 'route' => 'admin.import-acceptance-settings', 'icon' => 'clipboard-document-list'],
```

- [ ] **Step 6: Run tests**

Run: `php artisan test --compact --filter=ImportAcceptanceSettingsTest`
Expected: All tests pass.

- [ ] **Step 7: Run Pint and commit**

```bash
vendor/bin/pint --dirty --format agent
git add app/Livewire/Admin/ImportAcceptanceSettings.php resources/views/livewire/admin/import-acceptance-settings.blade.php resources/views/admin/import-acceptance-settings.blade.php routes/web.php app/View/Components/Sidebar.php tests/Feature/Admin/ImportAcceptanceSettingsTest.php
git commit -m "feat: add import acceptance settings reference page"
```

---

## Task 5: Clinician Support Services Page

**Files:**
- Create: `app/Livewire/Admin/ClinicianSupportServices.php`
- Create: `resources/views/livewire/admin/clinician-support-services.blade.php`
- Create: `resources/views/admin/clinician-support-services.blade.php`
- Modify: `routes/web.php`
- Create: `tests/Feature/Admin/ClinicianSupportServicesTest.php`

- [ ] **Step 1: Write the failing tests**

Create `tests/Feature/Admin/ClinicianSupportServicesTest.php`:

```php
<?php

use App\Enums\ChargeStatus;
use App\Enums\ServiceType;
use App\Livewire\Admin\ClinicianSupportServices;
use App\Models\Charge;
use App\Models\Clinician;
use App\Models\ClinicianService;
use App\Models\ServiceCatalog;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

it('renders the clinician support services page', function () {
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();

    $this->actingAs($admin)
        ->get(route('admin.clinician-support-services', $clinician))
        ->assertStatus(200)
        ->assertSeeLivewire(ClinicianSupportServices::class);
});

it('shows assigned services for a clinician', function () {
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->recurring()->create(['title' => 'Email account', 'default_price' => 15.00]);
    ClinicianService::factory()->create(['clinician_id' => $clinician->id, 'service_catalog_id' => $service->id]);

    Livewire::actingAs($admin)
        ->test(ClinicianSupportServices::class, ['clinician' => $clinician])
        ->assertSee('Email account');
});

it('activates a service for a clinician', function () {
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->recurring()->create(['title' => 'Email account']);

    Livewire::actingAs($admin)
        ->test(ClinicianSupportServices::class, ['clinician' => $clinician])
        ->set('selectedServiceId', $service->id)
        ->call('activateService');

    expect(ClinicianService::where('clinician_id', $clinician->id)->where('service_catalog_id', $service->id)->exists())->toBeTrue();
});

it('deactivates an active service', function () {
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->recurring()->create();
    $cs = ClinicianService::factory()->create(['clinician_id' => $clinician->id, 'service_catalog_id' => $service->id]);

    Livewire::actingAs($admin)
        ->test(ClinicianSupportServices::class, ['clinician' => $clinician])
        ->call('deactivateService', $cs->id);

    expect($cs->fresh()->is_active)->toBeFalse();
    expect($cs->fresh()->deactivated_at)->not->toBeNull();
});

it('creates a manual one-time charge', function () {
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->create(['type' => ServiceType::OneTime, 'default_price' => 25.00]);

    Livewire::actingAs($admin)
        ->test(ClinicianSupportServices::class, ['clinician' => $clinician])
        ->set('chargeServiceId', $service->id)
        ->set('chargeAmount', '25.00')
        ->set('chargeNotes', 'Test charge')
        ->call('createCharge');

    expect(Charge::where('clinician_id', $clinician->id)->where('amount', 25.00)->exists())->toBeTrue();
});

it('shows pending and settled charges', function () {
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->create();

    Charge::factory()->create(['clinician_id' => $clinician->id, 'service_catalog_id' => $service->id, 'status' => ChargeStatus::Pending, 'amount' => 10.00]);
    Charge::factory()->settled()->create(['clinician_id' => $clinician->id, 'service_catalog_id' => $service->id, 'amount' => 20.00]);

    Livewire::actingAs($admin)
        ->test(ClinicianSupportServices::class, ['clinician' => $clinician])
        ->assertSee('10.00')
        ->assertSee('20.00');
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `php artisan test --compact --filter=ClinicianSupportServicesTest`
Expected: Route/component not found.

- [ ] **Step 3: Create the Livewire component**

Create `app/Livewire/Admin/ClinicianSupportServices.php`:

```php
<?php

namespace App\Livewire\Admin;

use App\Models\Charge;
use App\Models\Clinician;
use App\Models\ClinicianService;
use App\Models\ServiceCatalog;
use App\Services\AuditLogService;
use App\Services\ChargeService;
use Illuminate\View\View;
use Livewire\Component;

class ClinicianSupportServices extends Component
{
    public Clinician $clinician;

    public string $selectedServiceId = '';

    public string $chargeServiceId = '';

    public string $chargeAmount = '';

    public string $chargeNotes = '';

    public function mount(Clinician $clinician): void
    {
        $this->clinician = $clinician;
    }

    public function activateService(): void
    {
        $this->validate([
            'selectedServiceId' => ['required', 'exists:service_catalog,id'],
        ]);

        $exists = ClinicianService::query()
            ->where('clinician_id', $this->clinician->id)
            ->where('service_catalog_id', $this->selectedServiceId)
            ->where('is_active', true)
            ->exists();

        if ($exists) {
            $this->addError('selectedServiceId', 'This service is already active.');

            return;
        }

        ClinicianService::query()->create([
            'clinician_id' => $this->clinician->id,
            'service_catalog_id' => $this->selectedServiceId,
            'is_active' => true,
            'activated_at' => now(),
        ]);

        AuditLogService::log(
            action: 'clinician_service.activated',
            actor: auth()->user(),
            targetType: 'Clinician',
            targetId: $this->clinician->id,
            targetLabel: $this->clinician->display_name,
            metadata: ['service_catalog_id' => $this->selectedServiceId],
        );

        $this->selectedServiceId = '';
    }

    public function deactivateService(int $clinicianServiceId): void
    {
        $cs = ClinicianService::query()
            ->where('id', $clinicianServiceId)
            ->where('clinician_id', $this->clinician->id)
            ->firstOrFail();

        $cs->update([
            'is_active' => false,
            'deactivated_at' => now(),
        ]);

        AuditLogService::log(
            action: 'clinician_service.deactivated',
            actor: auth()->user(),
            targetType: 'Clinician',
            targetId: $this->clinician->id,
            targetLabel: $this->clinician->display_name,
            metadata: ['clinician_service_id' => $clinicianServiceId],
        );
    }

    public function createCharge(): void
    {
        $this->validate([
            'chargeServiceId' => ['required', 'exists:service_catalog,id'],
            'chargeAmount' => ['required', 'numeric'],
            'chargeNotes' => ['nullable', 'string', 'max:500'],
        ]);

        $service = ServiceCatalog::query()->findOrFail($this->chargeServiceId);

        ChargeService::createManualCharge(
            clinician: $this->clinician,
            service: $service,
            amount: (float) $this->chargeAmount,
            notes: $this->chargeNotes ?: null,
            creator: auth()->user(),
        );

        $this->reset(['chargeServiceId', 'chargeAmount', 'chargeNotes']);
    }

    public function updatedChargeServiceId(string $value): void
    {
        if ($value) {
            $service = ServiceCatalog::find($value);
            $this->chargeAmount = $service?->default_price ? (string) $service->default_price : '';
        }
    }

    public function render(): View
    {
        $assignedServices = ClinicianService::query()
            ->where('clinician_id', $this->clinician->id)
            ->with('serviceCatalog')
            ->orderByDesc('is_active')
            ->orderBy('activated_at')
            ->get();

        $availableRecurring = ServiceCatalog::query()
            ->where('type', 'recurring')
            ->active()
            ->whereNotIn('id', $assignedServices->where('is_active', true)->pluck('service_catalog_id'))
            ->orderBy('title')
            ->get();

        $availableOneTime = ServiceCatalog::query()
            ->manual()
            ->active()
            ->orderBy('title')
            ->get();

        $pendingCharges = Charge::query()
            ->where('clinician_id', $this->clinician->id)
            ->pending()
            ->with('serviceCatalog')
            ->orderByDesc('created_at')
            ->get();

        $settledCharges = Charge::query()
            ->where('clinician_id', $this->clinician->id)
            ->where(fn ($q) => $q->settled()->orWhere('status', 'waived'))
            ->with('serviceCatalog')
            ->orderByDesc('settlement_date')
            ->get();

        return view('livewire.admin.clinician-support-services', [
            'assignedServices' => $assignedServices,
            'availableRecurring' => $availableRecurring,
            'availableOneTime' => $availableOneTime,
            'pendingCharges' => $pendingCharges,
            'settledCharges' => $settledCharges,
        ]);
    }
}
```

- [ ] **Step 4: Create the Blade views**

Create `resources/views/admin/clinician-support-services.blade.php`:

```blade
<x-layouts.app title="Support Services — {{ $clinician->display_name }}">
    <div class="space-y-6">
        <div class="flex items-center gap-3">
            <a href="{{ route('admin.clinician-master') }}" class="text-gray-400 hover:text-navy transition-colors">
                <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /></svg>
            </a>
            <h1 class="text-2xl font-bold text-navy">{{ $clinician->display_name }} — Support Services</h1>
        </div>
        <livewire:admin.clinician-support-services :clinician="$clinician" />
    </div>
</x-layouts.app>
```

Create `resources/views/livewire/admin/clinician-support-services.blade.php`:

```blade
<div class="space-y-6">
    {{-- Section A: Monthly / Ongoing Services --}}
    <div class="bg-white rounded-lg shadow overflow-hidden">
        <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
            <h2 class="text-base font-semibold text-navy">Monthly / Ongoing Services</h2>
        </div>
        <div class="px-6 py-5 space-y-4">
            {{-- Add service --}}
            <div class="flex gap-2">
                <select wire:model="selectedServiceId" class="flex-1 max-w-sm border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold">
                    <option value="">Select a service...</option>
                    @foreach ($availableRecurring as $service)
                        <option value="{{ $service->id }}">{{ $service->title }} {{ $service->default_price ? '($' . number_format($service->default_price, 2) . '/mo)' : '' }}</option>
                    @endforeach
                </select>
                <button wire:click="activateService" class="px-4 py-2 text-sm font-medium text-white bg-gold rounded-lg hover:bg-gold-dark transition-colors" @if(!$selectedServiceId) disabled @endif>Add</button>
            </div>
            @error('selectedServiceId')
                <p class="text-xs text-red-600">{{ $message }}</p>
            @enderror

            {{-- Services table --}}
            @if ($assignedServices->count())
                <table class="w-full text-sm text-left">
                    <thead class="border-b border-gray-200">
                        <tr>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Service</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Price</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Since</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Status</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase"></th>
                        </tr>
                    </thead>
                    <tbody class="divide-y divide-gray-100">
                        @foreach ($assignedServices as $cs)
                            <tr>
                                <td class="py-3 text-gray-900">{{ $cs->serviceCatalog->title }}</td>
                                <td class="py-3 text-gray-600">${{ number_format($cs->serviceCatalog->default_price ?? 0, 2) }}/mo</td>
                                <td class="py-3 text-gray-500 text-xs">{{ $cs->activated_at?->format('m/d/Y') }}</td>
                                <td class="py-3">
                                    @if ($cs->is_active)
                                        <span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">Active</span>
                                    @else
                                        <span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">Inactive</span>
                                    @endif
                                </td>
                                <td class="py-3 text-right">
                                    @if ($cs->is_active)
                                        <button wire:click="deactivateService({{ $cs->id }})" wire:confirm="Deactivate this service?" class="text-xs text-red-600 hover:text-red-800">Deactivate</button>
                                    @endif
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            @else
                <p class="text-sm text-gray-400">No services assigned.</p>
            @endif
        </div>
    </div>

    {{-- Section B: Pending Charges --}}
    <div class="bg-white rounded-lg shadow overflow-hidden">
        <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
            <h2 class="text-base font-semibold text-navy">Pending Charges</h2>
        </div>
        <div class="px-6 py-5 space-y-4">
            {{-- Add charge form --}}
            <div class="flex flex-wrap gap-2 items-end">
                <div>
                    <label class="block text-xs font-medium text-gray-500 mb-1">Service</label>
                    <select wire:model.live="chargeServiceId" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold">
                        <option value="">Select...</option>
                        @foreach ($availableOneTime as $service)
                            <option value="{{ $service->id }}">{{ $service->title }}</option>
                        @endforeach
                    </select>
                </div>
                <div>
                    <label class="block text-xs font-medium text-gray-500 mb-1">Amount ($)</label>
                    <input type="number" step="0.01" wire:model="chargeAmount" class="w-28 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold" placeholder="0.00">
                </div>
                <div>
                    <label class="block text-xs font-medium text-gray-500 mb-1">Notes</label>
                    <input type="text" wire:model="chargeNotes" class="w-48 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gold/50 focus:border-gold" placeholder="Optional">
                </div>
                <button wire:click="createCharge" class="px-4 py-2 text-sm font-medium text-white bg-gold rounded-lg hover:bg-gold-dark transition-colors">Add Charge</button>
            </div>
            @error('chargeAmount')
                <p class="text-xs text-red-600">{{ $message }}</p>
            @enderror

            {{-- Pending charges table --}}
            @if ($pendingCharges->count())
                <table class="w-full text-sm text-left">
                    <thead class="border-b border-gray-200">
                        <tr>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Date</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Description</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase text-right">Amount</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Status</th>
                        </tr>
                    </thead>
                    <tbody class="divide-y divide-gray-100">
                        @foreach ($pendingCharges as $charge)
                            <tr>
                                <td class="py-3 text-gray-500 text-xs">{{ $charge->created_at->format('m/d/Y') }}</td>
                                <td class="py-3 text-gray-900">{{ $charge->serviceCatalog?->title ?? 'Manual charge' }}{{ $charge->notes ? " — {$charge->notes}" : '' }}</td>
                                <td class="py-3 text-right {{ (float) $charge->amount < 0 ? 'text-green-600' : 'text-gray-900' }}">${{ number_format(abs($charge->amount), 2) }}{{ (float) $charge->amount < 0 ? ' CR' : '' }}</td>
                                <td class="py-3"><span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-700">Pending</span></td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            @else
                <p class="text-sm text-gray-400">No pending charges.</p>
            @endif
        </div>
    </div>

    {{-- Section C: Paid / Processed Charges --}}
    <div class="bg-white rounded-lg shadow overflow-hidden">
        <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
            <h2 class="text-base font-semibold text-navy">Paid / Processed Charges</h2>
        </div>
        <div class="px-6 py-5">
            @if ($settledCharges->count())
                <table class="w-full text-sm text-left">
                    <thead class="border-b border-gray-200">
                        <tr>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Date</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Description</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase text-right">Amount</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Settlement ID</th>
                            <th class="pb-2 font-medium text-gray-500 text-xs uppercase">Settled</th>
                        </tr>
                    </thead>
                    <tbody class="divide-y divide-gray-100">
                        @foreach ($settledCharges as $charge)
                            <tr>
                                <td class="py-3 text-gray-500 text-xs">{{ $charge->created_at->format('m/d/Y') }}</td>
                                <td class="py-3 text-gray-900">{{ $charge->serviceCatalog?->title ?? 'Manual charge' }}</td>
                                <td class="py-3 text-right text-gray-900">${{ number_format($charge->amount, 2) }}</td>
                                <td class="py-3 text-xs font-mono text-gray-500">{{ $charge->settlement_id ?? '—' }}</td>
                                <td class="py-3 text-xs text-gray-500">{{ $charge->settlement_date?->format('m/d/Y') ?? '—' }}</td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            @else
                <p class="text-sm text-gray-400">No settled charges.</p>
            @endif
        </div>
    </div>
</div>
```

- [ ] **Step 5: Add route**

Add to `routes/web.php` inside the admin group (after the import-acceptance-settings route):

```php
Route::get('/clinicians/{clinician}/support-services', function (\App\Models\Clinician $clinician) {
    return view('admin.clinician-support-services', ['clinician' => $clinician]);
})->name('clinician-support-services');
```

- [ ] **Step 6: Run tests**

Run: `php artisan test --compact --filter=ClinicianSupportServicesTest`
Expected: All tests pass.

- [ ] **Step 7: Run Pint and commit**

```bash
vendor/bin/pint --dirty --format agent
git add app/Livewire/Admin/ClinicianSupportServices.php resources/views/livewire/admin/clinician-support-services.blade.php resources/views/admin/clinician-support-services.blade.php routes/web.php tests/Feature/Admin/ClinicianSupportServicesTest.php
git commit -m "feat: add clinician support services page with service management and charges"
```

---

## Task 6: Monthly Charge Auto-Generation Command

**Files:**
- Create: `app/Console/Commands/GenerateMonthlyCharges.php`
- Modify: `routes/console.php`
- Create: `tests/Feature/Commands/GenerateMonthlyChargesTest.php`

- [ ] **Step 1: Write the failing tests**

Create `tests/Feature/Commands/GenerateMonthlyChargesTest.php`:

```php
<?php

use App\Enums\ChargeStatus;
use App\Models\Charge;
use App\Models\Clinician;
use App\Models\ClinicianService;
use App\Models\ServiceCatalog;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

it('generates charges for active clinician services', function () {
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->recurring()->create(['default_price' => 15.00]);
    ClinicianService::factory()->create([
        'clinician_id' => $clinician->id,
        'service_catalog_id' => $service->id,
        'is_active' => true,
        'activated_at' => now()->subMonth(),
    ]);

    $this->artisan('app:generate-monthly-charges')->assertSuccessful();

    expect(Charge::where('clinician_id', $clinician->id)->where('service_catalog_id', $service->id)->count())->toBe(1);
    $charge = Charge::where('clinician_id', $clinician->id)->first();
    expect((float) $charge->amount)->toBe(15.00);
    expect($charge->status)->toBe(ChargeStatus::Pending);
});

it('prorates charge for service activated in current month', function () {
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->recurring()->create(['default_price' => 30.00]);

    $activatedAt = now()->startOfMonth()->addDays(14); // midway through the month
    ClinicianService::factory()->create([
        'clinician_id' => $clinician->id,
        'service_catalog_id' => $service->id,
        'is_active' => true,
        'activated_at' => $activatedAt,
    ]);

    $this->artisan('app:generate-monthly-charges')->assertSuccessful();

    $charge = Charge::where('clinician_id', $clinician->id)->first();
    $daysInMonth = now()->daysInMonth;
    $daysRemaining = $daysInMonth - 14;
    $expected = round(30.00 * $daysRemaining / $daysInMonth, 2);
    expect((float) $charge->amount)->toBe($expected);
});

it('does not generate duplicate charges in the same month', function () {
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->recurring()->create(['default_price' => 15.00]);
    ClinicianService::factory()->create([
        'clinician_id' => $clinician->id,
        'service_catalog_id' => $service->id,
        'is_active' => true,
        'activated_at' => now()->subMonth(),
    ]);

    $this->artisan('app:generate-monthly-charges')->assertSuccessful();
    $this->artisan('app:generate-monthly-charges')->assertSuccessful();

    expect(Charge::where('clinician_id', $clinician->id)->count())->toBe(1);
});

it('skips inactive services', function () {
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->recurring()->create(['default_price' => 15.00]);
    ClinicianService::factory()->inactive()->create([
        'clinician_id' => $clinician->id,
        'service_catalog_id' => $service->id,
    ]);

    $this->artisan('app:generate-monthly-charges')->assertSuccessful();

    expect(Charge::where('clinician_id', $clinician->id)->count())->toBe(0);
});

it('skips services with null default price', function () {
    $clinician = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->recurring()->create(['default_price' => null]);
    ClinicianService::factory()->create([
        'clinician_id' => $clinician->id,
        'service_catalog_id' => $service->id,
        'is_active' => true,
        'activated_at' => now()->subMonth(),
    ]);

    $this->artisan('app:generate-monthly-charges')->assertSuccessful();

    expect(Charge::where('clinician_id', $clinician->id)->count())->toBe(0);
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `php artisan test --compact --filter=GenerateMonthlyChargesTest`
Expected: Command not found.

- [ ] **Step 3: Create the Artisan command**

Create `app/Console/Commands/GenerateMonthlyCharges.php`:

```php
<?php

namespace App\Console\Commands;

use App\Enums\ChargeStatus;
use App\Models\Charge;
use App\Models\ClinicianService;
use App\Services\AuditLogService;
use Illuminate\Console\Command;

class GenerateMonthlyCharges extends Command
{
    protected $signature = 'app:generate-monthly-charges';

    protected $description = 'Generate monthly charges for active clinician services';

    public function handle(): int
    {
        $month = now()->format('Y-m');
        $generated = 0;
        $skipped = 0;

        $activeServices = ClinicianService::query()
            ->active()
            ->with('serviceCatalog')
            ->get();

        foreach ($activeServices as $cs) {
            if ($cs->serviceCatalog->default_price === null) {
                $skipped++;

                continue;
            }

            $alreadyCharged = Charge::query()
                ->where('clinician_id', $cs->clinician_id)
                ->where('service_catalog_id', $cs->service_catalog_id)
                ->whereRaw("DATE_FORMAT(created_at, '%Y-%m') = ?", [$month])
                ->exists();

            if ($alreadyCharged) {
                $skipped++;

                continue;
            }

            $amount = $this->calculateAmount($cs);

            Charge::query()->create([
                'clinician_id' => $cs->clinician_id,
                'service_catalog_id' => $cs->service_catalog_id,
                'amount' => $amount,
                'status' => ChargeStatus::Pending,
                'notes' => "Monthly charge for {$month}",
            ]);

            $generated++;
        }

        AuditLogService::log(
            action: 'monthly_charges.generated',
            metadata: ['month' => $month, 'generated' => $generated, 'skipped' => $skipped],
        );

        $this->info("Generated {$generated} charges, skipped {$skipped}.");

        return self::SUCCESS;
    }

    private function calculateAmount(ClinicianService $cs): float
    {
        $price = (float) $cs->serviceCatalog->default_price;
        $activatedAt = $cs->activated_at;

        if ($activatedAt && $activatedAt->format('Y-m') === now()->format('Y-m')) {
            $daysInMonth = now()->daysInMonth;
            $dayOfActivation = $activatedAt->day;
            $daysRemaining = $daysInMonth - $dayOfActivation;

            return round($price * $daysRemaining / $daysInMonth, 2);
        }

        return $price;
    }
}
```

- [ ] **Step 4: Register the schedule**

Replace `routes/console.php`:

```php
<?php

use Illuminate\Support\Facades\Schedule;

Schedule::command('app:generate-monthly-charges')
    ->monthlyOn(1, '00:00')
    ->timezone('America/Los_Angeles');
```

- [ ] **Step 5: Run tests**

Run: `php artisan test --compact --filter=GenerateMonthlyChargesTest`
Expected: All tests pass.

- [ ] **Step 6: Run Pint and commit**

```bash
vendor/bin/pint --dirty --format agent
git add app/Console/Commands/GenerateMonthlyCharges.php routes/console.php tests/Feature/Commands/GenerateMonthlyChargesTest.php
git commit -m "feat: add monthly charge auto-generation command with proration"
```

---

## Task 7: Migration — payment_method_type + Disbursement status

**Files:**
- Create: `database/migrations/2026_04_04_000001_add_payment_method_type_to_clinicians.php`
- Create: `database/migrations/2026_04_04_000002_add_status_and_error_to_disbursements.php`
- Modify: `app/Models/Clinician.php`
- Modify: `app/Models/Disbursement.php`

- [ ] **Step 1: Create migrations**

Run:
```bash
php artisan make:migration add_payment_method_type_to_clinicians_table --table=clinicians --no-interaction
php artisan make:migration add_status_and_error_to_disbursements_table --table=disbursements --no-interaction
```

- [ ] **Step 2: Write the clinicians migration**

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('clinicians', function (Blueprint $table) {
            $table->string('payment_method_type', 10)->default('ach')->after('hide_from_roster');
        });
    }

    public function down(): void
    {
        Schema::table('clinicians', function (Blueprint $table) {
            $table->dropColumn('payment_method_type');
        });
    }
};
```

- [ ] **Step 3: Write the disbursements migration**

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('disbursements', function (Blueprint $table) {
            $table->string('status', 20)->default('queued')->after('newest_dos');
            $table->text('error_message')->nullable()->after('status');
        });
    }

    public function down(): void
    {
        Schema::table('disbursements', function (Blueprint $table) {
            $table->dropColumn(['status', 'error_message']);
        });
    }
};
```

- [ ] **Step 4: Update Clinician model fillable + casts**

Add `'payment_method_type'` to `$fillable` in `app/Models/Clinician.php` (line 14-20).

- [ ] **Step 5: Update Disbursement model fillable + casts**

Add `'status'` and `'error_message'` to `$fillable` in `app/Models/Disbursement.php` (line 13-24).

- [ ] **Step 6: Run migrations**

```bash
php artisan migrate
```

- [ ] **Step 7: Run Pint and commit**

```bash
vendor/bin/pint --dirty --format agent
git add database/migrations/2026_04_04_* app/Models/Clinician.php app/Models/Disbursement.php
git commit -m "feat: add payment_method_type to clinicians and status to disbursements"
```

---

## Task 8: Queue Jobs — ProcessDisbursementTransfer + ProcessFeeCharge

**Files:**
- Create: `app/Jobs/ProcessDisbursementTransfer.php`
- Create: `app/Jobs/ProcessFeeCharge.php`
- Create: `tests/Feature/Jobs/ProcessDisbursementTransferTest.php`
- Create: `tests/Feature/Jobs/ProcessFeeChargeTest.php`

- [ ] **Step 1: Write the failing tests**

Create `tests/Feature/Jobs/ProcessDisbursementTransferTest.php`:

```php
<?php

use App\Jobs\ProcessDisbursementTransfer;
use App\Models\Batch;
use App\Models\Clinician;
use App\Models\Disbursement;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

it('marks disbursement as complete on success', function () {
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();
    $batch = Batch::factory()->create(['processed_by' => $admin->id]);
    $disbursement = Disbursement::factory()->create([
        'batch_id' => $batch->id,
        'clinician_id' => $clinician->id,
        'status' => 'queued',
    ]);

    (new ProcessDisbursementTransfer($disbursement))->handle();

    expect($disbursement->fresh()->status)->toBe('complete');
    expect($disbursement->fresh()->stripe_transfer_id)->toStartWith('stub_');
});

it('marks disbursement as failed on exception', function () {
    $admin = User::factory()->admin()->create();
    $clinician = Clinician::factory()->create();
    $batch = Batch::factory()->create(['processed_by' => $admin->id]);
    $disbursement = Disbursement::factory()->create([
        'batch_id' => $batch->id,
        'clinician_id' => $clinician->id,
        'status' => 'queued',
    ]);

    $job = new ProcessDisbursementTransfer($disbursement);
    $job->failed(new \RuntimeException('Stripe API error'));

    expect($disbursement->fresh()->status)->toBe('failed');
    expect($disbursement->fresh()->error_message)->toBe('Stripe API error');
});
```

Create `tests/Feature/Jobs/ProcessFeeChargeTest.php`:

```php
<?php

use App\Enums\ChargeStatus;
use App\Jobs\ProcessFeeCharge;
use App\Models\Charge;
use App\Models\Clinician;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

it('settles charges with settlement id', function () {
    $clinician = Clinician::factory()->create();
    $charge1 = Charge::factory()->create(['clinician_id' => $clinician->id, 'status' => ChargeStatus::Pending]);
    $charge2 = Charge::factory()->create(['clinician_id' => $clinician->id, 'status' => ChargeStatus::Pending]);

    $settlementId = 'stl_' . $clinician->clinician_code . '-' . now()->format('ymd') . '-01';
    (new ProcessFeeCharge($clinician, [$charge1->id, $charge2->id], $settlementId))->handle();

    expect($charge1->fresh()->status)->toBe(ChargeStatus::Settled);
    expect($charge1->fresh()->settlement_id)->toBe($settlementId);
    expect($charge2->fresh()->status)->toBe(ChargeStatus::Settled);
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `php artisan test --compact --filter="ProcessDisbursementTransferTest|ProcessFeeChargeTest"`
Expected: Class not found.

- [ ] **Step 3: Create ProcessDisbursementTransfer job**

Create `app/Jobs/ProcessDisbursementTransfer.php`:

```php
<?php

namespace App\Jobs;

use App\Models\Disbursement;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessDisbursementTransfer implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;

    public int $backoff = 60;

    public function __construct(public Disbursement $disbursement) {}

    public function handle(): void
    {
        $this->disbursement->update(['status' => 'processing']);

        // TODO Phase 4: Replace with actual Stripe Transfer
        // Stripe\Transfer::create([
        //     'amount' => (int) ($this->disbursement->net_amount * 100),
        //     'currency' => 'usd',
        //     'destination' => $clinician->stripe_connect_id,
        // ]);

        $this->disbursement->update([
            'status' => 'complete',
            'stripe_transfer_id' => 'stub_' . uniqid(),
        ]);
    }

    public function failed(\Throwable $exception): void
    {
        $this->disbursement->update([
            'status' => 'failed',
            'error_message' => $exception->getMessage(),
        ]);
    }
}
```

- [ ] **Step 4: Create ProcessFeeCharge job**

Create `app/Jobs/ProcessFeeCharge.php`:

```php
<?php

namespace App\Jobs;

use App\Enums\ChargeStatus;
use App\Models\Charge;
use App\Models\Clinician;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessFeeCharge implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;

    public int $backoff = 60;

    /**
     * @param  int[]  $chargeIds
     */
    public function __construct(
        public Clinician $clinician,
        public array $chargeIds,
        public string $settlementId,
    ) {}

    public function handle(): void
    {
        // TODO Phase 4: Replace with actual Stripe PaymentIntent
        // Stripe\PaymentIntent::create([
        //     'amount' => $totalInCents,
        //     'currency' => 'usd',
        //     'customer' => $this->clinician->stripe_customer_id,
        //     'payment_method' => $defaultPaymentMethod,
        //     'confirm' => true,
        // ]);

        Charge::query()->whereIn('id', $this->chargeIds)->update([
            'status' => ChargeStatus::Settled,
            'settlement_id' => $this->settlementId,
            'settlement_date' => now()->toDateString(),
        ]);
    }

    public function failed(\Throwable $exception): void
    {
        \Log::error('Fee charge processing failed', [
            'clinician_id' => $this->clinician->id,
            'charge_ids' => $this->chargeIds,
            'error' => $exception->getMessage(),
        ]);
    }
}
```

- [ ] **Step 5: Run tests**

Run: `php artisan test --compact --filter="ProcessDisbursementTransferTest|ProcessFeeChargeTest"`
Expected: All tests pass.

- [ ] **Step 6: Run Pint and commit**

```bash
vendor/bin/pint --dirty --format agent
git add app/Jobs/ProcessDisbursementTransfer.php app/Jobs/ProcessFeeCharge.php tests/Feature/Jobs/ProcessDisbursementTransferTest.php tests/Feature/Jobs/ProcessFeeChargeTest.php
git commit -m "feat: add queue jobs for disbursement transfers and fee charges"
```

---

## Task 9: Fee Processing — Selective Processing + Card Surcharge

**Files:**
- Modify: `app/Livewire/Admin/FeeProcessing.php:1-170`
- Modify: `resources/views/livewire/admin/fee-processing.blade.php`
- Create: `tests/Feature/Admin/FeeProcessingEnhancedTest.php`

- [ ] **Step 1: Write the failing tests**

Create `tests/Feature/Admin/FeeProcessingEnhancedTest.php`:

```php
<?php

use App\Enums\ChargeStatus;
use App\Livewire\Admin\FeeProcessing;
use App\Models\Charge;
use App\Models\Clinician;
use App\Models\ServiceCatalog;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

it('selects and processes specific clinicians', function () {
    $admin = User::factory()->admin()->create();
    $c1 = Clinician::factory()->create();
    $c2 = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->create();

    Charge::factory()->create(['clinician_id' => $c1->id, 'service_catalog_id' => $service->id, 'status' => ChargeStatus::Pending, 'amount' => 10]);
    Charge::factory()->create(['clinician_id' => $c2->id, 'service_catalog_id' => $service->id, 'status' => ChargeStatus::Pending, 'amount' => 20]);

    Livewire::actingAs($admin)
        ->test(FeeProcessing::class)
        ->call('toggleClinician', $c1->id)
        ->call('processSelected')
        ->call('confirmProcess');

    // Only c1's charge should be settled
    expect(Charge::where('clinician_id', $c1->id)->first()->status)->toBe(ChargeStatus::Settled);
    expect(Charge::where('clinician_id', $c2->id)->first()->status)->toBe(ChargeStatus::Pending);
});

it('toggles select all', function () {
    $admin = User::factory()->admin()->create();
    $c1 = Clinician::factory()->create();
    $c2 = Clinician::factory()->create();
    $service = ServiceCatalog::factory()->create();
    Charge::factory()->create(['clinician_id' => $c1->id, 'service_catalog_id' => $service->id, 'status' => ChargeStatus::Pending]);
    Charge::factory()->create(['clinician_id' => $c2->id, 'service_catalog_id' => $service->id, 'status' => ChargeStatus::Pending]);

    Livewire::actingAs($admin)
        ->test(FeeProcessing::class)
        ->call('toggleSelectAll')
        ->assertNotEmpty('selectedClinicianIds');
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `php artisan test --compact --filter=FeeProcessingEnhancedTest`
Expected: Method `toggleClinician` not found.

- [ ] **Step 3: Add selective processing methods to FeeProcessing**

Add these properties after `$confirmTotalAmount` in `app/Livewire/Admin/FeeProcessing.php`:

```php
/** @var int[] */
public array $selectedClinicianIds = [];

public bool $selectAll = false;

public bool $processingSelected = false;
```

Add these methods before `confirmProcess()`:

```php
public function toggleClinician(int $id): void
{
    if (in_array($id, $this->selectedClinicianIds)) {
        $this->selectedClinicianIds = array_values(array_diff($this->selectedClinicianIds, [$id]));
    } else {
        $this->selectedClinicianIds[] = $id;
    }
}

public function toggleSelectAll(): void
{
    if (count($this->selectedClinicianIds) > 0) {
        $this->selectedClinicianIds = [];
        $this->selectAll = false;
    } else {
        $this->selectedClinicianIds = Clinician::query()
            ->whereHas('charges', fn ($q) => $q->pending())
            ->pluck('id')
            ->toArray();
        $this->selectAll = true;
    }
}

public function processSelected(): void
{
    if (empty($this->selectedClinicianIds)) {
        return;
    }

    $totals = $this->getPendingTotals($this->selectedClinicianIds);
    $this->confirmTotalCount = $totals['totalCount'];
    $this->confirmTotalAmount = number_format($totals['totalAmount'], 2);
    $this->showConfirmation = true;
    $this->processingSelected = true;
    $this->processingResult = null;
}
```

Update `processAllPending()` to set `$this->processingSelected = false;`.

Update `confirmProcess()` to filter by `$this->selectedClinicianIds` when `$this->processingSelected` is true:

Replace the clinicians query in `confirmProcess()`:

```php
$cliniciansQuery = Clinician::query()->with('user')
    ->whereHas('charges', fn ($q) => $q->pending());

if ($this->processingSelected && ! empty($this->selectedClinicianIds)) {
    $cliniciansQuery->whereIn('id', $this->selectedClinicianIds);
}

$clinicians = $cliniciansQuery->get();
```

Also add after the settlement is complete:

```php
$this->selectedClinicianIds = [];
$this->selectAll = false;
$this->processingSelected = false;
```

Update `getPendingTotals()` to accept an optional filter:

```php
/**
 * @param  int[]|null  $clinicianIds
 * @return array{totalCount: int, totalAmount: float}
 */
private function getPendingTotals(?array $clinicianIds = null): array
{
    $query = Charge::query()->pending();

    if ($clinicianIds) {
        $query->whereIn('clinician_id', $clinicianIds);
    }

    $result = $query->selectRaw('COUNT(*) as total_count, COALESCE(SUM(amount), 0) as total_amount')->first();

    return [
        'totalCount' => (int) $result->total_count,
        'totalAmount' => (float) $result->total_amount,
    ];
}
```

- [ ] **Step 4: Update the Blade view for checkboxes and "Process Selected"**

In `resources/views/livewire/admin/fee-processing.blade.php`, add a checkbox column to the summary table header and each row. Add a "Process Selected" button next to "Process All Pending". The clinician name should link to the support services page.

Add checkbox to table header:
```blade
<th class="px-4 py-3 w-10">
    <input type="checkbox" wire:click="toggleSelectAll" @checked(count($selectedClinicianIds) > 0) class="rounded border-gray-300 text-gold focus:ring-gold">
</th>
```

Add checkbox to each row:
```blade
<td class="px-4 py-3">
    <input type="checkbox" wire:click="toggleClinician({{ $row['clinician']->id }})" @checked(in_array($row['clinician']->id, $selectedClinicianIds)) class="rounded border-gray-300 text-gold focus:ring-gold">
</td>
```

Make clinician name a link:
```blade
<a href="{{ route('admin.clinician-support-services', $row['clinician']) }}" class="text-navy hover:text-gold transition-colors">
    {{ $row['clinician']->display_name }} ({{ $row['clinician']->clinician_code }})
</a>
```

Add "Process Selected" button:
```blade
@if (count($selectedClinicianIds))
    <button wire:click="processSelected" class="px-4 py-2 text-sm font-medium text-white bg-navy rounded-lg hover:bg-navy/80 transition-colors">
        Process Selected ({{ count($selectedClinicianIds) }})
    </button>
@endif
```

- [ ] **Step 5: Run tests**

Run: `php artisan test --compact --filter=FeeProcessingEnhancedTest`
Expected: All tests pass.

- [ ] **Step 6: Run Pint and commit**

```bash
vendor/bin/pint --dirty --format agent
git add app/Livewire/Admin/FeeProcessing.php resources/views/livewire/admin/fee-processing.blade.php tests/Feature/Admin/FeeProcessingEnhancedTest.php
git commit -m "feat: add selective clinician processing and clinician name links in fee processing"
```

---

## Task 10: UI Polish — ID Click Behavior + Clinician Name Link

**Files:**
- Modify: `resources/views/livewire/admin/sessions-database.blade.php`

- [ ] **Step 1: Update session ID click behavior**

In the sessions database table, find the `session_id` cell and change it to:

```blade
<td class="px-4 py-3">
    <span class="inline-flex items-center gap-1">
        <button wire:click="$set('search', '{{ $session->session_id }}')" class="text-xs font-mono text-navy hover:text-gold transition-colors" title="Search for this ID">
            {{ $session->session_id }}
        </button>
        <button wire:click="$dispatch('openSession', { id: {{ $session->id }} })" class="text-gray-400 hover:text-gold transition-colors" title="View details">
            <svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /></svg>
        </button>
    </span>
</td>
```

Similarly for `client_id`:

```blade
<td class="px-4 py-3">
    @if ($session->client_id)
        <button wire:click="$set('search', '{{ $session->client_id }}')" class="text-xs font-mono text-navy hover:text-gold transition-colors" title="Filter by this Client ID">
            {{ $session->client_id }}
        </button>
    @else
        <span class="text-gray-400">—</span>
    @endif
</td>
```

- [ ] **Step 2: Visual verification**

Run `npm run build` (or ask user to start dev server) and visit `/admin/sessions` to verify:
- Clicking a session ID populates the search bar
- Arrow icon opens the slide-over
- Clicking a client ID populates the search bar

- [ ] **Step 3: Run Pint and commit**

```bash
vendor/bin/pint --dirty --format agent
git add resources/views/livewire/admin/sessions-database.blade.php
git commit -m "feat: add ID click-to-search and arrow navigation in sessions database"
```

---

## Task 11: Final Integration — Run All Tests

- [ ] **Step 1: Run the full test suite**

```bash
php artisan test --compact
```

Expected: All tests pass.

- [ ] **Step 2: Fix any failures**

If any tests fail, diagnose and fix. Common issues:
- Migration order (run `php artisan migrate:fresh` in test to reset)
- Factory states missing new columns (add `payment_method_type` default to ClinicianFactory)

- [ ] **Step 3: Run Pint on everything**

```bash
vendor/bin/pint --format agent
```

- [ ] **Step 4: Final commit if needed**

```bash
git add -A
git commit -m "fix: resolve integration test issues from Phase 2+3 gap-fill"
```
