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..b60130f59
--- /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 @@
+
+
+
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/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php
index 9e5fa4603..093b35dcc 100644
--- a/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.php
+++ b/tests/app/Allocator/Stand/AirlineAircraftArrivalStandAllocatorTest.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 AirlineAircraftArrivalStandAllocatorTest extends BaseFunctionalTestCase
@@ -16,6 +21,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 +313,188 @@ 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));
+ }
+
+ 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. B1 has a request, to show that requests arent considered
+ // when ranking.
+ $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 => ['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 $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,
+ '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..24e2a4464 100644
--- a/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.php
+++ b/tests/app/Allocator/Stand/AirlineAircraftTerminalArrivalStandAllocatorTest.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 AirlineAircraftTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase
@@ -17,6 +22,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 +255,7 @@ public function testItDoesntAllocateAStandWithNoAircraftType()
]
);
- $aircraft = $this->createAircraft('BAW23451', 'EGLL', 'EGGD');
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL', 'EGGD', aircraftType: 'XXX');
$this->assertNull($this->allocator->allocate($aircraft));
}
@@ -335,19 +341,218 @@ 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 => ['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),
+ ]
+ );
+ 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 => ['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 $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,
+ '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/AirlineCallsignArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php
index 071ef1c90..db54d0e45 100644
--- a/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.php
+++ b/tests/app/Allocator/Stand/AirlineCallsignArrivalStandAllocatorTest.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 AirlineCallsignArrivalStandAllocatorTest extends BaseFunctionalTestCase
@@ -16,6 +21,7 @@ public function setUp(): void
{
parent::setUp();
$this->allocator = $this->app->make(AirlineCallsignArrivalStandAllocator::class);
+ Airline::factory()->create(['icao_code' => 'EZY']);
}
public function testItAllocatesAStandWithAFixedCallsign()
@@ -327,19 +333,167 @@ 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 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(
[
'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 66%
rename from tests/app/Allocator/Stand/AirlineCallsignSlugStandAllocatorTest.php
rename to tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php
index 61cdbdb01..f4ff1afdf 100644
--- a/tests/app/Allocator/Stand/AirlineCallsignSlugStandAllocatorTest.php
+++ b/tests/app/Allocator/Stand/AirlineCallsignSlugArrivalStandAllocatorTest.php
@@ -4,16 +4,24 @@
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 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()
@@ -475,19 +483,195 @@ 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, '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 - 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 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,
+ '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..7641fe9f2 100644
--- a/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.php
+++ b/tests/app/Allocator/Stand/AirlineCallsignSlugTerminalArrivalStandAllocatorTest.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 AirlineCallsignSlugTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase
@@ -17,6 +22,7 @@ public function setUp(): void
{
parent::setUp();
$this->allocator = $this->app->make(AirlineCallsignSlugTerminalArrivalStandAllocator::class);
+ Airline::factory()->create(['icao_code' => 'EZY']);
}
public function testItAllocatesAStandWithAFixedCallsignSlug()
@@ -501,19 +507,268 @@ 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 $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,
+ '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..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,19 +360,223 @@ 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 $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,
+ '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/AirlineDestinationArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php
index 161bdc6f4..06ee0255d 100644
--- a/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php
+++ b/tests/app/Allocator/Stand/AirlineDestinationArrivalStandAllocatorTest.php
@@ -4,21 +4,24 @@
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
{
- /**
- * @var AirlineArrivalStandAllocator
- */
- private $allocator;
+ private readonly AirlineDestinationArrivalStandAllocator $allocator;
public function setUp(): void
{
parent::setUp();
$this->allocator = $this->app->make(AirlineDestinationArrivalStandAllocator::class);
+ Airline::factory()->create(['icao_code' => 'EZY']);
}
public function testItAllocatesAStandWithAFixedDestination()
@@ -480,20 +483,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
- ): NetworkAircraft
- {
- return NetworkAircraft::create(
+ 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 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,
+ '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 7bd9c45e3..031a42f26 100644
--- a/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php
+++ b/tests/app/Allocator/Stand/AirlineDestinationTerminalArrivalStandAllocatorTest.php
@@ -4,10 +4,16 @@
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;
class AirlineDestinationTerminalArrivalStandAllocatorTest extends BaseFunctionalTestCase
{
@@ -17,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()
@@ -501,19 +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,
+ '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/AirlineArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineGeneralArrivalStandAllocatorTest.php
similarity index 57%
rename from tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php
rename to tests/app/Allocator/Stand/AirlineGeneralArrivalStandAllocatorTest.php
index de7b4ff6d..01eb48ba9 100644
--- a/tests/app/Allocator/Stand/AirlineArrivalStandAllocatorTest.php
+++ b/tests/app/Allocator/Stand/AirlineGeneralArrivalStandAllocatorTest.php
@@ -4,18 +4,24 @@
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
+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']);
}
public function testItAllocatesAStandForTheAirline()
@@ -317,15 +323,195 @@ public function testItDoesntAllocateNonExistentAirlines()
$this->assertNull($this->allocator->allocate($aircraft));
}
- private function createAircraft(string $callsign, string $arrivalAirport): NetworkAircraft
+ public function testItDoesntRankStandsIfUnknownAirline()
{
- return NetworkAircraft::create(
+ $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 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,
- '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/AirlineGeneralTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocatorTest.php
new file mode 100644
index 000000000..f37275dbd
--- /dev/null
+++ b/tests/app/Allocator/Stand/AirlineGeneralTerminalArrivalStandAllocatorTest.php
@@ -0,0 +1,461 @@
+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]);
+ Airline::factory()->create(['icao_code' => 'EZY']);
+ }
+
+ public function testItAllocatesAStandAtTheRightTerminal()
+ {
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+ $this->assertEquals(2, $this->allocator->allocate($aircraft));
+ }
+
+ public function testItDoesntAssignTerminalsWithSpecificDestinations()
+ {
+ Stand::query()->update(['terminal_id' => null]);
+ $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
+ Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1A']);
+ DB::table('airline_terminal')->insert(
+ [
+ [
+ 'airline_id' => 1,
+ 'terminal_id' => $terminal1->id,
+ 'destination' => 'EGFF',
+ ],
+ ]
+ );
+
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+ $this->assertNull($this->allocator->allocate($aircraft));
+ }
+
+ public function testItDoesntAssignTerminalsWithSpecificCallsignSlugs()
+ {
+ Stand::query()->update(['terminal_id' => null]);
+ $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
+ Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1A']);
+ DB::table('airline_terminal')->insert(
+ [
+ [
+ 'airline_id' => 1,
+ 'terminal_id' => $terminal1->id,
+ 'callsign_slug' => '333',
+ ],
+ ]
+ );
+
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+ $this->assertNull($this->allocator->allocate($aircraft));
+ }
+
+ public function testItAssignsStandsWithSpecificFullCallsigns()
+ {
+ Stand::query()->update(['terminal_id' => null]);
+ $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
+ Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1A']);
+ DB::table('airline_terminal')->insert(
+ [
+ [
+ 'airline_id' => 1,
+ 'terminal_id' => $terminal1->id,
+ 'full_callsign' => '333',
+ ],
+ ]
+ );
+
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+ $this->assertNull($this->allocator->allocate($aircraft));
+ }
+
+ public function testItDoesntAssignTerminalsWithSpecificAircraftTypes()
+ {
+ Stand::query()->update(['terminal_id' => null]);
+ $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
+ Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1A']);
+ DB::table('airline_terminal')->insert(
+ [
+ [
+ 'airline_id' => 1,
+ 'terminal_id' => $terminal1->id,
+ 'aircraft_id' => 1,
+ ],
+ ]
+ );
+
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+ $this->assertNull($this->allocator->allocate($aircraft));
+ }
+
+
+ public function testItAPrefersStandsWithNoSpecificCallsignSlugs()
+ {
+ Stand::query()->update(['terminal_id' => null]);
+ $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
+ $stand1 = Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1B']);
+ $terminal2 = Terminal::factory()->create(['airfield_id' => 1]);
+ Stand::factory()->withTerminal($terminal2)->create(['airfield_id' => 1, 'identifier' => '1A']);
+
+ DB::table('airline_terminal')->insert(
+ [
+ [
+ 'airline_id' => 1,
+ 'terminal_id' => $terminal2->id,
+ 'callsign_slug' => '333',
+ ],
+ [
+ 'airline_id' => 1,
+ 'terminal_id' => $terminal1->id,
+ 'callsign_slug' => null,
+ ],
+ ]
+ );
+
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+ $this->assertEquals($stand1->id, $this->allocator->allocate($aircraft));
+ }
+
+ public function testItAllocatesStandsInAerodromeReferenceAscendingOrder()
+ {
+ Aircraft::where('code', 'B738')->update(['aerodrome_reference_code' => 'B']);
+ $weightAppropriateStand = Stand::create(
+ [
+ 'airfield_id' => 1,
+ 'identifier' => '502',
+ 'latitude' => 54.65875500,
+ 'longitude' => -6.22258694,
+ 'aerodrome_reference_code' => 'B',
+ 'terminal_id' => 2,
+ ]
+ );
+
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+ $this->assertEquals($weightAppropriateStand->id, $this->allocator->allocate($aircraft));
+ }
+
+ public function testItAllocatesStandsAtAppropriateReferenceCode()
+ {
+ Aircraft::where('code', 'B738')->update(['aerodrome_reference_code' => 'E']);
+ $weightAppropriateStand = Stand::create(
+ [
+ 'airfield_id' => 1,
+ 'terminal_id' => 2,
+ 'identifier' => '502',
+ 'latitude' => 54.65875500,
+ 'longitude' => -6.22258694,
+ 'aerodrome_reference_code' => 'E',
+ ]
+ );
+
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+ $this->assertEquals($weightAppropriateStand->id, $this->allocator->allocate($aircraft));
+ }
+
+ public function testItDoesntAllocateOccupiedStands()
+ {
+ $extraStand = Stand::create(
+ [
+ 'airfield_id' => 1,
+ 'identifier' => '502',
+ 'latitude' => 54.65875500,
+ 'longitude' => -6.22258694,
+ 'aerodrome_reference_code' => 'E',
+ 'terminal_id' => 2,
+ ]
+ );
+
+ $occupier = $this->createAircraft('EZY7823', 'EGLL');
+ $occupier->occupiedStand()->sync([2]);
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+
+ $this->assertEquals($extraStand->id, $this->allocator->allocate($aircraft));
+ }
+
+ public function testItDoesntAllocateUnavailableStands()
+ {
+ $extraStand = Stand::create(
+ [
+ 'airfield_id' => 1,
+ 'identifier' => '502',
+ 'latitude' => 54.65875500,
+ 'longitude' => -6.22258694,
+ 'aerodrome_reference_code' => 'E',
+ 'terminal_id' => 2,
+ ]
+ );
+ NetworkAircraft::find('BAW123')->occupiedStand()->sync([2]);
+
+ $aircraft = $this->createAircraft('BAW23451', 'EGLL');
+ $this->assertEquals($extraStand->id, $this->allocator->allocate($aircraft));
+ }
+
+ public function testItDoesntAllocateNonExistentAirlines()
+ {
+ $aircraft = $this->createAircraft('***1234', 'EGLL');
+ $this->assertNull($this->allocator->allocate($aircraft));
+ }
+
+ public function testItReturnsNullOnNoStandAllocated()
+ {
+ Stand::all()->each(function (Stand $stand)
+ {
+ $stand->delete();
+ });
+ $aircraft = $this->createAircraft('BAW999', 'EGLL');
+ $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 $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 new NetworkAircraft(
+ [
+ 'callsign' => $callsign,
+ 'cid' => 1234,
+ '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/AirlineTerminalArrivalStandAllocatorTest.php b/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php
deleted file mode 100644
index dc0abca00..000000000
--- a/tests/app/Allocator/Stand/AirlineTerminalArrivalStandAllocatorTest.php
+++ /dev/null
@@ -1,244 +0,0 @@
-allocator = $this->app->make(AirlineTerminalArrivalStandAllocator::class);
- Airline::where('icao_code', 'BAW')->first()->terminals()->attach(2);
- Stand::find(1)->update(['terminal_id' => 1]);
- Stand::find(2)->update(['terminal_id' => 2]);
- }
-
- public function testItAllocatesAStandAtTheRightTerminal()
- {
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
- $this->assertEquals(2, $this->allocator->allocate($aircraft));
- }
-
- public function testItDoesntAssignTerminalsWithSpecificDestinations()
- {
- Stand::query()->update(['terminal_id' => null]);
- $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
- Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1A']);
- DB::table('airline_terminal')->insert(
- [
- [
- 'airline_id' => 1,
- 'terminal_id' => $terminal1->id,
- 'destination' => 'EGFF',
- ],
- ]
- );
-
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
- $this->assertNull($this->allocator->allocate($aircraft));
- }
-
- public function testItDoesntAssignTerminalsWithSpecificCallsignSlugs()
- {
- Stand::query()->update(['terminal_id' => null]);
- $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
- Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1A']);
- DB::table('airline_terminal')->insert(
- [
- [
- 'airline_id' => 1,
- 'terminal_id' => $terminal1->id,
- 'callsign_slug' => '333',
- ],
- ]
- );
-
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
- $this->assertNull($this->allocator->allocate($aircraft));
- }
-
- public function testItAssignsStandsWithSpecificFullCallsigns()
- {
- Stand::query()->update(['terminal_id' => null]);
- $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
- Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1A']);
- DB::table('airline_terminal')->insert(
- [
- [
- 'airline_id' => 1,
- 'terminal_id' => $terminal1->id,
- 'full_callsign' => '333',
- ],
- ]
- );
-
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
- $this->assertNull($this->allocator->allocate($aircraft));
- }
-
- public function testItDoesntAssignTerminalsWithSpecificAircraftTypes()
- {
- Stand::query()->update(['terminal_id' => null]);
- $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
- Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1A']);
- DB::table('airline_terminal')->insert(
- [
- [
- 'airline_id' => 1,
- 'terminal_id' => $terminal1->id,
- 'aircraft_id' => 1,
- ],
- ]
- );
-
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
- $this->assertNull($this->allocator->allocate($aircraft));
- }
-
-
- public function testItAPrefersStandsWithNoSpecificCallsignSlugs()
- {
- Stand::query()->update(['terminal_id' => null]);
- $terminal1 = Terminal::factory()->create(['airfield_id' => 1]);
- $stand1 = Stand::factory()->withTerminal($terminal1)->create(['airfield_id' => 1, 'identifier' => '1B']);
- $terminal2 = Terminal::factory()->create(['airfield_id' => 1]);
- Stand::factory()->withTerminal($terminal2)->create(['airfield_id' => 1, 'identifier' => '1A']);
-
- DB::table('airline_terminal')->insert(
- [
- [
- 'airline_id' => 1,
- 'terminal_id' => $terminal2->id,
- 'callsign_slug' => '333',
- ],
- [
- 'airline_id' => 1,
- 'terminal_id' => $terminal1->id,
- 'callsign_slug' => null,
- ],
- ]
- );
-
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
- $this->assertEquals($stand1->id, $this->allocator->allocate($aircraft));
- }
-
- public function testItAllocatesStandsInAerodromeReferenceAscendingOrder()
- {
- Aircraft::where('code', 'B738')->update(['aerodrome_reference_code' => 'B']);
- $weightAppropriateStand = Stand::create(
- [
- 'airfield_id' => 1,
- 'identifier' => '502',
- 'latitude' => 54.65875500,
- 'longitude' => -6.22258694,
- 'aerodrome_reference_code' => 'B',
- 'terminal_id' => 2,
- ]
- );
-
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
- $this->assertEquals($weightAppropriateStand->id, $this->allocator->allocate($aircraft));
- }
-
- public function testItAllocatesStandsAtAppropriateReferenceCode()
- {
- Aircraft::where('code', 'B738')->update(['aerodrome_reference_code' => 'E']);
- $weightAppropriateStand = Stand::create(
- [
- 'airfield_id' => 1,
- 'terminal_id' => 2,
- 'identifier' => '502',
- 'latitude' => 54.65875500,
- 'longitude' => -6.22258694,
- 'aerodrome_reference_code' => 'E',
- ]
- );
-
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
- $this->assertEquals($weightAppropriateStand->id, $this->allocator->allocate($aircraft));
- }
-
- public function testItDoesntAllocateOccupiedStands()
- {
- $extraStand = Stand::create(
- [
- 'airfield_id' => 1,
- 'identifier' => '502',
- 'latitude' => 54.65875500,
- 'longitude' => -6.22258694,
- 'aerodrome_reference_code' => 'E',
- 'terminal_id' => 2,
- ]
- );
-
- $occupier = $this->createAircraft('EZY7823', 'EGLL');
- $occupier->occupiedStand()->sync([2]);
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
-
- $this->assertEquals($extraStand->id, $this->allocator->allocate($aircraft));
- }
-
- public function testItDoesntAllocateUnavailableStands()
- {
- $extraStand = Stand::create(
- [
- 'airfield_id' => 1,
- 'identifier' => '502',
- 'latitude' => 54.65875500,
- 'longitude' => -6.22258694,
- 'aerodrome_reference_code' => 'E',
- 'terminal_id' => 2,
- ]
- );
- NetworkAircraft::find('BAW123')->occupiedStand()->sync([2]);
-
- $aircraft = $this->createAircraft('BAW23451', 'EGLL');
- $this->assertEquals($extraStand->id, $this->allocator->allocate($aircraft));
- }
-
- public function testItDoesntAllocateNonExistentAirlines()
- {
- $aircraft = $this->createAircraft('***1234', 'EGLL');
- $this->assertNull($this->allocator->allocate($aircraft));
- }
-
- public function testItReturnsNullOnNoStandAllocated()
- {
- Stand::all()->each(function (Stand $stand) {
- $stand->delete();
- });
- $aircraft = $this->createAircraft('BAW999', 'EGLL');
- $this->assertNull($this->allocator->allocate($aircraft));
- }
-
- private function createAircraft(
- string $callsign,
- string $arrivalAirport,
- string $departureAirport = 'EGGD'
- ): NetworkAircraft {
- return NetworkAircraft::create(
- [
- 'callsign' => $callsign,
- 'cid' => 1234,
- 'planned_aircraft' => 'B738',
- 'planned_aircraft_short' => 'B738',
- 'planned_destairport' => $arrivalAirport,
- 'planned_depairport' => $departureAirport,
- ]
- );
- }
-}
diff --git a/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php b/tests/app/Allocator/Stand/CargoAirlineFallbackStandAllocatorTest.php
index 1e93a14e6..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,17 +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,
+ '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 ff2f5859b..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,17 +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,
+ '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 a5660f33c..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
{
@@ -102,7 +107,7 @@ public function testItReturnsNothingIfNoStandsToAllocated()
public function testItDoesntAllocateOccupiedStands()
{
- StandAssignment::create(
+ StandAssignment::create(
[
'callsign' => 'BAW123',
'stand_id' => $this->cargoStand->id
@@ -126,17 +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', 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 6341aeb93..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
{
@@ -167,16 +171,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));
}
@@ -185,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,
@@ -199,6 +480,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..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,18 +227,163 @@ 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,
'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..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,
@@ -266,6 +447,7 @@ private function createAircraft(
'planned_aircraft_short' => 'B738',
'planned_destairport' => $arrivalAirport,
'planned_depairport' => $departureAirport,
+ 'aircraft_id' => 1,
]
);
}
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()
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/Models/Stand/StandTest.php b/tests/app/Models/Stand/StandTest.php
index ef3f8b17b..b83ecb187 100644
--- a/tests/app/Models/Stand/StandTest.php
+++ b/tests/app/Models/Stand/StandTest.php
@@ -120,68 +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'));
- 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/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/AirlineServiceTest.php b/tests/app/Services/AirlineServiceTest.php
index 9974edc6c..251b06d51 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,72 @@ 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'));
+ }
+
+
#[DataProvider('aircraftProvider')]
public function testItReturnsAirlinesForAircraft(string $callsign, string $expectedAirline)
{
diff --git a/tests/app/Services/NetworkAircraftServiceTest.php b/tests/app/Services/NetworkAircraftServiceTest.php
index 0d3fd8f6d..627cc21b6 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,27 @@ 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,9 +362,9 @@ private function getPilotData(
bool $hasFlightplan,
float $latitude = null,
float $longitude = null,
- string $transponder = null
- ): array
- {
+ string $transponder = null,
+ string $aircraftType = 'B738',
+ ): array {
return [
'callsign' => $callsign,
'cid' => self::ACTIVE_USER_CID,
@@ -353,8 +375,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 +391,10 @@ private function getPilotData(
private function getTransformedPilotData(
string $callsign,
bool $hasFlightplan = true,
- string $transponder = null
- ): array
- {
- $pilot = $this->getPilotData($callsign, $hasFlightplan, null, null, $transponder);
+ string $transponder = null,
+ string $aircraftType = 'B738'
+ ): array {
+ $pilot = $this->getPilotData($callsign, $hasFlightplan, null, null, $transponder, $aircraftType);
$baseData = [
'callsign' => $pilot['callsign'],
'cid' => $pilot['cid'],
@@ -394,6 +416,14 @@ 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,
+ 'airline_id' => match (Str::substr($pilot['callsign'], 0, 3)) {
+ '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 5a7ae29c4..8e61588e1 100644
--- a/tests/app/Services/Stand/ArrivalAllocationServiceTest.php
+++ b/tests/app/Services/Stand/ArrivalAllocationServiceTest.php
@@ -4,15 +4,15 @@
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\ArrivalStandAllocatorInterface;
+use App\Allocator\Stand\AirlineGeneralTerminalArrivalStandAllocator;
+use App\Allocator\Stand\ArrivalStandAllocator;
use App\Allocator\Stand\CallsignFlightplanReservedArrivalStandAllocator;
use App\Allocator\Stand\CargoAirlineFallbackStandAllocator;
use App\Allocator\Stand\CargoFlightArrivalStandAllocator;
@@ -26,11 +26,14 @@
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;
+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;
@@ -38,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()
@@ -62,6 +68,8 @@ public function testItDeallocatesStandForDivertingAircraft()
// London
'latitude' => 51.487202,
'longitude' => -0.466667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -86,6 +94,8 @@ public function testItAllocatesANewStandForDivertingAircraft()
// London
'latitude' => 51.487202,
'longitude' => -0.466667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -111,6 +121,8 @@ public function testItDoesntDeallocateStandIfAircraftNotDiverting()
// London
'latitude' => 51.487202,
'longitude' => -0.466667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -135,6 +147,8 @@ public function testItDoesntDeallocateStandIfForDepartureAirport()
// London
'latitude' => 51.487202,
'longitude' => -0.466667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -156,6 +170,8 @@ public function testItDoesntDeallocateStandIfNoStandToDeallocate()
// London
'latitude' => 51.487202,
'longitude' => -0.466667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -176,19 +192,19 @@ 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,
FallbackArrivalStandAllocator::class,
],
array_map(
- fn(ArrivalStandAllocatorInterface $allocator) => get_class($allocator),
+ fn(ArrivalStandAllocator $allocator) => get_class($allocator),
$this->service->getAllocators()
)
);
@@ -219,6 +235,8 @@ public function testItAllocatesAStandFromAllocator()
// London
'latitude' => 51.487202,
'longitude' => -0.466667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -241,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);
@@ -265,6 +285,8 @@ public function testItDoesntAllocateStandIfPerformingCircuits()
// London
'latitude' => 51.487202,
'longitude' => -0.466667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -287,6 +309,8 @@ public function testItDoesntPerformAllocationIfStandTooFarFromAirfield()
// Lambourne
'latitude' => 51.646099,
'longitude' => 0.151667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -309,6 +333,8 @@ public function testItDoesntPerformAllocationIfAircraftHasNoGroundspeed()
// Lambourne
'latitude' => 51.646099,
'longitude' => 0.151667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -337,6 +363,8 @@ public function testItDoesntPerformAllocationIfNoStandAllocated()
// Lambourne
'latitude' => 51.646099,
'longitude' => 0.151667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -359,6 +387,8 @@ public function testItDoesntPerformAllocationIfStandAlreadyAssigned()
// Lambourne
'latitude' => 51.646099,
'longitude' => 0.151667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
StandAssignment::create(
@@ -387,6 +417,8 @@ public function testItDoesntReturnAllocationIfAirfieldNotFound()
// Lambourne
'latitude' => 51.646099,
'longitude' => 0.151667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -409,6 +441,8 @@ public function testItDoesntPerformAllocationIfUnknownAircraftType()
// Lambourne
'latitude' => 51.646099,
'longitude' => 0.151667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => null,
]
);
@@ -433,6 +467,8 @@ public function testItDoesntPerformAllocationIfAircraftTypeNotStandAssignable()
// Lambourne
'latitude' => 51.646099,
'longitude' => 0.151667,
+ 'airline_id' => $this->bmi->id,
+ 'aircraft_id' => 1,
]
);
@@ -451,4 +487,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 => [],
+ AirlineGeneralArrivalStandAllocator::class => [
+ 0 => [
+ $stand4->id,
+ ],
+ ],
+ AirlineCallsignTerminalArrivalStandAllocator::class => [],
+ AirlineCallsignSlugTerminalArrivalStandAllocator::class => [],
+ AirlineAircraftTerminalArrivalStandAllocator::class => [],
+ AirlineDestinationTerminalArrivalStandAllocator::class => [],
+ AirlineGeneralTerminalArrivalStandAllocator::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()
+ );
+ }
}
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;