Skip to content

Commit

Permalink
Planned disruptions logic (#2373)
Browse files Browse the repository at this point in the history
* date_time module and tests

* docs

* linting

* some cleanup

* some cleanup

* remove broken tests and add new tests

* this week test

* make service rollover time configurable

* split into two modules

* more tests

* 100% coverage

* some docs

* some docs

* format

* helper funcs

* move test to helper func

* docs

* format

* move private funcs to factory

* format

* alphabetize config

* remove unused import

* service range and tests

* PR feedback

* microsecond fidelity

* microsecond fidelity

* nil in range and docs

* more tests

* docs

* move in_range?

* call it date time range

* docs

* docs

* one hundred percent

* spec ref

* format

* tests

* type spec

* more date time tests

* typo

* some renaming

* docs

* naming and tests

* remove log and allow error

* add a mock for date_time

* mocks, tests, format

* docs

* one hundred percent coverage

* make test more clear

* move test

* docs

* start work

* logic looks solid

* use helper func

* tests

* tests

* tests

* one hundred

* dialyzer

* format

* specs

* tests

* spec error

* change tests

* type check

* type check

* pr feedback

* make behaviour load consistent

* remove livebook
  • Loading branch information
anthonyshull authored Feb 11, 2025
1 parent 177282b commit 9c7a295
Show file tree
Hide file tree
Showing 16 changed files with 270 additions and 24 deletions.
1 change: 1 addition & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ config :dotcom, :redix, Redix
config :dotcom, :redix_pub_sub, Redix.PubSub

config :dotcom, :repo_modules,
alerts: Alerts.Repo,
predictions: Predictions.Repo,
route_patterns: RoutePatterns.Repo,
routes: Routes.Repo,
Expand Down
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ config :dotcom, :mbta_api_module, MBTA.Api.Mock
config :dotcom, :location_service, LocationService.Mock

config :dotcom, :repo_modules,
alerts: Alerts.Repo.Mock,
predictions: Predictions.Repo.Mock,
route_patterns: RoutePatterns.Repo.Mock,
routes: Routes.Repo.Mock,
Expand Down
5 changes: 4 additions & 1 deletion lib/alerts/repo.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
defmodule Alerts.Repo do
alias Alerts.{Alert, Banner, Priority}
alias Alerts.Cache.Store
alias Alerts.Repo.Behaviour

@behaviour Behaviour

@spec all(DateTime.t()) :: [Alert.t()]
def all(now) do
Expand All @@ -17,7 +20,7 @@ defmodule Alerts.Repo do
Store.alert(id)
end

@spec by_route_ids([String.t()], DateTime.t()) :: [Alert.t()]
@impl Behaviour
def by_route_ids([], _now) do
[]
end
Expand Down
12 changes: 12 additions & 0 deletions lib/alerts/repo/behaviour.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Alerts.Repo.Behaviour do
@moduledoc """
Behaviour for the Alerts repo.
"""

alias Alerts.Alert

@doc """
Return all alerts for the given route ids.
"""
@callback by_route_ids([String.t()], DateTime.t()) :: [Alert.t()]
end
23 changes: 23 additions & 0 deletions lib/dotcom/alerts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Dotcom.Alerts do
@moduledoc """
A collection of functions that help to work with alerts in a unified way.
"""

alias Alerts.Alert

@service_impacting_effects [:delay, :shuttle, :suspension, :station_closure]

@doc """
Does the alert have an effect that is considered service-impacting?
"""
@spec service_impacting_alert?(Alert.t()) :: boolean()
def service_impacting_alert?(%Alert{effect: effect}) do
effect in @service_impacting_effects
end

@doc """
Returns a list of the alert effects that are considered service-impacting.
"""
@spec service_impacting_effects() :: [atom()]
def service_impacting_effects(), do: @service_impacting_effects
end
55 changes: 55 additions & 0 deletions lib/dotcom/alerts/disruptions/subway.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
defmodule Dotcom.Alerts.Disruptions.Subway do
@moduledoc """
Disruptions are alerts that have `service_impacting_effects` grouped by `service_range`.
"""

import Dotcom.Alerts, only: [service_impacting_alert?: 1]
import Dotcom.Routes, only: [subway_route_ids: 0]
import Dotcom.Utils.ServiceDateTime, only: [service_range: 1]

alias Alerts.Alert
alias Dotcom.Utils

@alerts_repo Application.compile_env!(:dotcom, :repo_modules)[:alerts]

@doc """
Disruptions that occur any time after today's service range.
"""
@spec future_disruptions() :: %{Utils.ServiceDateTime.named_service_range() => [Alert.t()]}
def future_disruptions() do
disruption_groups() |> Map.take([:later_this_week, :next_week, :after_next_week])
end

@doc """
Disruptions that occur during today's service range.
"""
@spec todays_disruptions() :: %{today: [Alert.t()]}
def todays_disruptions() do
disruption_groups() |> Map.take([:today])
end

# Groups all disruption alerts by service range.
#
# 1. Gets all alerts for subway routes.
# 2. Filters out non-service-impacting alerts
# 3. Groups them according to service range.
defp disruption_groups() do
subway_route_ids()
|> @alerts_repo.by_route_ids(Utils.DateTime.now())
|> Enum.filter(&service_impacting_alert?/1)
|> Enum.reduce(%{}, &group_alerts/2)
end

# Looks at every active period for an alert and groups that alert by service range.
# Alerts can overlap service ranges, in which case we want them to appear in both.
defp group_alerts(alert, groups) do
alert
|> Map.get(:active_period)
|> Enum.map(fn {start, stop} -> [service_range(start), service_range(stop)] end)
|> List.flatten()
|> Enum.uniq()
|> Enum.reduce(groups, fn service_range, groups ->
Map.update(groups, service_range, [alert], &(&1 ++ [alert]))
end)
end
end
13 changes: 13 additions & 0 deletions lib/dotcom/routes.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule Dotcom.Routes do
@moduledoc """
A collection of functions that help to work with routes in a unified way.
"""

@subway_route_ids ["Blue", "Green", "Orange", "Red"]

@doc """
Returns a list of route ids for all subway routes.
"""
@spec subway_route_ids() :: [String.t()]
def subway_route_ids(), do: @subway_route_ids
end
10 changes: 3 additions & 7 deletions lib/dotcom/system_status/alerts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ defmodule Dotcom.SystemStatus.Alerts do
belong in the main `Alerts` module.
"""

import Dotcom.Alerts, only: [service_impacting_alert?: 1]

@type service_effect_t :: :delay | :shuttle | :suspension | :station_closure
@service_effects [:delay, :shuttle, :suspension, :station_closure]
@doc """
Returns a list of the alert effects that are considered
service-impacting.
"""
def service_effects(), do: @service_effects

@doc """
Returns `true` if the alert is active at some point during the day
Expand Down Expand Up @@ -92,7 +88,7 @@ defmodule Dotcom.SystemStatus.Alerts do
}
"""
def filter_relevant(alerts) do
alerts |> Enum.filter(fn %{effect: effect} -> effect in @service_effects end)
alerts |> Enum.filter(&service_impacting_alert?/1)
end

# Returns true if the alert (as signified by the active_period_start provided)
Expand Down
2 changes: 2 additions & 0 deletions lib/dotcom/utils/date_time.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ defmodule Dotcom.Utils.DateTime do
Timex will return an error if the time occurs when we "spring-forward" in DST transitions.
That is because one hour does not occur--02:00:00am to 03:00:00am.
In that case, we set the time to 03:00:00am.
If we are given something tha tis not a DateTime, AmbiguousDateTime, or an error tuple, we log the input and return `now`.
"""
@impl Behaviour
def coerce_ambiguous_date_time(%DateTime{} = date_time), do: date_time
Expand Down
92 changes: 92 additions & 0 deletions test/dotcom/alerts/disruptions/subway_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
defmodule Dotcom.Alerts.Disruptions.SubwayTest do
use ExUnit.Case

import Dotcom.Alerts, only: [service_impacting_effects: 0]
import Dotcom.Alerts.Disruptions.Subway

import Dotcom.Utils.ServiceDateTime,
only: [
service_range_day: 0,
service_range_later_this_week: 0,
service_range_next_week: 0,
service_range_after_next_week: 0
]

import Mox

alias Test.Support.Factories

setup :verify_on_exit!

setup _ do
stub_with(Dotcom.Utils.DateTime.Mock, Dotcom.Utils.DateTime)

:ok
end

describe "future_disruptions/0" do
test "returns an empty map when there are no alerts" do
expect(Alerts.Repo.Mock, :by_route_ids, fn _route_ids, _now ->
[]
end)

# Exercise/Verify
assert %{} = future_disruptions()
end

test "returns alerts for later this week, next week, and after next week" do
# Setup
alert_today = service_range_day() |> disruption_alert()
alert_later_this_week = service_range_later_this_week() |> disruption_alert()
alert_next_week = service_range_next_week() |> disruption_alert()

{alert_after_next_week_start, _} = service_range_after_next_week()

alert_after_next_week =
{alert_after_next_week_start, Timex.shift(alert_after_next_week_start, days: 1)}
|> disruption_alert()

expect(Alerts.Repo.Mock, :by_route_ids, fn _route_ids, _now ->
[alert_today, alert_later_this_week, alert_next_week, alert_after_next_week]
end)

# Exercise/Verify
assert %{
later_this_week: [^alert_later_this_week],
next_week: [^alert_next_week],
after_next_week: [^alert_after_next_week]
} = future_disruptions()
end
end

describe "todays_disruptions/0" do
test "returns an empty map when there are no alerts" do
expect(Alerts.Repo.Mock, :by_route_ids, fn _route_ids, _now ->
[]
end)

# Exercise/Verify
assert %{} = todays_disruptions()
end

test "returns alerts for today only" do
# Setup
alert_today = service_range_day() |> disruption_alert()
alert_next_week = service_range_next_week() |> disruption_alert()

expect(Alerts.Repo.Mock, :by_route_ids, fn _route_ids, _now ->
[alert_today, alert_next_week]
end)

# Exercise/Verify
assert %{today: [^alert_today]} = todays_disruptions()
end
end

defp disruption_alert(active_period) do
Factories.Alerts.Alert.build(:alert,
active_period: [active_period],
effect: service_impacting_effects() |> Faker.Util.pick()
)
end
end
31 changes: 31 additions & 0 deletions test/dotcom/alerts_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule Dotcom.AlertsTest do
use ExUnit.Case

import Dotcom.Alerts

describe "service_impacting_alert?/1" do
test "returns true if the alert has an effect that is considered service-impacting" do
# Setup
effect = service_impacting_effects() |> Faker.Util.pick()
alert = %Alerts.Alert{effect: effect}

# Exercise/Verify
assert service_impacting_alert?(alert)
end

test "returns false if the alert does not have an effect that is considered service-impacting" do
# Setup
alert = %Alerts.Alert{effect: :not_service_impacting}

# Exercise/Verify
refute service_impacting_alert?(alert)
end
end

describe "service_impacting_effects/0" do
test "returns a list of the alert effects as atoms" do
# Exercise/Verify
assert Enum.all?(service_impacting_effects(), &is_atom/1)
end
end
end
12 changes: 12 additions & 0 deletions test/dotcom/routes_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Dotcom.RoutesTest do
use ExUnit.Case

import Dotcom.Routes

describe "subway_route_ids/0" do
test "returns a list of route ids" do
# Exercise/Verify
assert Enum.all?(subway_route_ids(), &is_binary/1)
end
end
end
Loading

0 comments on commit 9c7a295

Please sign in to comment.