diff --git a/lib/dotcom/schedule_finder.ex b/lib/dotcom/schedule_finder.ex index 07b8b97b40..f10057ac7b 100644 --- a/lib/dotcom/schedule_finder.ex +++ b/lib/dotcom/schedule_finder.ex @@ -3,29 +3,35 @@ defmodule Dotcom.ScheduleFinder do Schedule Finder data includes daily schedules, predictions, services """ - use Nebulex.Caching.Decorators - import Dotcom.Alerts alias Alerts.{Alert, InformedEntity, InformedEntitySet} alias Dotcom.ScheduleFinder.{DailyDeparture, FutureArrival} - alias JsonApi.Item + alias RoutePatterns.RoutePattern alias Routes.Route - alias Schedules.Trip + alias Schedules.{Schedule, Trip} alias Stops.Stop - @cache Application.compile_env!(:dotcom, :cache) @alerts_repo_module Application.compile_env!(:dotcom, :repo_modules)[:alerts] @date_time_module Application.compile_env!(:dotcom, :date_time_module) + @route_patterns_repo Application.compile_env!(:dotcom, :repo_modules)[:route_patterns] @routes_repo Application.compile_env!(:dotcom, :repo_modules)[:routes] - @timezone Application.compile_env!(:dotcom, :timezone) - @schedule_ttl :timer.hours(24) + @schedules_repo Application.compile_env!(:dotcom, :repo_modules)[:schedules] defmodule DailyDeparture do @moduledoc """ A scheduled departure for a trip on a route, described by a headsign and time. """ - defstruct [:headsign, :route_id, :schedule_id, :stop_sequence, :time, :trip_id, :trip_name] + defstruct [ + :headsign, + :route_id, + :schedule_id, + :stop_sequence, + :time, + :time_desc, + :trip_id, + :trip_name + ] @type t :: %__MODULE__{ headsign: Trip.headsign() | String.t() | nil, @@ -33,6 +39,7 @@ defmodule Dotcom.ScheduleFinder do schedule_id: String.t(), stop_sequence: non_neg_integer(), time: DateTime.t(), + time_desc: String.t() | nil, trip_id: Trip.id_t(), trip_name: String.t() | nil } @@ -93,63 +100,56 @@ defmodule Dotcom.ScheduleFinder do {:ok, [DailyDeparture.t()]} | {:error, term()} def daily_departures(route_id, direction_id, stop_id, date) do # Maybe add filter[stop_sequence] to help looped routes - params = [ - include: "trip", - "fields[trip]": "headsign,name", - "fields[schedule]": "departure_time,pickup_type,stop_headsign,stop_sequence", - "filter[route]": routes(route_id), - "filter[direction_id]": direction_id, - "filter[stop]": stop_id, - "filter[date]": date, - sort: "departure_time" - ] - - case get_schedules(params) do - {:ok, data} -> - {:ok, - data - |> Stream.reject(&no_pick_up?/1) - |> Enum.map(&to_departure/1)} + case @schedules_repo.by_route_ids(routes(route_id), + date: date, + direction_id: direction_id, + stop_ids: stop_id + ) do + schedules when is_list(schedules) -> + departures = + schedules + |> Enum.reject(&no_pick_up?/1) + |> Enum.map(&to_departure/1) + + {:ok, departures} - {:error, error} -> - {:error, inspect(error)} + error -> + error end end - defp routes("Green"), do: GreenLine.branch_ids() |> Enum.join(",") - defp routes(route_id), do: route_id + defp routes("Green"), do: GreenLine.branch_ids() + defp routes(other), do: List.wrap(other) - defp no_pick_up?(%Item{attributes: %{"pickup_type" => 1}}), do: true + defp no_pick_up?(%Schedule{pickup_type: 1}), do: true defp no_pick_up?(_), do: false - defp to_departure(%Item{ - id: schedule_id, - attributes: %{ - "departure_time" => departure_time, - "stop_headsign" => stop_headsign, - "stop_sequence" => stop_sequence - }, - relationships: %{ - "route" => [%Item{id: route_id}], - "trip" => [ - %Item{ - id: trip_id, - attributes: %{"headsign" => trip_headsign, "name" => trip_name} - } - ] - } - }) do + defp to_departure(%Schedule{schedule_id: schedule_id, route: route, trip: trip} = schedule) do %DailyDeparture{ - route_id: route_id, + route_id: route.id, schedule_id: schedule_id, - time: to_datetime(departure_time), - headsign: stop_headsign || trip_headsign, - trip_name: trip_name, - trip_id: trip_id, - stop_sequence: stop_sequence + time: schedule.departure_time, + time_desc: time_desc(trip), + headsign: schedule.stop_headsign || if(trip, do: Map.get(trip, :headsign)), + trip_name: if(trip, do: Map.get(trip, :name)), + trip_id: if(trip, do: Map.get(trip, :id)), + stop_sequence: schedule.stop_sequence } end + defp time_desc(%Trip{route_pattern_id: route_pattern_id}) + when not is_nil(route_pattern_id) do + case @route_patterns_repo.get(route_pattern_id) do + %RoutePattern{time_desc: time_desc} -> + time_desc + + _ -> + nil + end + end + + defp time_desc(_), do: nil + @doc """ Get scheduled arrivals for one trip on a date, starting at a given stop_sequence. """ @@ -157,20 +157,14 @@ defmodule Dotcom.ScheduleFinder do {:ok, [FutureArrival.t()]} | {:error, term()} def next_arrivals(trip_id, min_stop_sequence, date) do # Maybe add filter[stop_sequence] to help looped routes - params = [ - include: "stop", - "fields[schedule]": "arrival_time,departure_time,stop_sequence,stop_headsign", - "fields[stop]": "platform_name,name,vehicle_type", - "filter[trip]": trip_id, - "filter[date]": date - ] + case @schedules_repo.schedule_for_trip(trip_id, date: date) do + schedules when is_list(schedules) -> + arrivals = + schedules + |> Enum.filter(&makes_subsequent_stop?(&1, min_stop_sequence)) + |> Enum.map(&to_arrival/1) - case get_schedules(params) do - {:ok, data} -> - {:ok, - data - |> Stream.filter(&makes_subsequent_stop?(&1, min_stop_sequence)) - |> Enum.map(&to_arrival/1)} + {:ok, arrivals} error -> error @@ -179,30 +173,24 @@ defmodule Dotcom.ScheduleFinder do # Instead of every stop in the trip, only return schedules that make later stops on the trip, as defined by the given `stop_sequence` value defp makes_subsequent_stop?( - %Item{attributes: %{"stop_sequence" => stop_sequence}}, + %Schedule{stop_sequence: stop_sequence}, min_stop_sequence ) do - {int, _} = Integer.parse(min_stop_sequence) - stop_sequence >= int + stop_sequence >= min_stop_sequence end - defp to_arrival(%Item{ - attributes: %{"departure_time" => departure_time, "arrival_time" => arrival_time}, - relationships: %{ - "stop" => [ - %Item{ - attributes: %{ - "platform_name" => platform_name, - "name" => stop_name, - "vehicle_type" => vehicle_type - } - } - ] + defp to_arrival(%Schedule{ + departure_time: departure_time, + arrival_time: arrival_time, + route: %Route{type: vehicle_type}, + stop: %Stop{ + platform_name: platform_name, + name: stop_name } }) do # If we happen to be looking at a stop that's the trip origin, there'll only be a departure time. Can use that if needed. %FutureArrival{ - time: if(arrival_time, do: to_datetime(arrival_time), else: to_datetime(departure_time)), + time: if(arrival_time, do: arrival_time, else: departure_time), platform_name: simplify(platform_name, vehicle_type), stop_name: stop_name } @@ -262,25 +250,4 @@ defmodule Dotcom.ScheduleFinder do defp departures_with_destination({route, departures}, direction_id, _) do {route, Route.direction_destination(route, direction_id), Enum.map(departures, & &1.time)} end - - @decorate cacheable(cache: @cache, on_error: :nothing, opts: [ttl: @schedule_ttl]) - defp get_schedules(params) do - case MBTA.Api.Schedules.all(params) do - %JsonApi{data: data} -> - {:ok, data} - - error -> - error - end - end - - defp to_datetime(time) do - case DateTime.from_iso8601(time) do - {:ok, dt, _} -> - DateTime.shift_zone!(dt, @timezone) - - _ -> - nil - end - end end diff --git a/lib/dotcom_web/live/schedule_finder_live.ex b/lib/dotcom_web/live/schedule_finder_live.ex index 43ca5c1816..4e80465323 100644 --- a/lib/dotcom_web/live/schedule_finder_live.ex +++ b/lib/dotcom_web/live/schedule_finder_live.ex @@ -172,6 +172,7 @@ defmodule DotcomWeb.ScheduleFinderLive do socket = update(socket, :loaded_trips, &Map.put(&1, schedule_id, AsyncResult.loading())) + {stop_sequence, _} = Integer.parse(stop_sequence) GenServer.cast(self(), {:get_next, {schedule_id, [trip_id, stop_sequence, date]}}) {:noreply, socket} end @@ -461,7 +462,15 @@ defmodule DotcomWeb.ScheduleFinderLive do
- {departure.headsign} +
+ {departure.headsign} + <.badge + :if={departure.time_desc == "School days only"} + class="bg-charcoal-80 text-nowrap text-sm" + > + {~t"School days only"} + +
{~t(Train)} {departure.trip_name}
diff --git a/test/dotcom/schedule_finder_test.exs b/test/dotcom/schedule_finder_test.exs index 285b07fae9..e7194ec369 100644 --- a/test/dotcom/schedule_finder_test.exs +++ b/test/dotcom/schedule_finder_test.exs @@ -3,21 +3,19 @@ defmodule Dotcom.ScheduleFinderTest do import Dotcom.ScheduleFinder import Mox - import Test.Support.Factories.MBTA.Api alias Dotcom.ScheduleFinder.{DailyDeparture, FutureArrival} + alias Test.Support.Factories.{RoutePatterns.RoutePattern, Schedules.Schedule} alias Test.Support.FactoryHelpers + setup :verify_on_exit! + setup do - Mox.stub_with(Dotcom.Utils.DateTime.Mock, Dotcom.Utils.DateTime) - cache = Application.get_env(:dotcom, :cache) - cache.flush() + stub_with(Dotcom.Utils.DateTime.Mock, Dotcom.Utils.DateTime) :ok end - setup :verify_on_exit! - describe "daily_departures/4" do test "requests schedules" do route_id = FactoryHelpers.build(:id) @@ -25,13 +23,12 @@ defmodule Dotcom.ScheduleFinderTest do stop_id = FactoryHelpers.build(:id) date = Faker.Util.format("%4d-%2d-%2d") - expect(MBTA.Api.Mock, :get_json, fn "/schedules/", opts -> - assert Keyword.get(opts, :include) == "trip" - assert Keyword.get(opts, :"filter[route]") == route_id - assert Keyword.get(opts, :"filter[direction_id]") == direction_id - assert Keyword.get(opts, :"filter[stop]") == stop_id - assert Keyword.get(opts, :"filter[date]") == date - %JsonApi{data: []} + expect(Schedules.Repo.Mock, :by_route_ids, fn routes, opts -> + assert routes == [route_id] + assert Keyword.get(opts, :direction_id) == direction_id + assert Keyword.get(opts, :stop_ids) == stop_id + assert Keyword.get(opts, :date) == date + [] end) assert {:ok, []} = daily_departures(route_id, direction_id, stop_id, date) @@ -42,26 +39,53 @@ defmodule Dotcom.ScheduleFinderTest do direction_id = Faker.Util.pick([0, 1]) stop_id = FactoryHelpers.build(:id) date = Faker.Util.format("%4d-%2d-%2d") + schedules = Schedule.build_list(4, :schedule) - expect(MBTA.Api.Mock, :get_json, fn "/schedules/", _ -> - %JsonApi{data: build_list(4, :schedule_item, departure_attributes())} + expect(Schedules.Repo.Mock, :by_route_ids, fn _, _ -> + schedules + end) + + expect(RoutePatterns.Repo.Mock, :get, length(schedules), fn id -> + RoutePattern.build(:route_pattern, id: id) end) assert {:ok, departures} = daily_departures(route_id, direction_id, stop_id, date) assert %DailyDeparture{} = List.first(departures) end + test "returns departures with route pattern time_desc" do + route_id = FactoryHelpers.build(:id) + direction_id = Faker.Util.pick([0, 1]) + stop_id = FactoryHelpers.build(:id) + date = Faker.Util.format("%4d-%2d-%2d") + schedules = Schedule.build_list(4, :schedule) + + expect(Schedules.Repo.Mock, :by_route_ids, fn _, _ -> + schedules + end) + + time_desc = Faker.Company.catch_phrase() + + expect(RoutePatterns.Repo.Mock, :get, length(schedules), fn id -> + RoutePattern.build(:route_pattern, id: id, time_desc: time_desc) + end) + + assert {:ok, departures} = daily_departures(route_id, direction_id, stop_id, date) + assert %DailyDeparture{time_desc: ^time_desc} = List.first(departures) + end + test "omits schedules that don't pick up passengers" do route_id = FactoryHelpers.build(:id) direction_id = Faker.Util.pick([0, 1]) stop_id = FactoryHelpers.build(:id) date = Faker.Util.format("%4d-%2d-%2d") - attributes_without_pickup = - departure_attributes() |> Map.merge(%{attributes: %{"pickup_type" => 1}}) + expect(Schedules.Repo.Mock, :by_route_ids, fn _, _ -> + Schedule.build_list(4, :schedule, pickup_type: 1) + end) - expect(MBTA.Api.Mock, :get_json, fn "/schedules/", _ -> - %JsonApi{data: build_list(4, :schedule_item, attributes_without_pickup)} + stub(RoutePatterns.Repo.Mock, :get, fn id -> + RoutePattern.build(:route_pattern, id: id) end) assert {:ok, []} = daily_departures(route_id, direction_id, stop_id, date) @@ -74,11 +98,9 @@ defmodule Dotcom.ScheduleFinderTest do stop_sequence = Faker.Util.pick([0, 1]) date = Faker.Util.format("%4d-%2d-%2d") - expect(MBTA.Api.Mock, :get_json, fn "/schedules/", opts -> - assert Keyword.get(opts, :include) == "stop" - assert Keyword.get(opts, :"filter[trip]") == trip_id - assert Keyword.get(opts, :"filter[date]") == date - %JsonApi{data: []} + expect(Schedules.Repo.Mock, :schedule_for_trip, fn ^trip_id, opts -> + assert Keyword.get(opts, :date) == date + [] end) assert {:ok, []} = next_arrivals(trip_id, stop_sequence, date) @@ -86,17 +108,12 @@ defmodule Dotcom.ScheduleFinderTest do test "omits schedules before the given stop_sequence" do trip_id = FactoryHelpers.build(:id) - stop_sequence = "20" + stop_sequence = Faker.random_between(2, 100) + earlier_stop_sequence = stop_sequence - 1 date = Faker.Util.format("%4d-%2d-%2d") - arrivals_before_stop = - arrival_attributes() |> Map.merge(%{attributes: %{"stop_sequence" => 10}}) - - expect(MBTA.Api.Mock, :get_json, fn "/schedules/", opts -> - assert Keyword.get(opts, :include) == "stop" - assert Keyword.get(opts, :"filter[trip]") == trip_id - assert Keyword.get(opts, :"filter[date]") == date - %JsonApi{data: build_list(4, :schedule_item, arrivals_before_stop)} + expect(Schedules.Repo.Mock, :schedule_for_trip, fn _, _ -> + Schedule.build_list(4, :schedule, stop_sequence: earlier_stop_sequence) end) assert {:ok, []} = next_arrivals(trip_id, stop_sequence, date) @@ -109,23 +126,13 @@ defmodule Dotcom.ScheduleFinderTest do Faker.Util.sample_uniq(2, fn -> Faker.random_between(1, 100) end) |> Enum.sort() date = Faker.Util.format("%4d-%2d-%2d") + schedules = Schedule.build_list(4, :schedule, stop_sequence: stop_sequence_for_arrivals) - expect(MBTA.Api.Mock, :get_json, fn "/schedules/", opts -> - assert Keyword.get(opts, :include) == "stop" - assert Keyword.get(opts, :"filter[trip]") == trip_id - assert Keyword.get(opts, :"filter[date]") == date - - %JsonApi{ - data: - build_list( - 4, - :schedule_item, - arrival_attributes() |> Map.put(:stop_sequence, stop_sequence_for_arrivals) - ) - } + expect(Schedules.Repo.Mock, :schedule_for_trip, fn _, _ -> + schedules end) - assert {:ok, arrivals} = next_arrivals(trip_id, stop_sequence_for_stop |> to_string(), date) + assert {:ok, arrivals} = next_arrivals(trip_id, stop_sequence_for_stop, date) assert %FutureArrival{} = List.first(arrivals) end end @@ -371,28 +378,17 @@ defmodule Dotcom.ScheduleFinderTest do stop_id = FactoryHelpers.build(:id) date = Faker.Util.format("%4d-%2d-%2d") - expect(MBTA.Api.Mock, :get_json, fn "/schedules/", _ -> - %JsonApi{data: build_list(4, :schedule_item, departure_attributes())} + schedules = Schedule.build_list(4, :schedule) + + expect(Schedules.Repo.Mock, :by_route_ids, fn _, _ -> + schedules + end) + + expect(RoutePatterns.Repo.Mock, :get, length(schedules), fn id -> + RoutePattern.build(:route_pattern, id: id) end) {:ok, departures} = daily_departures(route_id, direction_id, stop_id, date) departures end - - defp departure_attributes do - %{ - relationships: %{ - "route" => [build(:item)], - "trip" => [build(:trip_item)] - } - } - end - - defp arrival_attributes do - %{ - relationships: %{ - "stop" => [build(:stop_item)] - } - } - end end