Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions lib/dotcom/system_status/commuter_rail.ex
Original file line number Diff line number Diff line change
Expand Up @@ -259,20 +259,6 @@ defmodule Dotcom.SystemStatus.CommuterRail do
|> Enum.filter(&Routes.Route.commuter_rail?/1)
end

# Returns a map where the key is the effect of the alert
# and the value is the number of alerts with that effect.
# For example, if there are 2 delays and 1 cancellation,
# the map would be `%{delay: 2, cancellation: 1}`.
@spec alert_counts([Alerts.Alert.t()]) :: map()
defp alert_counts(alerts) do
alerts
|> Enum.group_by(& &1.effect)
|> Enum.map(fn {effect, alerts} ->
{effect, %{count: Kernel.length(alerts), next_active: next_active_time(alerts)}}
end)
|> Map.new()
end

# Returns a boolean indicating whether or not the route has a schedule
# for today. This is used to determine if the route is running service today.
@spec service_today?(String.t()) :: boolean()
Expand All @@ -282,6 +268,20 @@ defmodule Dotcom.SystemStatus.CommuterRail do
|> Enum.any?(fn %{time: time} -> Dotcom.Utils.ServiceDateTime.service_today?(time) end)
end

# Given a list of impacts - or any map/struct with an alert, returns
# the count and next_active time (as a tuple of either :current or
# :future, along with the actual start time).
@spec impact_summary([%{alert: Alert.t()}]) :: %{
count: integer(),
next_active: :past | {:current, DateTime.t()} | {:future, DateTime.t()}
}
defp impact_summary(impact_list) do
%{
count: impact_list |> Enum.count(),
next_active: impact_list |> Enum.map(& &1.alert) |> next_active_time()
}
end

# Returns a tuple with the Route ID and a map containing
# the alert counts, name of the route, sort order, and whether the route
# is running service today.
Expand All @@ -295,9 +295,21 @@ defmodule Dotcom.SystemStatus.CommuterRail do
}}
defp route_info(%Route{id: id, name: name, sort_order: sort_order}) do
alert_counts =
id
|> commuter_rail_route_alerts()
|> alert_counts()
case commuter_rail_route_status(id) do
%{
delays: delays,
cancellations: cancellations,
service_impacts: service_impacts
} ->
service_impacts
|> Enum.group_by(& &1.alert.effect)
|> Enum.into(%{cancellation: cancellations, delay: delays})
|> Enum.reject(fn {_effect, impact_list} -> Enum.empty?(impact_list) end)
|> Map.new(fn {effect, impact_list} -> {effect, impact_summary(impact_list)} end)

_ ->
%{}
end

service_today? = service_today?(id)

Expand Down
181 changes: 181 additions & 0 deletions livebooks/reusable/alerts.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,184 @@ Alerts.Cache.Store.update(alerts, nil)

now() |> Alerts.Repo.all()
```

<!-- livebook:{"branch_parent_index":1} -->

## Commuter Rail Multi-Trip Train Alert

```elixir
# Create an informed entity for a commuter rail route
route = Routes.Repo.by_type(2) |> Faker.Util.pick()

schedules = Schedules.Repo.by_route_ids([route.id])

trip_ids =
Faker.Util.sample_uniq(3, fn -> Faker.Util.pick(schedules) end)
|> Enum.map(& &1.trip.id)

informed_entities =
trip_ids
|> Enum.map(
&InformedEntity.build(:informed_entity,
activities: MapSet.new([:exit, :ride, :board]),
route: route.id,
route_type: 2,
trip: &1
)
)

alert =
Alert.build(:alert,
active_period: [
{
now() |> Timex.shift(hours: -4),
now() |> Timex.shift(hours: 8)
}
],
effect: Faker.Util.pick([:delay, :cancellation]),
informed_entity: Alerts.InformedEntitySet.new(informed_entities),
priority: :high,
severity: 3
)

# Remove all other alerts and only use the one you created
Alerts.Cache.Store.update([alert], nil)

now() |> Alerts.Repo.all()
```

<!-- livebook:{"branch_parent_index":1} -->

## Commuter Rail All Trains or Direction-Specific Alert

```elixir
# Create an informed entity for a commuter rail route
route = Routes.Repo.by_type(2) |> Faker.Util.pick()

informed_entities = [
InformedEntity.build(:informed_entity,
activities: MapSet.new([:exit, :ride, :board]),
direction_id: Faker.Util.pick([0, 1, nil]),
route: route.id,
route_type: 2,
trip: nil
)
]

alert =
Alert.build(:alert,
active_period: [
{
now() |> Timex.shift(hours: -4),
now() |> Timex.shift(hours: 8)
}
],
effect: Faker.Util.pick([:delay, :cancellation]),
informed_entity: Alerts.InformedEntitySet.new(informed_entities),
priority: :high,
severity: 3
)

# Remove all other alerts and only use the one you created
Alerts.Cache.Store.update([alert], nil)

now() |> Alerts.Repo.all()
```

<!-- livebook:{"branch_parent_index":1} -->

## Commuter Rail Upcoming Shuttle

```elixir
# Create an informed entity for a commuter rail route
route = Routes.Repo.by_type(2) |> Faker.Util.pick()

schedules = Schedules.Repo.by_route_ids([route.id])

informed_entities =
[
InformedEntity.build(:informed_entity,
activities: MapSet.new([:exit, :ride, :board]),
route: route.id,
route_type: 2
)
]

alert =
Alert.build(:alert,
active_period: [
{
now() |> Timex.shift(hours: 4),
now() |> Timex.shift(hours: 8)
}
],
effect: :shuttle,
informed_entity: Alerts.InformedEntitySet.new(informed_entities),
priority: :high,
severity: 3
)

# Remove all other alerts and only use the one you created
Alerts.Cache.Store.update([alert], nil)

now() |> Alerts.Repo.all()
```

## Commuter Rail Delays and Cancellations

```elixir
# Create an informed entity for a commuter rail route
route = Routes.Repo.by_type(2) |> Faker.Util.pick()

schedules = Schedules.Repo.by_route_ids([route.id])

[trip_id_1, trip_id_2] =
Faker.Util.sample_uniq(2, fn -> Faker.Util.pick(schedules) end)
|> Enum.map(& &1.trip.id)

active_period = [
{
now() |> Timex.shift(hours: -4),
now() |> Timex.shift(hours: 8)
}
]

alerts =
[
Alert.build(:alert,
active_period: active_period,
effect: :cancellation,
informed_entity:
Alerts.InformedEntitySet.new([
InformedEntity.build(:informed_entity,
activities: MapSet.new([:exit, :ride, :board]),
route: route.id,
route_type: 2,
trip: trip_id_1
)
]),
priority: :high,
severity: 3
),
Alert.build(:alert,
active_period: active_period,
effect: :delay,
informed_entity:
Alerts.InformedEntitySet.new([
InformedEntity.build(:informed_entity,
activities: MapSet.new([:exit, :ride, :board]),
route: route.id,
route_type: 2,
trip: trip_id_2
)
]),
priority: :high,
severity: 3
)
]

# Remove all other alerts and only use the one you created
Alerts.Cache.Store.update(alerts, nil)

now() |> Alerts.Repo.all()
```
33 changes: 32 additions & 1 deletion test/dotcom/system_status/commuter_rail_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,37 @@ defmodule Dotcom.SystemStatus.CommuterRailTest do
}
end

test "counts a single alert with multiple trips as multiple of that kind" do
# SETUP
now = Dotcom.Utils.DateTime.now()

active_period = [
{now, Timex.shift(now, hours: 1)}
]

[trip_id1, trip_id2] = Faker.Util.sample_uniq(2, fn -> FactoryHelpers.build(:id) end)

stub(Alerts.Repo.Mock, :by_route_ids, fn _, _ ->
[
Factories.Alerts.Alert.build(:alert_for_trips,
active_period: active_period,
effect: :delay,
severity: 3,
trip_ids: [trip_id1, trip_id2]
)
]
end)

# EXERCISE
result = Dotcom.SystemStatus.CommuterRail.commuter_rail_status()

# VERIFY
assert result
|> Map.values()
|> List.first()
|> Kernel.get_in([:alert_counts, :delay, :count]) == 2
end

test "indicates whether or not the route is running service today" do
# SETUP
commuter_rail_id = Faker.Color.fancy_name()
Expand All @@ -170,7 +201,7 @@ defmodule Dotcom.SystemStatus.CommuterRailTest do
]
end)

expect(Schedules.RepoCondensed.Mock, :by_route_ids, fn _ ->
expect(Schedules.RepoCondensed.Mock, :by_route_ids, 2, fn _ ->
[
%Schedules.ScheduleCondensed{
time: Dotcom.Utils.DateTime.now() |> Timex.shift(days: 1)
Expand Down
Loading