diff --git a/apps/api_web/lib/api_web/controllers/schedule_controller.ex b/apps/api_web/lib/api_web/controllers/schedule_controller.ex index ba642fb4..56582a2c 100644 --- a/apps/api_web/lib/api_web/controllers/schedule_controller.ex +++ b/apps/api_web/lib/api_web/controllers/schedule_controller.ex @@ -18,7 +18,7 @@ defmodule ApiWeb.ScheduleController do @filters ~w(date direction_id max_time min_time route stop stop_sequence route_type trip)s @pagination_opts ~w(offset limit order_by)a - @includes ~w(stop trip prediction route) + @includes ~w(stop trip prediction route added_routes) def state_module, do: State.Schedule @@ -368,6 +368,7 @@ defmodule ApiWeb.ScheduleController do relationship(:trip) relationship(:stop) relationship(:prediction) + relationship(:added_routes, type: :has_many) end, Schedules: page(:ScheduleResource) } diff --git a/apps/api_web/lib/api_web/views/schedule_view.ex b/apps/api_web/lib/api_web/views/schedule_view.ex index dbede210..4d908250 100644 --- a/apps/api_web/lib/api_web/views/schedule_view.ex +++ b/apps/api_web/lib/api_web/views/schedule_view.ex @@ -1,7 +1,7 @@ defmodule ApiWeb.ScheduleView do use ApiWeb.Web, :api_view alias ApiWeb.Plugs.Deadline - alias JaSerializer.Relationship.HasOne + alias JaSerializer.Relationship.{HasMany, HasOne} def relationships(_, _) do %{ @@ -15,6 +15,13 @@ defmodule ApiWeb.ScheduleView do serializer: ApiWeb.PredictionView, identifiers: :when_included, include: false + }, + added_routes: %HasMany{ + type: :route, + name: :added_routes, + data: :added_routes, + serializer: ApiWeb.RouteView, + include: true } } end @@ -92,6 +99,10 @@ defmodule ApiWeb.ScheduleView do State.Prediction.prediction_for(schedule, date) end + def added_routes(%{added_route_ids: added_route_ids}, conn) do + optional_relationship("added_route", added_route_ids, &State.Route.by_ids/1, conn) + end + defp format_time(seconds, conn) when is_integer(seconds) do conn.assigns |> case do diff --git a/apps/api_web/test/api_web/views/schedule_view_test.exs b/apps/api_web/test/api_web/views/schedule_view_test.exs index 1930da8e..1a78e19d 100644 --- a/apps/api_web/test/api_web/views/schedule_view_test.exs +++ b/apps/api_web/test/api_web/views/schedule_view_test.exs @@ -18,7 +18,8 @@ defmodule ApiWeb.ScheduleViewTest do pickup_type: 2, drop_off_type: 3, timepoint?: true, - stop_headsign: "headsign" + stop_headsign: "headsign", + added_route_ids: [] } test "renders the dates properly", %{conn: conn} do @@ -155,4 +156,31 @@ defmodule ApiWeb.ScheduleViewTest do refute prediction end end + + describe "added_routes" do + test "empty if not present", %{conn: conn} do + conn = assign(conn, :date, ~D[2026-03-31]) + rendered = render(ApiWeb.ScheduleView, "index.json-api", data: @schedule, conn: conn) + assert %{"added_routes" => %{"data" => []}} = rendered["data"]["relationships"] + end + + test "populated if present", %{conn: conn} do + conn = assign(conn, :date, ~D[2026-03-31]) + + rendered = + render(ApiWeb.ScheduleView, "index.json-api", + data: %{@schedule | added_route_ids: ["route1", "route2"]}, + conn: conn + ) + + assert %{ + "added_routes" => %{ + "data" => [ + %{"type" => "route", "id" => "route1"}, + %{"type" => "route", "id" => "route2"} + ] + } + } = rendered["data"]["relationships"] + end + end end diff --git a/apps/model/lib/model/schedule.ex b/apps/model/lib/model/schedule.ex index e57bf591..f4555a03 100644 --- a/apps/model/lib/model/schedule.ex +++ b/apps/model/lib/model/schedule.ex @@ -23,7 +23,8 @@ defmodule Model.Schedule do :route_id, :direction_id, :service_id, - :timepoint? + :timepoint?, + added_route_ids: [] ] @typedoc """ @@ -73,6 +74,7 @@ defmodule Model.Schedule do [GTFS `trips.txt` `route_id`](https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md#tripstxt) * `:service_id` - The service that `trip_id` is following to determine when it is active. See [GTFS `trips.txt` `service_id`](https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md#tripstxxt) + * `:added_route_ids` - Routes that this trip has been added to via [`multi_route_trips.txt`](https://github.com/mbta/gtfs-documentation/blob/master/reference/gtfs.md#multi_route_tripstxt) * `:stop_id` - The stop being arrived at and departed from. See [GTFS `stop_times.txt` `stop_id`](https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md#stop_timestxt) * `:stop_sequence` - The sequence the `stop_id` is arrived at during the `trip_id`. See diff --git a/apps/model/lib/model/trip.ex b/apps/model/lib/model/trip.ex index 0c88f907..52f8f4d5 100644 --- a/apps/model/lib/model/trip.ex +++ b/apps/model/lib/model/trip.ex @@ -17,7 +17,8 @@ defmodule Model.Trip do :route_type, :bikes_allowed, :route_pattern_id, - revenue: :REVENUE + revenue: :REVENUE, + added_route_ids: [] ] @type id :: String.t() @@ -36,7 +37,8 @@ defmodule Model.Trip do alternate_route: boolean | nil, bikes_allowed: 0..2, route_pattern_id: Model.RoutePattern.id() | nil, - revenue: :REVENUE | :NON_REVENUE + revenue: :REVENUE | :NON_REVENUE, + added_route_ids: [Model.Route.id()] } @doc """ diff --git a/apps/parse/lib/parse/stop_times.ex b/apps/parse/lib/parse/stop_times.ex index 93e03d43..5b800cb2 100644 --- a/apps/parse/lib/parse/stop_times.ex +++ b/apps/parse/lib/parse/stop_times.ex @@ -37,7 +37,8 @@ defmodule Parse.StopTimes do stop_headsign: optional_copy(row["stop_headsign"]), pickup_type: pick_drop_type(row["pickup_type"]), drop_off_type: pick_drop_type(row["drop_off_type"]), - timepoint?: row["timepoint"] != "0" + timepoint?: row["timepoint"] != "0", + added_route_ids: [] } end @@ -72,6 +73,7 @@ defmodule Parse.StopTimes do &%{ &1 | route_id: trip.route_id, + added_route_ids: trip.added_route_ids, direction_id: trip.direction_id, service_id: trip.service_id } diff --git a/apps/state/lib/state/trip.ex b/apps/state/lib/state/trip.ex index b17a371a..09f50898 100644 --- a/apps/state/lib/state/trip.ex +++ b/apps/state/lib/state/trip.ex @@ -154,7 +154,7 @@ defmodule State.Trip do %{trip | route_id: route_id, alternate_route: true} end - new_trip = %{trip | alternate_route: false} + new_trip = %{trip | alternate_route: false, added_route_ids: alternate_routes} [new_trip | new_alternates] end diff --git a/apps/state/test/state/schedule_test.exs b/apps/state/test/state/schedule_test.exs index 4e0f1f06..4985edfd 100644 --- a/apps/state/test/state/schedule_test.exs +++ b/apps/state/test/state/schedule_test.exs @@ -177,7 +177,7 @@ defmodule State.ScheduleTest do ] schedules = [ - @schedule + %{@schedule | added_route_ids: [other_route_id]} ] State.Route.new_state(routes) @@ -185,13 +185,10 @@ defmodule State.ScheduleTest do Schedule.new_state(schedules) State.RoutesPatternsAtStop.update!() - assert Enum.sort(Schedule.filter_by(%{stops: [@stop.id], routes: [@route.id]})) == [ - @schedule - ] + assert Enum.sort(Schedule.filter_by(%{stops: [@stop.id], routes: [@route.id]})) == schedules - assert Enum.sort(Schedule.filter_by(%{stops: [@stop.id], routes: [other_route_id]})) == [ - @schedule - ] + assert Enum.sort(Schedule.filter_by(%{stops: [@stop.id], routes: [other_route_id]})) == + schedules end test "filters on :stops and :routes" do diff --git a/apps/state/test/state/trip_test.exs b/apps/state/test/state/trip_test.exs index abd93212..7a1b6a35 100644 --- a/apps/state/test/state/trip_test.exs +++ b/apps/state/test/state/trip_test.exs @@ -209,9 +209,20 @@ defmodule State.TripTest do trips: [trip] }) - assert [%Model.Trip{alternate_route: false, id: id, route_id: "746"}] = by_route_id("746") - assert [%Model.Trip{alternate_route: true, id: ^id, route_id: "741"}] = by_route_id("741") - assert [%Model.Trip{alternate_route: true, id: ^id, route_id: "742"}] = by_route_id("742") + assert [ + %Model.Trip{ + alternate_route: false, + id: id, + route_id: "746", + added_route_ids: ["742", "741"] + } + ] = by_route_id("746") + + assert [%Model.Trip{alternate_route: true, id: ^id, route_id: "741", added_route_ids: []}] = + by_route_id("741") + + assert [%Model.Trip{alternate_route: true, id: ^id, route_id: "742", added_route_ids: []}] = + by_route_id("742") end test "creates alternate for CR-Haverhill" do @@ -226,16 +237,29 @@ defmodule State.TripTest do new_state(%{ multi_route_trips: [ - %Model.MultiRouteTrip{added_route_id: "CR-Haverhill", trip_id: trip_id}, %Model.MultiRouteTrip{added_route_id: "CR-Lowell", trip_id: trip_id} ], trips: [trip] }) - assert [%Model.Trip{alternate_route: false, id: ^trip_id, route_id: "CR-Haverhill"}] = + assert [ + %Model.Trip{ + alternate_route: false, + id: ^trip_id, + route_id: "CR-Haverhill", + added_route_ids: ["CR-Lowell"] + } + ] = by_route_id("CR-Haverhill") - assert [%Model.Trip{alternate_route: true, id: ^trip_id, route_id: "CR-Lowell"}] = + assert [ + %Model.Trip{ + alternate_route: true, + id: ^trip_id, + route_id: "CR-Lowell", + added_route_ids: [] + } + ] = by_route_id("CR-Lowell") end end