201 lines
7.8 KiB
PHP
201 lines
7.8 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use Modules\Booking\Enums\BookingStatus;
|
|
use Modules\Booking\Models\Booking;
|
|
use Modules\Payment\Enums\PaymentMethod;
|
|
use Modules\Payment\Models\Payment;
|
|
use Spatie\Permission\Models\Permission;
|
|
|
|
beforeEach(function () {
|
|
Permission::findOrCreate('view_bookings', 'web');
|
|
|
|
$this->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();
|
|
});
|