@ticketboothapp/booking 1.2.104 → 1.2.106

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.104",
3
+ "version": "1.2.106",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -333,18 +333,30 @@ function findMergedAvailabilityForSelection(
333
333
  selected: Availability | null
334
334
  ): Availability | undefined {
335
335
  if (!selected) return undefined;
336
- const optId = selected.productOptionId ?? undefined;
336
+ const optId = getAvailabilityOptionId(selected) || undefined;
337
337
  const dt = selected.dateTime;
338
338
  const availId = selected.availabilityId?.trim();
339
339
  if (availId && optId) {
340
340
  const exact = merged.find(
341
341
  (a) =>
342
342
  a.availabilityId === availId &&
343
- (a.productOptionId === optId || a.productOptionId === undefined)
343
+ getAvailabilityOptionId(a) === optId
344
344
  );
345
345
  if (exact) return exact;
346
346
  }
347
- return merged.find((a) => a.dateTime === dt && a.productOptionId === optId);
347
+ return merged.find((a) => a.dateTime === dt && getAvailabilityOptionId(a) === optId);
348
+ }
349
+
350
+ function getAvailabilityOptionId(availability: Availability, fallbackOptionId?: string): string {
351
+ const productOptionId = availability.productOptionId?.trim();
352
+ if (productOptionId) return productOptionId;
353
+ const productId = availability.productId?.trim();
354
+ if (productId?.startsWith('po_')) return productId;
355
+ return fallbackOptionId?.trim() ?? '';
356
+ }
357
+
358
+ function getAvailabilityCacheKey(availability: Availability): string {
359
+ return `${availability.dateTime}-${getAvailabilityOptionId(availability)}`;
348
360
  }
349
361
 
350
362
  /** Ticket rates for one availability row — mirrors main cart [pricing] useMemo so baseline slots match BE. */
@@ -358,7 +370,7 @@ function buildPricingFromAvailability(
358
370
  isSimplifiedPricingView: boolean,
359
371
  ) {
360
372
  if (!selectedAvailability || !pricingConfig) return [];
361
- const optionId = selectedAvailability.productOptionId;
373
+ const optionId = getAvailabilityOptionId(selectedAvailability);
362
374
  const selectedOption = activeOptions.find((opt) => opt.optionId === optionId);
363
375
  const precomputed = optionId ? precomputedPricesByOption?.[optionId] : undefined;
364
376
  const rateToDisplayPrice = (backendInDisplayCurrency: number) =>
@@ -376,11 +388,17 @@ function buildPricingFromAvailability(
376
388
  appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }>,
377
389
  ) => {
378
390
  const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
391
+ const hasOptionScopedPrice = baseInDisplayCurrency > 0;
392
+ const priceCADForBreakdown =
393
+ hasOptionScopedPrice && currency === 'CAD' ? baseInDisplayCurrency : backendPriceCAD;
394
+ const displayCurrencyPriceForFallback = hasOptionScopedPrice
395
+ ? baseInDisplayCurrency
396
+ : backendInDisplayCurrency;
379
397
  const isPublicMode = isSimplifiedPricingView;
380
398
  const breakdown = computePriceBreakdown(
381
399
  pricingConfig,
382
400
  currency,
383
- backendPriceCAD,
401
+ priceCADForBreakdown,
384
402
  basePriceCAD,
385
403
  hasFees,
386
404
  appliedAdjustments,
@@ -388,8 +406,8 @@ function buildPricingFromAvailability(
388
406
  baseInDisplayCurrency,
389
407
  isPublicMode,
390
408
  );
391
- const price = breakdown?.finalPrice ?? rateToDisplayPrice(backendInDisplayCurrency);
392
- return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: backendPriceCAD };
409
+ const price = breakdown?.finalPrice ?? rateToDisplayPrice(displayCurrencyPriceForFallback);
410
+ return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: priceCADForBreakdown };
393
411
  };
394
412
  return (
395
413
  selectedAvailability.rates?.map((rate) => {
@@ -1281,10 +1299,10 @@ export function AdminChangeBookingFlow({
1281
1299
  let mergedOut: Availability[] = [];
1282
1300
  setAvailabilities((prev) => {
1283
1301
  const existingMap = new Map(
1284
- prev.map((avail) => [`${avail.dateTime}-${avail.productOptionId}`, avail])
1302
+ prev.map((avail) => [getAvailabilityCacheKey(avail), avail])
1285
1303
  );
1286
1304
  allFetchedAvailabilities.forEach((avail) => {
1287
- existingMap.set(`${avail.dateTime}-${avail.productOptionId}`, avail);
1305
+ existingMap.set(getAvailabilityCacheKey(avail), avail);
1288
1306
  });
1289
1307
  mergedOut = Array.from(existingMap.values());
1290
1308
  return mergedOut;
@@ -1318,10 +1336,10 @@ export function AdminChangeBookingFlow({
1318
1336
  const existingCache = availabilitiesCache.get(cacheKey);
1319
1337
  const existingAvailabilities = existingCache?.availabilities ?? [];
1320
1338
  const mergedAvailabilitiesMap = new Map(
1321
- existingAvailabilities.map((a) => [`${a.dateTime}-${a.productOptionId}`, a])
1339
+ existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
1322
1340
  );
1323
1341
  allFetchedAvailabilities.forEach((a) => {
1324
- mergedAvailabilitiesMap.set(`${a.dateTime}-${a.productOptionId}`, a);
1342
+ mergedAvailabilitiesMap.set(getAvailabilityCacheKey(a), a);
1325
1343
  });
1326
1344
  const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
1327
1345
  results.forEach((r) => {
@@ -1598,12 +1616,12 @@ export function AdminChangeBookingFlow({
1598
1616
  // Merge with existing availabilities (avoid duplicates by dateTime + productOptionId)
1599
1617
  setAvailabilities(prev => {
1600
1618
  const existingMap = new Map(
1601
- prev.map(avail => [`${avail.dateTime}-${avail.productOptionId}`, avail])
1619
+ prev.map(avail => [getAvailabilityCacheKey(avail), avail])
1602
1620
  );
1603
1621
 
1604
1622
  // Merge new availabilities - update existing ones or add new ones
1605
1623
  allFetchedAvailabilities.forEach(avail => {
1606
- const key = `${avail.dateTime}-${avail.productOptionId}`;
1624
+ const key = getAvailabilityCacheKey(avail);
1607
1625
  // Always update to get latest data (vacancies, prices, etc.)
1608
1626
  existingMap.set(key, avail);
1609
1627
  });
@@ -1632,10 +1650,10 @@ export function AdminChangeBookingFlow({
1632
1650
  const existingCache = availabilitiesCache.get(cacheKey);
1633
1651
  const existingAvailabilities = existingCache?.availabilities ?? [];
1634
1652
  const mergedAvailabilitiesMap = new Map(
1635
- existingAvailabilities.map((a) => [`${a.dateTime}-${a.productOptionId}`, a])
1653
+ existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
1636
1654
  );
1637
1655
  allFetchedAvailabilities.forEach((a) => {
1638
- mergedAvailabilitiesMap.set(`${a.dateTime}-${a.productOptionId}`, a);
1656
+ mergedAvailabilitiesMap.set(getAvailabilityCacheKey(a), a);
1639
1657
  });
1640
1658
  const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
1641
1659
  results.forEach((r) => {
@@ -1881,7 +1899,7 @@ export function AdminChangeBookingFlow({
1881
1899
  timesForSelectedDate
1882
1900
  .map(
1883
1901
  (avail) =>
1884
- `${avail.dateTime}|${avail.productOptionId ?? ''}|${avail.vacancies ?? 0}`,
1902
+ `${avail.dateTime}|${getAvailabilityOptionId(avail)}|${avail.vacancies ?? 0}`,
1885
1903
  )
1886
1904
  .join('||'),
1887
1905
  [timesForSelectedDate],
@@ -2315,7 +2333,9 @@ export function AdminChangeBookingFlow({
2315
2333
  const calculateStaySummary = useCallback((returnDateTime: Date): string | null => {
2316
2334
  if (!selectedAvailability) return null;
2317
2335
 
2318
- const availabilityProductOptionId = selectedAvailability.productOptionId || activeOptions[0]?.optionId;
2336
+ const availabilityProductOptionId =
2337
+ getAvailabilityOptionId(selectedAvailability) ||
2338
+ (activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
2319
2339
  const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
2320
2340
  if (!selectedOption) return null;
2321
2341
 
@@ -2391,7 +2411,9 @@ export function AdminChangeBookingFlow({
2391
2411
  // Helper function to compute itinerary display for storage (returns same shape as "Your Itinerary" box)
2392
2412
  const computeItineraryDisplay = useCallback((): ItineraryDisplayStep[] | null => {
2393
2413
  if (!selectedAvailability) return null;
2394
- const availabilityProductOptionId = selectedAvailability.productOptionId || activeOptions[0]?.optionId;
2414
+ const availabilityProductOptionId =
2415
+ getAvailabilityOptionId(selectedAvailability) ||
2416
+ (activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
2395
2417
  const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
2396
2418
  if (!selectedOption) return null;
2397
2419
  const tourStartTime = parseISO(selectedAvailability.dateTime);
@@ -2630,13 +2652,13 @@ export function AdminChangeBookingFlow({
2630
2652
  const selectedMs = parseAvailabilityDateTime(selectedAvailability.dateTime).getTime();
2631
2653
  const initialMs = parseAvailabilityDateTime(initialValues.dateTime).getTime();
2632
2654
  if (selectedMs !== initialMs) return false;
2633
- const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability.productOptionId);
2655
+ const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
2634
2656
  const initialOpt = changeFlowResolvedInitialProductOptionId;
2635
2657
  if (selectedOpt != null && initialOpt != null) {
2636
2658
  return selectedOpt === initialOpt;
2637
2659
  }
2638
2660
  const legacyInitial = initialValues.productOptionId?.trim() || null;
2639
- const legacySelected = selectedAvailability.productOptionId?.trim() || null;
2661
+ const legacySelected = getAvailabilityOptionId(selectedAvailability) || null;
2640
2662
  return !legacySelected || !legacyInitial || legacySelected === legacyInitial;
2641
2663
  }, [
2642
2664
  selectedAvailability,
@@ -2663,13 +2685,13 @@ export function AdminChangeBookingFlow({
2663
2685
  return false;
2664
2686
  }
2665
2687
  if (!selectedDate || selectedDate !== initialDay) return false;
2666
- const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability.productOptionId);
2688
+ const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
2667
2689
  const initialOpt = changeFlowResolvedInitialProductOptionId;
2668
2690
  if (selectedOpt != null && initialOpt != null) {
2669
2691
  return selectedOpt === initialOpt;
2670
2692
  }
2671
2693
  const legacyInitial = initialValues.productOptionId?.trim() || null;
2672
- const legacySelected = selectedAvailability.productOptionId?.trim() || null;
2694
+ const legacySelected = getAvailabilityOptionId(selectedAvailability) || null;
2673
2695
  return Boolean(legacySelected && legacyInitial && legacySelected === legacyInitial);
2674
2696
  }, [
2675
2697
  selectedDate,
@@ -2866,7 +2888,9 @@ export function AdminChangeBookingFlow({
2866
2888
  // Price breakdown: mid-layer returns line items (base + one per rule/deal). UI renders each line; rate in brackets when used.
2867
2889
  const getPriceBreakdown = useCallback((category: string, priceCAD: number, baseInDisplayCurrency: number | undefined, appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }> = []): PriceBreakdownData | null => {
2868
2890
  if (!pricingConfig) return null;
2869
- const selectedOption = activeOptions.find(opt => opt.optionId === selectedAvailability?.productOptionId);
2891
+ const selectedOption = activeOptions.find(
2892
+ opt => opt.optionId === (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '')
2893
+ );
2870
2894
  const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
2871
2895
  const isPublicMode = isSimplifiedPricingView;
2872
2896
  return computePriceBreakdown(
@@ -3342,7 +3366,7 @@ export function AdminChangeBookingFlow({
3342
3366
  if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
3343
3367
  const companyId = product.companyId ?? env.COMPANY_ID;
3344
3368
  if (!companyId) return '';
3345
- const optionId = selectedAvailability.productOptionId;
3369
+ const optionId = getAvailabilityOptionId(selectedAvailability);
3346
3370
  if (!optionId || !quantitiesSignature) return '';
3347
3371
  return [
3348
3372
  appliedPromoCode,
@@ -3359,7 +3383,7 @@ export function AdminChangeBookingFlow({
3359
3383
  }, [
3360
3384
  appliedPromoCode,
3361
3385
  selectedAvailability?.dateTime,
3362
- selectedAvailability?.productOptionId,
3386
+ selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
3363
3387
  selectedAvailability?.availabilityId,
3364
3388
  totalQuantity,
3365
3389
  product.companyId,
@@ -3402,7 +3426,7 @@ export function AdminChangeBookingFlow({
3402
3426
  } = promoDiscountParamsRef.current;
3403
3427
  if (!code || !sel) return;
3404
3428
  const companyId = product.companyId ?? env.COMPANY_ID;
3405
- const optionId = sel.productOptionId;
3429
+ const optionId = getAvailabilityOptionId(sel);
3406
3430
  if (!companyId || !optionId) return;
3407
3431
  const items = lines.map((l) => ({ category: l.category, qty: l.qty }));
3408
3432
  if (items.length === 0) return;
@@ -3802,7 +3826,7 @@ export function AdminChangeBookingFlow({
3802
3826
  const initialOpt =
3803
3827
  changeFlowResolvedInitialProductOptionId ??
3804
3828
  (initialValues.productOptionId?.trim() || null);
3805
- const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability.productOptionId);
3829
+ const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
3806
3830
  const optionChanged = Boolean(
3807
3831
  selectedOpt != null && initialOpt != null && initialOpt !== selectedOpt
3808
3832
  );
@@ -3933,7 +3957,7 @@ export function AdminChangeBookingFlow({
3933
3957
  bookingReference: initialValues?.bookingReference?.trim() ?? '',
3934
3958
  lastName: lastName.trim().toLowerCase(),
3935
3959
  parentProductId: product.productId,
3936
- optionId: selectedAvailability?.productOptionId?.trim() || activeOptions[0]?.optionId || '',
3960
+ optionId: selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
3937
3961
  dateTime: selectedAvailability?.dateTime ?? '',
3938
3962
  availabilityId: selectedAvailability?.availabilityId ?? null,
3939
3963
  pickupLocationId: pickupLocationId ?? null,
@@ -3948,7 +3972,7 @@ export function AdminChangeBookingFlow({
3948
3972
  initialValues?.bookingReference,
3949
3973
  lastName,
3950
3974
  product.productId,
3951
- selectedAvailability?.productOptionId,
3975
+ selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
3952
3976
  selectedAvailability?.dateTime,
3953
3977
  selectedAvailability?.availabilityId,
3954
3978
  activeOptions,
@@ -4506,8 +4530,7 @@ export function AdminChangeBookingFlow({
4506
4530
  return;
4507
4531
  }
4508
4532
 
4509
- const optionId =
4510
- selectedAvailability.productOptionId?.trim() || activeOptions[0]?.optionId;
4533
+ const optionId = getAvailabilityOptionId(selectedAvailability);
4511
4534
  if (!optionId) {
4512
4535
  setLatestChangeQuote(null);
4513
4536
  setChangeQuoteLoading(false);
@@ -4605,7 +4628,7 @@ export function AdminChangeBookingFlow({
4605
4628
  hasChangeSelection,
4606
4629
  selectedAvailability,
4607
4630
  selectedAvailability?.dateTime,
4608
- selectedAvailability?.productOptionId,
4631
+ selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
4609
4632
  initialValues?.bookingReference,
4610
4633
  lastName,
4611
4634
  missingRequiredReturnSelection,
@@ -4671,7 +4694,7 @@ export function AdminChangeBookingFlow({
4671
4694
 
4672
4695
  const mostPopularOption = activeOptions.find(opt => opt.mostPopular);
4673
4696
  const candidate = mostPopularOption
4674
- ? timesForSelectedDate.find(avail => avail.productOptionId === mostPopularOption.optionId && avail.vacancies > 0)
4697
+ ? timesForSelectedDate.find(avail => getAvailabilityOptionId(avail) === mostPopularOption.optionId && avail.vacancies > 0)
4675
4698
  : null;
4676
4699
  const fallback = timesForSelectedDate.find(avail => avail.vacancies > 0);
4677
4700
  const toSelect = candidate ?? fallback;
@@ -4697,9 +4720,9 @@ export function AdminChangeBookingFlow({
4697
4720
  useEffect(() => {
4698
4721
  if (selectedAvailability && availabilities.length > 0) {
4699
4722
  const updatedAvailability = availabilities.find(
4700
- avail =>
4723
+ avail =>
4701
4724
  avail.dateTime === selectedAvailability.dateTime &&
4702
- avail.productOptionId === selectedAvailability.productOptionId
4725
+ getAvailabilityOptionId(avail) === getAvailabilityOptionId(selectedAvailability)
4703
4726
  );
4704
4727
  if (updatedAvailability) {
4705
4728
  setSelectedAvailability(updatedAvailability);
@@ -4730,7 +4753,7 @@ export function AdminChangeBookingFlow({
4730
4753
  setImplicitReturnBaselineId(null);
4731
4754
  }, [
4732
4755
  selectedAvailability?.dateTime,
4733
- selectedAvailability?.productOptionId,
4756
+ selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
4734
4757
  initialValues?.returnAvailabilityId,
4735
4758
  initialValues?.returnDateTime,
4736
4759
  ]);
@@ -4818,7 +4841,7 @@ export function AdminChangeBookingFlow({
4818
4841
  }, [selectedAvailability?.returnOptions, selectedReturnOption]);
4819
4842
 
4820
4843
  // Fetch add-ons when availability (product option) is selected; clear selections when option changes
4821
- const availabilityProductOptionId = selectedAvailability?.productOptionId ?? null;
4844
+ const availabilityProductOptionId = selectedAvailability ? getAvailabilityOptionId(selectedAvailability) || null : null;
4822
4845
  const prevAvailabilityProductOptionIdRef = useRef<string | null>(null);
4823
4846
  useEffect(() => {
4824
4847
  if (!availabilityProductOptionId || !product.companyId) {
@@ -4838,7 +4861,7 @@ export function AdminChangeBookingFlow({
4838
4861
  if (date === selectedDate) return;
4839
4862
  if (selectedAvailability) {
4840
4863
  changeFlowOutboundAnchorRef.current = {
4841
- productOptionId: selectedAvailability.productOptionId ?? null,
4864
+ productOptionId: getAvailabilityOptionId(selectedAvailability) || null,
4842
4865
  minutesFromMidnight: getMinutesFromMidnightInTimezone(
4843
4866
  selectedAvailability.dateTime,
4844
4867
  companyTimezone
@@ -4975,8 +4998,8 @@ export function AdminChangeBookingFlow({
4975
4998
  [selectedAvailability]
4976
4999
  );
4977
5000
  const selectedAvailabilityKey = useMemo(
4978
- () => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability?.productOptionId ?? ''}`,
4979
- [selectedAvailability?.dateTime, selectedAvailability?.productOptionId]
5001
+ () => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''}`,
5002
+ [selectedAvailability]
4980
5003
  );
4981
5004
  // Remember where promo was successfully applied to avoid self-clearing on same selection.
4982
5005
  const promoAppliedSelectionKeyRef = useRef<string | null>(null);
@@ -5131,8 +5154,7 @@ export function AdminChangeBookingFlow({
5131
5154
  .map(([category, count]) => ({ category, count }));
5132
5155
 
5133
5156
  // Get the productOptionId from the selected availability (we tagged it when fetching)
5134
- const availabilityProductOptionId = selectedAvailability.productOptionId
5135
- || activeOptions[0]?.optionId;
5157
+ const availabilityProductOptionId = getAvailabilityOptionId(selectedAvailability);
5136
5158
 
5137
5159
  if (!availabilityProductOptionId) {
5138
5160
  setError('No product option selected');
@@ -5667,8 +5689,7 @@ export function AdminChangeBookingFlow({
5667
5689
  .map(([category, count]) => ({ category, count }));
5668
5690
  const availabilityProductOptionId =
5669
5691
  adminChoiceData.availabilityProductOptionId ||
5670
- selectedAvailability.productOptionId ||
5671
- activeOptions[0]?.optionId;
5692
+ getAvailabilityOptionId(selectedAvailability);
5672
5693
  if (!availabilityProductOptionId) {
5673
5694
  setError('No product option selected');
5674
5695
  setLoading(false);
@@ -263,18 +263,30 @@ function findMergedAvailabilityForSelection(
263
263
  selected: Availability | null
264
264
  ): Availability | undefined {
265
265
  if (!selected) return undefined;
266
- const optId = selected.productOptionId ?? undefined;
266
+ const optId = getAvailabilityOptionId(selected) || undefined;
267
267
  const dt = selected.dateTime;
268
268
  const availId = selected.availabilityId?.trim();
269
269
  if (availId && optId) {
270
270
  const exact = merged.find(
271
271
  (a) =>
272
272
  a.availabilityId === availId &&
273
- (a.productOptionId === optId || a.productOptionId === undefined)
273
+ getAvailabilityOptionId(a) === optId
274
274
  );
275
275
  if (exact) return exact;
276
276
  }
277
- return merged.find((a) => a.dateTime === dt && a.productOptionId === optId);
277
+ return merged.find((a) => a.dateTime === dt && getAvailabilityOptionId(a) === optId);
278
+ }
279
+
280
+ function getAvailabilityOptionId(availability: Availability, fallbackOptionId?: string): string {
281
+ const productOptionId = availability.productOptionId?.trim();
282
+ if (productOptionId) return productOptionId;
283
+ const productId = availability.productId?.trim();
284
+ if (productId?.startsWith('po_')) return productId;
285
+ return fallbackOptionId?.trim() ?? '';
286
+ }
287
+
288
+ function getAvailabilityCacheKey(availability: Availability): string {
289
+ return `${availability.dateTime}-${getAvailabilityOptionId(availability)}`;
278
290
  }
279
291
 
280
292
  /** Ticket rates for one availability row — mirrors main cart [pricing] useMemo so baseline slots match BE. */
@@ -288,7 +300,7 @@ function buildPricingFromAvailability(
288
300
  isSimplifiedPricingView: boolean,
289
301
  ) {
290
302
  if (!selectedAvailability || !pricingConfig) return [];
291
- const optionId = selectedAvailability.productOptionId;
303
+ const optionId = getAvailabilityOptionId(selectedAvailability);
292
304
  const selectedOption = activeOptions.find((opt) => opt.optionId === optionId);
293
305
  const precomputed = optionId ? precomputedPricesByOption?.[optionId] : undefined;
294
306
  const rateToDisplayPrice = (backendInDisplayCurrency: number) =>
@@ -306,11 +318,17 @@ function buildPricingFromAvailability(
306
318
  appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }>,
307
319
  ) => {
308
320
  const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
321
+ const hasOptionScopedPrice = baseInDisplayCurrency > 0;
322
+ const priceCADForBreakdown =
323
+ hasOptionScopedPrice && currency === 'CAD' ? baseInDisplayCurrency : backendPriceCAD;
324
+ const displayCurrencyPriceForFallback = hasOptionScopedPrice
325
+ ? baseInDisplayCurrency
326
+ : backendInDisplayCurrency;
309
327
  const isPublicMode = isSimplifiedPricingView;
310
328
  const breakdown = computePriceBreakdown(
311
329
  pricingConfig,
312
330
  currency,
313
- backendPriceCAD,
331
+ priceCADForBreakdown,
314
332
  basePriceCAD,
315
333
  hasFees,
316
334
  appliedAdjustments,
@@ -318,8 +336,8 @@ function buildPricingFromAvailability(
318
336
  baseInDisplayCurrency,
319
337
  isPublicMode,
320
338
  );
321
- const price = breakdown?.finalPrice ?? rateToDisplayPrice(backendInDisplayCurrency);
322
- return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: backendPriceCAD };
339
+ const price = breakdown?.finalPrice ?? rateToDisplayPrice(displayCurrencyPriceForFallback);
340
+ return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: priceCADForBreakdown };
323
341
  };
324
342
  return (
325
343
  selectedAvailability.rates?.map((rate) => {
@@ -1033,10 +1051,10 @@ export function ChangeBookingFlow({
1033
1051
  let mergedOut: Availability[] = [];
1034
1052
  setAvailabilities((prev) => {
1035
1053
  const existingMap = new Map(
1036
- prev.map((avail) => [`${avail.dateTime}-${avail.productOptionId}`, avail])
1054
+ prev.map((avail) => [getAvailabilityCacheKey(avail), avail])
1037
1055
  );
1038
1056
  allFetchedAvailabilities.forEach((avail) => {
1039
- existingMap.set(`${avail.dateTime}-${avail.productOptionId}`, avail);
1057
+ existingMap.set(getAvailabilityCacheKey(avail), avail);
1040
1058
  });
1041
1059
  mergedOut = Array.from(existingMap.values());
1042
1060
  return mergedOut;
@@ -1070,10 +1088,10 @@ export function ChangeBookingFlow({
1070
1088
  const existingCache = availabilitiesCache.get(cacheKey);
1071
1089
  const existingAvailabilities = existingCache?.availabilities ?? [];
1072
1090
  const mergedAvailabilitiesMap = new Map(
1073
- existingAvailabilities.map((a) => [`${a.dateTime}-${a.productOptionId}`, a])
1091
+ existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
1074
1092
  );
1075
1093
  allFetchedAvailabilities.forEach((a) => {
1076
- mergedAvailabilitiesMap.set(`${a.dateTime}-${a.productOptionId}`, a);
1094
+ mergedAvailabilitiesMap.set(getAvailabilityCacheKey(a), a);
1077
1095
  });
1078
1096
  const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
1079
1097
  results.forEach((r) => {
@@ -1351,12 +1369,12 @@ export function ChangeBookingFlow({
1351
1369
  // Merge with existing availabilities (avoid duplicates by dateTime + productOptionId)
1352
1370
  setAvailabilities(prev => {
1353
1371
  const existingMap = new Map(
1354
- prev.map(avail => [`${avail.dateTime}-${avail.productOptionId}`, avail])
1372
+ prev.map(avail => [getAvailabilityCacheKey(avail), avail])
1355
1373
  );
1356
1374
 
1357
1375
  // Merge new availabilities - update existing ones or add new ones
1358
1376
  allFetchedAvailabilities.forEach(avail => {
1359
- const key = `${avail.dateTime}-${avail.productOptionId}`;
1377
+ const key = getAvailabilityCacheKey(avail);
1360
1378
  // Always update to get latest data (vacancies, prices, etc.)
1361
1379
  existingMap.set(key, avail);
1362
1380
  });
@@ -1385,10 +1403,10 @@ export function ChangeBookingFlow({
1385
1403
  const existingCache = availabilitiesCache.get(cacheKey);
1386
1404
  const existingAvailabilities = existingCache?.availabilities ?? [];
1387
1405
  const mergedAvailabilitiesMap = new Map(
1388
- existingAvailabilities.map((a) => [`${a.dateTime}-${a.productOptionId}`, a])
1406
+ existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
1389
1407
  );
1390
1408
  allFetchedAvailabilities.forEach((a) => {
1391
- mergedAvailabilitiesMap.set(`${a.dateTime}-${a.productOptionId}`, a);
1409
+ mergedAvailabilitiesMap.set(getAvailabilityCacheKey(a), a);
1392
1410
  });
1393
1411
  const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
1394
1412
  results.forEach((r) => {
@@ -1634,7 +1652,7 @@ export function ChangeBookingFlow({
1634
1652
  timesForSelectedDate
1635
1653
  .map(
1636
1654
  (avail) =>
1637
- `${avail.dateTime}|${avail.productOptionId ?? ''}|${avail.vacancies ?? 0}`,
1655
+ `${avail.dateTime}|${getAvailabilityOptionId(avail)}|${avail.vacancies ?? 0}`,
1638
1656
  )
1639
1657
  .join('||'),
1640
1658
  [timesForSelectedDate],
@@ -2066,7 +2084,9 @@ export function ChangeBookingFlow({
2066
2084
  const calculateStaySummary = useCallback((returnDateTime: Date): string | null => {
2067
2085
  if (!selectedAvailability) return null;
2068
2086
 
2069
- const availabilityProductOptionId = selectedAvailability.productOptionId || activeOptions[0]?.optionId;
2087
+ const availabilityProductOptionId =
2088
+ getAvailabilityOptionId(selectedAvailability) ||
2089
+ (activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
2070
2090
  const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
2071
2091
  if (!selectedOption) return null;
2072
2092
 
@@ -2142,7 +2162,9 @@ export function ChangeBookingFlow({
2142
2162
  // Helper function to compute itinerary display for storage (returns same shape as "Your Itinerary" box)
2143
2163
  const computeItineraryDisplay = useCallback((): ItineraryDisplayStep[] | null => {
2144
2164
  if (!selectedAvailability) return null;
2145
- const availabilityProductOptionId = selectedAvailability.productOptionId || activeOptions[0]?.optionId;
2165
+ const availabilityProductOptionId =
2166
+ getAvailabilityOptionId(selectedAvailability) ||
2167
+ (activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
2146
2168
  const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
2147
2169
  if (!selectedOption) return null;
2148
2170
  const tourStartTime = parseISO(selectedAvailability.dateTime);
@@ -2381,13 +2403,13 @@ export function ChangeBookingFlow({
2381
2403
  const selectedMs = parseAvailabilityDateTime(selectedAvailability.dateTime).getTime();
2382
2404
  const initialMs = parseAvailabilityDateTime(initialValues.dateTime).getTime();
2383
2405
  if (selectedMs !== initialMs) return false;
2384
- const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability.productOptionId);
2406
+ const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
2385
2407
  const initialOpt = changeFlowResolvedInitialProductOptionId;
2386
2408
  if (selectedOpt != null && initialOpt != null) {
2387
2409
  return selectedOpt === initialOpt;
2388
2410
  }
2389
2411
  const legacyInitial = initialValues.productOptionId?.trim() || null;
2390
- const legacySelected = selectedAvailability.productOptionId?.trim() || null;
2412
+ const legacySelected = getAvailabilityOptionId(selectedAvailability) || null;
2391
2413
  return !legacySelected || !legacyInitial || legacySelected === legacyInitial;
2392
2414
  }, [
2393
2415
  selectedAvailability,
@@ -2414,13 +2436,13 @@ export function ChangeBookingFlow({
2414
2436
  return false;
2415
2437
  }
2416
2438
  if (!selectedDate || selectedDate !== initialDay) return false;
2417
- const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability.productOptionId);
2439
+ const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
2418
2440
  const initialOpt = changeFlowResolvedInitialProductOptionId;
2419
2441
  if (selectedOpt != null && initialOpt != null) {
2420
2442
  return selectedOpt === initialOpt;
2421
2443
  }
2422
2444
  const legacyInitial = initialValues.productOptionId?.trim() || null;
2423
- const legacySelected = selectedAvailability.productOptionId?.trim() || null;
2445
+ const legacySelected = getAvailabilityOptionId(selectedAvailability) || null;
2424
2446
  return Boolean(legacySelected && legacyInitial && legacySelected === legacyInitial);
2425
2447
  }, [
2426
2448
  selectedDate,
@@ -2617,7 +2639,9 @@ export function ChangeBookingFlow({
2617
2639
  // Price breakdown: mid-layer returns line items (base + one per rule/deal). UI renders each line; rate in brackets when used.
2618
2640
  const getPriceBreakdown = useCallback((category: string, priceCAD: number, baseInDisplayCurrency: number | undefined, appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }> = []): PriceBreakdownData | null => {
2619
2641
  if (!pricingConfig) return null;
2620
- const selectedOption = activeOptions.find(opt => opt.optionId === selectedAvailability?.productOptionId);
2642
+ const selectedOption = activeOptions.find(
2643
+ opt => opt.optionId === (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '')
2644
+ );
2621
2645
  const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
2622
2646
  const isPublicMode = isSimplifiedPricingView;
2623
2647
  return computePriceBreakdown(
@@ -3049,7 +3073,7 @@ export function ChangeBookingFlow({
3049
3073
  if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
3050
3074
  const companyId = product.companyId ?? env.COMPANY_ID;
3051
3075
  if (!companyId) return '';
3052
- const optionId = selectedAvailability.productOptionId;
3076
+ const optionId = getAvailabilityOptionId(selectedAvailability);
3053
3077
  if (!optionId || !quantitiesSignature) return '';
3054
3078
  return [
3055
3079
  appliedPromoCode,
@@ -3066,7 +3090,7 @@ export function ChangeBookingFlow({
3066
3090
  }, [
3067
3091
  appliedPromoCode,
3068
3092
  selectedAvailability?.dateTime,
3069
- selectedAvailability?.productOptionId,
3093
+ selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
3070
3094
  selectedAvailability?.availabilityId,
3071
3095
  totalQuantity,
3072
3096
  product.companyId,
@@ -3109,7 +3133,7 @@ export function ChangeBookingFlow({
3109
3133
  } = promoDiscountParamsRef.current;
3110
3134
  if (!code || !sel) return;
3111
3135
  const companyId = product.companyId ?? env.COMPANY_ID;
3112
- const optionId = sel.productOptionId;
3136
+ const optionId = getAvailabilityOptionId(sel);
3113
3137
  if (!companyId || !optionId) return;
3114
3138
  const items = lines.map((l) => ({ category: l.category, qty: l.qty }));
3115
3139
  if (items.length === 0) return;
@@ -3252,7 +3276,7 @@ export function ChangeBookingFlow({
3252
3276
  const initialOpt =
3253
3277
  changeFlowResolvedInitialProductOptionId ??
3254
3278
  (initialValues.productOptionId?.trim() || null);
3255
- const selectedOpt = normalizeProductOptionIdForChangeFlow(selectedAvailability.productOptionId);
3279
+ const selectedOpt = normalizeProductOptionIdForChangeFlow(getAvailabilityOptionId(selectedAvailability));
3256
3280
  const optionChanged = Boolean(
3257
3281
  selectedOpt != null && initialOpt != null && initialOpt !== selectedOpt
3258
3282
  );
@@ -3620,8 +3644,7 @@ export function ChangeBookingFlow({
3620
3644
  return;
3621
3645
  }
3622
3646
 
3623
- const optionId =
3624
- selectedAvailability.productOptionId?.trim() || activeOptions[0]?.optionId;
3647
+ const optionId = getAvailabilityOptionId(selectedAvailability);
3625
3648
  if (!optionId) {
3626
3649
  setLatestChangeQuote(null);
3627
3650
  setChangeQuoteLoading(false);
@@ -3695,7 +3718,7 @@ export function ChangeBookingFlow({
3695
3718
  hasChangeSelection,
3696
3719
  selectedAvailability,
3697
3720
  selectedAvailability?.dateTime,
3698
- selectedAvailability?.productOptionId,
3721
+ selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
3699
3722
  initialValues?.bookingReference,
3700
3723
  lastName,
3701
3724
  missingRequiredReturnSelection,
@@ -3754,7 +3777,7 @@ export function ChangeBookingFlow({
3754
3777
 
3755
3778
  const mostPopularOption = activeOptions.find(opt => opt.mostPopular);
3756
3779
  const candidate = mostPopularOption
3757
- ? timesForSelectedDate.find(avail => avail.productOptionId === mostPopularOption.optionId && avail.vacancies > 0)
3780
+ ? timesForSelectedDate.find(avail => getAvailabilityOptionId(avail) === mostPopularOption.optionId && avail.vacancies > 0)
3758
3781
  : null;
3759
3782
  const fallback = timesForSelectedDate.find(avail => avail.vacancies > 0);
3760
3783
  const toSelect = candidate ?? fallback;
@@ -3780,9 +3803,9 @@ export function ChangeBookingFlow({
3780
3803
  useEffect(() => {
3781
3804
  if (selectedAvailability && availabilities.length > 0) {
3782
3805
  const updatedAvailability = availabilities.find(
3783
- avail =>
3806
+ avail =>
3784
3807
  avail.dateTime === selectedAvailability.dateTime &&
3785
- avail.productOptionId === selectedAvailability.productOptionId
3808
+ getAvailabilityOptionId(avail) === getAvailabilityOptionId(selectedAvailability)
3786
3809
  );
3787
3810
  if (updatedAvailability) {
3788
3811
  setSelectedAvailability(updatedAvailability);
@@ -3813,7 +3836,7 @@ export function ChangeBookingFlow({
3813
3836
  setImplicitReturnBaselineId(null);
3814
3837
  }, [
3815
3838
  selectedAvailability?.dateTime,
3816
- selectedAvailability?.productOptionId,
3839
+ selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
3817
3840
  initialValues?.returnAvailabilityId,
3818
3841
  initialValues?.returnDateTime,
3819
3842
  ]);
@@ -3885,7 +3908,7 @@ export function ChangeBookingFlow({
3885
3908
  ]);
3886
3909
 
3887
3910
  // Fetch add-ons when availability (product option) is selected; clear selections when option changes
3888
- const availabilityProductOptionId = selectedAvailability?.productOptionId ?? null;
3911
+ const availabilityProductOptionId = selectedAvailability ? getAvailabilityOptionId(selectedAvailability) || null : null;
3889
3912
  const prevAvailabilityProductOptionIdRef = useRef<string | null>(null);
3890
3913
  useEffect(() => {
3891
3914
  if (!availabilityProductOptionId || !product.companyId) {
@@ -3905,7 +3928,7 @@ export function ChangeBookingFlow({
3905
3928
  if (date === selectedDate) return;
3906
3929
  if (selectedAvailability) {
3907
3930
  changeFlowOutboundAnchorRef.current = {
3908
- productOptionId: selectedAvailability.productOptionId ?? null,
3931
+ productOptionId: getAvailabilityOptionId(selectedAvailability) || null,
3909
3932
  minutesFromMidnight: getMinutesFromMidnightInTimezone(
3910
3933
  selectedAvailability.dateTime,
3911
3934
  companyTimezone
@@ -4042,8 +4065,8 @@ export function ChangeBookingFlow({
4042
4065
  [selectedAvailability]
4043
4066
  );
4044
4067
  const selectedAvailabilityKey = useMemo(
4045
- () => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability?.productOptionId ?? ''}`,
4046
- [selectedAvailability?.dateTime, selectedAvailability?.productOptionId]
4068
+ () => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''}`,
4069
+ [selectedAvailability]
4047
4070
  );
4048
4071
  // Remember where promo was successfully applied to avoid self-clearing on same selection.
4049
4072
  const promoAppliedSelectionKeyRef = useRef<string | null>(null);
@@ -4195,8 +4218,7 @@ export function ChangeBookingFlow({
4195
4218
  .map(([category, count]) => ({ category, count }));
4196
4219
 
4197
4220
  // Get the productOptionId from the selected availability (we tagged it when fetching)
4198
- const availabilityProductOptionId = selectedAvailability.productOptionId
4199
- || activeOptions[0]?.optionId;
4221
+ const availabilityProductOptionId = getAvailabilityOptionId(selectedAvailability);
4200
4222
 
4201
4223
  if (!availabilityProductOptionId) {
4202
4224
  setError('No product option selected');
@@ -158,22 +158,30 @@ function findMergedAvailabilityForSelection(
158
158
  selected: Availability | null
159
159
  ): Availability | undefined {
160
160
  if (!selected) return undefined;
161
- const optId = selected.productOptionId ?? undefined;
161
+ const optId = getAvailabilityOptionId(selected) || undefined;
162
162
  const dt = selected.dateTime;
163
163
  const availId = selected.availabilityId?.trim();
164
164
  if (availId && optId) {
165
165
  const exact = merged.find(
166
166
  (a) =>
167
167
  a.availabilityId === availId &&
168
- (a.productOptionId === optId || a.productOptionId === undefined)
168
+ getAvailabilityOptionId(a) === optId
169
169
  );
170
170
  if (exact) return exact;
171
171
  }
172
- return merged.find((a) => a.dateTime === dt && a.productOptionId === optId);
172
+ return merged.find((a) => a.dateTime === dt && getAvailabilityOptionId(a) === optId);
173
+ }
174
+
175
+ function getAvailabilityOptionId(availability: Availability, fallbackOptionId?: string): string {
176
+ const productOptionId = availability.productOptionId?.trim();
177
+ if (productOptionId) return productOptionId;
178
+ const productId = availability.productId?.trim();
179
+ if (productId?.startsWith('po_')) return productId;
180
+ return fallbackOptionId?.trim() ?? '';
173
181
  }
174
182
 
175
183
  function availabilityWithOptionId(availability: Availability, fallbackOptionId?: string): Availability {
176
- const productOptionId = availability.productOptionId || availability.productId || fallbackOptionId;
184
+ const productOptionId = getAvailabilityOptionId(availability, fallbackOptionId);
177
185
  return productOptionId ? { ...availability, productOptionId } : availability;
178
186
  }
179
187
 
@@ -195,11 +203,15 @@ function mergeAvailabilitiesIntoMap(
195
203
  availabilities: Availability[],
196
204
  ): void {
197
205
  for (const availability of availabilities) {
198
- const key = `${availability.dateTime}-${availability.productOptionId}`;
206
+ const key = getAvailabilityCacheKey(availability);
199
207
  map.set(key, mergeAvailabilityRecord(map.get(key), availability));
200
208
  }
201
209
  }
202
210
 
211
+ function getAvailabilityCacheKey(availability: Availability): string {
212
+ return `${availability.dateTime}-${getAvailabilityOptionId(availability)}`;
213
+ }
214
+
203
215
  function pickDefaultAvailabilityForTimes(
204
216
  times: Availability[],
205
217
  activeOptions: Product['options'],
@@ -208,7 +220,7 @@ function pickDefaultAvailabilityForTimes(
208
220
  const mostPopularOption = activeOptions.find((opt) => opt.mostPopular);
209
221
  const candidate = mostPopularOption
210
222
  ? times.find(
211
- (avail) => avail.productOptionId === mostPopularOption.optionId && avail.vacancies > 0,
223
+ (avail) => getAvailabilityOptionId(avail) === mostPopularOption.optionId && avail.vacancies > 0,
212
224
  )
213
225
  : null;
214
226
  const fallback = times.find((avail) => avail.vacancies > 0);
@@ -271,7 +283,7 @@ function buildPricingFromAvailability(
271
283
  isSimplifiedPricingView: boolean,
272
284
  ) {
273
285
  if (!selectedAvailability || !pricingConfig) return [];
274
- const optionId = selectedAvailability.productOptionId;
286
+ const optionId = getAvailabilityOptionId(selectedAvailability);
275
287
  const selectedOption = activeOptions.find((opt) => opt.optionId === optionId);
276
288
  const precomputed = optionId ? precomputedPricesByOption?.[optionId] : undefined;
277
289
  const rateToDisplayPrice = (backendInDisplayCurrency: number) =>
@@ -289,11 +301,17 @@ function buildPricingFromAvailability(
289
301
  appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }>,
290
302
  ) => {
291
303
  const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
304
+ const hasOptionScopedPrice = baseInDisplayCurrency > 0;
305
+ const priceCADForBreakdown =
306
+ hasOptionScopedPrice && currency === 'CAD' ? baseInDisplayCurrency : backendPriceCAD;
307
+ const displayCurrencyPriceForFallback = hasOptionScopedPrice
308
+ ? baseInDisplayCurrency
309
+ : backendInDisplayCurrency;
292
310
  const isPublicMode = isSimplifiedPricingView;
293
311
  const breakdown = computePriceBreakdown(
294
312
  pricingConfig,
295
313
  currency,
296
- backendPriceCAD,
314
+ priceCADForBreakdown,
297
315
  basePriceCAD,
298
316
  hasFees,
299
317
  appliedAdjustments,
@@ -301,8 +319,8 @@ function buildPricingFromAvailability(
301
319
  baseInDisplayCurrency,
302
320
  isPublicMode,
303
321
  );
304
- const price = breakdown?.finalPrice ?? rateToDisplayPrice(backendInDisplayCurrency);
305
- return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: backendPriceCAD };
322
+ const price = breakdown?.finalPrice ?? rateToDisplayPrice(displayCurrencyPriceForFallback);
323
+ return { category, baseInDisplayCurrency, appliedAdjustments, price, priceCAD: priceCADForBreakdown };
306
324
  };
307
325
  return (
308
326
  selectedAvailability.rates?.map((rate) => {
@@ -708,9 +726,7 @@ export function NewBookingFlow({
708
726
 
709
727
  let mergedOut: Availability[] = [];
710
728
  setAvailabilities((prev) => {
711
- const existingMap = new Map(
712
- prev.map((avail) => [`${avail.dateTime}-${avail.productOptionId}`, avail])
713
- );
729
+ const existingMap = new Map(prev.map((avail) => [getAvailabilityCacheKey(avail), avail]));
714
730
  mergeAvailabilitiesIntoMap(existingMap, allFetchedAvailabilities);
715
731
  mergedOut = Array.from(existingMap.values());
716
732
  return mergedOut;
@@ -744,7 +760,7 @@ export function NewBookingFlow({
744
760
  const existingCache = availabilitiesCache.get(cacheKey);
745
761
  const existingAvailabilities = existingCache?.availabilities ?? [];
746
762
  const mergedAvailabilitiesMap = new Map(
747
- existingAvailabilities.map((a) => [`${a.dateTime}-${a.productOptionId}`, a])
763
+ existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
748
764
  );
749
765
  mergeAvailabilitiesIntoMap(mergedAvailabilitiesMap, allFetchedAvailabilities);
750
766
  const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
@@ -828,7 +844,7 @@ export function NewBookingFlow({
828
844
 
829
845
  let mergedOut: Availability[] = [];
830
846
  setAvailabilities((prev) => {
831
- const merged = new Map(prev.map((availability) => [`${availability.dateTime}-${availability.productOptionId}`, availability]));
847
+ const merged = new Map(prev.map((availability) => [getAvailabilityCacheKey(availability), availability]));
832
848
  mergeAvailabilitiesIntoMap(merged, detailedAvailabilities);
833
849
  mergedOut = Array.from(merged.values());
834
850
  return mergedOut;
@@ -847,7 +863,7 @@ export function NewBookingFlow({
847
863
  const existingCache = availabilitiesCache.get(cacheKey);
848
864
  const mergedAvailabilities = new Map(
849
865
  (existingCache?.availabilities ?? []).map((availability) => [
850
- `${availability.dateTime}-${availability.productOptionId}`,
866
+ getAvailabilityCacheKey(availability),
851
867
  availability,
852
868
  ])
853
869
  );
@@ -1083,7 +1099,7 @@ export function NewBookingFlow({
1083
1099
  // Merge with existing availabilities (avoid duplicates by dateTime + productOptionId)
1084
1100
  setAvailabilities(prev => {
1085
1101
  const existingMap = new Map(
1086
- prev.map(avail => [`${avail.dateTime}-${avail.productOptionId}`, avail])
1102
+ prev.map(avail => [getAvailabilityCacheKey(avail), avail])
1087
1103
  );
1088
1104
 
1089
1105
  // Merge new availabilities - update existing ones or add new ones
@@ -1112,7 +1128,7 @@ export function NewBookingFlow({
1112
1128
  const existingCache = availabilitiesCache.get(cacheKey);
1113
1129
  const existingAvailabilities = existingCache?.availabilities ?? [];
1114
1130
  const mergedAvailabilitiesMap = new Map(
1115
- existingAvailabilities.map((a) => [`${a.dateTime}-${a.productOptionId}`, a])
1131
+ existingAvailabilities.map((a) => [getAvailabilityCacheKey(a), a])
1116
1132
  );
1117
1133
  mergeAvailabilitiesIntoMap(mergedAvailabilitiesMap, allFetchedAvailabilities);
1118
1134
  const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
@@ -1294,7 +1310,7 @@ export function NewBookingFlow({
1294
1310
  });
1295
1311
 
1296
1312
  setAvailabilities((prev) => {
1297
- const merged = new Map(prev.map((availability) => [`${availability.dateTime}-${availability.productOptionId}`, availability]));
1313
+ const merged = new Map(prev.map((availability) => [getAvailabilityCacheKey(availability), availability]));
1298
1314
  mergeAvailabilitiesIntoMap(merged, detailedAvailabilities);
1299
1315
  return Array.from(merged.values());
1300
1316
  });
@@ -1322,7 +1338,7 @@ export function NewBookingFlow({
1322
1338
  const existingCache = availabilitiesCache.get(cacheKey);
1323
1339
  const mergedAvailabilities = new Map(
1324
1340
  (existingCache?.availabilities ?? []).map((availability) => [
1325
- `${availability.dateTime}-${availability.productOptionId}`,
1341
+ getAvailabilityCacheKey(availability),
1326
1342
  availability,
1327
1343
  ])
1328
1344
  );
@@ -1507,7 +1523,7 @@ export function NewBookingFlow({
1507
1523
  timesForSelectedDate
1508
1524
  .map(
1509
1525
  (avail) =>
1510
- `${avail.dateTime}|${avail.productOptionId ?? ''}|${avail.vacancies ?? 0}`,
1526
+ `${avail.dateTime}|${getAvailabilityOptionId(avail)}|${avail.vacancies ?? 0}`,
1511
1527
  )
1512
1528
  .join('||'),
1513
1529
  [timesForSelectedDate],
@@ -1694,7 +1710,9 @@ export function NewBookingFlow({
1694
1710
  const calculateStaySummary = useCallback((returnDateTime: Date): string | null => {
1695
1711
  if (!selectedAvailability) return null;
1696
1712
 
1697
- const availabilityProductOptionId = selectedAvailability.productOptionId || activeOptions[0]?.optionId;
1713
+ const availabilityProductOptionId =
1714
+ getAvailabilityOptionId(selectedAvailability) ||
1715
+ (activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
1698
1716
  const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
1699
1717
  if (!selectedOption) return null;
1700
1718
 
@@ -1770,7 +1788,9 @@ export function NewBookingFlow({
1770
1788
  // Helper function to compute itinerary display for storage (returns same shape as "Your Itinerary" box)
1771
1789
  const computeItineraryDisplay = useCallback((): ItineraryDisplayStep[] | null => {
1772
1790
  if (!selectedAvailability) return null;
1773
- const availabilityProductOptionId = selectedAvailability.productOptionId || activeOptions[0]?.optionId;
1791
+ const availabilityProductOptionId =
1792
+ getAvailabilityOptionId(selectedAvailability) ||
1793
+ (activeOptions.length === 1 ? activeOptions[0]?.optionId : undefined);
1774
1794
  const selectedOption = activeOptions.find(opt => opt.optionId === availabilityProductOptionId);
1775
1795
  if (!selectedOption) return null;
1776
1796
  const tourStartTime = parseISO(selectedAvailability.dateTime);
@@ -1940,7 +1960,7 @@ export function NewBookingFlow({
1940
1960
  // Price breakdown: mid-layer returns line items (base + one per rule/deal). UI renders each line; rate in brackets when used.
1941
1961
  const getPriceBreakdown = useCallback((category: string, priceCAD: number, baseInDisplayCurrency: number | undefined, appliedAdjustments: Array<{ type: string; id: string; name: string; changeByCurrency?: Record<string, number> }> = []): PriceBreakdownData | null => {
1942
1962
  if (!pricingConfig) return null;
1943
- const selectedOption = activeOptions.find(opt => opt.optionId === selectedAvailability?.productOptionId);
1963
+ const selectedOption = activeOptions.find(opt => opt.optionId === (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''));
1944
1964
  const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
1945
1965
  const isPublicMode = isSimplifiedPricingView;
1946
1966
  return computePriceBreakdown(
@@ -2163,7 +2183,7 @@ export function NewBookingFlow({
2163
2183
  if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
2164
2184
  const companyId = product.companyId ?? env.COMPANY_ID;
2165
2185
  if (!companyId) return '';
2166
- const optionId = selectedAvailability.productOptionId;
2186
+ const optionId = getAvailabilityOptionId(selectedAvailability);
2167
2187
  if (!optionId || !quantitiesSignature) return '';
2168
2188
  return [
2169
2189
  appliedPromoCode,
@@ -2179,7 +2199,7 @@ export function NewBookingFlow({
2179
2199
  }, [
2180
2200
  appliedPromoCode,
2181
2201
  selectedAvailability?.dateTime,
2182
- selectedAvailability?.productOptionId,
2202
+ selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : '',
2183
2203
  selectedAvailability?.availabilityId,
2184
2204
  totalQuantity,
2185
2205
  product.companyId,
@@ -2375,9 +2395,9 @@ export function NewBookingFlow({
2375
2395
  useEffect(() => {
2376
2396
  if (selectedAvailability && availabilities.length > 0) {
2377
2397
  const updatedAvailability = availabilities.find(
2378
- avail =>
2398
+ avail =>
2379
2399
  avail.dateTime === selectedAvailability.dateTime &&
2380
- avail.productOptionId === selectedAvailability.productOptionId
2400
+ getAvailabilityOptionId(avail) === getAvailabilityOptionId(selectedAvailability)
2381
2401
  );
2382
2402
  if (updatedAvailability && shouldSyncSelectedAvailability(selectedAvailability, updatedAvailability)) {
2383
2403
  setSelectedAvailability(updatedAvailability);
@@ -2435,7 +2455,7 @@ export function NewBookingFlow({
2435
2455
  ]);
2436
2456
 
2437
2457
  // Fetch add-ons when availability (product option) is selected; clear selections when option changes
2438
- const availabilityProductOptionId = selectedAvailability?.productOptionId ?? null;
2458
+ const availabilityProductOptionId = selectedAvailability ? getAvailabilityOptionId(selectedAvailability) || null : null;
2439
2459
  const prevAvailabilityProductOptionIdRef = useRef<string | null>(null);
2440
2460
  useEffect(() => {
2441
2461
  if (!availabilityProductOptionId || !product.companyId) {
@@ -2564,8 +2584,8 @@ export function NewBookingFlow({
2564
2584
  [selectedAvailability]
2565
2585
  );
2566
2586
  const selectedAvailabilityKey = useMemo(
2567
- () => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability?.productOptionId ?? ''}`,
2568
- [selectedAvailability?.dateTime, selectedAvailability?.productOptionId]
2587
+ () => `${selectedAvailability?.dateTime ?? ''}::${selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''}`,
2588
+ [selectedAvailability]
2569
2589
  );
2570
2590
  // Remember where promo was successfully applied to avoid self-clearing on same selection.
2571
2591
  const promoAppliedSelectionKeyRef = useRef<string | null>(null);
@@ -2756,8 +2776,7 @@ export function NewBookingFlow({
2756
2776
  .map(([category, count]) => ({ category, count }));
2757
2777
 
2758
2778
  // Get the productOptionId from the selected availability (we tagged it when fetching)
2759
- const availabilityProductOptionId = selectedAvailability.productOptionId
2760
- || activeOptions[0]?.optionId;
2779
+ const availabilityProductOptionId = getAvailabilityOptionId(selectedAvailability);
2761
2780
 
2762
2781
  if (!availabilityProductOptionId) {
2763
2782
  setError('No product option selected');
@@ -251,6 +251,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
251
251
  const [mapZoom, setMapZoom] = useState(DEFAULT_MAP_ZOOM);
252
252
  const [showSkipWarning, setShowSkipWarning] = useState(false);
253
253
  const [dontKnowFilterActive, setDontKnowFilterActive] = useState(false);
254
+ const [mapsAuthError, setMapsAuthError] = useState(false);
254
255
  const mapRef = useRef<google.maps.Map | null>(null);
255
256
  const highlightedPickupLocationIdSet = useMemo(
256
257
  () => new Set(highlightIds),
@@ -285,8 +286,26 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
285
286
  googleMapsApiKey,
286
287
  libraries,
287
288
  });
289
+ const canUseMap = isLoaded && !loadError && !mapsAuthError;
288
290
 
289
291
  // ============ Effects ============
292
+
293
+ useEffect(() => {
294
+ const mapsWindow = window as typeof window & { gm_authFailure?: () => void };
295
+ const previousAuthFailure = mapsWindow.gm_authFailure;
296
+
297
+ const handleAuthFailure = () => {
298
+ setMapsAuthError(true);
299
+ previousAuthFailure?.();
300
+ };
301
+ mapsWindow.gm_authFailure = handleAuthFailure;
302
+
303
+ return () => {
304
+ if (mapsWindow.gm_authFailure === handleAuthFailure) {
305
+ mapsWindow.gm_authFailure = previousAuthFailure;
306
+ }
307
+ };
308
+ }, []);
290
309
 
291
310
  // Apply filter pills to pickup locations
292
311
  const filteredPickupLocations = useMemo(() => {
@@ -868,16 +887,20 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
868
887
 
869
888
  // ============ Render ============
870
889
 
871
- if (loadError) {
872
- return (
873
- <div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
874
- Error loading Google Maps. Please check your API key.
875
- </div>
876
- );
877
- }
878
-
879
890
  return (
880
891
  <div className="space-y-3">
892
+ {loadError && (
893
+ <div className="p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-800 text-sm">
894
+ Google Maps could not load. You can still select a pickup location from the list below.
895
+ </div>
896
+ )}
897
+
898
+ {mapsAuthError && (
899
+ <div className="p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-800 text-sm">
900
+ Google Maps is not authorized for this website. You can still select a pickup location from the list below.
901
+ </div>
902
+ )}
903
+
881
904
  {!hideTitle && (
882
905
  <div className="space-y-0.5">
883
906
  <h2 className="text-2xl font-bold text-stone-900">
@@ -1038,7 +1061,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1038
1061
  </div>
1039
1062
 
1040
1063
  {/* Filter Pills + I don't know pill - horizontal scroll on mobile */}
1041
- {isLoaded && pickupLocations.length > 0 && (
1064
+ {pickupLocations.length > 0 && (
1042
1065
  <div className={styles.filterPillsScroll}>
1043
1066
  <FilterPills
1044
1067
  pickupLocations={pickupLocations}
@@ -1112,7 +1135,7 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1112
1135
  )}
1113
1136
 
1114
1137
  {/* Two-column layout: list left, map right - list height matches map */}
1115
- {isLoaded && pickupLocations.length > 0 && (
1138
+ {pickupLocations.length > 0 && (
1116
1139
  <div className={styles.twoColLayout}>
1117
1140
  {/* Left column: scrollable list of pickup locations + I don't know */}
1118
1141
  <div className={styles.leftColumn}>
@@ -1235,13 +1258,14 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1235
1258
  {/* Right column: map */}
1236
1259
  <div className={styles.rightColumn}>
1237
1260
  <div className={styles.mapWrapper}>
1238
- <GoogleMap
1239
- mapContainerClassName="w-full h-full rounded-lg overflow-hidden"
1240
- center={mapCenter}
1241
- zoom={mapZoom}
1242
- options={mapOptions}
1243
- onLoad={onMapLoad}
1244
- >
1261
+ {canUseMap ? (
1262
+ <GoogleMap
1263
+ mapContainerClassName="w-full h-full rounded-lg overflow-hidden"
1264
+ center={mapCenter}
1265
+ zoom={mapZoom}
1266
+ options={mapOptions}
1267
+ onLoad={onMapLoad}
1268
+ >
1245
1269
  {/* Markers for filtered locations */}
1246
1270
  {nearbyLocations.map((location) => {
1247
1271
  if (!location.coordinates) return null;
@@ -1480,7 +1504,12 @@ function PickupLocationSelectorWithMap(props: PickupLocationSelectorProps) {
1480
1504
  cursor="default"
1481
1505
  />
1482
1506
  ))}
1483
- </GoogleMap>
1507
+ </GoogleMap>
1508
+ ) : (
1509
+ <div className="h-full min-h-[280px] flex items-center justify-center rounded-lg bg-stone-50 border border-stone-200 p-4 text-center text-sm text-stone-600">
1510
+ Google Maps is unavailable right now. The pickup list is still available.
1511
+ </div>
1512
+ )}
1484
1513
  </div>
1485
1514
  </div>
1486
1515
  </div>
@@ -130,7 +130,7 @@ function findMergedPrivateShuttleAvailability(
130
130
  tz: string
131
131
  ): Availability | undefined {
132
132
  if (!sel || !optionId || !selectedDateStr) return undefined;
133
- const oidMatches = (a: Availability) => (a.productOptionId ?? a.productId) === optionId;
133
+ const oidMatches = (a: Availability) => getPrivateShuttleAvailabilityOptionId(a) === optionId;
134
134
  const availId = sel.availabilityId?.trim();
135
135
  if (availId) {
136
136
  const hit = merged.find((a) => a.availabilityId === availId && oidMatches(a));
@@ -146,6 +146,40 @@ function findMergedPrivateShuttleAvailability(
146
146
  return onDay[0];
147
147
  }
148
148
 
149
+ function getPrivateShuttleAvailabilityOptionId(availability: Availability): string {
150
+ const productOptionId = availability.productOptionId?.trim();
151
+ if (productOptionId) return productOptionId;
152
+ const productId = availability.productId?.trim();
153
+ return productId?.startsWith('po_') ? productId : '';
154
+ }
155
+
156
+ function getPrivateShuttleAvailabilityCacheKey(availability: Availability): string {
157
+ return `${availability.dateTime}-${getPrivateShuttleAvailabilityOptionId(availability)}`;
158
+ }
159
+
160
+ function getResourceRate(availability: Availability | undefined | null) {
161
+ return availability?.rates?.find((r) => r.rateId === 'RESOURCE' || r.category === 'RESOURCE');
162
+ }
163
+
164
+ function applyResourceAdjustments(
165
+ basePrice: number,
166
+ availability: Availability | undefined | null,
167
+ currency: Currency
168
+ ): number {
169
+ const resourceRate = getResourceRate(availability);
170
+ const adjustmentTotal =
171
+ resourceRate?.appliedAdjustments?.reduce(
172
+ (sum, adjustment) => sum + (adjustment.changeByCurrency?.[currency] ?? 0),
173
+ 0
174
+ ) ??
175
+ resourceRate?.applied_adjustments?.reduce(
176
+ (sum, adjustment) => sum + (adjustment.changeByCurrency?.[currency] ?? 0),
177
+ 0
178
+ ) ??
179
+ 0;
180
+ return basePrice + adjustmentTotal;
181
+ }
182
+
149
183
  export function PrivateShuttleBookingFlow({
150
184
  product,
151
185
  productId,
@@ -394,11 +428,9 @@ export function PrivateShuttleBookingFlow({
394
428
 
395
429
  let mergedAvailabilities: Availability[] = [];
396
430
  setAvailabilities((prev) => {
397
- const existingMap = new Map(
398
- prev.map((avail) => [`${avail.dateTime}-${avail.productId || avail.productOptionId}`, avail])
399
- );
431
+ const existingMap = new Map(prev.map((avail) => [getPrivateShuttleAvailabilityCacheKey(avail), avail]));
400
432
  result.availabilities.forEach((avail) => {
401
- existingMap.set(`${avail.dateTime}-${avail.productId || avail.productOptionId}`, avail);
433
+ existingMap.set(getPrivateShuttleAvailabilityCacheKey(avail), avail);
402
434
  });
403
435
  mergedAvailabilities = Array.from(existingMap.values());
404
436
  return mergedAvailabilities;
@@ -476,11 +508,9 @@ export function PrivateShuttleBookingFlow({
476
508
 
477
509
  let mergedAvailabilities: Availability[] = [];
478
510
  setAvailabilities((prev) => {
479
- const existingMap = new Map(
480
- prev.map((avail) => [`${avail.dateTime}-${avail.productId || avail.productOptionId}`, avail])
481
- );
511
+ const existingMap = new Map(prev.map((avail) => [getPrivateShuttleAvailabilityCacheKey(avail), avail]));
482
512
  result.availabilities.forEach((avail) => {
483
- existingMap.set(`${avail.dateTime}-${avail.productId || avail.productOptionId}`, avail);
513
+ existingMap.set(getPrivateShuttleAvailabilityCacheKey(avail), avail);
484
514
  });
485
515
  mergedAvailabilities = Array.from(existingMap.values());
486
516
  return mergedAvailabilities;
@@ -498,12 +528,12 @@ export function PrivateShuttleBookingFlow({
498
528
  const existingCache = availabilitiesCache.get(cacheKey);
499
529
  const mergedAvailabilitiesMap = new Map(
500
530
  (existingCache?.availabilities ?? []).map((availability) => [
501
- `${availability.dateTime}-${availability.productId || availability.productOptionId}`,
531
+ getPrivateShuttleAvailabilityCacheKey(availability),
502
532
  availability,
503
533
  ])
504
534
  );
505
535
  result.availabilities.forEach((availability) => {
506
- mergedAvailabilitiesMap.set(`${availability.dateTime}-${availability.productId || availability.productOptionId}`, availability);
536
+ mergedAvailabilitiesMap.set(getPrivateShuttleAvailabilityCacheKey(availability), availability);
507
537
  });
508
538
  availabilitiesCache.merge(cacheKey, {
509
539
  fetchedRanges: existingCache?.fetchedRanges ?? fetchedRangesRef.current,
@@ -687,13 +717,12 @@ export function PrivateShuttleBookingFlow({
687
717
  if (existing.length === 0) return result.availabilities;
688
718
  const existingMap = new Map(
689
719
  existing.map((avail) => [
690
- `${avail.dateTime}-${avail.productId || avail.productOptionId}`,
720
+ getPrivateShuttleAvailabilityCacheKey(avail),
691
721
  avail,
692
722
  ])
693
723
  );
694
724
  result.availabilities.forEach((avail) => {
695
- const key = `${avail.dateTime}-${avail.productId || avail.productOptionId}`;
696
- existingMap.set(key, avail);
725
+ existingMap.set(getPrivateShuttleAvailabilityCacheKey(avail), avail);
697
726
  });
698
727
  return Array.from(existingMap.values());
699
728
  })();
@@ -779,9 +808,7 @@ export function PrivateShuttleBookingFlow({
779
808
  const optionsAvailableForSelectedDate = useMemo(() => {
780
809
  if (!selectedDate) return [];
781
810
  const dateAvailabilities = availabilitiesByDate[selectedDate] || [];
782
- const optionIds = new Set(
783
- dateAvailabilities.map((a) => a.productId || a.productOptionId).filter(Boolean)
784
- );
811
+ const optionIds = new Set(dateAvailabilities.map(getPrivateShuttleAvailabilityOptionId).filter(Boolean));
785
812
  return activeOptions.filter((opt) => optionIds.has(opt.optionId));
786
813
  }, [selectedDate, availabilitiesByDate, activeOptions]);
787
814
 
@@ -819,12 +846,12 @@ export function PrivateShuttleBookingFlow({
819
846
  setAvailabilities((prev) => {
820
847
  const merged = new Map(
821
848
  prev.map((availability) => [
822
- `${availability.dateTime}-${availability.productId || availability.productOptionId}`,
849
+ getPrivateShuttleAvailabilityCacheKey(availability),
823
850
  availability,
824
851
  ])
825
852
  );
826
853
  result.availabilities.forEach((availability) => {
827
- merged.set(`${availability.dateTime}-${availability.productId || availability.productOptionId}`, availability);
854
+ merged.set(getPrivateShuttleAvailabilityCacheKey(availability), availability);
828
855
  });
829
856
  return Array.from(merged.values());
830
857
  });
@@ -841,12 +868,12 @@ export function PrivateShuttleBookingFlow({
841
868
  const existingCache = availabilitiesCache.get(cacheKey);
842
869
  const mergedAvailabilities = new Map(
843
870
  (existingCache?.availabilities ?? []).map((availability) => [
844
- `${availability.dateTime}-${availability.productId || availability.productOptionId}`,
871
+ getPrivateShuttleAvailabilityCacheKey(availability),
845
872
  availability,
846
873
  ])
847
874
  );
848
875
  result.availabilities.forEach((availability) => {
849
- mergedAvailabilities.set(`${availability.dateTime}-${availability.productId || availability.productOptionId}`, availability);
876
+ mergedAvailabilities.set(getPrivateShuttleAvailabilityCacheKey(availability), availability);
850
877
  });
851
878
  availabilitiesCache.merge(cacheKey, {
852
879
  fetchedRanges: existingCache?.fetchedRanges ?? fetchedRangesRef.current,
@@ -898,22 +925,25 @@ export function PrivateShuttleBookingFlow({
898
925
 
899
926
  const getOptionPrice = useCallback(
900
927
  (optionId: string) => {
928
+ const opt = activeOptions.find((o) => o.optionId === optionId);
901
929
  const dateAvailabilities = selectedDate ? availabilitiesByDate[selectedDate] || [] : [];
902
930
  const avail = dateAvailabilities.find(
903
- (a) => (a.productId || a.productOptionId) === optionId
931
+ (a) => getPrivateShuttleAvailabilityOptionId(a) === optionId
904
932
  );
933
+ const profileOptionPrice = resourcePriceByOption?.[optionId]?.[currency];
934
+ if (profileOptionPrice != null) {
935
+ return applyResourceAdjustments(profileOptionPrice, avail, currency);
936
+ }
937
+ if (currency === 'CAD' && opt?.pricing?.['RESOURCE'] != null) {
938
+ return applyResourceAdjustments(opt.pricing['RESOURCE'], avail, currency);
939
+ }
905
940
  if (avail?.rates) {
906
- const resourceRate = avail.rates.find(
907
- (r) => r.rateId === 'RESOURCE' || r.category === 'RESOURCE'
908
- );
941
+ const resourceRate = getResourceRate(avail);
909
942
  if (resourceRate?.priceByCurrency?.[currency] != null)
910
943
  return resourceRate.priceByCurrency[currency];
911
944
  if (currency === 'CAD' && resourceRate?.price != null) return resourceRate.price;
912
945
  }
913
- if (resourcePriceByOption?.[optionId]?.[currency] != null)
914
- return resourcePriceByOption[optionId][currency];
915
- if (resourcePriceByCurrency?.[currency] != null) return resourcePriceByCurrency[currency];
916
- const opt = activeOptions.find((o) => o.optionId === optionId);
946
+ if (currency === 'CAD' && resourcePriceByCurrency?.[currency] != null) return resourcePriceByCurrency[currency];
917
947
  return opt?.pricing?.['RESOURCE'] ?? 0;
918
948
  },
919
949
  [
@@ -927,10 +957,20 @@ export function PrivateShuttleBookingFlow({
927
957
  );
928
958
 
929
959
  const resourcePrice = useMemo(() => {
930
- if (selectedAvailability?.rates) {
931
- const resourceRate = selectedAvailability.rates.find(
932
- (r) => r.rateId === 'RESOURCE' || r.category === 'RESOURCE'
960
+ const selectedProfileOptionPrice = selectedOption
961
+ ? resourcePriceByOption?.[selectedOption]?.[currency]
962
+ : undefined;
963
+ if (selectedProfileOptionPrice != null) {
964
+ return applyResourceAdjustments(selectedProfileOptionPrice, selectedAvailability, currency);
965
+ }
966
+ if (currency === 'CAD' && selectedOptionConfig?.pricing?.['RESOURCE'] != null)
967
+ return applyResourceAdjustments(
968
+ selectedOptionConfig.pricing['RESOURCE'],
969
+ selectedAvailability,
970
+ currency
933
971
  );
972
+ if (selectedAvailability?.rates) {
973
+ const resourceRate = getResourceRate(selectedAvailability);
934
974
  if (resourceRate) {
935
975
  const fromRate =
936
976
  resourceRate.priceByCurrency?.[currency] ??
@@ -938,12 +978,12 @@ export function PrivateShuttleBookingFlow({
938
978
  if (fromRate != null) return fromRate;
939
979
  }
940
980
  }
941
- if (selectedOption && resourcePriceByOption?.[selectedOption]?.[currency] != null)
942
- return resourcePriceByOption[selectedOption][currency];
943
- if (resourcePriceByCurrency?.[currency] != null) return resourcePriceByCurrency[currency];
981
+ if (currency === 'CAD' && resourcePriceByCurrency?.[currency] != null) return resourcePriceByCurrency[currency];
944
982
  if (precomputedPrices?.['RESOURCE']?.[currency] != null)
945
983
  return precomputedPrices['RESOURCE'][currency];
946
- return selectedOptionConfig?.pricing?.['RESOURCE'] ?? 0;
984
+ if (selectedOptionConfig?.pricing?.['RESOURCE'] != null)
985
+ return selectedOptionConfig.pricing['RESOURCE'];
986
+ return 0;
947
987
  }, [
948
988
  selectedAvailability,
949
989
  resourcePriceByOption,
@@ -1173,7 +1213,7 @@ export function PrivateShuttleBookingFlow({
1173
1213
  setSelectedOption(opt.optionId);
1174
1214
  const dateAvailabilities = availabilitiesByDate[selectedDate] || [];
1175
1215
  const avail = dateAvailabilities.find(
1176
- (a) => (a.productId || a.productOptionId) === opt.optionId
1216
+ (a) => getPrivateShuttleAvailabilityOptionId(a) === opt.optionId
1177
1217
  );
1178
1218
  setSelectedAvailability(avail || null);
1179
1219
  }
@@ -1231,7 +1271,7 @@ export function PrivateShuttleBookingFlow({
1231
1271
  if (selectedDate) {
1232
1272
  const dateAvailabilities = availabilitiesByDate[selectedDate] || [];
1233
1273
  const avail = dateAvailabilities.find(
1234
- (a) => (a.productId || a.productOptionId) === optionId
1274
+ (a) => getPrivateShuttleAvailabilityOptionId(a) === optionId
1235
1275
  );
1236
1276
  setSelectedAvailability(avail || null);
1237
1277
  } else {
@@ -331,10 +331,14 @@ export function computePriceBreakdown(
331
331
 
332
332
  // Subtotal in display currency = base + adjustments; pass so final price is (subtotal + fees) * (1+tax) and breakdown adds up
333
333
  const subtotalInDisplayCurrency = baseAmount + adjustmentSum;
334
+ const backendPriceForDetails =
335
+ currency === BASE_CURRENCY && subtotalInDisplayCurrency > 0
336
+ ? subtotalInDisplayCurrency
337
+ : backendPriceCAD;
334
338
  const details = computeTicketPriceDetails(
335
339
  pricingConfig,
336
340
  currency,
337
- backendPriceCAD,
341
+ backendPriceForDetails,
338
342
  hasFees,
339
343
  subtotalInDisplayCurrency > 0 ? subtotalInDisplayCurrency : baseInDisplayCurrency
340
344
  );