Skip to content
Closed
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
72 changes: 37 additions & 35 deletions lib/dotcom/trip_plan/fares.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,48 +25,50 @@ defmodule Dotcom.TripPlan.Fares do
nil
else
transit_legs
|> Stream.with_index()
|> Enum.reduce(0, &add_fares(&1, &2, transit_legs))
|> group_transferable_legs()
|> Enum.map(&group_fare/1)
|> Enum.sum()
end
end

defp add_fares({leg, 0}, 0, _), do: cents_for_leg(leg)

# credo:disable-for-next-line
defp add_fares({leg, leg_index}, total, transit_legs) do
# Look at this transit leg and previous transit leg(s)
two_legs = transit_legs |> Enum.slice(leg_index - 1, 2)
three_legs = transit_legs |> Enum.slice(leg_index - 2, 3)
# If this is part of a free transfer, don't add fare
cond do
Transfer.subway_transfer?(three_legs) ->
total

Transfer.bus_to_subway_transfer?(three_legs) ->
if total == cents_for_leg(List.first(three_legs)),
do: total + 70,
else: total

Transfer.maybe_transfer?(three_legs) ->
total

Transfer.subway_transfer?(two_legs) ->
total

Transfer.subway_after_sl1_from_airport?(two_legs) ->
total

Transfer.bus_to_subway_transfer?(two_legs) ->
total + 70

Transfer.maybe_transfer?(two_legs) ->
total
# Splits legs into consecutive groups that can be transferred between one
# another (per `Transfer.maybe_transfer?/1`). There's no limit on the
# number of transfers within a group.
@spec group_transferable_legs([Leg.t()]) :: [[Leg.t()]]
defp group_transferable_legs([]), do: []

defp group_transferable_legs([first_leg | rest_legs]) do
rest_legs
|> Enum.reduce([[first_leg]], fn leg, [current_group | finished_groups] ->
if Transfer.maybe_transfer?([List.last(current_group), leg]) do
[current_group ++ [leg] | finished_groups]
else
[[leg], current_group | finished_groups]
end
end)
|> Enum.reverse()
end

true ->
total + cents_for_leg(leg)
# A group of transferable legs is charged only the cost of its
# highest-priced leg -- unless it starts with a free SL1 boarding from the
# airport, in which case the whole group remains free.
@spec group_fare([Leg.t()]) :: non_neg_integer()
defp group_fare([first_leg | _] = group) do
if free_airport_boarding?(first_leg) do
0
else
group
|> Enum.map(&cents_for_leg/1)
|> Enum.max()
end
end

defp free_airport_boarding?(%Leg{route: route, from: from}) do
Fares.silver_line_airport_stop?(mbta_id(route), mbta_id(from.stop))
end

defp free_airport_boarding?(_), do: false

# Massport shuttles are free
def cents_for_leg(leg) when agency_name?(leg, "Massport"), do: 0

Expand Down
120 changes: 17 additions & 103 deletions lib/dotcom/trip_plan/transfer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,42 @@ defmodule Dotcom.TripPlan.Transfer do
The MBTA allows transfers between services depending on the fare media used
and the amount paid.

Local Bus, Express Bus, Silver Line, Subway, and Ferry legs can all be
freely transferred between one another (in any combination, for any
number of consecutive transfers), so a chain of such legs is only
charged the cost of its single highest-priced leg.

This logic may be superseded by the upcoming fares work.
"""

import Dotcom.TripPlan.Helpers

alias OpenTripPlannerClient.Schema.{Leg, Place, Route, Stop}
alias OpenTripPlannerClient.Schema.{Leg, Route}

# Paying a single-ride fare for the first may get you a transfer to the second
# (can't be certain, as it depends on media used)!
@single_ride_transfers %{
:bus => [:subway, :bus],
:subway => [:bus],
:bus => [:subway, :bus, :ferry],
:subway => [:bus, :subway, :ferry],
:ferry => [:bus, :subway, :ferry],
:express_bus => [:subway, :bus, :express_bus]
}

# For Local Bus, Express Bus, Silver Line, and/or Subway, transfer up to two times
# and pay only the cost of the highest-priced service.
@multi_ride_transfers [
[:bus, :subway, :bus],
[:bus, :subway, :subway],
[:bus, :bus, :subway],
[:bus, :bus, :bus],
[:subway, :bus, :bus],
[:subway, :bus, :subway],
[:subway, :subway, :bus],
[:subway, :subway, :subway]
]

@doc "Searches a list of legs for evidence of an in-station subway transfer."
@spec subway_transfer?([Leg.t()]) :: boolean
def subway_transfer?([first_leg, next_leg])
when agency_name?(first_leg, "MBTA") and agency_name?(next_leg, "MBTA") do
same_station?(first_leg.to, next_leg.from) and subway?(first_leg.route) and
subway?(next_leg.route)
end

def subway_transfer?([first_leg, next_leg, last_leg])
when agency_name?(first_leg, "MBTA") and agency_name?(next_leg, "MBTA") and
agency_name?(last_leg, "MBTA") do
same_station?(first_leg.to, next_leg.from) and subway?(first_leg.route) and
subway?(next_leg.route) and same_station?(next_leg.to, last_leg.from) and
subway?(last_leg.route)
end

def subway_transfer?([_ | legs]), do: subway_transfer?(legs)

def subway_transfer?(_), do: false

@doc """
Takes a set of legs and returns true if there might be a transfer between the legs, based on the lists in @single_ride_transfers and @multi_ride_transfers.
Takes a set of legs and returns true if there might be a transfer between
every consecutive pair of legs, based on the list in @single_ride_transfers.
Any number of legs may be passed; there's no limit on how many consecutive
transfers can be made.

Exceptions:
- no transfers from bus route to same bus route
- no transfers from a shuttle to any other mode
"""
@spec maybe_transfer?([Leg.t()]) :: boolean
def maybe_transfer?([first_leg, middle_leg, last_leg])
when agency_name?(first_leg, "MBTA") and agency_name?(middle_leg, "MBTA") and
agency_name?(last_leg, "MBTA") do
@multi_ride_transfers
|> Enum.member?(
Enum.map([first_leg.route, middle_leg.route, last_leg.route], &to_fare_atom/1)
)
|> Kernel.and(maybe_transfer?([first_leg, middle_leg]))
|> Kernel.and(maybe_transfer?([middle_leg, last_leg]))
def maybe_transfer?([_first, _second, _third | _] = legs) do
legs
|> Enum.chunk_every(2, 1, :discard)
|> Enum.all?(&maybe_transfer?/1)
end

def maybe_transfer?([from, to]) when agency_name?(from, "MBTA") and agency_name?(to, "MBTA") do
Expand Down Expand Up @@ -103,53 +74,6 @@ defmodule Dotcom.TripPlan.Transfer do
end
end

@doc """
Is there a bus to subway transfer?
"""
def bus_to_subway_transfer?([first, middle, last])
when agency_name?(first, "MBTA") and agency_name?(middle, "MBTA") and
agency_name?(last, "MBTA") do
(bus_to_subway_transfer?([first, middle]) ||
bus_to_subway_transfer?([middle, last])) && !commuter_rail?([first, middle, last])
end

def bus_to_subway_transfer?([from, to])
when agency_name?(from, "MBTA") and agency_name?(to, "MBTA") do
bus?(from.route) && subway?(to.route)
end

def bus_to_subway_transfer?(_), do: false

def commuter_rail?([_, _, _] = legs) do
legs |> Enum.any?(fn leg -> commuter_rail?(leg) end)
end

def commuter_rail?(%{mode: :RAIL}) do
true
end

def commuter_rail?(_) do
false
end

defp same_station?(%Place{stop: %Stop{} = from_stop}, %Place{stop: %Stop{} = to_stop}) do
cond do
is_nil(from_stop.parent_station) or is_nil(to_stop.parent_station) ->
false

from_stop.parent_station == to_stop.parent_station ->
true

true ->
# Check whether this is DTX <-> Park St via. the Winter St. Concourse
stop_id = mbta_id(to_stop.parent_station)
other_stop_id = mbta_id(from_stop.parent_station)
Enum.all?([stop_id, other_stop_id], &Enum.member?(["place-dwnxg", "place-pktrm"], &1))
end
end

defp same_station?(_, _), do: false

defp bus?(route) when route.type == 3 and not mbta_shuttle?(route) do
route_id = mbta_id(route)
not Fares.silver_line_rapid_transit?(route_id)
Expand All @@ -171,14 +95,4 @@ defmodule Dotcom.TripPlan.Transfer do
end

def bus_or_subway?(_), do: false

def subway_after_sl1_from_airport?([first_leg, second_leg])
when agency_name?(first_leg, "MBTA") and agency_name?(second_leg, "MBTA") and
second_leg.route.type in [0, 1] do
from_route_id = mbta_id(first_leg.route)
from_stop_id = mbta_id(first_leg.from)
Fares.silver_line_airport_stop?(from_route_id, from_stop_id)
end

def subway_after_sl1_from_airport?(_), do: false
end
4 changes: 2 additions & 2 deletions lib/fares/fare_info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,10 @@ defmodule Fares.FareInfo do
charlie_card_price: "1.70",
day_reduced_price: "0.85",
week_reduced_price: "10.00",
month_reduced_price: "67.00",
month_reduced_price: "30.00",
day_pass_price: "11.00",
week_pass_price: "22.50",
month_pass_price: "136.00"
month_pass_price: "55.00"
},
%{
mode: :ferry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ defmodule Dotcom.ContentRewriters.LiquidObjects.FareTest do

assert fare_request("commuter_rail:month:reduced") == {:ok, "$30.00 – $209.00"}
assert fare_request("commuter_ferry:month:reduced") == {:ok, "$164.00"}
assert fare_request("express_bus:month:reduced") == {:ok, "$67.00"}
assert fare_request("express_bus:month:reduced") == {:ok, "$30.00"}
end

test "it handles weekend rail fare requests" do
Expand Down
Loading
Loading