Deploy Ladill Care / deploy (push) Successful in 1m0s
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.
272 lines
9.4 KiB
PHP
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;
|
|
});
|
|
}
|
|
}
|