From 9be66133bf4f583d82e765027fdc8a8d8e18c354 Mon Sep 17 00:00:00 2001 From: Lucy Tan Date: Fri, 12 Jun 2026 12:08:45 -0400 Subject: [PATCH 1/3] fix(timetable): Restrict Upcoming Changes to service-impacting and schedule changes --- lib/dotcom/system_status/commuter_rail.ex | 32 +++++++++++ .../schedule/timetable_controller.ex | 57 ++++++------------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/lib/dotcom/system_status/commuter_rail.ex b/lib/dotcom/system_status/commuter_rail.ex index 12b3a99fc1..be7fed74ac 100644 --- a/lib/dotcom/system_status/commuter_rail.ex +++ b/lib/dotcom/system_status/commuter_rail.ex @@ -91,6 +91,38 @@ defmodule Dotcom.SystemStatus.CommuterRail do end end + @doc """ + Returns upcoming alerts for the route given. + """ + @spec commuter_rail_upcoming_alerts(String.t()) :: [Alerts.Alert.t()] + def commuter_rail_upcoming_alerts(id) do + [id] + |> @alerts_repo.by_route_ids(@date_time_module.now()) + |> Enum.filter(fn alert -> + (service_impacting_alert?(alert) || alert.effect == :schedule_change) && + future_alert?(alert) + end) + end + + # Checks if the next active period for an alert is in the future. + # Excludes alerts that end today. + defp future_alert?(alert) do + case next_active_time(alert) do + {:future, _} -> + true + + {:current, start_time} -> + {_, end_time} = + alert.active_period + |> Enum.find(fn {start, _} -> DateTime.compare(start, start_time) == :eq end) + + Util.safe_time_compare(end_time, Util.end_of_service()) == :gt + + _ -> + false + end + end + # Groups the alerts given into train impacts (delays and # cancellations) versus service impacts (everything else). For # train impacts, add trip info (first departure time, train number, diff --git a/lib/dotcom_web/controllers/schedule/timetable_controller.ex b/lib/dotcom_web/controllers/schedule/timetable_controller.ex index 5424175ad9..001e226b13 100644 --- a/lib/dotcom_web/controllers/schedule/timetable_controller.ex +++ b/lib/dotcom_web/controllers/schedule/timetable_controller.ex @@ -6,8 +6,8 @@ defmodule DotcomWeb.ScheduleController.TimetableController do require Logger - import Dotcom.Alerts.StartTime, only: [next_active_time: 1] - import Dotcom.SystemStatus.CommuterRail, only: [commuter_rail_route_status: 1] + import Dotcom.SystemStatus.CommuterRail, + only: [commuter_rail_route_status: 1, commuter_rail_upcoming_alerts: 1] alias Dotcom.Timetables alias DotcomWeb.ScheduleView @@ -52,8 +52,7 @@ defmodule DotcomWeb.ScheduleController.TimetableController do |> assign(:meta_description, meta_description) |> assign(:direction_name, direction_name) |> assign(:formatted_date, formatted_date) - |> assign_cr_status() - |> assign_cr_upcoming() + |> assign_cr_info() |> assign_banner_alerts() |> put_view(ScheduleView) |> render("show.html", []) @@ -68,43 +67,19 @@ defmodule DotcomWeb.ScheduleController.TimetableController do defp station_type_name(%Route{type: 4}), do: ~t"docks" defp station_type_name(_route), do: ~t"stations" - defp assign_cr_status(%{assigns: %{route: route}} = conn) do - cr_status = - if Routes.Route.type_atom(route) == :commuter_rail do - commuter_rail_route_status(route.id) - end - - conn - |> assign(:cr_status, cr_status) - end - - defp assign_cr_upcoming(%{assigns: %{alerts: alerts, route: route}} = conn) do - cr_upcoming = - if Routes.Route.type_atom(route) == :commuter_rail do - alerts - |> Enum.filter(&future_alert?/1) - else - [] - end - - conn - |> assign(:cr_upcoming, cr_upcoming) - end - - defp future_alert?(alert) do - case next_active_time(alert) do - {:future, _} -> - true - - {:current, start_time} -> - {_, end_time} = - alert.active_period - |> Enum.find(fn {start, _} -> DateTime.compare(start, start_time) == :eq end) - - Util.safe_time_compare(end_time, Util.end_of_service()) == :gt - - _ -> - false + defp assign_cr_info(%{assigns: %{route: route}} = conn) do + if Routes.Route.type_atom(route) == :commuter_rail do + conn + |> assign(%{ + cr_status: commuter_rail_route_status(route.id), + cr_upcoming: commuter_rail_upcoming_alerts(route.id) + }) + else + conn + |> assign(%{ + cr_status: nil, + cr_upcoming: [] + }) end end From f68ef15c9bb033b665ffe04fbfb33c1fd34feb57 Mon Sep 17 00:00:00 2001 From: Lucy Tan Date: Fri, 12 Jun 2026 12:59:52 -0400 Subject: [PATCH 2/3] Formatting --- lib/dotcom/system_status/commuter_rail.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dotcom/system_status/commuter_rail.ex b/lib/dotcom/system_status/commuter_rail.ex index be7fed74ac..348dda456d 100644 --- a/lib/dotcom/system_status/commuter_rail.ex +++ b/lib/dotcom/system_status/commuter_rail.ex @@ -100,7 +100,7 @@ defmodule Dotcom.SystemStatus.CommuterRail do |> @alerts_repo.by_route_ids(@date_time_module.now()) |> Enum.filter(fn alert -> (service_impacting_alert?(alert) || alert.effect == :schedule_change) && - future_alert?(alert) + future_alert?(alert) end) end From 0538f04a337f0cdd9ff0a366eb0157c6e79da62d Mon Sep 17 00:00:00 2001 From: Lucy Tan Date: Fri, 12 Jun 2026 13:43:05 -0400 Subject: [PATCH 3/3] Tests --- lib/dotcom/system_status/commuter_rail.ex | 4 +- .../schedule/timetable_controller.ex | 4 +- .../system_status/commuter_rail_test.exs | 76 ++++++++++++++++++- 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/lib/dotcom/system_status/commuter_rail.ex b/lib/dotcom/system_status/commuter_rail.ex index 348dda456d..27235d8840 100644 --- a/lib/dotcom/system_status/commuter_rail.ex +++ b/lib/dotcom/system_status/commuter_rail.ex @@ -94,8 +94,8 @@ defmodule Dotcom.SystemStatus.CommuterRail do @doc """ Returns upcoming alerts for the route given. """ - @spec commuter_rail_upcoming_alerts(String.t()) :: [Alerts.Alert.t()] - def commuter_rail_upcoming_alerts(id) do + @spec commuter_rail_upcoming_changes(String.t()) :: [Alerts.Alert.t()] + def commuter_rail_upcoming_changes(id) do [id] |> @alerts_repo.by_route_ids(@date_time_module.now()) |> Enum.filter(fn alert -> diff --git a/lib/dotcom_web/controllers/schedule/timetable_controller.ex b/lib/dotcom_web/controllers/schedule/timetable_controller.ex index 001e226b13..7e13c62cdd 100644 --- a/lib/dotcom_web/controllers/schedule/timetable_controller.ex +++ b/lib/dotcom_web/controllers/schedule/timetable_controller.ex @@ -7,7 +7,7 @@ defmodule DotcomWeb.ScheduleController.TimetableController do require Logger import Dotcom.SystemStatus.CommuterRail, - only: [commuter_rail_route_status: 1, commuter_rail_upcoming_alerts: 1] + only: [commuter_rail_route_status: 1, commuter_rail_upcoming_changes: 1] alias Dotcom.Timetables alias DotcomWeb.ScheduleView @@ -72,7 +72,7 @@ defmodule DotcomWeb.ScheduleController.TimetableController do conn |> assign(%{ cr_status: commuter_rail_route_status(route.id), - cr_upcoming: commuter_rail_upcoming_alerts(route.id) + cr_upcoming: commuter_rail_upcoming_changes(route.id) }) else conn diff --git a/test/dotcom/system_status/commuter_rail_test.exs b/test/dotcom/system_status/commuter_rail_test.exs index 0f53aaa55a..48c85aa337 100644 --- a/test/dotcom/system_status/commuter_rail_test.exs +++ b/test/dotcom/system_status/commuter_rail_test.exs @@ -1,7 +1,9 @@ defmodule Dotcom.SystemStatus.CommuterRailTest do use ExUnit.Case - import Dotcom.SystemStatus.CommuterRail, only: [commuter_rail_route_status: 1] + import Dotcom.SystemStatus.CommuterRail, + only: [commuter_rail_route_status: 1, commuter_rail_upcoming_changes: 1] + import Mox import Test.Support.Generators.DateTime, only: [random_time_range_date_time: 1] @@ -701,4 +703,76 @@ defmodule Dotcom.SystemStatus.CommuterRailTest do assert delay.trip_info == :all end end + + describe "commuter_rail_upcoming_changes/1" do + test "includes only schedule changes and service-impacting alerts" do + # SETUP + today = Util.now() + active_period = [{today, DateTime.shift(today, week: 2)}] + + service_impacting_effects = + Dotcom.Alerts.service_impacting_effects() + |> Enum.map(fn item -> elem(item, 0) end) + + non_service_impacting_effects = Alerts.Alert.all_types() -- service_impacting_effects + + schedule_change = + Factories.Alerts.Alert.build( + :alert, + active_period: active_period, + effect: :schedule_change + ) + + service_impact = + Factories.Alerts.Alert.build( + :alert, + active_period: active_period, + effect: Faker.Util.pick(service_impacting_effects) + ) + + other_effect = + Factories.Alerts.Alert.build( + :alert, + active_period: active_period, + effect: Faker.Util.pick(non_service_impacting_effects -- [:schedule_change]) + ) + + # EXERCISE + expect(Alerts.Repo.Mock, :by_route_ids, fn _, _ -> + [schedule_change, service_impact, other_effect] + end) + + alerts = commuter_rail_upcoming_changes("foo") + + # VERIFY + assert Enum.sort(alerts) == Enum.sort([schedule_change, service_impact]) + end + + test "excludes alerts that end today" do + # SETUP + today = Util.now() + later = DateTime.shift(today, day: 3) + + alert1 = + Factories.Alerts.Alert.build( + :alert, + active_period: [{today, today}], + effect: :schedule_change + ) + + alert2 = + Factories.Alerts.Alert.build( + :alert, + active_period: [{today, later}], + effect: :schedule_change + ) + + # EXERCISE + expect(Alerts.Repo.Mock, :by_route_ids, fn _, _ -> [alert1, alert2] end) + alerts = commuter_rail_upcoming_changes("foo") + + # VERIFY + assert alerts == [alert2] + end + end end