375 lines
12 KiB
PHP
375 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\ImportExport;
|
|
|
|
use App\Contracts\ImportExport;
|
|
use App\Enums\FlightType;
|
|
use App\Models\Airport;
|
|
use App\Models\Fare;
|
|
use App\Models\Flight;
|
|
use App\Models\FlightBundle;
|
|
use App\Models\Subfleet;
|
|
use App\Services\AirportService;
|
|
use App\Services\FareService;
|
|
use App\Services\FlightService;
|
|
use App\Support\Days;
|
|
use App\Support\FlightTimeParser;
|
|
use Exception;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* The flight importer can be imported or export. Operates on rows
|
|
*/
|
|
class FlightImporter extends ImportExport
|
|
{
|
|
public string $assetType = 'flight';
|
|
|
|
/**
|
|
* All of the columns that are in the CSV import
|
|
* Should match the database fields, for the most part
|
|
*/
|
|
public static array $columns = [
|
|
'airline' => 'required',
|
|
'flight_number' => 'required',
|
|
'route_code' => 'nullable',
|
|
'callsign' => 'nullable',
|
|
'route_leg' => 'nullable',
|
|
'dpt_airport' => 'required',
|
|
'arr_airport' => 'required',
|
|
'alt_airport' => 'nullable',
|
|
'days' => 'nullable',
|
|
'dpt_time' => 'nullable',
|
|
'arr_time' => 'nullable',
|
|
'level' => 'nullable|integer',
|
|
'distance' => 'nullable|numeric',
|
|
'flight_time' => 'required|integer',
|
|
'flight_type' => 'required|alpha',
|
|
'load_factor' => 'nullable|numeric|min:0|max:100',
|
|
'load_factor_variance' => 'nullable|numeric|min:0|max:100',
|
|
'pilot_pay' => 'nullable',
|
|
'route' => 'nullable',
|
|
'notes' => 'nullable',
|
|
'start_date' => 'nullable|date',
|
|
'end_date' => 'nullable|date',
|
|
'enabled' => 'nullable|boolean',
|
|
'subfleets' => 'nullable',
|
|
'fares' => 'nullable',
|
|
'fields' => 'nullable',
|
|
'event_id' => 'nullable|integer',
|
|
'user_id' => 'nullable|integer',
|
|
];
|
|
|
|
private readonly AirportService $airportSvc;
|
|
|
|
private readonly FareService $fareSvc;
|
|
|
|
private readonly FlightService $flightSvc;
|
|
|
|
/**
|
|
* Memoized id of the bundle named "Default", used as the bundle_id fallback
|
|
* for rows whose CSV value is blank. Cached per importer instance to avoid
|
|
* an N+1 lookup across imports of many rows. `false` sentinel = lookup not
|
|
* yet attempted.
|
|
*/
|
|
private int|false|null $defaultBundleId = false;
|
|
|
|
/**
|
|
* FlightImportExporter constructor.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->airportSvc = app(AirportService::class);
|
|
$this->fareSvc = app(FareService::class);
|
|
$this->flightSvc = app(FlightService::class);
|
|
}
|
|
|
|
/**
|
|
* Import a flight, parse out the different rows
|
|
*/
|
|
public function import(array $row, int $index): bool
|
|
{
|
|
// Map legacy `dpt_time` / `arr_time` CSV columns onto the structured
|
|
// `departure_time` / `arrival_time` columns by parsing the free-form
|
|
// input through FlightTimeParser. Preserves backward CSV compatibility
|
|
// for re-imports of archived exports.
|
|
if (array_key_exists('dpt_time', $row)) {
|
|
$row['departure_time'] = filled($row['dpt_time'])
|
|
? FlightTimeParser::parse((string) $row['dpt_time'])
|
|
: null;
|
|
unset($row['dpt_time']);
|
|
}
|
|
|
|
if (array_key_exists('arr_time', $row)) {
|
|
$row['arrival_time'] = filled($row['arr_time'])
|
|
? FlightTimeParser::parse((string) $row['arr_time'])
|
|
: null;
|
|
unset($row['arr_time']);
|
|
}
|
|
|
|
// Get the airline ID from the ICAO code
|
|
$airline = $this->getAirline($row['airline']);
|
|
|
|
// Try to find this flight
|
|
/** @var Flight $flight */
|
|
$flight = Flight::withTrashed()->firstOrNew([
|
|
'airline_id' => $airline->id,
|
|
'flight_number' => $row['flight_number'],
|
|
'dpt_airport_id' => strtoupper((string) $row['dpt_airport']),
|
|
'arr_airport_id' => strtoupper((string) $row['arr_airport']),
|
|
'route_code' => filled($row['route_code']) ? $row['route_code'] : null,
|
|
'route_leg' => filled($row['route_leg']) ? $row['route_leg'] : null,
|
|
'days' => filled($row['days']) ? $this->setDays($row['days']) : 0,
|
|
], $row);
|
|
|
|
$row['dpt_airport'] = strtoupper((string) $row['dpt_airport']);
|
|
$row['arr_airport'] = strtoupper((string) $row['arr_airport']);
|
|
|
|
// Airport attributes
|
|
$flight->setAttribute('dpt_airport_id', $row['dpt_airport']);
|
|
$flight->setAttribute('arr_airport_id', $row['arr_airport']);
|
|
if (filled($row['alt_airport'])) {
|
|
$flight->setAttribute('alt_airport_id', strtoupper((string) $row['alt_airport']));
|
|
}
|
|
|
|
// Handle schedule details (days)
|
|
$flight->setAttribute('days', $this->setDays($row['days']));
|
|
|
|
// Handle route and Level fields
|
|
$flight->setAttribute('route', strtoupper((string) $row['route']));
|
|
$flight->setAttribute('level', $row['level']);
|
|
|
|
// Check row for empty/blank values and set them null
|
|
if (blank($row['route_code'])) {
|
|
$flight->setAttribute('route_code', null);
|
|
}
|
|
|
|
if (blank($row['route_leg'])) {
|
|
$flight->setAttribute('route_leg', null);
|
|
}
|
|
|
|
if (blank($row['level'])) {
|
|
$flight->setAttribute('level', null);
|
|
}
|
|
|
|
if (blank($row['load_factor'])) {
|
|
$flight->setAttribute('load_factor', null);
|
|
}
|
|
|
|
if (blank($row['load_factor_variance'])) {
|
|
$flight->setAttribute('load_factor_variance', null);
|
|
}
|
|
|
|
if (blank($row['pilot_pay'])) {
|
|
$flight->setAttribute('pilot_pay', null);
|
|
} else {
|
|
$flight->setAttribute('pilot_pay', (float) $row['pilot_pay']);
|
|
}
|
|
|
|
if (blank($row['start_date'])) {
|
|
$flight->setAttribute('start_date', null);
|
|
}
|
|
|
|
if (blank($row['end_date'])) {
|
|
$flight->setAttribute('end_date', null);
|
|
}
|
|
|
|
if (blank($row['event_id'])) {
|
|
$flight->setAttribute('event_id', null);
|
|
}
|
|
|
|
if (blank($row['user_id'])) {
|
|
$flight->setAttribute('user_id', null);
|
|
}
|
|
|
|
// Check/calculate the distance
|
|
if (blank($row['distance'])) {
|
|
$flight->setAttribute('distance', $this->airportSvc->calculateDistance($row['dpt_airport'], $row['arr_airport']));
|
|
}
|
|
|
|
// Restore the flight if it is deleted
|
|
$flight->setAttribute('deleted_at', null);
|
|
|
|
// Any other specific transformations
|
|
$flight->setAttribute('notes', filled($row['notes']) ? $row['notes'] : null);
|
|
|
|
$flight->setAttribute('callsign', filled($row['callsign']) ? $row['callsign'] : null);
|
|
|
|
// Check for a valid value
|
|
$flight_type = $row['flight_type'];
|
|
if (!array_key_exists($flight_type, FlightType::labels())) {
|
|
$flight_type = FlightType::SCHED_PAX;
|
|
}
|
|
|
|
$flight->setAttribute('flight_type', $flight_type);
|
|
$flight->setAttribute('enabled', get_truth_state($row['enabled'] ?? $row['active'] ?? false));
|
|
|
|
if (filled($row['bundle_id'] ?? null)) {
|
|
$flight->setAttribute('bundle_id', (int) $row['bundle_id']);
|
|
} elseif (blank($flight->bundle_id)) {
|
|
$flight->setAttribute('bundle_id', $this->getDefaultBundleId());
|
|
}
|
|
|
|
try {
|
|
$flight->save();
|
|
} catch (Exception $exception) {
|
|
$this->errorLog('Error in row '.($index + 1).': '.$exception->getMessage());
|
|
|
|
return false;
|
|
}
|
|
|
|
// Create/check that they exist
|
|
$this->processAirport($row['dpt_airport']);
|
|
$this->processAirport($row['arr_airport']);
|
|
if ($row['alt_airport']) {
|
|
$this->processAirport($row['alt_airport']);
|
|
}
|
|
|
|
$this->processSubfleets($flight, $row['subfleets'] ?? '');
|
|
$this->processFares($flight, $row['fares'] ?? '');
|
|
$this->processFields($flight, $row['fields'] ?? '');
|
|
|
|
$this->log('Imported row '.($index + 1));
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Resolve the id of the bundle named "Default" once per importer instance.
|
|
*
|
|
* `bundle_id` resolution precedence: explicit CSV value > existing flight
|
|
* value > this "Default" bundle. Returns null only when no bundle named
|
|
* "Default" exists in the database (rare; usually the migration seeds one).
|
|
*/
|
|
private function getDefaultBundleId(): ?int
|
|
{
|
|
if ($this->defaultBundleId === false) {
|
|
$id = FlightBundle::query()->where('name', 'Default')->value('id');
|
|
$this->defaultBundleId = $id === null ? null : (int) $id;
|
|
}
|
|
|
|
return $this->defaultBundleId;
|
|
}
|
|
|
|
/**
|
|
* Return the mask of the days
|
|
*/
|
|
protected function setDays($day_str): int
|
|
{
|
|
if (!$day_str) {
|
|
return 0;
|
|
}
|
|
|
|
$days = [];
|
|
if (str_contains((string) $day_str, '1')) {
|
|
$days[] = Days::MONDAY;
|
|
}
|
|
|
|
if (str_contains((string) $day_str, '2')) {
|
|
$days[] = Days::TUESDAY;
|
|
}
|
|
|
|
if (str_contains((string) $day_str, '3')) {
|
|
$days[] = Days::WEDNESDAY;
|
|
}
|
|
|
|
if (str_contains((string) $day_str, '4')) {
|
|
$days[] = Days::THURSDAY;
|
|
}
|
|
|
|
if (str_contains((string) $day_str, '5')) {
|
|
$days[] = Days::FRIDAY;
|
|
}
|
|
|
|
if (str_contains((string) $day_str, '6')) {
|
|
$days[] = Days::SATURDAY;
|
|
}
|
|
|
|
if (str_contains((string) $day_str, '7')) {
|
|
$days[] = Days::SUNDAY;
|
|
}
|
|
|
|
return Days::getDaysMask($days);
|
|
}
|
|
|
|
/**
|
|
* Process the airport
|
|
*/
|
|
protected function processAirport($airport): Airport
|
|
{
|
|
return $this->airportSvc->lookupAirportIfNotFound($airport);
|
|
}
|
|
|
|
/**
|
|
* Parse out all of the subfleets and associate them to the flight
|
|
* The subfleet is created if it doesn't exist
|
|
*/
|
|
protected function processSubfleets(Flight $flight, string $col): void
|
|
{
|
|
$count = 0;
|
|
$subfleets = $this->parseMultiColumnValues($col);
|
|
foreach ($subfleets as $subfleet_type) {
|
|
$subfleet_type = trim((string) $subfleet_type);
|
|
if ($subfleet_type === '') {
|
|
continue;
|
|
}
|
|
|
|
if ($subfleet_type === '0') {
|
|
continue;
|
|
}
|
|
|
|
$subfleet = Subfleet::firstOrCreate(
|
|
['type' => $subfleet_type],
|
|
[
|
|
'name' => $subfleet_type,
|
|
'airline_id' => $flight->airline_id,
|
|
]
|
|
);
|
|
|
|
$subfleet->save();
|
|
|
|
// sync
|
|
$flight->subfleets()->syncWithoutDetaching([$subfleet->id]);
|
|
$count++;
|
|
}
|
|
|
|
Log::info('Subfleets added/processed: '.$count);
|
|
}
|
|
|
|
/**
|
|
* Parse all of the fares in the multi-format
|
|
*/
|
|
protected function processFares(Flight $flight, string $col): void
|
|
{
|
|
$fares = $this->parseMultiColumnValues($col);
|
|
foreach ($fares as $fare_code => $fare_attributes) {
|
|
if (\is_int($fare_code)) {
|
|
$fare_code = $fare_attributes;
|
|
$fare_attributes = [];
|
|
}
|
|
|
|
$fare = Fare::firstOrCreate(['code' => $fare_code], ['name' => $fare_code]);
|
|
$this->fareSvc->setForFlight($flight, $fare, $fare_attributes);
|
|
$fare->save();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse all of the subfields
|
|
*/
|
|
protected function processFields(Flight $flight, string $col): void
|
|
{
|
|
$pass_fields = [];
|
|
$fields = $this->parseMultiColumnValues($col);
|
|
foreach ($fields as $field_name => $field_value) {
|
|
$pass_fields[] = [
|
|
'name' => $field_name,
|
|
'value' => $field_value,
|
|
];
|
|
}
|
|
|
|
$this->flightSvc->updateCustomFields($flight, $pass_fields);
|
|
}
|
|
}
|