@ticketboothapp/booking 1.2.176 → 1.2.178
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 +236 -144
- package/src/components/booking/AdminChangeBookingFlow.tsx +3 -2
- package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +4 -1
- package/src/components/booking/CollapsibleAddOnItem.module.css +72 -0
- package/src/components/booking/CollapsibleAddOnItem.tsx +38 -0
- package/src/components/booking/MealDrinkAddOnSelector.tsx +48 -27
- 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 +3 -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
|
@@ -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
|
@@ -808,13 +808,15 @@ export interface AddOn {
|
|
|
808
808
|
addOnId: string;
|
|
809
809
|
name: string;
|
|
810
810
|
description?: string | null;
|
|
811
|
+
emoji?: string | null;
|
|
811
812
|
price: number;
|
|
812
813
|
currency: string;
|
|
813
814
|
preCheckout: boolean;
|
|
814
|
-
postCheckout: boolean;
|
|
815
815
|
variantType: 'none' | 'single_choice' | 'multi_quantity';
|
|
816
816
|
variants: AddOnVariant[];
|
|
817
817
|
productOptionIds: string[];
|
|
818
|
+
minHoursBeforeStart?: number | null;
|
|
819
|
+
blackoutDates?: string[];
|
|
818
820
|
}
|
|
819
821
|
|
|
820
822
|
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
|
+
});
|