@ticketboothapp/booking 1.2.177 → 1.2.179
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 +1 -1
- package/src/components/booking/AddOnsSection.module.css +36 -5
- package/src/components/booking/AddOnsSection.tsx +250 -144
- package/src/components/booking/AdminChangeBookingFlow.tsx +3 -2
- package/src/components/booking/CollapsibleAddOnItem.module.css +72 -0
- package/src/components/booking/CollapsibleAddOnItem.tsx +38 -0
- package/src/components/booking/MealDrinkAddOnSelector.tsx +70 -29
- package/src/components/booking/PrivateShuttleAddOnsSection.tsx +124 -57
- package/src/components/booking/PrivateShuttleBookingFlow.module.css +26 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +8 -5
- package/src/components/booking/PrivateShuttlePreferencesSection.tsx +29 -55
- package/src/components/booking/add-on-selection-helpers.ts +13 -0
- package/src/components/booking/change-booking-checkout-builders.ts +1 -1
- package/src/components/booking/private-shuttle-checkout-builders.ts +2 -2
- package/src/components/booking/private-shuttle-lunch-visibility.ts +20 -0
- package/src/components/booking/standard-booking-checkout-builders.ts +1 -1
- package/src/components/booking/useAdminChangeProtectedPricing.ts +1 -1
- package/src/components/booking/useBookingAvailabilityAddOns.ts +26 -4
- package/src/components/booking/useChangeBookingProtectedPricing.ts +1 -1
- package/src/components/booking/usePrivateShuttlePriceSummary.ts +13 -6
- package/src/components/booking/useStandardBookingPriceSummary.ts +1 -1
- package/src/lib/booking-api.ts +4 -1
- package/test/add-on-selection-helpers.test.ts +24 -0
- package/test/change-booking-helpers.test.ts +42 -1
- package/test/private-shuttle-lunch-visibility.test.ts +27 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const EMERALD_LAKE_LUNCH_ADD_ON_ID = 'addon_el_lunch';
|
|
2
|
+
export const WILD_FLOUR_LUNCH_ADD_ON_ID = 'addon_wild_flour_bakery_lunch_package';
|
|
3
|
+
|
|
4
|
+
const EMERALD_LAKE_DESTINATION_ID = 'emerald_lake';
|
|
5
|
+
|
|
6
|
+
export function privateShuttleLunchAddOnIsVisible(
|
|
7
|
+
addOnId: string,
|
|
8
|
+
selectedDestinationIds: string[],
|
|
9
|
+
): boolean {
|
|
10
|
+
const includesEmeraldLake = selectedDestinationIds.includes(EMERALD_LAKE_DESTINATION_ID);
|
|
11
|
+
if (addOnId === EMERALD_LAKE_LUNCH_ADD_ON_ID) return includesEmeraldLake;
|
|
12
|
+
if (addOnId === WILD_FLOUR_LUNCH_ADD_ON_ID) return !includesEmeraldLake;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function privateShuttleHiddenLunchAddOnId(selectedDestinationIds: string[]): string {
|
|
17
|
+
return selectedDestinationIds.includes(EMERALD_LAKE_DESTINATION_ID)
|
|
18
|
+
? WILD_FLOUR_LUNCH_ADD_ON_ID
|
|
19
|
+
: EMERALD_LAKE_LUNCH_ADD_ON_ID;
|
|
20
|
+
}
|
|
@@ -104,7 +104,7 @@ function buildStandardBookingAddOnLineItems(
|
|
|
104
104
|
const amount = ((addOn.price ?? 0) + (variant?.priceAdjustment ?? 0)) * quantity;
|
|
105
105
|
const label = variant?.label
|
|
106
106
|
? `${addOn.name} (${variant.label})${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`
|
|
107
|
-
: addOn.name
|
|
107
|
+
: `${addOn.name}${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`;
|
|
108
108
|
|
|
109
109
|
checkoutLines.push({
|
|
110
110
|
label,
|
|
@@ -283,7 +283,7 @@ export function useAdminChangeProtectedPricing({
|
|
|
283
283
|
: null;
|
|
284
284
|
const name = variantLabel
|
|
285
285
|
? `${addOn.name} (${variantLabel})${qty > 1 ? ` \u00d7 ${qty}` : ''}`
|
|
286
|
-
: addOn.name
|
|
286
|
+
: `${addOn.name}${qty > 1 ? ` \u00d7 ${qty}` : ''}`;
|
|
287
287
|
return { name, totalAmount: amt, description: addOn.description ?? undefined };
|
|
288
288
|
})
|
|
289
289
|
.filter((x): x is NonNullable<typeof x> => x != null);
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from 'react';
|
|
2
|
+
import { formatInTimeZone } from 'date-fns-tz';
|
|
2
3
|
import {
|
|
3
4
|
getAddOns,
|
|
4
5
|
type AddOn,
|
|
5
6
|
type Availability,
|
|
6
7
|
} from '../../lib/booking-api';
|
|
7
|
-
import { getAvailabilityOptionId } from './change-booking-flow-helpers';
|
|
8
|
+
import { getAvailabilityOptionId, parseAvailabilityDateTime } from './change-booking-flow-helpers';
|
|
8
9
|
|
|
9
10
|
export interface UseBookingAvailabilityAddOnsParams {
|
|
10
11
|
selectedAvailability: Availability | null;
|
|
11
12
|
companyId?: string | null;
|
|
12
13
|
onProductOptionChange?: () => void;
|
|
14
|
+
/** Ignore lead-time cutoffs for admin changes while continuing to honor blackout dates. */
|
|
15
|
+
ignoreCutoff?: boolean;
|
|
16
|
+
companyTimezone?: string;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export interface UseBookingAvailabilityAddOnsResult {
|
|
@@ -23,6 +27,8 @@ export function useBookingAvailabilityAddOns({
|
|
|
23
27
|
selectedAvailability,
|
|
24
28
|
companyId,
|
|
25
29
|
onProductOptionChange,
|
|
30
|
+
ignoreCutoff = false,
|
|
31
|
+
companyTimezone,
|
|
26
32
|
}: UseBookingAvailabilityAddOnsParams): UseBookingAvailabilityAddOnsResult {
|
|
27
33
|
const [addOns, setAddOns] = useState<AddOn[]>([]);
|
|
28
34
|
const [loadedProductOptionId, setLoadedProductOptionId] = useState<string | null>(null);
|
|
@@ -50,11 +56,20 @@ export function useBookingAvailabilityAddOns({
|
|
|
50
56
|
getAddOns(companyId, {
|
|
51
57
|
productOptionId: availabilityProductOptionId,
|
|
52
58
|
preCheckout: true,
|
|
53
|
-
dateTime: availabilityDateTime ?? undefined,
|
|
59
|
+
dateTime: ignoreCutoff ? undefined : (availabilityDateTime ?? undefined),
|
|
54
60
|
})
|
|
55
61
|
.then((rows) => {
|
|
56
62
|
if (cancelled) return;
|
|
57
|
-
|
|
63
|
+
let visibleRows = rows;
|
|
64
|
+
if (ignoreCutoff && availabilityDateTime && companyTimezone) {
|
|
65
|
+
const localDate = formatInTimeZone(
|
|
66
|
+
parseAvailabilityDateTime(availabilityDateTime),
|
|
67
|
+
companyTimezone,
|
|
68
|
+
'yyyy-MM-dd',
|
|
69
|
+
);
|
|
70
|
+
visibleRows = rows.filter((addOn) => !addOn.blackoutDates?.includes(localDate));
|
|
71
|
+
}
|
|
72
|
+
setAddOns(visibleRows);
|
|
58
73
|
setLoadedProductOptionId(availabilityProductOptionId);
|
|
59
74
|
})
|
|
60
75
|
.catch(() => {
|
|
@@ -65,7 +80,14 @@ export function useBookingAvailabilityAddOns({
|
|
|
65
80
|
return () => {
|
|
66
81
|
cancelled = true;
|
|
67
82
|
};
|
|
68
|
-
}, [
|
|
83
|
+
}, [
|
|
84
|
+
availabilityProductOptionId,
|
|
85
|
+
availabilityDateTime,
|
|
86
|
+
companyTimezone,
|
|
87
|
+
companyId,
|
|
88
|
+
ignoreCutoff,
|
|
89
|
+
onProductOptionChange,
|
|
90
|
+
]);
|
|
69
91
|
|
|
70
92
|
return {
|
|
71
93
|
addOns,
|
|
@@ -252,7 +252,7 @@ export function useChangeBookingProtectedPricing({
|
|
|
252
252
|
: null;
|
|
253
253
|
const name = variantLabel
|
|
254
254
|
? `${addOn.name} (${variantLabel})${qty > 1 ? ` \u00d7 ${qty}` : ''}`
|
|
255
|
-
: addOn.name
|
|
255
|
+
: `${addOn.name}${qty > 1 ? ` \u00d7 ${qty}` : ''}`;
|
|
256
256
|
return { name, totalAmount: amt, description: addOn.description ?? undefined };
|
|
257
257
|
})
|
|
258
258
|
.filter((x): x is NonNullable<typeof x> => x != null);
|
|
@@ -305,9 +305,12 @@ export function usePrivateShuttlePriceSummary({
|
|
|
305
305
|
: null;
|
|
306
306
|
const amount =
|
|
307
307
|
((addOn.price ?? 0) + (variant?.priceAdjustment ?? 0)) * (selection.quantity ?? 1);
|
|
308
|
+
const quantity = selection.quantity ?? 1;
|
|
308
309
|
lineItems.push({
|
|
309
310
|
type: 'FEE',
|
|
310
|
-
label: variant?.label
|
|
311
|
+
label: variant?.label
|
|
312
|
+
? `${addOn.name} (${variant.label})${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`
|
|
313
|
+
: `${addOn.name}${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`,
|
|
311
314
|
amount: roundMoney(amount),
|
|
312
315
|
});
|
|
313
316
|
}
|
|
@@ -419,13 +422,17 @@ export function usePrivateShuttlePriceSummary({
|
|
|
419
422
|
const hasVariant =
|
|
420
423
|
(addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
|
|
421
424
|
selection.variantId;
|
|
422
|
-
const
|
|
423
|
-
?
|
|
424
|
-
:
|
|
425
|
+
const variant = hasVariant
|
|
426
|
+
? addOn.variants?.find((candidate) => candidate.id === selection.variantId)
|
|
427
|
+
: null;
|
|
428
|
+
const adjustment = variant?.priceAdjustment ?? 0;
|
|
429
|
+
const quantity = selection.quantity ?? 1;
|
|
425
430
|
return {
|
|
426
431
|
kind: 'line',
|
|
427
|
-
label:
|
|
428
|
-
|
|
432
|
+
label: variant?.label
|
|
433
|
+
? `${addOn.name} (${variant.label})${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`
|
|
434
|
+
: `${addOn.name}${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`,
|
|
435
|
+
amount: (base + adjustment) * quantity,
|
|
429
436
|
type: 'fee',
|
|
430
437
|
};
|
|
431
438
|
})
|
|
@@ -267,7 +267,7 @@ export function useStandardBookingPriceSummary({
|
|
|
267
267
|
: null;
|
|
268
268
|
const name = variantLabel
|
|
269
269
|
? `${addOn.name} (${variantLabel})${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`
|
|
270
|
-
: addOn.name
|
|
270
|
+
: `${addOn.name}${quantity > 1 ? ` \u00d7 ${quantity}` : ''}`;
|
|
271
271
|
return { name, totalAmount: amount, description: addOn.description ?? undefined };
|
|
272
272
|
})
|
|
273
273
|
.filter((line): line is NonNullable<typeof line> => line != null);
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -802,19 +802,22 @@ export interface AddOnVariant {
|
|
|
802
802
|
id: string;
|
|
803
803
|
label: string;
|
|
804
804
|
priceAdjustment: number;
|
|
805
|
+
description?: string | null;
|
|
805
806
|
}
|
|
806
807
|
|
|
807
808
|
export interface AddOn {
|
|
808
809
|
addOnId: string;
|
|
809
810
|
name: string;
|
|
810
811
|
description?: string | null;
|
|
812
|
+
emoji?: string | null;
|
|
811
813
|
price: number;
|
|
812
814
|
currency: string;
|
|
813
815
|
preCheckout: boolean;
|
|
814
|
-
postCheckout: boolean;
|
|
815
816
|
variantType: 'none' | 'single_choice' | 'multi_quantity';
|
|
816
817
|
variants: AddOnVariant[];
|
|
817
818
|
productOptionIds: string[];
|
|
819
|
+
minHoursBeforeStart?: number | null;
|
|
820
|
+
blackoutDates?: string[];
|
|
818
821
|
}
|
|
819
822
|
|
|
820
823
|
export interface CompanySettings {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { selectedAddOnQuantity } from '../src/components/booking/add-on-selection-helpers';
|
|
4
|
+
|
|
5
|
+
test('selectedAddOnQuantity totals variants and treats an omitted quantity as one', () => {
|
|
6
|
+
assert.equal(
|
|
7
|
+
selectedAddOnQuantity('addon_lunch', [
|
|
8
|
+
{ addOnId: 'addon_lunch', quantity: 2 },
|
|
9
|
+
{ addOnId: 'addon_lunch' },
|
|
10
|
+
{ addOnId: 'addon_other', quantity: 9 },
|
|
11
|
+
]),
|
|
12
|
+
3,
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('selectedAddOnQuantity ignores zero and negative quantities', () => {
|
|
17
|
+
assert.equal(
|
|
18
|
+
selectedAddOnQuantity('addon_lunch', [
|
|
19
|
+
{ addOnId: 'addon_lunch', quantity: 0 },
|
|
20
|
+
{ addOnId: 'addon_lunch', quantity: -2 },
|
|
21
|
+
]),
|
|
22
|
+
0,
|
|
23
|
+
);
|
|
24
|
+
});
|
|
@@ -114,6 +114,7 @@ import {
|
|
|
114
114
|
resolveInitialPrivateShuttlePassengerCount,
|
|
115
115
|
} from '../src/components/booking/private-shuttle-passenger-count';
|
|
116
116
|
import { hasEffectiveAdminChangeSelection } from '../src/components/booking/useAdminProviderPricingAdjustments';
|
|
117
|
+
import { buildPrivateShuttleCheckoutArtifacts } from '../src/components/booking/private-shuttle-checkout-builders';
|
|
117
118
|
import {
|
|
118
119
|
lineFromActiveAdjustment,
|
|
119
120
|
reverseAdjustmentIdsForAdminCustomLines,
|
|
@@ -1903,7 +1904,6 @@ test('keeps an incompatible add-on visible until the admin explicitly removes it
|
|
|
1903
1904
|
price: 28,
|
|
1904
1905
|
currency: 'CAD',
|
|
1905
1906
|
preCheckout: true,
|
|
1906
|
-
postCheckout: false,
|
|
1907
1907
|
variantType: 'multi_quantity' as const,
|
|
1908
1908
|
variants: [
|
|
1909
1909
|
{
|
|
@@ -2264,3 +2264,44 @@ test('exposes only authoritative selectable amendment components to admin adjust
|
|
|
2264
2264
|
},
|
|
2265
2265
|
]);
|
|
2266
2266
|
});
|
|
2267
|
+
|
|
2268
|
+
test('private shuttle checkout receipt labels add-on variants and quantities', () => {
|
|
2269
|
+
const artifacts = buildPrivateShuttleCheckoutArtifacts({
|
|
2270
|
+
resourceCount: 1,
|
|
2271
|
+
resourcePrice: 2199.99,
|
|
2272
|
+
basePrice: 2199.99,
|
|
2273
|
+
addOnSelections: [
|
|
2274
|
+
{ addOnId: 'addon_croissants', variantId: 'butter', quantity: 2 },
|
|
2275
|
+
],
|
|
2276
|
+
addOns: [
|
|
2277
|
+
{
|
|
2278
|
+
addOnId: 'addon_croissants',
|
|
2279
|
+
name: 'Croissants',
|
|
2280
|
+
price: 5.99,
|
|
2281
|
+
currency: 'CAD',
|
|
2282
|
+
preCheckout: true,
|
|
2283
|
+
variantType: 'multi_quantity',
|
|
2284
|
+
variants: [{ id: 'butter', label: 'Butter Croissant', priceAdjustment: 0 }],
|
|
2285
|
+
productOptionIds: [],
|
|
2286
|
+
},
|
|
2287
|
+
],
|
|
2288
|
+
additionalHoursAmount: 0,
|
|
2289
|
+
additionalHoursCount: 0,
|
|
2290
|
+
perBookingFeeLineItems: [],
|
|
2291
|
+
cancellationPolicyFee: 0,
|
|
2292
|
+
effectiveTaxAmount: 0,
|
|
2293
|
+
taxLabel: 'Taxes and fees',
|
|
2294
|
+
effectivePromoDiscountAmount: 0,
|
|
2295
|
+
discountLabel: 'Discount',
|
|
2296
|
+
promoLineType: 'PROMO_CODE',
|
|
2297
|
+
depositInfo: null,
|
|
2298
|
+
totalPrice: 2211.97,
|
|
2299
|
+
currency: 'CAD',
|
|
2300
|
+
pricingV2Quote: null,
|
|
2301
|
+
roundingLabel: 'Rounding adjustment',
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
assert.deepEqual(artifacts.feeLineItemsForModal, [
|
|
2305
|
+
{ name: 'Croissants (Butter Croissant) \u00d7 2', totalAmount: 11.98 },
|
|
2306
|
+
]);
|
|
2307
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
EMERALD_LAKE_LUNCH_ADD_ON_ID,
|
|
5
|
+
WILD_FLOUR_LUNCH_ADD_ON_ID,
|
|
6
|
+
privateShuttleHiddenLunchAddOnId,
|
|
7
|
+
privateShuttleLunchAddOnIsVisible,
|
|
8
|
+
} from '../src/components/booking/private-shuttle-lunch-visibility';
|
|
9
|
+
|
|
10
|
+
test('private shuttle shows only Emerald Lake lunch when Emerald Lake is selected', () => {
|
|
11
|
+
const destinations = ['moraine_lake', 'emerald_lake'];
|
|
12
|
+
assert.equal(privateShuttleLunchAddOnIsVisible(EMERALD_LAKE_LUNCH_ADD_ON_ID, destinations), true);
|
|
13
|
+
assert.equal(privateShuttleLunchAddOnIsVisible(WILD_FLOUR_LUNCH_ADD_ON_ID, destinations), false);
|
|
14
|
+
assert.equal(privateShuttleHiddenLunchAddOnId(destinations), WILD_FLOUR_LUNCH_ADD_ON_ID);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('private shuttle shows only Wild Flour lunch without Emerald Lake', () => {
|
|
18
|
+
const destinations = ['moraine_lake', 'lake_louise'];
|
|
19
|
+
assert.equal(privateShuttleLunchAddOnIsVisible(EMERALD_LAKE_LUNCH_ADD_ON_ID, destinations), false);
|
|
20
|
+
assert.equal(privateShuttleLunchAddOnIsVisible(WILD_FLOUR_LUNCH_ADD_ON_ID, destinations), true);
|
|
21
|
+
assert.equal(privateShuttleHiddenLunchAddOnId(destinations), EMERALD_LAKE_LUNCH_ADD_ON_ID);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('private shuttle leaves non-lunch add-ons visible', () => {
|
|
25
|
+
assert.equal(privateShuttleLunchAddOnIsVisible('addon_animals', ['emerald_lake']), true);
|
|
26
|
+
assert.equal(privateShuttleLunchAddOnIsVisible('addon_breakfast', []), true);
|
|
27
|
+
});
|