register(PaymentMethod::KbzMiniApp, FakeRefundApiGateway::class); Permission::findOrCreate('process_refunds', 'web'); $this->staff = User::factory()->create()->givePermissionTo('process_refunds'); $this->staffToken = $this->staff->createToken('staff-token')->plainTextToken; }); test('staff with process_refunds can refund a confirmed booking', function () { $booking = Booking::factory()->create(['status' => BookingStatus::Confirmed, 'price' => 15000]); Payment::factory()->completed()->create([ 'booking_id' => $booking->id, 'gateway' => PaymentMethod::KbzMiniApp, 'amount' => 15000, 'gateway_transaction_id' => 'EVB-API-REFUND-1', ]); $this->withHeader('Authorization', "Bearer {$this->staffToken}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/refund", [ 'amount' => 15000, 'reason' => 'customer requested cancellation', ]) ->assertCreated() ->assertJsonPath('data.status', RefundStatus::Completed->value); expect($booking->refresh()->status)->toBe(BookingStatus::Cancelled); }); test('a customer without process_refunds cannot refund their own booking', function () { $owner = User::factory()->create(); $token = $owner->createToken('customer-token')->plainTextToken; $booking = Booking::factory()->create(['user_id' => $owner->id, 'status' => BookingStatus::Confirmed, 'price' => 15000]); Payment::factory()->completed()->create([ 'booking_id' => $booking->id, 'gateway' => PaymentMethod::KbzMiniApp, 'amount' => 15000, 'gateway_transaction_id' => 'EVB-API-REFUND-2', ]); $this->withHeader('Authorization', "Bearer {$token}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/refund", [ 'amount' => 15000, 'reason' => 'customer requested cancellation', ]) ->assertForbidden(); expect($booking->refresh()->status)->toBe(BookingStatus::Confirmed); }); test('refunding a pending_payment booking surfaces as 422', function () { $booking = Booking::factory()->create(['status' => BookingStatus::PendingPayment]); $this->withHeader('Authorization', "Bearer {$this->staffToken}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/refund", [ 'amount' => 5000, 'reason' => 'reason', ]) ->assertStatus(422); }); test('a failed gateway refund surfaces the gateway message as 422 and leaves the booking confirmed', function () { FakeRefundApiGateway::$resultStatus = RefundStatus::Failed; $booking = Booking::factory()->create(['status' => BookingStatus::Confirmed, 'price' => 15000]); Payment::factory()->completed()->create([ 'booking_id' => $booking->id, 'gateway' => PaymentMethod::KbzMiniApp, 'amount' => 15000, 'gateway_transaction_id' => 'EVB-API-REFUND-3', ]); $this->withHeader('Authorization', "Bearer {$this->staffToken}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/refund", [ 'amount' => 15000, 'reason' => 'reason', ]) ->assertStatus(422) ->assertJsonPath('message', 'Refund failed at gateway'); expect($booking->refresh()->status)->toBe(BookingStatus::Confirmed); }); test('unauthenticated requests are rejected', function () { $booking = Booking::factory()->create(['status' => BookingStatus::Confirmed]); $this->postJson("/api/v1/bookings/{$booking->booking_ref}/refund", ['amount' => 100, 'reason' => 'x']) ->assertUnauthorized(); });