From 47caa52a1638617c97ee7fff3b3bcdf113d8395d Mon Sep 17 00:00:00 2001 From: Cristen Jones Date: Mon, 9 Feb 2026 11:38:59 -0500 Subject: [PATCH 1/4] refactor(UpcomingDeparture): get entire vehicle --- .../schedule_finder/upcoming_departures.ex | 12 +++++++++++- lib/predicted_schedule.ex | 17 ----------------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/dotcom/schedule_finder/upcoming_departures.ex b/lib/dotcom/schedule_finder/upcoming_departures.ex index 2784d56b50..ebaee3979a 100644 --- a/lib/dotcom/schedule_finder/upcoming_departures.ex +++ b/lib/dotcom/schedule_finder/upcoming_departures.ex @@ -247,8 +247,10 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do stop_id: stop_id }) do trip = predicted_schedule |> PredictedSchedule.trip() + stop = predicted_schedule |> PredictedSchedule.stop() stop_sequence = PredictedSchedule.stop_sequence(predicted_schedule) - vehicle_at_stop_status = PredictedSchedule.vehicle_at_stop_status(predicted_schedule) + vehicle = PredictedSchedule.vehicle(predicted_schedule) + vehicle_at_stop_status = vehicle_at_stop_status(vehicle, stop, stop_sequence) trip_details = trip_details(%{ @@ -282,6 +284,14 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do } end + # Retrieves status if a vehicle is associated with the given stop/sequence + defp vehicle_at_stop_status(nil, _, _), do: nil + + defp vehicle_at_stop_status(vehicle, stop, stop_sequence) do + at_stop? = vehicle.stop_id in [stop.id | stop.child_ids] + if at_stop? && vehicle.stop_sequence == stop_sequence, do: vehicle.status + end + defp trip_details(%{ predicted_schedules_by_trip_id: predicted_schedules_by_trip_id, trip_id: trip_id, diff --git a/lib/predicted_schedule.ex b/lib/predicted_schedule.ex index 1f22959f89..47948a8344 100644 --- a/lib/predicted_schedule.ex +++ b/lib/predicted_schedule.ex @@ -290,23 +290,6 @@ defmodule PredictedSchedule do def vehicle(_), do: nil - @doc """ - Retrieves status from predicted schedule vehicle if a vehicle is at or approaching the predicted schedule trip/stop/stop_sequence - """ - @spec vehicle_at_stop_status(PredictedSchedule.t()) :: Vehicles.Vehicle.status() | nil - def vehicle_at_stop_status(ps) do - stop = stop(ps) - stop_sequence = stop_sequence(ps) - vehicle = vehicle(ps) - - if not is_nil(vehicle) and vehicle.stop_sequence == stop_sequence and - vehicle.stop_id in [stop.id | stop.child_ids] do - vehicle.status - end - end - - def vehicle_status(_predicted_schedule), do: nil - @doc """ Determines if the given predicted schedule occurs after the given time """ From 9f0ac62224635ab118c647604b16b57004ae5141 Mon Sep 17 00:00:00 2001 From: Cristen Jones Date: Mon, 9 Feb 2026 11:43:48 -0500 Subject: [PATCH 2/4] refactor(TripDetails): use UpcomingDepartures vehicle if same trip --- lib/dotcom/schedule_finder/trip_details.ex | 13 +--- .../schedule_finder/upcoming_departures.ex | 8 ++- .../schedule_finder/trip_details_test.exs | 60 ++++++++++--------- .../upcoming_departures_test.exs | 47 +++++++++++---- test/support/factories/vehicles/vehicle.ex | 1 + 5 files changed, 74 insertions(+), 55 deletions(-) diff --git a/lib/dotcom/schedule_finder/trip_details.ex b/lib/dotcom/schedule_finder/trip_details.ex index ddcfdcfab2..02c0fe8b7e 100644 --- a/lib/dotcom/schedule_finder/trip_details.ex +++ b/lib/dotcom/schedule_finder/trip_details.ex @@ -70,20 +70,13 @@ defmodule Dotcom.ScheduleFinder.TripDetails do alias Vehicles.Vehicle @stops_repo Application.compile_env!(:dotcom, :repo_modules)[:stops] - @vehicles_repo Application.compile_env!(:dotcom, :repo_modules)[:vehicles] @spec trip_details(%{ predicted_schedules: [PredictedSchedule.t()], - trip_id: Schedules.Trip.id_t() + trip_vehicle: Vehicles.Vehicle.t() | nil }) :: __MODULE__.t() - def trip_details(%{predicted_schedules: predicted_schedules, trip_id: trip_id}) do - vehicle = - trip_id - |> @vehicles_repo.trip() - - vehicle_info = - vehicle - |> vehicle_info() + def trip_details(%{predicted_schedules: predicted_schedules, trip_vehicle: vehicle}) do + vehicle_info = vehicle_info(vehicle) stops = predicted_schedules diff --git a/lib/dotcom/schedule_finder/upcoming_departures.ex b/lib/dotcom/schedule_finder/upcoming_departures.ex index ebaee3979a..f1075a2527 100644 --- a/lib/dotcom/schedule_finder/upcoming_departures.ex +++ b/lib/dotcom/schedule_finder/upcoming_departures.ex @@ -257,7 +257,8 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do predicted_schedules_by_trip_id: predicted_schedules_by_trip_id, trip_id: trip.id, stop_id: stop_id, - stop_sequence: stop_sequence + stop_sequence: stop_sequence, + vehicle: vehicle }) %UpcomingDeparture{ @@ -296,12 +297,13 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do predicted_schedules_by_trip_id: predicted_schedules_by_trip_id, trip_id: trip_id, stop_id: stop_id, - stop_sequence: stop_sequence + stop_sequence: stop_sequence, + vehicle: vehicle }) do %TripDetails{stops: stops, vehicle_info: vehicle_info} = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules_by_trip_id |> Map.get(trip_id, []), - trip_id: trip_id + trip_vehicle: if(vehicle && vehicle.trip_id == trip_id, do: vehicle) }) {stops_before, stop, stops_after} = diff --git a/test/dotcom/schedule_finder/trip_details_test.exs b/test/dotcom/schedule_finder/trip_details_test.exs index 608b0aceab..b3747e3fbc 100644 --- a/test/dotcom/schedule_finder/trip_details_test.exs +++ b/test/dotcom/schedule_finder/trip_details_test.exs @@ -16,8 +16,6 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do Factories.Stops.Stop.build(:stop, id: id, parent_id: nil) end) - stub(Vehicles.Repo.Mock, :trip, fn _ -> Factories.Vehicles.Vehicle.build(:vehicle) end) - :ok end @@ -58,7 +56,7 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do trip_details = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules, - trip_id: trip.id + trip_vehicle: nil }) assert trip_details.stops |> Enum.map(& &1.stop_id) == stop_ids @@ -92,7 +90,7 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do trip_details = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules, - trip_id: trip.id + trip_vehicle: nil }) assert [trip_stop] = trip_details.stops @@ -133,7 +131,7 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do trip_details = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules, - trip_id: trip.id + trip_vehicle: nil }) assert [trip_stop] = trip_details.stops @@ -174,7 +172,7 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do trip_details = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules, - trip_id: trip.id + trip_vehicle: nil }) assert [trip_stop] = trip_details.stops @@ -241,7 +239,7 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do trip_details = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules, - trip_id: trip.id + trip_vehicle: nil }) assert [trip_stop_1, trip_stop_2, trip_stop_3] = trip_details.stops @@ -269,15 +267,13 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do } end) - trip_id = FactoryHelpers.build(:id) crowding = Faker.Util.pick([:not_crowded, :crowded, :some_crowding]) vehicle = Factories.Vehicles.Vehicle.build(:vehicle, stop_id: stop_id, crowding: crowding) - stub(Vehicles.Repo.Mock, :trip, fn ^trip_id -> vehicle end) trip_details = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules, - trip_id: trip_id + trip_vehicle: vehicle }) vehicle_info = trip_details.vehicle_info @@ -297,15 +293,12 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do } end) - trip_id = FactoryHelpers.build(:id) - vehicle = Factories.Vehicles.Vehicle.build(:vehicle, stop_id: nil) - stub(Vehicles.Repo.Mock, :trip, fn ^trip_id -> vehicle end) trip_details = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules, - trip_id: trip_id + trip_vehicle: vehicle }) vehicle_info = trip_details.vehicle_info @@ -332,15 +325,12 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do } end) - trip_id = FactoryHelpers.build(:id) - vehicle = Factories.Vehicles.Vehicle.build(:vehicle, stop_id: child_stop_id) - stub(Vehicles.Repo.Mock, :trip, fn ^trip_id -> vehicle end) trip_details = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules, - trip_id: trip_id + trip_vehicle: vehicle }) vehicle_info = trip_details.vehicle_info @@ -368,23 +358,35 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do predicted_schedules = stops - |> Enum.map( - &%PredictedSchedule{ - prediction: Factories.Predictions.Prediction.build(:prediction, trip: trip, stop: &1), - schedule: Factories.Schedules.Schedule.build(:schedule, trip: trip, stop: &1) + |> Enum.with_index() + |> Enum.map(fn {stop, index} -> + %PredictedSchedule{ + prediction: + Factories.Predictions.Prediction.build(:prediction, + trip: trip, + stop: stop, + stop_sequence: index + ), + schedule: + Factories.Schedules.Schedule.build(:schedule, + trip: trip, + stop: stop, + stop_sequence: index + ) } - ) - - trip_id = trip.id + end) - stub(Vehicles.Repo.Mock, :trip, fn ^trip_id -> - Factories.Vehicles.Vehicle.build(:vehicle, status: :stopped, stop_id: current_stop_id) - end) + vehicle = + Factories.Vehicles.Vehicle.build(:vehicle, + status: :stopped, + stop_id: current_stop_id, + stop_sequence: 0 + ) trip_details = TripDetails.trip_details(%{ predicted_schedules: predicted_schedules, - trip_id: trip_id + trip_vehicle: vehicle }) assert trip_details.stops |> Enum.map(& &1.stop_id) == future_stop_ids diff --git a/test/dotcom/schedule_finder/upcoming_departures_test.exs b/test/dotcom/schedule_finder/upcoming_departures_test.exs index bc097192e0..53f06e81df 100644 --- a/test/dotcom/schedule_finder/upcoming_departures_test.exs +++ b/test/dotcom/schedule_finder/upcoming_departures_test.exs @@ -16,7 +16,6 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do Factories.Stops.Stop.build(:stop, id: id, parent_id: nil) end) - stub(Vehicles.Repo.Mock, :trip, fn _ -> Factories.Vehicles.Vehicle.build(:vehicle) end) stub(Vehicles.Repo.Mock, :get, fn _ -> Factories.Vehicles.Vehicle.build(:vehicle) end) stub(Schedules.Repo.Mock, :by_route_ids, fn _, _ -> [] end) stub(Predictions.Repo.Mock, :all, fn _ -> [] end) @@ -2230,22 +2229,33 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do route_id = route.id stop = Factories.Stops.Stop.build(:stop) - + stop_sequence = Faker.random_between(0, 1000) trip_id = FactoryHelpers.build(:id) trip = Factories.Schedules.Trip.build(:trip, id: trip_id) direction_id = Faker.Util.pick([0, 1]) + vehicle = + Factories.Vehicles.Vehicle.build(:vehicle, + stop_sequence: stop_sequence, + stop_id: stop.id, + trip_id: trip_id + ) + + expect(Vehicles.Repo.Mock, :get, fn _ -> vehicle end) + expect(Predictions.Repo.Mock, :all, fn [ route: ^route_id, direction_id: ^direction_id, include_terminals: true ] -> - Factories.Predictions.Prediction.build_list(3, :prediction, stop: stop, trip: trip) + Factories.Predictions.Prediction.build_list(3, :prediction, + stop: stop, + trip: trip, + vehicle_id: vehicle.id, + stop_sequence: stop_sequence + ) end) - vehicle = Factories.Vehicles.Vehicle.build(:vehicle) - expect(Vehicles.Repo.Mock, :trip, fn ^trip_id -> vehicle end) - # Exercise departures = UpcomingDepartures.upcoming_departures(%{ @@ -2257,9 +2267,11 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do # Verify assert [departure] = departures - vehicle_info = departure.trip_details.vehicle_info - assert vehicle_info.status == vehicle.status + assert %Dotcom.ScheduleFinder.TripDetails.VehicleInfo{} = + departure.trip_details.vehicle_info + + assert departure.trip_details.vehicle_info.status == vehicle.status end test "drops the current stop if the vehicle is currently stopped there" do @@ -2289,6 +2301,16 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do [arrival_time, arrival_time_after] = arrival_time_offsets |> Enum.map(&(now |> DateTime.shift(minute: &1))) + vehicle = + Factories.Vehicles.Vehicle.build(:vehicle, + status: :stopped, + stop_sequence: stop_sequence, + stop_id: stop.id, + trip_id: trip_id + ) + + expect(Vehicles.Repo.Mock, :get, fn _ -> vehicle end) + expect(Predictions.Repo.Mock, :all, fn [ route: ^route_id, direction_id: ^direction_id, @@ -2299,20 +2321,19 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do arrival_time: arrival_time, stop: stop, stop_sequence: stop_sequence, - trip: trip + trip: trip, + vehicle_id: vehicle.id ), Factories.Predictions.Prediction.build(:prediction, arrival_time: arrival_time_after, stop: stop_after, stop_sequence: stop_sequence_after, - trip: trip + trip: trip, + vehicle_id: vehicle.id ) ] end) - vehicle = Factories.Vehicles.Vehicle.build(:vehicle, status: :stopped, stop_id: stop.id) - expect(Vehicles.Repo.Mock, :trip, fn ^trip_id -> vehicle end) - # Exercise departures = UpcomingDepartures.upcoming_departures(%{ diff --git a/test/support/factories/vehicles/vehicle.ex b/test/support/factories/vehicles/vehicle.ex index 43228c688b..5afd7540de 100644 --- a/test/support/factories/vehicles/vehicle.ex +++ b/test/support/factories/vehicles/vehicle.ex @@ -10,6 +10,7 @@ defmodule Test.Support.Factories.Vehicles.Vehicle do def vehicle_factory do %Vehicle{ + id: FactoryHelpers.build(:id), direction_id: Faker.Util.pick([0, 1]), status: Faker.Util.pick([:in_transit, :stopped, :incoming]), stop_id: FactoryHelpers.build(:id), From d235f8059971f5aa19e5e4ec8ec55769801b0f39 Mon Sep 17 00:00:00 2001 From: Cristen Jones Date: Mon, 9 Feb 2026 11:44:05 -0500 Subject: [PATCH 3/4] feat(TripDetails): remove earlier predictions --- lib/dotcom/schedule_finder/trip_details.ex | 52 +++++++++++++++++----- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/lib/dotcom/schedule_finder/trip_details.ex b/lib/dotcom/schedule_finder/trip_details.ex index 02c0fe8b7e..4a6ef9e728 100644 --- a/lib/dotcom/schedule_finder/trip_details.ex +++ b/lib/dotcom/schedule_finder/trip_details.ex @@ -50,7 +50,8 @@ defmodule Dotcom.ScheduleFinder.TripDetails do :crowding, :status, :stop_id, - :stop_name + :stop_name, + :stop_sequence ] @type trip_vehicle_status_t() :: @@ -61,7 +62,8 @@ defmodule Dotcom.ScheduleFinder.TripDetails do crowding: Vehicles.Vehicle.crowding(), status: trip_vehicle_status_t(), stop_id: Stops.Stop.id_t(), - stop_name: String.t() + stop_name: String.t(), + stop_sequence: non_neg_integer() } end @@ -94,7 +96,7 @@ defmodule Dotcom.ScheduleFinder.TripDetails do } end) |> Enum.sort_by(& &1.stop_sequence) - |> drop_prediction_for_current_station(vehicle_info) + |> drop_predictions_before_current_station(vehicle_info) %__MODULE__{ stops: stops, @@ -127,23 +129,53 @@ defmodule Dotcom.ScheduleFinder.TripDetails do defp vehicle_info(%Vehicle{stop_id: nil}), do: %VehicleInfo{status: :location_unavailable} - defp vehicle_info(%Vehicle{crowding: crowding, status: status, stop_id: stop_id}) do + defp vehicle_info(%Vehicle{ + crowding: crowding, + status: status, + stop_id: stop_id, + stop_sequence: stop_sequence + }) do stop = @stops_repo.get(stop_id) %VehicleInfo{ crowding: crowding, status: status, stop_id: stop.parent_id || stop.id, - stop_name: stop.name + stop_name: stop.name, + stop_sequence: stop_sequence } end - defp drop_prediction_for_current_station(stops, %{status: :stopped, stop_id: stop_id}) do - case stops do - [%{stop_id: ^stop_id} | remaining_stops] -> remaining_stops - _ -> stops + # Sometimes predictions might not keep up with vehicles + # If we have a vehicle status, use it to omit past stops + defp drop_predictions_before_current_station( + trip_stops, + %VehicleInfo{} = vehicle_info + ) do + current = current_stop_index(trip_stops, vehicle_info) + + if is_nil(current) do + # finishing another trip -- show all the stops + trip_stops + else + upcoming_stops = Enum.take(trip_stops, current - length(trip_stops)) + + # Omit current trip_stop if the vehicle is stopped + if vehicle_info.status == :stopped do + Enum.drop(upcoming_stops, 1) + else + upcoming_stops + end end end - defp drop_prediction_for_current_station(stops, _), do: stops + defp drop_predictions_before_current_station(trip_stops, _), do: trip_stops + + defp current_stop_index(trip_stops, vehicle_info) do + Enum.find_index(trip_stops, &vehicle_at_stop?(&1, vehicle_info)) + end + + defp vehicle_at_stop?(stop, vehicle) do + stop.stop_id == vehicle.stop_id && stop.stop_sequence == vehicle.stop_sequence + end end From aaf10b8c764b90d2a3777cb9c3d67c471e8f38a8 Mon Sep 17 00:00:00 2001 From: Cristen Jones Date: Mon, 9 Feb 2026 14:01:19 -0500 Subject: [PATCH 4/4] !fixup add test --- .../schedule_finder/trip_details_test.exs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/dotcom/schedule_finder/trip_details_test.exs b/test/dotcom/schedule_finder/trip_details_test.exs index b3747e3fbc..08559a62c6 100644 --- a/test/dotcom/schedule_finder/trip_details_test.exs +++ b/test/dotcom/schedule_finder/trip_details_test.exs @@ -392,4 +392,50 @@ defmodule Dotcom.ScheduleFinder.TripDetailsTest do assert trip_details.stops |> Enum.map(& &1.stop_id) == future_stop_ids assert trip_details.stops |> Enum.map(& &1.stop_name) == future_stop_names end + + test "drops earlier stops before the active vehicle" do + trip = Factories.Schedules.Trip.build(:trip) + + stops = Faker.Util.sample_uniq(20, fn -> Factories.Stops.Stop.build(:stop) end) + + predicted_schedules = + stops + |> Enum.with_index() + |> Enum.map(fn {stop, index} -> + %PredictedSchedule{ + prediction: + Factories.Predictions.Prediction.build(:prediction, + trip: trip, + stop: stop, + stop_sequence: index + ), + schedule: + Factories.Schedules.Schedule.build(:schedule, + trip: trip, + stop: stop, + stop_sequence: index + ) + } + end) + + current_index = Faker.random_between(5, 15) + {_earlier_stops, [current_stop | future_stops]} = stops |> Enum.split(current_index) + + vehicle = + Factories.Vehicles.Vehicle.build(:vehicle, + status: :stopped, + stop_id: current_stop.id, + stop_sequence: current_index, + trip_id: trip.id + ) + + trip_details = + TripDetails.trip_details(%{ + predicted_schedules: predicted_schedules, + trip_vehicle: vehicle + }) + + assert trip_details.stops |> Enum.map(& &1.stop_id) == Enum.map(future_stops, & &1.id) + assert trip_details.stops |> Enum.map(& &1.stop_name) == Enum.map(future_stops, & &1.name) + end end