Files
ladill-care/tests/Feature/CarePaymentGatewayTest.php
T
isaacclad 7a0a7fcb88
Deploy Ladill Care / deploy (push) Successful in 1m0s
Add Paystack, Flutterwave, and Hubtel for encounter billing.
Clinics on Pro/Enterprise can connect their own gateway in Integrations and collect card or MoMo on bills with 0% Ladill fee, while cash payments and balance totals stay correct for pending checkouts.
2026-07-16 10:24:28 +00:00

272 lines
9.4 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\Payment;
use App\Models\PaymentGatewaySetting;
use App\Models\User;
use App\Models\Visit;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class CarePaymentGatewayTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Organization $organization;
protected Visit $visit;
protected Bill $bill;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(EnsurePlatformSession::class);
$this->user = User::create([
'public_id' => 'test-user-gateway',
'name' => 'Gateway Admin',
'email' => 'gateway@example.com',
]);
$this->organization = Organization::create([
'owner_ref' => $this->user->public_id,
'name' => 'Gateway Clinic',
'slug' => 'gateway-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' => 'hospital_admin',
]);
$branch = Branch::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'name' => 'Main',
'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-00099',
'first_name' => 'Ama',
'last_name' => 'Mensah',
'email' => 'ama@example.com',
]);
$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(),
]);
$this->actingAs($this->user)
->post(route('care.bills.generate', $this->visit));
$this->bill = Bill::firstOrFail();
}
public function test_admin_can_save_paystack_gateway(): void
{
$this->actingAs($this->user)
->post(route('care.integrations.gateway.save'), [
'gateway_provider' => 'paystack',
'gateway_public_key' => 'pk_test_care',
'gateway_secret_key' => 'sk_test_care',
'gateway_is_active' => '1',
])
->assertRedirect();
$this->assertDatabaseHas('care_payment_gateway_settings', [
'organization_id' => $this->organization->id,
'provider' => 'paystack',
'is_active' => 1,
]);
$setting = PaymentGatewaySetting::query()->where('organization_id', $this->organization->id)->first();
$this->assertNotNull($setting);
$this->assertTrue($setting->isConfigured());
$this->assertSame('sk_test_care', $setting->secret_key);
}
public function test_free_plan_cannot_save_gateway(): void
{
$this->organization->update([
'settings' => ['onboarded' => true, 'plan' => 'free'],
]);
$this->actingAs($this->user)
->post(route('care.integrations.gateway.save'), [
'gateway_provider' => 'paystack',
'gateway_secret_key' => 'sk_test',
'gateway_is_active' => '1',
])
->assertRedirect()
->assertSessionHas('error');
$this->assertDatabaseMissing('care_payment_gateway_settings', [
'organization_id' => $this->organization->id,
]);
}
public function test_gateway_checkout_and_callback_records_payment(): void
{
PaymentGatewaySetting::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'provider' => PaymentGatewaySetting::PROVIDER_PAYSTACK,
'public_key' => 'pk_test',
'secret_key' => 'sk_test',
'is_active' => true,
]);
Http::fake([
'https://api.paystack.co/transaction/initialize' => Http::response([
'status' => true,
'data' => [
'authorization_url' => 'https://checkout.paystack.com/care-test',
'reference' => 'CARE-TEST-REF-001',
],
], 200),
'https://api.paystack.co/transaction/verify/*' => Http::response([
'status' => true,
'data' => [
'status' => 'success',
'amount' => $this->bill->balance_minor,
'reference' => 'CARE-TEST-REF-001',
],
], 200),
]);
$balance = $this->bill->balance_minor;
$this->actingAs($this->user)
->post(route('care.bills.payments.gateway', $this->bill), [
'amount_minor' => $balance,
])
->assertRedirect('https://checkout.paystack.com/care-test');
$pending = Payment::query()->where('bill_id', $this->bill->id)->first();
$this->assertNotNull($pending);
$this->assertSame(Payment::STATUS_PENDING, $pending->status);
$this->assertSame(Payment::METHOD_GATEWAY, $pending->method);
$this->assertSame('CARE-TEST-REF-001', $pending->reference);
// Pending must not reduce balance.
$this->bill->refresh();
$this->assertSame($balance, $this->bill->balance_minor);
$this->actingAs($this->user)
->get(route('care.bills.payments.callback', ['reference' => 'CARE-TEST-REF-001']))
->assertOk();
$pending->refresh();
$this->assertSame(Payment::STATUS_PAID, $pending->status);
$this->bill->refresh();
$this->assertSame(Bill::STATUS_PAID, $this->bill->status);
$this->assertSame(0, $this->bill->balance_minor);
Http::assertSent(fn ($request) => str_contains($request->url(), 'paystack.co/transaction/initialize')
&& $request->hasHeader('Authorization', 'Bearer sk_test'));
}
public function test_flutterwave_initialize_uses_major_units(): void
{
PaymentGatewaySetting::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'provider' => PaymentGatewaySetting::PROVIDER_FLUTTERWAVE,
'public_key' => 'FLWPUBK_TEST',
'secret_key' => 'FLWSECK_TEST',
'is_active' => true,
]);
Http::fake([
'https://api.flutterwave.com/v3/payments' => Http::response([
'status' => 'success',
'data' => ['link' => 'https://checkout.flutterwave.com/v3/hosted/pay/test'],
], 200),
]);
$this->actingAs($this->user)
->post(route('care.bills.payments.gateway', $this->bill), [
'amount_minor' => 5000,
])
->assertRedirect('https://checkout.flutterwave.com/v3/hosted/pay/test');
Http::assertSent(function ($request) {
if (! str_contains($request->url(), 'flutterwave.com')) {
return false;
}
$data = $request->data();
return (float) ($data['amount'] ?? 0) === 50.0
&& $request->hasHeader('Authorization', 'Bearer FLWSECK_TEST');
});
}
public function test_hubtel_initialize_uses_basic_auth(): void
{
PaymentGatewaySetting::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'provider' => PaymentGatewaySetting::PROVIDER_HUBTEL,
'public_key' => '201XXXX',
'secret_key' => 'hubtel-secret',
'is_active' => true,
]);
Http::fake([
'https://payproxyapi.hubtel.com/items/initiate' => Http::response([
'data' => ['checkoutUrl' => 'https://pay.hubtel.com/checkout/test'],
], 200),
]);
$this->actingAs($this->user)
->post(route('care.bills.payments.gateway', $this->bill), [
'amount_minor' => 2500,
])
->assertRedirect('https://pay.hubtel.com/checkout/test');
Http::assertSent(function ($request) {
return str_contains($request->url(), 'hubtel.com')
&& ($request->data()['merchantAccountNumber'] ?? null) === '201XXXX'
&& (float) ($request->data()['totalAmount'] ?? 0) === 25.0;
});
}
}