import { ElevationProfile } from "@opentripplanner/types"; import { calculateTncFares, descope, getCompanyFromLeg, getDisplayedStopCode, getElevationProfile, getItineraryCost, getLegCost, getLegRouteLongName, getLegRouteName, getLegRouteShortName, isFlex, isTransit, legElevationAtDistance, mapOldElevationComponentToNew } from "../itinerary"; import bikeRentalItinerary from "./__mocks__/bike-rental-itinerary.json"; import tncItinerary from "./__mocks__/tnc-itinerary.json"; import fareProductItinerary from "./__mocks__/fare-products-itinerary.json"; import complexItinerary from "./__mocks__/complex-fares.json"; import tripleItinerary from "./__mocks__/three-transfer-itinerary.json"; import flexItinerary from "../../../itinerary-body/src/__mocks__/itineraries/flex-itinerary.json"; import faresv2Itinerary from "../../../itinerary-body/src/__mocks__/itineraries/fares-v2-fare-components.json"; import legFareProdcuts from "../../../itinerary-body/src/__mocks__/itineraries/leg-fare-products.json"; global.structuredClone = val => JSON.parse(JSON.stringify(val)); const basePlace = { lat: 0, lon: 0, name: "stop" }; describe("util > itinerary", () => { describe("descope", () => { it("should return the descope'd part for standard scoped strings", () => { expect(descope("agency:cash")).toBe("cash"); expect(descope("foo:bar")).toBe("bar"); }); it("should return the input if it is not scoped", () => { expect(descope("cash")).toBe("cash"); expect(descope("")).toBe(""); }); it("should only return the segment after the first ':'", () => { expect(descope("aaaa:bbbb:cccc")).toBe("bbbb:cccc"); expect(descope(":value")).toBe("value"); expect(descope("value:")).toBe(""); expect(descope(":")).toBe(""); }); it("should handle null and undefined", () => { expect(descope(null)).toBeNull(); expect(descope(undefined)).toBeUndefined(); }); }); describe("getElevationProfile", () => { it("should work with REST legacy data and GraphQL elevationProfile", () => { const legacyOutput = getElevationProfile( bikeRentalItinerary.legs[1].steps ); const graphqlOutput = getElevationProfile( bikeRentalItinerary.legs[1].steps.map(step => ({ ...step, elevationProfile: step.elevation.map(mapOldElevationComponentToNew), elevation: null })) ); expect(legacyOutput).toEqual(graphqlOutput); }); }); describe("isFlex", () => { it("should detect flex if present", () => { fareProductItinerary.legs.forEach(leg => expect(isFlex(leg)).toBe(false)); tncItinerary.legs.forEach(leg => expect(isFlex(leg)).toBe(false)); expect(isFlex(flexItinerary.legs[0])).toBe(false); expect(isFlex(flexItinerary.legs[1])).toBe(false); expect(isFlex(flexItinerary.legs[2])).toBe(false); expect(isFlex(flexItinerary.legs[3])).toBe(false); expect(isFlex(flexItinerary.legs[4])).toBe(false); expect(isFlex(flexItinerary.legs[5])).toBe(true); expect(isFlex(flexItinerary.legs[6])).toBe(false); // Does not exist expect(isFlex(flexItinerary.legs[7])).toBe(false); // Does not exist }); }); describe("isTransit", () => { it("should work", () => { expect(isTransit("CAR")).toBeFalsy(); expect(isTransit("BUS")).toBeTruthy(); }); }); describe("getCompanyFromLeg", () => { it("should return company for bike rental leg", () => { const company = getCompanyFromLeg(bikeRentalItinerary.legs[1]); expect(company).toEqual("GBFS"); }); it("should return company for TNC leg", () => { const company = getCompanyFromLeg(tncItinerary.legs[0]); expect(company).toEqual("uber"); }); }); describe("calculateTncFares", () => { it("should return the correct amounts and currency for an itinerary with TNC", () => { const fareResult = calculateTncFares(tncItinerary, true); expect(fareResult.currencyCode).toEqual("USD"); expect(fareResult.maxTNCFare).toEqual(38); expect(fareResult.minTNCFare).toEqual(34); }); }); describe("getDisplayedStopCode", () => { it("should return the stop code if one is provided", () => { const place = { ...basePlace, stopCode: "code123", stopId: "xagency:id123" }; expect(getDisplayedStopCode(place)).toEqual("code123"); const stop = { ...basePlace, code: "code123", id: "xagency:id123" }; expect(getDisplayedStopCode(stop)).toEqual("code123"); }); it("should return undefined if id is present and no stopCode is provided", () => { const place = { ...basePlace, stopId: "xagency:id123" }; expect(getDisplayedStopCode(place)).toBeFalsy(); const stop = { ...basePlace, id: "xagency:id123" }; expect(getDisplayedStopCode(stop)).toBeFalsy(); }); it("should return undefined if stopId is null (and no stopCode is provided)", () => { expect(getDisplayedStopCode(basePlace)).toBeFalsy(); }); }); describe("getLegCost", () => { const freeLeg = { fareProducts: [ { id: "testId", product: { medium: null, name: "rideCost", price: { amount: 0, currency: "USD" }, riderCategory: null } } ] }; const leg = { fareProducts: [ { id: "testId", product: { medium: null, name: "rideCost", price: { amount: 200, currency: "USD" }, riderCategory: null } }, { id: "testId", product: { medium: null, name: "transfer-from-other-vehicle", price: { amount: 50, currency: "USD" }, riderCategory: null } } ] }; const complicatedLeg = { fareProducts: [ { id: "testId", product: { medium: { id: "agency:cash" }, name: "transfer-from-other-vehicle", price: { amount: 50, currency: "USD" }, riderCategory: { id: "agency:child" } } }, { id: "testId", product: { medium: "cash", name: "rideCost", price: { amount: 200, currency: "USD" }, riderCategory: "adult" } } ] }; const halfLeg = { fareProducts: [ { id: "testId", product: { medium: { id: "agency:cash" }, name: "rideCost", price: { amount: 30, currency: "USD" }, riderCategory: null } }, { id: "testId2", product: { medium: { id: "agency:digital" }, name: "rideCost", price: { amount: 20, currency: "USD" }, riderCategory: null } } ] }; it("should return the total cost for a leg", () => { const result = getLegCost(leg, null, null); expect(result.price).toEqual({ amount: 50, currency: "USD" }); }); it("should return the 0 cost for a free leg", () => { const result = getLegCost(freeLeg, null, null); expect(result.price).toEqual({ amount: 0, currency: "USD" }); }); it("should return the correct cost for a leg with multiple types of fares", () => { const result = getLegCost(complicatedLeg, "cash", "child"); expect(result.price).toEqual({ amount: 50, currency: "USD" }); }); it("should return the correct cost for a leg with no rider category", () => { const result = getLegCost(halfLeg, "cash", null); expect(result.price).toEqual({ amount: 30, currency: "USD" }); }); it("should return undefined if no fare products exist on the leg", () => { const emptyleg = {}; const result = getLegCost(emptyleg, "cash", "regular"); expect(result.price).toBeUndefined(); }); it("should return undefined if the keys are invalid", () => { const result = getLegCost(leg, "invalidkey", "invalidkey"); expect(result.price).toBeUndefined(); }); }); describe("getItineraryCost", () => { it("should calculate the total cost of an itinerary", () => { const result = getItineraryCost( fareProductItinerary.legs, "cash", "regular" ); expect(result?.amount).toEqual(5.75); expect(result?.currency).toEqual({ code: "USD", digits: 2 }); }); it("should calculate the total cost of an itinerary using fares v2", () => { const result = getItineraryCost(faresv2Itinerary.legs, "3", "ADULT"); expect(result?.amount).toEqual(2.8); const complexResult = getItineraryCost( faresv2Itinerary.legs, ["0", "0"], ["ADULT", "ADULT"] ); expect(complexResult?.amount).toEqual(2.8 * 2); }); it("should calculate the total cost of an itinerary with multiple v2 fares & transfers", () => { const result = getItineraryCost(complexItinerary.legs, "0", "ADULT"); expect(result.amount).toEqual(3.0); }); it("should calculate the total cost of an itinerary with three different fares v2 transfers with different rider categories", () => { const result = getItineraryCost( tripleItinerary.legs, ["0", "0"], ["ADULT", null] ); expect(result?.amount).toEqual(11.55); }); it("should calculate the individual leg cost of a fares v2 legs", () => { const firstLegResult = getLegCost(faresv2Itinerary.legs[1], "3", "ADULT"); expect(firstLegResult.price?.amount).toEqual(2); const secondLegResult = getLegCost( faresv2Itinerary.legs[3], "3", "ADULT" ); expect(secondLegResult.price?.amount).toEqual(0.8); // Check for alternate product expect(firstLegResult.alternateFareProducts?.[0]?.price?.amount).toEqual( undefined ); expect(secondLegResult.alternateFareProducts?.[0]?.price?.amount).toEqual( 2.8 ); }); it("should calculate the total cost of an itinerary with null ids", () => { const result = getItineraryCost(fareProductItinerary.legs, null, null); expect(result.amount).toEqual(2.75); expect(result.currency).toEqual({ code: "USD", digits: 2 }); const resultWithDashEnabled = getItineraryCost( legFareProdcuts.legs, "orca", "youth", true ); expect(resultWithDashEnabled).toEqual(undefined); }); it("should return undefined when the keys are invalid", () => { const result = getItineraryCost( fareProductItinerary.legs, "invalidkey", "invalidkey" ); expect(result).toBeUndefined(); }); it("should exclude duplicated fareProduct uses based on ID", () => { const legsWithDuplicatedProducts = [...fareProductItinerary.legs]; legsWithDuplicatedProducts[3].fareProducts = legsWithDuplicatedProducts[1].fareProducts; const result = getItineraryCost(fareProductItinerary.legs, null, null); expect(result.amount).toEqual(2.75); expect(result.currency).toEqual({ code: "USD", digits: 2 }); }); }); describe("getLegRouteShortName", () => { it("should extract a route short name from an OTP1 leg", () => { expect(getLegRouteShortName({ route: "15" })).toBe("15"); expect(getLegRouteShortName({ route: "15", routeShortName: "31" })).toBe( "31" ); }); it("should extract a route short name from an OTP2 leg", () => { expect( getLegRouteShortName({ route: { id: "id15", shortName: "15" }, routeShortName: "31" }) ).toBe("15"); }); }); describe("getLegRouteLongName", () => { it("should extract a route long name from an OTP1 leg", () => { expect(getLegRouteLongName({ route: "15" })).toBeUndefined(); expect( getLegRouteLongName({ route: "15", routeLongName: "Candler Road" }) ).toBe("Candler Road"); }); it("should extract a route long name from an OTP2 leg", () => { expect( getLegRouteLongName({ route: { id: "id15", longName: "15" }, routeLongName: "31" }) ).toBe("15"); }); }); describe("getLegRouteName", () => { it("should extract a route name from an OTP1 leg where a route short name is provided", () => { expect( getLegRouteName({ route: "15", routeShortName: "31", routeLongName: "Route 31" }) ).toBe("31"); }); it("should extract a route name from an OTP1 leg where a route short name is not provided", () => { expect( getLegRouteName({ routeLongName: "Route 31" }) ).toBe("Route 31"); }); it("should extract a route name from an OTP2 leg where a route short name is provided", () => { expect( getLegRouteName({ route: { id: "id15", longName: "15", shortName: "10" }, routeLongName: "31" }) ).toBe("10"); }); it("should extract a route name from an OTP2 leg where a route short name is not provided", () => { expect( getLegRouteName({ route: { id: "id15", longName: "15" }, routeLongName: "31", routeShortName: "31" }) ).toBe("15"); }); }); describe("legElevationAtDistance", () => { it("should interpolate elevation within a segment", () => { const points: any = [ [0, 100], [10, 110], [20, 120] ]; // halfway between 100 and 110 expect(legElevationAtDistance(points, 5)).toBe(105); expect(legElevationAtDistance(points, 15)).toBe(115); }); it("should return start elevation at distance 0", () => { const points: any = [ [0, 42], [10, 52] ]; expect(legElevationAtDistance(points, 0)).toBe(42); }); it("should return end elevation at the segment boundary", () => { const points: any = [ [0, 100], [10, 110], [20, 120] ]; expect(legElevationAtDistance(points, 10)).toBe(110); expect(legElevationAtDistance(points, 20)).toBe(120); }); it("should return undefined when distance is before the profile", () => { const points: any = [ [0, 100], [10, 110], [20, 120] ]; expect(legElevationAtDistance(points, -1)).toBeUndefined(); expect(legElevationAtDistance(points, 21)).toBeUndefined(); }); it("should return undefined when first point is not at zero", () => { const points: any = [ [5, 100], [15, 110] ]; expect(legElevationAtDistance(points, 4)).toBeUndefined(); }); }); });