withoutMiddleware(EnsurePlatformSession::class); $this->user = User::create([ 'public_id' => 'test-user-001', 'name' => 'Test User', 'email' => 'test@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->user->public_id, 'name' => 'Test Clinic', 'slug' => 'test-clinic', 'timezone' => 'UTC', 'settings' => ['onboarded' => true, 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String()], ]); Member::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->user->public_id, 'role' => 'cashier', ]); $branch = Branch::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'name' => 'Main Branch', 'is_active' => true, ]); $patient = Patient::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $branch->id, 'patient_number' => 'LC-2026-00001', 'first_name' => 'Kofi', 'last_name' => 'Asante', ]); $this->visit = Visit::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'status' => Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), ]); Consultation::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, 'visit_id' => $this->visit->id, 'patient_id' => $patient->id, 'status' => Consultation::STATUS_COMPLETED, 'started_at' => now()->subHour(), 'completed_at' => now(), ]); } public function test_cashier_can_generate_bill_from_visit(): void { $this->actingAs($this->user) ->post(route('care.bills.generate', $this->visit)) ->assertRedirect(); $bill = Bill::first(); $this->assertNotNull($bill); $this->assertSame(Bill::STATUS_OPEN, $bill->status); $this->assertTrue($bill->lineItems()->where('type', 'consultation')->exists()); $this->assertDatabaseHas('care_audit_logs', ['action' => 'bill.created']); } public function test_cashier_can_add_line_item_and_record_payments(): void { $this->actingAs($this->user) ->post(route('care.bills.generate', $this->visit)); $bill = Bill::firstOrFail(); $consultationFee = (int) config('care.billing.consultation_fee_minor', 5000); $this->actingAs($this->user) ->post(route('care.bills.line-items.store', $bill), [ 'type' => 'misc', 'description' => 'Dressing supplies', 'quantity' => 2, 'unit_price_minor' => 1000, ]) ->assertRedirect(); $bill->refresh(); $expectedTotal = $consultationFee + 2000; $this->assertSame($expectedTotal, $bill->total_minor); $this->actingAs($this->user) ->post(route('care.bills.payments.store', $bill), [ 'amount' => 30.00, 'method' => 'cash', ]) ->assertRedirect(); $bill->refresh(); $this->assertSame(Bill::STATUS_PARTIAL, $bill->status); $this->assertSame(3000, $bill->amount_paid_minor); $this->actingAs($this->user) ->post(route('care.bills.payments.store', $bill), [ 'amount' => $bill->balance_minor / 100, 'method' => 'momo', 'reference' => 'MOMO-123', ]) ->assertRedirect(); $bill->refresh(); $this->assertSame(Bill::STATUS_PAID, $bill->status); $this->assertSame(0, $bill->balance_minor); $this->assertDatabaseHas('care_audit_logs', ['action' => 'payment.recorded']); } public function test_bills_index_is_accessible(): void { $this->actingAs($this->user) ->post(route('care.bills.generate', $this->visit)); $this->actingAs($this->user) ->get(route('care.bills.index')) ->assertOk() ->assertSee('INV-'); } public function test_cashier_bills_index_defaults_to_outstanding(): void { $this->actingAs($this->user) ->post(route('care.bills.generate', $this->visit)); $open = Bill::firstOrFail(); $paid = Bill::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->visit->branch_id, 'visit_id' => $this->visit->id, 'patient_id' => $this->visit->patient_id, 'invoice_number' => 'INV-PAID-001', 'status' => Bill::STATUS_PAID, 'subtotal_minor' => 1000, 'total_minor' => 1000, 'amount_paid_minor' => 1000, 'balance_minor' => 0, ]); $this->actingAs($this->user) ->get(route('care.bills.index')) ->assertOk() ->assertSee($open->invoice_number) ->assertSee('Pay') ->assertDontSee($paid->invoice_number, false); $this->actingAs($this->user) ->get(route('care.bills.index', ['status' => ''])) ->assertOk() ->assertSee($paid->invoice_number); } public function test_cashier_bills_index_links_each_invoice_for_payment(): void { $this->actingAs($this->user) ->post(route('care.bills.generate', $this->visit)); $bill = Bill::firstOrFail(); $this->actingAs($this->user) ->get(route('care.bills.index')) ->assertOk() ->assertSee($bill->invoice_number) ->assertSee(route('care.bills.show', $bill), false) ->assertSee('Pay') ->assertSee('Actions'); $balanceMajor = number_format($bill->balance_minor / 100, 2, '.', ''); $this->actingAs($this->user) ->get(route('care.bills.show', $bill)) ->assertOk() ->assertSee('Record payment') ->assertSee('name="amount"', false) ->assertSee('value="'.$balanceMajor.'"', false) ->assertDontSee('value="'.$bill->balance_minor.'"', false); } public function test_call_next_empty_booth_explains_walk_up_invoices(): void { \Illuminate\Support\Facades\Http::fake(); $settings = $this->organization->settings ?? []; $settings['queue_integration_enabled'] = true; $this->organization->update(['settings' => $settings]); $this->actingAs($this->user) ->post(route('care.bills.call-next'), ['branch_id' => Branch::firstOrFail()->id]) ->assertRedirect() ->assertSessionHas('info', 'No patients waiting at the billing booth. Open an unpaid invoice above to record a walk-up payment.'); } }