From cb59cb53a071a6e6d7fb43363cde2e79e498b909 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Wed, 30 Aug 2023 18:39:23 +0000 Subject: [PATCH 01/24] test: use array driver for cache tests --- phpunit.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/phpunit.xml b/phpunit.xml index e7b9a0d68..866dbc7de 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,6 +8,9 @@ + + + From 4ef262ed5929b13814b62ac9f85fb18280dd1b41 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Wed, 30 Aug 2023 18:40:36 +0000 Subject: [PATCH 02/24] feat: store resolved aircraft type with network aircraft --- .../Aircraft/AircraftDataUpdatedEvent.php | 7 +++ .../NotifyAircraftServiceOfDataUpdate.php | 20 ++++++++ app/Models/Vatsim/NetworkAircraft.php | 1 + app/Providers/NetworkServiceProvider.php | 2 + app/Services/AircraftService.php | 28 ++++++++++- app/Services/NetworkAircraftService.php | 34 +++++++++---- ..._type_column_to_network_aircraft_table.php | 35 +++++++++++++ tests/app/Services/AircraftServiceTest.php | 50 +++++++++++++++++++ .../Services/NetworkAircraftServiceTest.php | 33 ++++++++++-- 9 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 app/Events/Aircraft/AircraftDataUpdatedEvent.php create mode 100644 app/Listeners/Aircraft/NotifyAircraftServiceOfDataUpdate.php create mode 100644 database/migrations/2023_08_30_170815_add_aircraft_type_column_to_network_aircraft_table.php diff --git a/app/Events/Aircraft/AircraftDataUpdatedEvent.php b/app/Events/Aircraft/AircraftDataUpdatedEvent.php new file mode 100644 index 000000000..ebf536dcb --- /dev/null +++ b/app/Events/Aircraft/AircraftDataUpdatedEvent.php @@ -0,0 +1,7 @@ +aircraftService->aircraftDataUpdated(); + } +} diff --git a/app/Models/Vatsim/NetworkAircraft.php b/app/Models/Vatsim/NetworkAircraft.php index a16bb0ecd..c2532aba7 100644 --- a/app/Models/Vatsim/NetworkAircraft.php +++ b/app/Models/Vatsim/NetworkAircraft.php @@ -48,6 +48,7 @@ class NetworkAircraft extends Model 'transponder', 'planned_flighttype', 'planned_route', + 'aircraft_id', 'remarks', ]; diff --git a/app/Providers/NetworkServiceProvider.php b/app/Providers/NetworkServiceProvider.php index 956d30492..8b960f042 100644 --- a/app/Providers/NetworkServiceProvider.php +++ b/app/Providers/NetworkServiceProvider.php @@ -10,6 +10,7 @@ use App\Jobs\Squawk\MarkAssignmentDeletedOnDisconnect; use App\Jobs\Stand\TriggerUnassignmentOnDisconnect; use App\Models\FlightInformationRegion\FlightInformationRegion; +use App\Services\AircraftService; use App\Services\NetworkAircraftService; use App\Services\NetworkDataDownloadService; use App\Services\NetworkDataService; @@ -30,6 +31,7 @@ public function register() $this->app->singleton(NetworkAircraftService::class, function (Application $application) { return new NetworkAircraftService( $application->make(NetworkDataService::class), + $application->make(AircraftService::class), FlightInformationRegion::with('proximityMeasuringPoints') ->get() ->pluck('proximityMeasuringPoints') diff --git a/app/Services/AircraftService.php b/app/Services/AircraftService.php index abb0f388f..bf4a6a9df 100644 --- a/app/Services/AircraftService.php +++ b/app/Services/AircraftService.php @@ -4,17 +4,41 @@ use App\Models\Aircraft\Aircraft; use App\Models\Aircraft\WakeCategory; +use Illuminate\Support\Facades\Cache; class AircraftService { + private const AIRCRAFT_CODE_ID_MAP_CACHE_KEY = 'AIRCRAFT_CODE_ID_MAP'; + public function getAircraftDependency(): array { - return Aircraft::with('wakeCategories')->get()->map(fn (Aircraft $aircraft) => [ + return Aircraft::with('wakeCategories')->get()->map(fn(Aircraft $aircraft) => [ 'id' => $aircraft->id, 'icao_code' => $aircraft->code, 'wake_categories' => $aircraft->wakeCategories->map( - fn (WakeCategory $category) => $category->id + fn(WakeCategory $category) => $category->id )->toArray(), ])->toArray(); } + + public function getAircraftIdFromCode(string $code): ?int + { + return $this->aircraftCodeIdMap()[$code] ?? null; + } + + public function aircraftDataUpdated(): void + { + Cache::forget(self::AIRCRAFT_CODE_ID_MAP_CACHE_KEY); + } + + private function aircraftCodeIdMap(): array + { + return Cache::rememberForever( + self::AIRCRAFT_CODE_ID_MAP_CACHE_KEY, + fn() => Aircraft::all()->mapWithKeys(fn(Aircraft $aircraft) => [ + $aircraft->code => $aircraft->id, + ])->toArray() + ); + } + } diff --git a/app/Services/NetworkAircraftService.php b/app/Services/NetworkAircraftService.php index 2e5d8a944..f72382ffe 100644 --- a/app/Services/NetworkAircraftService.php +++ b/app/Services/NetworkAircraftService.php @@ -21,15 +21,22 @@ class NetworkAircraftService private NetworkDataService $dataService; private Collection $allAircraftBeforeUpdate; - public function __construct(NetworkDataService $dataService, Collection $measuringPoints) - { + private readonly AircraftService $aircraftService; + + public function __construct( + NetworkDataService $dataService, + AircraftService $aircraftService, + Collection $measuringPoints + ) { $this->measuringPoints = $measuringPoints; + $this->aircraftService = $aircraftService; $this->dataService = $dataService; } public function updateNetworkData(): void { - $this->allAircraftBeforeUpdate = NetworkAircraft::all()->mapWithKeys(function (NetworkAircraft $aircraft) { + $this->allAircraftBeforeUpdate = NetworkAircraft::all()->mapWithKeys(function (NetworkAircraft $aircraft) + { return [$aircraft->callsign => $aircraft]; }); @@ -46,14 +53,16 @@ private function formatPilotData(Collection $pilots): Collection private function mapPilotData(Collection $pilotData): Collection { - return $pilotData->map(function (array $pilot) { + return $pilotData->map(function (array $pilot) + { return $this->formatPilot($pilot); }); } private function filterPilotData(Collection $pilotData): Collection { - return $pilotData->filter(function (array $pilot) { + return $pilotData->filter(function (array $pilot) + { return $this->shouldProcessPilot($pilot) && $this->pilotValid($pilot); }); @@ -72,7 +81,8 @@ private function processPilots(Collection $pilots): void private function shouldProcessPilot(array $pilot): bool { - return $this->measuringPoints->contains(function (Coordinate $coordinate) use ($pilot) { + return $this->measuringPoints->contains(function (Coordinate $coordinate) use ($pilot) + { return LocationService::metersToNauticalMiles( $coordinate->getDistance(new Coordinate($pilot['latitude'], $pilot['longitude']), new Haversine()) ) < self::MAX_PROCESSING_DISTANCE; @@ -84,6 +94,8 @@ private function shouldProcessPilot(array $pilot): bool */ private function formatPilot(array $pilot): array { + $shortAircraftCode = $this->getFlightplanDataElement($pilot, 'aircraft_short'); + return [ 'callsign' => $pilot['callsign'], 'cid' => $pilot['cid'], @@ -93,7 +105,7 @@ private function formatPilot(array $pilot): array 'groundspeed' => $pilot['groundspeed'], 'transponder' => $pilot['transponder'], 'planned_aircraft' => $this->getFlightplanDataElement($pilot, 'aircraft'), - 'planned_aircraft_short' => $this->getFlightplanDataElement($pilot, 'aircraft_short'), + 'planned_aircraft_short' => $shortAircraftCode, 'planned_depairport' => $this->getFlightplanDataElement($pilot, 'departure'), 'planned_destairport' => $this->getFlightplanDataElement($pilot, 'arrival'), 'planned_altitude' => $this->getFlightplanDataElement($pilot, 'altitude'), @@ -101,6 +113,9 @@ private function formatPilot(array $pilot): array 'planned_route' => $this->getFlightplanDataElement($pilot, 'route'), 'remarks' => $this->getFlightplanDataElement($pilot, 'remarks'), 'transponder_last_updated_at' => $this->getTransponderUpdatedAtTime($pilot), + 'aircraft_id' => $shortAircraftCode + ? $this->aircraftService->getAircraftIdFromCode($shortAircraftCode) + : null, ]; } @@ -111,7 +126,7 @@ private function formatPilot(array $pilot): array private function getTransponderUpdatedAtTime(array $pilot): Carbon { return $this->allAircraftBeforeUpdate->has($pilot['callsign']) && - $this->allAircraftBeforeUpdate->get($pilot['callsign'])->transponder === $pilot['transponder'] + $this->allAircraftBeforeUpdate->get($pilot['callsign'])->transponder === $pilot['transponder'] ? $this->allAircraftBeforeUpdate->get($pilot['callsign'])->transponder_last_updated_at : Carbon::now(); } @@ -132,7 +147,8 @@ private function handleTimeouts(): void NetworkAircraft::timedOut() ->get() ->each( - function (NetworkAircraft $aircraft): void { + function (NetworkAircraft $aircraft): void + { AircraftDisconnected::dispatchSync($aircraft); } ); diff --git a/database/migrations/2023_08_30_170815_add_aircraft_type_column_to_network_aircraft_table.php b/database/migrations/2023_08_30_170815_add_aircraft_type_column_to_network_aircraft_table.php new file mode 100644 index 000000000..d9a4d85cc --- /dev/null +++ b/database/migrations/2023_08_30_170815_add_aircraft_type_column_to_network_aircraft_table.php @@ -0,0 +1,35 @@ +foreignIdFor(Aircraft::class) + ->after('remarks') + ->comment('The matched aircraft type for this flight') + ->nullable() + ->constrained() + ->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('network_aircraft', function (Blueprint $table) { + $table->dropForeign(['aircraft_id']); + $table->dropColumn('aircraft_id'); + }); + } +}; diff --git a/tests/app/Services/AircraftServiceTest.php b/tests/app/Services/AircraftServiceTest.php index c862b6c5e..6c2633edf 100644 --- a/tests/app/Services/AircraftServiceTest.php +++ b/tests/app/Services/AircraftServiceTest.php @@ -3,8 +3,11 @@ namespace App\Services; use App\BaseFunctionalTestCase; +use App\Events\Aircraft\AircraftDataUpdatedEvent; +use App\Models\Aircraft\Aircraft; use App\Models\Aircraft\WakeCategory; use App\Models\Aircraft\WakeCategoryScheme; +use Illuminate\Support\Facades\Cache; class AircraftServiceTest extends BaseFunctionalTestCase { @@ -14,6 +17,9 @@ public function setUp(): void { parent::setUp(); $this->service = $this->app->make(AircraftService::class); + + // Call this to ensure the cache is cleared before each test + $this->service->aircraftDataUpdated(); } public function testItGeneratesDependency() @@ -47,4 +53,48 @@ public function testItGeneratesDependency() $this->assertEquals($expected, $this->service->getAircraftDependency()); } + + public function testItGetsAircraftIdFromCode() + { + $this->assertEquals(1, $this->service->getAircraftIdFromCode('B738')); + $this->assertEquals(2, $this->service->getAircraftIdFromCode('A333')); + $this->assertNull($this->service->getAircraftIdFromCode('A332')); + } + + public function testAircraftIfFromCodeIsCached() + { + $this->assertEquals(1, $this->service->getAircraftIdFromCode('B738')); + Aircraft::withoutEvents(function () + { + Aircraft::where('code', 'B738')->update(['code' => 'B799']); + }); + $this->assertEquals(1, $this->service->getAircraftIdFromCode('B738')); + $this->assertNull($this->service->getAircraftIdFromCode('B799')); + } + + public function testItClearsAircraftCodeCacheOnAircraftUpdated() + { + $this->assertEquals(1, $this->service->getAircraftIdFromCode('B738')); + Aircraft::withoutEvents(function () + { + Aircraft::where('code', 'B738')->update(['code' => 'B799']); + }); + $this->service->aircraftDataUpdated(); + + $this->assertNull($this->service->getAircraftIdFromCode('B738')); + $this->assertEquals(1, $this->service->getAircraftIdFromCode('B799')); + } + + public function testItClearsAircraftCodeCacheOnEvent() + { + $this->assertEquals(1, $this->service->getAircraftIdFromCode('B738')); + Aircraft::where('code', 'B738')->update(['code' => 'B799']); + $this->assertEquals(1, $this->service->getAircraftIdFromCode('B738')); + $this->assertNull($this->service->getAircraftIdFromCode('B799')); + + event(new AircraftDataUpdatedEvent); + + $this->assertNull($this->service->getAircraftIdFromCode('B738')); + $this->assertEquals(1, $this->service->getAircraftIdFromCode('B799')); + } } diff --git a/tests/app/Services/NetworkAircraftServiceTest.php b/tests/app/Services/NetworkAircraftServiceTest.php index 0d3fd8f6d..6d4b54386 100644 --- a/tests/app/Services/NetworkAircraftServiceTest.php +++ b/tests/app/Services/NetworkAircraftServiceTest.php @@ -34,6 +34,7 @@ protected function setUp(): void $this->getPilotData('BMI221', true, null, null, '777'), $this->getPilotData('BMI222', true, null, null, '12a4'), $this->getPilotData('BMI223', true, null, null, '7778'), + $this->getPilotData('BAW999', true, aircraftType: 'XYZ'), ]; Bus::fake(); @@ -67,6 +68,24 @@ public function testItAddsNewAircraftFromDataFeed() ); } + public function testItAddsNewAircraftWithUnknownAircraftType() + { + Event::fake(); + $this->fakeNetworkDataReturn(); + $this->service->updateNetworkData(); + $this->assertDatabaseHas( + 'network_aircraft', + array_merge( + $this->getTransformedPilotData('BAW999', aircraftType: 'XYZ'), + [ + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + 'transponder_last_updated_at' => Carbon::now() + ] + ), + ); + } + public function testItUpdatesExistingAircraftFromDataFeed() { Event::fake(); @@ -340,7 +359,8 @@ private function getPilotData( bool $hasFlightplan, float $latitude = null, float $longitude = null, - string $transponder = null + string $transponder = null, + string $aircraftType = 'B738', ): array { return [ @@ -353,8 +373,8 @@ private function getPilotData( 'transponder' => $transponder ?? '0457', 'flight_plan' => $hasFlightplan ? [ - 'aircraft' => 'H/B738/M', - 'aircraft_short' => 'B738', + 'aircraft' => sprintf('H/%s/M', $aircraftType), + 'aircraft_short' => $aircraftType, 'departure' => 'EGKK', 'arrival' => 'EGPH', 'altitude' => '15001', @@ -369,10 +389,11 @@ private function getPilotData( private function getTransformedPilotData( string $callsign, bool $hasFlightplan = true, - string $transponder = null + string $transponder = null, + string $aircraftType = 'B738' ): array { - $pilot = $this->getPilotData($callsign, $hasFlightplan, null, null, $transponder); + $pilot = $this->getPilotData($callsign, $hasFlightplan, null, null, $transponder, $aircraftType); $baseData = [ 'callsign' => $pilot['callsign'], 'cid' => $pilot['cid'], @@ -394,6 +415,8 @@ private function getTransformedPilotData( 'planned_altitude' => $pilot['flight_plan']['altitude'], 'planned_flighttype' => $pilot['flight_plan']['flight_rules'], 'planned_route' => $pilot['flight_plan']['route'], + 'remarks' => $pilot['flight_plan']['remarks'], + 'aircraft_id' => $pilot['flight_plan']['aircraft_short'] === 'B738' ? 1 : null, ] ); } From 486202ff6f298efadc53345f66d7100fff53f734 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Thu, 31 Aug 2023 19:31:07 +0000 Subject: [PATCH 03/24] refactor: resolve airline on network data update --- .gitignore | 1 + app/Events/Airline/AirlinesUpdatedEvent.php | 6 ++ .../NotifyAirlineServiceOfDataUpdate.php | 17 +++++ app/Models/Airline/Airline.php | 3 + app/Models/Vatsim/NetworkAircraft.php | 6 ++ app/Providers/NetworkServiceProvider.php | 2 + app/Services/AirlineService.php | 29 +++++++- app/Services/NetworkAircraftService.php | 5 ++ database/factories/Airline/AirlineFactory.php | 33 +++++++++ ...d_airline_id_to_network_aircraft_table.php | 34 ++++++++++ tests/app/Services/AirlineServiceTest.php | 68 +++++++++++++++++++ .../Services/NetworkAircraftServiceTest.php | 17 +++-- 12 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 app/Events/Airline/AirlinesUpdatedEvent.php create mode 100644 app/Listeners/Airline/NotifyAirlineServiceOfDataUpdate.php create mode 100644 database/factories/Airline/AirlineFactory.php create mode 100644 database/migrations/2023_08_31_171451_add_airline_id_to_network_aircraft_table.php diff --git a/.gitignore b/.gitignore index be9e081f7..28f831f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ bootstrap/cache/ /public/mix-manifest.json yarn-error.log .phpunit.cache +DEVSENSE/ diff --git a/app/Events/Airline/AirlinesUpdatedEvent.php b/app/Events/Airline/AirlinesUpdatedEvent.php new file mode 100644 index 000000000..77beee738 --- /dev/null +++ b/app/Events/Airline/AirlinesUpdatedEvent.php @@ -0,0 +1,6 @@ +airlineService->airlinesUpdated(); + } +} diff --git a/app/Models/Airline/Airline.php b/app/Models/Airline/Airline.php index c9d411409..a04cd078f 100644 --- a/app/Models/Airline/Airline.php +++ b/app/Models/Airline/Airline.php @@ -4,11 +4,14 @@ use App\Models\Airfield\Terminal; use App\Models\Stand\Stand; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Airline extends Model { + use HasFactory; + protected $fillable = [ 'icao_code', 'name', diff --git a/app/Models/Vatsim/NetworkAircraft.php b/app/Models/Vatsim/NetworkAircraft.php index c2532aba7..f18d0fb6d 100644 --- a/app/Models/Vatsim/NetworkAircraft.php +++ b/app/Models/Vatsim/NetworkAircraft.php @@ -49,6 +49,7 @@ class NetworkAircraft extends Model 'planned_flighttype', 'planned_route', 'aircraft_id', + 'airline_id', 'remarks', ]; @@ -156,4 +157,9 @@ public function user(): HasOne { return $this->hasOne(User::class, 'id', 'cid'); } + + public function aircraft(): BelongsTo + { + return $this->belongsTo(Aircraft::class); + } } diff --git a/app/Providers/NetworkServiceProvider.php b/app/Providers/NetworkServiceProvider.php index 8b960f042..57c111b6c 100644 --- a/app/Providers/NetworkServiceProvider.php +++ b/app/Providers/NetworkServiceProvider.php @@ -11,6 +11,7 @@ use App\Jobs\Stand\TriggerUnassignmentOnDisconnect; use App\Models\FlightInformationRegion\FlightInformationRegion; use App\Services\AircraftService; +use App\Services\AirlineService; use App\Services\NetworkAircraftService; use App\Services\NetworkDataDownloadService; use App\Services\NetworkDataService; @@ -32,6 +33,7 @@ public function register() return new NetworkAircraftService( $application->make(NetworkDataService::class), $application->make(AircraftService::class), + $application->make(AirlineService::class), FlightInformationRegion::with('proximityMeasuringPoints') ->get() ->pluck('proximityMeasuringPoints') diff --git a/app/Services/AirlineService.php b/app/Services/AirlineService.php index e56fd72cc..cd9afbe52 100644 --- a/app/Services/AirlineService.php +++ b/app/Services/AirlineService.php @@ -4,13 +4,24 @@ use App\Models\Airline\Airline; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; class AirlineService { + private const AIRLINE_CODE_ID_CACHE_MAP = 'AIRLINE_CODE_ID_MAP'; + public function getAirlineForAircraft(NetworkAircraft $aircraft): ?Airline { - return Airline::where('icao_code', Str::substr($aircraft->callsign, 0, 3))->first(); + $airlineId = $this->airlineIdForCallsign($aircraft->callsign); + return $airlineId + ? Airline::find($airlineId) + : null; + } + + public function airlineIdForCallsign(string $callsign): ?int + { + return $this->airlineCodeIdMap()[Str::substr($callsign, 0, 3)] ?? null; } public function getCallsignSlugForAircraft(NetworkAircraft $aircraft): string @@ -20,4 +31,20 @@ public function getCallsignSlugForAircraft(NetworkAircraft $aircraft): string ? Str::substr($aircraft->callsign, 3) : $aircraft->callsign; } + + public function airlinesUpdated() + { + Cache::forget(self::AIRLINE_CODE_ID_CACHE_MAP); + } + + private function airlineCodeIdMap(): array + { + return Cache::rememberForever( + self::AIRLINE_CODE_ID_CACHE_MAP, + fn() => Airline::all(['id', 'icao_code'])->mapWithKeys(function (Airline $airline) + { + return [$airline->icao_code => $airline->id]; + })->toArray() + ); + } } diff --git a/app/Services/NetworkAircraftService.php b/app/Services/NetworkAircraftService.php index f72382ffe..aa5af1c69 100644 --- a/app/Services/NetworkAircraftService.php +++ b/app/Services/NetworkAircraftService.php @@ -23,13 +23,17 @@ class NetworkAircraftService private readonly AircraftService $aircraftService; + private readonly AirlineService $airlineService; + public function __construct( NetworkDataService $dataService, AircraftService $aircraftService, + AirlineService $airlineService, Collection $measuringPoints ) { $this->measuringPoints = $measuringPoints; $this->aircraftService = $aircraftService; + $this->airlineService = $airlineService; $this->dataService = $dataService; } @@ -116,6 +120,7 @@ private function formatPilot(array $pilot): array 'aircraft_id' => $shortAircraftCode ? $this->aircraftService->getAircraftIdFromCode($shortAircraftCode) : null, + 'airline_id' => $this->airlineService->airlineIdForCallsign($pilot['callsign']), ]; } diff --git a/database/factories/Airline/AirlineFactory.php b/database/factories/Airline/AirlineFactory.php new file mode 100644 index 000000000..6f001ea04 --- /dev/null +++ b/database/factories/Airline/AirlineFactory.php @@ -0,0 +1,33 @@ + Str::upper($this->faker->unique()->lexify('???')), + 'name' => $this->faker->unique()->company(), + 'callsign' => $this->faker->unique()->word(), + 'is_cargo' => false, + ]; + } +} diff --git a/database/migrations/2023_08_31_171451_add_airline_id_to_network_aircraft_table.php b/database/migrations/2023_08_31_171451_add_airline_id_to_network_aircraft_table.php new file mode 100644 index 000000000..509127220 --- /dev/null +++ b/database/migrations/2023_08_31_171451_add_airline_id_to_network_aircraft_table.php @@ -0,0 +1,34 @@ +foreignIdFor(Airline::class) + ->nullable() + ->constrained() + ->after('aircraft_id') + ->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('network_aircraft', function (Blueprint $table) { + $table->dropForeign(['airline_id']); + $table->dropColumn('airline_id'); + }); + } +}; diff --git a/tests/app/Services/AirlineServiceTest.php b/tests/app/Services/AirlineServiceTest.php index 9974edc6c..3724f873d 100644 --- a/tests/app/Services/AirlineServiceTest.php +++ b/tests/app/Services/AirlineServiceTest.php @@ -3,6 +3,7 @@ namespace App\Services; use App\BaseFunctionalTestCase; +use App\Events\Airline\AirlinesUpdatedEvent; use App\Models\Airline\Airline; use App\Models\Vatsim\NetworkAircraft; use PHPUnit\Framework\Attributes\DataProvider; @@ -20,6 +21,73 @@ public function setUp(): void $this->service = $this->app->make(AirlineService::class); } + public function testItReturnsNullIfCallsignDoesNotMatchAirline() + { + $this->assertNull($this->service->airlineIdForCallsign('ABC123')); + } + + public function testItReturnsAirlineIdForCallsign() + { + $airline = Airline::factory()->create(); + $this->assertEquals( + $airline->id, + $this->service->airlineIdForCallsign($airline->icao_code . '123') + ); + } + + public function testItCachesAirlineIdForCallsignResult() + { + $airline = Airline::factory()->create(); + $originalIcaoCode = $airline->icao_code; + $this->assertEquals( + $airline->id, + $this->service->airlineIdForCallsign($airline->icao_code . '123') + ); + + $airline->update(['icao_code' => 'XXX']); + + $this->assertEquals( + $airline->id, + $this->service->airlineIdForCallsign($originalIcaoCode . '123') + ); + } + + public function testItClearsAirlineIdForCallsignCacheWhenAirlinesUpdated() + { + $airline = Airline::factory()->create(); + $originalIcaoCode = $airline->icao_code; + $this->assertEquals( + $airline->id, + $this->service->airlineIdForCallsign($originalIcaoCode . '123') + ); + + $airline->update(['icao_code' => 'XXX']); + $this->assertEquals($airline->id, $this->service->airlineIdForCallsign($originalIcaoCode . '123')); + $this->assertNull($this->service->airlineIdForCallsign('XXX123')); + + $this->service->airlinesUpdated(); + + $this->assertNull($this->service->airlineIdForCallsign($originalIcaoCode . '123')); + $this->assertEquals($airline->id, $this->service->airlineIdForCallsign('XXX123')); + } + + public function testItClearsAirlineIdForCallsignCacheOnEvent() + { + $airline = Airline::factory()->create(); + $originalIcaoCode = $airline->icao_code; + $this->assertEquals( + $airline->id, + $this->service->airlineIdForCallsign($airline->icao_code . '123') + ); + + event(new AirlinesUpdatedEvent()); + + $this->assertEquals($airline->id, $this->service->airlineIdForCallsign($originalIcaoCode . '123')); + $this->assertNull($this->service->airlineIdForCallsign('XXX123')); + } + + // TODO: Add events to filament + #[DataProvider('aircraftProvider')] public function testItReturnsAirlinesForAircraft(string $callsign, string $expectedAirline) { diff --git a/tests/app/Services/NetworkAircraftServiceTest.php b/tests/app/Services/NetworkAircraftServiceTest.php index 6d4b54386..ba6516792 100644 --- a/tests/app/Services/NetworkAircraftServiceTest.php +++ b/tests/app/Services/NetworkAircraftServiceTest.php @@ -76,7 +76,10 @@ public function testItAddsNewAircraftWithUnknownAircraftType() $this->assertDatabaseHas( 'network_aircraft', array_merge( - $this->getTransformedPilotData('BAW999', aircraftType: 'XYZ'), + $this->getTransformedPilotData( + 'BAW999', + aircraftType: 'XYZ' + ), [ 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), @@ -361,8 +364,7 @@ private function getPilotData( float $longitude = null, string $transponder = null, string $aircraftType = 'B738', - ): array - { + ): array { return [ 'callsign' => $callsign, 'cid' => self::ACTIVE_USER_CID, @@ -391,8 +393,7 @@ private function getTransformedPilotData( bool $hasFlightplan = true, string $transponder = null, string $aircraftType = 'B738' - ): array - { + ): array { $pilot = $this->getPilotData($callsign, $hasFlightplan, null, null, $transponder, $aircraftType); $baseData = [ 'callsign' => $pilot['callsign'], @@ -417,6 +418,12 @@ private function getTransformedPilotData( 'planned_route' => $pilot['flight_plan']['route'], 'remarks' => $pilot['flight_plan']['remarks'], 'aircraft_id' => $pilot['flight_plan']['aircraft_short'] === 'B738' ? 1 : null, + 'airline_id' => match (Str::substr($pilot['callsign'], 0, 3)) { + 'BAW' => 2, + 'SHT' => 2, + 'VIR' => 3, + default => null, + }, ] ); } From fd04dba8dfbb25cfa27e669e8676d20b2141f266 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Thu, 31 Aug 2023 20:01:50 +0000 Subject: [PATCH 04/24] fix: trigger events on page saves --- app/Filament/Resources/AircraftResource.php | 6 +- .../AircraftResource/Pages/CreateAircraft.php | 6 + .../AircraftResource/Pages/EditAircraft.php | 12 +- app/Filament/Resources/AirlineResource.php | 14 ++- .../AirlineResource/Pages/CreateAirline.php | 6 + .../AirlineResource/Pages/EditAirline.php | 11 ++ tests/app/Filament/AircraftResourceTest.php | 94 +++++++++++++- tests/app/Filament/AirlineResourceTest.php | 117 ++++++++++++++++++ 8 files changed, 255 insertions(+), 11 deletions(-) diff --git a/app/Filament/Resources/AircraftResource.php b/app/Filament/Resources/AircraftResource.php index 1a8f401f4..e2afada44 100644 --- a/app/Filament/Resources/AircraftResource.php +++ b/app/Filament/Resources/AircraftResource.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources; +use App\Events\Aircraft\AircraftDataUpdatedEvent; use App\Filament\Resources\AircraftResource\Pages; use App\Filament\Resources\AircraftResource\RelationManagers\WakeCategoriesRelationManager; use App\Models\Aircraft\Aircraft; @@ -104,7 +105,10 @@ public static function table(Table $table): Table ->actions([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), - Tables\Actions\DeleteAction::make(), + Tables\Actions\DeleteAction::make() + ->after(function () { + event(new AircraftDataUpdatedEvent); + }), ]) ->defaultSort('code'); } diff --git a/app/Filament/Resources/AircraftResource/Pages/CreateAircraft.php b/app/Filament/Resources/AircraftResource/Pages/CreateAircraft.php index 3734465d7..7a31748ea 100644 --- a/app/Filament/Resources/AircraftResource/Pages/CreateAircraft.php +++ b/app/Filament/Resources/AircraftResource/Pages/CreateAircraft.php @@ -2,10 +2,16 @@ namespace App\Filament\Resources\AircraftResource\Pages; +use App\Events\Aircraft\AircraftDataUpdatedEvent; use App\Filament\Resources\AircraftResource; use Filament\Resources\Pages\CreateRecord; class CreateAircraft extends CreateRecord { protected static string $resource = AircraftResource::class; + + protected function afterCreate(): void + { + event(new AircraftDataUpdatedEvent); + } } diff --git a/app/Filament/Resources/AircraftResource/Pages/EditAircraft.php b/app/Filament/Resources/AircraftResource/Pages/EditAircraft.php index 7091a87ba..623cbf307 100644 --- a/app/Filament/Resources/AircraftResource/Pages/EditAircraft.php +++ b/app/Filament/Resources/AircraftResource/Pages/EditAircraft.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources\AircraftResource\Pages; +use App\Events\Aircraft\AircraftDataUpdatedEvent; use App\Filament\Resources\AircraftResource; use Filament\Pages\Actions; use Filament\Resources\Pages\EditRecord; @@ -10,10 +11,13 @@ class EditAircraft extends EditRecord { protected static string $resource = AircraftResource::class; - protected function getActions(): array + protected function afterSave(): void { - return [ - Actions\DeleteAction::make(), - ]; + event(new AircraftDataUpdatedEvent); + } + + protected function afterDelete(): void + { + event(new AircraftDataUpdatedEvent); } } diff --git a/app/Filament/Resources/AirlineResource.php b/app/Filament/Resources/AirlineResource.php index 03cbe4fac..111d1940d 100644 --- a/app/Filament/Resources/AirlineResource.php +++ b/app/Filament/Resources/AirlineResource.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources; +use App\Events\Airline\AirlinesUpdatedEvent; use App\Filament\Helpers\SelectOptions; use App\Filament\Resources\AirlineResource\Pages; use App\Filament\Resources\AirlineResource\RelationManagers\StandsRelationManager; @@ -63,9 +64,9 @@ public static function form(Form $form): Form ->helperText(self::translateFormPath('copy_stand_assignments.helper')) ] ) - ->hidden(fn (Page $livewire) => !$livewire instanceof CreateRecord) - ->disabled(fn (Page $livewire) => !$livewire instanceof CreateRecord) - ->dehydrated(fn (Page $livewire) => $livewire instanceof CreateRecord), + ->hidden(fn(Page $livewire) => !$livewire instanceof CreateRecord) + ->disabled(fn(Page $livewire) => !$livewire instanceof CreateRecord) + ->dehydrated(fn(Page $livewire) => $livewire instanceof CreateRecord), ]); } @@ -88,7 +89,12 @@ public static function table(Table $table): Table ->actions([ ViewAction::make(), EditAction::make(), - DeleteAction::make(), + DeleteAction::make() + ->after(function () + { + dd('hi'); + event(new AirlinesUpdatedEvent); + }), ]); } diff --git a/app/Filament/Resources/AirlineResource/Pages/CreateAirline.php b/app/Filament/Resources/AirlineResource/Pages/CreateAirline.php index a55fa22a1..f0db6535e 100644 --- a/app/Filament/Resources/AirlineResource/Pages/CreateAirline.php +++ b/app/Filament/Resources/AirlineResource/Pages/CreateAirline.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources\AirlineResource\Pages; +use App\Events\Airline\AirlinesUpdatedEvent; use App\Filament\Resources\AirlineResource; use App\Models\Airfield\Terminal; use App\Models\Airline\Airline; @@ -75,4 +76,9 @@ private function getCopyablePivotAttributes(string $localModelColumn, Pivot $piv ARRAY_FILTER_USE_BOTH ); } + + protected function afterCreate(): void + { + event(new AirlinesUpdatedEvent); + } } diff --git a/app/Filament/Resources/AirlineResource/Pages/EditAirline.php b/app/Filament/Resources/AirlineResource/Pages/EditAirline.php index 8fe69c546..576acb3c5 100644 --- a/app/Filament/Resources/AirlineResource/Pages/EditAirline.php +++ b/app/Filament/Resources/AirlineResource/Pages/EditAirline.php @@ -2,10 +2,21 @@ namespace App\Filament\Resources\AirlineResource\Pages; +use App\Events\Airline\AirlinesUpdatedEvent; use App\Filament\Resources\AirlineResource; use Filament\Resources\Pages\EditRecord; class EditAirline extends EditRecord { public static string $resource = AirlineResource::class; + + protected function afterSave(): void + { + event(new AirlinesUpdatedEvent); + } + + protected function afterDelete(): void + { + event(new AirlinesUpdatedEvent); + } } diff --git a/tests/app/Filament/AircraftResourceTest.php b/tests/app/Filament/AircraftResourceTest.php index e348239c9..b7404a34f 100644 --- a/tests/app/Filament/AircraftResourceTest.php +++ b/tests/app/Filament/AircraftResourceTest.php @@ -3,6 +3,7 @@ namespace App\Filament; use App\BaseFilamentTestCase; +use App\Events\Aircraft\AircraftDataUpdatedEvent; use App\Filament\Resources\AircraftResource; use App\Filament\Resources\AircraftResource\Pages\CreateAircraft; use App\Filament\Resources\AircraftResource\Pages\EditAircraft; @@ -13,6 +14,7 @@ use App\Models\Aircraft\WakeCategory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; use Livewire\Livewire; class AircraftResourceTest extends BaseFilamentTestCase @@ -20,6 +22,12 @@ class AircraftResourceTest extends BaseFilamentTestCase use ChecksOperationsContributorActionVisibility; use ChecksOperationsContributorAccess; + public function setUp(): void + { + parent::setUp(); + Event::fake(); + } + public function testItLoadsDataForView() { Livewire::test(ViewAircraft::class, ['record' => 1]) @@ -48,6 +56,9 @@ public function testItCreatesAnAircraft() 'length' => 208.99, 'allocate_stands' => true, ]); + + // Check that the event was dispatched + Event::assertDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntCreateAnAircraftWithNoCode() @@ -59,6 +70,9 @@ public function testItDoesntCreateAnAircraftWithNoCode() ->set('data.allocate_stands', true) ->call('create') ->assertHasErrors('data.code'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntCreateAnAircraftWithEmptyCode() @@ -71,6 +85,9 @@ public function testItDoesntCreateAnAircraftWithEmptyCode() ->set('data.allocate_stands', true) ->call('create') ->assertHasErrors('data.code'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntCreateAnAircraftWithClashingCode() @@ -83,6 +100,9 @@ public function testItDoesntCreateAnAircraftWithClashingCode() ->set('data.allocate_stands', true) ->call('create') ->assertHasErrors('data.code'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntCreateAnAircraftWithNoAerodromeReferenceCode() @@ -94,6 +114,9 @@ public function testItDoesntCreateAnAircraftWithNoAerodromeReferenceCode() ->set('data.allocate_stands', true) ->call('create') ->assertHasErrors('data.aerodrome_reference_code'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntCreateAnAircraftWithNoWingspan() @@ -105,6 +128,9 @@ public function testItDoesntCreateAnAircraftWithNoWingspan() ->set('data.allocate_stands', true) ->call('create') ->assertHasErrors('data.wingspan'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntCreateAnAircraftWithNegativeWingspan() @@ -117,6 +143,9 @@ public function testItDoesntCreateAnAircraftWithNegativeWingspan() ->set('data.allocate_stands', true) ->call('create') ->assertHasErrors('data.wingspan'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntCreateAnAircraftWithNoLength() @@ -128,6 +157,9 @@ public function testItDoesntCreateAnAircraftWithNoLength() ->set('data.allocate_stands', true) ->call('create') ->assertHasErrors('data.length'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntCreateAnAircraftWithNegativeLength() @@ -140,6 +172,9 @@ public function testItDoesntCreateAnAircraftWithNegativeLength() ->set('data.allocate_stands', true) ->call('create') ->assertHasErrors('data.length'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItLoadsDataForEdit() @@ -171,6 +206,9 @@ public function testItEditsAnAircraft() 'length' => 129.50, 'allocate_stands' => false, ]); + + // Check that the event was dispatched + Event::assertDispatched(AircraftDataUpdatedEvent::class); } public function testItEditsAnAircraftAndDoesntErrorWithExistingCode() @@ -192,6 +230,9 @@ public function testItEditsAnAircraftAndDoesntErrorWithExistingCode() 'length' => 129.50, 'allocate_stands' => false, ]); + + // Check that the event was dispatched + Event::assertDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntEditAnAircraftWithNoCode() @@ -204,6 +245,9 @@ public function testItDoesntEditAnAircraftWithNoCode() ->set('data.allocate_stands', false) ->call('save') ->assertHasErrors('data.code'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntEditAnAircraftWithEmptyCode() @@ -216,6 +260,9 @@ public function testItDoesntEditAnAircraftWithEmptyCode() ->set('data.allocate_stands', false) ->call('save') ->assertHasErrors('data.code'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntEditAnAircraftWithClashingCode() @@ -228,11 +275,14 @@ public function testItDoesntEditAnAircraftWithClashingCode() ->set('data.allocate_stands', false) ->call('save') ->assertHasErrors('data.code'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntEditAnAircraftWithNoWingspan - () - { + ( + ) { Livewire::test(EditAircraft::class, ['record' => 1]) ->set('data.code', 'B738') ->set('data.aerodrome_reference_code', 'F') @@ -241,6 +291,9 @@ public function testItDoesntEditAnAircraftWithNoWingspan ->set('data.allocate_stands', false) ->call('save') ->assertHasErrors('data.wingspan'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntEditAnAircraftWithNegativeWingspan() @@ -253,6 +306,9 @@ public function testItDoesntEditAnAircraftWithNegativeWingspan() ->set('data.allocate_stands', false) ->call('save') ->assertHasErrors('data.wingspan'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntEditAnAircraftWithNoLength() @@ -265,6 +321,9 @@ public function testItDoesntEditAnAircraftWithNoLength() ->set('data.allocate_stands', false) ->call('save') ->assertHasErrors('data.length'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); } public function testItDoesntEditAnAircraftWithNegativeLength() @@ -277,6 +336,37 @@ public function testItDoesntEditAnAircraftWithNegativeLength() ->set('data.allocate_stands', false) ->call('save') ->assertHasErrors('data.length'); + + // Check that the event was not dispatched + Event::assertNotDispatched(AircraftDataUpdatedEvent::class); + } + + public function testItDeletesAircraftFromTheListingPage() + { + Livewire::test(ListAircraft::class) + ->callTableAction('delete', 1) + ->assertHasNoErrors(); + + $this->assertDatabaseMissing('aircraft', [ + 'id' => 1, + ]); + + // Check that the event was dispatched + Event::assertDispatched(AircraftDataUpdatedEvent::class); + } + + public function testItDeletesAircraftFromTheEditPage() + { + Livewire::test(EditAircraft::class, ['record' => 1]) + ->callPageAction('delete') + ->assertHasNoPageActionErrors(); + + $this->assertDatabaseMissing('aircraft', [ + 'id' => 1, + ]); + + // Check that the event was dispatched + Event::assertDispatched(AircraftDataUpdatedEvent::class); } public function testItAllowsWakeCategoryAssociation() diff --git a/tests/app/Filament/AirlineResourceTest.php b/tests/app/Filament/AirlineResourceTest.php index 3033ac96a..87106f768 100644 --- a/tests/app/Filament/AirlineResourceTest.php +++ b/tests/app/Filament/AirlineResourceTest.php @@ -3,6 +3,7 @@ namespace App\Filament; use App\BaseFilamentTestCase; +use App\Events\Airline\AirlinesUpdatedEvent; use App\Filament\Resources\AirlineResource; use App\Filament\Resources\AirlineResource\Pages\CreateAirline; use App\Filament\Resources\AirlineResource\Pages\EditAirline; @@ -15,6 +16,7 @@ use App\Models\Stand\Stand; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; use Livewire\Livewire; @@ -23,6 +25,12 @@ class AirlineResourceTest extends BaseFilamentTestCase use ChecksOperationsContributorActionVisibility; use ChecksOperationsContributorAccess; + public function setUp(): void + { + parent::setUp(); + Event::fake(); + } + public function testItLoadsDataForView() { Livewire::test(ViewAirline::class, ['record' => 1]) @@ -51,6 +59,9 @@ public function testItCreatesAnAirline() 'is_cargo' => false, ] ); + + // Check that the event was dispatched + Event::assertDispatched(AirlinesUpdatedEvent::class); } public function testItCreatesACargoAirline() @@ -72,6 +83,9 @@ public function testItCreatesACargoAirline() 'is_cargo' => true, ] ); + + // Check that the event was dispatched + Event::assertDispatched(AirlinesUpdatedEvent::class); } public function testItCreatesAnAirlineAndCopiesStandAndTerminalAssignments() @@ -179,6 +193,9 @@ public function testItCreatesAnAirlineAndCopiesStandAndTerminalAssignments() 'full_callsign' => 'def', ] ); + + // Check that the event was dispatched + Event::assertDispatched(AirlinesUpdatedEvent::class); } public function testItCreatesAnAirlineAndDoesntCopyStandAndTerminalAssignments() @@ -234,6 +251,9 @@ public function testItCreatesAnAirlineAndDoesntCopyStandAndTerminalAssignments() 'airline_id' => $airline->id, ] ); + + // Check that the event was dispatched + Event::assertDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntCreateAnAirlineNoIcaoCode() @@ -244,6 +264,9 @@ public function testItDoesntCreateAnAirlineNoIcaoCode() ->set('data.is_cargo', false) ->call('create') ->assertHasErrors(['data.icao_code']); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntCreateAnAirlineIcaoCodeEmpty() @@ -255,6 +278,9 @@ public function testItDoesntCreateAnAirlineIcaoCodeEmpty() ->set('data.is_cargo', false) ->call('create') ->assertHasErrors(['data.icao_code']); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntCreateAnAirlineIcaoCodeTooLong() @@ -266,6 +292,9 @@ public function testItDoesntCreateAnAirlineIcaoCodeTooLong() ->set('data.is_cargo', false) ->call('create') ->assertHasErrors(['data.icao_code']); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntCreateAnAirlineNoName() @@ -276,6 +305,9 @@ public function testItDoesntCreateAnAirlineNoName() ->set('data.is_cargo', false) ->call('create') ->assertHasErrors(['data.name']); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntCreateAnAirlineNameEmpty() @@ -287,6 +319,9 @@ public function testItDoesntCreateAnAirlineNameEmpty() ->set('data.is_cargo', false) ->call('create') ->assertHasErrors(['data.name']); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntCreateAnAirlineNameTooLong() @@ -298,6 +333,9 @@ public function testItDoesntCreateAnAirlineNameTooLong() ->set('data.is_cargo', false) ->call('create') ->assertHasErrors(['data.name']); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntCreateAnAirlineNoCallsign() @@ -308,6 +346,9 @@ public function testItDoesntCreateAnAirlineNoCallsign() ->set('data.is_cargo', false) ->call('create') ->assertHasErrors(['data.callsign']); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntCreateAnAirlineCallsignEmpty() @@ -319,6 +360,9 @@ public function testItDoesntCreateAnAirlineCallsignEmpty() ->set('data.is_cargo', false) ->call('create') ->assertHasErrors(['data.callsign']); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntCreateAnAirlineCallsignTooLong() @@ -330,6 +374,9 @@ public function testItDoesntCreateAnAirlineCallsignTooLong() ->set('data.is_cargo', false) ->call('create') ->assertHasErrors(['data.callsign']); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItLoadsDataForEdit() @@ -339,6 +386,9 @@ public function testItLoadsDataForEdit() ->assertSet('data.name', 'British Airways') ->assertSet('data.callsign', 'SPEEDBIRD') ->assertSet('data.is_cargo', false); + + // Check that the event was not dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItEditsAnAirline() @@ -361,6 +411,9 @@ public function testItEditsAnAirline() 'is_cargo' => false, ] ); + + // Check that the event was dispatched + Event::assertDispatched(AirlinesUpdatedEvent::class); } public function testItEditsACargoAirline() @@ -383,6 +436,9 @@ public function testItEditsACargoAirline() 'is_cargo' => true, ] ); + + // Check that the event was dispatched + Event::assertDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntEditAnAirlineNoIcaoCode() @@ -394,6 +450,9 @@ public function testItDoesntEditAnAirlineNoIcaoCode() ->set('data.is_cargo', false) ->call('save') ->assertHasErrors(['data.icao_code']); + + // Check that the event was nmot dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntEditAnAirlineIcaoCodeEmpty() @@ -405,6 +464,9 @@ public function testItDoesntEditAnAirlineIcaoCodeEmpty() ->set('data.is_cargo', false) ->call('save') ->assertHasErrors(['data.icao_code']); + + // Check that the event was nmot dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntEditAnAirlineIcaoCodeTooLong() @@ -416,6 +478,9 @@ public function testItDoesntEditAnAirlineIcaoCodeTooLong() ->set('data.is_cargo', false) ->call('save') ->assertHasErrors(['data.icao_code']); + + // Check that the event was nmot dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntEditAnAirlineNoName() @@ -427,6 +492,9 @@ public function testItDoesntEditAnAirlineNoName() ->set('data.is_cargo', false) ->call('save') ->assertHasErrors(['data.name']); + + // Check that the event was nmot dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntEditAnAirlineNameEmpty() @@ -438,6 +506,9 @@ public function testItDoesntEditAnAirlineNameEmpty() ->set('data.is_cargo', false) ->call('save') ->assertHasErrors(['data.name']); + + // Check that the event was nmot dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntEditAnAirlineNameTooLong() @@ -449,6 +520,9 @@ public function testItDoesntEditAnAirlineNameTooLong() ->set('data.is_cargo', false) ->call('save') ->assertHasErrors(['data.name']); + + // Check that the event was nmot dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntEditAnAirlineNoCallsign() @@ -460,6 +534,9 @@ public function testItDoesntEditAnAirlineNoCallsign() ->set('data.is_cargo', false) ->call('save') ->assertHasErrors(['data.callsign']); + + // Check that the event was nmot dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntEditAnAirlineCallsignEmpty() @@ -471,6 +548,9 @@ public function testItDoesntEditAnAirlineCallsignEmpty() ->set('data.is_cargo', false) ->call('save') ->assertHasErrors(['data.callsign']); + + // Check that the event was nmot dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); } public function testItDoesntEditAnAirlineCallsignTooLong() @@ -482,6 +562,43 @@ public function testItDoesntEditAnAirlineCallsignTooLong() ->set('data.is_cargo', false) ->call('save') ->assertHasErrors(['data.callsign']); + + // Check that the event was nmot dispatched + Event::assertNotDispatched(AirlinesUpdatedEvent::class); + } + + public function testAirlinesCanBeDeletedFromTheEditPage() + { + Livewire::test(EditAirline::class, ['record' => 1]) + ->callPageAction('delete') + ->assertHasNoPageActionErrors(); + + $this->assertDatabaseMissing( + 'airlines', + [ + 'id' => 1, + ] + ); + + // Check that the event was dispatched + Event::assertDispatched(AirlinesUpdatedEvent::class); + } + + public function testAirlinesCanBeDeletedFromTheListingPage() + { + Livewire::test(ListAirlines::class) + ->callTableAction('delete', 1) + ->assertHasNoPageActionErrors(); + + $this->assertDatabaseMissing( + 'airlines', + [ + 'id' => 1, + ] + ); + + // Check that the event was dispatched + Event::assertDispatched(AirlinesUpdatedEvent::class); } public function testItAllowsTerminalPairingWithMinimalData() From e343746cd770136a0fa76aa8e20b74793986b7d4 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Sat, 2 Sep 2023 16:26:00 +0000 Subject: [PATCH 05/24] refactor: change stand allocation strategy remote the abstract class move common functionality into traits This allows us to do ranking later... --- .../Stand/AbstractArrivalStandAllocator.php | 88 ------------------- .../AirlineAircraftArrivalStandAllocator.php | 55 +++++++----- ...eAircraftTerminalArrivalStandAllocator.php | 60 ++++++++----- .../Stand/AirlineArrivalStandAllocator.php | 60 +++++++++---- .../AirlineCallsignArrivalStandAllocator.php | 47 +++++++--- ...rlineCallsignSlugArrivalStandAllocator.php | 50 ++++++++--- ...lsignSlugTerminalArrivalStandAllocator.php | 56 +++++++++--- ...eCallsignTerminalArrivalStandAllocator.php | 50 ++++++++--- ...irlineDestinationArrivalStandAllocator.php | 58 ++++++++---- ...stinationTerminalArrivalStandAllocator.php | 60 +++++++++---- .../AirlineTerminalArrivalStandAllocator.php | 59 +++++++++---- app/Allocator/Stand/AppliesOrdering.php | 16 ++++ app/Allocator/Stand/ArrivalStandAllocator.php | 15 ++++ .../Stand/ArrivalStandAllocatorInterface.php | 1 - ...lightplanReservedArrivalStandAllocator.php | 9 +- .../CargoAirlineFallbackStandAllocator.php | 27 +++++- .../CargoFlightArrivalStandAllocator.php | 27 +++++- ...goFlightPreferredArrivalStandAllocator.php | 35 ++++++-- .../CidReservedArrivalStandAllocator.php | 9 +- .../Stand/ConsidersStandRequests.php | 33 +++++++ .../DomesticInternationalStandAllocator.php | 23 ++++- .../Stand/FallbackArrivalStandAllocator.php | 31 ++++++- .../Stand/OrdersStandsByCommonConditions.php | 28 ++++++ .../Stand/OriginAirfieldStandAllocator.php | 33 +++++-- .../Stand/SelectsFirstApplicableStand.php | 12 +++ ...ectsFromSizeAppropriateAvailableStands.php | 24 +++++ .../SelectsStandsFromAirlineTerminals.php | 22 +++++ .../UserRequestedArrivalStandAllocator.php | 24 ++--- app/Models/Stand/Stand.php | 6 +- app/Models/Vatsim/NetworkAircraft.php | 1 + app/Services/AirlineService.php | 20 +++-- .../Stand/ArrivalAllocationService.php | 19 ++-- ...rlineAircraftArrivalStandAllocatorTest.php | 35 +++++++- ...craftTerminalArrivalStandAllocatorTest.php | 20 ++++- .../AirlineArrivalStandAllocatorTest.php | 26 ++++-- ...rlineCallsignArrivalStandAllocatorTest.php | 15 +++- ...CallsignSlugArrivalStandAllocatorTest.php} | 19 +++- ...nSlugTerminalArrivalStandAllocatorTest.php | 15 +++- ...lsignTerminalArrivalStandAllocatorTest.php | 12 ++- ...neDestinationArrivalStandAllocatorTest.php | 5 +- ...ationTerminalArrivalStandAllocatorTest.php | 3 + ...rlineTerminalArrivalStandAllocatorTest.php | 5 +- ...CargoAirlineFallbackStandAllocatorTest.php | 1 + .../CargoFlightArrivalStandAllocatorTest.php | 1 + ...ightPreferredArrivalStandAllocatorTest.php | 4 +- ...omesticInternationalStandAllocatorTest.php | 12 +-- .../FallbackArrivalStandAllocatorTest.php | 1 + .../OriginAirfieldStandAllocatorTest.php | 1 + 48 files changed, 886 insertions(+), 347 deletions(-) delete mode 100644 app/Allocator/Stand/AbstractArrivalStandAllocator.php create mode 100644 app/Allocator/Stand/AppliesOrdering.php create mode 100644 app/Allocator/Stand/ArrivalStandAllocator.php create mode 100644 app/Allocator/Stand/ConsidersStandRequests.php create mode 100644 app/Allocator/Stand/OrdersStandsByCommonConditions.php create mode 100644 app/Allocator/Stand/SelectsFirstApplicableStand.php create mode 100644 app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php create mode 100644 app/Allocator/Stand/SelectsStandsFromAirlineTerminals.php rename tests/app/Allocator/Stand/{AirlineCallsignSlugStandAllocatorTest.php => AirlineCallsignSlugArrivalStandAllocatorTest.php} (95%) diff --git a/app/Allocator/Stand/AbstractArrivalStandAllocator.php b/app/Allocator/Stand/AbstractArrivalStandAllocator.php deleted file mode 100644 index 42e5d8733..000000000 --- a/app/Allocator/Stand/AbstractArrivalStandAllocator.php +++ /dev/null @@ -1,88 +0,0 @@ -getPossibleStands($aircraft)->first()?->id; - } - - /* - * Base query for stands at the arrival airfield, which are of a suitable - * size (or max size if no type) for the aircraft and not occupied. - */ - private function getArrivalAirfieldStandQuery(NetworkAircraft $aircraft): Builder - { - return Stand::whereHas('airfield', function (Builder $query) use ($aircraft) { - $query->where('code', $aircraft->planned_destairport); - }) - ->sizeAppropriate(Aircraft::where('code', $aircraft->planned_aircraft_short)->first()) - ->available() - ->select('stands.*'); - } - - /** - * Get all the possible stands that are available for allocation. - * - * @param NetworkAircraft $aircraft - * @return Collection|Stand[] - */ - private function getPossibleStands(NetworkAircraft $aircraft): Collection - { - $orderedQuery = $this->getOrderedStandsQuery($this->getArrivalAirfieldStandQuery($aircraft), $aircraft); - return $orderedQuery === null - ? new Collection() - : $this->applyBaseOrderingToStandsQuery($orderedQuery, $aircraft)->get(); - } - - /** - * Apply the base ordering to the stands query. This orders stands by weight ascending - * so smaller aircraft prefer smaller stands and also applies an element of randomness - * so we don't just put all the aircraft next to each other. - * - * @param Builder $query - * @return Builder - */ - private function applyBaseOrderingToStandsQuery(Builder $query, NetworkAircraft $aircraft): Builder - { - return $query->orderByAerodromeReferenceCode() - ->orderByAssignmentPriority() - ->leftJoin('stand_requests as other_stand_requests', function (JoinClause $join) use ($aircraft) { - // Prefer stands that haven't been requested by someone else - $join->on('stands.id', '=', 'other_stand_requests.stand_id') - ->on('other_stand_requests.user_id', '<>', $join->raw($aircraft->cid)) - ->on( - 'other_stand_requests.requested_time', - '>', - $join->raw( - sprintf( - '\'%s\'', - Carbon::now() - ) - ) - ); - }) - ->orderByRaw('other_stand_requests.id IS NULL') - ->inRandomOrder(); - } - - /** - * If true, will prefer stands that haven't been requsted by the user; - */ - protected function prefersNonRequestedStands(): bool - { - return true; - } - - abstract protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder; -} diff --git a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php index 5230864c7..60b608ef8 100644 --- a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php @@ -2,35 +2,50 @@ namespace App\Allocator\Stand; -use App\Models\Aircraft\Aircraft; use App\Models\Vatsim\NetworkAircraft; -use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; -class AirlineAircraftArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineAircraftArrivalStandAllocator implements ArrivalStandAllocator { - private AirlineService $airlineService; + use SelectsFromSizeAppropriateAvailableStands; + use SelectsFirstApplicableStand; + use OrdersStandsByCommonConditions; + use AppliesOrdering; - public function __construct(AirlineService $airlineService) - { - $this->airlineService = $airlineService; - } + private const ORDER_BYS = [ + 'airline_stand.priority ASC', + ]; - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands that are specifically selected for the airline AND a given aircraft type + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions) + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { - $airline = $this->airlineService->getAirlineForAircraft($aircraft); - if ($airline === null) { + // We cant allocate a stand if we don't know the airline or aircraft type + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { return null; } - $aircraftType = Aircraft::where('code', $aircraft->planned_aircraft)->first(); - if (!$aircraftType) { - return null; - } - return $stands->with('airlines') - ->airline($airline) - ->where('airline_stand.aircraft_id', $aircraftType->id) - ->orderBy('airline_stand.priority'); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->airline($aircraft->airline_id) + ->where('airline_stand.aircraft_id', $aircraft->aircraft_id), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php index 19e0687f6..9151deeea 100644 --- a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php @@ -2,36 +2,52 @@ namespace App\Allocator\Stand; -use App\Models\Aircraft\Aircraft; use App\Models\Vatsim\NetworkAircraft; -use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; -class AirlineAircraftTerminalArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineAircraftTerminalArrivalStandAllocator implements ArrivalStandAllocator { - private AirlineService $airlineService; + use OrdersStandsByCommonConditions; + use SelectsFirstApplicableStand; + use SelectsFromSizeAppropriateAvailableStands; + use SelectsStandsFromAirlineTerminals; + use AppliesOrdering; - public function __construct(AirlineService $airlineService) - { - $this->airlineService = $airlineService; - } + private const ORDER_BYS = [ + 'airline_terminal.priority ASC', + ]; - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands at terminals that are specifically selected for the airline AND a given aircraft type + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions) + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { - $airline = $this->airlineService->getAirlineForAircraft($aircraft); - if ($airline === null) { - return null; - } - - $aircraftType = Aircraft::where('code', $aircraft->planned_aircraft)->first(); - if (!$aircraftType) { + // We can only allocate a stand if we know the airline and aircraft type + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { return null; } - return $stands->join('terminals', 'terminals.id', '=', 'stands.terminal_id') - ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') - ->where('airline_terminal.airline_id', $airline->id) - ->where('airline_terminal.aircraft_id', $aircraftType->id) - ->orderBy('airline_terminal.priority'); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->standsAtAirlineTerminals( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->where('airline_terminal.aircraft_id', $aircraft->aircraft_id), + $aircraft + ), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AirlineArrivalStandAllocator.php b/app/Allocator/Stand/AirlineArrivalStandAllocator.php index 75fa7f8cc..058bc0529 100644 --- a/app/Allocator/Stand/AirlineArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineArrivalStandAllocator.php @@ -3,28 +3,52 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; -use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; -class AirlineArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineArrivalStandAllocator implements ArrivalStandAllocator { - private AirlineService $airlineService; + use AppliesOrdering; + use SelectsFirstApplicableStand; + use SelectsFromSizeAppropriateAvailableStands; + use OrdersStandsByCommonConditions; - public function __construct(AirlineService $airlineService) - { - $this->airlineService = $airlineService; - } + private const ORDER_BYS = [ + 'airline_stand.priority ASC', + ]; - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands that are specifically selected for the airline and do not have any specific conditions + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions) + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { - $airline = $this->airlineService->getAirlineForAircraft($aircraft); - return $airline === null - ? null - : $stands->airline($airline) - ->whereNull('airline_stand.destination') - ->whereNull('airline_stand.callsign_slug') - ->whereNull('airline_stand.full_callsign') - ->whereNull('airline_stand.aircraft_id') - ->orderBy('airline_stand.priority'); + // We can only allocate a stand if we know the airline + if ($aircraft->airline_id === null) { + return null; + } + + + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->airline($aircraft->airline_id) + ->whereNull('airline_stand.destination') + ->whereNull('airline_stand.callsign_slug') + ->whereNull('airline_stand.full_callsign') + ->whereNull('airline_stand.aircraft_id'), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php index 72df86540..1f2bede7a 100644 --- a/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php @@ -4,29 +4,56 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; -class AirlineCallsignArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineCallsignArrivalStandAllocator implements ArrivalStandAllocator { + use AppliesOrdering; use UsesCallsignSlugs; + use SelectsFirstApplicableStand; + use SelectsFromSizeAppropriateAvailableStands; + use OrdersStandsByCommonConditions; - private AirlineService $airlineService; + private const ORDER_BYS = [ + 'airline_stand.priority ASC', + ]; + + private readonly AirlineService $airlineService; public function __construct(AirlineService $airlineService) { $this->airlineService = $airlineService; } - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands that are specifically selected for the airline and a specific callsign + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions) + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { - $airline = $this->airlineService->getAirlineForAircraft($aircraft); - if ($airline === null) { + // We can only allocate a stand if we know the airline + if ($aircraft->airline_id === null) { return null; } - return $stands->with('airlines') - ->airline($airline) - ->where('airline_stand.full_callsign', $this->getFullCallsignSlug($aircraft)) - ->orderBy('airline_stand.priority'); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->airline($aircraft->airline_id) + ->where('airline_stand.full_callsign', $this->getFullCallsignSlug($aircraft)), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php index fddf60824..29f08ca58 100644 --- a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php @@ -4,11 +4,20 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; -class AirlineCallsignSlugArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineCallsignSlugArrivalStandAllocator implements ArrivalStandAllocator { + use AppliesOrdering; use UsesCallsignSlugs; + use SelectsFirstApplicableStand; + use SelectsFromSizeAppropriateAvailableStands; + use OrdersStandsByCommonConditions; + + private const ORDER_BYS = [ + 'airline_stand.callsign_slug IS NOT NULL', + 'LENGTH(airline_stand.callsign_slug) DESC', + 'airline_stand.priority ASC', + ]; private AirlineService $airlineService; @@ -17,17 +26,38 @@ public function __construct(AirlineService $airlineService) $this->airlineService = $airlineService; } - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands that are specifically selected for the airline and a specific callsign slug + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions) + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { - $airline = $this->airlineService->getAirlineForAircraft($aircraft); - if ($airline === null) { + // We can't allocate a stand if we don't know the airline + if ($aircraft->airline_id === null) { return null; } - return $stands->with('airlines') - ->airlineCallsign($airline, $this->getCallsignSlugs($aircraft)) - ->orderByRaw('airline_stand.callsign_slug IS NOT NULL') - ->orderByRaw('LENGTH(airline_stand.callsign_slug) DESC') - ->orderBy('airline_stand.priority'); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->airline($aircraft->airline_id) + ->whereIn('airline_stand.callsign_slug', $this->getCallsignSlugs($aircraft)) + ->orderByRaw('airline_stand.callsign_slug IS NOT NULL') + ->orderByRaw('LENGTH(airline_stand.callsign_slug) DESC'), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php index 43d2cc8aa..81909100c 100644 --- a/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php @@ -4,11 +4,21 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; -class AirlineCallsignSlugTerminalArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineCallsignSlugTerminalArrivalStandAllocator implements ArrivalStandAllocator { + use AppliesOrdering; use UsesCallsignSlugs; + use SelectsFirstApplicableStand; + use SelectsStandsFromAirlineTerminals; + use SelectsFromSizeAppropriateAvailableStands; + use OrdersStandsByCommonConditions; + + private const ORDER_BYS = [ + 'airline_terminal.callsign_slug IS NOT NULL', + 'LENGTH(airline_terminal.callsign_slug) DESC', + 'airline_terminal.priority', + ]; private AirlineService $airlineService; @@ -17,19 +27,39 @@ public function __construct(AirlineService $airlineService) $this->airlineService = $airlineService; } - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder - { - $airline = $this->airlineService->getAirlineForAircraft($aircraft); - if ($airline === null) { + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands at a terminal that is specifically selected for the airline and + * a set of callsign slugs + * - Orders these by the specific callsign slug, descending by length + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions) + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { + // If the aircraft doesnt have an airline, we cant allocate a stand + if ($aircraft->airline_id === null) { return null; } - return $stands->join('terminals', 'terminals.id', '=', 'stands.terminal_id') - ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') - ->where('airline_terminal.airline_id', $airline->id) - ->whereIn('airline_terminal.callsign_slug', $this->getCallsignSlugs($aircraft)) - ->orderByRaw('airline_terminal.callsign_slug IS NOT NULL') - ->orderByRaw('LENGTH(airline_terminal.callsign_slug) DESC') - ->orderBy('airline_terminal.priority'); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->standsAtAirlineTerminals( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->whereIn('airline_terminal.callsign_slug', $this->getCallsignSlugs($aircraft)), + $aircraft + ), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php index b2e29e476..f6027aa51 100644 --- a/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php @@ -4,11 +4,19 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; -class AirlineCallsignTerminalArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineCallsignTerminalArrivalStandAllocator implements ArrivalStandAllocator { + use AppliesOrdering; use UsesCallsignSlugs; + use SelectsFirstApplicableStand; + use SelectsStandsFromAirlineTerminals; + use SelectsFromSizeAppropriateAvailableStands; + use OrdersStandsByCommonConditions; + + private const ORDER_BYS = [ + 'airline_terminal.priority ASC', + ]; private AirlineService $airlineService; @@ -17,17 +25,39 @@ public function __construct(AirlineService $airlineService) $this->airlineService = $airlineService; } - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands at a terminal that is specifically selected for the airline and + * a specific callsign + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions)` + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { - $airline = $this->airlineService->getAirlineForAircraft($aircraft); - if ($airline === null) { + // If the aircraft doesnt have an airline, we cant allocate a stand + if ($aircraft->airline_id === null) { return null; } - return $stands->join('terminals', 'terminals.id', '=', 'stands.terminal_id') - ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') - ->where('airline_terminal.airline_id', $airline->id) - ->where('airline_terminal.full_callsign', $this->getFullCallsignSlug($aircraft)) - ->orderBy('airline_terminal.priority'); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->standsAtAirlineTerminals( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->where('airline_terminal.full_callsign', $this->getFullCallsignSlug($aircraft)), + $aircraft + ), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php index f10882874..06194497c 100644 --- a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php @@ -4,31 +4,55 @@ use App\Allocator\UsesDestinationStrings; use App\Models\Vatsim\NetworkAircraft; -use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; -class AirlineDestinationArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineDestinationArrivalStandAllocator implements ArrivalStandAllocatorInterface { use UsesDestinationStrings; + use OrdersStandsByCommonConditions; + use SelectsFirstApplicableStand; + use SelectsFromSizeAppropriateAvailableStands; + use AppliesOrdering; - private AirlineService $airlineService; + private const ORDER_BYS = [ + 'airline_stand.destination IS NOT NULL', + 'LENGTH(airline_stand.destination) DESC', + 'airline_stand.priority ASC', + ]; - public function __construct(AirlineService $airlineService) + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands that are specifically selected for the airline and a specific set of destinations + * - Orders these by the most specific destination first + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions) + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { - $this->airlineService = $airlineService; - } - - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder - { - $airline = $this->airlineService->getAirlineForAircraft($aircraft); - if ($airline === null) { + // We cant allocate a stand if we don't know the airline + if ($aircraft->airline_id === null) { return null; } - return $stands->with('airlines') - ->airlineDestination($airline, $this->getDestinationStrings($aircraft)) - ->orderByRaw('airline_stand.destination IS NOT NULL') - ->orderByRaw('LENGTH(airline_stand.destination) DESC') - ->orderBy('airline_stand.priority'); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->with('airlines') + ->airlineDestination( + $aircraft->airline_id, + $this->getDestinationStrings($aircraft) + ), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php index 26a31b144..996e79584 100644 --- a/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php @@ -4,33 +4,55 @@ use App\Allocator\UsesDestinationStrings; use App\Models\Vatsim\NetworkAircraft; -use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; -class AirlineDestinationTerminalArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineDestinationTerminalArrivalStandAllocator implements ArrivalStandAllocator { + use AppliesOrdering; use UsesDestinationStrings; + use SelectsFirstApplicableStand; + use SelectsStandsFromAirlineTerminals; + use SelectsFromSizeAppropriateAvailableStands; + use OrdersStandsByCommonConditions; - private AirlineService $airlineService; + private const ORDER_BYS = [ + 'airline_terminal.destination IS NOT NULL', + 'LENGTH(airline_terminal.destination) DESC', + 'airline_terminal.priority ASC', + ]; - public function __construct(AirlineService $airlineService) + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands that are at terminals specifically selected for the airline and a specific set of destinations + * - Orders these by the most specific destination first + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions) + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { - $this->airlineService = $airlineService; - } - - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder - { - $airline = $this->airlineService->getAirlineForAircraft($aircraft); - if ($airline === null) { + // If the aircraft doesnt have an airline, we cant allocate a stand + if ($aircraft->airline_id === null) { return null; } - return $stands->join('terminals', 'terminals.id', '=', 'stands.terminal_id') - ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') - ->where('airline_terminal.airline_id', $airline->id) - ->whereIn('airline_terminal.destination', $this->getDestinationStrings($aircraft)) - ->orderByRaw('airline_terminal.destination IS NOT NULL') - ->orderByRaw('LENGTH(airline_terminal.destination) DESC') - ->orderBy('airline_terminal.priority'); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->standsAtAirlineTerminals( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->whereIn('airline_terminal.destination', $this->getDestinationStrings($aircraft)), + $aircraft + ), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php index 0ac5d3e85..d12f2b50d 100644 --- a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php @@ -3,31 +3,56 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; -use App\Services\AirlineService; use Illuminate\Database\Eloquent\Builder; -class AirlineTerminalArrivalStandAllocator extends AbstractArrivalStandAllocator +class AirlineTerminalArrivalStandAllocator implements ArrivalStandAllocator { - private AirlineService $airlineService; + use AppliesOrdering; + use SelectsFirstApplicableStand; + use SelectsStandsFromAirlineTerminals; + use SelectsFromSizeAppropriateAvailableStands; + use OrdersStandsByCommonConditions; - public function __construct(AirlineService $airlineService) - { - $this->airlineService = $airlineService; - } + private const ORDER_BYS = [ + 'airline_terminal.priority ASC', + ]; - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + /** + * This allocator: + * + * - Selects stands that are size appropriate and available + * - Filters these to stands that are at terminals specifically selected for the airline + * - Filters stands to those that dont have specific conditions + * - Orders these stands by the airline's priority for the stand + * - Orders these stands by the common conditions, minus the general allocation priority + * (see OrdersStandsByCommonConditions) + * - Selects the first stand that pops up + */ + public function allocate(NetworkAircraft $aircraft): ?int { - if (($airline = $this->airlineService->getAirlineForAircraft($aircraft)) === null) { + // If the aircraft doesnt have an airline, we cant allocate a stand + if ($aircraft->airline_id === null) { return null; } - return $stands->join('terminals', 'terminals.id', '=', 'stands.terminal_id') - ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') - ->where('airline_terminal.airline_id', $airline->id) - ->whereNull('airline_terminal.destination') - ->whereNull('airline_terminal.callsign_slug') - ->whereNull('airline_terminal.full_callsign') - ->whereNull('airline_terminal.aircraft_id') - ->orderBy('airline_terminal.priority'); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->standsAtAirlineTerminals( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->whereNull('airline_terminal.destination') + ->whereNull('airline_terminal.callsign_slug') + ->whereNull('airline_terminal.full_callsign') + ->whereNull('airline_terminal.aircraft_id'), + $aircraft + ), + $aircraft + ), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditionsWithoutAssignmentPriority + ) + ) + ); } } diff --git a/app/Allocator/Stand/AppliesOrdering.php b/app/Allocator/Stand/AppliesOrdering.php new file mode 100644 index 000000000..20b75ba51 --- /dev/null +++ b/app/Allocator/Stand/AppliesOrdering.php @@ -0,0 +1,16 @@ + $query->orderByRaw($orderBy), + $stands + ); + } +} diff --git a/app/Allocator/Stand/ArrivalStandAllocator.php b/app/Allocator/Stand/ArrivalStandAllocator.php new file mode 100644 index 000000000..4e5326e9d --- /dev/null +++ b/app/Allocator/Stand/ArrivalStandAllocator.php @@ -0,0 +1,15 @@ +whereHas('stand', function (Builder $standQuery) { + ->whereHas('stand', function (Builder $standQuery) + { $standQuery->unoccupied()->unassigned(); }) ->where('callsign', $aircraft->callsign) @@ -26,7 +27,7 @@ protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircr ->first(); return $reservation - ? Stand::where('stands.id', $reservation->stand_id)->select('stands.*') + ? Stand::where('stands.id', $reservation->stand_id)->first()->id : null; } } diff --git a/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php b/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php index a1af7ce3a..27c7b3407 100644 --- a/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php +++ b/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php @@ -4,15 +4,19 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; /** * A fallback allocator for cargo airlines. Will allocate any * cargo stand to any airline that is type cargo. */ -class CargoAirlineFallbackStandAllocator extends AbstractArrivalStandAllocator +class CargoAirlineFallbackStandAllocator implements ArrivalStandAllocator { use ChecksForCargoAirlines; + use AppliesOrdering; + use OrdersStandsByCommonConditions; + use SelectsFromSizeAppropriateAvailableStands; + use SelectsFirstApplicableStand; + use ConsidersStandRequests; private AirlineService $airlineService; @@ -21,12 +25,27 @@ public function __construct(AirlineService $airlineService) $this->airlineService = $airlineService; } - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + /** + * This allocator: + * + * - Only allocates cargo stands to cargo airlines + * - Orders by common conditions (see OrdersStandsByCommonConditions) + * - Selects the first available stand (see SelectsFirstApplicableStand) + */ + public function allocate(NetworkAircraft $aircraft): ?int { if (!$this->isCargoAirline($aircraft)) { return null; } - return $stands->cargo(); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft), + $aircraft + )->cargo(), + $this->commonOrderByConditions + ) + ); } } diff --git a/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php b/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php index 0449ea232..2b769d99e 100644 --- a/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php +++ b/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php @@ -4,15 +4,19 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; /** * Secondary cargo stand allocator, with no airline preferences. Only concerned with FP remarks explicitly * stating that the flight is cargo - which means a cargo stand should be given. */ -class CargoFlightArrivalStandAllocator extends AbstractArrivalStandAllocator +class CargoFlightArrivalStandAllocator implements ArrivalStandAllocator { use ChecksForCargoAirlines; + use AppliesOrdering; + use OrdersStandsByCommonConditions; + use SelectsFromSizeAppropriateAvailableStands; + use SelectsFirstApplicableStand; + use ConsidersStandRequests; private AirlineService $airlineService; @@ -21,12 +25,27 @@ public function __construct(AirlineService $airlineService) $this->airlineService = $airlineService; } - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + /** + * This allocator: + * + * - Only allocates cargo stands to cargo airlines + * - Orders by common conditions (see OrdersStandsByCommonConditions) + * - Selects the first available stand (see SelectsFirstApplicableStand) + */ + public function allocate(NetworkAircraft $aircraft): ?int { if (!$this->isCargoFlight($aircraft)) { return null; } - return $stands->cargo(); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft), + $aircraft + )->cargo(), + $this->commonOrderByConditions + ) + ); } } diff --git a/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php b/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php index c3fc51f2a..882db5dd4 100644 --- a/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php +++ b/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php @@ -4,7 +4,6 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; -use Illuminate\Database\Eloquent\Builder; /** * The primary arrival stand allocator for cargo. Looks for either a cargo airline @@ -13,9 +12,18 @@ * * This allows airlines that also handle passengers to have stands for their cargo operation. */ -class CargoFlightPreferredArrivalStandAllocator extends AbstractArrivalStandAllocator +class CargoFlightPreferredArrivalStandAllocator implements ArrivalStandAllocator { use ChecksForCargoAirlines; + use AppliesOrdering; + use OrdersStandsByCommonConditions; + use SelectsFromSizeAppropriateAvailableStands; + use SelectsFirstApplicableStand; + use ConsidersStandRequests; + + private const ORDER_BYS = [ + 'airline_stand.priority ASC', + ]; private AirlineService $airlineService; @@ -24,18 +32,27 @@ public function __construct(AirlineService $airlineService) $this->airlineService = $airlineService; } - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + public function allocate(NetworkAircraft $aircraft): ?int { + + // If the aircraft doesnt have an airline, we cant allocate a stand if (!$this->isCargoAirline($aircraft) && !$this->isCargoFlight($aircraft)) { return null; } - if (!($airline = $this->airlineService->getAirlineForAircraft($aircraft))) { - return null; - } + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->airline($aircraft->airline_id), + $aircraft + )->cargo(), + array_merge( + self::ORDER_BYS, + $this->commonOrderByConditions + ) + ) + ); - return $stands->cargo() - ->airline($airline) - ->orderBy('airline_stand.priority'); } } diff --git a/app/Allocator/Stand/CidReservedArrivalStandAllocator.php b/app/Allocator/Stand/CidReservedArrivalStandAllocator.php index 54b120385..b47d11ea5 100644 --- a/app/Allocator/Stand/CidReservedArrivalStandAllocator.php +++ b/app/Allocator/Stand/CidReservedArrivalStandAllocator.php @@ -10,12 +10,13 @@ /** * Matches the network aircraft with a stand reservation based on the pilots CID. */ -class CidReservedArrivalStandAllocator extends AbstractArrivalStandAllocator +class CidReservedArrivalStandAllocator implements ArrivalStandAllocator { - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + public function allocate(NetworkAircraft $aircraft): ?int { $reservation = StandReservation::with('stand') - ->whereHas('stand', function (Builder $standQuery) { + ->whereHas('stand', function (Builder $standQuery) + { $standQuery->unoccupied()->unassigned(); }) ->where('cid', $aircraft->cid) @@ -24,7 +25,7 @@ protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircr ->first(); return $reservation - ? Stand::where('stands.id', $reservation->stand_id)->select('stands.*') + ? Stand::where('stands.id', $reservation->stand_id)->first()->id : null; } } diff --git a/app/Allocator/Stand/ConsidersStandRequests.php b/app/Allocator/Stand/ConsidersStandRequests.php new file mode 100644 index 000000000..801758267 --- /dev/null +++ b/app/Allocator/Stand/ConsidersStandRequests.php @@ -0,0 +1,33 @@ +leftJoin('stand_requests as other_stand_requests', function (JoinClause $join) use ($aircraft) + { + // Prefer stands that haven't been requested by someone else + $join->on('stands.id', '=', 'other_stand_requests.stand_id') + ->on('other_stand_requests.user_id', '<>', $join->raw($aircraft->cid)) + ->on( + 'other_stand_requests.requested_time', + '>', + $join->raw( + sprintf( + '\'%s\'', + Carbon::now()->subMinutes( + config('vatsim-connect.stand_request_expiry_minutes') + )->toDateTimeString() + ) + ) + ); + }); + } +} diff --git a/app/Allocator/Stand/DomesticInternationalStandAllocator.php b/app/Allocator/Stand/DomesticInternationalStandAllocator.php index ca31ce8a1..2c2a8451c 100644 --- a/app/Allocator/Stand/DomesticInternationalStandAllocator.php +++ b/app/Allocator/Stand/DomesticInternationalStandAllocator.php @@ -6,15 +6,32 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Str; -class DomesticInternationalStandAllocator extends AbstractArrivalStandAllocator +class DomesticInternationalStandAllocator implements ArrivalStandAllocator { - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + use AppliesOrdering; + use OrdersStandsByCommonConditions; + use SelectsFromSizeAppropriateAvailableStands; + use SelectsFirstApplicableStand; + use ConsidersStandRequests; + + public function allocate(NetworkAircraft $aircraft): ?int { if (!$aircraft->planned_depairport) { return null; } - return $this->getDomesticInternationalScope($aircraft, $stands); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->getDomesticInternationalScope( + $aircraft, + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ), + $aircraft + ), + $this->commonOrderByConditions + ) + ); } protected function getDomesticInternationalScope(NetworkAircraft $aircraft, Builder $builder): Builder diff --git a/app/Allocator/Stand/FallbackArrivalStandAllocator.php b/app/Allocator/Stand/FallbackArrivalStandAllocator.php index fa4614d67..13739335f 100644 --- a/app/Allocator/Stand/FallbackArrivalStandAllocator.php +++ b/app/Allocator/Stand/FallbackArrivalStandAllocator.php @@ -3,12 +3,35 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; -use Illuminate\Database\Eloquent\Builder; -class FallbackArrivalStandAllocator extends AbstractArrivalStandAllocator +class FallbackArrivalStandAllocator implements ArrivalStandAllocator { - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + use AppliesOrdering; + use OrdersStandsByCommonConditions; + use SelectsFromSizeAppropriateAvailableStands; + use SelectsFirstApplicableStand; + use ConsidersStandRequests; + + /** + * This allocator: + * + * - Only allocates stands that are not cargo + * - Orders by common conditions (see OrdersStandsByCommonConditions) + * - Selects the first available stand (see SelectsFirstApplicableStand) + * + * @param NetworkAircraft $aircraft + * @return integer|null + */ + public function allocate(NetworkAircraft $aircraft): ?int { - return $stands->notCargo(); + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft)->notCargo(), + $aircraft + ), + $this->commonOrderByConditions + ) + ); } } diff --git a/app/Allocator/Stand/OrdersStandsByCommonConditions.php b/app/Allocator/Stand/OrdersStandsByCommonConditions.php new file mode 100644 index 000000000..13e4409df --- /dev/null +++ b/app/Allocator/Stand/OrdersStandsByCommonConditions.php @@ -0,0 +1,28 @@ +whereIn('origin_slug', $this->getDestinationStrings($aircraft)) - ->orderByRaw('origin_slug IS NOT NULL') - ->orderByRaw('LENGTH(origin_slug) DESC'); + if (!$aircraft->planned_depairport) { + return null; + } + + return $this->selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ->whereIn('origin_slug', $this->getDestinationStrings($aircraft)) + ->notCargo(), + $aircraft + ), + array_merge(self::ORDER_BYS, $this->commonOrderByConditions) + ) + ); } } diff --git a/app/Allocator/Stand/SelectsFirstApplicableStand.php b/app/Allocator/Stand/SelectsFirstApplicableStand.php new file mode 100644 index 000000000..818c1592a --- /dev/null +++ b/app/Allocator/Stand/SelectsFirstApplicableStand.php @@ -0,0 +1,12 @@ +first()?->id; + } +} diff --git a/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php b/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php new file mode 100644 index 000000000..42b0aacb0 --- /dev/null +++ b/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php @@ -0,0 +1,24 @@ +where('code', $aircraft->planned_destairport); + }) + ->sizeAppropriate($aircraft->aircraft) + ->available() + ->select('stands.*'); + } +} diff --git a/app/Allocator/Stand/SelectsStandsFromAirlineTerminals.php b/app/Allocator/Stand/SelectsStandsFromAirlineTerminals.php new file mode 100644 index 000000000..35d3f7e3e --- /dev/null +++ b/app/Allocator/Stand/SelectsStandsFromAirlineTerminals.php @@ -0,0 +1,22 @@ +join('terminals', 'terminals.id', '=', 'stands.terminal_id') + ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') + ->where('airline_terminal.airline_id', $aircraft->airline_id); + } +} diff --git a/app/Allocator/Stand/UserRequestedArrivalStandAllocator.php b/app/Allocator/Stand/UserRequestedArrivalStandAllocator.php index 42d04061b..b580d8737 100644 --- a/app/Allocator/Stand/UserRequestedArrivalStandAllocator.php +++ b/app/Allocator/Stand/UserRequestedArrivalStandAllocator.php @@ -2,19 +2,26 @@ namespace App\Allocator\Stand; +use App\Models\Stand\Stand; use App\Models\Stand\StandRequest; use App\Models\Vatsim\NetworkAircraft; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; -class UserRequestedArrivalStandAllocator extends AbstractArrivalStandAllocator +class UserRequestedArrivalStandAllocator implements ArrivalStandAllocator { - protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircraft): ?Builder + use SelectsFirstApplicableStand; + + public function allocate(NetworkAircraft $aircraft): ?int { $requestedStands = StandRequest::where('user_id', $aircraft->cid) - ->whereHas('stand.airfield', function (Builder $airfield) use ($aircraft) { + ->whereHas('stand.airfield', function (Builder $airfield) use ($aircraft) + { $airfield->where('code', $aircraft->planned_destairport); }) + ->whereHas('stand', function (Builder $standQuery) + { + $standQuery->unoccupied()->unassigned(); + }) ->current() ->get(); @@ -22,11 +29,8 @@ protected function getOrderedStandsQuery(Builder $stands, NetworkAircraft $aircr return null; } - return $stands->whereIn('stands.id', $requestedStands->pluck('stand_id')); - } - - protected function prefersNonRequestedStands(): bool - { - return false; + return $this->selectFirstStand( + Stand::whereIn('id', $requestedStands->pluck('stand_id')) + ); } } diff --git a/app/Models/Stand/Stand.php b/app/Models/Stand/Stand.php index 18b27dcc7..b33d32a49 100644 --- a/app/Models/Stand/Stand.php +++ b/app/Models/Stand/Stand.php @@ -126,10 +126,10 @@ public function scopeAvailable(Builder $builder): Builder return $this->scopeNotClosed($this->scopeNotReserved($this->scopeUnassigned($this->scopeUnoccupied($builder)))); } - public function scopeAirline(Builder $builder, Airline $airline): Builder + public function scopeAirline(Builder $builder, Airline|int $airline): Builder { return $builder->join('airline_stand', 'stands.id', '=', 'airline_stand.stand_id') - ->where('airline_stand.airline_id', $airline->id) + ->where('airline_stand.airline_id', is_int($airline) ? $airline : $airline->id) ->where( function (Builder $query) { // Timezones here should be local because Heathrow. @@ -140,7 +140,7 @@ function (Builder $query) { ); } - public function scopeAirlineDestination(Builder $builder, Airline $airline, array $destinationStrings): Builder + public function scopeAirlineDestination(Builder $builder, Airline|int $airline, array $destinationStrings): Builder { return $this->scopeAirline($builder, $airline)->whereIn('destination', $destinationStrings); } diff --git a/app/Models/Vatsim/NetworkAircraft.php b/app/Models/Vatsim/NetworkAircraft.php index f18d0fb6d..373cf1ff4 100644 --- a/app/Models/Vatsim/NetworkAircraft.php +++ b/app/Models/Vatsim/NetworkAircraft.php @@ -2,6 +2,7 @@ namespace App\Models\Vatsim; +use App\Models\Aircraft\Aircraft; use App\Models\Airfield\Airfield; use App\Models\Hold\NavaidNetworkAircraft; use App\Models\Navigation\Navaid; diff --git a/app/Services/AirlineService.php b/app/Services/AirlineService.php index cd9afbe52..8ee27941e 100644 --- a/app/Services/AirlineService.php +++ b/app/Services/AirlineService.php @@ -21,15 +21,23 @@ public function getAirlineForAircraft(NetworkAircraft $aircraft): ?Airline public function airlineIdForCallsign(string $callsign): ?int { - return $this->airlineCodeIdMap()[Str::substr($callsign, 0, 3)] ?? null; + return $this->airlineCodeIdMap()[$this->airlineCodeForCallsign($callsign)] ?? null; } - public function getCallsignSlugForAircraft(NetworkAircraft $aircraft): string + public function getCallsignSlugForAircraft(NetworkAircraft|string $aircraft): string { - $airline = $this->getAirlineForAircraft($aircraft); - return $airline - ? Str::substr($aircraft->callsign, 3) - : $aircraft->callsign; + $callsign = $aircraft instanceof NetworkAircraft + ? $aircraft->callsign + : $aircraft; + + return $this->airlineIdForCallsign($callsign) === null + ? $callsign + : Str::substr($callsign, 3); + } + + private function airlineCodeForCallsign(string $callsign): string + { + return Str::substr($callsign, 0, 3); } public function airlinesUpdated() diff --git a/app/Services/Stand/ArrivalAllocationService.php b/app/Services/Stand/ArrivalAllocationService.php index cfc7813e1..06c5d5b53 100644 --- a/app/Services/Stand/ArrivalAllocationService.php +++ b/app/Services/Stand/ArrivalAllocationService.php @@ -2,6 +2,7 @@ namespace App\Services\Stand; +use App\Allocator\Stand\ArrivalStandAllocator; use App\Models\Airfield\Airfield; use App\Models\Stand\StandAssignment; use App\Models\Vatsim\NetworkAircraft; @@ -19,7 +20,7 @@ class ArrivalAllocationService private readonly StandAssignmentsService $assignmentsService; /** - * @var ArrivalStandAllocatorInterface[] + * @var ArrivalStandAllocator[] */ private readonly array $allocators; @@ -44,7 +45,8 @@ private function deleteAssignmentsForAircraftWithChangedDestination(): void ->whereRaw('airfield.code <> network_aircraft.planned_depairport') ->select('stand_assignments.*') ->get() - ->each(function (StandAssignment $standAssignment) { + ->each(function (StandAssignment $standAssignment) + { $this->assignmentsService->deleteStandAssignment($standAssignment); }); } @@ -55,11 +57,16 @@ private function deleteAssignmentsForAircraftWithChangedDestination(): void private function allocateStandsForArrivingAircraft(): void { $this->getAircraftThatCanHaveArrivalStandsAllocated() - ->filter(fn (NetworkAircraft $aircraft) => $this->aircraftWithAssignmentDistance($aircraft)) - ->each(function (NetworkAircraft $aircraft) { + ->filter(fn(NetworkAircraft $aircraft) => $this->aircraftWithAssignmentDistance($aircraft)) + ->each(function (NetworkAircraft $aircraft) + { foreach ($this->allocators as $allocator) { if ($allocation = $allocator->allocate($aircraft)) { - $this->assignmentsService->createStandAssignment($aircraft->callsign, $allocation, get_class($allocator)); + $this->assignmentsService->createStandAssignment( + $aircraft->callsign, + $allocation, + get_class($allocator) + ); return; } } @@ -114,7 +121,7 @@ private function getTimeFromAirfieldInMinutes(NetworkAircraft $aircraft, Airfiel ); $groundspeed = $aircraft->groundspeed === 0 ? 1 : $aircraft->groundspeed; - return (float)($distanceToAirfieldInNm / $groundspeed) * 60.0; + return (float) ($distanceToAirfieldInNm / $groundspeed) * 60.0; } public function getAllocators(): array diff --git a/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php index 9e5fa4603..ad8c93f48 100644 --- a/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php @@ -4,6 +4,7 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Vatsim\NetworkAircraft; use Illuminate\Support\Facades\DB; @@ -16,6 +17,7 @@ public function setUp(): void { parent::setUp(); $this->allocator = $this->app->make(AirlineAircraftArrivalStandAllocator::class); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandWithAnAircraftType() @@ -307,19 +309,46 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntAllocateUnknownAircraftTypes() + { + DB::table('airline_stand')->insert( + [ + [ + 'airline_id' => 1, + 'stand_id' => 3, + 'aircraft_id' => 1 + ], + [ + 'airline_id' => 2, + 'stand_id' => 1, + 'aircraft_id' => 1 + ], + ] + ); + $aircraft = $this->createAircraft('***1234', 'EGLL', 'EGGD', aircraftType: 'XXX'); + $this->assertNull($this->allocator->allocate($aircraft)); + } + private function createAircraft( string $callsign, string $arrivalAirport, - string $departureAirport + string $departureAirport, + string $aircraftType = 'B738' ): NetworkAircraft { return NetworkAircraft::create( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php index af606a5d4..abc8a306e 100644 --- a/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php @@ -5,6 +5,7 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; use App\Models\Airfield\Terminal; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Vatsim\NetworkAircraft; use Illuminate\Support\Facades\DB; @@ -17,6 +18,7 @@ public function setUp(): void { parent::setUp(); $this->allocator = $this->app->make(AirlineAircraftTerminalArrivalStandAllocator::class); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandWithAFixedCallsign() @@ -249,7 +251,7 @@ public function testItDoesntAllocateAStandWithNoAircraftType() ] ); - $aircraft = $this->createAircraft('BAW23451', 'EGLL', 'EGGD'); + $aircraft = $this->createAircraft('BAW23451', 'EGLL', 'EGGD', aircraftType: 'XXX'); $this->assertNull($this->allocator->allocate($aircraft)); } @@ -338,16 +340,26 @@ public function testItDoesntAllocateNonExistentAirlines() private function createAircraft( string $callsign, string $arrivalAirport, - string $departureAirport + string $departureAirport, + string $aircraftType = 'B738' ): NetworkAircraft { return NetworkAircraft::create( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + default => null, + }, + 'aircraft_id' => match ($aircraftType) { + 'B738' => 1, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php index de7b4ff6d..84ee9a441 100644 --- a/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php @@ -4,6 +4,7 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Vatsim\NetworkAircraft; use Illuminate\Support\Facades\DB; @@ -16,6 +17,7 @@ public function setUp(): void { parent::setUp(); $this->allocator = $this->app->make(AirlineArrivalStandAllocator::class); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandForTheAirline() @@ -317,15 +319,29 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } - private function createAircraft(string $callsign, string $arrivalAirport): NetworkAircraft - { + private function createAircraft( + string $callsign, + string $arrivalAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { return NetworkAircraft::create( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', - 'planned_destairport' => $arrivalAirport] + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, + 'planned_destairport' => $arrivalAirport, + 'planned_depairport' => 'EGGD', + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + default => null, + }, + 'aircraft_id' => match ($aircraftType) { + 'B738' => 1, + default => null, + }, + ] ); } } diff --git a/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php index 071ef1c90..e51d5b13b 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php @@ -4,6 +4,7 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Vatsim\NetworkAircraft; use Illuminate\Support\Facades\DB; @@ -16,6 +17,7 @@ public function setUp(): void { parent::setUp(); $this->allocator = $this->app->make(AirlineCallsignArrivalStandAllocator::class); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandWithAFixedCallsign() @@ -330,16 +332,23 @@ public function testItDoesntAllocateNonExistentAirlines() private function createAircraft( string $callsign, string $arrivalAirport, - string $departureAirport + string $departureAirport, + string $aircraftType = 'B738' ): NetworkAircraft { return NetworkAircraft::create( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineCallsignSlugStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php similarity index 95% rename from tests/app/Allocator/Stand/AirlineCallsignSlugStandAllocatorTest.php rename to tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php index 61cdbdb01..7de96ad6b 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignSlugStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php @@ -4,16 +4,20 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Vatsim\NetworkAircraft; use Illuminate\Support\Facades\DB; -class AirlineCallsignSlugStandAllocatorTest extends BaseFunctionalTestCase +class AirlineCallsignSlugArrivalStandAllocatorTest extends BaseFunctionalTestCase { + private readonly AirlineCallsignSlugArrivalStandAllocator $allocator; + public function setUp(): void { parent::setUp(); $this->allocator = $this->app->make(AirlineCallsignSlugArrivalStandAllocator::class); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandWithAFixedCallsignSlug() @@ -478,16 +482,23 @@ public function testItDoesntAllocateNonExistentAirlines() private function createAircraft( string $callsign, string $arrivalAirport, - string $departureAirport + string $departureAirport, + string $aircraftType = 'B738' ): NetworkAircraft { return NetworkAircraft::create( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.php index 6b0451cf9..d622af440 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.php @@ -5,6 +5,7 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; use App\Models\Airfield\Terminal; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Vatsim\NetworkAircraft; use Illuminate\Support\Facades\DB; @@ -17,6 +18,7 @@ public function setUp(): void { parent::setUp(); $this->allocator = $this->app->make(AirlineCallsignSlugTerminalArrivalStandAllocator::class); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandWithAFixedCallsignSlug() @@ -504,16 +506,23 @@ public function testItDoesntAllocateNonExistentAirlines() private function createAircraft( string $callsign, string $arrivalAirport, - string $departureAirport + string $departureAirport, + string $aircraftType = 'B738' ): NetworkAircraft { return NetworkAircraft::create( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocatorTest.php index 4bb3748e5..80791cc14 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocatorTest.php @@ -357,16 +357,22 @@ public function testItDoesntAllocateNonExistentAirlines() private function createAircraft( string $callsign, string $arrivalAirport, - string $departureAirport + string $departureAirport, + string $aircraftType = 'B738' ): NetworkAircraft { return NetworkAircraft::create( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php index 161bdc6f4..216986109 100644 --- a/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php @@ -484,8 +484,7 @@ private function createAircraft( string $callsign, string $arrivalAirport, string $departureAirport - ): NetworkAircraft - { + ): NetworkAircraft { return NetworkAircraft::create( [ 'callsign' => $callsign, @@ -494,6 +493,8 @@ private function createAircraft( 'planned_aircraft_short' => 'B738', 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'airline_id' => 1, + 'aircraft_id' => 1, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php index 7bd9c45e3..a0da1722b 100644 --- a/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php @@ -8,6 +8,7 @@ use App\Models\Stand\Stand; use App\Models\Vatsim\NetworkAircraft; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; class AirlineDestinationTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase { @@ -514,6 +515,8 @@ private function createAircraft( 'planned_aircraft_short' => 'B738', 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'airline_id' => Str::substr($callsign, 0, 3) === 'BAW' ? 1 : null, + 'aircraft_id' => 1, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php index dc0abca00..faf36ef22 100644 --- a/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php @@ -218,7 +218,8 @@ public function testItDoesntAllocateNonExistentAirlines() public function testItReturnsNullOnNoStandAllocated() { - Stand::all()->each(function (Stand $stand) { + Stand::all()->each(function (Stand $stand) + { $stand->delete(); }); $aircraft = $this->createAircraft('BAW999', 'EGLL'); @@ -238,6 +239,8 @@ private function createAircraft( 'planned_aircraft_short' => 'B738', 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'airline_id' => $callsign === 'BAW23451' ? 1 : null, + 'aircraft_id' => 1, ] ); } diff --git a/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php b/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php index 1e93a14e6..9aab8749b 100644 --- a/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php @@ -126,6 +126,7 @@ private function createAircraft( 'planned_aircraft' => 'B744', 'planned_aircraft_short' => 'B744', 'planned_destairport' => $arrivalAirport, + 'aircraft_id' => Aircraft::where('code', 'B744')->first()->id, ] ); } diff --git a/tests/app/Allocator/Stand/CargoFlightArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/CargoFlightArrivalStandAllocatorTest.php index ff2f5859b..f5e1c073e 100644 --- a/tests/app/Allocator/Stand/CargoFlightArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/CargoFlightArrivalStandAllocatorTest.php @@ -125,6 +125,7 @@ private function createAircraft( 'planned_aircraft' => 'B744', 'planned_aircraft_short' => 'B744', 'planned_destairport' => $arrivalAirport, + 'aircraft_id' => Aircraft::where('code', 'B744')->first()->id, ] ); } diff --git a/tests/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocatorTest.php index a5660f33c..90d881b0b 100644 --- a/tests/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocatorTest.php @@ -102,7 +102,7 @@ public function testItReturnsNothingIfNoStandsToAllocated() public function testItDoesntAllocateOccupiedStands() { - StandAssignment::create( + StandAssignment::create( [ 'callsign' => 'BAW123', 'stand_id' => $this->cargoStand->id @@ -137,6 +137,8 @@ private function createAircraft( 'planned_aircraft' => 'B744', 'planned_aircraft_short' => 'B744', 'planned_destairport' => $arrivalAirport, + 'airline_id' => Airline::where('icao_code', 'VIR')->first()->id, + 'aircraft_id' => Aircraft::where('code', 'B744')->first()->id, ] ); } diff --git a/tests/app/Allocator/Stand/DomesticInternationalStandAllocatorTest.php b/tests/app/Allocator/Stand/DomesticInternationalStandAllocatorTest.php index 6341aeb93..c72021da6 100644 --- a/tests/app/Allocator/Stand/DomesticInternationalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/DomesticInternationalStandAllocatorTest.php @@ -167,16 +167,7 @@ public function testItDoesntAllocateTakenStands() public function testItReturnsNothingOnNoDestinationAirport() { - $aircraft = NetworkAircraft::create( - [ - 'callsign' => 'BAW898', - 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', - 'planned_destairport' => '', - 'planned_depairport' => 'EIDW', - ] - ); + $aircraft = $this->createAircraft('BAW898', 'B738', ''); $this->assertNull($this->allocator->allocate($aircraft)); } @@ -199,6 +190,7 @@ private function createAircraft( 'planned_aircraft_short' => $type, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'aircraft_id' => Aircraft::where('code', $type)->first()->id, ] ); } diff --git a/tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php index b89aca3a7..413053a11 100644 --- a/tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php @@ -235,6 +235,7 @@ private function createAircraft( 'planned_aircraft' => $type, 'planned_aircraft_short' => $type, 'planned_destairport' => $arrivalAirport, + 'aircraft_id' => Aircraft::where('code', $type)->first()?->id, ] ); } diff --git a/tests/app/Allocator/Stand/OriginAirfieldStandAllocatorTest.php b/tests/app/Allocator/Stand/OriginAirfieldStandAllocatorTest.php index 75e830fbe..6fd3ba6d7 100644 --- a/tests/app/Allocator/Stand/OriginAirfieldStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/OriginAirfieldStandAllocatorTest.php @@ -266,6 +266,7 @@ private function createAircraft( 'planned_aircraft_short' => 'B738', 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'aircraft_id' => 1, ] ); } From 425504306be071511b69f8cdab27b445c931c555 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Sat, 2 Sep 2023 17:21:30 +0000 Subject: [PATCH 06/24] refactor: query re-use --- .../AirlineAircraftArrivalStandAllocator.php | 37 +++--------- ...eAircraftTerminalArrivalStandAllocator.php | 31 ++-------- .../Stand/AirlineArrivalStandAllocator.php | 33 +++-------- .../AirlineCallsignArrivalStandAllocator.php | 26 ++------- ...rlineCallsignSlugArrivalStandAllocator.php | 26 ++------- ...lsignSlugTerminalArrivalStandAllocator.php | 30 +++------- ...eCallsignTerminalArrivalStandAllocator.php | 29 ++-------- ...irlineDestinationArrivalStandAllocator.php | 32 ++++------- ...stinationTerminalArrivalStandAllocator.php | 33 ++++------- .../AirlineTerminalArrivalStandAllocator.php | 34 +++-------- .../Stand/ArrivalStandAllocatorInterface.php | 13 ----- .../CargoAirlineFallbackStandAllocator.php | 18 ++---- .../CargoFlightArrivalStandAllocator.php | 19 ++----- ...goFlightPreferredArrivalStandAllocator.php | 28 ++------- .../DomesticInternationalStandAllocator.php | 20 ++----- .../Stand/FallbackArrivalStandAllocator.php | 19 ++----- .../Stand/OriginAirfieldStandAllocator.php | 21 ++----- .../SelectsFromAirlineSpecificStands.php | 51 +++++++++++++++++ ...ectsStandsFromAirlineSpecificTerminals.php | 57 +++++++++++++++++++ .../SelectsStandsFromAirlineTerminals.php | 22 ------- .../SelectsStandsUsingStandardConditions.php | 43 ++++++++++++++ app/Models/Stand/Stand.php | 5 -- tests/app/Models/Stand/StandTest.php | 29 ---------- .../Stand/ArrivalAllocationServiceTest.php | 4 +- 24 files changed, 258 insertions(+), 402 deletions(-) delete mode 100644 app/Allocator/Stand/ArrivalStandAllocatorInterface.php create mode 100644 app/Allocator/Stand/SelectsFromAirlineSpecificStands.php create mode 100644 app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php delete mode 100644 app/Allocator/Stand/SelectsStandsFromAirlineTerminals.php create mode 100644 app/Allocator/Stand/SelectsStandsUsingStandardConditions.php diff --git a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php index 60b608ef8..19a8bdf4f 100644 --- a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php @@ -3,27 +3,15 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Database\Eloquent\Builder; class AirlineAircraftArrivalStandAllocator implements ArrivalStandAllocator { - use SelectsFromSizeAppropriateAvailableStands; - use SelectsFirstApplicableStand; - use OrdersStandsByCommonConditions; - use AppliesOrdering; - - private const ORDER_BYS = [ - 'airline_stand.priority ASC', - ]; + use SelectsFromAirlineSpecificStands; /** - * This allocator: - * - * - Selects stands that are size appropriate and available - * - Filters these to stands that are specifically selected for the airline AND a given aircraft type - * - Orders these stands by the airline's priority for the stand - * - Orders these stands by the common conditions, minus the general allocation priority - * (see OrdersStandsByCommonConditions) - * - Selects the first stand that pops up + * This allocator uses the standard SelectsFromAirlineSpecificStands trait to generate a stand query, + * with additional filters that only stands for a specific aircraft type are selected. */ public function allocate(NetworkAircraft $aircraft): ?int { @@ -32,20 +20,9 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->airline($aircraft->airline_id) - ->where('airline_stand.aircraft_id', $aircraft->aircraft_id), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectAirlineSpecificStands( + $aircraft, + fn(Builder $query) => $query->where('airline_stand.aircraft_id', $aircraft->aircraft_id), ); } } diff --git a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php index 9151deeea..be320c799 100644 --- a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php @@ -3,20 +3,13 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Database\Eloquent\Builder; class AirlineAircraftTerminalArrivalStandAllocator implements ArrivalStandAllocator { - use OrdersStandsByCommonConditions; - use SelectsFirstApplicableStand; - use SelectsFromSizeAppropriateAvailableStands; - use SelectsStandsFromAirlineTerminals; - use AppliesOrdering; + use SelectsStandsFromAirlineSpecificTerminals; - private const ORDER_BYS = [ - 'airline_terminal.priority ASC', - ]; - - /** + /* * This allocator: * * - Selects stands that are size appropriate and available @@ -33,21 +26,9 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->standsAtAirlineTerminals( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->where('airline_terminal.aircraft_id', $aircraft->aircraft_id), - $aircraft - ), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectStandsAtAirlineSpecificTerminals( + $aircraft, + fn(Builder $query) => $query->where('airline_terminal.aircraft_id', $aircraft->aircraft_id) ); } } diff --git a/app/Allocator/Stand/AirlineArrivalStandAllocator.php b/app/Allocator/Stand/AirlineArrivalStandAllocator.php index 058bc0529..116db4ccb 100644 --- a/app/Allocator/Stand/AirlineArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineArrivalStandAllocator.php @@ -3,17 +3,11 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Database\Eloquent\Builder; class AirlineArrivalStandAllocator implements ArrivalStandAllocator { - use AppliesOrdering; - use SelectsFirstApplicableStand; - use SelectsFromSizeAppropriateAvailableStands; - use OrdersStandsByCommonConditions; - - private const ORDER_BYS = [ - 'airline_stand.priority ASC', - ]; + use SelectsFromAirlineSpecificStands; /** * This allocator: @@ -32,23 +26,12 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->airline($aircraft->airline_id) - ->whereNull('airline_stand.destination') - ->whereNull('airline_stand.callsign_slug') - ->whereNull('airline_stand.full_callsign') - ->whereNull('airline_stand.aircraft_id'), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectAirlineSpecificStands( + $aircraft, + fn(Builder $query) => $query->whereNull('airline_stand.destination') + ->whereNull('airline_stand.callsign_slug') + ->whereNull('airline_stand.full_callsign') + ->whereNull('airline_stand.aircraft_id'), ); } } diff --git a/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php index 1f2bede7a..71dc62e79 100644 --- a/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php @@ -4,18 +4,12 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Illuminate\Database\Eloquent\Builder; class AirlineCallsignArrivalStandAllocator implements ArrivalStandAllocator { - use AppliesOrdering; + use SelectsFromAirlineSpecificStands; use UsesCallsignSlugs; - use SelectsFirstApplicableStand; - use SelectsFromSizeAppropriateAvailableStands; - use OrdersStandsByCommonConditions; - - private const ORDER_BYS = [ - 'airline_stand.priority ASC', - ]; private readonly AirlineService $airlineService; @@ -41,19 +35,9 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->airline($aircraft->airline_id) - ->where('airline_stand.full_callsign', $this->getFullCallsignSlug($aircraft)), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectAirlineSpecificStands( + $aircraft, + fn(Builder $query) => $query->where('airline_stand.full_callsign', $this->getFullCallsignSlug($aircraft)), ); } } diff --git a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php index 29f08ca58..dee23bd61 100644 --- a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php @@ -4,19 +4,16 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Illuminate\Database\Eloquent\Builder; class AirlineCallsignSlugArrivalStandAllocator implements ArrivalStandAllocator { - use AppliesOrdering; + use SelectsFromAirlineSpecificStands; use UsesCallsignSlugs; - use SelectsFirstApplicableStand; - use SelectsFromSizeAppropriateAvailableStands; - use OrdersStandsByCommonConditions; private const ORDER_BYS = [ 'airline_stand.callsign_slug IS NOT NULL', 'LENGTH(airline_stand.callsign_slug) DESC', - 'airline_stand.priority ASC', ]; private AirlineService $airlineService; @@ -43,21 +40,10 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->airline($aircraft->airline_id) - ->whereIn('airline_stand.callsign_slug', $this->getCallsignSlugs($aircraft)) - ->orderByRaw('airline_stand.callsign_slug IS NOT NULL') - ->orderByRaw('LENGTH(airline_stand.callsign_slug) DESC'), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectAirlineSpecificStands( + $aircraft, + fn(Builder $query) => $query->whereIn('airline_stand.callsign_slug', $this->getCallsignSlugs($aircraft)), + self::ORDER_BYS ); } } diff --git a/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php index 81909100c..68ff7ffb7 100644 --- a/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php @@ -4,20 +4,16 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Illuminate\Database\Eloquent\Builder; class AirlineCallsignSlugTerminalArrivalStandAllocator implements ArrivalStandAllocator { - use AppliesOrdering; use UsesCallsignSlugs; - use SelectsFirstApplicableStand; - use SelectsStandsFromAirlineTerminals; - use SelectsFromSizeAppropriateAvailableStands; - use OrdersStandsByCommonConditions; + use SelectsStandsFromAirlineSpecificTerminals; private const ORDER_BYS = [ 'airline_terminal.callsign_slug IS NOT NULL', 'LENGTH(airline_terminal.callsign_slug) DESC', - 'airline_terminal.priority', ]; private AirlineService $airlineService; @@ -39,27 +35,17 @@ public function __construct(AirlineService $airlineService) * (see OrdersStandsByCommonConditions) * - Selects the first stand that pops up */ - public function allocate(NetworkAircraft $aircraft): ?int { + public function allocate(NetworkAircraft $aircraft): ?int + { // If the aircraft doesnt have an airline, we cant allocate a stand if ($aircraft->airline_id === null) { return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->standsAtAirlineTerminals( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->whereIn('airline_terminal.callsign_slug', $this->getCallsignSlugs($aircraft)), - $aircraft - ), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectStandsAtAirlineSpecificTerminals( + $aircraft, + fn(Builder $query) => $query->whereIn('airline_terminal.callsign_slug', $this->getCallsignSlugs($aircraft)), + self::ORDER_BYS ); } } diff --git a/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php index f6027aa51..a3a864196 100644 --- a/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php @@ -4,19 +4,12 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Illuminate\Database\Eloquent\Builder; class AirlineCallsignTerminalArrivalStandAllocator implements ArrivalStandAllocator { - use AppliesOrdering; use UsesCallsignSlugs; - use SelectsFirstApplicableStand; - use SelectsStandsFromAirlineTerminals; - use SelectsFromSizeAppropriateAvailableStands; - use OrdersStandsByCommonConditions; - - private const ORDER_BYS = [ - 'airline_terminal.priority ASC', - ]; + use SelectsStandsFromAirlineSpecificTerminals; private AirlineService $airlineService; @@ -43,21 +36,9 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->standsAtAirlineTerminals( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->where('airline_terminal.full_callsign', $this->getFullCallsignSlug($aircraft)), - $aircraft - ), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectStandsAtAirlineSpecificTerminals( + $aircraft, + fn(Builder $query) => $query->where('airline_terminal.full_callsign', $this->getFullCallsignSlug($aircraft)) ); } } diff --git a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php index 06194497c..953731452 100644 --- a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php @@ -4,19 +4,16 @@ use App\Allocator\UsesDestinationStrings; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Database\Eloquent\Builder; -class AirlineDestinationArrivalStandAllocator implements ArrivalStandAllocatorInterface +class AirlineDestinationArrivalStandAllocator implements ArrivalStandAllocator { use UsesDestinationStrings; - use OrdersStandsByCommonConditions; - use SelectsFirstApplicableStand; - use SelectsFromSizeAppropriateAvailableStands; - use AppliesOrdering; + use SelectsFromAirlineSpecificStands; private const ORDER_BYS = [ 'airline_stand.destination IS NOT NULL', 'LENGTH(airline_stand.destination) DESC', - 'airline_stand.priority ASC', ]; /** @@ -37,22 +34,13 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->with('airlines') - ->airlineDestination( - $aircraft->airline_id, - $this->getDestinationStrings($aircraft) - ), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectAirlineSpecificStands( + $aircraft, + fn(Builder $query) => $query->whereIn( + 'airline_stand.destination', + $this->getDestinationStrings($aircraft) + ), + self::ORDER_BYS ); } } diff --git a/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php index 996e79584..5e428f95f 100644 --- a/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php @@ -4,27 +4,24 @@ use App\Allocator\UsesDestinationStrings; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Database\Eloquent\Builder; class AirlineDestinationTerminalArrivalStandAllocator implements ArrivalStandAllocator { - use AppliesOrdering; use UsesDestinationStrings; - use SelectsFirstApplicableStand; - use SelectsStandsFromAirlineTerminals; - use SelectsFromSizeAppropriateAvailableStands; - use OrdersStandsByCommonConditions; + use SelectsStandsFromAirlineSpecificTerminals; private const ORDER_BYS = [ 'airline_terminal.destination IS NOT NULL', 'LENGTH(airline_terminal.destination) DESC', - 'airline_terminal.priority ASC', ]; /** * This allocator: * * - Selects stands that are size appropriate and available - * - Filters these to stands that are at terminals specifically selected for the airline and a specific set of destinations + * - Filters these to stands that are at terminals specifically selected for the airline and a + * specific set of destinations * - Orders these by the most specific destination first * - Orders these stands by the airline's priority for the stand * - Orders these stands by the common conditions, minus the general allocation priority @@ -38,21 +35,13 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->standsAtAirlineTerminals( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->whereIn('airline_terminal.destination', $this->getDestinationStrings($aircraft)), - $aircraft - ), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectStandsAtAirlineSpecificTerminals( + $aircraft, + fn(Builder $query) => $query->whereIn( + 'airline_terminal.destination', + $this->getDestinationStrings($aircraft) + ), + self::ORDER_BYS ); } } diff --git a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php index d12f2b50d..9e6f0dd82 100644 --- a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php @@ -7,15 +7,7 @@ class AirlineTerminalArrivalStandAllocator implements ArrivalStandAllocator { - use AppliesOrdering; - use SelectsFirstApplicableStand; - use SelectsStandsFromAirlineTerminals; - use SelectsFromSizeAppropriateAvailableStands; - use OrdersStandsByCommonConditions; - - private const ORDER_BYS = [ - 'airline_terminal.priority ASC', - ]; + use SelectsStandsFromAirlineSpecificTerminals; /** * This allocator: @@ -35,24 +27,12 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->standsAtAirlineTerminals( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->whereNull('airline_terminal.destination') - ->whereNull('airline_terminal.callsign_slug') - ->whereNull('airline_terminal.full_callsign') - ->whereNull('airline_terminal.aircraft_id'), - $aircraft - ), - $aircraft - ), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditionsWithoutAssignmentPriority - ) - ) + return $this->selectStandsAtAirlineSpecificTerminals( + $aircraft, + fn(Builder $query) => $query->whereNull('airline_terminal.destination') + ->whereNull('airline_terminal.callsign_slug') + ->whereNull('airline_terminal.full_callsign') + ->whereNull('airline_terminal.aircraft_id') ); } } diff --git a/app/Allocator/Stand/ArrivalStandAllocatorInterface.php b/app/Allocator/Stand/ArrivalStandAllocatorInterface.php deleted file mode 100644 index 729604c60..000000000 --- a/app/Allocator/Stand/ArrivalStandAllocatorInterface.php +++ /dev/null @@ -1,13 +0,0 @@ -selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft), - $aircraft - )->cargo(), - $this->commonOrderByConditions - ) + return $this->selectStandsUsingStandardConditions( + $aircraft, + fn(Builder $query) => $query->cargo(), ); } } diff --git a/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php b/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php index 2b769d99e..7f9cd9e3c 100644 --- a/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php +++ b/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php @@ -4,6 +4,7 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Illuminate\Database\Eloquent\Builder; /** * Secondary cargo stand allocator, with no airline preferences. Only concerned with FP remarks explicitly @@ -11,12 +12,8 @@ */ class CargoFlightArrivalStandAllocator implements ArrivalStandAllocator { + use SelectsStandsUsingStandardConditions; use ChecksForCargoAirlines; - use AppliesOrdering; - use OrdersStandsByCommonConditions; - use SelectsFromSizeAppropriateAvailableStands; - use SelectsFirstApplicableStand; - use ConsidersStandRequests; private AirlineService $airlineService; @@ -38,14 +35,10 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft), - $aircraft - )->cargo(), - $this->commonOrderByConditions - ) + return $this->selectStandsUsingStandardConditions( + $aircraft, + fn(Builder $query) => $query->cargo(), + $this->commonOrderByConditions ); } } diff --git a/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php b/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php index 882db5dd4..5fd8faed1 100644 --- a/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php +++ b/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php @@ -4,6 +4,7 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Illuminate\Database\Eloquent\Builder; /** * The primary arrival stand allocator for cargo. Looks for either a cargo airline @@ -14,16 +15,8 @@ */ class CargoFlightPreferredArrivalStandAllocator implements ArrivalStandAllocator { + use SelectsFromAirlineSpecificStands; use ChecksForCargoAirlines; - use AppliesOrdering; - use OrdersStandsByCommonConditions; - use SelectsFromSizeAppropriateAvailableStands; - use SelectsFirstApplicableStand; - use ConsidersStandRequests; - - private const ORDER_BYS = [ - 'airline_stand.priority ASC', - ]; private AirlineService $airlineService; @@ -34,25 +27,14 @@ public function __construct(AirlineService $airlineService) public function allocate(NetworkAircraft $aircraft): ?int { - // If the aircraft doesnt have an airline, we cant allocate a stand if (!$this->isCargoAirline($aircraft) && !$this->isCargoFlight($aircraft)) { return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->airline($aircraft->airline_id), - $aircraft - )->cargo(), - array_merge( - self::ORDER_BYS, - $this->commonOrderByConditions - ) - ) + return $this->selectAirlineSpecificStands( + $aircraft, + fn(Builder $query) => $query->cargo() ); - } } diff --git a/app/Allocator/Stand/DomesticInternationalStandAllocator.php b/app/Allocator/Stand/DomesticInternationalStandAllocator.php index 2c2a8451c..a705b3117 100644 --- a/app/Allocator/Stand/DomesticInternationalStandAllocator.php +++ b/app/Allocator/Stand/DomesticInternationalStandAllocator.php @@ -8,11 +8,7 @@ class DomesticInternationalStandAllocator implements ArrivalStandAllocator { - use AppliesOrdering; - use OrdersStandsByCommonConditions; - use SelectsFromSizeAppropriateAvailableStands; - use SelectsFirstApplicableStand; - use ConsidersStandRequests; + use SelectsStandsUsingStandardConditions; public function allocate(NetworkAircraft $aircraft): ?int { @@ -20,17 +16,9 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->getDomesticInternationalScope( - $aircraft, - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ), - $aircraft - ), - $this->commonOrderByConditions - ) + return $this->selectStandsUsingStandardConditions( + $aircraft, + fn(Builder $query) => $this->getDomesticInternationalScope($aircraft, $query) ); } diff --git a/app/Allocator/Stand/FallbackArrivalStandAllocator.php b/app/Allocator/Stand/FallbackArrivalStandAllocator.php index 13739335f..bdf8702a8 100644 --- a/app/Allocator/Stand/FallbackArrivalStandAllocator.php +++ b/app/Allocator/Stand/FallbackArrivalStandAllocator.php @@ -3,14 +3,11 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Database\Eloquent\Builder; class FallbackArrivalStandAllocator implements ArrivalStandAllocator { - use AppliesOrdering; - use OrdersStandsByCommonConditions; - use SelectsFromSizeAppropriateAvailableStands; - use SelectsFirstApplicableStand; - use ConsidersStandRequests; + use SelectsStandsUsingStandardConditions; /** * This allocator: @@ -24,14 +21,10 @@ class FallbackArrivalStandAllocator implements ArrivalStandAllocator */ public function allocate(NetworkAircraft $aircraft): ?int { - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft)->notCargo(), - $aircraft - ), - $this->commonOrderByConditions - ) + return $this->selectStandsUsingStandardConditions( + $aircraft, + fn(Builder $query) => $query->notCargo(), + $this->commonOrderByConditions ); } } diff --git a/app/Allocator/Stand/OriginAirfieldStandAllocator.php b/app/Allocator/Stand/OriginAirfieldStandAllocator.php index 1be0108c5..5cfb565d6 100644 --- a/app/Allocator/Stand/OriginAirfieldStandAllocator.php +++ b/app/Allocator/Stand/OriginAirfieldStandAllocator.php @@ -8,12 +8,8 @@ class OriginAirfieldStandAllocator implements ArrivalStandAllocator { + use SelectsStandsUsingStandardConditions; use UsesDestinationStrings; - use AppliesOrdering; - use OrdersStandsByCommonConditions; - use SelectsFromSizeAppropriateAvailableStands; - use SelectsFirstApplicableStand; - use ConsidersStandRequests; private const ORDER_BYS = [ 'origin_slug IS NOT NULL', @@ -26,16 +22,11 @@ public function allocate(NetworkAircraft $aircraft): ?int return null; } - return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ->whereIn('origin_slug', $this->getDestinationStrings($aircraft)) - ->notCargo(), - $aircraft - ), - array_merge(self::ORDER_BYS, $this->commonOrderByConditions) - ) + return $this->selectStandsUsingStandardConditions( + $aircraft, + fn(Builder $query) => $query->whereIn('origin_slug', $this->getDestinationStrings($aircraft)) + ->notCargo(), + self::ORDER_BYS, ); } } diff --git a/app/Allocator/Stand/SelectsFromAirlineSpecificStands.php b/app/Allocator/Stand/SelectsFromAirlineSpecificStands.php new file mode 100644 index 000000000..72fecc975 --- /dev/null +++ b/app/Allocator/Stand/SelectsFromAirlineSpecificStands.php @@ -0,0 +1,51 @@ +selectStandsUsingStandardConditions( + $aircraft, + fn(Builder $query) => $specificFilters($query->airline($aircraft->airline_id)), + array_merge( + $specificOrders, + ['airline_stand.priority ASC'], + ), + false + ); + } + + private function orderByForAirlineStandQuery(array $specificOrders): array + { + return $this->orderByForStandsQuery( + array_merge( + $specificOrders, + ['airline_stand.priority ASC'], + ), + false + ); + } +} diff --git a/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php b/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php new file mode 100644 index 000000000..2e0805f22 --- /dev/null +++ b/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php @@ -0,0 +1,57 @@ +selectStandsUsingStandardConditions( + $aircraft, + fn(Builder $query) => $specificFilters($query->join('terminals', 'terminals.id', '=', 'stands.terminal_id') + ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') + ->where('airline_terminal.airline_id', $aircraft->airline_id)), + array_merge( + $specificOrders, + ['airline_terminal.priority ASC'], + ), + false + ); + } + + private function orderByForAirlineStandQuery(array $specificOrders): array + { + return $this->orderByForStandsQuery( + array_merge( + $specificOrders, + ['airline_terminal.priority ASC'], + ), + false + ); + } +} diff --git a/app/Allocator/Stand/SelectsStandsFromAirlineTerminals.php b/app/Allocator/Stand/SelectsStandsFromAirlineTerminals.php deleted file mode 100644 index 35d3f7e3e..000000000 --- a/app/Allocator/Stand/SelectsStandsFromAirlineTerminals.php +++ /dev/null @@ -1,22 +0,0 @@ -join('terminals', 'terminals.id', '=', 'stands.terminal_id') - ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') - ->where('airline_terminal.airline_id', $aircraft->airline_id); - } -} diff --git a/app/Allocator/Stand/SelectsStandsUsingStandardConditions.php b/app/Allocator/Stand/SelectsStandsUsingStandardConditions.php new file mode 100644 index 000000000..3bf909929 --- /dev/null +++ b/app/Allocator/Stand/SelectsStandsUsingStandardConditions.php @@ -0,0 +1,43 @@ +selectFirstStand( + $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $specificFilters( + $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ), + $aircraft + ), + $this->orderByForStandsQuery($specificOrders, $includeAssignmentPriority) + ) + ); + } + + private function orderByForStandsQuery(array $customOrders, bool $includeAssignmentPriority): array + { + return array_merge( + $customOrders, + $includeAssignmentPriority + ? [...$this->commonOrderByConditions] + : [...$this->commonOrderByConditionsWithoutAssignmentPriority] + ); + } +} diff --git a/app/Models/Stand/Stand.php b/app/Models/Stand/Stand.php index b33d32a49..5c8032b26 100644 --- a/app/Models/Stand/Stand.php +++ b/app/Models/Stand/Stand.php @@ -140,11 +140,6 @@ function (Builder $query) { ); } - public function scopeAirlineDestination(Builder $builder, Airline|int $airline, array $destinationStrings): Builder - { - return $this->scopeAirline($builder, $airline)->whereIn('destination', $destinationStrings); - } - public function scopeAirlineCallsign(Builder $builder, Airline $airline, array $slugs): Builder { return $this->scopeAirline($builder, $airline)->whereIn('callsign_slug', $slugs); diff --git a/tests/app/Models/Stand/StandTest.php b/tests/app/Models/Stand/StandTest.php index ef3f8b17b..b897e4639 100644 --- a/tests/app/Models/Stand/StandTest.php +++ b/tests/app/Models/Stand/StandTest.php @@ -120,35 +120,6 @@ public function testAirlineOnlyReturnsStandsAtTheRightTime() $this->assertEquals([2, 3], $stands); } - public function testAirlineDestinationOnlyReturnsStandsForTheCorrectAirlineAndDestinations() - { - DB::table('airline_stand')->insert( - [ - [ - 'airline_id' => 1, - 'stand_id' => 1, - 'destination' => 'EGGD', - ], - [ - 'airline_id' => 1, - 'stand_id' => 2, - 'destination' => 'EGFF', - ], - [ - 'airline_id' => 2, - 'stand_id' => 1, - 'destination' => 'EGGD', - ], - ] - ); - - $stands = Stand::airlineDestination( - Airline::find(1), - ['EGGD'] - )->pluck('stands.id')->toArray(); - $this->assertEquals([1], $stands); - } - public function testAirlineDestinationOnlyReturnsStandsWithinTheRightTime() { Carbon::setTestNow(Carbon::parse('2020-12-05 16:00:00')); diff --git a/tests/app/Services/Stand/ArrivalAllocationServiceTest.php b/tests/app/Services/Stand/ArrivalAllocationServiceTest.php index 5a7ae29c4..86af351b5 100644 --- a/tests/app/Services/Stand/ArrivalAllocationServiceTest.php +++ b/tests/app/Services/Stand/ArrivalAllocationServiceTest.php @@ -12,7 +12,7 @@ use App\Allocator\Stand\AirlineDestinationArrivalStandAllocator; use App\Allocator\Stand\AirlineDestinationTerminalArrivalStandAllocator; use App\Allocator\Stand\AirlineTerminalArrivalStandAllocator; -use App\Allocator\Stand\ArrivalStandAllocatorInterface; +use App\Allocator\Stand\ArrivalStandAllocator; use App\Allocator\Stand\CallsignFlightplanReservedArrivalStandAllocator; use App\Allocator\Stand\CargoAirlineFallbackStandAllocator; use App\Allocator\Stand\CargoFlightArrivalStandAllocator; @@ -188,7 +188,7 @@ public function testItHasAllocatorPreference() FallbackArrivalStandAllocator::class, ], array_map( - fn(ArrivalStandAllocatorInterface $allocator) => get_class($allocator), + fn(ArrivalStandAllocator $allocator) => get_class($allocator), $this->service->getAllocators() ) ); From ea535006d7b9f27b039281b4544f2b06e1fd45eb Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Sat, 2 Sep 2023 17:27:12 +0000 Subject: [PATCH 07/24] fix: join aircraft table on id --- app/Services/Stand/ArrivalAllocationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/Stand/ArrivalAllocationService.php b/app/Services/Stand/ArrivalAllocationService.php index 06c5d5b53..82dbc4642 100644 --- a/app/Services/Stand/ArrivalAllocationService.php +++ b/app/Services/Stand/ArrivalAllocationService.php @@ -86,7 +86,7 @@ private function allocateStandsForArrivingAircraft(): void private function getAircraftThatCanHaveArrivalStandsAllocated(): Collection { return NetworkAircraft::join('airfield', 'airfield.code', '=', 'network_aircraft.planned_destairport') - ->join('aircraft', 'aircraft.code', '=', 'network_aircraft.planned_aircraft_short') + ->join('aircraft', 'network_aircraft.aircraft_id', '=', 'aircraft.id') ->leftJoin('stand_assignments', 'stand_assignments.callsign', '=', 'network_aircraft.callsign') ->whereRaw('network_aircraft.planned_destairport <> network_aircraft.planned_depairport') ->where('aircraft.allocate_stands', '<>', 0) From 04873d17839bb6041bb3af26e66482fec049aac7 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Sun, 3 Sep 2023 12:36:37 +0000 Subject: [PATCH 08/24] wip: start stand ranking --- .../AirlineAircraftArrivalStandAllocator.php | 17 +- ...eAircraftTerminalArrivalStandAllocator.php | 16 +- .../Stand/OrdersStandsByCommonConditions.php | 9 + .../Stand/RankableArrivalStandAllocator.php | 15 ++ .../SelectsFromAirlineSpecificStands.php | 12 +- ...ectsFromSizeAppropriateAvailableStands.php | 14 +- ...ectsStandsFromAirlineSpecificTerminals.php | 14 +- .../SelectsStandsUsingStandardConditions.php | 78 +++++-- ...rlineAircraftArrivalStandAllocatorTest.php | 145 ++++++++++++- ...craftTerminalArrivalStandAllocatorTest.php | 201 +++++++++++++++++- 10 files changed, 494 insertions(+), 27 deletions(-) create mode 100644 app/Allocator/Stand/RankableArrivalStandAllocator.php diff --git a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php index 19a8bdf4f..f4fe6988f 100644 --- a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php @@ -4,8 +4,9 @@ use App\Models\Vatsim\NetworkAircraft; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class AirlineAircraftArrivalStandAllocator implements ArrivalStandAllocator +class AirlineAircraftArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsFromAirlineSpecificStands; @@ -25,4 +26,18 @@ public function allocate(NetworkAircraft $aircraft): ?int fn(Builder $query) => $query->where('airline_stand.aircraft_id', $aircraft->aircraft_id), ); } + + // TODO: Move airline and aircraft checks into a separate class + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // We cant allocate a stand if we don't know the airline or aircraft type + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedAirlineSpecificStands( + $aircraft, + fn(Builder $query) => $query->where('airline_stand.aircraft_id', $aircraft->aircraft_id) + ); + } } diff --git a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php index be320c799..72f4df8a0 100644 --- a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php @@ -4,8 +4,9 @@ use App\Models\Vatsim\NetworkAircraft; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class AirlineAircraftTerminalArrivalStandAllocator implements ArrivalStandAllocator +class AirlineAircraftTerminalArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsStandsFromAirlineSpecificTerminals; @@ -31,4 +32,17 @@ public function allocate(NetworkAircraft $aircraft): ?int fn(Builder $query) => $query->where('airline_terminal.aircraft_id', $aircraft->aircraft_id) ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // We cant allocate a stand if we don't know the airline or aircraft type + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedStandsAtAirlineSpecificTerminals( + $aircraft, + fn(Builder $query) => $query->where('airline_terminal.aircraft_id', $aircraft->aircraft_id) + ); + } } diff --git a/app/Allocator/Stand/OrdersStandsByCommonConditions.php b/app/Allocator/Stand/OrdersStandsByCommonConditions.php index 13e4409df..77913327c 100644 --- a/app/Allocator/Stand/OrdersStandsByCommonConditions.php +++ b/app/Allocator/Stand/OrdersStandsByCommonConditions.php @@ -25,4 +25,13 @@ trait OrdersStandsByCommonConditions 'other_stand_requests.id ASC', 'RAND() ASC', ]; + + private array $commonOrderByConditionsForRanking = [ + 'aerodrome_reference_code ASC', + 'assignment_priority ASC', + ]; + + private array $commonOrderByConditionsWithoutAssignmentPriorityForRanking = [ + 'aerodrome_reference_code ASC', + ]; } diff --git a/app/Allocator/Stand/RankableArrivalStandAllocator.php b/app/Allocator/Stand/RankableArrivalStandAllocator.php new file mode 100644 index 000000000..86104c68a --- /dev/null +++ b/app/Allocator/Stand/RankableArrivalStandAllocator.php @@ -0,0 +1,15 @@ +orderByForStandsQuery( + private function selectRankedAirlineSpecificStands( + NetworkAircraft $aircraft, + Closure $specificFilters, + array $specificOrders = [] + ): Collection { + return $this->selectRankedStandsUsingStandardConditions( + $aircraft, + fn(Builder $query) => $specificFilters($query->airline($aircraft->airline_id)), array_merge( $specificOrders, ['airline_stand.priority ASC'], diff --git a/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php b/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php index 42b0aacb0..4ae766d50 100644 --- a/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php +++ b/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php @@ -6,7 +6,8 @@ use App\Models\Vatsim\NetworkAircraft; use Illuminate\Database\Eloquent\Builder; -trait SelectsFromSizeAppropriateAvailableStands { +trait SelectsFromSizeAppropriateAvailableStands +{ /* * Base query for stands at the arrival airfield, which are of a suitable * size (or max size if no type) for the aircraft and not occupied. @@ -21,4 +22,15 @@ private function sizeAppropriateAvailableStandsAtAirfield(NetworkAircraft $aircr ->available() ->select('stands.*'); } + + private function sizeAppropriateAvailableStandsAtAirfieldForRanking(NetworkAircraft $aircraft): Builder + { + return Stand::whereHas('airfield', function (Builder $query) use ($aircraft) + { + $query->where('code', $aircraft->planned_destairport); + }) + ->sizeAppropriate($aircraft->aircraft) + ->notClosed() + ->select('stands.*'); + } } diff --git a/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php b/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php index 2e0805f22..510064e6e 100644 --- a/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php +++ b/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php @@ -5,6 +5,7 @@ use App\Models\Vatsim\NetworkAircraft; use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; /** * A trait that can be used by any allocator that needs to select stands from @@ -44,9 +45,16 @@ private function selectStandsAtAirlineSpecificTerminals( ); } - private function orderByForAirlineStandQuery(array $specificOrders): array - { - return $this->orderByForStandsQuery( + private function selectRankedStandsAtAirlineSpecificTerminals( + NetworkAircraft $aircraft, + Closure $specificFilters, + array $specificOrders = [] + ): Collection { + return $this->selectRankedStandsUsingStandardConditions( + $aircraft, + fn(Builder $query) => $specificFilters($query->join('terminals', 'terminals.id', '=', 'stands.terminal_id') + ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') + ->where('airline_terminal.airline_id', $aircraft->airline_id)), array_merge( $specificOrders, ['airline_terminal.priority ASC'], diff --git a/app/Allocator/Stand/SelectsStandsUsingStandardConditions.php b/app/Allocator/Stand/SelectsStandsUsingStandardConditions.php index 3bf909929..17de9e173 100644 --- a/app/Allocator/Stand/SelectsStandsUsingStandardConditions.php +++ b/app/Allocator/Stand/SelectsStandsUsingStandardConditions.php @@ -4,6 +4,8 @@ use App\Models\Vatsim\NetworkAircraft; use Closure; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; trait SelectsStandsUsingStandardConditions { @@ -19,25 +21,77 @@ private function selectStandsUsingStandardConditions( bool $includeAssignmentPriority = true ): ?int { return $this->selectFirstStand( - $this->applyOrderingToStandsQuery( - $this->joinOtherStandRequests( - $specificFilters( - $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) - ), - $aircraft - ), - $this->orderByForStandsQuery($specificOrders, $includeAssignmentPriority) + $this->standardConditionsStandQuery( + $aircraft, + $specificFilters, + $specificOrders, + $includeAssignmentPriority, + false ) ); } - private function orderByForStandsQuery(array $customOrders, bool $includeAssignmentPriority): array + private function selectRankedStandsUsingStandardConditions( + NetworkAircraft $aircraft, + Closure $specificFilters, + array $specificOrders = [], + bool $includeAssignmentPriority = true + ): Collection { + $orderByForRankQuery = implode( + ',', + $this->orderByForStandsQuery($specificOrders, $includeAssignmentPriority, true) + ); + + return $this->standardConditionsStandQuery( + $aircraft, + $specificFilters, + $specificOrders, + $includeAssignmentPriority, + true + ) + ->selectRaw(sprintf('DENSE_RANK() OVER (ORDER BY %s) AS `rank`', $orderByForRankQuery)) + ->get(); + } + + private function standardConditionsStandQuery( + NetworkAircraft $aircraft, + Closure $specificFilters, + array $specificOrders = [], + bool $includeAssignmentPriority = true, + bool $isRanking = false + ): Builder { + return $this->applyOrderingToStandsQuery( + $this->joinOtherStandRequests( + $specificFilters( + $isRanking + ? $this->sizeAppropriateAvailableStandsAtAirfieldForRanking($aircraft) + : $this->sizeAppropriateAvailableStandsAtAirfield($aircraft) + ), + $aircraft + ), + $this->orderByForStandsQuery($specificOrders, $includeAssignmentPriority, $isRanking) + ); + } + + private function orderByForStandsQuery(array $customOrders, bool $includeAssignmentPriority, bool $isRanking): array { + /** + * If we are doing ranking, we don't need to consider stand requests in the priority, nor do we need + * a random order. + */ + if ($includeAssignmentPriority) { + $commonConditions = $isRanking + ? $this->commonOrderByConditionsForRanking + : $this->commonOrderByConditions; + } else { + $commonConditions = $isRanking + ? $this->commonOrderByConditionsWithoutAssignmentPriorityForRanking + : $this->commonOrderByConditionsWithoutAssignmentPriority; + } + return array_merge( $customOrders, - $includeAssignmentPriority - ? [...$this->commonOrderByConditions] - : [...$this->commonOrderByConditionsWithoutAssignmentPriority] + $commonConditions ); } } diff --git a/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php index ad8c93f48..78a10e311 100644 --- a/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php @@ -4,9 +4,12 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class AirlineAircraftArrivalStandAllocatorTest extends BaseFunctionalTestCase @@ -329,13 +332,153 @@ public function testItDoesntAllocateUnknownAircraftTypes() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAircraftType() + { + $aircraft = $this->newAircraft('BAW23451', 'EGLL', 'EGGD', aircraftType: 'XXX'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. It gets a stand reservation to make + // sure it is ranked first even if it is occupied. + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + ] + ); + $standA1->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 100]]); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, lower priority than A1 + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + $standB1->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); + $standB2->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); + + // Should be ranked joint third, same priority as B1 and B2 but smaller stands + $standC1 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C1']); + $standC1->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); + $standC2 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C2']); + $standC2->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); + + // Should not appear in rankings - wrong airfield + $standD1 = Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1']); + $standD1->airlines()->sync([1 => ['aircraft_id' => 1]]); + + // Should not appear in rankings - wrong aircraft type + $standE1 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E1']); + $standE1->airlines()->sync([1 => ['aircraft_id' => $cessna->id]]); + + // Should not appear in rankings - too small ARC + $standF1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + $standF1->airlines()->sync([1 => ['aircraft_id' => 1]]); + + // Should not appear in rankings - too small max aircraft size + $standG1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + $standG1->airlines()->sync([1 => ['aircraft_id' => 1]]); + + // Should not appear in rankings - closed + $standH1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'C', + 'closed_at' => Carbon::now(), + ] + ); + $standH1->airlines()->sync([1 => ['aircraft_id' => 1]]); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $arrivalAirport, string $departureAirport, string $aircraftType = 'B738' ): NetworkAircraft { - return NetworkAircraft::create( + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, diff --git a/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php index abc8a306e..cda95b812 100644 --- a/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php @@ -4,10 +4,13 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airfield\Terminal; use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class AirlineAircraftTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase @@ -337,13 +340,204 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAircraftType() + { + $aircraft = $this->newAircraft('BAW23451', 'EGLL', 'EGGD', aircraftType: 'XXX'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. Both stands on the terminal should be + // included. Stand A1 gets a reservation so that we show its not considered. + $terminalA1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalA1->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 100]]); + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A1', + ] + ); + $standA2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A2', + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, lower priority than A1 + $terminalB1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB1->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB1->id, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + + $terminalB2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB2->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB2->id, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + + // Should be ranked joint third, same priority as B1 and B2 but smaller stands + $terminalC1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC1->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'terminal_id' => $terminalC1->id, + ] + ); + + $terminalC2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC2->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'terminal_id' => $terminalC2->id, + ] + ); + + // Should not appear in rankings - wrong airfield + Terminal::find(1)->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); + Stand::factory()->create( + [ + 'airfield_id' => 1, + 'identifier' => 'D1', + 'terminal_id' => 1 + ] + ); + + // Should not appear in rankings - wrong terminal + $terminalD2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'D1', + 'terminal_id' => $terminalD2->id + ] + ); + + // Should not appear in rankings - wrong aircraft type + $terminalE1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE1->airlines()->sync([1 => ['aircraft_id' => 1]]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E1']); + + // Should not appear in rankings - too small ARC + $terminalF1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalF1->airlines()->sync([1 => ['aircraft_id' => 1]]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + + // Should not appear in rankings - too small max aircraft size + $terminalG1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalG1->airlines()->sync([1 => ['aircraft_id' => 1]]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + + + // Should not appear in rankings - closed + $terminalH1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalH1->airlines()->sync([1 => ['aircraft_id' => 1]]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'closed_at' => Carbon::now() + ] + ); + + + $expectedRanks = [ + $standA1->id => 1, + $standA2->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $arrivalAirport, string $departureAirport, string $aircraftType = 'B738' ): NetworkAircraft { - return NetworkAircraft::create( + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, @@ -351,15 +545,12 @@ private function createAircraft( 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, + 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, 'airline_id' => match ($callsign) { 'BAW23451' => 1, 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, default => null, }, - 'aircraft_id' => match ($aircraftType) { - 'B738' => 1, - default => null, - }, ] ); } From bf200131879733fe4b0875454b38da7f49c918f7 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Sun, 3 Sep 2023 13:31:34 +0000 Subject: [PATCH 09/24] wip: more allocator ranking --- .../AirlineAircraftArrivalStandAllocator.php | 10 +- ...eAircraftTerminalArrivalStandAllocator.php | 10 +- .../Stand/AirlineArrivalStandAllocator.php | 32 ++- .../AirlineCallsignArrivalStandAllocator.php | 25 +- ...rlineCallsignSlugArrivalStandAllocator.php | 27 +- ...lsignSlugTerminalArrivalStandAllocator.php | 28 +- ...eCallsignTerminalArrivalStandAllocator.php | 27 +- ...rlineAircraftArrivalStandAllocatorTest.php | 5 +- ...craftTerminalArrivalStandAllocatorTest.php | 6 +- .../AirlineArrivalStandAllocatorTest.php | 172 +++++++++++- ...rlineCallsignArrivalStandAllocatorTest.php | 145 ++++++++++ ...eCallsignSlugArrivalStandAllocatorTest.php | 174 ++++++++++++ ...nSlugTerminalArrivalStandAllocatorTest.php | 248 +++++++++++++++++- ...lsignTerminalArrivalStandAllocatorTest.php | 206 ++++++++++++++- 14 files changed, 1088 insertions(+), 27 deletions(-) diff --git a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php index f4fe6988f..a6d21097d 100644 --- a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php @@ -3,6 +3,7 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Closure; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; @@ -23,7 +24,7 @@ public function allocate(NetworkAircraft $aircraft): ?int return $this->selectAirlineSpecificStands( $aircraft, - fn(Builder $query) => $query->where('airline_stand.aircraft_id', $aircraft->aircraft_id), + $this->queryFilter($aircraft) ); } @@ -37,7 +38,12 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection return $this->selectRankedAirlineSpecificStands( $aircraft, - fn(Builder $query) => $query->where('airline_stand.aircraft_id', $aircraft->aircraft_id) + $this->queryFilter($aircraft) ); } + + private function queryFilter(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) => $query->where('airline_stand.aircraft_id', $aircraft->aircraft_id); + } } diff --git a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php index 72f4df8a0..ba60b7efd 100644 --- a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php @@ -3,6 +3,7 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Closure; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; @@ -29,7 +30,7 @@ public function allocate(NetworkAircraft $aircraft): ?int return $this->selectStandsAtAirlineSpecificTerminals( $aircraft, - fn(Builder $query) => $query->where('airline_terminal.aircraft_id', $aircraft->aircraft_id) + $this->queryFilter($aircraft) ); } @@ -42,7 +43,12 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection return $this->selectRankedStandsAtAirlineSpecificTerminals( $aircraft, - fn(Builder $query) => $query->where('airline_terminal.aircraft_id', $aircraft->aircraft_id) + $this->queryFilter($aircraft) ); } + + private function queryFilter(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) => $query->where('airline_terminal.aircraft_id', $aircraft->aircraft_id); + } } diff --git a/app/Allocator/Stand/AirlineArrivalStandAllocator.php b/app/Allocator/Stand/AirlineArrivalStandAllocator.php index 116db4ccb..9001eba49 100644 --- a/app/Allocator/Stand/AirlineArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineArrivalStandAllocator.php @@ -3,9 +3,11 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class AirlineArrivalStandAllocator implements ArrivalStandAllocator +class AirlineArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsFromAirlineSpecificStands; @@ -22,16 +24,34 @@ class AirlineArrivalStandAllocator implements ArrivalStandAllocator public function allocate(NetworkAircraft $aircraft): ?int { // We can only allocate a stand if we know the airline - if ($aircraft->airline_id === null) { + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { return null; } return $this->selectAirlineSpecificStands( $aircraft, - fn(Builder $query) => $query->whereNull('airline_stand.destination') - ->whereNull('airline_stand.callsign_slug') - ->whereNull('airline_stand.full_callsign') - ->whereNull('airline_stand.aircraft_id'), + $this->queryFilter() ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // We cant allocate a stand if we don't know the airline or aircraft type + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedAirlineSpecificStands( + $aircraft, + $this->queryFilter() + ); + } + + private function queryFilter(): Closure + { + return fn(Builder $query) => $query->whereNull('airline_stand.destination') + ->whereNull('airline_stand.callsign_slug') + ->whereNull('airline_stand.full_callsign') + ->whereNull('airline_stand.aircraft_id'); + } } diff --git a/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php index 71dc62e79..b5ae19b26 100644 --- a/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php @@ -4,9 +4,11 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class AirlineCallsignArrivalStandAllocator implements ArrivalStandAllocator +class AirlineCallsignArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsFromAirlineSpecificStands; use UsesCallsignSlugs; @@ -37,7 +39,26 @@ public function allocate(NetworkAircraft $aircraft): ?int return $this->selectAirlineSpecificStands( $aircraft, - fn(Builder $query) => $query->where('airline_stand.full_callsign', $this->getFullCallsignSlug($aircraft)), + $this->queryFilter($aircraft) ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // We can only allocate a stand if we know the airline + if ($aircraft->airline_id === null) { + return collect(); + } + + return $this->selectRankedAirlineSpecificStands( + $aircraft, + $this->queryFilter($aircraft) + ); + } + + private function queryFilter(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) => + $query->where('airline_stand.full_callsign', $this->getFullCallsignSlug($aircraft)); + } } diff --git a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php index dee23bd61..a0f9c8b19 100644 --- a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php @@ -4,9 +4,11 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class AirlineCallsignSlugArrivalStandAllocator implements ArrivalStandAllocator +class AirlineCallsignSlugArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsFromAirlineSpecificStands; use UsesCallsignSlugs; @@ -36,14 +38,33 @@ public function __construct(AirlineService $airlineService) public function allocate(NetworkAircraft $aircraft): ?int { // We can't allocate a stand if we don't know the airline - if ($aircraft->airline_id === null) { + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { return null; } return $this->selectAirlineSpecificStands( $aircraft, - fn(Builder $query) => $query->whereIn('airline_stand.callsign_slug', $this->getCallsignSlugs($aircraft)), + $this->queryFilter($aircraft), self::ORDER_BYS ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // We can only allocate a stand if we know the airline + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedAirlineSpecificStands( + $aircraft, + $this->queryFilter($aircraft), + self::ORDER_BYS + ); + } + + private function queryFilter(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) => $query->whereIn('airline_stand.callsign_slug', $this->getCallsignSlugs($aircraft)); + } } diff --git a/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php index 68ff7ffb7..990f51e30 100644 --- a/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php @@ -4,9 +4,11 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class AirlineCallsignSlugTerminalArrivalStandAllocator implements ArrivalStandAllocator +class AirlineCallsignSlugTerminalArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use UsesCallsignSlugs; use SelectsStandsFromAirlineSpecificTerminals; @@ -38,14 +40,34 @@ public function __construct(AirlineService $airlineService) public function allocate(NetworkAircraft $aircraft): ?int { // If the aircraft doesnt have an airline, we cant allocate a stand - if ($aircraft->airline_id === null) { + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { return null; } return $this->selectStandsAtAirlineSpecificTerminals( $aircraft, - fn(Builder $query) => $query->whereIn('airline_terminal.callsign_slug', $this->getCallsignSlugs($aircraft)), + $this->queryFilter($aircraft), self::ORDER_BYS ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // We can only allocate a stand if we know the airline + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedStandsAtAirlineSpecificTerminals( + $aircraft, + $this->queryFilter($aircraft), + self::ORDER_BYS + ); + } + + private function queryFilter(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) + => $query->whereIn('airline_terminal.callsign_slug', $this->getCallsignSlugs($aircraft)); + } } diff --git a/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php index a3a864196..29a3cbbf5 100644 --- a/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php @@ -4,9 +4,11 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class AirlineCallsignTerminalArrivalStandAllocator implements ArrivalStandAllocator +class AirlineCallsignTerminalArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use UsesCallsignSlugs; use SelectsStandsFromAirlineSpecificTerminals; @@ -32,13 +34,32 @@ public function __construct(AirlineService $airlineService) public function allocate(NetworkAircraft $aircraft): ?int { // If the aircraft doesnt have an airline, we cant allocate a stand - if ($aircraft->airline_id === null) { + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { return null; } return $this->selectStandsAtAirlineSpecificTerminals( $aircraft, - fn(Builder $query) => $query->where('airline_terminal.full_callsign', $this->getFullCallsignSlug($aircraft)) + $this->queryFilter($aircraft) ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // If the aircraft doesnt have an airline, we cant allocate a stand + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedStandsAtAirlineSpecificTerminals( + $aircraft, + $this->queryFilter($aircraft) + ); + } + + private function queryFilter(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) + => $query->where('airline_terminal.full_callsign', $this->getFullCallsignSlug($aircraft)); + } } diff --git a/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php index 78a10e311..093b35dcc 100644 --- a/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php @@ -7,6 +7,7 @@ use App\Models\Airfield\Airfield; use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; use Illuminate\Support\Carbon; @@ -378,7 +379,8 @@ public function testItGetsRankedStandAllocation() ] ); - // Should be ranked joint second, lower priority than A1 + // Should be ranked joint second, lower priority than A1. B1 has a request, to show that requests arent considered + // when ranking. $standB1 = Stand::factory()->create( [ 'airfield_id' => $airfieldId, @@ -386,6 +388,7 @@ public function testItGetsRankedStandAllocation() 'aerodrome_reference_code' => 'C' ] ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); $standB2 = Stand::factory()->create( [ 'airfield_id' => $airfieldId, diff --git a/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php index cda95b812..24e2a4464 100644 --- a/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php @@ -8,6 +8,7 @@ use App\Models\Airfield\Terminal; use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; use Illuminate\Support\Carbon; @@ -370,7 +371,7 @@ public function testItGetsRankedStandAllocation() ); // Should be ranked first - it has the highest priority. Both stands on the terminal should be - // included. Stand A1 gets a reservation so that we show its not considered. + // included. Stand A1 gets a reservation and a request so that we show its not considered. $terminalA1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); $terminalA1->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 100]]); $standA1 = Stand::factory()->create( @@ -394,8 +395,9 @@ public function testItGetsRankedStandAllocation() 'end' => Carbon::now()->addMinutes(1), ] ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standA1->id]); - // Should be ranked joint second, lower priority than A1 + // Should be ranked joint second, lower priority than A1. $terminalB1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); $terminalB1->airlines()->sync([1 => ['aircraft_id' => 1, 'priority' => 101]]); $standB1 = Stand::factory()->create( diff --git a/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php index 84ee9a441..8e25b7b3b 100644 --- a/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php @@ -4,9 +4,13 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Carbon\Carbon; use Illuminate\Support\Facades\DB; class AirlineArrivalStandAllocatorTest extends BaseFunctionalTestCase @@ -319,12 +323,178 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. It gets a stand reservation to make + // sure it is ranked first even if it is occupied. + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + ] + ); + $standA1->airlines()->sync([1 => ['priority' => 100]]); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, lower priority than A1. Stand B1 gets a request. + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + $standB1->airlines()->sync([1 => ['priority' => 101]]); + $standB2->airlines()->sync([1 => ['priority' => 101]]); + + // Should be ranked joint third, same priority as B1 and B2 but smaller stands + $standC1 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C1']); + $standC1->airlines()->sync([1 => ['priority' => 101]]); + $standC2 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C2']); + $standC2->airlines()->sync([1 => ['priority' => 101]]); + + // Should not appear in rankings - wrong airfield + $standD1 = Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1']); + $standD1->airlines()->sync([1]); + + // Should not appear in rankings - has a specific aircraft type + $standE1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + ] + ); + $standE1->airlines()->sync([1 => ['aircraft_id' => 1]]); + + // Should not appear in rankings - has a specific destination + $standE2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E2', + ] + ); + $standE2->airlines()->sync([1 => ['destination' => 'abc']]); + + // Should not appear in rankings - has a specific callsign + $standE3 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E3', + ] + ); + $standE3->airlines()->sync([1 => ['full_callsign' => 'abc']]); + + // Should not appear in rankings - has a specific callsign slug + $standE4 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E4', + ] + ); + $standE4->airlines()->sync([1 => ['callsign_slug' => 'abc']]); + + // Should not appear in rankings - too small ARC + $standF1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + $standF1->airlines()->sync([1]); + + // Should not appear in rankings - too small max aircraft size + $standG1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + $standG1->airlines()->sync([1]); + + // Should not appear in rankings - closed + $standH1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'C', + 'closed_at' => Carbon::now(), + ] + ); + $standH1->airlines()->sync([1]); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code) + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $arrivalAirport, string $aircraftType = 'B738' ): NetworkAircraft { - return NetworkAircraft::create( + return tap( + $this->newAircraft($callsign, $arrivalAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, diff --git a/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php index e51d5b13b..7da6e6705 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php @@ -4,9 +4,13 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class AirlineCallsignArrivalStandAllocatorTest extends BaseFunctionalTestCase @@ -329,6 +333,147 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. It gets a stand reservation to make + // sure it is ranked first even if it is occupied. + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + ] + ); + $standA1->airlines()->sync([1 => ['priority' => 100, 'full_callsign' => '23451']]); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, lower priority than A1 + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + $standB1->airlines()->sync([1 => ['priority' => 101, 'full_callsign' => '23451']]); + $standB2->airlines()->sync([1 => ['priority' => 101, 'full_callsign' => '23451']]); + + // Should be ranked joint third, same priority as B1 and B2 but smaller stands + $standC1 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C1']); + $standC1->airlines()->sync([1 => ['priority' => 101, 'full_callsign' => '23451']]); + $standC2 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C2']); + $standC2->airlines()->sync([1 => ['priority' => 101, 'full_callsign' => '23451']]); + + // Should not appear in rankings - wrong airfield + $standD1 = Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1']); + $standD1->airlines()->sync([1 => ['full_callsign' => '23451']]); + + // Should not appear in rankings - wrong callsign + $standE1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + ] + ); + $standE1->airlines()->sync([1 => ['full_callsign' => 'XYZ']]); + + // Should not appear in rankings - too small ARC + $standF1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + $standF1->airlines()->sync([1 => ['full_callsign' => '23451']]); + + // Should not appear in rankings - too small max aircraft size + $standG1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + $standG1->airlines()->sync([1 => ['full_callsign' => '23451']]); + + // Should not appear in rankings - closed + $standH1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'C', + 'closed_at' => Carbon::now(), + ] + ); + $standH1->airlines()->sync([1 => ['full_callsign' => '23451']]); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + private function createAircraft( string $callsign, string $arrivalAirport, diff --git a/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php index 7de96ad6b..9892d3fb0 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php @@ -6,7 +6,10 @@ use App\Models\Aircraft\Aircraft; use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class AirlineCallsignSlugArrivalStandAllocatorTest extends BaseFunctionalTestCase @@ -479,6 +482,177 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. It gets a stand reservation to make + // sure it is ranked first even if it is occupied. + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + ] + ); + $standA1->airlines()->sync([1 => ['priority' => 100, 'callsign_slug' => '23451']]); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, lower priority than A1 + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + $standB1->airlines()->sync([1 => ['priority' => 101, 'callsign_slug' => '23451']]); + $standB2->airlines()->sync([1 => ['priority' => 101, 'callsign_slug' => '23451']]); + + // Should be ranked joint third, same priority as B1 and B2 but smaller stands + $standC1 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C1']); + $standC1->airlines()->sync([1 => ['priority' => 101, 'callsign_slug' => '23451']]); + $standC2 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C2']); + $standC2->airlines()->sync([1 => ['priority' => 101, 'callsign_slug' => '23451']]); + + // Should be ranked 4th, 5th, 6th, 7th, less specific callsign slugs + $standC3 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C3']); + $standC3->airlines()->sync([1 => ['priority' => 101, 'callsign_slug' => '2345']]); + $standC4 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C4']); + $standC4->airlines()->sync([1 => ['priority' => 101, 'callsign_slug' => '234']]); + $standC5 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C5']); + $standC5->airlines()->sync([1 => ['priority' => 101, 'callsign_slug' => '23']]); + $standC6 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C6']); + $standC6->airlines()->sync([1 => ['priority' => 101, 'callsign_slug' => '2']]); + + // Should not appear in rankings - wrong airfield + $standD1 = Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1']); + $standD1->airlines()->sync([1 => ['callsign_slug' => '23451']]); + + // Should not appear in rankings - wrong callsign + $standE1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + ] + ); + $standE1->airlines()->sync([1 => ['callsign_slug' => 'XYZ']]); + + // Should not appear in rankings - no callsign + $standE2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E2', + ] + ); + $standE2->airlines()->sync([1]); + + // Should not appear in rankings - no callsign + $standE2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E2', + ] + ); + $standE2->airlines()->sync([1]); + + // Should not appear in rankings - too small ARC + $standF1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + $standF1->airlines()->sync([1 => ['callsign_slug' => '23451']]); + + // Should not appear in rankings - too small max aircraft size + $standG1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + $standG1->airlines()->sync([1 => ['callsign_slug' => '23451']]); + + // Should not appear in rankings - closed + $standH1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'C', + 'closed_at' => Carbon::now(), + ] + ); + $standH1->airlines()->sync([1 => ['callsign_slug' => '23451']]); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + $standC3->id => 4, + $standC4->id => 5, + $standC5->id => 6, + $standC6->id => 7, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + private function createAircraft( string $callsign, string $arrivalAirport, diff --git a/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.php index d622af440..7641fe9f2 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.php @@ -4,10 +4,14 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airfield\Terminal; use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class AirlineCallsignSlugTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase @@ -503,13 +507,255 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAircraftType() + { + $aircraft = $this->newAircraft('BAW23451', 'EGLL', 'EGGD', aircraftType: 'XXX'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. Both stands on the terminal should be + // included. Stand A1 gets a reservation and a request so that we show its not considered. + $terminalA1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalA1->airlines()->sync([1 => ['callsign_slug' => '23451', 'priority' => 100]]); + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A1', + ] + ); + $standA2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A2', + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standA1->id]); + + // Should be ranked joint second, lower priority than A1. + $terminalB1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB1->airlines()->sync([1 => ['callsign_slug' => '23451', 'priority' => 101]]); + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB1->id, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + + $terminalB2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB2->airlines()->sync([1 => ['callsign_slug' => '23451', 'priority' => 101]]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB2->id, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + + // Should be ranked joint third, same priority as B1 and B2 but smaller stands + $terminalC1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC1->airlines()->sync([1 => ['callsign_slug' => '23451', 'priority' => 101]]); + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'terminal_id' => $terminalC1->id, + ] + ); + + $terminalC2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC2->airlines()->sync([1 => ['callsign_slug' => '23451', 'priority' => 101]]); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'terminal_id' => $terminalC2->id, + ] + ); + + // Should be ranked 4th, 5th, 6th, 7th, less specific callsign slugs + $terminalC3 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC3->airlines()->sync([1 => ['callsign_slug' => '2345', 'priority' => 101]]); + $standC3 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C3', + 'terminal_id' => $terminalC3->id, + ] + ); + + $terminalC4 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC4->airlines()->sync([1 => ['callsign_slug' => '234', 'priority' => 101]]); + $standC4 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C4', + 'terminal_id' => $terminalC4->id, + ] + ); + + $terminalC5 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC5->airlines()->sync([1 => ['callsign_slug' => '23', 'priority' => 101]]); + $standC5 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C5', + 'terminal_id' => $terminalC5->id, + ] + ); + + $terminalC6 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC6->airlines()->sync([1 => ['callsign_slug' => '2', 'priority' => 101]]); + $standC6 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C6', + 'terminal_id' => $terminalC6->id, + ] + ); + + // Should not appear in rankings - wrong airfield + Terminal::find(1)->airlines()->sync([1 => ['callsign_slug' => '23451', 'priority' => 101]]); + Stand::factory()->create( + [ + 'airfield_id' => 1, + 'identifier' => 'D1', + 'terminal_id' => 1 + ] + ); + + // Should not appear in rankings - wrong terminal + $terminalD2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'D1', + 'terminal_id' => $terminalD2->id + ] + ); + + // Should not appear in rankings - wrong callsign_slug + $terminalE1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE1->airlines()->sync([1 => ['callsign_slug' => 'xxxx']]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E1']); + + // Should not appear in rankings - no wrong callsign_slug + $terminalE2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE2->airlines()->sync([1]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E2']); + + // Should not appear in rankings - too small ARC + $terminalF1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalF1->airlines()->sync([1 => ['callsign_slug' => '23451']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + + // Should not appear in rankings - too small max aircraft size + $terminalG1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalG1->airlines()->sync([1 => ['callsign_slug' => '23451']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + + + // Should not appear in rankings - closed + $terminalH1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalH1->airlines()->sync([1 => ['callsign_slug' => '23451']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'closed_at' => Carbon::now() + ] + ); + + + $expectedRanks = [ + $standA1->id => 1, + $standA2->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + $standC3->id => 4, + $standC4->id => 5, + $standC5->id => 6, + $standC6->id => 7, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $arrivalAirport, string $departureAirport, string $aircraftType = 'B738' ): NetworkAircraft { - return NetworkAircraft::create( + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, diff --git a/tests/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocatorTest.php index 80791cc14..711c14efa 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocatorTest.php @@ -4,9 +4,14 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airfield\Terminal; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class AirlineCallsignTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase @@ -17,6 +22,7 @@ public function setUp(): void { parent::setUp(); $this->allocator = $this->app->make(AirlineCallsignTerminalArrivalStandAllocator::class); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandWithAFixedCallsign() @@ -354,13 +360,210 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAircraftType() + { + $aircraft = $this->newAircraft('BAW23451', 'EGLL', 'EGGD', aircraftType: 'XXX'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. Both stands on the terminal should be + // included. Stand A1 gets a reservation and a request so that we show its not considered. + $terminalA1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalA1->airlines()->sync([1 => ['full_callsign' => '23451', 'priority' => 100]]); + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A1', + ] + ); + $standA2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A2', + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standA1->id]); + + // Should be ranked joint second, lower priority than A1. + $terminalB1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB1->airlines()->sync([1 => ['full_callsign' => '23451', 'priority' => 101]]); + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB1->id, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + + $terminalB2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB2->airlines()->sync([1 => ['full_callsign' => '23451', 'priority' => 101]]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB2->id, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + + // Should be ranked joint third, same priority as B1 and B2 but smaller stands + $terminalC1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC1->airlines()->sync([1 => ['full_callsign' => '23451', 'priority' => 101]]); + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'terminal_id' => $terminalC1->id, + ] + ); + + $terminalC2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC2->airlines()->sync([1 => ['full_callsign' => '23451', 'priority' => 101]]); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'terminal_id' => $terminalC2->id, + ] + ); + + // Should not appear in rankings - wrong airfield + Terminal::find(1)->airlines()->sync([1 => ['full_callsign' => '23451', 'priority' => 101]]); + Stand::factory()->create( + [ + 'airfield_id' => 1, + 'identifier' => 'D1', + 'terminal_id' => 1 + ] + ); + + // Should not appear in rankings - wrong terminal + $terminalD2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'D1', + 'terminal_id' => $terminalD2->id + ] + ); + + // Should not appear in rankings - wrong full_callsign + $terminalE1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE1->airlines()->sync([1 => ['full_callsign' => 'xxxx']]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E1']); + + // Should not appear in rankings - no callsig + $terminalE2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE2->airlines()->sync([1]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E2']); + + // Should not appear in rankings - too small ARC + $terminalF1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalF1->airlines()->sync([1 => ['full_callsign' => '23451']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + + // Should not appear in rankings - too small max aircraft size + $terminalG1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalG1->airlines()->sync([1 => ['full_callsign' => '23451']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + + + // Should not appear in rankings - closed + $terminalH1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalH1->airlines()->sync([1 => ['full_callsign' => '23451']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'closed_at' => Carbon::now() + ] + ); + + + $expectedRanks = [ + $standA1->id => 1, + $standA2->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $arrivalAirport, string $departureAirport, string $aircraftType = 'B738' ): NetworkAircraft { - return NetworkAircraft::create( + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, @@ -371,6 +574,7 @@ private function createAircraft( 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, 'airline_id' => match ($callsign) { 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, default => null, }, ] From 1b54fe67ae6371646962a3e5398f2aed25265637 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Sun, 3 Sep 2023 15:48:43 +0000 Subject: [PATCH 10/24] wip: ranked allocators --- ...irlineDestinationArrivalStandAllocator.php | 31 +- ...stinationTerminalArrivalStandAllocator.php | 33 +- .../AirlineTerminalArrivalStandAllocator.php | 30 +- .../CargoAirlineFallbackStandAllocator.php | 25 +- .../CargoFlightArrivalStandAllocator.php | 22 +- ...goFlightPreferredArrivalStandAllocator.php | 22 +- .../DomesticInternationalStandAllocator.php | 25 +- .../Stand/FallbackArrivalStandAllocator.php | 28 +- .../Stand/OriginAirfieldStandAllocator.php | 26 +- ...rlineCallsignArrivalStandAllocatorTest.php | 4 +- ...eCallsignSlugArrivalStandAllocatorTest.php | 23 +- ...neDestinationArrivalStandAllocatorTest.php | 188 ++++++++++- ...ationTerminalArrivalStandAllocatorTest.php | 254 ++++++++++++++- ...rlineTerminalArrivalStandAllocatorTest.php | 229 +++++++++++++- ...CargoAirlineFallbackStandAllocatorTest.php | 178 ++++++++++- .../CargoFlightArrivalStandAllocatorTest.php | 169 +++++++++- ...ightPreferredArrivalStandAllocatorTest.php | 185 ++++++++++- ...omesticInternationalStandAllocatorTest.php | 294 +++++++++++++++++- .../FallbackArrivalStandAllocatorTest.php | 150 ++++++++- .../OriginAirfieldStandAllocatorTest.php | 187 ++++++++++- 20 files changed, 2018 insertions(+), 85 deletions(-) diff --git a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php index 953731452..3e9167168 100644 --- a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php @@ -4,7 +4,9 @@ use App\Allocator\UsesDestinationStrings; use App\Models\Vatsim\NetworkAircraft; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; class AirlineDestinationArrivalStandAllocator implements ArrivalStandAllocator { @@ -30,17 +32,36 @@ class AirlineDestinationArrivalStandAllocator implements ArrivalStandAllocator public function allocate(NetworkAircraft $aircraft): ?int { // We cant allocate a stand if we don't know the airline - if ($aircraft->airline_id === null) { + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { return null; } return $this->selectAirlineSpecificStands( $aircraft, - fn(Builder $query) => $query->whereIn( - 'airline_stand.destination', - $this->getDestinationStrings($aircraft) - ), + $this->queryFilter($aircraft), self::ORDER_BYS ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // We can only allocate a stand if we know the airline + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedAirlineSpecificStands( + $aircraft, + $this->queryFilter($aircraft), + self::ORDER_BYS + ); + } + + private function queryFilter(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) => $query->whereIn( + 'airline_stand.destination', + $this->getDestinationStrings($aircraft) + ); + } } diff --git a/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php index 5e428f95f..0494a0f68 100644 --- a/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php @@ -4,9 +4,11 @@ use App\Allocator\UsesDestinationStrings; use App\Models\Vatsim\NetworkAircraft; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class AirlineDestinationTerminalArrivalStandAllocator implements ArrivalStandAllocator +class AirlineDestinationTerminalArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use UsesDestinationStrings; use SelectsStandsFromAirlineSpecificTerminals; @@ -31,17 +33,36 @@ class AirlineDestinationTerminalArrivalStandAllocator implements ArrivalStandAll public function allocate(NetworkAircraft $aircraft): ?int { // If the aircraft doesnt have an airline, we cant allocate a stand - if ($aircraft->airline_id === null) { + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { return null; } return $this->selectStandsAtAirlineSpecificTerminals( $aircraft, - fn(Builder $query) => $query->whereIn( - 'airline_terminal.destination', - $this->getDestinationStrings($aircraft) - ), + $this->queryFilter($aircraft), self::ORDER_BYS ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // If the aircraft doesnt have an airline, we cant allocate a stand + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedStandsAtAirlineSpecificTerminals( + $aircraft, + $this->queryFilter($aircraft), + self::ORDER_BYS + ); + } + + public function queryFilter(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) => $query->whereIn( + 'airline_terminal.destination', + $this->getDestinationStrings($aircraft) + ); + } } diff --git a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php index 9e6f0dd82..7039ca196 100644 --- a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php @@ -3,7 +3,9 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; class AirlineTerminalArrivalStandAllocator implements ArrivalStandAllocator { @@ -23,16 +25,34 @@ class AirlineTerminalArrivalStandAllocator implements ArrivalStandAllocator public function allocate(NetworkAircraft $aircraft): ?int { // If the aircraft doesnt have an airline, we cant allocate a stand - if ($aircraft->airline_id === null) { + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { return null; } return $this->selectStandsAtAirlineSpecificTerminals( $aircraft, - fn(Builder $query) => $query->whereNull('airline_terminal.destination') - ->whereNull('airline_terminal.callsign_slug') - ->whereNull('airline_terminal.full_callsign') - ->whereNull('airline_terminal.aircraft_id') + $this->queryFilter() ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // If the aircraft doesnt have an airline, we cant allocate a stand + if ($aircraft->airline_id === null || $aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedStandsAtAirlineSpecificTerminals( + $aircraft, + $this->queryFilter() + ); + } + + private function queryFilter(): Closure + { + return fn(Builder $query) => $query->whereNull('airline_terminal.destination') + ->whereNull('airline_terminal.callsign_slug') + ->whereNull('airline_terminal.full_callsign') + ->whereNull('airline_terminal.aircraft_id'); + } } diff --git a/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php b/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php index 21dc4d526..e91294e89 100644 --- a/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php +++ b/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php @@ -4,13 +4,15 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; /** * A fallback allocator for cargo airlines. Will allocate any * cargo stand to any airline that is type cargo. */ -class CargoAirlineFallbackStandAllocator implements ArrivalStandAllocator +class CargoAirlineFallbackStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use ChecksForCargoAirlines; use SelectsStandsUsingStandardConditions; @@ -31,13 +33,30 @@ public function __construct(AirlineService $airlineService) */ public function allocate(NetworkAircraft $aircraft): ?int { - if (!$this->isCargoAirline($aircraft)) { + if ($aircraft->aircraft_id === null || !$this->isCargoAirline($aircraft)) { return null; } return $this->selectStandsUsingStandardConditions( $aircraft, - fn(Builder $query) => $query->cargo(), + $this->queryFilter() ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + if ($aircraft->aircraft_id === null || !$this->isCargoAirline($aircraft)) { + return collect(); + } + + return $this->selectRankedStandsUsingStandardConditions( + $aircraft, + $this->queryFilter() + ); + } + + private function queryFilter(): Closure + { + return fn(Builder $query) => $query->cargo(); + } } diff --git a/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php b/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php index 7f9cd9e3c..b925cbf15 100644 --- a/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php +++ b/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php @@ -4,7 +4,9 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; /** * Secondary cargo stand allocator, with no airline preferences. Only concerned with FP remarks explicitly @@ -37,8 +39,24 @@ public function allocate(NetworkAircraft $aircraft): ?int return $this->selectStandsUsingStandardConditions( $aircraft, - fn(Builder $query) => $query->cargo(), - $this->commonOrderByConditions + $this->queryFilter() ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + if (!$this->isCargoFlight($aircraft)) { + return collect(); + } + + return $this->selectRankedStandsUsingStandardConditions( + $aircraft, + $this->queryFilter() + ); + } + + private function queryFilter(): Closure + { + return fn(Builder $query) => $query->cargo(); + } } diff --git a/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php b/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php index 5fd8faed1..6d0f906b1 100644 --- a/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php +++ b/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php @@ -4,7 +4,9 @@ use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; /** * The primary arrival stand allocator for cargo. Looks for either a cargo airline @@ -34,7 +36,25 @@ public function allocate(NetworkAircraft $aircraft): ?int return $this->selectAirlineSpecificStands( $aircraft, - fn(Builder $query) => $query->cargo() + $this->queryFilter() ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + // If the aircraft doesnt have an airline, we cant allocate a stand + if (!$this->isCargoAirline($aircraft) && !$this->isCargoFlight($aircraft)) { + return collect(); + } + + return $this->selectRankedAirlineSpecificStands( + $aircraft, + $this->queryFilter() + ); + } + + private function queryFilter(): Closure + { + return fn(Builder $query) => $query->cargo(); + } } diff --git a/app/Allocator/Stand/DomesticInternationalStandAllocator.php b/app/Allocator/Stand/DomesticInternationalStandAllocator.php index a705b3117..363c229ea 100644 --- a/app/Allocator/Stand/DomesticInternationalStandAllocator.php +++ b/app/Allocator/Stand/DomesticInternationalStandAllocator.php @@ -3,25 +3,44 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; use Illuminate\Support\Str; -class DomesticInternationalStandAllocator implements ArrivalStandAllocator +class DomesticInternationalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsStandsUsingStandardConditions; public function allocate(NetworkAircraft $aircraft): ?int { - if (!$aircraft->planned_depairport) { + if (!$aircraft->planned_depairport || !$aircraft->aircraft_id) { return null; } return $this->selectStandsUsingStandardConditions( $aircraft, - fn(Builder $query) => $this->getDomesticInternationalScope($aircraft, $query) + $this->queryFilter($aircraft) ); } + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + if (!$aircraft->planned_depairport || !$aircraft->aircraft_id) { + return collect(); + } + + return $this->selectRankedStandsUsingStandardConditions( + $aircraft, + $this->queryFilter($aircraft) + ); + } + + private function queryFilter(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) => $this->getDomesticInternationalScope($aircraft, $query); + } + protected function getDomesticInternationalScope(NetworkAircraft $aircraft, Builder $builder): Builder { return $this->isDomestic($aircraft) diff --git a/app/Allocator/Stand/FallbackArrivalStandAllocator.php b/app/Allocator/Stand/FallbackArrivalStandAllocator.php index bdf8702a8..bb8ebd014 100644 --- a/app/Allocator/Stand/FallbackArrivalStandAllocator.php +++ b/app/Allocator/Stand/FallbackArrivalStandAllocator.php @@ -3,9 +3,11 @@ namespace App\Allocator\Stand; use App\Models\Vatsim\NetworkAircraft; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class FallbackArrivalStandAllocator implements ArrivalStandAllocator +class FallbackArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsStandsUsingStandardConditions; @@ -21,10 +23,30 @@ class FallbackArrivalStandAllocator implements ArrivalStandAllocator */ public function allocate(NetworkAircraft $aircraft): ?int { + if ($aircraft->aircraft_id === null) { + return null; + } + return $this->selectStandsUsingStandardConditions( $aircraft, - fn(Builder $query) => $query->notCargo(), - $this->commonOrderByConditions + $this->filterQuery(), + ); + } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + if ($aircraft->aircraft_id === null) { + return collect(); + } + + return $this->selectRankedStandsUsingStandardConditions( + $aircraft, + $this->filterQuery(), ); } + + private function filterQuery(): Closure + { + return fn(Builder $query) => $query->notCargo(); + } } diff --git a/app/Allocator/Stand/OriginAirfieldStandAllocator.php b/app/Allocator/Stand/OriginAirfieldStandAllocator.php index 5cfb565d6..9004bdebc 100644 --- a/app/Allocator/Stand/OriginAirfieldStandAllocator.php +++ b/app/Allocator/Stand/OriginAirfieldStandAllocator.php @@ -4,9 +4,11 @@ use App\Allocator\UsesDestinationStrings; use App\Models\Vatsim\NetworkAircraft; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; -class OriginAirfieldStandAllocator implements ArrivalStandAllocator +class OriginAirfieldStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsStandsUsingStandardConditions; use UsesDestinationStrings; @@ -24,9 +26,27 @@ public function allocate(NetworkAircraft $aircraft): ?int return $this->selectStandsUsingStandardConditions( $aircraft, - fn(Builder $query) => $query->whereIn('origin_slug', $this->getDestinationStrings($aircraft)) - ->notCargo(), + $this->filterQuery($aircraft), self::ORDER_BYS, ); } + + public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection + { + if (!$aircraft->planned_depairport) { + return collect(); + } + + return $this->selectRankedStandsUsingStandardConditions( + $aircraft, + $this->filterQuery($aircraft), + self::ORDER_BYS, + ); + } + + private function filterQuery(NetworkAircraft $aircraft): Closure + { + return fn(Builder $query) + => $query->notCargo()->whereIn('origin_slug', $this->getDestinationStrings($aircraft)); + } } diff --git a/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php index 7da6e6705..db54d0e45 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php @@ -462,7 +462,7 @@ public function testItGetsRankedStandAllocation() $this->assertEquals($expectedRanks, $actualRanks); } - private function newAircraft( + private function createAircraft( string $callsign, string $arrivalAirport, string $departureAirport, @@ -474,7 +474,7 @@ private function newAircraft( ); } - private function createAircraft( + private function newAircraft( string $callsign, string $arrivalAirport, string $departureAirport, diff --git a/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php index 9892d3fb0..f4ff1afdf 100644 --- a/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php @@ -4,6 +4,7 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Stand\StandRequest; @@ -488,6 +489,12 @@ public function testItDoesntRankStandsIfUnknownAirline() $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); } + public function testItDoesntRankStandsIfUnknownAircraft() + { + $aircraft = $this->newAircraft('BAW1234', 'EGLL', 'EGGD', 'C172'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + public function testItGetsRankedStandAllocation() { // Create an airfield that we dont have so we know its a clean test @@ -579,15 +586,6 @@ public function testItGetsRankedStandAllocation() ); $standE2->airlines()->sync([1]); - // Should not appear in rankings - no callsign - $standE2 = Stand::factory()->create( - [ - 'airfield_id' => $airfieldId, - 'identifier' => 'E2', - ] - ); - $standE2->airlines()->sync([1]); - // Should not appear in rankings - too small ARC $standF1 = Stand::factory()->create( [ @@ -641,7 +639,8 @@ public function testItGetsRankedStandAllocation() $this->assertEquals($expectedRanks, $actualRanks); } - private function newAircraft( + + private function createAircraft( string $callsign, string $arrivalAirport, string $departureAirport, @@ -653,13 +652,13 @@ private function newAircraft( ); } - private function createAircraft( + private function newAircraft( string $callsign, string $arrivalAirport, string $departureAirport, string $aircraftType = 'B738' ): NetworkAircraft { - return NetworkAircraft::create( + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, diff --git a/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php index 216986109..cf9db1471 100644 --- a/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php @@ -4,8 +4,13 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class AirlineDestinationArrivalStandAllocatorTest extends BaseFunctionalTestCase @@ -19,6 +24,7 @@ public function setUp(): void { parent::setUp(); $this->allocator = $this->app->make(AirlineDestinationArrivalStandAllocator::class); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandWithAFixedDestination() @@ -480,21 +486,191 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItDoesntRankStandsIfUnknownAircraft() + { + $aircraft = $this->newAircraft('BAW1234', 'EGLL', 'EGGD', 'C172'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. It gets a stand reservation to make + // sure it is ranked first even if it is occupied. + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + ] + ); + $standA1->airlines()->sync([1 => ['priority' => 100, 'destination' => 'EGGD']]); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, lower priority than A1 + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + $standB1->airlines()->sync([1 => ['priority' => 101, 'destination' => 'EGGD']]); + $standB2->airlines()->sync([1 => ['priority' => 101, 'destination' => 'EGGD']]); + + // Should be ranked joint third, same priority as lB1 and B2 but smaller stands + $standC1 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C1']); + $standC1->airlines()->sync([1 => ['priority' => 101, 'destination' => 'EGGD']]); + $standC2 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C2']); + $standC2->airlines()->sync([1 => ['priority' => 101, 'destination' => 'EGGD']]); + + // Should be ranked 4th, 5th, 6th less specific destinations + $standC3 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C3']); + $standC3->airlines()->sync([1 => ['priority' => 101, 'destination' => 'EGG']]); + $standC4 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C4']); + $standC4->airlines()->sync([1 => ['priority' => 101, 'destination' => 'EG']]); + $standC5 = Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'C5']); + $standC5->airlines()->sync([1 => ['priority' => 101, 'destination' => 'E']]); + + // Should not appear in rankings - wrong airfield + $standD1 = Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1']); + $standD1->airlines()->sync([1 => ['destination' => 'EGGD']]); + + // Should not appear in rankings - wrong destination + $standE1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + ] + ); + $standE1->airlines()->sync([1 => ['destination' => 'XYZ']]); + + // Should not appear in rankings - no destination + $standE2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E2', + ] + ); + $standE2->airlines()->sync([1]); + + // Should not appear in rankings - too small ARC + $standF1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + $standF1->airlines()->sync([1 => ['destination' => 'EGGD']]); + + // Should not appear in rankings - too small max aircraft size + $standG1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + $standG1->airlines()->sync([1 => ['destination' => 'EGGD']]); + + // Should not appear in rankings - closed + $standH1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'C', + 'closed_at' => Carbon::now(), + ] + ); + $standH1->airlines()->sync([1 => ['destination' => 'EGGD']]); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + $standC3->id => 4, + $standC4->id => 5, + $standC5->id => 6, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } private function createAircraft( string $callsign, string $arrivalAirport, - string $departureAirport + string $departureAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport, + string $aircraftType = 'B738' ): NetworkAircraft { - return NetworkAircraft::create( + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, - 'airline_id' => 1, - 'aircraft_id' => 1, + 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php index a0da1722b..031a42f26 100644 --- a/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php @@ -4,9 +4,14 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airfield\Terminal; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -18,6 +23,7 @@ public function setUp(): void { parent::setUp(); $this->allocator = $this->app->make(AirlineDestinationTerminalArrivalStandAllocator::class); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandWithAFixedCallsignSlug() @@ -502,21 +508,257 @@ public function testItDoesntAllocateNonExistentAirlines() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAircraftType() + { + $aircraft = $this->newAircraft('BAW23451', 'EGLL', 'EGGD', aircraftType: 'XXX'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. Both stands on the terminal should be + // included. Stand A1 gets a reservation and a request so that we show its not considered. + $terminalA1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalA1->airlines()->sync([1 => ['destination' => 'EGGD', 'priority' => 100]]); + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A1', + ] + ); + $standA2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A2', + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standA1->id]); + + // Should be ranked joint second, lower priority than A1. + $terminalB1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB1->airlines()->sync([1 => ['destination' => 'EGGD', 'priority' => 101]]); + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB1->id, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + + $terminalB2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB2->airlines()->sync([1 => ['destination' => 'EGGD', 'priority' => 101]]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB2->id, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + + // Should be ranked joint third, same priority as B1 and B2 but smaller stands + $terminalC1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC1->airlines()->sync([1 => ['destination' => 'EGGD', 'priority' => 101]]); + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'terminal_id' => $terminalC1->id, + ] + ); + + $terminalC2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC2->airlines()->sync([1 => ['destination' => 'EGGD', 'priority' => 101]]); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'terminal_id' => $terminalC2->id, + ] + ); + + // Should be ranked 4th, 5th, 6th, 7th, less specific destinations + $terminalC3 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC3->airlines()->sync([1 => ['destination' => 'EGG', 'priority' => 101]]); + $standC3 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C3', + 'terminal_id' => $terminalC3->id, + ] + ); + + $terminalC4 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC4->airlines()->sync([1 => ['destination' => 'EG', 'priority' => 101]]); + $standC4 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C4', + 'terminal_id' => $terminalC4->id, + ] + ); + + $terminalC5 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC5->airlines()->sync([1 => ['destination' => 'E', 'priority' => 101]]); + $standC5 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C5', + 'terminal_id' => $terminalC5->id, + ] + ); + + // Should not appear in rankings - wrong airfield + Terminal::find(1)->airlines()->sync([1 => ['destination' => 'EGGD', 'priority' => 101]]); + Stand::factory()->create( + [ + 'airfield_id' => 1, + 'identifier' => 'D1', + 'terminal_id' => 1 + ] + ); + + // Should not appear in rankings - wrong terminal + $terminalD2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'D1', + 'terminal_id' => $terminalD2->id + ] + ); + + // Should not appear in rankings - wrong destination + $terminalE1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE1->airlines()->sync([1 => ['destination' => 'xxxx']]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E1']); + + // Should not appear in rankings - no destination + $terminalE2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE2->airlines()->sync([1]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E2']); + + // Should not appear in rankings - too small ARC + $terminalF1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalF1->airlines()->sync([1 => ['destination' => 'EGGD']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + + // Should not appear in rankings - too small max aircraft size + $terminalG1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalG1->airlines()->sync([1 => ['destination' => 'EGGD']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + + + // Should not appear in rankings - closed + $terminalH1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalH1->airlines()->sync([1 => ['destination' => 'EGGD']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'closed_at' => Carbon::now() + ] + ); + + + $expectedRanks = [ + $standA1->id => 1, + $standA2->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + $standC3->id => 4, + $standC4->id => 5, + $standC5->id => 6, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $arrivalAirport, - string $departureAirport + string $departureAirport, + string $aircraftType = 'B738' + ): NetworkAircraft { + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport, + string $aircraftType = 'B738' ): NetworkAircraft { - return NetworkAircraft::create( + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, - 'airline_id' => Str::substr($callsign, 0, 3) === 'BAW' ? 1 : null, - 'aircraft_id' => 1, + 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php index faf36ef22..eceef8fb5 100644 --- a/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php @@ -4,10 +4,14 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airfield\Terminal; use App\Models\Airline\Airline; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class AirlineTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase @@ -24,6 +28,7 @@ public function setUp(): void Airline::where('icao_code', 'BAW')->first()->terminals()->attach(2); Stand::find(1)->update(['terminal_id' => 1]); Stand::find(2)->update(['terminal_id' => 2]); + Airline::factory()->create(['icao_code' => 'EZY']); } public function testItAllocatesAStandAtTheRightTerminal() @@ -226,21 +231,233 @@ public function testItReturnsNullOnNoStandAllocated() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItDoesntRankStandsIfUnknownAircraftType() + { + $aircraft = $this->newAircraft('BAW23451', 'EGLL', 'EGGD', aircraftType: 'XXX'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('***1234', 'EGLL', 'EGGD'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. Both stands on the terminal should be + // included. Stand A1 gets a reservation and a request so that we show its not considered. + $terminalA1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalA1->airlines()->sync([1 => ['priority' => 100]]); + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A1', + ] + ); + $standA2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalA1->id, + 'identifier' => 'A2', + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standA1->id]); + + // Should be ranked joint second, lower priority than A1. + $terminalB1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB1->airlines()->sync([1 => ['priority' => 101]]); + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB1->id, + 'identifier' => 'B1', + 'aerodrome_reference_code' => 'C' + ] + ); + + $terminalB2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalB2->airlines()->sync([1 => ['priority' => 101]]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'terminal_id' => $terminalB2->id, + 'identifier' => 'B2', + 'aerodrome_reference_code' => 'C' + ] + ); + + // Should be ranked joint third, same priority as B1 and B2 but smaller stands + $terminalC1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC1->airlines()->sync([1 => ['priority' => 101]]); + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'terminal_id' => $terminalC1->id, + ] + ); + + $terminalC2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalC2->airlines()->sync([1 => ['priority' => 101]]); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'terminal_id' => $terminalC2->id, + ] + ); + + // Should not appear in rankings - wrong airfield + Terminal::find(1)->airlines()->sync([1 => ['priority' => 101]]); + Stand::factory()->create( + [ + 'airfield_id' => 1, + 'identifier' => 'D1', + 'terminal_id' => 1 + ] + ); + + // Should not appear in rankings - wrong terminal + $terminalD2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'D1', + 'terminal_id' => $terminalD2->id + ] + ); + + // Should not appear in rankings - has a full callsign + $terminalE1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE1->airlines()->sync([1 => ['full_callsign' => 'xxxx']]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E1']); + + // Should not appear in rankings - has a callsign_slug + $terminalE2 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE2->airlines()->sync([1 => ['callsign_slug' => 'xxxx']]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E2']); + + // Should not appear in rankings - has a specific aircraft_type + $terminalE3 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE3->airlines()->sync([1 => ['aircraft_id' => 1]]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E3']); + + // Should not appear in rankings - has a specific destination + $terminalE4 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalE4->airlines()->sync([1 => ['destination' => 'ABC']]); + Stand::factory()->create(['airfield_id' => $airfieldId, 'identifier' => 'E4']); + + // Should not appear in rankings - too small ARC + $terminalF1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalF1->airlines()->sync([1 => ['full_callsign' => '23451']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + + // Should not appear in rankings - too small max aircraft size + $terminalG1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalG1->airlines()->sync([1 => ['full_callsign' => '23451']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + + + // Should not appear in rankings - closed + $terminalH1 = Terminal::factory()->create(['airfield_id' => $airfieldId]); + $terminalH1->airlines()->sync([1 => ['full_callsign' => '23451']]); + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'closed_at' => Carbon::now() + ] + ); + + + $expectedRanks = [ + $standA1->id => 1, + $standA2->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $arrivalAirport, - string $departureAirport = 'EGGD' + string $departureAirport = 'EGGD', + string $aircraftType = 'B738' + ): NetworkAircraft { + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport, + string $aircraftType = 'B738' ): NetworkAircraft { - return NetworkAircraft::create( + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B738', - 'planned_aircraft_short' => 'B738', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, - 'airline_id' => $callsign === 'BAW23451' ? 1 : null, - 'aircraft_id' => 1, + 'aircraft_id' => $aircraftType === 'B738' ? 1 : null, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php b/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php index 9aab8749b..233fd81ba 100644 --- a/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php @@ -4,11 +4,15 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Stand\StandAssignment; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Stand\StandType; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; class CargoAirlineFallbackStandAllocatorTest extends BaseFunctionalTestCase { @@ -115,18 +119,182 @@ public function testItDoesntAllocateCargoStandsIfNoAirline() $this->assertNull($allocation); } + public function testItDoesntRankStandsIfUnknownAircraft() + { + Airline::where('icao_code', 'VIR')->update(['is_cargo' => true]); + $aircraft = $this->newAircraft('VIR22F', 'EGLL', 'EGGD', 'C172'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Make Virgin a cargo airline + Airline::where('icao_code', 'VIR')->update(['is_cargo' => true]); + + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 0.5, + 'length' => 0.6, + ] + ); + + // Should be ranked first - its the smallest stand that's applicable + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + 'assignment_priority' => 100, + 'aerodrome_reference_code' => 'E', + 'type_id' => 3, + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, bigger than A1, but same priority + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'assignment_priority' => 100, + 'type_id' => 3, + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'assignment_priority' => 100, + 'type_id' => 3, + ] + ); + + // Should be ranked joint third, same size as B1 and B2, but lower priority + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'assignment_priority' => 101, + 'type_id' => 3, + ] + ); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'assignment_priority' => 101, + 'type_id' => 3, + ] + ); + + // Should not appear in rankings - wrong airfield + Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1', 'type_id' => 3]); + + // Should not appear in rankings - not cargo + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + 'type_id' => 2, + ] + ); + + // Should not appear in rankings - too small ARC + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A', + 'type_id' => 3, + ] + ); + + // Should not appear in rankings - too small max aircraft size + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id, + 'type_id' => 3, + ] + ); + + // Should not appear in rankings - closed + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'E', + 'closed_at' => Carbon::now(), + 'type_id' => 3, + ] + ); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('VIR22F', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } private function createAircraft( string $callsign, - string $arrivalAirport + string $arrivalAirport, + string $departureAirport = 'EGGD', + string $aircraftType = 'B744' + ): NetworkAircraft { + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport = 'EGGD', + string $aircraftType = 'B744' ): NetworkAircraft { - return NetworkAircraft::create( + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B744', - 'planned_aircraft_short' => 'B744', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, - 'aircraft_id' => Aircraft::where('code', 'B744')->first()->id, + 'planned_depairport' => $departureAirport, + 'aircraft_id' => Aircraft::where('code', $aircraftType)->first()?->id, + 'airline_id' => match ($callsign) { + 'BAW23451' => 1, + 'EZY7823' => Airline::where('icao_code', 'EZY')->first()->id, + 'VIR22F' => Airline::where('icao_code', 'VIR')->first()->id, + default => null, + }, ] ); } diff --git a/tests/app/Allocator/Stand/CargoFlightArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/CargoFlightArrivalStandAllocatorTest.php index f5e1c073e..57b421f10 100644 --- a/tests/app/Allocator/Stand/CargoFlightArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/CargoFlightArrivalStandAllocatorTest.php @@ -4,10 +4,15 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Stand\StandAssignment; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Stand\StandType; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; class CargoFlightArrivalStandAllocatorTest extends BaseFunctionalTestCase { @@ -114,18 +119,172 @@ public function testItDoesntAllocateCargoStandsIfFlightplanNotCargo() $this->assertNull($allocation); } + public function testItDoesntRankStandsIfUnknownAircraft() + { + $aircraft = $this->newAircraft('BAW1234', 'EGLL', 'C172'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 0.5, + 'length' => 0.6, + ] + ); + + // Should be ranked first - its the smallest stand that's applicable + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + 'assignment_priority' => 100, + 'aerodrome_reference_code' => 'E', + 'type_id' => 3, + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, bigger than A1, but same priority + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'assignment_priority' => 100, + 'type_id' => 3, + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'assignment_priority' => 100, + 'type_id' => 3, + ] + ); + + // Should be ranked joint third, same size as B1 and B2, but lower priority + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'assignment_priority' => 101, + 'type_id' => 3, + ] + ); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'assignment_priority' => 101, + 'type_id' => 3, + ] + ); + + // Should not appear in rankings - wrong airfield + Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1', 'type_id' => 3]); + + // Should not appear in rankings - not cargo + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + 'type_id' => 2, + ] + ); + + // Should not appear in rankings - too small ARC + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A', + 'type_id' => 3, + ] + ); + + // Should not appear in rankings - too small max aircraft size + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id, + 'type_id' => 3, + ] + ); + + // Should not appear in rankings - closed + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'E', + 'closed_at' => Carbon::now(), + 'type_id' => 3, + ] + ); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $aircraft = $this->newAircraft('VIR22F', $airfield->code); + $aircraft->remarks = 'Some stuff RMK/CARGO Some more stuff'; + $actualRanks = $this->allocator->getRankedStandAllocation( + $aircraft + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } private function createAircraft( string $callsign, - string $arrivalAirport + string $arrivalAirport, + string $aircraftType = 'B744' + ): NetworkAircraft { + return tap( + $this->newAircraft($callsign, $arrivalAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $aircraftType = 'B744' ): NetworkAircraft { - return NetworkAircraft::create( + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B744', - 'planned_aircraft_short' => 'B744', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, - 'aircraft_id' => Aircraft::where('code', 'B744')->first()->id, + 'airline_id' => Airline::where('icao_code', 'VIR')->first()->id, + 'aircraft_id' => Aircraft::where('code', $aircraftType)->first()?->id, ] ); } diff --git a/tests/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocatorTest.php index 90d881b0b..1824b4aa6 100644 --- a/tests/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocatorTest.php @@ -4,12 +4,17 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Stand\StandAssignment; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Stand\StandType; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; class CargoFlightPreferredArrivalStandAllocatorTest extends BaseFunctionalTestCase { @@ -126,19 +131,187 @@ public function testItDoesntAllocateCargoStandsIfNoAirline() $this->assertNull($allocation); } + public function testItDoesntRankStandsIfUnknownAircraft() + { + $aircraft = $this->newAircraft('BAW1234', 'EGLL', 'C172'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItDoesntRankStandsIfUnknownAirline() + { + $aircraft = $this->newAircraft('XXX123', 'EGLL'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 0.5, + 'length' => 0.6, + ] + ); + + // Should be ranked first - its the smallest stand that's applicable + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + 'assignment_priority' => 100, + 'aerodrome_reference_code' => 'E', + 'type_id' => 3, + ] + ); + $standA1->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 100]]); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, bigger than A1, but same priority + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'assignment_priority' => 100, + 'type_id' => 3, + ] + ); + $standB1->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 100]]); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'assignment_priority' => 100, + 'type_id' => 3, + ] + ); + $standB2->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 100]]); + + // Should be ranked joint third, same size as B1 and B2, but lower priority + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'assignment_priority' => 101, + 'type_id' => 3, + ] + ); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'assignment_priority' => 101, + 'type_id' => 3, + ] + ); + $standC1->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 101]]); + $standC2->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 101]]); + + // Should not appear in rankings - wrong airfield + $standD1 = Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1', 'type_id' => 3]); + $standD1->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 100]]); + + // Should not appear in rankings - not cargo + $standE1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + 'type_id' => 2, + ] + ); + $standE1->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 100]]); + + // Should not appear in rankings - too small ARC + $standF1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A', + 'type_id' => 3, + ] + ); + $standF1->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 100]]); + + // Should not appear in rankings - too small max aircraft size + $standG1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id, + 'type_id' => 3, + ] + ); + $standG1->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 100]]); + + // Should not appear in rankings - closed + $standH1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'E', + 'closed_at' => Carbon::now(), + 'type_id' => 3, + ] + ); + $standH1->airlines()->sync([Airline::where('icao_code', 'VIR')->first()->id => ['priority' => 100]]); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('VIR22F', $airfield->code) + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, - string $arrivalAirport + string $arrivalAirport, + string $aircraftType = 'B744' + ): NetworkAircraft { + return tap( + $this->newAircraft($callsign, $arrivalAirport, $aircraftType), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $aircraftType = 'B744' ): NetworkAircraft { - return NetworkAircraft::create( + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, - 'planned_aircraft' => 'B744', - 'planned_aircraft_short' => 'B744', + 'planned_aircraft' => $aircraftType, + 'planned_aircraft_short' => $aircraftType, 'planned_destairport' => $arrivalAirport, - 'airline_id' => Airline::where('icao_code', 'VIR')->first()->id, - 'aircraft_id' => Aircraft::where('code', 'B744')->first()->id, + 'airline_id' => Airline::where('icao_code', Str::substr($callsign, 0, 3))->first()?->id, + 'aircraft_id' => Aircraft::where('code', $aircraftType)->first()?->id, ] ); } diff --git a/tests/app/Allocator/Stand/DomesticInternationalStandAllocatorTest.php b/tests/app/Allocator/Stand/DomesticInternationalStandAllocatorTest.php index c72021da6..fc1584169 100644 --- a/tests/app/Allocator/Stand/DomesticInternationalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/DomesticInternationalStandAllocatorTest.php @@ -4,10 +4,14 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Stand\Stand; use App\Models\Stand\StandAssignment; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Stand\StandType; use App\Models\Vatsim\NetworkAircraft; +use Illuminate\Support\Carbon; class DomesticInternationalStandAllocatorTest extends BaseFunctionalTestCase { @@ -176,13 +180,299 @@ public function testItReturnsNothingOnNoStandAllocated() $this->assertNull($this->allocator->allocate($this->createAircraft('BAW898', 'B738', 'XXXX'))); } + public function testItDoesntRankStandsIfUnknownAircraft() + { + $aircraft = $this->newAircraft('BAW123', 'XXX', 'EGLL', 'EIDW'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocationForDomestic() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - its the smallest stand that's applicable + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + 'assignment_priority' => 100, + 'aerodrome_reference_code' => 'E', + 'type_id' => 1, + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, bigger than A1, but same priority + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'assignment_priority' => 100, + 'type_id' => 1, + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'assignment_priority' => 100, + 'type_id' => 1, + ] + ); + + // Should be ranked joint third, same size as B1 and B2, but lower priority + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'assignment_priority' => 101, + 'type_id' => 1, + ] + ); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'assignment_priority' => 101, + 'type_id' => 1, + ] + ); + + // Should not appear in rankings - wrong airfield + Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1', 'type_id' => 1]); + + // Should not appear in rankings - not domestic + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + 'type_id' => 2, + ] + ); + + // Should not appear in rankings - too small ARC + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A', + 'type_id' => 1, + ] + ); + + // Should not appear in rankings - too small max aircraft size + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id, + 'type_id' => 1, + ] + ); + + // Should not appear in rankings - closed + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'E', + 'closed_at' => Carbon::now(), + 'type_id' => 1, + ] + ); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('VIR22F', 'B738', $airfield->code, 'EGLL') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + + public function testItGetsRankedStandAllocationForInternational() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - its the smallest stand that's applicable + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + 'assignment_priority' => 100, + 'aerodrome_reference_code' => 'E', + 'type_id' => 2, + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, bigger than A1, but same priority + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'assignment_priority' => 100, + 'type_id' => 2, + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'assignment_priority' => 100, + 'type_id' => 2, + ] + ); + + // Should be ranked joint third, same size as B1 and B2, but lower priority + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'assignment_priority' => 101, + 'type_id' => 2, + ] + ); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'assignment_priority' => 101, + 'type_id' => 2, + ] + ); + + // Should not appear in rankings - wrong airfield + Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1', 'type_id' => 1]); + + // Should not appear in rankings - is domestic + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + 'type_id' => 1, + ] + ); + + // Should not appear in rankings - too small ARC + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A', + 'type_id' => 1, + ] + ); + + // Should not appear in rankings - too small max aircraft size + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id, + 'type_id' => 1, + ] + ); + + // Should not appear in rankings - closed + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'E', + 'closed_at' => Carbon::now(), + 'type_id' => 1, + ] + ); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('VIR22F', 'B738', $airfield->code, 'KJFK') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $type, string $arrivalAirport, string $departureAirport = 'EGKK' ): NetworkAircraft { - return NetworkAircraft::create( + return tap( + $this->newAircraft($callsign, $type, $arrivalAirport, $departureAirport), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $type, + string $arrivalAirport, + string $departureAirport = 'EGKK' + ): NetworkAircraft { + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, @@ -190,7 +480,7 @@ private function createAircraft( 'planned_aircraft_short' => $type, 'planned_destairport' => $arrivalAirport, 'planned_depairport' => $departureAirport, - 'aircraft_id' => Aircraft::where('code', $type)->first()->id, + 'aircraft_id' => Aircraft::where('code', $type)->first()?->id, ] ); } diff --git a/tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php index 413053a11..194142298 100644 --- a/tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/FallbackArrivalStandAllocatorTest.php @@ -4,9 +4,13 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Stand\StandType; use App\Models\Vatsim\NetworkAircraft; +use Carbon\Carbon; class FallbackArrivalStandAllocatorTest extends BaseFunctionalTestCase { @@ -223,12 +227,156 @@ public function testItReturnsNothingOnNoStandAllocated() $this->assertNull($this->allocator->allocate($this->createAircraft('BAW898', 'B738', 'XXXX'))); } + public function testItDoesntRankStandsIfUnknownAircraft() + { + $aircraft = $this->newAircraft('BAW123', 'XXX', 'EGLL', 'EIDW'); + $this->assertEquals(collect(), $this->allocator->getRankedStandAllocation($aircraft)); + } + + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - its the smallest stand that's applicable + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + 'assignment_priority' => 100, + 'aerodrome_reference_code' => 'E', + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, bigger than A1, but same priority + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'assignment_priority' => 100, + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'assignment_priority' => 100, + ] + ); + + // Should be ranked joint third, same size as B1 and B2, but lower priority + $standC1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C1', + 'assignment_priority' => 101, + ] + ); + $standC2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C2', + 'assignment_priority' => 101, + ] + ); + + // Should not appear in rankings - wrong airfield + Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1', 'type_id' => 1]); + + // Should not appear in rankings - is cargo + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + 'type_id' => 3, + ] + ); + + // Should not appear in rankings - too small ARC + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A' + ] + ); + + // Should not appear in rankings - too small max aircraft size + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id + ] + ); + + // Should not appear in rankings - closed + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'E', + 'closed_at' => Carbon::now() + ] + ); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('VIR22F', 'B738', $airfield->code) + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $type, string $arrivalAirport ): NetworkAircraft { - return NetworkAircraft::create( + return tap( + $this->newAircraft($callsign, $type, $arrivalAirport), + fn(NetworkAircraft $aircraft) => + $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $type, + string $arrivalAirport + ): NetworkAircraft { + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, diff --git a/tests/app/Allocator/Stand/OriginAirfieldStandAllocatorTest.php b/tests/app/Allocator/Stand/OriginAirfieldStandAllocatorTest.php index 6fd3ba6d7..b99c0c867 100644 --- a/tests/app/Allocator/Stand/OriginAirfieldStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/OriginAirfieldStandAllocatorTest.php @@ -4,8 +4,12 @@ use App\BaseFunctionalTestCase; use App\Models\Aircraft\Aircraft; +use App\Models\Airfield\Airfield; use App\Models\Stand\Stand; +use App\Models\Stand\StandRequest; +use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; +use Carbon\Carbon; class OriginAirfieldStandAllocatorTest extends BaseFunctionalTestCase { @@ -252,13 +256,190 @@ public function testItDoesntAllocateAStandWithNoDestination() $this->assertNull($this->allocator->allocate($aircraft)); } + public function testItGetsRankedStandAllocation() + { + // Create an airfield that we dont have so we know its a clean test + $airfield = Airfield::factory()->create(['code' => 'EXXX']); + $airfieldId = $airfield->id; + + // Create a small aircraft type to test stand size ranking + $cessna = Aircraft::create( + [ + 'code' => 'C172', + 'allocate_stands' => true, + 'aerodrome_reference_code' => 'A', + 'wingspan' => 1, + 'length' => 12, + ] + ); + + // Should be ranked first - it has the highest priority. It gets a stand reservation to make + // sure it is ranked first even if it is occupied. + $standA1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'A1', + 'origin_slug' => 'EGGD', + 'assignment_priority' => 100, + 'aerodrome_reference_code' => 'C' + ] + ); + StandReservation::create( + [ + 'stand_id' => $standA1->id, + 'start' => Carbon::now()->subMinutes(1), + 'end' => Carbon::now()->addMinutes(1), + ] + ); + + // Should be ranked joint second, bigger than A1 but same priority + $standB1 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B1', + 'origin_slug' => 'EGGD', + 'assignment_priority' => 100, + ] + ); + StandRequest::factory()->create(['requested_time' => Carbon::now(), 'stand_id' => $standB1->id]); + $standB2 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'B2', + 'origin_slug' => 'EGGD', + 'assignment_priority' => 100, + ] + ); + + // Should be ranked joint third, same size as B1 but lower priority + $standC1 = Stand::factory()->create( + ['airfield_id' => $airfieldId, 'identifier' => 'C1', 'origin_slug' => 'EGGD', 'assignment_priority' => 101] + ); + $standC2 = Stand::factory()->create( + ['airfield_id' => $airfieldId, 'identifier' => 'C2', 'origin_slug' => 'EGGD', 'assignment_priority' => 101] + ); + + // Should be ranked 4th, 5th, 6th, less specific destinations slugs + $standC3 = Stand::factory()->create( + ['airfield_id' => $airfieldId, 'identifier' => 'C3', 'origin_slug' => 'EGG', 'assignment_priority' => 101] + ); + $standC4 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C4', + 'origin_slug' => 'EG', + 'assignment_priority' => 101 + ] + ); + $standC5 = Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'C5', + 'origin_slug' => 'E', + 'assignment_priority' => 101 + ] + ); + + // Should not appear in rankings - wrong airfield + Stand::factory()->create(['airfield_id' => 2, 'identifier' => 'D1']); + + // Should not appear in rankings - wrong origin slug + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E1', + 'origin_slug' => 'EGKK', + ] + ); + + // Should not appear in rankings - is cargo + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E2', + 'origin_slug' => 'EGGD', + 'type_id' => 3, + ] + ); + + // Should not appear in rankings - no origin slug + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'E3', + ] + ); + + // Should not appear in rankings - too small ARC + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'F1', + 'aerodrome_reference_code' => 'A', + 'origin_slug' => 'EGGD', + ] + ); + + // Should not appear in rankings - too small max aircraft size + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'G1', + 'max_aircraft_id_length' => $cessna->id, + 'max_aircraft_id_wingspan' => $cessna->id, + 'origin_slug' => 'EGGD', + ] + ); + + // Should not appear in rankings - closed + Stand::factory()->create( + [ + 'airfield_id' => $airfieldId, + 'identifier' => 'H1', + 'aerodrome_reference_code' => 'C', + 'closed_at' => Carbon::now(), + 'origin_slug' => 'EGGD', + ] + ); + + $expectedRanks = [ + $standA1->id => 1, + $standB1->id => 2, + $standB2->id => 2, + $standC1->id => 3, + $standC2->id => 3, + $standC3->id => 4, + $standC4->id => 5, + $standC5->id => 6 + ]; + + $actualRanks = $this->allocator->getRankedStandAllocation( + $this->newAircraft('BAW23451', $airfield->code, 'EGGD') + )->mapWithKeys( + fn($stand) => [$stand->id => $stand->rank] + ) + ->toArray(); + + $this->assertEquals($expectedRanks, $actualRanks); + } + private function createAircraft( string $callsign, string $arrivalAirport, string $departureAirport - ): NetworkAircraft - { - return NetworkAircraft::create( + ): NetworkAircraft { + return tap( + $this->newAircraft($callsign, $arrivalAirport, $departureAirport), + fn(NetworkAircraft $aircraft) => $aircraft->save() + ); + } + + private function newAircraft( + string $callsign, + string $arrivalAirport, + string $departureAirport + ): NetworkAircraft { + return new NetworkAircraft( [ 'callsign' => $callsign, 'cid' => 1234, From 6531a49d70ab9e3ae6aaf6463ca09b6d033d2303 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Sun, 3 Sep 2023 18:59:09 +0000 Subject: [PATCH 11/24] wip: stand predictor form --- app/Filament/Pages/StandPredictor.php | 24 ++++++ .../StandAssignmentsHistoryResource.php | 6 -- app/Http/Livewire/StandPredictorForm.php | 74 +++++++++++++++++++ app/Providers/AppServiceProvider.php | 23 ++++-- .../Stand/ArrivalAllocationService.php | 24 ++++++ .../filament/pages/stand-predictor.blade.php | 24 ++++++ .../livewire/stand-predictor-form.blade.php | 8 ++ 7 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 app/Filament/Pages/StandPredictor.php create mode 100644 app/Http/Livewire/StandPredictorForm.php create mode 100644 resources/views/filament/pages/stand-predictor.blade.php create mode 100644 resources/views/livewire/stand-predictor-form.blade.php diff --git a/app/Filament/Pages/StandPredictor.php b/app/Filament/Pages/StandPredictor.php new file mode 100644 index 000000000..444f69578 --- /dev/null +++ b/app/Filament/Pages/StandPredictor.php @@ -0,0 +1,24 @@ +currentPrediction = app()->make(ArrivalAllocationService::class) + ->getAllocationRankingForAircraft(new NetworkAircraft($data)); + } +} diff --git a/app/Filament/Resources/StandAssignmentsHistoryResource.php b/app/Filament/Resources/StandAssignmentsHistoryResource.php index 3a2eaf3d8..c1ced3eb0 100644 --- a/app/Filament/Resources/StandAssignmentsHistoryResource.php +++ b/app/Filament/Resources/StandAssignmentsHistoryResource.php @@ -62,12 +62,6 @@ protected static function shouldRegisterNavigation(): bool return self::userCanAccess(); } - public function mount(): void - { - dd(self::userCanAccess()); - abort_unless(self::userCanAccess(), 403); - } - public static function form(Form $form): Form { return $form diff --git a/app/Http/Livewire/StandPredictorForm.php b/app/Http/Livewire/StandPredictorForm.php new file mode 100644 index 000000000..3eac158db --- /dev/null +++ b/app/Http/Livewire/StandPredictorForm.php @@ -0,0 +1,74 @@ + 'You must select a valid stand.', + 'requestedTime' => 'Please enter a valid time.', + ]; + + public function getFormSchema(): array + { + return [ + Grid::make() + ->schema([ + TextInput::make('callsign') + ->placeholder('BAW123') + ->required() + ->label('Callsign'), + Select::make('aircraftType') + ->label('Aircraft Type') + ->options(SelectOptions::aircraftTypes()) + ->required() + ->searchable(), + Select::make('departureAirfield') + ->label('Departure Airfield') + ->options(Airfield::all()->mapWithKeys(fn($airfield) => [$airfield->code => $airfield->code])) + ->required() + ->searchable(), + Select::make('arrivalAirfield') + ->label('Arrival Airfield') + ->options(Airfield::all()->mapWithKeys(fn ($airfield) => [$airfield->code => $airfield->code])) + ->required() + ->searchable(), + ]) + ]; + } + + public function submit(): void + { + $this->form->validate(); + $this->emit('standPredictorFormSubmitted', [ + 'callsign' => $this->callsign, + 'cid' => Auth::id(), + 'aircraft_id' => $this->aircraftType, + 'airline_id' => app()->make(AirlineService::class)->airlineIdForCallsign($this->callsign), + 'planned_depairport' => $this->departureAirfield, + 'planned_destairport' => $this->arrivalAirfield, + ]); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index b0d2c45c5..547f01f1a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ use App\Http\Livewire\CurrentStandRequest; use App\Http\Livewire\RequestAStandForm; +use App\Http\Livewire\StandPredictorForm; use App\SocialiteProviders\CoreProvider; use Bugsnag\BugsnagLaravel\Facades\Bugsnag; use Filament\Facades\Filament; @@ -40,22 +41,25 @@ public function register() */ public function boot() { - Bugsnag::registerCallback(function ($report) { + Bugsnag::registerCallback(function ($report) + { if (Auth::check()) { $user = Auth::user(); $report->setUser([ - 'id' => $user->id, - 'name' => $user->name - ]); + 'id' => $user->id, + 'name' => $user->name + ]); } }); - Rule::macro('latitudeString', function () { + Rule::macro('latitudeString', function () + { return 'regex:' . SectorfileService::SECTORFILE_LATITUDE_REGEX; }); - Rule::macro('longitudeString', function () { + Rule::macro('longitudeString', function () + { return 'regex:' . SectorfileService::SECTORFILE_LONGITUDE_REGEX; }); @@ -63,7 +67,8 @@ public function boot() $socialite = $this->app->make(Factory::class); $socialite->extend( 'vatsimuk', - function ($app) use ($socialite) { + function ($app) use ($socialite) + { $config = $app['config']['services.vatsim_uk_core']; $config['redirect'] = route('auth.login.callback'); @@ -72,7 +77,8 @@ function ($app) use ($socialite) { ); // Filament styling - Filament::serving(function () { + Filament::serving(function () + { Filament::registerTheme(mix('css/vatukfilament.css')); Filament::registerNavigationGroups( [ @@ -93,5 +99,6 @@ function ($app) use ($socialite) { // Livewire Livewire::component('request-a-stand-form', RequestAStandForm::class); Livewire::component('current-stand-request', CurrentStandRequest::class); + Livewire::component('stand-predictor-form', StandPredictorForm::class); } } diff --git a/app/Services/Stand/ArrivalAllocationService.php b/app/Services/Stand/ArrivalAllocationService.php index 82dbc4642..c10e3d015 100644 --- a/app/Services/Stand/ArrivalAllocationService.php +++ b/app/Services/Stand/ArrivalAllocationService.php @@ -3,7 +3,9 @@ namespace App\Services\Stand; use App\Allocator\Stand\ArrivalStandAllocator; +use App\Allocator\Stand\RankableArrivalStandAllocator; use App\Models\Airfield\Airfield; +use App\Models\Stand\Stand; use App\Models\Stand\StandAssignment; use App\Models\Vatsim\NetworkAircraft; use App\Services\LocationService; @@ -128,4 +130,26 @@ public function getAllocators(): array { return $this->allocators; } + + public function getAllocationRankingForAircraft(NetworkAircraft $aircraft): array + { + $ranking = []; + + foreach ($this->allocators as $allocator) { + if (!$allocator instanceof RankableArrivalStandAllocator) { + continue; + } + + $ranking[get_class($allocator)] = $allocator->getRankedStandAllocation($aircraft) + ->groupBy('rank') + ->map( + fn(Collection $stands) => + $stands->sortBy('identifier', SORT_NATURAL)->map(fn(Stand $stand) => $stand->identifier)->values() + ) + ->values() + ->toArray(); + } + + return $ranking; + } } diff --git a/resources/views/filament/pages/stand-predictor.blade.php b/resources/views/filament/pages/stand-predictor.blade.php new file mode 100644 index 000000000..20f14df23 --- /dev/null +++ b/resources/views/filament/pages/stand-predictor.blade.php @@ -0,0 +1,24 @@ + + @livewire('stand-predictor-form') + + @if ($this->currentPrediction) + @foreach ($this->currentPrediction as $allocator => $groups) +

Allocator: {{ $allocator }}

+ @if (empty($groups)) + No stands for this allocator. + @continue + @endif + + @php + $rank = 0; + @endphp + @while ($rank < count($groups)) +

Rank {{ $rank + 1 }}

+ {{ implode(',', $groups[$rank]) }} + @php + $rank++; + @endphp + @endwhile + @endforeach + @endif +
diff --git a/resources/views/livewire/stand-predictor-form.blade.php b/resources/views/livewire/stand-predictor-form.blade.php new file mode 100644 index 000000000..152bf0913 --- /dev/null +++ b/resources/views/livewire/stand-predictor-form.blade.php @@ -0,0 +1,8 @@ +
+
+ {{ $this->form }} + + Predict + +
+
From f37e4c0650dce86593ce1dd9710fc446a583f04d Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Tue, 5 Sep 2023 17:57:18 +0000 Subject: [PATCH 12/24] fix: rankable allocators --- app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php | 2 +- app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php index 3e9167168..36a8db0a9 100644 --- a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; -class AirlineDestinationArrivalStandAllocator implements ArrivalStandAllocator +class AirlineDestinationArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use UsesDestinationStrings; use SelectsFromAirlineSpecificStands; diff --git a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php index 7039ca196..5b3699160 100644 --- a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; -class AirlineTerminalArrivalStandAllocator implements ArrivalStandAllocator +class AirlineTerminalArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsStandsFromAirlineSpecificTerminals; From d518cd9e209040b65da6fbd274590424148b3f1d Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Tue, 5 Sep 2023 17:59:13 +0000 Subject: [PATCH 13/24] feat: stand prediction form --- app/Filament/Pages/StandPredictor.php | 38 +++++- app/Http/Livewire/StandPredictorForm.php | 1 - .../Stand/ArrivalAllocationService.php | 13 +- .../filament/pages/stand-predictor.blade.php | 2 +- tests/BaseFilamentTestCase.php | 10 ++ .../app/Filament/Pages/StandPredictorTest.php | 111 ++++++++++++++++++ .../Http/Livewire/StandPredictorFormTest.php | 76 ++++++++++++ .../Stand/ArrivalAllocationServiceTest.php | 93 +++++++++++++++ 8 files changed, 331 insertions(+), 13 deletions(-) create mode 100644 tests/app/Filament/Pages/StandPredictorTest.php create mode 100644 tests/app/Http/Livewire/StandPredictorFormTest.php diff --git a/app/Filament/Pages/StandPredictor.php b/app/Filament/Pages/StandPredictor.php index 444f69578..08f35609b 100644 --- a/app/Filament/Pages/StandPredictor.php +++ b/app/Filament/Pages/StandPredictor.php @@ -2,23 +2,57 @@ namespace App\Filament\Pages; +use App\Models\Stand\Stand; +use App\Models\User\RoleKeys; use App\Models\Vatsim\NetworkAircraft; use App\Services\Stand\ArrivalAllocationService; use Filament\Pages\Page; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; class StandPredictor extends Page { protected static ?string $navigationIcon = 'heroicon-o-document-text'; - + protected static ?string $navigationLabel = 'Stand Predictor'; + protected static ?string $slug = 'stand-predictor'; protected static string $view = 'filament.pages.stand-predictor'; protected $listeners = ['standPredictorFormSubmitted']; private ?array $currentPrediction = null; + public static function shouldRegisterNavigation(): bool + { + return self::userCanAccess(); + } + + public function mount(): void + { + abort_unless(self::userCanAccess(), 403); + } + + private static function userCanAccess() + { + return Auth::user()->roles() + ->whereIn('key', [ + RoleKeys::OPERATIONS_TEAM, + RoleKeys::WEB_TEAM, + RoleKeys::DIVISION_STAFF_GROUP, + RoleKeys::OPERATIONS_CONTRIBUTOR, + ])->exists(); + } + public function standPredictorFormSubmitted(array $data) { $this->currentPrediction = app()->make(ArrivalAllocationService::class) - ->getAllocationRankingForAircraft(new NetworkAircraft($data)); + ->getAllocationRankingForAircraft(new NetworkAircraft($data)) + ->map( + fn(Collection $stands) => + $stands->map( + fn(Collection $standsForRank) => + $standsForRank->sortBy('identifier', SORT_NATURAL) + ->map(fn(Stand $stand) => $stand->identifier)->values() + ) + )->toArray(); } } diff --git a/app/Http/Livewire/StandPredictorForm.php b/app/Http/Livewire/StandPredictorForm.php index 3eac158db..914c4a01a 100644 --- a/app/Http/Livewire/StandPredictorForm.php +++ b/app/Http/Livewire/StandPredictorForm.php @@ -4,7 +4,6 @@ use App\Filament\Helpers\SelectOptions; use App\Models\Airfield\Airfield; -use App\Models\Vatsim\NetworkAircraft; use App\Services\AirlineService; use Filament\Forms\Components\Grid; use Filament\Forms\Components\Select; diff --git a/app/Services/Stand/ArrivalAllocationService.php b/app/Services/Stand/ArrivalAllocationService.php index c10e3d015..c60984963 100644 --- a/app/Services/Stand/ArrivalAllocationService.php +++ b/app/Services/Stand/ArrivalAllocationService.php @@ -131,9 +131,9 @@ public function getAllocators(): array return $this->allocators; } - public function getAllocationRankingForAircraft(NetworkAircraft $aircraft): array + public function getAllocationRankingForAircraft(NetworkAircraft $aircraft): Collection { - $ranking = []; + $ranking = collect(); foreach ($this->allocators as $allocator) { if (!$allocator instanceof RankableArrivalStandAllocator) { @@ -142,14 +142,9 @@ public function getAllocationRankingForAircraft(NetworkAircraft $aircraft): arra $ranking[get_class($allocator)] = $allocator->getRankedStandAllocation($aircraft) ->groupBy('rank') - ->map( - fn(Collection $stands) => - $stands->sortBy('identifier', SORT_NATURAL)->map(fn(Stand $stand) => $stand->identifier)->values() - ) - ->values() - ->toArray(); + ->values(); } - + return $ranking; } } diff --git a/resources/views/filament/pages/stand-predictor.blade.php b/resources/views/filament/pages/stand-predictor.blade.php index 20f14df23..b60130f59 100644 --- a/resources/views/filament/pages/stand-predictor.blade.php +++ b/resources/views/filament/pages/stand-predictor.blade.php @@ -14,7 +14,7 @@ @endphp @while ($rank < count($groups))

Rank {{ $rank + 1 }}

- {{ implode(',', $groups[$rank]) }} +
{{ implode(',', $groups[$rank]) }}
@php $rank++; @endphp diff --git a/tests/BaseFilamentTestCase.php b/tests/BaseFilamentTestCase.php index d0030ea3b..c3685c87a 100644 --- a/tests/BaseFilamentTestCase.php +++ b/tests/BaseFilamentTestCase.php @@ -20,4 +20,14 @@ protected function filamentUser(): User { return User::findOrFail(self::ACTIVE_USER_CID); } + + protected function assumeRole(RoleKeys $role): void + { + $this->filamentUser()->roles()->sync([Role::idFromKey($role)]); + } + + protected function noRole(): void + { + $this->filamentUser()->roles()->sync([]); + } } diff --git a/tests/app/Filament/Pages/StandPredictorTest.php b/tests/app/Filament/Pages/StandPredictorTest.php new file mode 100644 index 000000000..bfc6071a5 --- /dev/null +++ b/tests/app/Filament/Pages/StandPredictorTest.php @@ -0,0 +1,111 @@ +assumeRole($role); + } else { + $this->noRole(); + } + + $response = Livewire::test(StandPredictor::class); + if ($shouldRender) { + $response->assertOk(); + } else { + $response->assertForbidden(); + } + } + + public static function renderRoleProvider(): array + { + return [ + 'None' => [null, false], + 'Contributor' => [RoleKeys::OPERATIONS_CONTRIBUTOR, true], + 'DSG' => [RoleKeys::DIVISION_STAFF_GROUP, true], + 'Web' => [RoleKeys::WEB_TEAM, true], + 'Operations' => [RoleKeys::OPERATIONS_TEAM, true], + ]; + } + + public function testItPresentsStandPredictionsOnEvent() + { + // Create models for the test + $arrivalAirfield = Airfield::factory()->create(); + $departureAirfield = Airfield::factory()->create(); + + // Callsign specific + $stand1 = Stand::factory()->create( + ['airfield_id' => $arrivalAirfield->id, 'identifier' => '1A', 'assignment_priority' => 1] + ); + $stand1->airlines()->sync([1 => ['full_callsign' => '221']]); + $stand2 = Stand::factory()->create( + ['airfield_id' => $arrivalAirfield->id, 'identifier' => '2A', 'assignment_priority' => 1] + ); + $stand2->airlines()->sync([1 => ['full_callsign' => '221']]); + + // Callsign specfic, but with a lower priorityy + $stand3 = Stand::factory()->create(['airfield_id' => $arrivalAirfield->id, 'assignment_priority' => 2]); + $stand3->airlines()->sync([1 => ['full_callsign' => '221', 'priority' => 101]]); + + // Generic + $stand4 = Stand::factory()->create(['airfield_id' => $arrivalAirfield->id, 'assignment_priority' => 3]); + $stand4->airlines()->sync([1]); + + Livewire::test(StandPredictor::class) + ->emit( + 'standPredictorFormSubmitted', + [ + 'callsign' => 'BAW999', + 'cid' => 1202533, + 'planned_destairport' => $arrivalAirfield->code, + 'planned_depairport' => $departureAirfield->code, + 'aircraft_id' => 1, + 'airline_id' => 1, + ] + ) + ->assertSeeHtmlInOrder( + [ + sprintf( + 'Allocator: %s', + CargoAirlineFallbackStandAllocator::class + ), + 'No stands for this allocator', + sprintf( + 'Allocator: %s', + OriginAirfieldStandAllocator::class + ) + ] + )->assertSeeHtml( + [ + sprintf( + 'Allocator: %s', + AirlineCallsignArrivalStandAllocator::class + ), + 'Rank 1', + implode(',', [$stand1->identifier, $stand2->identifier]), + 'Rank 2', + $stand3->identifier, + sprintf( + 'Allocator: %s', + AirlineCallsignSlugArrivalStandAllocator::class + ) + ] + ); + } +} diff --git a/tests/app/Http/Livewire/StandPredictorFormTest.php b/tests/app/Http/Livewire/StandPredictorFormTest.php new file mode 100644 index 000000000..1a95b323d --- /dev/null +++ b/tests/app/Http/Livewire/StandPredictorFormTest.php @@ -0,0 +1,76 @@ +assertOk(); + } + + public function testItSubmits() + { + Livewire::test(StandPredictorForm::class) + ->set('callsign', 'BAW999') + ->set('aircraftType', 1) + ->set('departureAirfield', 'EGKK') + ->set('arrivalAirfield', 'EGLL') + ->call('submit') + ->assertHasNoErrors() + ->assertEmitted('standPredictorFormSubmitted', [ + 'callsign' => 'BAW999', + 'cid' => 1203533, + 'aircraft_id' => 1, + 'airline_id' => 1, + 'planned_depairport' => 'EGKK', + 'planned_destairport' => 'EGLL', + ]); + } + + public function testItDoesntSubmitIfNoCallsign() + { + Livewire::test(StandPredictorForm::class) + ->set('aircraftType', 1) + ->set('departureAirfield', 'EGKK') + ->set('arrivalAirfield', 'EGLL') + ->call('submit') + ->assertHasErrors(['callsign']) + ->assertNotEmitted('standPredictorFormSubmitted'); + } + + public function testItDoesntSubmitIfNoAircraftType() + { + Livewire::test(StandPredictorForm::class) + ->set('callsign', 'BAW123') + ->set('departureAirfield', 'EGKK') + ->set('arrivalAirfield', 'EGLL') + ->call('submit') + ->assertHasErrors(['aircraftType']) + ->assertNotEmitted('standPredictorFormSubmitted'); + } + + public function testItDoesntSubmitIfNoDepartureAirfield() + { + Livewire::test(StandPredictorForm::class) + ->set('callsign', 'BAW123') + ->set('arrivalAirfield', 'EGLL') + ->call('submit') + ->assertHasErrors(['departureAirfield']) + ->assertNotEmitted('standPredictorFormSubmitted'); + } + + public function testItDoesntSubmitIfNoArrivalAirfield() + { + Livewire::test(StandPredictorForm::class) + ->set('callsign', 'BAW123') + ->set('departureAirfield', 'EGKK') + ->call('submit') + ->assertHasErrors(['arrivalAirfield']) + ->assertNotEmitted('standPredictorFormSubmitted'); + } +} diff --git a/tests/app/Services/Stand/ArrivalAllocationServiceTest.php b/tests/app/Services/Stand/ArrivalAllocationServiceTest.php index 86af351b5..fa6bbed51 100644 --- a/tests/app/Services/Stand/ArrivalAllocationServiceTest.php +++ b/tests/app/Services/Stand/ArrivalAllocationServiceTest.php @@ -29,8 +29,10 @@ use App\Models\Stand\Stand; use App\Models\Stand\StandAssignment; use App\Models\Stand\StandReservation; +use App\Models\Vatsim\NetworkAircraft; use App\Services\NetworkAircraftService; use Carbon\Carbon; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; @@ -451,4 +453,95 @@ private function addStandAssignment(string $callsign, int $standId): void ] ); } + + public function testItReturnsRankedStandAllocations() + { + // Delete other stand + DB::table('stands')->delete(); + + $aircraft = new NetworkAircraft( + [ + 'callsign' => 'BAW221', + 'cid' => 1234, + 'planned_aircraft' => 'B738', + 'planned_aircraft_short' => 'B738', + 'planned_destairport' => 'EGLL', + 'planned_depairport' => 'LFPG', + 'groundspeed' => 150, + // Lambourne + 'latitude' => 51.646099, + 'longitude' => 0.151667, + 'airline_id' => 1, + 'aircraft_id' => 1, + ] + ); + + // Callsign specific + $stand1 = Stand::factory()->create(['airfield_id' => 1, 'assignment_priority' => 1]); + $stand1->airlines()->sync([1 => ['full_callsign' => '221']]); + $stand2 = Stand::factory()->create(['airfield_id' => 1, 'assignment_priority' => 1]); + $stand2->airlines()->sync([1 => ['full_callsign' => '221']]); + + // Callsign specfic, but with a lower priorityy + $stand3 = Stand::factory()->create(['airfield_id' => 1, 'assignment_priority' => 2]); + $stand3->airlines()->sync([1 => ['full_callsign' => '221', 'priority' => 101]]); + + // Generic + $stand4 = Stand::factory()->create(['airfield_id' => 1, 'assignment_priority' => 3]); + $stand4->airlines()->sync([1]); + + $expected = [ + AirlineCallsignArrivalStandAllocator::class => [ + 0 => [ + $stand1->id, + $stand2->id, + ], + 1 => [ + $stand3->id, + ], + ], + AirlineCallsignSlugArrivalStandAllocator::class => [], + AirlineAircraftArrivalStandAllocator::class => [], + AirlineDestinationArrivalStandAllocator::class => [], + AirlineArrivalStandAllocator::class => [ + 0 => [ + $stand4->id, + ], + ], + AirlineCallsignTerminalArrivalStandAllocator::class => [], + AirlineCallsignSlugTerminalArrivalStandAllocator::class => [], + AirlineAircraftTerminalArrivalStandAllocator::class => [], + AirlineDestinationTerminalArrivalStandAllocator::class => [], + AirlineTerminalArrivalStandAllocator::class => [], + CargoAirlineFallbackStandAllocator::class => [], + OriginAirfieldStandAllocator::class => [], + DomesticInternationalStandAllocator::class => [], + FallbackArrivalStandAllocator::class => [ + 0 => [ + $stand1->id, + $stand2->id, + ], + 1 => [ + $stand3->id, + ], + 2 => [ + $stand4->id, + ], + ], + ]; + + $this->assertEquals( + $expected, + $this->service->getAllocationRankingForAircraft($aircraft) + ->map( + fn(Collection $stands) => + $stands->map( + fn(Collection $standsForRank) => + $standsForRank->sortBy('id') + ->map(fn(Stand $stand) => $stand->id)->values() + ) + ) + ->toArray() + ); + } } From c5c080b6a815c235ab0c61c1cfae0c4fb592071a Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Fri, 8 Sep 2023 10:56:09 +0000 Subject: [PATCH 14/24] refactor: rename general allocators --- ...r.php => AirlineGeneralArrivalStandAllocator.php} | 2 +- ... AirlineGeneralTerminalArrivalStandAllocator.php} | 2 +- app/Providers/StandServiceProvider.php | 8 ++++---- .../Stand/AirlineArrivalStandAllocatorTest.php | 6 +++--- .../AirlineDestinationArrivalStandAllocatorTest.php | 5 +---- .../AirlineTerminalArrivalStandAllocatorTest.php | 9 +++------ .../Services/Stand/ArrivalAllocationServiceTest.php | 12 ++++++------ 7 files changed, 19 insertions(+), 25 deletions(-) rename app/Allocator/Stand/{AirlineArrivalStandAllocator.php => AirlineGeneralArrivalStandAllocator.php} (94%) rename app/Allocator/Stand/{AirlineTerminalArrivalStandAllocator.php => AirlineGeneralTerminalArrivalStandAllocator.php} (94%) diff --git a/app/Allocator/Stand/AirlineArrivalStandAllocator.php b/app/Allocator/Stand/AirlineGeneralArrivalStandAllocator.php similarity index 94% rename from app/Allocator/Stand/AirlineArrivalStandAllocator.php rename to app/Allocator/Stand/AirlineGeneralArrivalStandAllocator.php index 9001eba49..f244fe080 100644 --- a/app/Allocator/Stand/AirlineArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineGeneralArrivalStandAllocator.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; -class AirlineArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator +class AirlineGeneralArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsFromAirlineSpecificStands; diff --git a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocator.php similarity index 94% rename from app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php rename to app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocator.php index 5b3699160..d589e8c6d 100644 --- a/app/Allocator/Stand/AirlineTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocator.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; -class AirlineTerminalArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator +class AirlineGeneralTerminalArrivalStandAllocator implements ArrivalStandAllocator, RankableArrivalStandAllocator { use SelectsStandsFromAirlineSpecificTerminals; diff --git a/app/Providers/StandServiceProvider.php b/app/Providers/StandServiceProvider.php index 68c38056e..8dd87c985 100644 --- a/app/Providers/StandServiceProvider.php +++ b/app/Providers/StandServiceProvider.php @@ -22,11 +22,11 @@ use Illuminate\Support\ServiceProvider; use App\Imports\Stand\StandReservationsImport; use App\Allocator\Stand\CargoAirlineFallbackStandAllocator; -use App\Allocator\Stand\AirlineArrivalStandAllocator; +use App\Allocator\Stand\AirlineGeneralArrivalStandAllocator; use App\Allocator\Stand\FallbackArrivalStandAllocator; use App\Allocator\Stand\CallsignFlightplanReservedArrivalStandAllocator; use App\Allocator\Stand\DomesticInternationalStandAllocator; -use App\Allocator\Stand\AirlineTerminalArrivalStandAllocator; +use App\Allocator\Stand\AirlineGeneralTerminalArrivalStandAllocator; use App\Allocator\Stand\AirlineDestinationArrivalStandAllocator; use App\Allocator\Stand\OriginAirfieldStandAllocator; @@ -50,12 +50,12 @@ public function register() $application->make(AirlineCallsignSlugArrivalStandAllocator::class), $application->make(AirlineAircraftArrivalStandAllocator::class), $application->make(AirlineDestinationArrivalStandAllocator::class), - $application->make(AirlineArrivalStandAllocator::class), + $application->make(AirlineGeneralArrivalStandAllocator::class), $application->make(AirlineCallsignTerminalArrivalStandAllocator::class), $application->make(AirlineCallsignSlugTerminalArrivalStandAllocator::class), $application->make(AirlineAircraftTerminalArrivalStandAllocator::class), $application->make(AirlineDestinationTerminalArrivalStandAllocator::class), - $application->make(AirlineTerminalArrivalStandAllocator::class), + $application->make(AirlineGeneralTerminalArrivalStandAllocator::class), $application->make(CargoAirlineFallbackStandAllocator::class), $application->make(OriginAirfieldStandAllocator::class), $application->make(DomesticInternationalStandAllocator::class), diff --git a/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php index 8e25b7b3b..01eb48ba9 100644 --- a/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php @@ -13,14 +13,14 @@ use Carbon\Carbon; use Illuminate\Support\Facades\DB; -class AirlineArrivalStandAllocatorTest extends BaseFunctionalTestCase +class AirlineGeneralArrivalStandAllocatorTest extends BaseFunctionalTestCase { - private AirlineArrivalStandAllocator $allocator; + private AirlineGeneralArrivalStandAllocator $allocator; public function setUp(): void { parent::setUp(); - $this->allocator = $this->app->make(AirlineArrivalStandAllocator::class); + $this->allocator = $this->app->make(AirlineGeneralArrivalStandAllocator::class); Airline::factory()->create(['icao_code' => 'EZY']); } diff --git a/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php index cf9db1471..06ee0255d 100644 --- a/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php @@ -15,10 +15,7 @@ class AirlineDestinationArrivalStandAllocatorTest extends BaseFunctionalTestCase { - /** - * @var AirlineArrivalStandAllocator - */ - private $allocator; + private readonly AirlineDestinationArrivalStandAllocator $allocator; public function setUp(): void { diff --git a/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php index eceef8fb5..f37275dbd 100644 --- a/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php +++ b/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php @@ -14,17 +14,14 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; -class AirlineTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase +class AirlineGeneralTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase { - /** - * @var AirlineArrivalStandAllocator - */ - private $allocator; + private readonly AirlineGeneralTerminalArrivalStandAllocator $allocator; public function setUp(): void { parent::setUp(); - $this->allocator = $this->app->make(AirlineTerminalArrivalStandAllocator::class); + $this->allocator = $this->app->make(AirlineGeneralTerminalArrivalStandAllocator::class); Airline::where('icao_code', 'BAW')->first()->terminals()->attach(2); Stand::find(1)->update(['terminal_id' => 1]); Stand::find(2)->update(['terminal_id' => 2]); diff --git a/tests/app/Services/Stand/ArrivalAllocationServiceTest.php b/tests/app/Services/Stand/ArrivalAllocationServiceTest.php index fa6bbed51..d8dc75ddb 100644 --- a/tests/app/Services/Stand/ArrivalAllocationServiceTest.php +++ b/tests/app/Services/Stand/ArrivalAllocationServiceTest.php @@ -4,14 +4,14 @@ use App\Allocator\Stand\AirlineAircraftArrivalStandAllocator; use App\Allocator\Stand\AirlineAircraftTerminalArrivalStandAllocator; -use App\Allocator\Stand\AirlineArrivalStandAllocator; +use App\Allocator\Stand\AirlineGeneralArrivalStandAllocator; use App\Allocator\Stand\AirlineCallsignArrivalStandAllocator; use App\Allocator\Stand\AirlineCallsignSlugArrivalStandAllocator; use App\Allocator\Stand\AirlineCallsignSlugTerminalArrivalStandAllocator; use App\Allocator\Stand\AirlineCallsignTerminalArrivalStandAllocator; use App\Allocator\Stand\AirlineDestinationArrivalStandAllocator; use App\Allocator\Stand\AirlineDestinationTerminalArrivalStandAllocator; -use App\Allocator\Stand\AirlineTerminalArrivalStandAllocator; +use App\Allocator\Stand\AirlineGeneralTerminalArrivalStandAllocator; use App\Allocator\Stand\ArrivalStandAllocator; use App\Allocator\Stand\CallsignFlightplanReservedArrivalStandAllocator; use App\Allocator\Stand\CargoAirlineFallbackStandAllocator; @@ -178,12 +178,12 @@ public function testItHasAllocatorPreference() AirlineCallsignSlugArrivalStandAllocator::class, AirlineAircraftArrivalStandAllocator::class, AirlineDestinationArrivalStandAllocator::class, - AirlineArrivalStandAllocator::class, + AirlineGeneralArrivalStandAllocator::class, AirlineCallsignTerminalArrivalStandAllocator::class, AirlineCallsignSlugTerminalArrivalStandAllocator::class, AirlineAircraftTerminalArrivalStandAllocator::class, AirlineDestinationTerminalArrivalStandAllocator::class, - AirlineTerminalArrivalStandAllocator::class, + AirlineGeneralTerminalArrivalStandAllocator::class, CargoAirlineFallbackStandAllocator::class, OriginAirfieldStandAllocator::class, DomesticInternationalStandAllocator::class, @@ -503,7 +503,7 @@ public function testItReturnsRankedStandAllocations() AirlineCallsignSlugArrivalStandAllocator::class => [], AirlineAircraftArrivalStandAllocator::class => [], AirlineDestinationArrivalStandAllocator::class => [], - AirlineArrivalStandAllocator::class => [ + AirlineGeneralArrivalStandAllocator::class => [ 0 => [ $stand4->id, ], @@ -512,7 +512,7 @@ public function testItReturnsRankedStandAllocations() AirlineCallsignSlugTerminalArrivalStandAllocator::class => [], AirlineAircraftTerminalArrivalStandAllocator::class => [], AirlineDestinationTerminalArrivalStandAllocator::class => [], - AirlineTerminalArrivalStandAllocator::class => [], + AirlineGeneralTerminalArrivalStandAllocator::class => [], CargoAirlineFallbackStandAllocator::class => [], OriginAirfieldStandAllocator::class => [], DomesticInternationalStandAllocator::class => [], From ddc2b948d878eba7f18297b05411fa7160c70397 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Fri, 8 Sep 2023 12:03:13 +0100 Subject: [PATCH 15/24] style: styleci --- .../AirlineAircraftArrivalStandAllocator.php | 2 +- ...ineAircraftTerminalArrivalStandAllocator.php | 4 ++-- .../AirlineCallsignArrivalStandAllocator.php | 4 ++-- ...AirlineCallsignSlugArrivalStandAllocator.php | 4 ++-- ...allsignSlugTerminalArrivalStandAllocator.php | 4 ++-- ...ineCallsignTerminalArrivalStandAllocator.php | 4 ++-- .../AirlineDestinationArrivalStandAllocator.php | 4 ++-- ...DestinationTerminalArrivalStandAllocator.php | 4 ++-- .../AirlineGeneralArrivalStandAllocator.php | 4 ++-- ...lineGeneralTerminalArrivalStandAllocator.php | 4 ++-- app/Allocator/Stand/AppliesOrdering.php | 3 ++- app/Allocator/Stand/ArrivalStandAllocator.php | 2 +- ...nFlightplanReservedArrivalStandAllocator.php | 3 +-- .../CargoAirlineFallbackStandAllocator.php | 4 ++-- .../Stand/CargoFlightArrivalStandAllocator.php | 4 ++-- ...argoFlightPreferredArrivalStandAllocator.php | 2 +- .../Stand/CidReservedArrivalStandAllocator.php | 3 +-- app/Allocator/Stand/ConsidersStandRequests.php | 3 +-- .../DomesticInternationalStandAllocator.php | 2 +- .../Stand/FallbackArrivalStandAllocator.php | 4 ++-- .../Stand/OrdersStandsByCommonConditions.php | 2 +- .../Stand/OriginAirfieldStandAllocator.php | 2 +- .../Stand/SelectsFirstApplicableStand.php | 3 ++- .../Stand/SelectsFromAirlineSpecificStands.php | 6 +++--- ...electsFromSizeAppropriateAvailableStands.php | 6 ++---- ...electsStandsFromAirlineSpecificTerminals.php | 6 +++--- .../UserRequestedArrivalStandAllocator.php | 6 ++---- app/Events/Airline/AirlinesUpdatedEvent.php | 3 ++- app/Filament/Pages/StandPredictor.php | 6 +++--- app/Filament/Resources/AirlineResource.php | 9 ++++----- app/Http/Livewire/StandPredictorForm.php | 2 +- .../NotifyAircraftServiceOfDataUpdate.php | 5 ++--- .../NotifyAirlineServiceOfDataUpdate.php | 4 +++- app/Providers/AppServiceProvider.php | 15 +++++---------- app/Services/AircraftService.php | 7 +++---- app/Services/AirlineService.php | 3 +-- app/Services/NetworkAircraftService.php | 17 ++++++----------- app/Services/Stand/ArrivalAllocationService.php | 8 +++----- database/factories/Airline/AirlineFactory.php | 1 - ...ft_type_column_to_network_aircraft_table.php | 3 +-- ...add_airline_id_to_network_aircraft_table.php | 3 +-- 41 files changed, 82 insertions(+), 103 deletions(-) diff --git a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php index a6d21097d..91de72c43 100644 --- a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php @@ -44,6 +44,6 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) => $query->where('airline_stand.aircraft_id', $aircraft->aircraft_id); + return fn (Builder $query) => $query->where('airline_stand.aircraft_id', $aircraft->aircraft_id); } } diff --git a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php index ba60b7efd..bac395cfa 100644 --- a/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocator.php @@ -13,7 +13,7 @@ class AirlineAircraftTerminalArrivalStandAllocator implements ArrivalStandAlloca /* * This allocator: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands at terminals that are specifically selected for the airline AND a given aircraft type * - Orders these stands by the airline's priority for the stand @@ -49,6 +49,6 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) => $query->where('airline_terminal.aircraft_id', $aircraft->aircraft_id); + return fn (Builder $query) => $query->where('airline_terminal.aircraft_id', $aircraft->aircraft_id); } } diff --git a/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php index b5ae19b26..986c8033b 100644 --- a/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignArrivalStandAllocator.php @@ -22,7 +22,7 @@ public function __construct(AirlineService $airlineService) /** * This allocator: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands that are specifically selected for the airline and a specific callsign * - Orders these stands by the airline's priority for the stand @@ -58,7 +58,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) => + return fn (Builder $query) => $query->where('airline_stand.full_callsign', $this->getFullCallsignSlug($aircraft)); } } diff --git a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php index a0f9c8b19..ec59d89a9 100644 --- a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php @@ -27,7 +27,7 @@ public function __construct(AirlineService $airlineService) /** * This allocator: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands that are specifically selected for the airline and a specific callsign slug * - Orders these stands by the airline's priority for the stand @@ -65,6 +65,6 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) => $query->whereIn('airline_stand.callsign_slug', $this->getCallsignSlugs($aircraft)); + return fn (Builder $query) => $query->whereIn('airline_stand.callsign_slug', $this->getCallsignSlugs($aircraft)); } } diff --git a/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php index 990f51e30..d7bc87fe7 100644 --- a/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocator.php @@ -27,7 +27,7 @@ public function __construct(AirlineService $airlineService) /** * This allocator: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands at a terminal that is specifically selected for the airline and * a set of callsign slugs @@ -67,7 +67,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) + return fn (Builder $query) => $query->whereIn('airline_terminal.callsign_slug', $this->getCallsignSlugs($aircraft)); } } diff --git a/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php index 29a3cbbf5..c4653c311 100644 --- a/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignTerminalArrivalStandAllocator.php @@ -22,7 +22,7 @@ public function __construct(AirlineService $airlineService) /** * This allocator: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands at a terminal that is specifically selected for the airline and * a specific callsign @@ -59,7 +59,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) + return fn (Builder $query) => $query->where('airline_terminal.full_callsign', $this->getFullCallsignSlug($aircraft)); } } diff --git a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php index 36a8db0a9..097c4575b 100644 --- a/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineDestinationArrivalStandAllocator.php @@ -20,7 +20,7 @@ class AirlineDestinationArrivalStandAllocator implements ArrivalStandAllocator, /** * This allocator: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands that are specifically selected for the airline and a specific set of destinations * - Orders these by the most specific destination first @@ -59,7 +59,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) => $query->whereIn( + return fn (Builder $query) => $query->whereIn( 'airline_stand.destination', $this->getDestinationStrings($aircraft) ); diff --git a/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php index 0494a0f68..47c0f7e12 100644 --- a/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocator.php @@ -20,7 +20,7 @@ class AirlineDestinationTerminalArrivalStandAllocator implements ArrivalStandAll /** * This allocator: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands that are at terminals specifically selected for the airline and a * specific set of destinations @@ -60,7 +60,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection public function queryFilter(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) => $query->whereIn( + return fn (Builder $query) => $query->whereIn( 'airline_terminal.destination', $this->getDestinationStrings($aircraft) ); diff --git a/app/Allocator/Stand/AirlineGeneralArrivalStandAllocator.php b/app/Allocator/Stand/AirlineGeneralArrivalStandAllocator.php index f244fe080..9f578b919 100644 --- a/app/Allocator/Stand/AirlineGeneralArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineGeneralArrivalStandAllocator.php @@ -13,7 +13,7 @@ class AirlineGeneralArrivalStandAllocator implements ArrivalStandAllocator, Rank /** * This allocator: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands that are specifically selected for the airline and do not have any specific conditions * - Orders these stands by the airline's priority for the stand @@ -49,7 +49,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(): Closure { - return fn(Builder $query) => $query->whereNull('airline_stand.destination') + return fn (Builder $query) => $query->whereNull('airline_stand.destination') ->whereNull('airline_stand.callsign_slug') ->whereNull('airline_stand.full_callsign') ->whereNull('airline_stand.aircraft_id'); diff --git a/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocator.php b/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocator.php index d589e8c6d..d8f53356e 100644 --- a/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocator.php @@ -13,7 +13,7 @@ class AirlineGeneralTerminalArrivalStandAllocator implements ArrivalStandAllocat /** * This allocator: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands that are at terminals specifically selected for the airline * - Filters stands to those that dont have specific conditions @@ -50,7 +50,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(): Closure { - return fn(Builder $query) => $query->whereNull('airline_terminal.destination') + return fn (Builder $query) => $query->whereNull('airline_terminal.destination') ->whereNull('airline_terminal.callsign_slug') ->whereNull('airline_terminal.full_callsign') ->whereNull('airline_terminal.aircraft_id'); diff --git a/app/Allocator/Stand/AppliesOrdering.php b/app/Allocator/Stand/AppliesOrdering.php index 20b75ba51..deb830251 100644 --- a/app/Allocator/Stand/AppliesOrdering.php +++ b/app/Allocator/Stand/AppliesOrdering.php @@ -1,6 +1,7 @@ $query->orderByRaw($orderBy), + fn (Builder $query, string $orderBy) => $query->orderByRaw($orderBy), $stands ); } diff --git a/app/Allocator/Stand/ArrivalStandAllocator.php b/app/Allocator/Stand/ArrivalStandAllocator.php index 4e5326e9d..afd30a19d 100644 --- a/app/Allocator/Stand/ArrivalStandAllocator.php +++ b/app/Allocator/Stand/ArrivalStandAllocator.php @@ -6,7 +6,7 @@ /** * An interface for any allocator that allocates stands for aircraft. - * + * * It is expected that the allocator will return the ID of the stand that it has allocated. */ interface ArrivalStandAllocator diff --git a/app/Allocator/Stand/CallsignFlightplanReservedArrivalStandAllocator.php b/app/Allocator/Stand/CallsignFlightplanReservedArrivalStandAllocator.php index c45448a65..c386b1a17 100644 --- a/app/Allocator/Stand/CallsignFlightplanReservedArrivalStandAllocator.php +++ b/app/Allocator/Stand/CallsignFlightplanReservedArrivalStandAllocator.php @@ -16,8 +16,7 @@ class CallsignFlightplanReservedArrivalStandAllocator implements ArrivalStandAll public function allocate(NetworkAircraft $aircraft): ?int { $reservation = StandReservation::with('stand') - ->whereHas('stand', function (Builder $standQuery) - { + ->whereHas('stand', function (Builder $standQuery) { $standQuery->unoccupied()->unassigned(); }) ->where('callsign', $aircraft->callsign) diff --git a/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php b/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php index e91294e89..4bdc158b3 100644 --- a/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php +++ b/app/Allocator/Stand/CargoAirlineFallbackStandAllocator.php @@ -26,7 +26,7 @@ public function __construct(AirlineService $airlineService) /** * This allocator: - * + * * - Only allocates cargo stands to cargo airlines * - Orders by common conditions (see OrdersStandsByCommonConditions) * - Selects the first available stand (see SelectsFirstApplicableStand) @@ -57,6 +57,6 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(): Closure { - return fn(Builder $query) => $query->cargo(); + return fn (Builder $query) => $query->cargo(); } } diff --git a/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php b/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php index b925cbf15..bb28da324 100644 --- a/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php +++ b/app/Allocator/Stand/CargoFlightArrivalStandAllocator.php @@ -26,7 +26,7 @@ public function __construct(AirlineService $airlineService) /** * This allocator: - * + * * - Only allocates cargo stands to cargo airlines * - Orders by common conditions (see OrdersStandsByCommonConditions) * - Selects the first available stand (see SelectsFirstApplicableStand) @@ -57,6 +57,6 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(): Closure { - return fn(Builder $query) => $query->cargo(); + return fn (Builder $query) => $query->cargo(); } } diff --git a/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php b/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php index 6d0f906b1..8b7272bf9 100644 --- a/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php +++ b/app/Allocator/Stand/CargoFlightPreferredArrivalStandAllocator.php @@ -55,6 +55,6 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(): Closure { - return fn(Builder $query) => $query->cargo(); + return fn (Builder $query) => $query->cargo(); } } diff --git a/app/Allocator/Stand/CidReservedArrivalStandAllocator.php b/app/Allocator/Stand/CidReservedArrivalStandAllocator.php index b47d11ea5..f9d2162a4 100644 --- a/app/Allocator/Stand/CidReservedArrivalStandAllocator.php +++ b/app/Allocator/Stand/CidReservedArrivalStandAllocator.php @@ -15,8 +15,7 @@ class CidReservedArrivalStandAllocator implements ArrivalStandAllocator public function allocate(NetworkAircraft $aircraft): ?int { $reservation = StandReservation::with('stand') - ->whereHas('stand', function (Builder $standQuery) - { + ->whereHas('stand', function (Builder $standQuery) { $standQuery->unoccupied()->unassigned(); }) ->where('cid', $aircraft->cid) diff --git a/app/Allocator/Stand/ConsidersStandRequests.php b/app/Allocator/Stand/ConsidersStandRequests.php index 801758267..32be3cb62 100644 --- a/app/Allocator/Stand/ConsidersStandRequests.php +++ b/app/Allocator/Stand/ConsidersStandRequests.php @@ -11,8 +11,7 @@ trait ConsidersStandRequests { private function joinOtherStandRequests(Builder $query, NetworkAircraft $aircraft): Builder { - return $query->leftJoin('stand_requests as other_stand_requests', function (JoinClause $join) use ($aircraft) - { + return $query->leftJoin('stand_requests as other_stand_requests', function (JoinClause $join) use ($aircraft) { // Prefer stands that haven't been requested by someone else $join->on('stands.id', '=', 'other_stand_requests.stand_id') ->on('other_stand_requests.user_id', '<>', $join->raw($aircraft->cid)) diff --git a/app/Allocator/Stand/DomesticInternationalStandAllocator.php b/app/Allocator/Stand/DomesticInternationalStandAllocator.php index 363c229ea..6f4812b4d 100644 --- a/app/Allocator/Stand/DomesticInternationalStandAllocator.php +++ b/app/Allocator/Stand/DomesticInternationalStandAllocator.php @@ -38,7 +38,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) => $this->getDomesticInternationalScope($aircraft, $query); + return fn (Builder $query) => $this->getDomesticInternationalScope($aircraft, $query); } protected function getDomesticInternationalScope(NetworkAircraft $aircraft, Builder $builder): Builder diff --git a/app/Allocator/Stand/FallbackArrivalStandAllocator.php b/app/Allocator/Stand/FallbackArrivalStandAllocator.php index bb8ebd014..ade10daa4 100644 --- a/app/Allocator/Stand/FallbackArrivalStandAllocator.php +++ b/app/Allocator/Stand/FallbackArrivalStandAllocator.php @@ -13,7 +13,7 @@ class FallbackArrivalStandAllocator implements ArrivalStandAllocator, RankableAr /** * This allocator: - * + * * - Only allocates stands that are not cargo * - Orders by common conditions (see OrdersStandsByCommonConditions) * - Selects the first available stand (see SelectsFirstApplicableStand) @@ -47,6 +47,6 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function filterQuery(): Closure { - return fn(Builder $query) => $query->notCargo(); + return fn (Builder $query) => $query->notCargo(); } } diff --git a/app/Allocator/Stand/OrdersStandsByCommonConditions.php b/app/Allocator/Stand/OrdersStandsByCommonConditions.php index 77913327c..a49d7bbd6 100644 --- a/app/Allocator/Stand/OrdersStandsByCommonConditions.php +++ b/app/Allocator/Stand/OrdersStandsByCommonConditions.php @@ -4,7 +4,7 @@ /** * Specifies the common conditions for ordering stands. - * + * * Any class that uses this trait must also use the ConsidersStandRequests trait and call * joinOtherStandRequests() on the query before applying the ordering. * diff --git a/app/Allocator/Stand/OriginAirfieldStandAllocator.php b/app/Allocator/Stand/OriginAirfieldStandAllocator.php index 9004bdebc..34d894696 100644 --- a/app/Allocator/Stand/OriginAirfieldStandAllocator.php +++ b/app/Allocator/Stand/OriginAirfieldStandAllocator.php @@ -46,7 +46,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function filterQuery(NetworkAircraft $aircraft): Closure { - return fn(Builder $query) + return fn (Builder $query) => $query->notCargo()->whereIn('origin_slug', $this->getDestinationStrings($aircraft)); } } diff --git a/app/Allocator/Stand/SelectsFirstApplicableStand.php b/app/Allocator/Stand/SelectsFirstApplicableStand.php index 818c1592a..0fea69313 100644 --- a/app/Allocator/Stand/SelectsFirstApplicableStand.php +++ b/app/Allocator/Stand/SelectsFirstApplicableStand.php @@ -4,7 +4,8 @@ use Illuminate\Database\Eloquent\Builder; -trait SelectsFirstApplicableStand { +trait SelectsFirstApplicableStand +{ private function selectFirstStand(Builder $query): ?int { return $query->first()?->id; diff --git a/app/Allocator/Stand/SelectsFromAirlineSpecificStands.php b/app/Allocator/Stand/SelectsFromAirlineSpecificStands.php index 5755235a1..a8308fc17 100644 --- a/app/Allocator/Stand/SelectsFromAirlineSpecificStands.php +++ b/app/Allocator/Stand/SelectsFromAirlineSpecificStands.php @@ -13,7 +13,7 @@ trait SelectsFromAirlineSpecificStands /** * This method generates a stand query that: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands that are specifically selected for the airline AND a given aircraft type * - Applies situational specific filters to the query @@ -30,7 +30,7 @@ private function selectAirlineSpecificStands( ): ?int { return $this->selectStandsUsingStandardConditions( $aircraft, - fn(Builder $query) => $specificFilters($query->airline($aircraft->airline_id)), + fn (Builder $query) => $specificFilters($query->airline($aircraft->airline_id)), array_merge( $specificOrders, ['airline_stand.priority ASC'], @@ -46,7 +46,7 @@ private function selectRankedAirlineSpecificStands( ): Collection { return $this->selectRankedStandsUsingStandardConditions( $aircraft, - fn(Builder $query) => $specificFilters($query->airline($aircraft->airline_id)), + fn (Builder $query) => $specificFilters($query->airline($aircraft->airline_id)), array_merge( $specificOrders, ['airline_stand.priority ASC'], diff --git a/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php b/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php index 4ae766d50..ce1372f49 100644 --- a/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php +++ b/app/Allocator/Stand/SelectsFromSizeAppropriateAvailableStands.php @@ -14,8 +14,7 @@ trait SelectsFromSizeAppropriateAvailableStands */ private function sizeAppropriateAvailableStandsAtAirfield(NetworkAircraft $aircraft): Builder { - return Stand::whereHas('airfield', function (Builder $query) use ($aircraft) - { + return Stand::whereHas('airfield', function (Builder $query) use ($aircraft) { $query->where('code', $aircraft->planned_destairport); }) ->sizeAppropriate($aircraft->aircraft) @@ -25,8 +24,7 @@ private function sizeAppropriateAvailableStandsAtAirfield(NetworkAircraft $aircr private function sizeAppropriateAvailableStandsAtAirfieldForRanking(NetworkAircraft $aircraft): Builder { - return Stand::whereHas('airfield', function (Builder $query) use ($aircraft) - { + return Stand::whereHas('airfield', function (Builder $query) use ($aircraft) { $query->where('code', $aircraft->planned_destairport); }) ->sizeAppropriate($aircraft->aircraft) diff --git a/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php b/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php index 510064e6e..e2f86da9f 100644 --- a/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php +++ b/app/Allocator/Stand/SelectsStandsFromAirlineSpecificTerminals.php @@ -17,7 +17,7 @@ trait SelectsStandsFromAirlineSpecificTerminals /** * This method generates a stand query that: - * + * * - Selects stands that are size appropriate and available * - Filters these to stands that are at terminals specifically selected for the airline AND a given aircraft type * - Applies situational specific filters to the query @@ -34,7 +34,7 @@ private function selectStandsAtAirlineSpecificTerminals( ): ?int { return $this->selectStandsUsingStandardConditions( $aircraft, - fn(Builder $query) => $specificFilters($query->join('terminals', 'terminals.id', '=', 'stands.terminal_id') + fn (Builder $query) => $specificFilters($query->join('terminals', 'terminals.id', '=', 'stands.terminal_id') ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') ->where('airline_terminal.airline_id', $aircraft->airline_id)), array_merge( @@ -52,7 +52,7 @@ private function selectRankedStandsAtAirlineSpecificTerminals( ): Collection { return $this->selectRankedStandsUsingStandardConditions( $aircraft, - fn(Builder $query) => $specificFilters($query->join('terminals', 'terminals.id', '=', 'stands.terminal_id') + fn (Builder $query) => $specificFilters($query->join('terminals', 'terminals.id', '=', 'stands.terminal_id') ->join('airline_terminal', 'terminals.id', '=', 'airline_terminal.terminal_id') ->where('airline_terminal.airline_id', $aircraft->airline_id)), array_merge( diff --git a/app/Allocator/Stand/UserRequestedArrivalStandAllocator.php b/app/Allocator/Stand/UserRequestedArrivalStandAllocator.php index b580d8737..62f023b29 100644 --- a/app/Allocator/Stand/UserRequestedArrivalStandAllocator.php +++ b/app/Allocator/Stand/UserRequestedArrivalStandAllocator.php @@ -14,12 +14,10 @@ class UserRequestedArrivalStandAllocator implements ArrivalStandAllocator public function allocate(NetworkAircraft $aircraft): ?int { $requestedStands = StandRequest::where('user_id', $aircraft->cid) - ->whereHas('stand.airfield', function (Builder $airfield) use ($aircraft) - { + ->whereHas('stand.airfield', function (Builder $airfield) use ($aircraft) { $airfield->where('code', $aircraft->planned_destairport); }) - ->whereHas('stand', function (Builder $standQuery) - { + ->whereHas('stand', function (Builder $standQuery) { $standQuery->unoccupied()->unassigned(); }) ->current() diff --git a/app/Events/Airline/AirlinesUpdatedEvent.php b/app/Events/Airline/AirlinesUpdatedEvent.php index 77beee738..5fd0ee00f 100644 --- a/app/Events/Airline/AirlinesUpdatedEvent.php +++ b/app/Events/Airline/AirlinesUpdatedEvent.php @@ -3,4 +3,5 @@ namespace App\Events\Airline; class AirlinesUpdatedEvent -{} +{ +} diff --git a/app/Filament/Pages/StandPredictor.php b/app/Filament/Pages/StandPredictor.php index 08f35609b..b6fdc6439 100644 --- a/app/Filament/Pages/StandPredictor.php +++ b/app/Filament/Pages/StandPredictor.php @@ -47,11 +47,11 @@ public function standPredictorFormSubmitted(array $data) $this->currentPrediction = app()->make(ArrivalAllocationService::class) ->getAllocationRankingForAircraft(new NetworkAircraft($data)) ->map( - fn(Collection $stands) => + fn (Collection $stands) => $stands->map( - fn(Collection $standsForRank) => + fn (Collection $standsForRank) => $standsForRank->sortBy('identifier', SORT_NATURAL) - ->map(fn(Stand $stand) => $stand->identifier)->values() + ->map(fn (Stand $stand) => $stand->identifier)->values() ) )->toArray(); } diff --git a/app/Filament/Resources/AirlineResource.php b/app/Filament/Resources/AirlineResource.php index 111d1940d..8b79893cb 100644 --- a/app/Filament/Resources/AirlineResource.php +++ b/app/Filament/Resources/AirlineResource.php @@ -64,9 +64,9 @@ public static function form(Form $form): Form ->helperText(self::translateFormPath('copy_stand_assignments.helper')) ] ) - ->hidden(fn(Page $livewire) => !$livewire instanceof CreateRecord) - ->disabled(fn(Page $livewire) => !$livewire instanceof CreateRecord) - ->dehydrated(fn(Page $livewire) => $livewire instanceof CreateRecord), + ->hidden(fn (Page $livewire) => !$livewire instanceof CreateRecord) + ->disabled(fn (Page $livewire) => !$livewire instanceof CreateRecord) + ->dehydrated(fn (Page $livewire) => $livewire instanceof CreateRecord), ]); } @@ -90,8 +90,7 @@ public static function table(Table $table): Table ViewAction::make(), EditAction::make(), DeleteAction::make() - ->after(function () - { + ->after(function () { dd('hi'); event(new AirlinesUpdatedEvent); }), diff --git a/app/Http/Livewire/StandPredictorForm.php b/app/Http/Livewire/StandPredictorForm.php index 914c4a01a..6c9a82bde 100644 --- a/app/Http/Livewire/StandPredictorForm.php +++ b/app/Http/Livewire/StandPredictorForm.php @@ -46,7 +46,7 @@ public function getFormSchema(): array ->searchable(), Select::make('departureAirfield') ->label('Departure Airfield') - ->options(Airfield::all()->mapWithKeys(fn($airfield) => [$airfield->code => $airfield->code])) + ->options(Airfield::all()->mapWithKeys(fn ($airfield) => [$airfield->code => $airfield->code])) ->required() ->searchable(), Select::make('arrivalAirfield') diff --git a/app/Listeners/Aircraft/NotifyAircraftServiceOfDataUpdate.php b/app/Listeners/Aircraft/NotifyAircraftServiceOfDataUpdate.php index 6e6ffb3c6..2ad843273 100644 --- a/app/Listeners/Aircraft/NotifyAircraftServiceOfDataUpdate.php +++ b/app/Listeners/Aircraft/NotifyAircraftServiceOfDataUpdate.php @@ -5,12 +5,11 @@ use App\Events\Aircraft\AircraftDataUpdatedEvent; use App\Services\AircraftService; -class NotifyAircraftServiceOfDataUpdate { - +class NotifyAircraftServiceOfDataUpdate +{ public function __construct( private readonly AircraftService $aircraftService ) { - } public function handle(AircraftDataUpdatedEvent $event) diff --git a/app/Listeners/Airline/NotifyAirlineServiceOfDataUpdate.php b/app/Listeners/Airline/NotifyAirlineServiceOfDataUpdate.php index 2addba5d3..3e9670235 100644 --- a/app/Listeners/Airline/NotifyAirlineServiceOfDataUpdate.php +++ b/app/Listeners/Airline/NotifyAirlineServiceOfDataUpdate.php @@ -1,6 +1,7 @@ app->make(Factory::class); $socialite->extend( 'vatsimuk', - function ($app) use ($socialite) - { + function ($app) use ($socialite) { $config = $app['config']['services.vatsim_uk_core']; $config['redirect'] = route('auth.login.callback'); @@ -77,8 +73,7 @@ function ($app) use ($socialite) ); // Filament styling - Filament::serving(function () - { + Filament::serving(function () { Filament::registerTheme(mix('css/vatukfilament.css')); Filament::registerNavigationGroups( [ diff --git a/app/Services/AircraftService.php b/app/Services/AircraftService.php index bf4a6a9df..536f7c4c7 100644 --- a/app/Services/AircraftService.php +++ b/app/Services/AircraftService.php @@ -12,11 +12,11 @@ class AircraftService public function getAircraftDependency(): array { - return Aircraft::with('wakeCategories')->get()->map(fn(Aircraft $aircraft) => [ + return Aircraft::with('wakeCategories')->get()->map(fn (Aircraft $aircraft) => [ 'id' => $aircraft->id, 'icao_code' => $aircraft->code, 'wake_categories' => $aircraft->wakeCategories->map( - fn(WakeCategory $category) => $category->id + fn (WakeCategory $category) => $category->id )->toArray(), ])->toArray(); } @@ -35,10 +35,9 @@ private function aircraftCodeIdMap(): array { return Cache::rememberForever( self::AIRCRAFT_CODE_ID_MAP_CACHE_KEY, - fn() => Aircraft::all()->mapWithKeys(fn(Aircraft $aircraft) => [ + fn () => Aircraft::all()->mapWithKeys(fn (Aircraft $aircraft) => [ $aircraft->code => $aircraft->id, ])->toArray() ); } - } diff --git a/app/Services/AirlineService.php b/app/Services/AirlineService.php index 8ee27941e..5db04276e 100644 --- a/app/Services/AirlineService.php +++ b/app/Services/AirlineService.php @@ -49,8 +49,7 @@ private function airlineCodeIdMap(): array { return Cache::rememberForever( self::AIRLINE_CODE_ID_CACHE_MAP, - fn() => Airline::all(['id', 'icao_code'])->mapWithKeys(function (Airline $airline) - { + fn () => Airline::all(['id', 'icao_code'])->mapWithKeys(function (Airline $airline) { return [$airline->icao_code => $airline->id]; })->toArray() ); diff --git a/app/Services/NetworkAircraftService.php b/app/Services/NetworkAircraftService.php index aa5af1c69..7a8f74795 100644 --- a/app/Services/NetworkAircraftService.php +++ b/app/Services/NetworkAircraftService.php @@ -39,8 +39,7 @@ public function __construct( public function updateNetworkData(): void { - $this->allAircraftBeforeUpdate = NetworkAircraft::all()->mapWithKeys(function (NetworkAircraft $aircraft) - { + $this->allAircraftBeforeUpdate = NetworkAircraft::all()->mapWithKeys(function (NetworkAircraft $aircraft) { return [$aircraft->callsign => $aircraft]; }); @@ -57,16 +56,14 @@ private function formatPilotData(Collection $pilots): Collection private function mapPilotData(Collection $pilotData): Collection { - return $pilotData->map(function (array $pilot) - { + return $pilotData->map(function (array $pilot) { return $this->formatPilot($pilot); }); } private function filterPilotData(Collection $pilotData): Collection { - return $pilotData->filter(function (array $pilot) - { + return $pilotData->filter(function (array $pilot) { return $this->shouldProcessPilot($pilot) && $this->pilotValid($pilot); }); @@ -85,8 +82,7 @@ private function processPilots(Collection $pilots): void private function shouldProcessPilot(array $pilot): bool { - return $this->measuringPoints->contains(function (Coordinate $coordinate) use ($pilot) - { + return $this->measuringPoints->contains(function (Coordinate $coordinate) use ($pilot) { return LocationService::metersToNauticalMiles( $coordinate->getDistance(new Coordinate($pilot['latitude'], $pilot['longitude']), new Haversine()) ) < self::MAX_PROCESSING_DISTANCE; @@ -117,7 +113,7 @@ private function formatPilot(array $pilot): array 'planned_route' => $this->getFlightplanDataElement($pilot, 'route'), 'remarks' => $this->getFlightplanDataElement($pilot, 'remarks'), 'transponder_last_updated_at' => $this->getTransponderUpdatedAtTime($pilot), - 'aircraft_id' => $shortAircraftCode + 'aircraft_id' => $shortAircraftCode ? $this->aircraftService->getAircraftIdFromCode($shortAircraftCode) : null, 'airline_id' => $this->airlineService->airlineIdForCallsign($pilot['callsign']), @@ -152,8 +148,7 @@ private function handleTimeouts(): void NetworkAircraft::timedOut() ->get() ->each( - function (NetworkAircraft $aircraft): void - { + function (NetworkAircraft $aircraft): void { AircraftDisconnected::dispatchSync($aircraft); } ); diff --git a/app/Services/Stand/ArrivalAllocationService.php b/app/Services/Stand/ArrivalAllocationService.php index c60984963..3b3f44de8 100644 --- a/app/Services/Stand/ArrivalAllocationService.php +++ b/app/Services/Stand/ArrivalAllocationService.php @@ -47,8 +47,7 @@ private function deleteAssignmentsForAircraftWithChangedDestination(): void ->whereRaw('airfield.code <> network_aircraft.planned_depairport') ->select('stand_assignments.*') ->get() - ->each(function (StandAssignment $standAssignment) - { + ->each(function (StandAssignment $standAssignment) { $this->assignmentsService->deleteStandAssignment($standAssignment); }); } @@ -59,9 +58,8 @@ private function deleteAssignmentsForAircraftWithChangedDestination(): void private function allocateStandsForArrivingAircraft(): void { $this->getAircraftThatCanHaveArrivalStandsAllocated() - ->filter(fn(NetworkAircraft $aircraft) => $this->aircraftWithAssignmentDistance($aircraft)) - ->each(function (NetworkAircraft $aircraft) - { + ->filter(fn (NetworkAircraft $aircraft) => $this->aircraftWithAssignmentDistance($aircraft)) + ->each(function (NetworkAircraft $aircraft) { foreach ($this->allocators as $allocator) { if ($allocation = $allocator->allocate($aircraft)) { $this->assignmentsService->createStandAssignment( diff --git a/database/factories/Airline/AirlineFactory.php b/database/factories/Airline/AirlineFactory.php index 6f001ea04..e20290bae 100644 --- a/database/factories/Airline/AirlineFactory.php +++ b/database/factories/Airline/AirlineFactory.php @@ -8,7 +8,6 @@ class AirlineFactory extends Factory { - /** * The name of the factory's corresponding model. * diff --git a/database/migrations/2023_08_30_170815_add_aircraft_type_column_to_network_aircraft_table.php b/database/migrations/2023_08_30_170815_add_aircraft_type_column_to_network_aircraft_table.php index d9a4d85cc..88502c2b8 100644 --- a/database/migrations/2023_08_30_170815_add_aircraft_type_column_to_network_aircraft_table.php +++ b/database/migrations/2023_08_30_170815_add_aircraft_type_column_to_network_aircraft_table.php @@ -5,8 +5,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/database/migrations/2023_08_31_171451_add_airline_id_to_network_aircraft_table.php b/database/migrations/2023_08_31_171451_add_airline_id_to_network_aircraft_table.php index 509127220..4fe298cdb 100644 --- a/database/migrations/2023_08_31_171451_add_airline_id_to_network_aircraft_table.php +++ b/database/migrations/2023_08_31_171451_add_airline_id_to_network_aircraft_table.php @@ -5,8 +5,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ From 04379a5335c8a8e5a2277293aa5bcbb0727cc8c9 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Fri, 8 Sep 2023 11:09:40 +0000 Subject: [PATCH 16/24] style: styleci --- .../AirlineAircraftArrivalStandAllocator.php | 1 - ...rlineCallsignSlugArrivalStandAllocator.php | 3 ++- .../Stand/OrdersStandsByCommonConditions.php | 25 +++++++++++-------- .../StandAssignmentsHistoryResource.php | 13 +++++++--- app/Http/Livewire/StandPredictorForm.php | 2 -- .../Stand/StandAssignmentsHistoryService.php | 7 ++++-- .../Stand/StandAssignmentsService.php | 7 +++++- tests/app/Services/AirlineServiceTest.php | 1 - 8 files changed, 38 insertions(+), 21 deletions(-) diff --git a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php index 91de72c43..2f80fc6df 100644 --- a/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineAircraftArrivalStandAllocator.php @@ -28,7 +28,6 @@ public function allocate(NetworkAircraft $aircraft): ?int ); } - // TODO: Move airline and aircraft checks into a separate class public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection { // We cant allocate a stand if we don't know the airline or aircraft type diff --git a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php index ec59d89a9..82e388617 100644 --- a/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php +++ b/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocator.php @@ -65,6 +65,7 @@ public function getRankedStandAllocation(NetworkAircraft $aircraft): Collection private function queryFilter(NetworkAircraft $aircraft): Closure { - return fn (Builder $query) => $query->whereIn('airline_stand.callsign_slug', $this->getCallsignSlugs($aircraft)); + return fn (Builder $query) => + $query->whereIn('airline_stand.callsign_slug', $this->getCallsignSlugs($aircraft)); } } diff --git a/app/Allocator/Stand/OrdersStandsByCommonConditions.php b/app/Allocator/Stand/OrdersStandsByCommonConditions.php index a49d7bbd6..5c00bf7da 100644 --- a/app/Allocator/Stand/OrdersStandsByCommonConditions.php +++ b/app/Allocator/Stand/OrdersStandsByCommonConditions.php @@ -13,25 +13,30 @@ trait OrdersStandsByCommonConditions { use ConsidersStandRequests; + const AERODROME_REFERENCE_CODE = 'aerodrome_reference_code ASC'; + const ASSIGNMENT_PRIORITY = 'assignment_priority ASC'; + const OTHER_STAND_REQUESTS = 'other_stand_requests.id ASC'; + const RANDOM = 'RAND() ASC'; + private array $commonOrderByConditions = [ - 'aerodrome_reference_code ASC', - 'assignment_priority ASC', - 'other_stand_requests.id ASC', - 'RAND() ASC', + self::AERODROME_REFERENCE_CODE, + self::ASSIGNMENT_PRIORITY, + self::OTHER_STAND_REQUESTS, + self::RANDOM, ]; private array $commonOrderByConditionsWithoutAssignmentPriority = [ - 'aerodrome_reference_code ASC', - 'other_stand_requests.id ASC', - 'RAND() ASC', + self::AERODROME_REFERENCE_CODE, + self::OTHER_STAND_REQUESTS, + self::RANDOM, ]; private array $commonOrderByConditionsForRanking = [ - 'aerodrome_reference_code ASC', - 'assignment_priority ASC', + self::AERODROME_REFERENCE_CODE, + self::ASSIGNMENT_PRIORITY, ]; private array $commonOrderByConditionsWithoutAssignmentPriorityForRanking = [ - 'aerodrome_reference_code ASC', + self::AERODROME_REFERENCE_CODE, ]; } diff --git a/app/Filament/Resources/StandAssignmentsHistoryResource.php b/app/Filament/Resources/StandAssignmentsHistoryResource.php index c1ced3eb0..b82fe511d 100644 --- a/app/Filament/Resources/StandAssignmentsHistoryResource.php +++ b/app/Filament/Resources/StandAssignmentsHistoryResource.php @@ -98,7 +98,9 @@ public static function table(Table $table): Table ->actions([ ViewAction::make('view_context') ->label('View Context') - ->hidden(fn (StandAssignmentsHistory $record) => is_null($record->context) || empty($record->context)), + ->hidden( + fn (StandAssignmentsHistory $record) => is_null($record->context) || empty($record->context) + ), ]) ->filters([ Filter::make('callsign') @@ -116,7 +118,9 @@ public static function table(Table $table): Table ->searchable() ->label('Airfield'), Select::make('stand') - ->options(fn (Closure $get) => SelectOptions::standsForAirfield(Airfield::find($get('airfield')))) + ->options( + fn (Closure $get) => SelectOptions::standsForAirfield(Airfield::find($get('airfield'))) + ) ->searchable() ->label('Stand') ->hidden(fn (Closure $get) => !$get('airfield')), @@ -134,7 +138,10 @@ public static function table(Table $table): Table }) ->query(function (Builder $query, array $data) { if (isset($data['airfield'])) { - $query->whereHas('stand.airfield', fn (Builder $query) => $query->where('id', $data['airfield'])); + $query->whereHas( + 'stand.airfield', + fn (Builder $query) => $query->where('id', $data['airfield']) + ); } if (isset($data['stand'])) { diff --git a/app/Http/Livewire/StandPredictorForm.php b/app/Http/Livewire/StandPredictorForm.php index 6c9a82bde..ebc1f47fa 100644 --- a/app/Http/Livewire/StandPredictorForm.php +++ b/app/Http/Livewire/StandPredictorForm.php @@ -23,8 +23,6 @@ class StandPredictorForm extends Component implements HasForms public ?string $departureAirfield = null; public ?int $aircraftType = null; - private readonly AirlineService $airlineService; - protected $messages = [ 'requestedStand' => 'You must select a valid stand.', 'requestedTime' => 'Please enter a valid time.', diff --git a/app/Services/Stand/StandAssignmentsHistoryService.php b/app/Services/Stand/StandAssignmentsHistoryService.php index 25a86954b..c8f1be4a5 100644 --- a/app/Services/Stand/StandAssignmentsHistoryService.php +++ b/app/Services/Stand/StandAssignmentsHistoryService.php @@ -70,8 +70,11 @@ function (StandAssignment $assignment) { ->get() ->map(fn (Stand $stand) => $stand->identifier), 'flightplan_remarks' => $context->aircraft->remarks, - 'requested_stand' => $this->standRequestService->activeRequestForAircraft($context->aircraft)?->stand->identifier, - 'other_requested_stands' => $this->standRequestService->allActiveStandRequestsForAirfield($context->assignment->stand->airfield->code) + 'requested_stand' => $this->standRequestService->activeRequestForAircraft($context->aircraft) + ?->stand->identifier, + 'other_requested_stands' => $this->standRequestService->allActiveStandRequestsForAirfield( + $context->assignment->stand->airfield->code + ) ->filter(fn (StandRequest $request) => $request->stand_id !== $context->assignment->stand_id) ->map(fn (StandRequest $request) => $request->stand->identifier) ->values() diff --git a/app/Services/Stand/StandAssignmentsService.php b/app/Services/Stand/StandAssignmentsService.php index adf0a5029..a24328894 100644 --- a/app/Services/Stand/StandAssignmentsService.php +++ b/app/Services/Stand/StandAssignmentsService.php @@ -74,7 +74,12 @@ public function createStandAssignment(string $callsign, int $standId, string $as ] ); - $assignmentContext = new StandAssignmentContext($assignment, $assignmentType, $existingAssignments, $assignment->aircraft); + $assignmentContext = new StandAssignmentContext( + $assignment, + $assignmentType, + $existingAssignments, + $assignment->aircraft + ); $this->historyService->createHistoryItem($assignmentContext); return [$assignment, $existingAssignments]; diff --git a/tests/app/Services/AirlineServiceTest.php b/tests/app/Services/AirlineServiceTest.php index 3724f873d..251b06d51 100644 --- a/tests/app/Services/AirlineServiceTest.php +++ b/tests/app/Services/AirlineServiceTest.php @@ -86,7 +86,6 @@ public function testItClearsAirlineIdForCallsignCacheOnEvent() $this->assertNull($this->service->airlineIdForCallsign('XXX123')); } - // TODO: Add events to filament #[DataProvider('aircraftProvider')] public function testItReturnsAirlinesForAircraft(string $callsign, string $expectedAirline) From 36d44367bfcb3ba88e329d92c35bbca3624c6e19 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Fri, 8 Sep 2023 11:13:34 +0000 Subject: [PATCH 17/24] style: sonar duplication --- app/Policies/ReadOnlyPolicy.php | 71 +---------------------- app/Policies/ReadOnlyPolicyMethods.php | 75 +++++++++++++++++++++++++ app/Policies/ReadOnlyWithRolePolicy.php | 71 +---------------------- 3 files changed, 77 insertions(+), 140 deletions(-) create mode 100644 app/Policies/ReadOnlyPolicyMethods.php diff --git a/app/Policies/ReadOnlyPolicy.php b/app/Policies/ReadOnlyPolicy.php index 567c1b9e9..8cc1d03fd 100644 --- a/app/Policies/ReadOnlyPolicy.php +++ b/app/Policies/ReadOnlyPolicy.php @@ -11,6 +11,7 @@ class ReadOnlyPolicy { use HandlesAuthorization; + use ReadOnlyPolicyMethods; public function view(): bool { @@ -21,74 +22,4 @@ public function viewAny(): bool { return true; } - - public function attach(): bool - { - return false; - } - - public function detach(): bool - { - return false; - } - - public function update(): bool - { - return false; - } - - public function create(): bool - { - return false; - } - - public function delete(): bool - { - return false; - } - - public function restore(): bool - { - return false; - } - - public function forceDelete(): bool - { - return false; - } - - public function detachAny(): bool - { - return false; - } - - public function dissociate(): bool - { - return false; - } - - public function dissociateAny(): bool - { - return false; - } - - public function replicate(): bool - { - return false; - } - - public function restoreAny(): bool - { - return false; - } - - public function deleteAny(): bool - { - return false; - } - - public function forceDeleteAny(): bool - { - return false; - } } diff --git a/app/Policies/ReadOnlyPolicyMethods.php b/app/Policies/ReadOnlyPolicyMethods.php new file mode 100644 index 000000000..3656a818b --- /dev/null +++ b/app/Policies/ReadOnlyPolicyMethods.php @@ -0,0 +1,75 @@ +userHasAnyRole($user); } - - public function attach(): bool - { - return false; - } - - public function detach(): bool - { - return false; - } - - public function update(): bool - { - return false; - } - - public function create(): bool - { - return false; - } - - public function delete(): bool - { - return false; - } - - public function restore(): bool - { - return false; - } - - public function forceDelete(): bool - { - return false; - } - - public function detachAny(): bool - { - return false; - } - - public function dissociate(): bool - { - return false; - } - - public function dissociateAny(): bool - { - return false; - } - - public function replicate(): bool - { - return false; - } - - public function restoreAny(): bool - { - return false; - } - - public function deleteAny(): bool - { - return false; - } - - public function forceDeleteAny(): bool - { - return false; - } } From 9c901cde41e383bd14d83304afc04c6b23a55cc8 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Fri, 8 Sep 2023 11:14:03 +0000 Subject: [PATCH 18/24] style: styleci --- app/Policies/ReadOnlyPolicyMethods.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Policies/ReadOnlyPolicyMethods.php b/app/Policies/ReadOnlyPolicyMethods.php index 3656a818b..7a46f41a6 100644 --- a/app/Policies/ReadOnlyPolicyMethods.php +++ b/app/Policies/ReadOnlyPolicyMethods.php @@ -2,7 +2,8 @@ namespace App\Policies; -trait ReadOnlyPolicyMethods { +trait ReadOnlyPolicyMethods +{ public function attach(): bool { return false; From d82a1a7deef2c810e892481f512cee478e987a6e Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Fri, 8 Sep 2023 11:20:34 +0000 Subject: [PATCH 19/24] fix: remove dump --- app/Filament/Resources/AirlineResource.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Filament/Resources/AirlineResource.php b/app/Filament/Resources/AirlineResource.php index 8b79893cb..fa977972c 100644 --- a/app/Filament/Resources/AirlineResource.php +++ b/app/Filament/Resources/AirlineResource.php @@ -91,7 +91,6 @@ public static function table(Table $table): Table EditAction::make(), DeleteAction::make() ->after(function () { - dd('hi'); event(new AirlinesUpdatedEvent); }), ]); From 1a79808452e01d1a752dd2e676347896953cceaa Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Fri, 8 Sep 2023 11:55:53 +0000 Subject: [PATCH 20/24] test: fix tests --- app/Services/Stand/StandStatusService.php | 18 ++++------ ...rlineGeneralArrivalStandAllocatorTest.php} | 0 ...eralTerminalArrivalStandAllocatorTest.php} | 0 tests/app/Models/Stand/StandTest.php | 33 ------------------ .../Services/NetworkAircraftServiceTest.php | 2 +- .../Stand/ArrivalAllocationServiceTest.php | 34 +++++++++++++++++++ .../Services/Stand/StandStatusServiceTest.php | 1 - 7 files changed, 41 insertions(+), 47 deletions(-) rename tests/app/Allocator/Stand/{AirlineArrivalStandAllocatorTest.php => AirlineGeneralArrivalStandAllocatorTest.php} (100%) rename tests/app/Allocator/Stand/{AirlineTerminalArrivalStandAllocatorTest.php => AirlineGeneralTerminalArrivalStandAllocatorTest.php} (100%) diff --git a/app/Services/Stand/StandStatusService.php b/app/Services/Stand/StandStatusService.php index 379d43521..aac3c15d2 100644 --- a/app/Services/Stand/StandStatusService.php +++ b/app/Services/Stand/StandStatusService.php @@ -15,7 +15,7 @@ class StandStatusService */ public static function getAirfieldStandStatus(string $airfield): array { - $stands = Stand::with( + return Stand::with( 'airlines', 'type', 'maxAircraftWingspan', @@ -34,17 +34,11 @@ public static function getAirfieldStandStatus(string $airfield): array ) ->withCasts(['latitude' => 'decimal:8', 'longitude' => 'decimal:8']) ->airfield($airfield) - ->get(); - - $stands->sortBy('identifier', SORT_NATURAL); - - $standStatuses = []; - - foreach ($stands as $stand) { - $standStatuses[] = self::getStandStatus($stand); - } - - return $standStatuses; + ->get() + ->sortBy('identifier', SORT_NATURAL) + ->values() + ->map(fn (Stand $stand) => self::getStandStatus($stand)) + ->toArray(); } public static function getStandStatus(Stand $stand): array diff --git a/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineGeneralArrivalStandAllocatorTest.php similarity index 100% rename from tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php rename to tests/app/Allocator/Stand/AirlineGeneralArrivalStandAllocatorTest.php diff --git a/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocatorTest.php similarity index 100% rename from tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php rename to tests/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocatorTest.php diff --git a/tests/app/Models/Stand/StandTest.php b/tests/app/Models/Stand/StandTest.php index b897e4639..b83ecb187 100644 --- a/tests/app/Models/Stand/StandTest.php +++ b/tests/app/Models/Stand/StandTest.php @@ -120,39 +120,6 @@ public function testAirlineOnlyReturnsStandsAtTheRightTime() $this->assertEquals([2, 3], $stands); } - public function testAirlineDestinationOnlyReturnsStandsWithinTheRightTime() - { - Carbon::setTestNow(Carbon::parse('2020-12-05 16:00:00')); - DB::table('airline_stand')->insert( - [ - [ - 'airline_id' => 1, - 'stand_id' => 1, - 'destination' => 'EGGD', - 'not_before' => '16:00:01', - ], - [ - 'airline_id' => 1, - 'stand_id' => 2, - 'destination' => 'EGGD', - 'not_before' => null, - ], - [ - 'airline_id' => 1, - 'stand_id' => 3, - 'destination' => 'EGGD', - 'not_before' => '16:00:00', - ], - ] - ); - - $stands = Stand::airlineDestination( - Airline::find(1), - ['EGGD'] - )->pluck('stands.id')->toArray(); - $this->assertEquals([2, 3], $stands); - } - public function testAirlineCallsignOnlyReturnsStandsForTheCorrectAirlineAndCallsigns() { DB::table('airline_stand')->insert( diff --git a/tests/app/Services/NetworkAircraftServiceTest.php b/tests/app/Services/NetworkAircraftServiceTest.php index ba6516792..627cc21b6 100644 --- a/tests/app/Services/NetworkAircraftServiceTest.php +++ b/tests/app/Services/NetworkAircraftServiceTest.php @@ -419,7 +419,7 @@ private function getTransformedPilotData( 'remarks' => $pilot['flight_plan']['remarks'], 'aircraft_id' => $pilot['flight_plan']['aircraft_short'] === 'B738' ? 1 : null, 'airline_id' => match (Str::substr($pilot['callsign'], 0, 3)) { - 'BAW' => 2, + 'BAW' => 1, 'SHT' => 2, 'VIR' => 3, default => null, diff --git a/tests/app/Services/Stand/ArrivalAllocationServiceTest.php b/tests/app/Services/Stand/ArrivalAllocationServiceTest.php index d8dc75ddb..8e61588e1 100644 --- a/tests/app/Services/Stand/ArrivalAllocationServiceTest.php +++ b/tests/app/Services/Stand/ArrivalAllocationServiceTest.php @@ -26,6 +26,7 @@ use App\Events\StandAssignedEvent; use App\Events\StandUnassignedEvent; use App\Models\Aircraft\Aircraft; +use App\Models\Airline\Airline; use App\Models\Stand\Stand; use App\Models\Stand\StandAssignment; use App\Models\Stand\StandReservation; @@ -40,12 +41,15 @@ class ArrivalAllocationServiceTest extends BaseFunctionalTestCase { private readonly ArrivalAllocationService $service; + private readonly Airline $bmi; + public function setUp(): void { parent::setUp(); Event::fake(); $this->service = $this->app->make(ArrivalAllocationService::class); DB::table('network_aircraft')->delete(); + $this->bmi = Airline::factory()->create(['icao_code' => 'BMI']); } public function testItDeallocatesStandForDivertingAircraft() @@ -64,6 +68,8 @@ public function testItDeallocatesStandForDivertingAircraft() // London 'latitude' => 51.487202, 'longitude' => -0.466667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -88,6 +94,8 @@ public function testItAllocatesANewStandForDivertingAircraft() // London 'latitude' => 51.487202, 'longitude' => -0.466667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -113,6 +121,8 @@ public function testItDoesntDeallocateStandIfAircraftNotDiverting() // London 'latitude' => 51.487202, 'longitude' => -0.466667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -137,6 +147,8 @@ public function testItDoesntDeallocateStandIfForDepartureAirport() // London 'latitude' => 51.487202, 'longitude' => -0.466667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -158,6 +170,8 @@ public function testItDoesntDeallocateStandIfNoStandToDeallocate() // London 'latitude' => 51.487202, 'longitude' => -0.466667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -221,6 +235,8 @@ public function testItAllocatesAStandFromAllocator() // London 'latitude' => 51.487202, 'longitude' => -0.466667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -243,6 +259,8 @@ public function testItDoesntAllocateStandIfTimedOut() // London 'latitude' => 51.487202, 'longitude' => -0.466667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); $aircraft->updated_at = Carbon::now()->subMinutes(30); @@ -267,6 +285,8 @@ public function testItDoesntAllocateStandIfPerformingCircuits() // London 'latitude' => 51.487202, 'longitude' => -0.466667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -289,6 +309,8 @@ public function testItDoesntPerformAllocationIfStandTooFarFromAirfield() // Lambourne 'latitude' => 51.646099, 'longitude' => 0.151667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -311,6 +333,8 @@ public function testItDoesntPerformAllocationIfAircraftHasNoGroundspeed() // Lambourne 'latitude' => 51.646099, 'longitude' => 0.151667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -339,6 +363,8 @@ public function testItDoesntPerformAllocationIfNoStandAllocated() // Lambourne 'latitude' => 51.646099, 'longitude' => 0.151667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -361,6 +387,8 @@ public function testItDoesntPerformAllocationIfStandAlreadyAssigned() // Lambourne 'latitude' => 51.646099, 'longitude' => 0.151667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); StandAssignment::create( @@ -389,6 +417,8 @@ public function testItDoesntReturnAllocationIfAirfieldNotFound() // Lambourne 'latitude' => 51.646099, 'longitude' => 0.151667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); @@ -411,6 +441,8 @@ public function testItDoesntPerformAllocationIfUnknownAircraftType() // Lambourne 'latitude' => 51.646099, 'longitude' => 0.151667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => null, ] ); @@ -435,6 +467,8 @@ public function testItDoesntPerformAllocationIfAircraftTypeNotStandAssignable() // Lambourne 'latitude' => 51.646099, 'longitude' => 0.151667, + 'airline_id' => $this->bmi->id, + 'aircraft_id' => 1, ] ); diff --git a/tests/app/Services/Stand/StandStatusServiceTest.php b/tests/app/Services/Stand/StandStatusServiceTest.php index d9225b6fb..33ad8457e 100644 --- a/tests/app/Services/Stand/StandStatusServiceTest.php +++ b/tests/app/Services/Stand/StandStatusServiceTest.php @@ -5,7 +5,6 @@ use App\BaseFunctionalTestCase; use App\Models\Stand\Stand; use App\Models\Stand\StandAssignment; -use App\Models\Stand\StandRequest; use App\Models\Stand\StandReservation; use App\Models\Vatsim\NetworkAircraft; use App\Services\NetworkAircraftService; From ef28bc5223ce908197c681c62793bea5231a1d73 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Fri, 8 Sep 2023 12:32:01 +0000 Subject: [PATCH 21/24] test: fix queue driver when testing --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 866dbc7de..474fc6f98 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -9,7 +9,7 @@ - + From b8688d3d467ed7fe444b9d0b70fcb33d0aaa05a8 Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Fri, 8 Sep 2023 12:32:12 +0000 Subject: [PATCH 22/24] test: force stand uniqueness from faker --- database/factories/Stand/StandFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/factories/Stand/StandFactory.php b/database/factories/Stand/StandFactory.php index a4090b768..da67d0eff 100644 --- a/database/factories/Stand/StandFactory.php +++ b/database/factories/Stand/StandFactory.php @@ -45,7 +45,7 @@ private function standIdentifier(): string { return sprintf( '%d%s', - $this->faker->numberBetween(0, 500), + $this->faker->unique()->numberBetween(0, 500), $this->faker->randomElement(['L', 'R', '', 'A']), ); } From 83157ede4d65b7cc67246ad8c83ba78b55574d3d Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Wed, 13 Sep 2023 14:49:09 +0100 Subject: [PATCH 23/24] Delete app/Policies/ReadOnlyPolicyMethods.php --- app/Policies/ReadOnlyPolicyMethods.php | 76 -------------------------- 1 file changed, 76 deletions(-) delete mode 100644 app/Policies/ReadOnlyPolicyMethods.php diff --git a/app/Policies/ReadOnlyPolicyMethods.php b/app/Policies/ReadOnlyPolicyMethods.php deleted file mode 100644 index 7a46f41a6..000000000 --- a/app/Policies/ReadOnlyPolicyMethods.php +++ /dev/null @@ -1,76 +0,0 @@ - Date: Wed, 13 Sep 2023 18:34:23 +0100 Subject: [PATCH 24/24] Update StandPredictor.php --- app/Filament/Pages/StandPredictor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Filament/Pages/StandPredictor.php b/app/Filament/Pages/StandPredictor.php index b6fdc6439..8314068b1 100644 --- a/app/Filament/Pages/StandPredictor.php +++ b/app/Filament/Pages/StandPredictor.php @@ -16,6 +16,7 @@ class StandPredictor extends Page protected static ?string $navigationLabel = 'Stand Predictor'; protected static ?string $slug = 'stand-predictor'; protected static string $view = 'filament.pages.stand-predictor'; + protected static ?string $navigationGroup = 'Airfield'; protected $listeners = ['standPredictorFormSubmitted'];