self::$verifyStatus->value], ); } public function refund(string $gatewayTransactionId, string $amount, string $reason): RefundResultData { throw new RuntimeException('not needed for this test'); } public function handleWebhook(array $payload): PaymentResultData { throw new RuntimeException('not needed for this test'); } } beforeEach(function () { FakeConfirmGateway::$verifyCallCount = 0; FakeConfirmGateway::$verifyStatus = PaymentStatus::Completed; app(PaymentGatewayFactory::class)->register(PaymentMethod::KbzMiniApp, FakeConfirmGateway::class); }); test('confirming a pending payment as completed dispatches PaymentCompleted once', function () { Event::fake([PaymentCompleted::class, PaymentFailed::class]); $payment = Payment::factory()->create([ 'status' => PaymentStatus::Pending, 'gateway' => PaymentMethod::KbzMiniApp, 'gateway_transaction_id' => 'EVB-TXN-001', ]); $result = app(ConfirmPaymentAction::class)->handle(PaymentMethod::KbzMiniApp, 'EVB-TXN-001'); expect($result->id)->toBe($payment->id) ->and($result->status)->toBe(PaymentStatus::Completed) ->and(FakeConfirmGateway::$verifyCallCount)->toBe(1); Event::assertDispatchedTimes(PaymentCompleted::class, 1); Event::assertNotDispatched(PaymentFailed::class); }); test('a redelivered confirmation for an already-completed payment is a no-op', function () { Event::fake([PaymentCompleted::class, PaymentFailed::class]); Payment::factory()->create([ 'status' => PaymentStatus::Pending, 'gateway' => PaymentMethod::KbzMiniApp, 'gateway_transaction_id' => 'EVB-TXN-002', ]); $action = app(ConfirmPaymentAction::class); $action->handle(PaymentMethod::KbzMiniApp, 'EVB-TXN-002'); $second = $action->handle(PaymentMethod::KbzMiniApp, 'EVB-TXN-002'); expect($second->status)->toBe(PaymentStatus::Completed) ->and(FakeConfirmGateway::$verifyCallCount)->toBe(1); Event::assertDispatchedTimes(PaymentCompleted::class, 1); }); test('confirming a payment the gateway reports as failed dispatches PaymentFailed and leaves the booking untouched', function () { Event::fake([PaymentCompleted::class, PaymentFailed::class]); FakeConfirmGateway::$verifyStatus = PaymentStatus::Failed; $payment = Payment::factory()->create([ 'status' => PaymentStatus::Pending, 'gateway' => PaymentMethod::KbzMiniApp, 'gateway_transaction_id' => 'EVB-TXN-003', ]); $result = app(ConfirmPaymentAction::class)->handle(PaymentMethod::KbzMiniApp, 'EVB-TXN-003'); expect($result->status)->toBe(PaymentStatus::Failed); Event::assertDispatchedTimes(PaymentFailed::class, 1); Event::assertNotDispatched(PaymentCompleted::class); }); test('a gateway verify still reporting pending leaves the payment pending and dispatches nothing', function () { Event::fake([PaymentCompleted::class, PaymentFailed::class]); FakeConfirmGateway::$verifyStatus = PaymentStatus::Pending; Payment::factory()->create([ 'status' => PaymentStatus::Pending, 'gateway' => PaymentMethod::KbzMiniApp, 'gateway_transaction_id' => 'EVB-TXN-004', ]); $result = app(ConfirmPaymentAction::class)->handle(PaymentMethod::KbzMiniApp, 'EVB-TXN-004'); expect($result->status)->toBe(PaymentStatus::Pending); Event::assertNothingDispatched(); }); test('returns null when no payment matches the gateway transaction id', function () { $result = app(ConfirmPaymentAction::class)->handle(PaymentMethod::KbzMiniApp, 'does-not-exist'); expect($result)->toBeNull(); }); test('confirming a completed payment flips its booking to confirmed via MarkBookingPaid', function () { $booking = Booking::factory()->create(['status' => BookingStatus::PendingPayment]); Payment::factory()->create([ 'booking_id' => $booking->id, 'status' => PaymentStatus::Pending, 'gateway' => PaymentMethod::KbzMiniApp, 'gateway_transaction_id' => 'EVB-TXN-005', ]); app(ConfirmPaymentAction::class)->handle(PaymentMethod::KbzMiniApp, 'EVB-TXN-005'); expect($booking->refresh()->status)->toBe(BookingStatus::Confirmed); });