owner = User::factory()->create(); $this->token = $this->owner->createToken('test-token')->plainTextToken; }); /** * Index only ever shows bookings with a completed payment — give the * booking a completed Payment row so it's not silently excluded. */ function paidBooking(array $attributes = []): Booking { $booking = Booking::factory()->create($attributes); Payment::factory()->completed()->create([ 'booking_id' => $booking->id, 'gateway' => PaymentMethod::KbzMiniApp, 'amount' => $booking->price, ]); return $booking; } test('index lists only the authenticated user\'s own bookings, latest first', function () { $mine = paidBooking(['user_id' => $this->owner->id, 'created_at' => now()->subMinute()]); $mineNewer = paidBooking(['user_id' => $this->owner->id]); paidBooking(['user_id' => User::factory()->create()->id]); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson('/api/v1/bookings') ->assertSuccessful() ->assertJsonCount(2, 'data') ->assertJsonPath('data.0.id', $mineNewer->id) ->assertJsonPath('data.1.id', $mine->id); }); test('index excludes bookings with no completed payment', function () { // pending_payment, never paid. Booking::factory()->create(['user_id' => $this->owner->id]); // Has a payment attempt, but it failed — still not "complete". $failedPayment = Booking::factory()->create(['user_id' => $this->owner->id]); Payment::factory()->failed()->create(['booking_id' => $failedPayment->id, 'gateway' => PaymentMethod::KbzMiniApp]); $paid = paidBooking(['user_id' => $this->owner->id]); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson('/api/v1/bookings') ->assertSuccessful() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.id', $paid->id); }); test('index surfaces a round trip once, not as two separate rows, with a combined total_price', function () { $outbound = paidBooking(['user_id' => $this->owner->id, 'price' => '9000.00']); $return = Booking::factory()->create([ 'user_id' => $this->owner->id, 'price' => '11000.00', 'is_return_leg' => true, 'linked_booking_id' => $outbound->id, ]); $outbound->update(['linked_booking_id' => $return->id]); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson('/api/v1/bookings') ->assertSuccessful() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.id', $outbound->id) ->assertJsonPath('data.0.price', '9000.00') ->assertJsonPath('data.0.total_price', '20000.00') ->assertJsonPath('data.0.linked_booking.id', $return->id); }); test('linked_booking carries the return leg\'s own driver/vehicle assignment, independent of the outbound leg\'s', function () { $outbound = paidBooking([ 'user_id' => $this->owner->id, 'status' => BookingStatus::Confirmed, 'driver_name' => 'U Aung', 'driver_phone' => '+959111222333', 'car_plate_number' => 'YGN-1234', 'car_model' => 'Tesla Model Y', ]); $return = Booking::factory()->create([ 'user_id' => $this->owner->id, 'is_return_leg' => true, 'linked_booking_id' => $outbound->id, 'status' => BookingStatus::Confirmed, 'driver_name' => 'Daw Hla', 'driver_phone' => '+959444555666', 'car_plate_number' => 'MDY-5678', 'car_model' => null, ]); $outbound->update(['linked_booking_id' => $return->id]); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson("/api/v1/bookings/{$outbound->booking_ref}") ->assertSuccessful() ->assertJsonPath('data.driver_name', 'U Aung') ->assertJsonPath('data.car_plate_number', 'YGN-1234') ->assertJsonPath('data.linked_booking.driver_name', 'Daw Hla') ->assertJsonPath('data.linked_booking.driver_phone', '+959444555666') ->assertJsonPath('data.linked_booking.car_plate_number', 'MDY-5678') ->assertJsonPath('data.linked_booking.car_model', null); }); test('total_price equals price for a plain one-way booking, on both index and show', function () { $booking = paidBooking(['user_id' => $this->owner->id, 'price' => '15000.00']); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson('/api/v1/bookings') ->assertJsonPath('data.0.total_price', '15000.00'); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson("/api/v1/bookings/{$booking->booking_ref}") ->assertJsonPath('data.total_price', '15000.00'); }); test('show returns the combined total_price for a round trip', function () { $outbound = Booking::factory()->create(['user_id' => $this->owner->id, 'price' => '9000.00']); $return = Booking::factory()->create([ 'user_id' => $this->owner->id, 'price' => '11000.00', 'is_return_leg' => true, 'linked_booking_id' => $outbound->id, ]); $outbound->update(['linked_booking_id' => $return->id]); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson("/api/v1/bookings/{$outbound->booking_ref}") ->assertSuccessful() ->assertJsonPath('data.price', '9000.00') ->assertJsonPath('data.total_price', '20000.00'); }); test('index filters by booking_ref, partial and case-insensitive', function () { $match = paidBooking(['user_id' => $this->owner->id, 'booking_ref' => 'EVB-FINDME1']); paidBooking(['user_id' => $this->owner->id, 'booking_ref' => 'EVB-OTHER01']); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson('/api/v1/bookings?booking_ref=findme') ->assertSuccessful() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.id', $match->id); }); test('index rejects unauthenticated requests', function () { $this->getJson('/api/v1/bookings')->assertUnauthorized(); }); test('show allows the owner to view their own booking', function () { $booking = Booking::factory()->create(['user_id' => $this->owner->id]); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson("/api/v1/bookings/{$booking->booking_ref}") ->assertSuccessful() ->assertJsonPath('data.id', $booking->id); }); test('show rejects a non-owner without the view_bookings permission', function () { $booking = Booking::factory()->create(['user_id' => User::factory()->create()->id]); $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson("/api/v1/bookings/{$booking->booking_ref}") ->assertForbidden(); }); test('show allows an admin/support user (view_bookings permission) to view someone else\'s booking', function () { $admin = User::factory()->create(); $admin->givePermissionTo('view_bookings'); $adminToken = $admin->createToken('admin-token')->plainTextToken; $booking = Booking::factory()->create(['user_id' => $this->owner->id]); $this->withHeader('Authorization', "Bearer {$adminToken}") ->getJson("/api/v1/bookings/{$booking->booking_ref}") ->assertSuccessful() ->assertJsonPath('data.id', $booking->id); }); test('show rejects unauthenticated requests', function () { $booking = Booking::factory()->create(); $this->getJson("/api/v1/bookings/{$booking->id}")->assertUnauthorized(); }); test('show 404s for a booking that does not exist', function () { $this->withHeader('Authorization', "Bearer {$this->token}") ->getJson('/api/v1/bookings/EVB-DOES-NOT-EXIST') ->assertNotFound(); });