Add Booking module: model, create/read/cancel API, Filament resource (T4.1-T4.7)

- Booking model with booking_vehicle_options line items (supports mixing
  vehicle options like front_seat + back_seat in one booking), price
  snapshot, status machine, and driver/car assignment fields
- BookingService: front-seat max, disabled-option toggles, duplicate-option
  and whole-vehicle-exclusivity guards
- CreateBookingAction, CancelBookingAction, AssignDriverAction
- BookingRefGenerator: sequential EVB-AAAAA1-style refs via row lock
- POST/GET/cancel booking API endpoints (Sanctum, ownership + admin policy)
- BookingPlugin + Filament BookingResource: list, detail view, Cancel and
  Assign Driver actions (shared between table and detail page)
- domain.md updated for multi-vehicle-option bookings (§2) and driver/
  vehicle assignment (§5a)
This commit is contained in:
Nyan Lin Paing
2026-08-08 21:43:15 +07:00
parent 4da9ecfe7d
commit 5b68f4fa38
51 changed files with 2698 additions and 42 deletions
@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('bookings', function (Blueprint $table) {
$table->id();
$table->string('booking_ref')->unique();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('openid')->nullable()->index();
$table->foreignId('ev_route_id')->constrained('ev_routes')->cascadeOnDelete();
$table->foreignId('departure_time_slot_id')->constrained('departure_time_slots')->cascadeOnDelete();
$table->date('travel_date');
$table->string('passenger_name');
$table->string('passenger_phone');
$table->string('pickup_address');
$table->decimal('pickup_lat', 10, 7)->nullable();
$table->decimal('pickup_lng', 10, 7)->nullable();
$table->string('dropoff_address');
$table->decimal('dropoff_lat', 10, 7)->nullable();
$table->decimal('dropoff_lng', 10, 7)->nullable();
// Total across all booking_vehicle_options lines — see that table for the
// per-vehicle-option breakdown (a booking can mix e.g. front_seat + back_seat).
$table->decimal('price', 10, 2);
$table->string('status')->default('pending_payment');
$table->boolean('is_round_trip')->default(false);
$table->date('return_travel_date')->nullable();
$table->string('created_by_channel');
$table->timestamps();
$table->index(['ev_route_id', 'travel_date', 'departure_time_slot_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bookings');
}
};
@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* One row per Vehicle Option selected on a booking a booking can mix
* e.g. front_seat + back_seat in one go (domain.md §2). The unique
* constraint keeps each option to a single line per booking (no
* duplicate front_seat rows); BookingService still enforces the
* business rules (front-seat max, disabled options, whole-vehicle
* exclusivity) on top of this shape.
*
* Also the natural source table for the deferred real-capacity-check
* phase (domain.md §7): summing passenger_count per vehicle_option for
* a route/date/time-slot is exactly what that future check needs.
*/
public function up(): void
{
Schema::create('booking_vehicle_options', function (Blueprint $table) {
$table->id();
$table->foreignId('booking_id')->constrained('bookings')->cascadeOnDelete();
$table->string('vehicle_option');
$table->unsignedInteger('passenger_count')->default(1);
// Snapshotted at booking time, same as bookings.price — never re-read
// from route_pricing later (domain.md §3).
$table->decimal('unit_price', 10, 2);
$table->decimal('line_total', 10, 2);
$table->timestamps();
$table->unique(['booking_id', 'vehicle_option']);
$table->index('vehicle_option');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('booking_vehicle_options');
}
};
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Driver/vehicle details, filled in by admin staff once a booking is
* confirmed (paid) and dispatch assigns who's actually doing the trip.
* Nullable unknown until assignment happens, and never required for
* pending_payment/cancelled/expired bookings.
*/
public function up(): void
{
Schema::table('bookings', function (Blueprint $table) {
$table->string('driver_name')->nullable();
$table->string('driver_phone')->nullable();
$table->string('car_plate_number')->nullable();
$table->string('car_model')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('bookings', function (Blueprint $table) {
$table->dropColumn(['driver_name', 'driver_phone', 'car_plate_number', 'car_model']);
});
}
};