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
66 changes: 40 additions & 26 deletions packages/core-utils/src/itinerary.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import polyline from "@mapbox/polyline";
import {
AppliedFareProduct,
Company,
Config,
Currency,
ElevationProfile,
ElevationProfileComponent,
FareProduct,
FlexBookingInfo,
ItineraryOnlyLegsRequired,
LatLngArray,
Expand Down Expand Up @@ -617,6 +618,11 @@ export const descope = (item: string): string => item?.split(":")?.[1];

export type ExtendedMoney = Money & { originalAmount?: number };

export const zeroDollars = (currency: Currency): Money => ({
amount: 0,
currency
});

/**
* Extracts useful data from the fare products on a leg, such as the leg cost and transfer info.
* @param leg Leg with fare products (must have used getLegsWithFares)
Expand All @@ -630,13 +636,13 @@ export function getLegCost(
riderCategoryId: string | null,
seenFareIds?: string[]
): {
price?: ExtendedMoney;
productUseId?: string;
alternateFareProducts?: FareProduct[];
alternateFareProducts?: AppliedFareProduct[];
appliedFareProduct?: AppliedFareProduct;
isDependent?: boolean;
price?: Money;
productUseId?: string;
} {
if (!leg.fareProducts) return { price: undefined };

const relevantFareProducts = leg.fareProducts
.filter(({ product }) => {
// riderCategory and medium can be specifically defined as null to handle
Expand All @@ -652,32 +658,38 @@ export function getLegCost(
descope(product?.medium?.id) || product?.medium?.id || null;
return (
productRiderCategoryId === riderCategoryId &&
productMediaId === mediumId
productMediaId === mediumId &&
// Make sure there's a price
// Some fare products don't have a price at all.
product?.price
);
})
.map(fare => {
const clonedFare = structuredClone(fare);
// If we've seen the fare ID already, it's a free transfer
if (seenFareIds?.indexOf(fare.id) > -1) {
(clonedFare.product.price as ExtendedMoney).originalAmount =
fare.product.price.amount;
clonedFare.product.price.amount = 0;
}
return clonedFare;
const alreadySeen = seenFareIds?.indexOf(fare.id) > -1;
const { currency } = fare.product.price;
return {
id: fare.id,
product: {
...fare.product,
legPrice: alreadySeen ? zeroDollars(currency) : fare.product.price
} as AppliedFareProduct
};
})
.sort((a, b) => a.product?.price?.amount - b.product?.price?.amount);
.sort((a, b) => a.product?.legPrice?.amount - b.product?.legPrice?.amount);

// Return the cheapest, but include other matches as well
const cheapestRelevantFareProduct = relevantFareProducts[0];

// TODO: return one object here instead of dumbing it down?
return {
alternateFareProducts: relevantFareProducts.splice(1).map(fp => fp.product),
price: cheapestRelevantFareProduct?.product?.price,
productUseId: cheapestRelevantFareProduct?.id,
appliedFareProduct: cheapestRelevantFareProduct?.product,
isDependent:
// eslint-disable-next-line no-underscore-dangle
cheapestRelevantFareProduct?.product.__typename === "DependentFareProduct"
cheapestRelevantFareProduct?.product.__typename ===
"DependentFareProduct",
price: cheapestRelevantFareProduct?.product.legPrice,
productUseId: cheapestRelevantFareProduct?.id
};
}

Expand All @@ -686,29 +698,31 @@ export function getLegCost(
* @param legs Itinerary legs with fare products (must have used getLegsWithFares)
* @param category Rider category (youth, regular, senior)
* @param container Fare container (cash, electronic)
* @param seenFareIds List of fare product IDs that have already been seen on prev legs.
* @returns Money object for the total itinerary cost.
*/
export function getItineraryCost(
legs: Leg[],
mediumId: string | null,
riderCategoryId: string | null
): Money | undefined {
const legCosts = legs
const legCostsObj = legs
// Only legs with fares (no walking legs)
.filter(leg => leg.fareProducts?.length > 0)
// Get the leg cost object of each leg
.map(leg => getLegCost(leg, mediumId, riderCategoryId))
.filter(cost => cost.price !== undefined)
.filter(cost => cost.appliedFareProduct?.legPrice !== undefined)
// Filter out duplicate use IDs
// One fare product can be used on multiple legs,
// and we don't want to count it more than once.
.reduce<{ productUseId: string; price: Money }[]>((prev, cur) => {
if (!prev.some(p => p.productUseId === cur.productUseId)) {
prev.push({ productUseId: cur.productUseId, price: cur.price });
// Use an object keyed by productUseId to deduplicate, then extract prices
.reduce<{ [productUseId: string]: Money }>((acc, cur) => {
if (cur.productUseId && acc[cur.productUseId] === undefined) {
acc[cur.productUseId] = cur.appliedFareProduct?.legPrice;
}
return prev;
}, [])
.map(productUse => productUse.price);
return acc;
}, {});
const legCosts = Object.values(legCostsObj);

if (legCosts.length === 0) return undefined;
// Calculate the total
Expand Down
18 changes: 6 additions & 12 deletions packages/trip-details/src/__snapshots__/TripDetails.story.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -543,11 +543,8 @@ exports[`TripDetails FaresV2TableStory smoke-test 1`] = `
<td style="text-align: right;">
$2.75
</td>
<td style="text-align: center;">
-
<span class="styled__InvisibleA11yLabel-sc-6q2ok2-0 elLeWK">
No fare information for this leg
</span>
<td style="text-align: right;">
$2.75
</td>
<td style="text-align: center;">
-
Expand All @@ -560,7 +557,7 @@ exports[`TripDetails FaresV2TableStory smoke-test 1`] = `
</td>
<td style="text-align: right;">
<em>
$5.50
$8.25
</em>
</td>
</tr>
Expand Down Expand Up @@ -621,11 +618,8 @@ exports[`TripDetails FaresV2TableStory smoke-test 1`] = `
<td style="text-align: right;">
$2.75
</td>
<td style="text-align: center;">
-
<span class="styled__InvisibleA11yLabel-sc-6q2ok2-0 elLeWK">
No fare information for this leg
</span>
<td style="text-align: right;">
$2.75
</td>
<td style="text-align: center;">
-
Expand All @@ -638,7 +632,7 @@ exports[`TripDetails FaresV2TableStory smoke-test 1`] = `
</td>
<td style="text-align: right;">
<em>
$5.50
$8.25
</em>
</td>
</tr>
Expand Down
22 changes: 11 additions & 11 deletions packages/trip-details/src/components/fares-v2-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { FormattedMessage, useIntl } from "react-intl";
import styled from "styled-components";
import { Transfer } from "@styled-icons/boxicons-regular/Transfer";

import { ExtendedMoney } from "@opentripplanner/core-utils/lib/itinerary";
import { renderFare } from "../utils";
import { InvisibleA11yLabel } from "../styled";

Expand Down Expand Up @@ -144,14 +143,16 @@ const FaresV2Table = ({
const {
alternateFareProducts,
isDependent,
price,
appliedFareProduct,
productUseId
} = getLegCost(
leg,
descope(medium.id) || null,
descope(rider.id) || null,
Array.from(productUseIds)
);
const legPrice = appliedFareProduct?.legPrice;

productUseIds.add(productUseId);

// Only consider alternateFareProducts if current product is dependent
Expand All @@ -160,11 +161,9 @@ const FaresV2Table = ({

// Calculate pre-tranfer amount either via alternate fare or fare-id matching (price.originalAmount)
const originalAmount =
price?.originalAmount ||
(dependentAlternateFareProducts?.[0]?.price?.amount ||
(dependentAlternateFareProducts?.[0]?.price as ExtendedMoney)
?.originalAmount) - price?.amount ||
null;
dependentAlternateFareProducts?.[0]?.price.amount -
legPrice?.amount ||
appliedFareProduct?.price.amount - legPrice?.amount;

const newCell = (
<>
Expand All @@ -182,7 +181,7 @@ const FaresV2Table = ({
</th>
)}
<td
style={{ textAlign: price ? "right" : "center" }}
style={{ textAlign: legPrice ? "right" : "center" }}
title={
!Number.isNaN(originalAmount) &&
originalAmount > 0 &&
Expand All @@ -195,7 +194,7 @@ const FaresV2Table = ({
},
{
transferAmount: intl.formatNumber(originalAmount, {
currency: price?.currency?.code,
currency: legPrice?.currency?.code,
currencyDisplay: "narrowSymbol",
style: "currency"
})
Expand All @@ -206,8 +205,9 @@ const FaresV2Table = ({
{!Number.isNaN(originalAmount) &&
originalAmount > 0 &&
index > 0 && <TransferIcon size={16} />}
{price
? renderFare(price?.currency?.code, price?.amount)
{/* Leg Price will always be defined if the fare product has a price */}
{legPrice

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a comment that legprice exists when original amount exists. There's probably a lot of cleanup possible overall but we can put it as a todo for now

? renderFare(legPrice?.currency?.code, legPrice?.amount)
: FailDash}
</td>
</>
Expand Down
15 changes: 14 additions & 1 deletion packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,11 @@ type TemporaryTNCPriceType = {
amount: number;
};

export type Currency = {
code: string;
digits: number;
};

/**
* Describes the cost of an itinerary leg.
*/
Expand Down Expand Up @@ -810,13 +815,21 @@ export type FareProduct = {
name: string;
};
name: string;
price: Money;
// Fare products may not have a price if they don't implement a FareProduct subclass.
price?: Money;
riderCategory?: {
id: string;
name: string;
};
};

/**
* This fare product is designed to represent the fare product applied to a leg.
*/
export type AppliedFareProduct = FareProduct & {
legPrice: Money;
};

export type FareProductSelector = {
mediumId?: string;
riderCategoryId?: string;
Expand Down