diff --git a/config/config.exs b/config/config.exs index 14ab2e7a5f..5c54443bf2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -58,8 +58,6 @@ config :dotcom, :req_module, Req config :dotcom, :search_service, Dotcom.SearchService -config :dotcom, :timetable_loader_module, Dotcom.TimetableLoader - config :dotcom, :service_rollover_time, ~T[03:00:00] config :dotcom, :timezone, "America/New_York" diff --git a/config/test.exs b/config/test.exs index a8bb820a96..20c304316e 100644 --- a/config/test.exs +++ b/config/test.exs @@ -52,8 +52,6 @@ config :dotcom, :otp_module, OpenTripPlannerClient.Mock config :dotcom, :req_module, Req.Mock config :dotcom, :search_service, Dotcom.SearchService.Mock -config :dotcom, :timetable_loader_module, Dotcom.TimetableLoader.Mock - # Let test requests get routed through the :secure pipeline config :dotcom, :secure_pipeline, force_ssl: [ diff --git a/lib/timetable_loader.ex b/lib/timetable_loader.ex deleted file mode 100644 index 0996041399..0000000000 --- a/lib/timetable_loader.ex +++ /dev/null @@ -1,130 +0,0 @@ -defmodule Dotcom.TimetableLoader do - @moduledoc """ - Gets timetable data from CSV files. - - The CSVs are expected to be generated from a timetable PDF file, and placed into the - `"/priv/timetables/"` directory under the following naming convention: - `"{route_id}-{direction_id}.csv"`. At minimum, the `route_id` must be included as a key - in the `Dotcom.Timetable` `@metadata` module attribute, with a value of a map containing - an `:effective_dates` key indicating the timetable's start and end dates as a tuple. - """ - - import Dotcom.Utils.Time, only: [between?: 3] - - @loader_module Application.compile_env!(:dotcom, :timetable_loader_module) - - @metadata %{ - "Boat-F6" => %{ - effective_dates: {~D[2026-04-27], ~D[2026-06-13]}, - special_dates: %{~D[2026-05-25] => "Boat-F8"}, - weekend: "Boat-F8" - }, - "Boat-F7" => %{ - effective_dates: {~D[2026-04-27], ~D[2026-06-13]}, - special_dates: %{~D[2026-05-25] => "Boat-F8"}, - weekend: "Boat-F8" - }, - "Boat-F8" => %{ - effective_dates: {~D[2026-05-23], ~D[2026-06-13]} - } - } - @available_route_ids Map.keys(@metadata) - - @doc """ - Routes supported by `Dotcom.Timetable`, listed by ID. - """ - @spec available_route_ids :: [String.t()] - def available_route_ids, do: @available_route_ids - - @doc """ - Retrieves timetable data for a given route, direction, and date. Returns nil if unavailable. - """ - @spec from_csv(Routes.Route.id_t(), 0 | 1, Date.t()) :: {:ok, list()} | {:error, term()} - def from_csv(route_id, direction_id, date) when route_id in @available_route_ids do - route_id = - get_in(@metadata, [route_id, :special_dates, date]) || - maybe_use_weekend_route(route_id, date) - - if in_timetable_date_range?(route_id, date) do - case @loader_module.get_csv("#{route_id}-#{direction_id}.csv") do - data when is_list(data) -> - {:ok, Enum.map(data, &trip_maps_from_row(&1, route_id == "CR-Foxboro"))} - - _ -> - {:error, :no_data} - end - else - {:ok, []} - end - end - - def from_csv(_, _, _), do: {:error, :invalid_route} - - defp maybe_use_weekend_route(route_id, date) do - with true <- date_in_weekend?(date), - new_route_id when is_binary(new_route_id) <- get_in(@metadata, [route_id, :weekend]) do - new_route_id - else - _ -> - route_id - end - end - - defp date_in_weekend?(~D[2025-07-04]), do: true - defp date_in_weekend?(date), do: Date.day_of_week(date) in [6, 7] - - @doc """ - Checks the `Dotcom.Timetable` metadata for whether the given date is effective on the given route. - """ - @spec in_timetable_date_range?(Routes.Route.id_t(), Date.t()) :: boolean() - def in_timetable_date_range?(route_id, date) do - case get_in(@metadata, [route_id, :effective_dates]) do - {start, ending} -> - between?(date, start, ending) - - _ -> - false - end - end - - # transform each row from a single map e.g. %{"Stop" => stop_id, "0600" => "11:00 AM", ...} - # to a list containing a map for each trip/time - defp trip_maps_from_row(stop_row, add_name?) do - {stop_id, trips} = Map.pop!(stop_row, "Stop") - - Enum.map(trips, fn {trip_key, time} -> - # The PDFs don't have trip names - Map.new( - stop_id: stop_id, - trip: %{name: if(add_name?, do: trip_key, else: ""), id: trip_key}, - time: time - ) - end) - end - - @behaviour Dotcom.TimetableLoader.Behaviour - - @impl Dotcom.TimetableLoader.Behaviour - def get_csv(filename) do - path = Path.join(timetable_directory(), filename) - - if File.exists?(path) do - path - |> File.stream!() - |> CSV.decode!(headers: true) - |> Enum.to_list() - end - end - - defp timetable_directory() do - Application.app_dir(:dotcom) |> Path.join("/priv/timetables") - end - - defmodule Behaviour do - @moduledoc """ - Behaviour for describing fetching a timetable CSV - """ - - @callback get_csv(String.t()) :: list() | nil - end -end diff --git a/priv/timetables/Boat-F6-1.csv b/priv/timetables/Boat-F6-1.csv deleted file mode 100644 index f24e37357a..0000000000 --- a/priv/timetables/Boat-F6-1.csv +++ /dev/null @@ -1,7 +0,0 @@ -Stop,0640,0755,0910,1025,1145,1355,1445,1600,1715,1830,1950 -Boat-Winthrop,6:40 AM,7:55 AM,9:10 AM,10:25 AM,11:45 AM,,2:45 PM,4:00 PM,5:15 PM,6:30 PM,7:50 PM -Boat-Logan,,,,,,,3:10 PM,4:25 PM,5:40 PM,6:55 PM,8:15 PM -Boat-Aquarium,7:05 AM,8:20 AM,9:35 AM,10:50 AM,12:10 PM,1:55 PM,3:20 PM,4:35 PM,5:50 PM,7:05 PM,8:25 PM -Boat-Fan,7:15 AM,8:30 AM,9:45 AM,11:05 AM,12:20 PM,2:05 PM,3:30 PM,4:45 PM,6:00 PM,7:15 PM,8:35 PM -Boat-Logan,7:25 AM,8:40 AM,9:55 AM,11:15 AM,,2:15 PM,,,,, -Boat-Winthrop,7:50 AM,9:05 AM,10:20 AM,11:40 AM,,2:40 PM,3:55 PM,5:10 PM,6:25 PM,7:40 PM,9:00 PM \ No newline at end of file diff --git a/priv/timetables/Boat-F7-1.csv b/priv/timetables/Boat-F7-1.csv deleted file mode 100644 index 10e80620f4..0000000000 --- a/priv/timetables/Boat-F7-1.csv +++ /dev/null @@ -1,7 +0,0 @@ -Stop,0615,0730,0845,1000,1115,1350,1440,1555,1710,1825,1940 -Boat-Quincy,6:15 AM,7:30 AM,8:45 AM,10:00 AM,11:15 AM,,2:40 PM,3:55 PM,5:10 PM,6:25 PM,7:40 PM -Boat-Logan,,,,,,,3:05 PM,4:20 PM,5:35 PM,6:50 PM,8:05 PM -Boat-Fan,6:40 AM,7:55 AM,9:10 AM,10:25 AM,11:40 AM,1:50 PM,3:15 PM,4:30 PM,5:45 PM,7:00 PM,8:20 PM -Boat-Aquarium,6:50 AM,8:05 AM,9:20 AM,10:35 AM,11:50 AM,2:00 PM,3:25 PM,4:40 PM,5:55 PM,7:10 PM,8:30 PM -Boat-Logan,7:00 AM,8:15 AM,9:30 AM,10:45 AM,,2:10 PM,,,,, -Boat-Quincy,7:25 AM,8:40 AM,9:55 AM,11:10 AM,,2:35 PM,3:50 PM,5:05 PM,6:20 PM,7:35 PM,8:55 PM \ No newline at end of file diff --git a/priv/timetables/Boat-F8-1.csv b/priv/timetables/Boat-F8-1.csv deleted file mode 100644 index 8255066d02..0000000000 --- a/priv/timetables/Boat-F8-1.csv +++ /dev/null @@ -1,9 +0,0 @@ -Stop,0845,1030,1215,1500,1645,1830,2015 -Boat-Winthrop,8:45 AM,10:30 AM,12:15 PM,3:00 PM,4:45 PM,6:30 PM,8:15 PM -Boat-Quincy,9:10 AM,10:55 AM,12:40 PM,3:25 PM,5:10 PM,6:55 PM,8:40 PM -Boat-Fan,9:35 AM,11:20 AM,1:05 PM,3:50 PM,5:35 PM,7:20 PM,9:05 PM -Boat-Aquarium,9:45 AM,11:30 AM,1:15 PM,4:00 PM,5:45 PM,7:30 PM,9:15 PM -Boat-Logan,9:55 AM,11:40 AM,1:25 PM,4:10 PM,5:55 PM,7:40 PM,9:25 PM -Boat-Fan,10:05 AM,11:50 AM,,4:20 PM,6:05 PM,7:50 PM,9:35 PM -Boat-Winthrop,10:30 AM,12:15 PM,,4:45 PM,6:30 PM,8:15 PM,10:00 PM -Boat-Quincy,10:55 AM,12:40 PM,,5:10 PM,6:55 PM,8:40 PM,10:25 PM diff --git a/test/support/mocks.ex b/test/support/mocks.ex index 4eee65f270..83547923f9 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -20,7 +20,6 @@ Mox.defmock(OpenTripPlannerClient.Mock, for: OpenTripPlannerClient.Behaviour) Mox.defmock(Predictions.Phoenix.PubSub.Mock, for: Phoenix.Channel) Mox.defmock(Predictions.PubSub.Mock, for: [GenServer, Predictions.PubSub.Behaviour]) Mox.defmock(Predictions.Store.Mock, for: Predictions.Store.Behaviour) -Mox.defmock(Dotcom.TimetableLoader.Mock, for: Dotcom.TimetableLoader.Behaviour) # Repos Mox.defmock(Alerts.Repo.Mock, for: Alerts.Repo.Behaviour) diff --git a/test/timetable_loader_test.exs b/test/timetable_loader_test.exs deleted file mode 100644 index 5e71430555..0000000000 --- a/test/timetable_loader_test.exs +++ /dev/null @@ -1,75 +0,0 @@ -defmodule Dotcom.TimetableLoaderTest do - use ExUnit.Case, async: true - - import Dotcom.TimetableLoader - import Mox - - @ferry_date_range Date.range(~D[2026-05-23], ~D[2026-06-13]) - - setup :verify_on_exit! - - describe "from_csv/1" do - defp valid_date(_), do: Faker.Date.between(@ferry_date_range.first, @ferry_date_range.last) - - test "error for invalid route" do - assert from_csv(Faker.Internet.slug(), Faker.Util.pick([0, 1]), Date.utc_today()) == - {:error, :invalid_route} - end - - test "fetches data for valid route/direction/date" do - valid_route_id = Faker.Util.pick(available_route_ids()) - - expect(Dotcom.TimetableLoader.Mock, :get_csv, fn _ -> - [%{"Stop" => "stop_id", "1" => "11:00 AM", "2" => "11:11 AM"}] - end) - - assert {:ok, data} = from_csv(valid_route_id, 1, valid_date(valid_route_id)) - assert [[%{time: _, trip: _, stop_id: _} | _] | _] = data - end - - test "handle missing csv" do - valid_route_id = Faker.Util.pick(available_route_ids()) - - expect(Dotcom.TimetableLoader.Mock, :get_csv, fn _ -> - nil - end) - - assert from_csv(valid_route_id, 1, valid_date(valid_route_id)) == {:error, :no_data} - end - - test "special case: weekend F6/F7 returns F8 table" do - f6_or_f7 = Faker.Util.pick(~w(Boat-F6 Boat-F7)) - - weekend_date = - @ferry_date_range - |> Enum.filter(&(Date.day_of_week(&1) in [6, 7])) - |> Faker.Util.pick() - - expect(Dotcom.TimetableLoader.Mock, :get_csv, fn filename -> - assert filename =~ "Boat-F8" - [] - end) - - assert {:ok, _} = from_csv(f6_or_f7, 1, weekend_date) - end - - test "no data for valid route on wrong date" do - valid_route_id = Faker.Util.pick(available_route_ids()) - invalid_date = ~D[2025-12-25] - assert from_csv(valid_route_id, 1, invalid_date) == {:ok, []} - end - - test "special case: specific overrides for F6/F7 return F8 table" do - f6_or_f7 = Faker.Util.pick(~w(Boat-F6 Boat-F7)) - - memorial_day = ~D[2026-05-25] - - expect(Dotcom.TimetableLoader.Mock, :get_csv, fn filename -> - assert filename =~ "Boat-F8" - [] - end) - - assert {:ok, _} = from_csv(f6_or_f7, 1, memorial_day) - end - end -end