Deploy Ladill Care / deploy (push) Successful in 39s
Demo seeding used i%3 for both branch and paid status, so Ridge Clinic only got paid invoices. Cashiers now default to outstanding balances and Call next is secondary to the walk-up unpaid list. Co-authored-by: Cursor <cursoragent@cursor.com>
232 lines
7.7 KiB
PHP
232 lines
7.7 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_minor' => 3000,
|
|
'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_minor' => $bill->balance_minor,
|
|
'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');
|
|
|
|
$this->actingAs($this->user)
|
|
->get(route('care.bills.show', $bill))
|
|
->assertOk()
|
|
->assertSee('Record payment');
|
|
}
|
|
|
|
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.');
|
|
}
|
|
}
|