Files
FlightsAPI/app/Console/Commands/GetAircraftInfoForArrivedFlights.php
T
2026-07-02 23:11:46 +10:00

218 lines
8.5 KiB
PHP

<?php
namespace App\Console\Commands;
use App\DTOs\FlightStatData;
use App\Models\Aircraft;
use App\Models\Notification;
use App\Models\UserFlight;
use App\Services\FlightStatsService;
use Carbon\CarbonInterface;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
#[Signature('app:get-aircraft-info-for-arrived-flights')]
#[Description('Search for flights that have arrived recently and try and determine any new aircraft info.')]
class GetAircraftInfoForArrivedFlights extends Command
{
public function __construct(protected FlightStatsService $flightStats)
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle(): void
{
$now = now()->utc();
$userFlights = UserFlight::where('arrival_date', '<=', $now->copy()->subHour()->toDateTimeString())
->where('auto_update', true)
->whereNotNull('flight_number')
->get();
$this->info("Found {$userFlights->count()} flights.");
foreach ($userFlights as $flight) {
$this->processFlight($flight);
}
}
protected function processFlight(UserFlight $flight): void
{
preg_match('/^([A-Z]{2,3})(\d+)$/i', $flight->flight_number, $matches);
// Case 1: unparseable flight number on our end — disable and move on, no notification.
if (empty($matches)) {
$this->warn("Could not parse flight number: {$flight->flight_number}");
Log::warning("Could not parse flight number for auto-update", ['flight_id' => $flight->id, 'flight_number' => $flight->flight_number]);
$flight->update(['auto_update' => false]);
return;
}
$airlineCode = strtoupper($matches[1]);
$flightNumber = $matches[2];
$arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone);
$data = $this->flightStats->fetchFlightData($airlineCode, $flightNumber, $arrivalDate);
// Case 2: valid flight number, but the API had nothing for it.
if (!$data) {
$this->warn("No flight data returned for {$airlineCode}{$flightNumber}");
$this->notifyLookupFailed($flight, $airlineCode, $flightNumber);
return;
}
// Case 3: API returned data, but for the wrong route — treat like a failed lookup.
if ($data->departure_iata !== $flight->departureAirport->iata_code ||
$data->arrival_iata !== $flight->arrivalAirport->iata_code) {
$this->warn("Airport mismatch for {$airlineCode}{$flightNumber} — API: {$data->departure_iata}{$data->arrival_iata}, expected: {$flight->departureAirport->iata_code}{$flight->arrivalAirport->iata_code}");
$this->notifyLookupFailed($flight, $airlineCode, $flightNumber);
return;
}
// Case 4/5: valid data — apply whatever changed, but always report every field.
$this->applyAndNotify($flight, $data, $airlineCode, $flightNumber);
}
protected function notifyLookupFailed(UserFlight $flight, string $airlineCode, string $flightNumber): void
{
Notification::create([
'user_id' => $flight->user_id,
'title' => "Auto update failed for {$flight->flight_number}",
'body' => "We tried to find up-to-date information for {$airlineCode}{$flightNumber}, but couldn't find a reliable match. Please manually check the aircraft type, registration and departure/arrival times.",
'url' => '/flights/' . $flight->id . '/edit',
]);
$flight->update(['auto_update' => false]);
}
protected function applyAndNotify(UserFlight $flight, FlightStatData $data, string $airlineCode, string $flightNumber): void
{
// Snapshot originals before we touch anything, so descriptions reflect the real "from" state.
$originalRegistration = $flight->aircraft_registration;
$originalDeparture = $flight->departure_date;
$originalArrival = $flight->arrival_date;
$originalAircraft = $flight->aircraft;
$updates = [];
$aircraftNotFound = false;
if ($data->aircraft_registration && $data->aircraft_registration !== $originalRegistration) {
$updates['aircraft_registration'] = $data->aircraft_registration;
}
if ($data->estimated_departure_utc?->ne($originalDeparture)) {
$updates['departure_date'] = $data->estimated_departure_utc;
}
if ($data->estimated_arrival_utc?->ne($originalArrival)) {
$updates['arrival_date'] = $data->estimated_arrival_utc;
}
if ($data->equipment_iata && $originalAircraft?->iata_code !== $data->equipment_iata) {
$match = $this->flightStats->guessAircraftFromIata($data->equipment_iata);
if ($match) {
$updates['aircraft_id'] = $match->id;
} else {
$aircraftNotFound = true;
Log::warning("No aircraft match for IATA code {$data->equipment_iata} on flight {$airlineCode}{$flightNumber}", [
'flight_id' => $flight->id,
]);
}
}
if (!empty($updates)) {
$flight->update($updates);
$this->info("Updated flight {$airlineCode}{$flightNumber}: " . implode(', ', array_keys($updates)));
} else {
$this->info("No changes for {$airlineCode}{$flightNumber}");
}
$flight->update(['auto_update' => false]);
$newAircraft = isset($updates['aircraft_id']) ? Aircraft::find($updates['aircraft_id']) : $originalAircraft;
$lines = [
$this->describeValue(
'Aircraft Registration',
$originalRegistration,
$data->aircraft_registration,
$updates['aircraft_registration'] ?? null,
),
$this->describeDate('Departure', $originalDeparture, $data->estimated_departure_utc, $updates['departure_date'] ?? null),
$this->describeDate('Arrival', $originalArrival, $data->estimated_arrival_utc, $updates['arrival_date'] ?? null),
$this->describeAircraftType($originalAircraft, $newAircraft, $data->equipment_iata, $aircraftNotFound),
];
$title = $aircraftNotFound
? "Flight {$airlineCode}{$flightNumber} updated — aircraft type not found"
: "Flight {$airlineCode}{$flightNumber} updated";
Notification::create([
'user_id' => $flight->user_id,
'title' => $title,
'body' => implode("\n", $lines),
'url' => '/u/' . $flight->user->name . '/flight/' . $flight->id,
]);
}
/**
* Describe a simple string field: from → to, or "no change" / "not found" as appropriate.
*/
protected function describeValue(string $label, ?string $from, ?string $apiValue, ?string $applied): string
{
$fromLabel = $from ?: 'None';
if (!$apiValue) {
return "{$label}: {$fromLabel} (not returned by API — no change)";
}
if (!$applied) {
return "{$label}: {$apiValue} (no change)";
}
return "{$label}: {$fromLabel}{$apiValue}";
}
protected function describeDate(string $label, ?CarbonInterface $from, ?CarbonInterface $apiValue, mixed $applied): string
{
$fromLabel = $from?->format('j M Y H:i') . ' ' . ($from?->tzName ?? '') ?: 'Unknown';
if (!$apiValue) {
return "{$label}: {$fromLabel} (not returned by API — no change)";
}
$toLabel = $apiValue->format('j M Y H:i') . ' ' . $apiValue->tzName;
if (!$applied) {
return "{$label}: {$toLabel} (no change)";
}
return "{$label}: {$fromLabel}{$toLabel}";
}
protected function describeAircraftType(?Aircraft $from, ?Aircraft $to, ?string $equipmentIata, bool $notFound): string
{
$fromLabel = $from?->display_name_short ?? 'None';
if (!$equipmentIata) {
return "Aircraft Type: {$fromLabel} (not returned by API — no change)";
}
if ($notFound) {
return "Aircraft Type: {$fromLabel} (API reported equipment code {$equipmentIata}, but no matching aircraft was found in our database — no change)";
}
if ($to?->id === $from?->id) {
return "Aircraft Type: {$fromLabel} (no change)";
}
return "Aircraft Type: {$fromLabel}{$to?->display_name_short}";
}
}