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:
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Booking\Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Booking\Enums\BookingChannel;
|
||||
use Modules\Booking\Enums\BookingStatus;
|
||||
use Modules\Booking\Models\Booking;
|
||||
use Modules\Catalog\Models\DepartureTimeSlot;
|
||||
use Modules\Routing\Models\EvRoute;
|
||||
|
||||
/**
|
||||
* @extends Factory<Booking>
|
||||
*/
|
||||
class BookingFactory extends Factory
|
||||
{
|
||||
protected $model = Booking::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'booking_ref' => 'EVB-'.strtoupper(Str::random(6)),
|
||||
'user_id' => null,
|
||||
'openid' => null,
|
||||
'ev_route_id' => EvRoute::factory(),
|
||||
'departure_time_slot_id' => DepartureTimeSlot::factory(),
|
||||
'travel_date' => now()->addDay()->toDateString(),
|
||||
'passenger_name' => $this->faker->name(),
|
||||
'passenger_phone' => $this->faker->phoneNumber(),
|
||||
'pickup_address' => $this->faker->address(),
|
||||
'pickup_lat' => null,
|
||||
'pickup_lng' => null,
|
||||
'dropoff_address' => $this->faker->address(),
|
||||
'dropoff_lat' => null,
|
||||
'dropoff_lng' => null,
|
||||
'price' => $this->faker->randomFloat(2, 5000, 50000),
|
||||
'status' => BookingStatus::PendingPayment,
|
||||
'is_round_trip' => false,
|
||||
'return_travel_date' => null,
|
||||
'created_by_channel' => BookingChannel::MiniApp,
|
||||
'driver_name' => null,
|
||||
'driver_phone' => null,
|
||||
'car_plate_number' => null,
|
||||
'car_model' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Booking\Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Modules\Booking\Models\Booking;
|
||||
use Modules\Booking\Models\BookingVehicleOption;
|
||||
use Modules\Shared\Enums\VehicleOption;
|
||||
|
||||
/**
|
||||
* @extends Factory<BookingVehicleOption>
|
||||
*/
|
||||
class BookingVehicleOptionFactory extends Factory
|
||||
{
|
||||
protected $model = BookingVehicleOption::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'booking_id' => Booking::factory(),
|
||||
'vehicle_option' => VehicleOption::BackSeat,
|
||||
'passenger_count' => 1,
|
||||
'unit_price' => 9000.00,
|
||||
'line_total' => 9000.00,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
+46
@@ -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');
|
||||
}
|
||||
};
|
||||
+34
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user