@opentripplanner/core-utils 9.0.0-alpha.8 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,15 +66,23 @@
66
66
  "rentedCar": false,
67
67
  "rentedVehicle": false,
68
68
  "hailedCar": true,
69
- "tncData": {
70
- "company": "UBER",
71
- "currency": "USD",
72
- "travelDuration": 300,
73
- "maxCost": 10,
74
- "minCost": 9,
75
- "productId": "a6eef2e1-c99a-436f-bde9-fefb9181c0b0",
76
- "displayName": "UberX",
77
- "estimatedArrival": 240
69
+ "rideHailingEstimate": {
70
+ "provider": {
71
+ "id": "uber"
72
+ },
73
+ "arrival": "PT4M",
74
+ "minPrice": {
75
+ "currency": {
76
+ "code": "USD"
77
+ },
78
+ "amount": 17
79
+ },
80
+ "maxPrice": {
81
+ "currency": {
82
+ "code": "USD"
83
+ },
84
+ "amount": 19
85
+ }
78
86
  },
79
87
  "duration": 180,
80
88
  "transitLeg": false,
@@ -1045,15 +1053,23 @@
1045
1053
  "rentedCar": false,
1046
1054
  "rentedVehicle": false,
1047
1055
  "hailedCar": true,
1048
- "tncData": {
1049
- "company": "UBER",
1050
- "currency": "USD",
1051
- "travelDuration": 300,
1052
- "maxCost": 9,
1053
- "minCost": 8,
1054
- "productId": "a6eef2e1-c99a-436f-bde9-fefb9181c0b0",
1055
- "displayName": "UberX",
1056
- "estimatedArrival": 240
1056
+ "rideHailingEstimate": {
1057
+ "provider": {
1058
+ "id": "uber"
1059
+ },
1060
+ "arrival": "PT4M",
1061
+ "minPrice": {
1062
+ "currency": {
1063
+ "code": "USD"
1064
+ },
1065
+ "amount": 17
1066
+ },
1067
+ "maxPrice": {
1068
+ "currency": {
1069
+ "code": "USD"
1070
+ },
1071
+ "amount": 19
1072
+ }
1057
1073
  },
1058
1074
  "duration": 341,
1059
1075
  "transitLeg": false,
@@ -2,12 +2,14 @@ import {
2
2
  calculateTncFares,
3
3
  getCompanyFromLeg,
4
4
  getDisplayedStopId,
5
- getTransitFare,
5
+ getItineraryCost,
6
+ getLegCost,
6
7
  isTransit
7
8
  } from "../itinerary";
8
9
 
9
10
  const bikeRentalItinerary = require("./__mocks__/bike-rental-itinerary.json");
10
11
  const tncItinerary = require("./__mocks__/tnc-itinerary.json");
12
+ const fareProductItinerary = require("./__mocks__/fare-products-itinerary.json");
11
13
 
12
14
  const basePlace = {
13
15
  lat: 0,
@@ -31,31 +33,7 @@ describe("util > itinerary", () => {
31
33
 
32
34
  it("should return company for TNC leg", () => {
33
35
  const company = getCompanyFromLeg(tncItinerary.legs[0]);
34
- expect(company).toEqual("UBER");
35
- });
36
- });
37
-
38
- describe("getTransitFare", () => {
39
- it("should return defaults with missing fare", () => {
40
- const { transitFare } = getTransitFare(null);
41
- // transit fare value should be zero
42
- expect(transitFare).toMatchSnapshot();
43
- });
44
-
45
- it("should work with valid fare component", () => {
46
- const fareComponent = {
47
- currency: {
48
- currency: "USD",
49
- defaultFractionDigits: 2,
50
- currencyCode: "USD",
51
- symbol: "$"
52
- },
53
- cents: 575
54
- };
55
- const { currencyCode, transitFare } = getTransitFare(fareComponent);
56
- expect(currencyCode).toEqual(fareComponent.currency.currencyCode);
57
- // Snapshot tests
58
- expect(transitFare).toMatchSnapshot();
36
+ expect(company).toEqual("uber");
59
37
  });
60
38
  });
61
39
 
@@ -63,8 +41,8 @@ describe("util > itinerary", () => {
63
41
  it("should return the correct amounts and currency for an itinerary with TNC", () => {
64
42
  const fareResult = calculateTncFares(tncItinerary, true);
65
43
  expect(fareResult.currencyCode).toEqual("USD");
66
- expect(fareResult.maxTNCFare).toEqual(19);
67
- expect(fareResult.minTNCFare).toEqual(17);
44
+ expect(fareResult.maxTNCFare).toEqual(38);
45
+ expect(fareResult.minTNCFare).toEqual(34);
68
46
  });
69
47
  });
70
48
 
@@ -111,4 +89,72 @@ describe("util > itinerary", () => {
111
89
  expect(getDisplayedStopId(basePlace)).toBeFalsy();
112
90
  });
113
91
  });
92
+
93
+ describe("getLegCost", () => {
94
+ const leg = {
95
+ fareProducts: [
96
+ {
97
+ id: "testId",
98
+ product: {
99
+ medium: { id: "cash" },
100
+ name: "rideCost",
101
+ price: { amount: 200, currency: "USD" },
102
+ riderCategory: { id: "regular" }
103
+ }
104
+ },
105
+ {
106
+ id: "testId",
107
+ product: {
108
+ medium: { id: "cash" },
109
+ name: "transfer",
110
+ price: { amount: 50, currency: "USD" },
111
+ riderCategory: { id: "regular" }
112
+ }
113
+ }
114
+ ]
115
+ };
116
+ it("should return the total cost for a leg", () => {
117
+ const result = getLegCost(leg, "cash", "regular");
118
+ expect(result.price).toEqual({ amount: 200, currency: "USD" });
119
+ });
120
+
121
+ it("should return the transfer discount amount if a transfer was used", () => {
122
+ const result = getLegCost(leg, "cash", "regular");
123
+ expect(result.price).toEqual({ amount: 200, currency: "USD" });
124
+ expect(result.transferAmount).toEqual({ amount: 50, currency: "USD" });
125
+ });
126
+
127
+ it("should return undefined if no fare products exist on the leg", () => {
128
+ const emptyleg = {};
129
+ const result = getLegCost(emptyleg, "cash", "regular");
130
+ expect(result.price).toBeUndefined();
131
+ });
132
+ it("should return undefined if the keys are invalid", () => {
133
+ const result = getLegCost(leg, "invalidkey", "invalidkey");
134
+ expect(result.price).toBeUndefined();
135
+ });
136
+ });
137
+
138
+ describe("getItineraryCost", () => {
139
+ it("should calculate the total cost of an itinerary", () => {
140
+ const result = getItineraryCost(
141
+ fareProductItinerary.legs,
142
+ "orca:cash",
143
+ "orca:regular"
144
+ );
145
+ expect(result.amount).toEqual(5.75);
146
+ expect(result.currency).toEqual({
147
+ code: "USD",
148
+ digits: 2
149
+ });
150
+ });
151
+ it("should return undefined when the keys are invalid", () => {
152
+ const result = getItineraryCost(
153
+ fareProductItinerary.legs,
154
+ "invalidkey",
155
+ "invalidkey"
156
+ );
157
+ expect(result).toBeUndefined();
158
+ });
159
+ });
114
160
  });
@@ -148,6 +148,10 @@ describe("query-gen", () => {
148
148
  ["WALK"]
149
149
  ]
150
150
  );
151
+ expectModes(
152
+ ["BICYCLE_RENT", "BICYCLE", "WALK"],
153
+ [["BICYCLE_RENT", "WALK"], ["BICYCLE"], ["WALK"]]
154
+ );
151
155
  expectModes(
152
156
  ["SCOOTER_RENT", "BICYCLE_RENT", "TRANSIT", "WALK"],
153
157
  [
@@ -159,6 +163,26 @@ describe("query-gen", () => {
159
163
  ["WALK"]
160
164
  ]
161
165
  );
166
+
167
+ expectModes(
168
+ ["CAR_HAIL", "TRANSIT"],
169
+ [["TRANSIT"], ["CAR_HAIL", "TRANSIT"]]
170
+ );
171
+ expectModes(
172
+ ["CAR_HAIL", "BICYCLE_RENT", "TRANSIT"],
173
+ [["TRANSIT"], ["CAR_HAIL", "TRANSIT"], ["BICYCLE_RENT", "TRANSIT"]]
174
+ );
175
+ expectModes(
176
+ ["CAR_HAIL", "BICYCLE_RENT", "TRANSIT", "WALK"],
177
+ [
178
+ ["TRANSIT"],
179
+ ["CAR_HAIL", "TRANSIT"],
180
+ ["CAR_HAIL", "WALK"],
181
+ ["BICYCLE_RENT", "TRANSIT"],
182
+ ["BICYCLE_RENT", "WALK"],
183
+ ["WALK"]
184
+ ]
185
+ );
162
186
  expectModes(
163
187
  ["FLEX", "TRANSIT", "WALK"],
164
188
  [["TRANSIT"], ["FLEX", "TRANSIT"], ["FLEX", "WALK"], ["WALK"]]
package/src/itinerary.ts CHANGED
@@ -4,7 +4,6 @@ import {
4
4
  Config,
5
5
  ElevationProfile,
6
6
  FlexBookingInfo,
7
- Itinerary,
8
7
  ItineraryOnlyLegsRequired,
9
8
  LatLngArray,
10
9
  Leg,
@@ -79,6 +78,10 @@ export function legDropoffRequiresAdvanceBooking(leg: Leg): boolean {
79
78
  return isAdvanceBookingRequired(leg.dropOffBookingInfo);
80
79
  }
81
80
 
81
+ export function isRideshareLeg(leg: Leg): boolean {
82
+ return !!leg.rideHailingEstimate?.provider?.id;
83
+ }
84
+
82
85
  export function isWalk(mode: string): boolean {
83
86
  if (!mode) return false;
84
87
 
@@ -194,16 +197,26 @@ export function toSentenceCase(str: string): string {
194
197
  */
195
198
  export function getCompanyFromLeg(leg: Leg): string {
196
199
  if (!leg) return null;
197
- const { from, mode, rentedBike, rentedCar, rentedVehicle, tncData } = leg;
200
+ const {
201
+ from,
202
+ mode,
203
+ rentedBike,
204
+ rentedCar,
205
+ rentedVehicle,
206
+ rideHailingEstimate
207
+ } = leg;
198
208
  if (mode === "CAR" && rentedCar) {
199
209
  return from.networks[0];
200
210
  }
201
- if (mode === "CAR" && tncData) {
202
- return tncData.company;
211
+ if (mode === "CAR" && rideHailingEstimate) {
212
+ return rideHailingEstimate.provider.id;
203
213
  }
204
214
  if (mode === "BICYCLE" && rentedBike && from.networks) {
205
215
  return from.networks[0];
206
216
  }
217
+ if (from.rentalVehicle) {
218
+ return from.rentalVehicle.network;
219
+ }
207
220
  if (
208
221
  (mode === "MICROMOBILITY" || mode === "SCOOTER") &&
209
222
  rentedVehicle &&
@@ -441,15 +454,15 @@ export function calculateTncFares(
441
454
  itinerary: ItineraryOnlyLegsRequired
442
455
  ): TncFare {
443
456
  return itinerary.legs
444
- .filter(leg => leg.mode === "CAR" && leg.hailedCar && leg.tncData)
457
+ .filter(leg => leg.mode === "CAR" && leg.rideHailingEstimate)
445
458
  .reduce(
446
- ({ maxTNCFare, minTNCFare }, { tncData }) => {
447
- const { currency, maxCost, minCost } = tncData;
459
+ ({ maxTNCFare, minTNCFare }, { rideHailingEstimate }) => {
460
+ const { minPrice, maxPrice } = rideHailingEstimate;
448
461
  return {
449
462
  // Assumes a single currency for entire itinerary.
450
- currencyCode: currency,
451
- maxTNCFare: maxTNCFare + maxCost,
452
- minTNCFare: minTNCFare + minCost
463
+ currencyCode: minPrice.currency.code,
464
+ maxTNCFare: maxTNCFare + maxPrice.amount,
465
+ minTNCFare: minTNCFare + minPrice.amount
453
466
  };
454
467
  },
455
468
  {
@@ -460,27 +473,6 @@ export function calculateTncFares(
460
473
  );
461
474
  }
462
475
 
463
- /**
464
- * For a given fare component (either total fare or component parts), returns
465
- * an object with the fare value (in cents).
466
- */
467
- export function getTransitFare(
468
- fareComponent: Money
469
- ): {
470
- currencyCode: string;
471
- transitFare: number;
472
- } {
473
- return fareComponent
474
- ? {
475
- currencyCode: fareComponent.currency.currencyCode,
476
- transitFare: fareComponent.cents
477
- }
478
- : {
479
- currencyCode: "USD",
480
- transitFare: 0
481
- };
482
- }
483
-
484
476
  /**
485
477
  * Sources:
486
478
  * - https://www.itf-oecd.org/sites/default/files/docs/environmental-performance-new-mobility.pdf
@@ -563,20 +555,6 @@ export function getDisplayedStopId(placeOrStop: Place | Stop): string {
563
555
  return stopCode || stopId?.split(":")[1] || stopId;
564
556
  }
565
557
 
566
- /**
567
- * Adds the fare product info to each leg in an itinerary, from the itinerary's fare property
568
- * @param itinerary Itinerary with legProducts in fare object
569
- * @returns Itinerary with legs that have fare products attached to them
570
- */
571
- export function getLegsWithFares(itinerary: Itinerary): Leg[] {
572
- return itinerary.legs.map((leg, i) => ({
573
- ...leg,
574
- fareProducts: itinerary.fare?.legProducts
575
- ?.filter(lp => lp?.legIndices?.includes(i))
576
- .flatMap(lp => lp.products)
577
- }));
578
- }
579
-
580
558
  /**
581
559
  * Extracts useful data from the fare products on a leg, such as the leg cost and transfer info.
582
560
  * @param leg Leg with fare products (must have used getLegsWithFares)
@@ -586,23 +564,26 @@ export function getLegsWithFares(itinerary: Itinerary): Leg[] {
586
564
  */
587
565
  export function getLegCost(
588
566
  leg: Leg,
589
- category: string,
590
- container: string
591
- ): { price?: Money; transferAmount?: number } {
567
+ mediumId: string,
568
+ riderCategoryId: string
569
+ ): { price?: Money; transferAmount?: Money | undefined } {
592
570
  if (!leg.fareProducts) return { price: undefined };
593
-
594
- const relevantFareProducts = leg.fareProducts.filter(
595
- fp => fp.category.name === category && fp.container.name === container
596
- );
597
- const totalCost = relevantFareProducts.find(fp => fp.name === "rideCost")
598
- ?.amount;
571
+ const relevantFareProducts = leg.fareProducts.filter(({ product }) => {
572
+ return (
573
+ product.riderCategory.id === riderCategoryId &&
574
+ product.medium.id === mediumId
575
+ );
576
+ });
577
+ const totalCost = relevantFareProducts.find(
578
+ fp => fp.product.name === "rideCost"
579
+ )?.product?.price;
599
580
  const transferFareProduct = relevantFareProducts.find(
600
- fp => fp.name === "transfer"
581
+ fp => fp.product.name === "transfer"
601
582
  );
602
583
 
603
584
  return {
604
585
  price: totalCost,
605
- transferAmount: transferFareProduct?.amount?.cents
586
+ transferAmount: transferFareProduct?.product.price
606
587
  };
607
588
  }
608
589
 
@@ -615,17 +596,64 @@ export function getLegCost(
615
596
  */
616
597
  export function getItineraryCost(
617
598
  legs: Leg[],
618
- category: string,
619
- container: string
620
- ): Money {
621
- return legs
622
- .filter(leg => !!leg.fareProducts)
623
- .map(leg => getLegCost(leg, category, container).price)
624
- .reduce<Money>(
625
- (prev, cur) => ({
626
- cents: prev.cents + cur?.cents || 0,
627
- currency: prev.currency ?? cur?.currency
628
- }),
629
- { cents: 0, currency: null }
630
- );
599
+ mediumId: string,
600
+ riderCategoryId: string
601
+ ): Money | undefined {
602
+ const legCosts = legs
603
+ .filter(leg => leg.fareProducts?.length > 0)
604
+ .map(leg => getLegCost(leg, mediumId, riderCategoryId).price)
605
+ .filter(cost => cost !== undefined);
606
+ if (legCosts.length === 0) return undefined;
607
+ return legCosts.reduce<Money>(
608
+ (prev, cur) => ({
609
+ amount: prev.amount + cur?.amount || 0,
610
+ currency: prev.currency ?? cur?.currency
611
+ }),
612
+ { amount: 0, currency: null }
613
+ );
631
614
  }
615
+
616
+ const pickupDropoffTypeToOtp1 = otp2Type => {
617
+ switch (otp2Type) {
618
+ case "COORDINATE_WITH_DRIVER":
619
+ return "coordinateWithDriver";
620
+ case "CALL_AGENCY":
621
+ return "mustPhone";
622
+ case "SCHEDULED":
623
+ return "scheduled";
624
+ case "NONE":
625
+ return "none";
626
+ default:
627
+ return null;
628
+ }
629
+ };
630
+
631
+ export const convertGraphQLResponseToLegacy = (leg: any): any => ({
632
+ ...leg,
633
+ agencyBrandingUrl: leg.agency?.url,
634
+ agencyName: leg.agency?.name,
635
+ agencyUrl: leg.agency?.url,
636
+ alightRule: pickupDropoffTypeToOtp1(leg.dropoffType),
637
+ boardRule: pickupDropoffTypeToOtp1(leg.pickupType),
638
+ dropOffBookingInfo: {
639
+ latestBookingTime: leg.dropOffBookingInfo
640
+ },
641
+ from: {
642
+ ...leg.from,
643
+ stopCode: leg.from.stop?.code,
644
+ stopId: leg.from.stop?.gtfsId
645
+ },
646
+ route: leg.route?.shortName,
647
+ routeColor: leg.route?.color,
648
+ routeId: leg.route?.id,
649
+ routeLongName: leg.route?.longName,
650
+ routeShortName: leg.route?.shortName,
651
+ routeTextColor: leg.route?.textColor,
652
+ to: {
653
+ ...leg.to,
654
+ stopCode: leg.to.stop?.code,
655
+ stopId: leg.to.stop?.gtfsId
656
+ },
657
+ tripHeadsign: leg.trip?.tripHeadsign,
658
+ tripId: leg.trip?.gtfsId
659
+ });