Skip to content

Commit

Permalink
Merge pull request #945 from opentripplanner/new-fare-product-impl
Browse files Browse the repository at this point in the history
New fare product impl
  • Loading branch information
daniel-heppner-ibigroup authored Jul 28, 2023
2 parents cf2dd8b + f4f2e64 commit e0d45ad
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 95 deletions.
7 changes: 1 addition & 6 deletions example-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ itinerary:
# electronicRegular: "SmartCard Fare"
# student: "Student Fare"
# One fare will always be shown by default
#defaultFareKey: electronicRegular
defaultFareType: { mediumId: null, riderCategoryId: null }

# The following settings must be set to these values to use the new
# "Metro" UI. The settings can be used without the Metro UI, but
Expand Down Expand Up @@ -544,11 +544,6 @@ itinerary:

### Localization section to provide language/locale settings
#localization:
# # An ambient currency should be defined here (defaults to USD).
# # In some components such as DefaultItinerary, we display a cost element
# # that falls back to $0.00 (or its equivalent in the configured ambient currency
# # and in the user-selected locale) if no fare or currency info is available.
# currency: 'USD'
# defaultLocale: 'en-US'

### If using OTP Middleware, you can define the optional phone number options below.
Expand Down
1 change: 1 addition & 0 deletions i18n/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ common:
"yes": "Yes"
itineraryDescriptions:
calories: "{calories, number} Cal"
fareUnknown: No fare information
noItineraryToDisplay: No itinerary to display.
relativeCo2: |
{co2} {isMore, select, true {more} other {less} } CO₂ than driving alone
Expand Down
1 change: 1 addition & 0 deletions i18n/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ common:
"yes": Oui
itineraryDescriptions:
calories: "{calories, number} kcal"
fareUnknown: Tarif inconnu
noItineraryToDisplay: Aucun trajet à afficher.
relativeCo2: |
{co2} de CO₂ en {isMore, select, true {plus} other {moins} } qu'en voiture
Expand Down
3 changes: 0 additions & 3 deletions i18n/i18n-exceptions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
"ignoredIds": [
"otpUi.TripDetails.transitFareUnknown"
],
"groups": {
"components.OTP2ErrorRenderer.*.body": [
"LOCATION_NOT_FOUND",
Expand Down
5 changes: 4 additions & 1 deletion lib/components/narrative/connected-trip-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ const mapStateToProps = (state) => {
const { co2, itinerary } = state.otp.config
return {
co2Config: co2,
defaultFareType: itinerary?.defaultFareType || undefined,
defaultFareType: itinerary?.defaultFareType || {
mediumId: null,
riderCategoryId: null
},
displayCalories:
typeof itinerary?.displayCalories === 'boolean'
? itinerary?.displayCalories
Expand Down
38 changes: 19 additions & 19 deletions lib/components/narrative/default/default-itinerary.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,23 @@ const ITINERARY_ATTRIBUTES = [
{
id: 'cost',
order: 2,
render: (itinerary, options, defaultFareKey = 'regular') => {
const fareInCents = getTotalFare(
itinerary,
options.configCosts,
defaultFareKey
render: (itinerary, options, defaultFareType) => {
const fare = getTotalFare(itinerary, options.configCosts, defaultFareType)
if (fare === null || fare === undefined || fare < 0) {
return (
<FormattedMessage id="common.itineraryDescriptions.fareUnknown" />
)
}

const { currency } = coreUtils.itinerary.getItineraryCost(
itinerary.legs,
defaultFareType.mediumId,
defaultFareType.riderCategoryId
)
const fareCurrency = itinerary.fare?.fare?.regular?.currency?.currencyCode
const fare = fareInCents === null ? null : fareInCents / 100
if (fare === null || fare < 0)
return <FormattedMessage id="otpUi.TripDetails.transitFareUnknown" />
return (
<FormattedNumber
// Currency from itinerary fare or from config.
currency={fareCurrency || options.currency}
currency={currency}
currencyDisplay="narrowSymbol"
// eslint-disable-next-line react/style-prop-object
style="currency"
Expand Down Expand Up @@ -241,8 +244,7 @@ class DefaultItinerary extends NarrativeItinerary {
active,
co2Config,
configCosts,
currency,
defaultFareKey,
defaultFareType,
expanded,
intl,
itinerary,
Expand Down Expand Up @@ -278,15 +280,14 @@ class DefaultItinerary extends NarrativeItinerary {
const itineraryAttributeOptions = {
co2Config,
configCosts,
currency,
LegIcon
}

const renderItineraryAttributes = (attribute) => {
return attribute.render(
itinerary,
itineraryAttributeOptions,
defaultFareKey
defaultFareType
)
}

Expand Down Expand Up @@ -391,11 +392,10 @@ const mapStateToProps = (state, ownProps) => {
state.otp.config.accessibilityScore?.gradationMap,
co2Config: state.otp.config.co2,
configCosts: state.otp.config.itinerary?.costs,
// The configured (ambient) currency is needed for rendering the cost
// of itineraries whether they include a fare or not, in which case
// we show $0.00 or its equivalent in the configured currency and selected locale.
currency: state.otp.config.localization?.currency || 'USD',
defaultFareKey: state.otp.config.itinerary?.defaultFareKey
defaultFareType: state.otp.config.itinerary?.defaultFareType || {
mediumId: null,
riderCategoryId: null
}
}
}

Expand Down
14 changes: 6 additions & 8 deletions lib/components/narrative/line-itin/itin-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const ShortName = styled.div<{ leg: Leg }>`

type Props = {
currency?: string
defaultFareKey: string
defaultFareType: string
itinerary: Itinerary
onClick: () => void
}
Expand All @@ -90,17 +90,16 @@ export class ItinerarySummary extends Component<Props> {
}

render(): JSX.Element {
const { currency, defaultFareKey, itinerary } = this.props
const { defaultFareType, itinerary } = this.props
const { LegIcon } = this.context

const { fareCurrency, maxTNCFare, minTNCFare, transitFare } = getFare(
itinerary,
defaultFareKey,
currency
defaultFareType
)

const minTotalFare = minTNCFare * 100 + transitFare
const maxTotalFare = maxTNCFare * 100 + transitFare
const minTotalFare = minTNCFare * 100 + (transitFare || 0)
const maxTotalFare = maxTNCFare * 100 + (transitFare || 0)

const { endTime, startTime } = itinerary

Expand Down Expand Up @@ -239,8 +238,7 @@ function getRouteColorForBadge(leg: Leg): string {

const mapStateToProps = (state: any) => {
return {
currency: state.otp.config.localization?.currency || 'USD',
defaultFareKey: state.otp.config.itinerary?.defaultFareKey
defaultFareType: state.otp.config.itinerary?.defaultFareType
}
}

Expand Down
53 changes: 25 additions & 28 deletions lib/components/narrative/metro/metro-itinerary.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { AccessibilityRating } from '@opentripplanner/itinerary-body'
import { connect } from 'react-redux'
import { FareProductSelector, Itinerary, Leg } from '@opentripplanner/types'
import {
FormattedMessage,
FormattedNumber,
injectIntl,
IntlShape
} from 'react-intl'
import { Itinerary, Leg } from '@opentripplanner/types'
import { Leaf } from '@styled-icons/fa-solid/Leaf'
import React from 'react'
import styled, { keyframes } from 'styled-components'
Expand Down Expand Up @@ -184,6 +184,7 @@ type Props = {
LegIcon: React.ReactNode
accessibilityScoreGradationMap: { [value: number]: string }
active: boolean
defaultFareType: FareProductSelector
/** This is true when there is only one itinerary being shown and the itinerary-body is visible */
expanded: boolean
intl: IntlShape
Expand Down Expand Up @@ -249,8 +250,7 @@ class MetroItinerary extends NarrativeItinerary {
active,
arrivesAt,
co2Config,
currency,
defaultFareKey,
defaultFareType,
expanded,
intl,
itinerary,
Expand All @@ -268,11 +268,7 @@ class MetroItinerary extends NarrativeItinerary {
const { isCallAhead, isContinuousDropoff, isFlexItinerary, phone } =
getFlexAttirbutes(itinerary)

const { fareCurrency, transitFare } = getFare(
itinerary,
defaultFareKey,
currency
)
const { fareCurrency, transitFare } = getFare(itinerary, defaultFareType)

const roundedCo2VsBaseline = Math.round(itinerary.co2VsBaseline * 100)
const emissionsNote = !mini &&
Expand Down Expand Up @@ -389,21 +385,26 @@ class MetroItinerary extends NarrativeItinerary {
)
)}
</SecondaryInfo>
<SecondaryInfo>
{transitFare === null || transitFare < 0 ? (
<FormattedMessage id="otpUi.TripDetails.transitFareUnknown" />
) : (
// TODO: re-implement TNC fares for metro UI?
<FormattedNumber
currency={fareCurrency}
currencyDisplay="narrowSymbol"
// This isn't a "real" style prop
// eslint-disable-next-line react/style-prop-object
style="currency"
value={transitFare / 100}
/>
)}
</SecondaryInfo>
{
// Hide the fare information entirely if the defaultFareType isn't specified.
<SecondaryInfo>
{transitFare === null ||
transitFare === undefined ||
transitFare < 0 ? (
<FormattedMessage id="common.itineraryDescriptions.fareUnknown" />
) : (
// TODO: re-implement TNC fares for metro UI?
<FormattedNumber
currency={fareCurrency}
currencyDisplay="narrowSymbol"
// This isn't a "real" style prop
// eslint-disable-next-line react/style-prop-object
style="currency"
value={transitFare}
/>
)}
</SecondaryInfo>
}
<SecondaryInfo>
<FormattedMessage
id="components.MetroUI.timeWalking"
Expand Down Expand Up @@ -476,11 +477,7 @@ const mapStateToProps = (state: any, ownProps: Props) => {
arrivesAt: state.otp.filter.sort.type === 'ARRIVALTIME',
co2Config: state.otp.config.co2,
configCosts: state.otp.config.itinerary?.costs,
// The configured (ambient) currency is needed for rendering the cost
// of itineraries whether they include a fare or not, in which case
// we show $0.00 or its equivalent in the configured currency and selected locale.
currency: state.otp.config.localization?.currency || 'USD',
defaultFareKey: state.otp.config.itinerary?.defaultFareKey,
defaultFareType: state.otp.config.itinerary?.defaultFareType,
enableDot: !state.otp.config.itinerary?.disableMetroSeperatorDot,
// @ts-expect-error TODO: type activeSearch
pending: activeSearch ? Boolean(activeSearch.pending) : false,
Expand Down
27 changes: 18 additions & 9 deletions lib/util/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,22 @@ function getDriveTime(itinerary) {
/**
* Parses OTP itinerary fare object and returns fares along with overridden currency
*/
export function getFare(itinerary, defaultFareKey, currency) {
export function getFare(itinerary, defaultFareType) {
const { maxTNCFare, minTNCFare } =
coreUtils.itinerary.calculateTncFares(itinerary)

const transitFares = itinerary.fare?.fare
const transitFare =
(transitFares?.[defaultFareKey] || transitFares?.regular)?.cents || null
const fareCurrency = transitFare?.currency?.symbol || currency
const itineraryCost = coreUtils.itinerary.getItineraryCost(
itinerary?.legs,
defaultFareType?.mediumId || null,
defaultFareType?.riderCategoryId || null
)

return { fareCurrency, maxTNCFare, minTNCFare, transitFare }
return {
fareCurrency: itineraryCost?.currency.code,
maxTNCFare,
minTNCFare,
transitFare: itineraryCost?.amount
}
}

/**
Expand All @@ -78,10 +84,13 @@ const DEFAULT_COSTS = {
export function getTotalFare(
itinerary,
configCosts = {},
defaultFareKey = 'regular'
defaultFareType = { mediumId: null, riderCategoryId: null }
) {
// Get TNC fares.
const { maxTNCFare, transitFare } = getFare(itinerary, defaultFareKey)
const { fareCurrency, maxTNCFare, transitFare } = getFare(
itinerary,
defaultFareType
)
// Start with default cost values.
const costs = DEFAULT_COSTS
// If config contains values to override defaults, apply those.
Expand Down Expand Up @@ -146,7 +155,7 @@ function calculateItineraryCost(itinerary, config = {}) {
getTotalFare(
itinerary,
config.itinerary?.costs,
config.itinerary?.defaultFareKey
config.itinerary?.defaultFareType
) *
weights.fareFactor +
itinerary.duration * weights.durationFactor +
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@
"@bugsnag/js": "^7.17.0",
"@bugsnag/plugin-react": "^7.17.0",
"@opentripplanner/base-map": "^3.0.13",
"@opentripplanner/core-utils": "^9.0.0-alpha.34",
"@opentripplanner/core-utils": "^9.0.3",
"@opentripplanner/endpoints-overlay": "^2.0.7",
"@opentripplanner/from-to-location-picker": "^2.1.7",
"@opentripplanner/geocoder": "^1.4.1",
"@opentripplanner/humanize-distance": "^1.2.0",
"@opentripplanner/icons": "^2.0.3",
"@opentripplanner/itinerary-body": "^4.2.0-alpha.3",
"@opentripplanner/itinerary-body": "^5.0.1",
"@opentripplanner/location-field": "^2.0.6",
"@opentripplanner/location-icon": "^1.4.1",
"@opentripplanner/map-popup": "^2.0.4",
Expand All @@ -56,7 +56,7 @@
"@opentripplanner/stops-overlay": "^5.1.0",
"@opentripplanner/transit-vehicle-overlay": "^4.0.4",
"@opentripplanner/transitive-overlay": "^3.0.13",
"@opentripplanner/trip-details": "^5.0.0-alpha.6",
"@opentripplanner/trip-details": "^5.0.2",
"@opentripplanner/trip-form": "^3.1.1",
"@opentripplanner/trip-viewer-overlay": "^2.0.5",
"@opentripplanner/vehicle-rental-overlay": "^2.1.1",
Expand Down Expand Up @@ -131,7 +131,7 @@
"@craco/craco": "^6.3.0",
"@jackwilsdon/craco-use-babelrc": "^1.0.0",
"@opentripplanner/scripts": "^1.2.0",
"@opentripplanner/types": "^6.0.0-alpha.8",
"@opentripplanner/types": "^6.0.0",
"@percy/cli": "^1.20.3",
"@percy/puppeteer": "^2.0.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
Expand Down
4 changes: 1 addition & 3 deletions percy/percy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,7 @@ test('OTP-RR', async () => {

// Open Trip Viewer
await page.waitForTimeout(2000)
const [tripViewerButton] = await page.$x(
"//button[contains(., 'Trip Viewer')]"
)
const [tripViewerButton] = await page.$x("//a[contains(., 'Trip Viewer')]")

// If the trip viewer button didn't appear, perhaps we need to click the itinerary again
if (!tripViewerButton) {
Expand Down
Loading

0 comments on commit e0d45ad

Please sign in to comment.