Files
ladill-care/tests/Feature/CareBillTest.php
T
isaaccladandCursor e69c7da0d6
Deploy Ladill Care / deploy (push) Successful in 56s
Fix bill payment forms to use major currency units.
Cashiers saw pesewa integers (e.g. 4000) while history showed GHS 40.00; accept and prefill major amounts and convert to minor on record/checkout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 23:30:31 +00:00

237 lines
7.9 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Bill;
use App\Models\Branch;
use App\Models\Consultation;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\User;
use App\Models\Visit;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CareBillTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Organization $organization;
protected Visit $visit;
protected function setUp(): void
{
parent::setUp();
$this->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.');
}
}