Add Routing module: EvRoute, pricing, time slots, read API, caching (T3.1-T3.7)
Implements Phase 3 in full: EvRoute model with company/destination relations and a from/to-must-differ guard; the ev_route_time_slots pivot; RoutePricing with a per-route is_blocked flag (every route auto-manages exactly one price row per vehicle option via a fixed-row Filament repeater on both create and edit); PricingService::quote() with a shared VehicleOption enum; RoutingPlugin with the EvRouteResource admin UI (activation gated on non-blocked options being priced); the /api/v1/routes read API (list/show/pricing/time-slots, AI-agent-friendly nested shape); and Redis-tag-based response caching invalidated via EvRoute/RoutePricing observers.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\QueryException;
|
||||
use Modules\Catalog\Models\DepartureTimeSlot;
|
||||
use Modules\Catalog\Models\Destination;
|
||||
use Modules\Catalog\Models\EvCompany;
|
||||
use Modules\Routing\Models\EvRoute;
|
||||
|
||||
test('an ev route belongs to a company and two destinations', function () {
|
||||
$company = EvCompany::factory()->create();
|
||||
$from = Destination::factory()->create();
|
||||
$to = Destination::factory()->create();
|
||||
|
||||
$route = EvRoute::factory()->create([
|
||||
'ev_company_id' => $company->id,
|
||||
'from_destination_id' => $from->id,
|
||||
'to_destination_id' => $to->id,
|
||||
]);
|
||||
|
||||
expect($route->company)->toBeInstanceOf(EvCompany::class)
|
||||
->and($route->company->is($company))->toBeTrue()
|
||||
->and($route->fromDestination)->toBeInstanceOf(Destination::class)
|
||||
->and($route->fromDestination->is($from))->toBeTrue()
|
||||
->and($route->toDestination)->toBeInstanceOf(Destination::class)
|
||||
->and($route->toDestination->is($to))->toBeTrue();
|
||||
});
|
||||
|
||||
test('is_round_trip and is_active cast to boolean', function () {
|
||||
$route = EvRoute::factory()->create([
|
||||
'is_round_trip' => 1,
|
||||
'is_active' => 0,
|
||||
]);
|
||||
|
||||
expect($route->is_round_trip)->toBeTrue()
|
||||
->and($route->is_active)->toBeFalse();
|
||||
});
|
||||
|
||||
test('a route can be attached to time slots via the pivot, carrying its own is_active flag', function () {
|
||||
$route = EvRoute::factory()->create();
|
||||
$morningSlot = DepartureTimeSlot::factory()->create();
|
||||
$eveningSlot = DepartureTimeSlot::factory()->create();
|
||||
|
||||
$route->timeSlots()->attach([
|
||||
$morningSlot->id => ['is_active' => true],
|
||||
$eveningSlot->id => ['is_active' => false],
|
||||
]);
|
||||
|
||||
$route->refresh();
|
||||
|
||||
expect($route->timeSlots)->toHaveCount(2);
|
||||
|
||||
$attachedMorning = $route->timeSlots->firstWhere('id', $morningSlot->id);
|
||||
$attachedEvening = $route->timeSlots->firstWhere('id', $eveningSlot->id);
|
||||
|
||||
expect($attachedMorning->pivot->is_active)->toBeTrue()
|
||||
->and($attachedEvening->pivot->is_active)->toBeFalse();
|
||||
|
||||
expect($morningSlot->routes)->toHaveCount(1)
|
||||
->and($morningSlot->routes->first()->is($route))->toBeTrue();
|
||||
});
|
||||
|
||||
test('a route cannot have the same from and to destination', function () {
|
||||
$destination = Destination::factory()->create();
|
||||
|
||||
expect(fn () => EvRoute::factory()->create([
|
||||
'from_destination_id' => $destination->id,
|
||||
'to_destination_id' => $destination->id,
|
||||
]))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
test('a route cannot be attached to the same time slot twice', function () {
|
||||
$route = EvRoute::factory()->create();
|
||||
$slot = DepartureTimeSlot::factory()->create();
|
||||
|
||||
$route->timeSlots()->attach($slot->id, ['is_active' => true]);
|
||||
|
||||
expect(fn () => $route->timeSlots()->attach($slot->id, ['is_active' => true]))
|
||||
->toThrow(QueryException::class);
|
||||
});
|
||||
Reference in New Issue
Block a user