From 6ee2bcde1e1e0ef6c0997cbf9fd847eb3936ce7f Mon Sep 17 00:00:00 2001 From: Levin Herr Date: Fri, 13 Dec 2024 09:31:44 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Preparations=20for=20multiple=20dat?= =?UTF-8?q?a=20sources=20(#3034)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/RefreshCurrentTrips.php | 43 ++- app/DataProviders/DataProviderBuilder.php | 10 + app/DataProviders/DataProviderInterface.php | 24 ++ app/DataProviders/FptfHelper.php | 12 + .../Hafas.php} | 325 ++++-------------- app/DataProviders/HafasStopoverService.php | 103 ++++++ .../Repositories/StationRepository.php | 62 ++++ app/Http/Controllers/API/v1/Controller.php | 9 + .../Controllers/API/v1/EventController.php | 3 +- .../API/v1/TransportController.php | 8 +- .../Backend/Transport/StationController.php | 34 -- .../Frontend/Admin/CheckinController.php | 83 ++++- .../Frontend/Admin/EventController.php | 16 +- .../FrontendTransportController.php | 29 +- app/Http/Controllers/TransportController.php | 85 +---- app/Jobs/RefreshStopover.php | 9 +- .../CheckinHydratorRepository.php | 7 +- ...26_203951_create_train_stopovers_table.php | 4 +- tests/Feature/CheckinTest.php | 13 +- tests/Feature/StationSearchTest.php | 32 +- .../Feature/Transport/BackendCheckinTest.php | 48 +-- .../Feature/Transport/TransportStatsTest.php | 11 - tests/Feature/Webhooks/WebhookStatusTest.php | 18 +- tests/TestHelpers/HafasHelpers.php | 46 +++ 24 files changed, 546 insertions(+), 488 deletions(-) create mode 100644 app/DataProviders/DataProviderBuilder.php create mode 100644 app/DataProviders/DataProviderInterface.php create mode 100644 app/DataProviders/FptfHelper.php rename app/{Http/Controllers/HafasController.php => DataProviders/Hafas.php} (52%) create mode 100644 app/DataProviders/HafasStopoverService.php create mode 100644 app/DataProviders/Repositories/StationRepository.php create mode 100644 tests/TestHelpers/HafasHelpers.php diff --git a/app/Console/Commands/RefreshCurrentTrips.php b/app/Console/Commands/RefreshCurrentTrips.php index 7b7e447b7..11ad93fe9 100644 --- a/app/Console/Commands/RefreshCurrentTrips.php +++ b/app/Console/Commands/RefreshCurrentTrips.php @@ -2,11 +2,13 @@ namespace App\Console\Commands; +use App\DataProviders\DataProviderBuilder; +use App\DataProviders\DataProviderInterface; +use App\DataProviders\HafasStopoverService; use App\Enum\TripSource; use App\Exceptions\HafasException; -use App\Http\Controllers\HafasController; -use App\Models\Trip; use App\Models\Checkin; +use App\Models\Trip; use Illuminate\Console\Command; use PDOException; @@ -15,6 +17,11 @@ class RefreshCurrentTrips extends Command protected $signature = 'trwl:refreshTrips'; protected $description = 'Refresh delay data from current active trips'; + private function getDataProvider(): DataProviderInterface { + // Probably only HafasController is needed here, because this Command is very Hafas specific + return (new DataProviderBuilder)->build(); + } + public function handle(): int { $this->info('Getting trips to be refreshed...'); @@ -23,22 +30,22 @@ public function handle(): int { ->join('train_stopovers as origin_stopovers', 'origin_stopovers.id', '=', 'train_checkins.origin_stopover_id') ->join('train_stopovers as destination_stopovers', 'destination_stopovers.id', '=', 'train_checkins.destination_stopover_id') ->where(function($query) { - $query->where('destination_stopovers.arrival_planned', '>=', now()->subMinutes(20)) - ->orWhere('destination_stopovers.arrival_real', '>=', now()->subMinutes(20)); - }) + $query->where('destination_stopovers.arrival_planned', '>=', now()->subMinutes(20)) + ->orWhere('destination_stopovers.arrival_real', '>=', now()->subMinutes(20)); + }) ->where(function($query) { - $query->where('origin_stopovers.departure_planned', '<=', now()->addMinutes(20)) - ->orWhere('origin_stopovers.departure_real', '<=', now()->addMinutes(20)); - }) + $query->where('origin_stopovers.departure_planned', '<=', now()->addMinutes(20)) + ->orWhere('origin_stopovers.departure_real', '<=', now()->addMinutes(20)); + }) ->where(function($query) { - $query->where('hafas_trips.last_refreshed', '<', now()->subMinutes(5)) - ->orWhereNull('hafas_trips.last_refreshed'); - }) - ->where('hafas_trips.source', TripSource::HAFAS->value) - ->select('hafas_trips.*') - ->distinct() - ->orderBy('hafas_trips.last_refreshed') - ->get(); + $query->where('hafas_trips.last_refreshed', '<', now()->subMinutes(5)) + ->orWhereNull('hafas_trips.last_refreshed'); + }) + ->where('hafas_trips.source', TripSource::HAFAS->value) + ->select('hafas_trips.*') + ->distinct() + ->orderBy('hafas_trips.last_refreshed') + ->get(); if ($trips->isEmpty()) { $this->warn('No trips to be refreshed'); @@ -53,8 +60,8 @@ public function handle(): int { $this->info('Refreshing trip ' . $trip->trip_id . ' (' . $trip->linename . ')...'); $trip->update(['last_refreshed' => now()]); - $rawHafas = HafasController::fetchRawHafasTrip($trip->trip_id, $trip->linename); - $updatedCounts = HafasController::refreshStopovers($rawHafas); + $rawHafas = $this->getDataProvider()->fetchRawHafasTrip($trip->trip_id, $trip->linename); + $updatedCounts = HafasStopoverService::refreshStopovers($rawHafas); $this->info('Updated ' . $updatedCounts->stopovers . ' stopovers.'); //set duration for refreshed trips to null, so it will be recalculated diff --git a/app/DataProviders/DataProviderBuilder.php b/app/DataProviders/DataProviderBuilder.php new file mode 100644 index 000000000..ff11dbecf --- /dev/null +++ b/app/DataProviders/DataProviderBuilder.php @@ -0,0 +1,10 @@ +timeout(config('trwl.db_rest_timeout')); } - public static function getStationByRilIdentifier(string $rilIdentifier): ?Station { + public function getStationByRilIdentifier(string $rilIdentifier): ?Station { $station = Station::where('rilIdentifier', $rilIdentifier)->first(); if ($station !== null) { return $station; } + try { - $response = self::getHttpClient() - ->get("/stations/$rilIdentifier"); - if (!$response->ok()) { + $response = $this->client()->get("/stations/$rilIdentifier"); + if ($response->ok() && !empty($response->body()) && $response->body() !== '[]') { + $data = json_decode($response->body(), false, 512, JSON_THROW_ON_ERROR); + $station = StationRepository::parseHafasStopObject($data); + CacheKey::increment(HCK::STATIONS_SUCCESS); + } else { CacheKey::increment(HCK::STATIONS_NOT_OK); - return null; } - $data = json_decode($response->body(), false, 512, JSON_THROW_ON_ERROR); - CacheKey::increment(HCK::STATIONS_SUCCESS); - return Station::updateOrCreate([ - 'ibnr' => $data->id - ], [ - 'rilIdentifier' => $data->ril100, - 'name' => $data->name, - 'latitude' => $data->location->latitude, - 'longitude' => $data->location->longitude - ]); } catch (Exception $exception) { CacheKey::increment(HCK::STATIONS_FAILURE); report($exception); } - return null; + return $station; } - public static function getStationsByFuzzyRilIdentifier(string $rilIdentifier): ?Collection { + public function getStationsByFuzzyRilIdentifier(string $rilIdentifier): ?Collection { $stations = Station::where('rilIdentifier', 'LIKE', "$rilIdentifier%")->orderBy('rilIdentifier')->get(); if ($stations->count() > 0) { return $stations; } - return collect([self::getStationByRilIdentifier(rilIdentifier: $rilIdentifier)]); + return collect([$this->getStationByRilIdentifier(rilIdentifier: $rilIdentifier)]); } /** * @throws HafasException */ - public static function getStations(string $query, int $results = 10): Collection { + public function getStations(string $query, int $results = 10): Collection { try { - $response = self::getHttpClient() - ->get("/locations", - [ - 'query' => $query, - 'fuzzy' => 'true', - 'stops' => 'true', - 'addresses' => 'false', - 'poi' => 'false', - 'results' => $results - ]); + $response = $this->client()->get( + "/locations", + [ + 'query' => $query, + 'fuzzy' => 'true', + 'stops' => 'true', + 'addresses' => 'false', + 'poi' => 'false', + 'results' => $results + ] + ); $data = json_decode($response->body(), false, 512, JSON_THROW_ON_ERROR); if (!$response->ok()) { @@ -93,7 +90,7 @@ public static function getStations(string $query, int $results = 10): Collection } CacheKey::increment(HCK::LOCATIONS_SUCCESS); - return self::parseHafasStops($data); + return Repositories\StationRepository::parseHafasStops($data); } catch (JsonException $exception) { throw new HafasException($exception->getMessage()); } catch (Exception $exception) { @@ -102,58 +99,19 @@ public static function getStations(string $query, int $results = 10): Collection } } - /** - * @param stdClass $hafasStop - * - * @return Station - * @throws PDOException - */ - public static function parseHafasStopObject(stdClass $hafasStop): Station { - return Station::updateOrCreate([ - 'ibnr' => $hafasStop->id - ], [ - 'name' => $hafasStop->name, - 'latitude' => $hafasStop->location?->latitude, - 'longitude' => $hafasStop->location?->longitude, - ]); - } - - private static function parseHafasStops(array $hafasResponse): Collection { - $payload = []; - foreach ($hafasResponse as $hafasStation) { - $payload[] = [ - 'ibnr' => $hafasStation->id, - 'name' => $hafasStation->name, - 'latitude' => $hafasStation?->location?->latitude, - 'longitude' => $hafasStation?->location?->longitude, - ]; - } - return self::upsertStations($payload); - } - - private static function upsertStations(array $payload) { - $ibnrs = array_column($payload, 'ibnr'); - if (empty($ibnrs)) { - return new Collection(); - } - Station::upsert($payload, ['ibnr'], ['name', 'latitude', 'longitude']); - return Station::whereIn('ibnr', $ibnrs)->get() - ->sortBy(function(Station $station) use ($ibnrs) { - return array_search($station->ibnr, $ibnrs); - }) - ->values(); - } - /** * @throws HafasException */ - public static function getNearbyStations(float $latitude, float $longitude, int $results = 8): Collection { + public function getNearbyStations(float $latitude, float $longitude, int $results = 8): Collection { try { - $response = self::getHttpClient()->get("/stops/nearby", [ - 'latitude' => $latitude, - 'longitude' => $longitude, - 'results' => $results - ]); + $response = $this->client()->get( + "/stops/nearby", + [ + 'latitude' => $latitude, + 'longitude' => $longitude, + 'results' => $results + ] + ); if (!$response->ok()) { CacheKey::increment(HCK::NEARBY_NOT_OK); @@ -161,7 +119,7 @@ public static function getNearbyStations(float $latitude, float $longitude, int } $data = json_decode($response->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR); - $stations = self::parseHafasStops($data); + $stations = Repositories\StationRepository::parseHafasStops($data); foreach ($data as $hafasStation) { $station = $stations->where('ibnr', $hafasStation->id)->first(); @@ -180,31 +138,30 @@ public static function getNearbyStations(float $latitude, float $longitude, int * @throws HafasException * @throws JsonException */ - public static function fetchDepartures( + private function fetchDepartures( Station $station, Carbon $when, int $duration = 15, TravelType $type = null, bool $skipTimeShift = false ) { - $client = self::getHttpClient(); - $time = $skipTimeShift ? $when : (clone $when)->shiftTimezone("Europe/Berlin"); - $query = [ + $time = $skipTimeShift ? $when : (clone $when)->shiftTimezone("Europe/Berlin"); + $query = [ 'when' => $time->toIso8601String(), 'duration' => $duration, - HTT::NATIONAL_EXPRESS->value => self::checkTravelType($type, TravelType::EXPRESS), - HTT::NATIONAL->value => self::checkTravelType($type, TravelType::EXPRESS), - HTT::REGIONAL_EXP->value => self::checkTravelType($type, TravelType::EXPRESS), - HTT::REGIONAL->value => self::checkTravelType($type, TravelType::REGIONAL), - HTT::SUBURBAN->value => self::checkTravelType($type, TravelType::SUBURBAN), - HTT::BUS->value => self::checkTravelType($type, TravelType::BUS), - HTT::FERRY->value => self::checkTravelType($type, TravelType::FERRY), - HTT::SUBWAY->value => self::checkTravelType($type, TravelType::SUBWAY), - HTT::TRAM->value => self::checkTravelType($type, TravelType::TRAM), - HTT::TAXI->value => self::checkTravelType($type, TravelType::TAXI), + HTT::NATIONAL_EXPRESS->value => FptfHelper::checkTravelType($type, TravelType::EXPRESS), + HTT::NATIONAL->value => FptfHelper::checkTravelType($type, TravelType::EXPRESS), + HTT::REGIONAL_EXP->value => FptfHelper::checkTravelType($type, TravelType::EXPRESS), + HTT::REGIONAL->value => FptfHelper::checkTravelType($type, TravelType::REGIONAL), + HTT::SUBURBAN->value => FptfHelper::checkTravelType($type, TravelType::SUBURBAN), + HTT::BUS->value => FptfHelper::checkTravelType($type, TravelType::BUS), + HTT::FERRY->value => FptfHelper::checkTravelType($type, TravelType::FERRY), + HTT::SUBWAY->value => FptfHelper::checkTravelType($type, TravelType::SUBWAY), + HTT::TRAM->value => FptfHelper::checkTravelType($type, TravelType::TRAM), + HTT::TAXI->value => FptfHelper::checkTravelType($type, TravelType::TAXI), ]; try { - $response = $client->get('/stops/' . $station->ibnr . '/departures', $query); + $response = $this->client()->get('/stops/' . $station->ibnr . '/departures', $query); } catch (Exception $exception) { CacheKey::increment(HCK::DEPARTURES_FAILURE); throw new HafasException($exception->getMessage()); @@ -219,10 +176,6 @@ public static function fetchDepartures( return json_decode($response->body(), false, 512, JSON_THROW_ON_ERROR); } - public static function checkTravelType(?TravelType $type, TravelType $travelType): string { - return (is_null($type) || $type === $travelType) ? 'true' : 'false'; - } - /** * @param Station $station * @param Carbon $when @@ -233,7 +186,7 @@ public static function checkTravelType(?TravelType $type, TravelType $travelType * @return Collection * @throws HafasException */ - public static function getDepartures( + public function getDepartures( Station $station, Carbon $when, int $duration = 15, @@ -243,7 +196,7 @@ public static function getDepartures( try { $requestTime = is_null($station->time_offset) || $localtime ? $when : (clone $when)->subHours($station->time_offset); - $data = self::fetchDepartures( + $data = $this->fetchDepartures( $station, $requestTime, $duration, @@ -262,14 +215,14 @@ public static function getDepartures( // Check if the timezone for this station is equal in its offset to Europe/Berlin. // If so, fetch again **without** adjusting the timezone if ($timezone === CarbonTimeZone::create("Europe/Berlin")->toOffsetName()) { - $data = self::fetchDepartures($station, $when, $duration, $type, true); + $data = $this->fetchDepartures($station, $when, $duration, $type, true); $station->shift_time = false; $station->save(); break; } // if the timezone is not equal to Europe/Berlin, fetch the offset - $data = self::fetchDepartures($station, (clone $when)->subHours($offset), $duration, $type); + $data = $this->fetchDepartures($station, (clone $when)->subHours($offset), $duration, $type); $station->time_offset = $offset; $station->save(); @@ -292,7 +245,7 @@ public static function getDepartures( 'longitude' => $departure->stop?->location?->longitude, ]; } - $stations = self::upsertStations($stationPayload); + $stations = Repositories\StationRepository::upsertStations($stationPayload); //Then match the stations to the departures $departures = collect(); @@ -307,76 +260,12 @@ public static function getDepartures( } } - /** - * Get the Stopover Model from Database - * - * @param int $ibnr - * @param string|null $name - * @param float|null $latitude - * @param float|null $longitude - * - * @return Station - * @throws HafasException - */ - public static function getStation( - int $ibnr, - string $name = null, - float $latitude = null, - float $longitude = null - ): Station { - - if ($name === null || $latitude === null || $longitude === null) { - $dbStation = Station::where('ibnr', $ibnr)->first(); - return $dbStation ?? self::fetchStation($ibnr); - } - return Station::updateOrCreate([ - 'ibnr' => $ibnr - ], [ - 'name' => $name, - 'latitude' => $latitude, - 'longitude' => $longitude - ]); - } - - /** - * Fetch from HAFAS - * - * @param int $ibnr - * - * @return Station - * @throws HafasException - */ - private static function fetchStation(int $ibnr): Station { - try { - $response = self::getHttpClient()->get("/stops/$ibnr"); - } catch (Exception $exception) { - CacheKey::increment(HCK::STOPS_FAILURE); - throw new HafasException($exception->getMessage()); - } - - if (!$response->ok()) { - CacheKey::increment(HCK::STOPS_NOT_OK); - throw new HafasException($response->reason()); - } - - $data = json_decode($response->body()); - CacheKey::increment(HCK::STOPS_SUCCESS); - return Station::updateOrCreate([ - 'ibnr' => $data->id - ], [ - 'name' => $data->name, - 'latitude' => $data->location->latitude, - 'longitude' => $data->location->longitude - ]); - - } - /** * @throws HafasException|JsonException */ - public static function fetchRawHafasTrip(string $tripId, string $lineName) { + public function fetchRawHafasTrip(string $tripId, string $lineName) { try { - $tripResponse = self::getHttpClient()->get("trips/" . rawurlencode($tripId), [ + $tripResponse = $this->client()->get("trips/" . rawurlencode($tripId), [ 'lineName' => $lineName, 'polyline' => 'true', 'stopovers' => 'true' @@ -411,10 +300,10 @@ public static function fetchRawHafasTrip(string $tripId, string $lineName) { * @return Trip * @throws HafasException|JsonException */ - public static function fetchHafasTrip(string $tripID, string $lineName): Trip { - $tripJson = self::fetchRawHafasTrip($tripID, $lineName); - $origin = self::parseHafasStopObject($tripJson->origin); - $destination = self::parseHafasStopObject($tripJson->destination); + public function fetchHafasTrip(string $tripID, string $lineName): Trip { + $tripJson = $this->fetchRawHafasTrip($tripID, $lineName); + $origin = Repositories\StationRepository::parseHafasStopObject($tripJson->origin); + $destination = Repositories\StationRepository::parseHafasStopObject($tripJson->destination); $operator = null; if (isset($tripJson->line->operator->id)) { @@ -462,7 +351,7 @@ public static function fetchHafasTrip(string $tripID, string $lineName): Trip { 'longitude' => $stopover->stop->location?->longitude, ]; } - $stations = self::upsertStations($payload); + $stations = Repositories\StationRepository::upsertStations($payload); foreach ($tripJson->stopovers as $stopover) { //TODO: make this better 🤯 @@ -514,84 +403,4 @@ public static function fetchHafasTrip(string $tripID, string $lineName): Trip { } return $trip; } - - public static function refreshStopovers(stdClass $rawHafas): stdClass { - $stopoversUpdated = 0; - $payloadArrival = []; - $payloadDeparture = []; - $payloadCancelled = []; - foreach ($rawHafas->stopovers ?? [] as $stopover) { - if (!isset($stopover->arrivalDelay) && !isset($stopover->departureDelay) && !isset($stopover->cancelled)) { - continue; // No realtime data present for this stopover, keep existing data - } - - $stop = self::parseHafasStopObject($stopover->stop); - $arrivalPlanned = Carbon::parse($stopover->plannedArrival)->tz(config('app.timezone')); - $departurePlanned = Carbon::parse($stopover->plannedDeparture)->tz(config('app.timezone')); - - $basePayload = [ - 'trip_id' => $rawHafas->id, - 'train_station_id' => $stop->id, - 'arrival_planned' => isset($stopover->plannedArrival) ? $arrivalPlanned : $departurePlanned, - 'departure_planned' => isset($stopover->plannedDeparture) ? $departurePlanned : $arrivalPlanned, - ]; - - if (isset($stopover->arrivalDelay) && isset($stopover->arrival)) { - $arrivalReal = Carbon::parse($stopover->arrival)->tz(config('app.timezone')); - $payloadArrival[] = array_merge($basePayload, ['arrival_real' => $arrivalReal]); - } - - if (isset($stopover->departureDelay) && isset($stopover->departure)) { - $departureReal = Carbon::parse($stopover->departure)->tz(config('app.timezone')); - $payloadDeparture[] = array_merge($basePayload, ['departure_real' => $departureReal]); - } - - // In case of cancellation, arrivalDelay/departureDelay will be null while the cancelled attribute will be present and true - // If cancelled is false / missing while other RT data is present (see initial if expression), it will be upserted to false - // This behavior is required for potential withdrawn cancellations - $payloadCancelled[] = array_merge($basePayload, ['cancelled' => $stopover->cancelled ?? false]); - - $stopoversUpdated++; - } - - $key = ['trip_id', 'train_station_id', 'departure_planned', 'arrival_planned']; - - return (object) [ - "stopovers" => $stopoversUpdated, - "rows" => [ - "arrival" => Stopover::upsert($payloadArrival, $key, ['arrival_real']), - "departure" => Stopover::upsert($payloadDeparture, $key, ['departure_real']), - "cancelled" => Stopover::upsert($payloadCancelled, $key, ['cancelled']) - ] - ]; - } - - /** - * This function is used to refresh the departure of a trip, if the planned_departure is in the past and no - * real-time data is given. The HAFAS stationboard gives us this real-time data even for trips in the past, so give - * it a chance. - * - * This function should be called in an async job, if not needed instantly. - * - * @param Stopover $stopover - * - * @return void - * @throws HafasException - */ - public static function refreshStopover(Stopover $stopover): void { - $departure = self::getDepartures( - station: $stopover->station, - when: $stopover->departure_planned, - )->filter(function(stdClass $trip) use ($stopover) { - return $trip->tripId === $stopover->trip_id; - })->first(); - - if ($departure === null || $departure->when === null || $departure->plannedWhen === $departure->when) { - return; //do nothing, if the trip isn't found. - } - - $stopover->update([ - 'departure_real' => Carbon::parse($departure->when), - ]); - } } diff --git a/app/DataProviders/HafasStopoverService.php b/app/DataProviders/HafasStopoverService.php new file mode 100644 index 000000000..5deca73e7 --- /dev/null +++ b/app/DataProviders/HafasStopoverService.php @@ -0,0 +1,103 @@ + $dataProvider + * @param DataProviderBuilder|null $dataProviderFactory + */ + public function __construct(string $dataProvider, ?DataProviderBuilder $dataProviderFactory = null) { + $dataProviderFactory ??= new DataProviderBuilder(); + $this->dataProvider = $dataProviderFactory->build($dataProvider); + } + + public static function refreshStopovers(stdClass $rawHafas): stdClass { + $stopoversUpdated = 0; + $payloadArrival = []; + $payloadDeparture = []; + $payloadCancelled = []; + foreach ($rawHafas->stopovers ?? [] as $stopover) { + if (!isset($stopover->arrivalDelay) && !isset($stopover->departureDelay) && !isset($stopover->cancelled)) { + continue; // No realtime data present for this stopover, keep existing data + } + + $stop = Repositories\StationRepository::parseHafasStopObject($stopover->stop); + $arrivalPlanned = Carbon::parse($stopover->plannedArrival)->tz(config('app.timezone')); + $departurePlanned = Carbon::parse($stopover->plannedDeparture)->tz(config('app.timezone')); + + $basePayload = [ + 'trip_id' => $rawHafas->id, + 'train_station_id' => $stop->id, + 'arrival_planned' => isset($stopover->plannedArrival) ? $arrivalPlanned : $departurePlanned, + 'departure_planned' => isset($stopover->plannedDeparture) ? $departurePlanned : $arrivalPlanned, + ]; + + if (isset($stopover->arrivalDelay) && isset($stopover->arrival)) { + $arrivalReal = Carbon::parse($stopover->arrival)->tz(config('app.timezone')); + $payloadArrival[] = array_merge($basePayload, ['arrival_real' => $arrivalReal]); + } + + if (isset($stopover->departureDelay) && isset($stopover->departure)) { + $departureReal = Carbon::parse($stopover->departure)->tz(config('app.timezone')); + $payloadDeparture[] = array_merge($basePayload, ['departure_real' => $departureReal]); + } + + // In case of cancellation, arrivalDelay/departureDelay will be null while the cancelled attribute will be present and true + // If cancelled is false / missing while other RT data is present (see initial if expression), it will be upserted to false + // This behavior is required for potential withdrawn cancellations + $payloadCancelled[] = array_merge($basePayload, ['cancelled' => $stopover->cancelled ?? false]); + + $stopoversUpdated++; + } + + $key = ['trip_id', 'train_station_id', 'departure_planned', 'arrival_planned']; + + return (object) [ + "stopovers" => $stopoversUpdated, + "rows" => [ + "arrival" => Stopover::upsert($payloadArrival, $key, ['arrival_real']), + "departure" => Stopover::upsert($payloadDeparture, $key, ['departure_real']), + "cancelled" => Stopover::upsert($payloadCancelled, $key, ['cancelled']) + ] + ]; + } + + /** + * This function is used to refresh the departure of a trip, if the planned_departure is in the past and no + * real-time data is given. The HAFAS stationboard gives us this real-time data even for trips in the past, so give + * it a chance. + * + * This function should be called in an async job, if not needed instantly. + * + * @param Stopover $stopover + * + * @return void + * @throws HafasException + */ + public function refreshStopover(Stopover $stopover): void { + $departure = $this->dataProvider->getDepartures( + station: $stopover->station, + when: $stopover->departure_planned, + )->filter(function(stdClass $trip) use ($stopover) { + return $trip->tripId === $stopover->trip_id; + })->first(); + + if ($departure === null || $departure->when === null || $departure->plannedWhen === $departure->when) { + return; //do nothing, if the trip isn't found. + } + + $stopover->update([ + 'departure_real' => Carbon::parse($departure->when), + ]); + } +} diff --git a/app/DataProviders/Repositories/StationRepository.php b/app/DataProviders/Repositories/StationRepository.php new file mode 100644 index 000000000..55bcd28fa --- /dev/null +++ b/app/DataProviders/Repositories/StationRepository.php @@ -0,0 +1,62 @@ + $hafasStop->name, + 'latitude' => $hafasStop->location?->latitude, + 'longitude' => $hafasStop->location?->longitude, + ]; + + if (isset($hafasStop->ril100)) { + $data['rilIdentifier'] = $hafasStop->ril100; + } + + return Station::updateOrCreate( + ['ibnr' => $hafasStop->id], + $data + ); + } + + public static function parseHafasStops(array $hafasResponse): Collection { + $payload = []; + foreach ($hafasResponse as $hafasStation) { + $payload[] = [ + 'ibnr' => $hafasStation->id, + 'name' => $hafasStation->name, + 'latitude' => $hafasStation?->location?->latitude, + 'longitude' => $hafasStation?->location?->longitude, + ]; + } + return self::upsertStations($payload); + } + + public static function upsertStations(array $payload) { + $ibnrs = array_column($payload, 'ibnr'); + if (empty($ibnrs)) { + return new Collection(); + } + Station::upsert($payload, ['ibnr'], ['name', 'latitude', 'longitude']); + return Station::whereIn('ibnr', $ibnrs)->get() + ->sortBy(function(Station $station) use ($ibnrs) { + return array_search($station->ibnr, $ibnrs); + }) + ->values(); + } +} diff --git a/app/Http/Controllers/API/v1/Controller.php b/app/Http/Controllers/API/v1/Controller.php index 484fe166a..8c0ec19fe 100644 --- a/app/Http/Controllers/API/v1/Controller.php +++ b/app/Http/Controllers/API/v1/Controller.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers\API\v1; +use App\DataProviders\DataProviderBuilder; +use App\DataProviders\DataProviderInterface; use App\Models\OAuthClient; use App\Models\User; use Illuminate\Contracts\Auth\Authenticatable; @@ -96,6 +98,13 @@ */ class Controller extends \App\Http\Controllers\Controller { + protected DataProviderInterface $dataProvider; + + public function __construct() { + // todo: set data provider based on user settings + $this->dataProvider = (new DataProviderBuilder())->build(); + } + public function sendResponse( mixed $data = null, int $code = 200, diff --git a/app/Http/Controllers/API/v1/EventController.php b/app/Http/Controllers/API/v1/EventController.php index 83e083851..50404045b 100644 --- a/app/Http/Controllers/API/v1/EventController.php +++ b/app/Http/Controllers/API/v1/EventController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\API\v1; use App\Http\Controllers\Backend\EventController as EventBackend; -use App\Http\Controllers\HafasController; use App\Http\Controllers\StatusController; use App\Http\Resources\EventDetailsResource; use App\Http\Resources\EventResource; @@ -250,7 +249,7 @@ public function suggest(Request $request): JsonResponse { ]); if (isset($validated['nearestStation'])) { - $stations = HafasController::getStations($validated['nearestStation'], 1); + $stations = $this->dataProvider->getStations($validated['nearestStation'], 1); if (count($stations) === 0) { return $this->sendError(error: __('events.request.station_not_found'), code: 400); } diff --git a/app/Http/Controllers/API/v1/TransportController.php b/app/Http/Controllers/API/v1/TransportController.php index fff3bcb5d..a6b8a9f6b 100644 --- a/app/Http/Controllers/API/v1/TransportController.php +++ b/app/Http/Controllers/API/v1/TransportController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\API\v1; +use App\DataProviders\Hafas; use App\Dto\Transport\Station as StationDto; use App\Enum\Business; use App\Enum\StatusVisibility; @@ -12,7 +13,6 @@ use App\Exceptions\StationNotOnTripException; use App\Http\Controllers\Backend\Transport\StationController; use App\Http\Controllers\Backend\Transport\TrainCheckinController; -use App\Http\Controllers\HafasController; use App\Http\Controllers\TransportController as TransportBackend; use App\Http\Resources\CheckinSuccessResource; use App\Http\Resources\StationResource; @@ -153,7 +153,7 @@ public function getDepartures(Request $request, int $stationId): JsonResponse { $station = Station::findOrFail($stationId); try { - $departures = HafasController::getDepartures( + $departures = $this->dataProvider->getDepartures( station: $station, when: $timestamp, type: TravelType::tryFrom($validated['travelType'] ?? null), @@ -311,7 +311,7 @@ public function getNextStationByCoordinates(Request $request): JsonResponse { ]); try { - $nearestStation = HafasController::getNearbyStations( + $nearestStation = $this->dataProvider->getNearbyStations( latitude: $validated['latitude'], longitude: $validated['longitude'], results: 1 @@ -513,7 +513,7 @@ public function setHome(int $stationId): JsonResponse { */ public function getTrainStationAutocomplete(string $query): JsonResponse { try { - $trainAutocompleteResponse = TransportBackend::getTrainStationAutocomplete($query); + $trainAutocompleteResponse = (new TransportBackend(Hafas::class))->getTrainStationAutocomplete($query); return $this->sendResponse($trainAutocompleteResponse); } catch (HafasException) { return $this->sendError("There has been an error with our data provider", 503); diff --git a/app/Http/Controllers/Backend/Transport/StationController.php b/app/Http/Controllers/Backend/Transport/StationController.php index ca61846c4..a7fd77e20 100644 --- a/app/Http/Controllers/Backend/Transport/StationController.php +++ b/app/Http/Controllers/Backend/Transport/StationController.php @@ -2,16 +2,12 @@ namespace App\Http\Controllers\Backend\Transport; -use App\Exceptions\HafasException; use App\Http\Controllers\Controller; -use App\Http\Controllers\HafasController; use App\Http\Resources\StationResource; use App\Models\Checkin; -use App\Models\Station; use App\Models\Stopover; use App\Models\User; use App\Repositories\StationRepository; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -24,36 +20,6 @@ public function __construct(?StationRepository $stationRepository = null) { $this->stationRepository = $stationRepository ?? new StationRepository(); } - /** - * @throws HafasException - * @throws ModelNotFoundException - */ - public static function lookupStation(string|int $query): Station { - //Lookup by station ibnr - if (is_numeric($query)) { - $station = Station::where('ibnr', $query)->first(); - if ($station !== null) { - return $station; - } - } - - //Lookup by ril identifier - if (!is_numeric($query) && strlen($query) <= 5 && ctype_upper($query)) { - $station = HafasController::getStationByRilIdentifier($query); - if ($station !== null) { - return $station; - } - } - - //Lookup HAFAS - $station = HafasController::getStations(query: $query, results: 1)->first(); - if ($station !== null) { - return $station; - } - - throw new ModelNotFoundException; - } - /** * Get the latest Stations the user is arrived. * diff --git a/app/Http/Controllers/Frontend/Admin/CheckinController.php b/app/Http/Controllers/Frontend/Admin/CheckinController.php index 1df1e3eea..6ecdbc5f8 100644 --- a/app/Http/Controllers/Frontend/Admin/CheckinController.php +++ b/app/Http/Controllers/Frontend/Admin/CheckinController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Frontend\Admin; +use App\DataProviders\DataProviderBuilder; use App\Enum\Business; use App\Enum\StatusVisibility; use App\Enum\TravelType; @@ -10,26 +11,98 @@ use App\Exceptions\StationNotOnTripException; use App\Exceptions\TrainCheckinAlreadyExistException; use App\Http\Controllers\Backend\Transport\TrainCheckinController; -use App\Http\Controllers\HafasController; -use App\Http\Controllers\TransportController as TransportBackend; use App\Hydrators\CheckinRequestHydrator; -use App\Jobs\PostStatusOnMastodon; use App\Models\Event; use App\Models\Station; use App\Models\Status; -use App\Models\Stopover; use App\Models\User; use Carbon\Carbon; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\Rules\Enum; use Illuminate\View\View; +use JetBrains\PhpStorm\ArrayShape; use Throwable; class CheckinController { + /** + * @throws HafasException + * @throws ModelNotFoundException + * @deprecated adapt admin panel to api endpoints + */ + public static function lookupStation(string|int $query): Station { + $dataProvider = (new DataProviderBuilder)->build(); + + //Lookup by station ibnr + if (is_numeric($query)) { + $station = Station::where('ibnr', $query)->first(); + if ($station !== null) { + return $station; + } + } + + //Lookup by ril identifier + if (!is_numeric($query) && strlen($query) <= 5 && ctype_upper($query)) { + $station = $dataProvider->getStationByRilIdentifier($query); + if ($station !== null) { + return $station; + } + } + + //Lookup HAFAS + $station = $dataProvider->getStations(query: $query, results: 1)->first(); + if ($station !== null) { + return $station; + } + + throw new ModelNotFoundException; + } + + /** + * @param string|int $stationQuery + * @param Carbon|null $when + * @param TravelType|null $travelType + * @param bool $localtime + * + * @return array + * @throws HafasException + * @deprecated use DataProviderInterface->getDepartures(...) directly instead (-> less overhead) + */ + #[ArrayShape([ + 'station' => Station::class, + 'departures' => Collection::class, + 'times' => "array" + ])] + public static function getDeprecatedDepartures( + string|int $stationQuery, + Carbon $when = null, + TravelType $travelType = null, + bool $localtime = false + ): array { + $station = self::lookupStation($stationQuery); + + $when = $when ?? Carbon::now()->subMinutes(5); + $times = [ + 'now' => $when, + 'prev' => $when->clone()->subMinutes(15), + 'next' => $when->clone()->addMinutes(15) + ]; + + $departures = (new DataProviderBuilder)->build()->getDepartures( + station: $station, + when: $when, + type: $travelType, + localtime: $localtime + )->sortBy(function($departure) { + return $departure->when ?? $departure->plannedWhen; + }); + + return ['station' => $station, 'departures' => $departures->values(), 'times' => $times]; + } public function renderStationboard(Request $request): View|RedirectResponse { $validated = $request->validate([ @@ -56,7 +129,7 @@ public function renderStationboard(Request $request): View|RedirectResponse { if (isset($validated['station'])) { try { - $trainStationboardResponse = TransportBackend::getDepartures( + $trainStationboardResponse = self::getDeprecatedDepartures( stationQuery: $validated['station'], when: $when, travelType: TravelType::tryFrom($validated['filter'] ?? null), diff --git a/app/Http/Controllers/Frontend/Admin/EventController.php b/app/Http/Controllers/Frontend/Admin/EventController.php index 77f99619d..25efd543e 100644 --- a/app/Http/Controllers/Frontend/Admin/EventController.php +++ b/app/Http/Controllers/Frontend/Admin/EventController.php @@ -2,11 +2,13 @@ namespace App\Http\Controllers\Frontend\Admin; +use App\DataProviders\DataProviderBuilder; +use App\DataProviders\DataProviderInterface; +use App\DataProviders\Hafas; use App\Enum\EventRejectionReason; use App\Exceptions\HafasException; use App\Http\Controllers\Backend\Admin\EventController as AdminEventBackend; use App\Http\Controllers\Controller; -use App\Http\Controllers\HafasController; use App\Models\Event; use App\Models\EventSuggestion; use App\Notifications\EventSuggestionProcessed; @@ -20,6 +22,12 @@ class EventController extends Controller { + private DataProviderInterface $dataProvider; + + public function __construct(string $dataProvider = null) { + $dataProvider ??= Hafas::class; + $this->dataProvider = (new DataProviderBuilder())->build($dataProvider); + } private const VALIDATOR_RULES = [ 'name' => ['required', 'max:255'], @@ -147,7 +155,7 @@ public function acceptSuggestion(Request $request): RedirectResponse { } if (isset($validated['nearest_station_name'])) { - $station = HafasController::getStations($validated['nearest_station_name'], 1)->first(); + $station = $this->dataProvider->getStations($validated['nearest_station_name'], 1)->first(); if ($station === null) { return back()->with('alert-danger', 'Die Station konnte nicht gefunden werden.'); @@ -187,7 +195,7 @@ public function create(Request $request): RedirectResponse { $station = null; if (isset($validated['nearest_station_name'])) { - $station = HafasController::getStations($validated['nearest_station_name'], 1)->first(); + $station = $this->dataProvider->getStations($validated['nearest_station_name'], 1)->first(); if ($station === null) { return back()->with('alert-danger', 'Die Station konnte nicht gefunden werden.'); @@ -219,7 +227,7 @@ public function edit(int $id, Request $request): RedirectResponse { if (strlen($validated['nearest_station_name'] ?? '') === 0) { $validated['station_id'] = null; } elseif ($validated['nearest_station_name'] && $validated['nearest_station_name'] !== $event->station->name) { - $station = HafasController::getStations($validated['nearest_station_name'], 1)->first(); + $station = $this->dataProvider->getStations($validated['nearest_station_name'], 1)->first(); if ($station === null) { return back()->with('alert-danger', 'Die Station konnte nicht gefunden werden.'); diff --git a/app/Http/Controllers/FrontendTransportController.php b/app/Http/Controllers/FrontendTransportController.php index 136757016..0b6b06b6f 100644 --- a/app/Http/Controllers/FrontendTransportController.php +++ b/app/Http/Controllers/FrontendTransportController.php @@ -2,33 +2,10 @@ namespace App\Http\Controllers; -use App\Dto\CheckinSuccess; -use App\Enum\Business; -use App\Enum\StatusVisibility; -use App\Enum\TravelType; -use App\Exceptions\Checkin\AlreadyCheckedInException; -use App\Exceptions\CheckInCollisionException; +use App\DataProviders\Hafas; use App\Exceptions\HafasException; -use App\Exceptions\StationNotOnTripException; -use App\Exceptions\TrainCheckinAlreadyExistException; -use App\Http\Controllers\Backend\Helper\StatusHelper; -use App\Http\Controllers\Backend\Transport\HomeController; -use App\Http\Controllers\Backend\Transport\StationController; -use App\Http\Controllers\Backend\Transport\TrainCheckinController; use App\Http\Controllers\TransportController as TransportBackend; -use App\Models\Event; -use App\Models\Station; -use App\Models\Stopover; -use App\Models\Trip; -use Carbon\Carbon; -use Illuminate\Contracts\Support\Renderable; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse; -use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use Illuminate\Validation\Rules\Enum; -use Throwable; /** * @deprecated Content will be moved to the backend/frontend/API packages soon, please don't add new functions here! @@ -37,7 +14,9 @@ class FrontendTransportController extends Controller { public function TrainAutocomplete(string $station): JsonResponse { try { - $trainAutocompleteResponse = TransportBackend::getTrainStationAutocomplete($station); + //todo: adapt data provider to users preferences + $provider = new TransportBackend(Hafas::class); + $trainAutocompleteResponse = $provider->getTrainStationAutocomplete($station); return response()->json($trainAutocompleteResponse); } catch (HafasException $e) { abort(503, $e->getMessage()); diff --git a/app/Http/Controllers/TransportController.php b/app/Http/Controllers/TransportController.php index e8c7c8f48..40f94c326 100644 --- a/app/Http/Controllers/TransportController.php +++ b/app/Http/Controllers/TransportController.php @@ -2,9 +2,9 @@ namespace App\Http\Controllers; -use App\Enum\TravelType; +use App\DataProviders\DataProviderBuilder; +use App\DataProviders\DataProviderInterface; use App\Exceptions\HafasException; -use App\Http\Controllers\Backend\Transport\StationController; use App\Http\Resources\StationResource; use App\Models\Checkin; use App\Models\PolyLine; @@ -12,13 +12,21 @@ use App\Models\User; use Carbon\Carbon; use Illuminate\Support\Collection; -use JetBrains\PhpStorm\ArrayShape; /** * @deprecated Content will be moved to the backend/frontend/API packages soon, please don't add new functions here! */ class TransportController extends Controller { + private DataProviderInterface $dataProvider; + + /** + * @template T of DataProviderInterface + * @param class-string $dataProvider + */ + public function __construct(string $dataProvider) { + $this->dataProvider = (new DataProviderBuilder())->build($dataProvider); + } /** * @param string $query @@ -27,13 +35,13 @@ class TransportController extends Controller * @throws HafasException * @api v1 */ - public static function getTrainStationAutocomplete(string $query): Collection { + public function getTrainStationAutocomplete(string $query): Collection { if (!is_numeric($query) && strlen($query) <= 5 && ctype_upper($query)) { - $stations = HafasController::getStationsByFuzzyRilIdentifier(rilIdentifier: $query); + $stations = $this->dataProvider->getStationsByFuzzyRilIdentifier(rilIdentifier: $query); } if (!isset($stations) || $stations[0] === null) { - $stations = HafasController::getStations($query); + $stations = $this->dataProvider->getStations($query); } return $stations->map(function(Station $station) { @@ -41,71 +49,6 @@ public static function getTrainStationAutocomplete(string $query): Collection { }); } - /** - * @param string|int $stationQuery - * @param Carbon|null $when - * @param TravelType|null $travelType - * @param bool $localtime - * - * @return array - * @throws HafasException - * @deprecated use HafasController::getDepartures(...) directly instead (-> less overhead) - * - * @api v1 - */ - #[ArrayShape([ - 'station' => Station::class, - 'departures' => Collection::class, - 'times' => "array" - ])] - public static function getDepartures( - string|int $stationQuery, - Carbon $when = null, - TravelType $travelType = null, - bool $localtime = false - ): array { - $station = StationController::lookupStation($stationQuery); - - $when = $when ?? Carbon::now()->subMinutes(5); - $times = [ - 'now' => $when, - 'prev' => $when->clone()->subMinutes(15), - 'next' => $when->clone()->addMinutes(15) - ]; - - $departures = HafasController::getDepartures( - station: $station, - when: $when, - type: $travelType, - localtime: $localtime - )->sortBy(function($departure) { - return $departure->when ?? $departure->plannedWhen; - }); - - return ['station' => $station, 'departures' => $departures->values(), 'times' => $times]; - } - - // Train with cancelled stops show up in the stationboard sometimes with when == 0. - // However, they will have a scheduledWhen. This snippet will sort the departures - // by actualWhen or use scheduledWhen if actual is empty. - public static function sortByWhenOrScheduledWhen(array $departuresList): array { - uasort($departuresList, function($a, $b) { - $dateA = $a->when; - if ($dateA == null) { - $dateA = $a->scheduledWhen; - } - - $dateB = $b->when; - if ($dateB == null) { - $dateB = $b->scheduledWhen; - } - - return ($dateA < $dateB) ? -1 : 1; - }); - - return $departuresList; - } - /** * Check if there are colliding CheckIns * diff --git a/app/Jobs/RefreshStopover.php b/app/Jobs/RefreshStopover.php index 802b4ef82..9d43328bb 100644 --- a/app/Jobs/RefreshStopover.php +++ b/app/Jobs/RefreshStopover.php @@ -2,7 +2,9 @@ namespace App\Jobs; -use App\Http\Controllers\HafasController; +use App\DataProviders\Hafas; +use App\DataProviders\HafasStopoverService; +use App\Exceptions\HafasException; use App\Models\Stopover; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -21,7 +23,10 @@ public function __construct(Stopover $stopover) { $this->stopover = $stopover; } + /** + * @throws HafasException + */ public function handle(): void { - HafasController::refreshStopover($this->stopover); + (new HafasStopoverService(Hafas::class))->refreshStopover($this->stopover); } } diff --git a/app/Repositories/CheckinHydratorRepository.php b/app/Repositories/CheckinHydratorRepository.php index ed1e8cde7..a6f76e687 100644 --- a/app/Repositories/CheckinHydratorRepository.php +++ b/app/Repositories/CheckinHydratorRepository.php @@ -2,8 +2,8 @@ namespace App\Repositories; +use App\DataProviders\DataProviderBuilder; use App\Exceptions\HafasException; -use App\Http\Controllers\HafasController; use App\Models\Event; use App\Models\Station; use App\Models\Stopover; @@ -25,11 +25,14 @@ public function getOneStation(string $searchKey, string|int $id): ?Station { * @throws JsonException */ public function getHafasTrip(string $tripID, string $lineName): Trip { + // todo: create trip IDs with a prefix, to distinguish between different data providers + $dataProvider = (new DataProviderBuilder)->build(); + if (is_numeric($tripID)) { $trip = Trip::where('id', $tripID)->where('linename', $lineName)->first(); } $trip = $trip ?? Trip::where('trip_id', $tripID)->where('linename', $lineName)->first(); - return $trip ?? HafasController::fetchHafasTrip($tripID, $lineName); + return $trip ?? $dataProvider->fetchHafasTrip($tripID, $lineName); } public function findEvent(int $id): ?Event { diff --git a/database/migrations/2021_01_26_203951_create_train_stopovers_table.php b/database/migrations/2021_01_26_203951_create_train_stopovers_table.php index 4370c7027..16fb0b2dd 100644 --- a/database/migrations/2021_01_26_203951_create_train_stopovers_table.php +++ b/database/migrations/2021_01_26_203951_create_train_stopovers_table.php @@ -1,6 +1,6 @@ stop); + $hafasStop = StationRepository::parseHafasStopObject($stopover->stop); Stopover::updateOrCreate( [ diff --git a/tests/Feature/CheckinTest.php b/tests/Feature/CheckinTest.php index 75f251e72..d9d1df3fd 100644 --- a/tests/Feature/CheckinTest.php +++ b/tests/Feature/CheckinTest.php @@ -2,23 +2,14 @@ namespace Tests\Feature; -use App\Dto\CheckinSuccess; -use App\Dto\Internal\CheckInRequestDto; -use App\Enum\Business; -use App\Enum\PointReason; -use App\Enum\StatusVisibility; -use App\Enum\TravelType; use App\Exceptions\CheckInCollisionException; use App\Exceptions\HafasException; -use App\Http\Controllers\Backend\Helper\StatusHelper; use App\Http\Controllers\Backend\Transport\TrainCheckinController; -use App\Http\Controllers\TransportController; -use App\Hydrators\CheckinRequestHydrator; +use App\Http\Controllers\Frontend\Admin\CheckinController; use App\Models\Station; use App\Models\Trip; use App\Models\User; use Carbon\Carbon; -use Illuminate\Database\Eloquent\Collection; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; use Tests\FeatureTestCase; @@ -43,7 +34,7 @@ public function stationboardTest(): void { $requestDate = Carbon::parse(self::DEPARTURE_TIME); - $trainStationboard = TransportController::getDepartures( + $trainStationboard = CheckinController::getDeprecatedDepartures( stationQuery: self::FRANKFURT_HBF['name'], when: $requestDate ); diff --git a/tests/Feature/StationSearchTest.php b/tests/Feature/StationSearchTest.php index 795c2da3b..04404fa66 100644 --- a/tests/Feature/StationSearchTest.php +++ b/tests/Feature/StationSearchTest.php @@ -2,9 +2,10 @@ namespace Tests\Feature; +use App\DataProviders\DataProviderBuilder; +use App\DataProviders\DataProviderInterface; use App\Exceptions\HafasException; -use App\Http\Controllers\Backend\Transport\StationController; -use App\Http\Controllers\HafasController; +use App\Http\Controllers\Frontend\Admin\CheckinController; use App\Models\Station; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -13,13 +14,21 @@ class StationSearchTest extends FeatureTestCase { + private DataProviderInterface $dataProvider; + + public function setUp(): void { + parent::setUp(); + $this->dataProvider = (new DataProviderBuilder())->build(); + } + + use RefreshDatabase; public function testStringSearch(): void { $searchResults = [self::HANNOVER_HBF]; Http::fake(["*" => Http::response($searchResults)]); - $station = StationController::lookupStation(self::HANNOVER_HBF['name']); + $station = CheckinController::lookupStation(self::HANNOVER_HBF['name']); $this->assertEquals(self::HANNOVER_HBF['name'], $station->name); } @@ -27,14 +36,14 @@ public function testNameNotFound(): void { Http::fake(["*" => Http::response([], 200)]); $this->assertThrows(function() { - StationController::lookupStation("Bielefeld Hbf"); + CheckinController::lookupStation("Bielefeld Hbf"); }, ModelNotFoundException::class); } public function testDs100Search(): void { Http::fake(["*/stations/" . self::HANNOVER_HBF['ril100'] => Http::response(self::HANNOVER_HBF)]); - $station = StationController::lookupStation(self::HANNOVER_HBF['ril100']); + $station = CheckinController::lookupStation(self::HANNOVER_HBF['ril100']); $this->assertEquals(self::HANNOVER_HBF['name'], $station->name); } @@ -42,7 +51,7 @@ public function testDs100NotFound(): void { Http::fake(["*" => Http::response([], 200)]); $this->assertThrows(function() { - StationController::lookupStation("EBIL"); + CheckinController::lookupStation("EBIL"); }, ModelNotFoundException::class); } @@ -51,29 +60,32 @@ public function testIbnrLocalSearch(): void { $expected = Station::factory()->make(); $expected->save(); - $station = StationController::lookupStation(str($expected->ibnr)); + $station = CheckinController::lookupStation(str($expected->ibnr)); $this->assertEquals(Station::find($expected->id)->name, $station->name); } + /** + * @throws HafasException + */ public function testGetNearbyStations(): void { Http::fake(["*/stops/nearby*" => Http::response([array_merge( self::HANNOVER_HBF, ["distance" => 421] )])]); - $result = HafasController::getNearbyStations( + $result = $this->dataProvider->getNearbyStations( self::HANNOVER_HBF['location']['latitude'], self::HANNOVER_HBF['location']['longitude']); $this->assertEquals(self::HANNOVER_HBF['name'], $result[0]->name); - $this->assertEquals(421, $result[0]->distance,); + $this->assertEquals(421, $result[0]->distance); } public function testGetNearbyStationFails(): void { Http::fake(Http::response(status: 503)); $this->assertThrows(function() { - HafasController::getNearbyStations( + $this->dataProvider->getNearbyStations( self::HANNOVER_HBF['location']['latitude'], self::HANNOVER_HBF['location']['longitude']); }, HafasException::class); diff --git a/tests/Feature/Transport/BackendCheckinTest.php b/tests/Feature/Transport/BackendCheckinTest.php index 9099086e0..148ee26fc 100644 --- a/tests/Feature/Transport/BackendCheckinTest.php +++ b/tests/Feature/Transport/BackendCheckinTest.php @@ -2,13 +2,14 @@ namespace Tests\Feature\Transport; +use App\DataProviders\DataProviderBuilder; +use App\DataProviders\DataProviderInterface; use App\Enum\TravelType; use App\Exceptions\CheckInCollisionException; use App\Exceptions\HafasException; use App\Exceptions\StationNotOnTripException; use App\Http\Controllers\Backend\Transport\TrainCheckinController; -use App\Http\Controllers\HafasController; -use App\Http\Controllers\TransportController; +use App\Http\Controllers\Frontend\Admin\CheckinController; use App\Models\Stopover; use App\Models\User; use App\Repositories\CheckinHydratorRepository; @@ -17,9 +18,16 @@ use Illuminate\Support\Facades\Http; use Tests\FeatureTestCase; use Tests\Helpers\CheckinRequestTestHydrator; +use Tests\TestHelpers\HafasHelpers; class BackendCheckinTest extends FeatureTestCase { + private DataProviderInterface $dataProvider; + + public function setUp(): void { + parent::setUp(); + $this->dataProvider = (new DataProviderBuilder())->build(); + } use RefreshDatabase; @@ -32,8 +40,8 @@ public function testStationNotOnTripException() { ]); $user = User::factory()->create(); - $stationHannover = HafasController::getStation(8000152); - $departures = HafasController::getDepartures( + $stationHannover = HafasHelpers::getStationById(8000152); + $departures = $this->dataProvider->getDepartures( station: $stationHannover, when: Carbon::parse('2023-01-12 08:00'), type: TravelType::EXPRESS, @@ -49,7 +57,7 @@ public function testStationNotOnTripException() { $this->expectException(StationNotOnTripException::class); $dto = (new CheckinRequestTestHydrator($user))->hydrateFromStopovers($trip, $originStopover, null); - $dto->setDestination(HafasController::getStation(8000001)) + $dto->setDestination(HafasHelpers::getStationById(8000001)) ->setArrival($originStopover->departure_planned); TrainCheckinController::checkin($dto); } @@ -63,8 +71,8 @@ public function testSwitchedOriginAndDestinationShouldThrowException() { ]); $user = User::factory()->create(); - $station = HafasController::getStation(8000105); - $departures = HafasController::getDepartures( + $station = HafasHelpers::getStationById(8000105); + $departures = $this->dataProvider->getDepartures( station: $station, when: Carbon::parse('2023-01-12 08:00'), type: TravelType::EXPRESS, @@ -97,8 +105,8 @@ public function testDuplicateCheckinsShouldThrowException() { ]); $user = User::factory()->create(); - $station = HafasController::getStation(8000105); - $departures = HafasController::getDepartures( + $station = HafasHelpers::getStationById(8000105); + $departures = $this->dataProvider->getDepartures( station: $station, when: Carbon::parse('2023-01-12 08:00'), type: TravelType::EXPRESS, @@ -139,7 +147,7 @@ public function testCheckinAtBus603Potsdam(): void { // First: Get a train that's fine for our stuff $timestamp = Carbon::parse("2023-01-15 10:15"); try { - $trainStationboard = TransportController::getDepartures( + $trainStationboard = CheckinController::getDeprecatedDepartures( stationQuery: 'Schloss Cecilienhof, Potsdam', when: $timestamp, travelType: TravelType::BUS @@ -205,8 +213,8 @@ public function testCheckinAtBerlinRingbahnRollingOverSuedkreuz(): void { // First: Get a train that's fine for our stuff // The 10:00 train actually quits at Südkreuz, but the 10:05 does not. - $station = HafasController::getStation(8089110); - $departures = HafasController::getDepartures( + $station = HafasHelpers::getStationById(8089110); + $departures = $this->dataProvider->getDepartures( station: $station, when: Carbon::parse('2023-01-16 10:00'), ); @@ -258,8 +266,8 @@ public function testDistanceCalculationOnRingLinesForFirstOccurrence(): void { ]); $user = User::factory()->create(); - $stationPlantagenPotsdam = HafasController::getStation(736165); - $departures = HafasController::getDepartures( + $stationPlantagenPotsdam = HafasHelpers::getStationById(736165); + $departures = $this->dataProvider->getDepartures( station: $stationPlantagenPotsdam, when: Carbon::parse('2023-01-16 10:00'), type: TravelType::TRAM, @@ -312,8 +320,8 @@ public function testDistanceCalculationOnRingLinesForSecondOccurrence(): void { ]); $user = User::factory()->create(); - $stationPlantagenPotsdam = HafasController::getStation(736165); - $departures = HafasController::getDepartures( + $stationPlantagenPotsdam = HafasHelpers::getStationById(736165); + $departures = $this->dataProvider->getDepartures( station: $stationPlantagenPotsdam, when: Carbon::parse('2023-01-16 10:00'), ); @@ -365,8 +373,8 @@ public function testBusAirAtFrankfurtAirport(): void { ]); $user = User::factory()->create(); - $station = HafasController::getStation(102932); // Flughafen Terminal 1, Frankfurt a.M. - $departures = HafasController::getDepartures( + $station = HafasHelpers::getStationById(102932); // Flughafen Terminal 1, Frankfurt a.M. + $departures = $this->dataProvider->getDepartures( station: $station, when: Carbon::parse('2023-01-16 10:00'), type: TravelType::BUS, @@ -406,8 +414,8 @@ public function testChangeTripDestination(): void { ]); $user = User::factory()->create(); - $station = HafasController::getStation(self::FRANKFURT_HBF['id']); - $departures = HafasController::getDepartures( + $station = HafasHelpers::getStationById(self::FRANKFURT_HBF['id']); + $departures = $this->dataProvider->getDepartures( station: $station, when: Carbon::parse('2023-01-16 08:00'), type: TravelType::EXPRESS, diff --git a/tests/Feature/Transport/TransportStatsTest.php b/tests/Feature/Transport/TransportStatsTest.php index 39c112968..1d3429e48 100644 --- a/tests/Feature/Transport/TransportStatsTest.php +++ b/tests/Feature/Transport/TransportStatsTest.php @@ -2,23 +2,12 @@ namespace Feature\Transport; -use App\Enum\TravelType; -use App\Exceptions\CheckInCollisionException; -use App\Exceptions\HafasException; -use App\Exceptions\StationNotOnTripException; -use App\Http\Controllers\API\v1\LikesController; use App\Http\Controllers\Backend\Stats\TransportStatsController; -use App\Http\Controllers\Backend\Transport\TrainCheckinController; -use App\Http\Controllers\HafasController; use App\Http\Controllers\StatusController as StatusBackend; -use App\Http\Controllers\TransportController; use App\Models\Checkin; -use App\Models\Stopover; use App\Models\User; use Carbon\Carbon; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Http; use Tests\FeatureTestCase; class TransportStatsTest extends FeatureTestCase diff --git a/tests/Feature/Webhooks/WebhookStatusTest.php b/tests/Feature/Webhooks/WebhookStatusTest.php index e48effe6e..cf6659df2 100644 --- a/tests/Feature/Webhooks/WebhookStatusTest.php +++ b/tests/Feature/Webhooks/WebhookStatusTest.php @@ -7,7 +7,6 @@ use App\Enum\StatusVisibility; use App\Enum\WebhookEvent; use App\Http\Controllers\Backend\Transport\TrainCheckinController; -use App\Http\Controllers\HafasController; use App\Http\Controllers\StatusController; use App\Http\Resources\StatusResource; use App\Jobs\MonitoredCallWebhookJob; @@ -17,6 +16,7 @@ use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Http; use Tests\FeatureTestCase; +use Tests\TestHelpers\HafasHelpers; use function PHPUnit\Framework\assertEquals; class WebhookStatusTest extends FeatureTestCase @@ -34,7 +34,7 @@ public function testWebhookSendingOnStatusCreation(): void { Bus::assertDispatched(function(MonitoredCallWebhookJob $job) use ($status) { assertEquals([ 'event' => WebhookEvent::CHECKIN_CREATE->value, - 'status' => new StatusResource($status), + 'status' => new StatusResource($status), ], $job->payload); return true; }); @@ -61,7 +61,7 @@ public function testWebhookSendingOnStatusBodyChange() { $job->payload['event'] ); assertEquals($status->id, $job->payload['status']['id']); - assertEquals('New Example Body', $job->payload['status']['body'],); + assertEquals('New Example Body', $job->payload['status']['body']); return true; }); } @@ -93,14 +93,14 @@ public function testWebhookSendingOnDestinationChange() { $user = User::factory()->create(); $client = $this->createWebhookClient($user); $this->createWebhook($user, $client, [WebhookEvent::CHECKIN_UPDATE]); - $status = $this->createStatus($user); - $checkin = $status->checkin()->first(); - $trip = TrainCheckinController::getHafasTrip( + $status = $this->createStatus($user); + $checkin = $status->checkin()->first(); + $trip = TrainCheckinController::getHafasTrip( tripId: self::TRIP_ID, lineName: self::ICE802['line']['name'], startId: self::FRANKFURT_HBF['id'] ); - $aachen = $trip->stopovers->where('station.ibnr', self::AACHEN_HBF['id'])->first(); + $aachen = $trip->stopovers->where('station.ibnr', self::AACHEN_HBF['id'])->first(); TrainCheckinController::changeDestination($checkin, $aachen); Bus::assertDispatched(function(MonitoredCallWebhookJob $job) use ($status) { @@ -199,8 +199,8 @@ protected function createStatus(User $user) { startId: self::FRANKFURT_HBF['id'] ); - $origin = HafasController::getStation(self::FRANKFURT_HBF['id']); - $destination = HafasController::getStation(self::HANNOVER_HBF['id']); + $origin = HafasHelpers::getStationById(self::FRANKFURT_HBF['id']); + $destination = HafasHelpers::getStationById(self::HANNOVER_HBF['id']); $dto = new CheckInRequestDto(); $dto->setUser($user) diff --git a/tests/TestHelpers/HafasHelpers.php b/tests/TestHelpers/HafasHelpers.php new file mode 100644 index 000000000..bf22998da --- /dev/null +++ b/tests/TestHelpers/HafasHelpers.php @@ -0,0 +1,46 @@ +first(); + return $dbStation ?? self::fetchStation($ibnr); + } + + /** + * Fetch from HAFAS + * + * @param int $ibnr + * + * @return Station + * @throws HafasException + */ + public static function fetchStation(int $ibnr): Station { + $httpClient = Http::baseUrl(config('trwl.db_rest')) + ->timeout(config('trwl.db_rest_timeout')); + $response = $httpClient->get("/stops/$ibnr"); + + if (!$response->ok()) { + throw new HafasException($response->reason()); + } + + $data = json_decode($response->body()); + return StationRepository::parseHafasStopObject($data); + } +}