travelExportClient->fetchAll(); } catch (ApiException $e) { $io->error('Der Reise-Export konnte nicht von MyE&P geladen werden – Import abgebrochen.'); $this->logger->error('Import aborted: the MyE&P travel export could not be read', [ 'reason' => $e->getMessage(), ]); return Command::FAILURE; } // An empty export would otherwise be a run that touches nothing and reports success, // which reads as "nothing changed upstream" rather than as a failure to read it. if ([] === $export->rows) { $io->error('Der Reise-Export von MyE&P enthält keine Reisen – Import abgebrochen.'); $this->logger->error('Import aborted: the MyE&P travel export was empty'); return Command::FAILURE; } // Every destination this import touches gets its cost unit and its country written from // the export, on update as well as on insert. If MyE&P stops reporting those two fields, // a run would quietly blank them on rows that have them: the cost unit then drops off // the invoices (App\Service\Pdf\InvoiceApprover) and a missing country code blanks the // flag icons and takes the row out of the country filters. Refusing to run is the // recoverable failure; overwriting good data is not. if ([] !== $missingFields = $this->missingHotelFields($export->rows)) { $io->error(sprintf( 'Die MyE&P-API liefert keine %s – Import abgebrochen, damit bestehende Werte nicht überschrieben werden.', implode(' und ', $missingFields), )); $this->logger->error('Import aborted: the MyE&P travel export carries no '.implode(', ', $missingFields)); return Command::FAILURE; } $io->info(sprintf('%d Reisen von MyE&P geladen', count($export->rows))); // Both are losses MyE&P made silently on its side: a skipped pair is a travel the // disposition will never offer, and a boarding point without master data reaches the // form with no town. Logged every run so they get chased instead of settling in. $this->logger->info('MyE&P travel export read', [ 'rows' => count($export->rows), 'skipped' => $export->skipped, 'pickups_without_master_data' => $export->pickupsWithoutMasterData, 'generated_at' => $export->generatedAt, ]); if (0 < $export->skipped || 0 < $export->pickupsWithoutMasterData) { $io->warning(sprintf( 'MyE&P hat %d Reise-Hotel-Kombinationen verworfen und %d Zustiege ohne Stammdaten gemeldet.', $export->skipped, $export->pickupsWithoutMasterData, )); } $now = new \DateTimeImmutable(); $addedCount = 0; $updatedCount = 0; $progressBar = $io->createProgressBar(count($export->rows)); $progressBar->setFormat(" %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%\n %message%"); $progressBar->setMessage('Starting'); $progressBar->start(); foreach ($export->rows as $row) { $progressBar->setMessage(sprintf('%s %s', $row->dateFrom->format('d.m.Y'), $row->product)); $id = $this->connection->fetchOne( 'SELECT id FROM destination WHERE bus_pro_id=? AND hotel_bus_pro_id=?', [$row->busProId, $row->hotelBusProId], ); if ($id) { $this->connection->update('destination', $row->toUpdatePayload($now), ['id' => $id]); ++$updatedCount; } else { $this->connection->insert('destination', $row->toInsertPayload($now)); ++$addedCount; } $progressBar->advance(); } $progressBar->setMessage('Done'); $progressBar->finish(); $io->newLine(2); $io->success(sprintf('%d Destinationen angelegt, %d aktualisiert', $addedCount, $updatedCount)); $this->logger->info('MyE&P destination import finished', [ 'added' => $addedCount, 'updated' => $updatedCount, ]); return Command::SUCCESS; } /** * Names the hotel fields MyE&P is not reporting, German labels for the operator. * * Asserted across the whole export rather than per row, because a row legitimately has * neither: only a minority of hotels carry a kostenstelle upstream, and some carry no * country BusProNet can resolve. An export where not one row has the field is a field * missing from the response, not a gap in the data. * * @param DestinationRow[] $rows * * @return list */ private function missingHotelFields(array $rows): array { $required = [ 'Kostenstellen' => static fn (DestinationRow $row): bool => '' !== (string) $row->costUnit, 'Länderkennungen' => static fn (DestinationRow $row): bool => '' !== (string) $row->country, ]; $missing = []; foreach ($required as $label => $isReported) { foreach ($rows as $row) { if ($isReported($row)) { continue 2; } } $missing[] = $label; } return $missing; } }