Skip to content

Commit

Permalink
🍎🚫💰 Add "Trip Revenue Status" 'revenue' filter to the API (Trip/Vehic…
Browse files Browse the repository at this point in the history
…le/Prediction) (#705)
  • Loading branch information
bfauble authored Dec 21, 2023
1 parent e5e8e55 commit 553dbf5
Show file tree
Hide file tree
Showing 30 changed files with 751 additions and 38 deletions.
31 changes: 28 additions & 3 deletions apps/api_web/lib/api_web/controllers/prediction_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule ApiWeb.PredictionController do
alias ApiWeb.LegacyStops
alias State.Prediction

@filters ~w(stop route trip latitude longitude radius direction_id stop_sequence route_type route_pattern)s
@filters ~w(stop route trip latitude longitude radius direction_id stop_sequence route_type route_pattern revenue)s
@pagination_opts ~w(offset limit order_by)a
@includes ~w(schedule stop route trip vehicle alerts)

Expand Down Expand Up @@ -61,6 +61,7 @@ defmodule ApiWeb.PredictionController do
filter_param(:stop_id, includes_children: true)
filter_param(:id, name: :route)
filter_param(:id, name: :trip)
filter_param(:revenue, desc: "Filter predictions by revenue status.")

parameter("filter[route_pattern]", :query, :string, """
Filter by `/included/{index}/relationships/route_pattern/data/id` of a trip. Multiple `route_pattern_id` #{comma_separated_list()}.
Expand All @@ -84,13 +85,17 @@ defmodule ApiWeb.PredictionController do
route_ids = Params.split_on_comma(filtered_params, "route")
route_pattern_ids = Params.split_on_comma(filtered_params, "route_pattern")
route_types = Params.route_types(filtered_params)
revenue = filtered_params |> Map.get("revenue") |> Params.revenue()

direction_id_matcher =
filtered_params
|> Params.direction_id()
|> direction_id_matcher()

matchers = stop_sequence_matchers(filtered_params, direction_id_matcher)
matchers =
filtered_params
|> build_stop_sequence_matchers(direction_id_matcher)
|> add_revenue_matchers(revenue)

case filtered_params do
%{"route_type" => _} = p when map_size(p) == 1 ->
Expand Down Expand Up @@ -217,7 +222,7 @@ defmodule ApiWeb.PredictionController do
%{direction_id: direction_id}
end

defp stop_sequence_matchers(params, direction_id_matcher) do
defp build_stop_sequence_matchers(params, direction_id_matcher) do
case Params.split_on_comma(params, "stop_sequence") do
[_ | _] = strs ->
for str <- strs,
Expand All @@ -230,6 +235,15 @@ defmodule ApiWeb.PredictionController do
end
end

defp add_revenue_matchers(matchers, :error),
do: add_revenue_matchers(matchers, {:ok, :REVENUE})

defp add_revenue_matchers(matchers, {:ok, revenue_matchers}) do
for revenue_matcher <- List.wrap(revenue_matchers), matcher <- matchers do
Map.put(matcher, :revenue, revenue_matcher)
end
end

def swagger_definitions do
import PhoenixSwagger.JsonApi, except: [page: 1]

Expand Down Expand Up @@ -298,6 +312,17 @@ defmodule ApiWeb.PredictionController do
)

status(:string, "Status of the schedule", example: "Approaching")

revenue_status(
:string,
"""
| Value | Description |
|-----------------|-------------|
| `"REVENUE"` | Indicates that the associated trip is accepting passengers. |
| `"NON_REVENUE"` | Indicates that the associated trip is not accepting passengers. |
""",
example: "REVENUE"
)
end

direction_id_attribute()
Expand Down
21 changes: 20 additions & 1 deletion apps/api_web/lib/api_web/controllers/trip_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule ApiWeb.TripController do

plug(ApiWeb.Plugs.ValidateDate)

@filters ~w(id date direction_id route route_pattern name)s
@filters ~w(id date direction_id route route_pattern name revenue)s
@pagination_opts ~w(offset limit order_by)a
@includes ~w(route vehicle service shape predictions route_pattern stops)

Expand All @@ -33,6 +33,7 @@ defmodule ApiWeb.TripController do
filter_param(:date, description: "Filter by trips on a particular date")
filter_param(:direction_id)
filter_param(:id, name: :route)
filter_param(:revenue, desc: "Filter trips by revenue status.")

parameter(
"filter[route_pattern]",
Expand Down Expand Up @@ -144,6 +145,13 @@ defmodule ApiWeb.TripController do
end
end

defp do_format_filter({"revenue", revenue}) do
case Params.revenue(revenue) do
:error -> []
{:ok, val} -> %{revenue: val}
end
end

swagger_path :show do
get(path(__MODULE__, :show))

Expand Down Expand Up @@ -239,6 +247,17 @@ defmodule ApiWeb.TripController do
""",
example: 1
)

revenue_status(
:string,
"""
| Value | Description |
|-----------------|-------------|
| `"REVENUE"` | Indicates that the associated trip is accepting passengers. |
| `"NON_REVENUE"` | Indicates that the associated trip is not accepting passengers. |
""",
example: "REVENUE"
)
end

direction_id_attribute()
Expand Down
21 changes: 20 additions & 1 deletion apps/api_web/lib/api_web/controllers/vehicle_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule ApiWeb.VehicleController do
use ApiWeb.Web, :api_controller
alias State.Vehicle

@filters ~w(trip route direction_id id label route_type)s
@filters ~w(trip route direction_id id label route_type revenue)s
@pagination_opts ~w(offset limit order_by)a
@includes ~w(trip stop route)

Expand Down Expand Up @@ -87,6 +87,7 @@ defmodule ApiWeb.VehicleController do

filter_param(:direction_id, desc: "Only used if `filter[route]` is also present.")
filter_param(:route_type)
filter_param(:revenue, desc: "Filter vehicles by revenue status.")

consumes("application/vnd.api+json")
produces("application/vnd.api+json")
Expand Down Expand Up @@ -176,6 +177,13 @@ defmodule ApiWeb.VehicleController do
end
end

defp do_format_filter({"revenue", revenue}) do
case Params.revenue(revenue) do
:error -> []
{:ok, val} -> %{revenue: val}
end
end

defp do_format_filter(_), do: []

def swagger_definitions do
Expand Down Expand Up @@ -263,6 +271,17 @@ defmodule ApiWeb.VehicleController do
}
]
)

revenue_status(
:string,
"""
| Value | Description |
|-----------------|-------------|
| `"REVENUE"` | Indicates that the associated trip is accepting passengers. |
| `"NON_REVENUE"` | Indicates that the associated trip is not accepting passengers. |
""",
example: "REVENUE"
)
end

direction_id_attribute()
Expand Down
18 changes: 18 additions & 0 deletions apps/api_web/lib/api_web/params.ex
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,24 @@ defmodule ApiWeb.Params do
def canonical("false"), do: false
def canonical(_), do: nil

@doc """
Parse revenue filter to valid params
"""
def revenue(values) when is_binary(values) do
values
|> split_on_comma()
|> Enum.reduce_while({:ok, []}, fn
"REVENUE", {:ok, acc} -> {:cont, {:ok, [:REVENUE | acc]}}
"NON_REVENUE", {:ok, acc} -> {:cont, {:ok, [:NON_REVENUE | acc]}}
_, _ -> {:halt, :error}
end)
|> revenue()
end

def revenue({:ok, []}), do: :error
def revenue(nil), do: :error
def revenue(val), do: val

@doc """
Parses and integer value from params.
Expand Down
17 changes: 17 additions & 0 deletions apps/api_web/lib/api_web/swagger_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,23 @@ defmodule ApiWeb.SwaggerHelpers do
)
end

def filter_param(path_object, :revenue, opts) do
Path.parameter(
path_object,
"filter[revenue]",
:query,
:string,
"""
#{opts[:desc]}
Revenue status indicates whether or not the vehicle is accepting passengers.
When filter is not included, the default behavior is to filter by `revenue=REVENUE`.
Multiple `revenue` types #{comma_separated_list()}.
""",
enum: ["NON_REVENUE", "REVENUE", "NON_REVENUE,REVENUE"]
)
end

def page(resource) do
resource
|> JsonApi.page()
Expand Down
10 changes: 8 additions & 2 deletions apps/api_web/lib/api_web/views/prediction_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ defmodule ApiWeb.PredictionView do
:schedule_relationship,
:status,
:stop_sequence,
:track
:track,
:revenue
])

def preload(predictions, conn, include_opts) do
Expand Down Expand Up @@ -43,7 +44,8 @@ defmodule ApiWeb.PredictionView do
direction_id: p.direction_id,
schedule_relationship: schedule_relationship(p),
status: p.status,
stop_sequence: p.stop_sequence
stop_sequence: p.stop_sequence,
revenue: revenue(p)
}

add_legacy_attributes(attributes, p, conn.assigns.api_version)
Expand Down Expand Up @@ -204,6 +206,10 @@ defmodule ApiWeb.PredictionView do
|> String.upcase()
end

def revenue(%{revenue: atom}) do
Atom.to_string(atom)
end

def format_time(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
def format_time(nil), do: nil
end
22 changes: 21 additions & 1 deletion apps/api_web/lib/api_web/views/trip_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,21 @@ defmodule ApiWeb.TripView do

def trip_location(trip, conn), do: trip_path(conn, :show, trip.id)

attributes([:name, :headsign, :direction_id, :wheelchair_accessible, :block_id, :bikes_allowed])
attributes([
:name,
:headsign,
:direction_id,
:wheelchair_accessible,
:block_id,
:bikes_allowed,
:revenue
])

def attributes(%Model.Trip{} = t, conn) do
t
|> super(conn)
|> encode_revenue(t)
end

has_one(
:route,
Expand Down Expand Up @@ -153,4 +167,10 @@ defmodule ApiWeb.TripView do
end
end)
end

defp encode_revenue(attributes, %{revenue: atom}) do
string_val = Atom.to_string(atom)

Map.put(attributes, :revenue, string_val)
end
end
10 changes: 9 additions & 1 deletion apps/api_web/lib/api_web/views/vehicle_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ defmodule ApiWeb.VehicleView do
:current_stop_sequence,
:updated_at,
:occupancy_status,
:carriages
:carriages,
:revenue
])

has_one(
Expand Down Expand Up @@ -45,6 +46,7 @@ defmodule ApiWeb.VehicleView do
|> super(conn)
|> backwards_compatible_attributes(vehicle, conn.assigns.api_version)
|> encode_carriages()
|> encode_revenue(vehicle)
end

for status <- ~w(in_transit_to incoming_at stopped_at)a do
Expand Down Expand Up @@ -101,4 +103,10 @@ defmodule ApiWeb.VehicleView do
occupancy_percentage: carriage.occupancy_percentage
}
end

defp encode_revenue(attributes, %{revenue: atom}) do
string_val = Atom.to_string(atom)

Map.put(attributes, :revenue, string_val)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,70 @@ defmodule ApiWeb.PredictionControllerTest do
assert [_ | _] = response["included"]
end

test "can filter by revenue status", %{conn: conn} do
route1 = %Model.Route{
id: "route1",
type: 2
}

route2 = %Model.Route{
id: "route2",
type: 2
}

State.Route.new_state([route1, route2])

p1 = %Prediction{trip_id: "trip1", route_id: "route1", revenue: :REVENUE}
p2 = %Prediction{trip_id: "trip2", route_id: "route1", revenue: :NON_REVENUE}
p3 = %Prediction{trip_id: "trip3", route_id: "route2", revenue: :REVENUE}
p4 = %Prediction{trip_id: "trip4", route_id: "route2", revenue: :NON_REVENUE}

:ok = State.Prediction.new_state([p1, p2, p3, p4])
State.Trip.Added.last_updated()

result = index_data(conn, %{"revenue" => "REVENUE,NON_REVENUE"})
assert Enum.sort_by(result, & &1.trip_id) == []

result = index_data(conn, %{"revenue" => "REVENUE,NON_REVENUE", "route" => "route1"})
assert Enum.sort_by(result, & &1.trip_id) == [p1, p2]

result = index_data(conn, %{"revenue" => "NON_REVENUE,REVENUE", "route" => "route1"})
assert Enum.sort_by(result, & &1.trip_id) == [p1, p2]

result = index_data(conn, %{"revenue" => "REVENUE,NON_REVENUE", "route" => "route1,route2"})
assert Enum.sort_by(result, & &1.trip_id) == [p1, p2, p3, p4]

result = index_data(conn, %{"revenue" => "REVENUE,NON_REVENUE", "route" => "route2"})
assert Enum.sort_by(result, & &1.trip_id) == [p3, p4]

result = index_data(conn, %{"revenue" => "REVENUE"})
assert Enum.sort_by(result, & &1.trip_id) == []

result = index_data(conn, %{"revenue" => "REVENUE", "route" => "route1"})
assert Enum.sort_by(result, & &1.trip_id) == [p1]

result = index_data(conn, %{"revenue" => "REVENUE", "route" => "route1,route2"})
assert Enum.sort_by(result, & &1.trip_id) == [p1, p3]

result = index_data(conn, %{"revenue" => "invalid", "route" => "route1,route2"})
assert Enum.sort_by(result, & &1.trip_id) == [p1, p3]

result = index_data(conn, %{"revenue" => "REVENUE", "route" => "route2"})
assert Enum.sort_by(result, & &1.trip_id) == [p3]

result = index_data(conn, %{"revenue" => "NON_REVENUE"})
assert Enum.sort_by(result, & &1.trip_id) == []

result = index_data(conn, %{"revenue" => "NON_REVENUE", "route" => "route1"})
assert Enum.sort_by(result, & &1.trip_id) == [p2]

result = index_data(conn, %{"revenue" => "NON_REVENUE", "route" => "route1,route2"})
assert Enum.sort_by(result, & &1.trip_id) == [p2, p4]

result = index_data(conn, %{"revenue" => "NON_REVENUE", "route" => "route2"})
assert Enum.sort_by(result, & &1.trip_id) == [p4]
end

test "conforms to swagger response", %{swagger_schema: schema, conn: conn} do
response =
get(
Expand Down
Loading

0 comments on commit 553dbf5

Please sign in to comment.