register(PaymentMethod::KbzMiniApp, FakeCancelApiRefundGateway::class); $this->owner = User::factory()->create(); $this->token = $this->owner->createToken('test-token')->plainTextToken; }); test('the owner can cancel their own pending_payment booking', function () { $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]); $this->withHeader('Authorization', "Bearer {$this->token}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/cancel") ->assertSuccessful() ->assertJsonPath('data.status', BookingStatus::Cancelled->value); expect($booking->refresh()->status)->toBe(BookingStatus::Cancelled); }); test('the owner cannot cancel their own confirmed booking without process_refunds', function () { $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::Confirmed]); $this->withHeader('Authorization', "Bearer {$this->token}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/cancel") ->assertForbidden(); expect($booking->refresh()->status)->toBe(BookingStatus::Confirmed); }); test('staff with process_refunds can cancel a confirmed booking, which refunds it in full', function () { $staff = User::factory()->create()->givePermissionTo('process_refunds'); $staffToken = $staff->createToken('staff-token')->plainTextToken; $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::Confirmed, 'price' => 15000]); Payment::factory()->completed()->create([ 'booking_id' => $booking->id, 'gateway' => PaymentMethod::KbzMiniApp, 'amount' => 15000, 'gateway_transaction_id' => 'EVB-CANCEL-API-1', ]); $this->withHeader('Authorization', "Bearer {$staffToken}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/cancel") ->assertSuccessful() ->assertJsonPath('data.status', BookingStatus::Cancelled->value); expect($booking->refresh()->status)->toBe(BookingStatus::Cancelled); }); test('cancelling a confirmed booking with no completed payment surfaces as 422 and leaves it untouched', function () { $staff = User::factory()->create()->givePermissionTo('process_refunds'); $staffToken = $staff->createToken('staff-token')->plainTextToken; $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::Confirmed]); $this->withHeader('Authorization', "Bearer {$staffToken}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/cancel") ->assertStatus(422); expect($booking->refresh()->status)->toBe(BookingStatus::Confirmed); }); test('a non-owner without manage_bookings cannot cancel someone else\'s booking', function () { $booking = Booking::factory()->create([ 'user_id' => User::factory()->create()->id, 'status' => BookingStatus::PendingPayment, ]); $this->withHeader('Authorization', "Bearer {$this->token}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/cancel") ->assertForbidden(); expect($booking->refresh()->status)->toBe(BookingStatus::PendingPayment); }); test('staff with manage_bookings can cancel someone else\'s pending_payment booking', function () { $staff = User::factory()->create()->givePermissionTo('manage_bookings'); $staffToken = $staff->createToken('staff-token')->plainTextToken; $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]); $this->withHeader('Authorization', "Bearer {$staffToken}") ->postJson("/api/v1/bookings/{$booking->booking_ref}/cancel") ->assertSuccessful(); expect($booking->refresh()->status)->toBe(BookingStatus::Cancelled); }); test('unauthenticated requests are rejected', function () { $booking = Booking::factory()->create(['status' => BookingStatus::PendingPayment]); $this->postJson("/api/v1/bookings/{$booking->booking_ref}/cancel")->assertUnauthorized(); }); test('404s for a booking that does not exist', function () { $this->withHeader('Authorization', "Bearer {$this->token}") ->postJson('/api/v1/bookings/EVB-DOES-NOT-EXIST/cancel') ->assertNotFound(); });