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; }); } }