@ticketboothapp/booking 1.2.180 → 1.2.182
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/AdminChangeBookingFlow.tsx +12 -7
- package/src/components/booking/BookingDialog.tsx +9 -4
- package/src/components/booking/BookingProductGrid.module.css +22 -10
- package/src/components/booking/BookingProductGrid.tsx +2 -2
- package/src/components/booking/ChangeBookingDialog.tsx +3 -1
- package/src/components/booking/ChangeBookingFlow.tsx +1 -1
- package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +3 -0
- package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +3 -0
- package/src/components/booking/NewBookingFlow.tsx +6 -2
- package/src/components/booking/PrivateShuttleAddOnsSection.tsx +1 -1
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +21 -1
- package/src/components/booking/StandardBookingSelectionControlsPanel.tsx +9 -2
- package/src/components/booking/TicketSelector.module.css +8 -0
- package/src/components/booking/TicketSelector.tsx +8 -0
- package/src/components/booking/admin-change-flow-state-helpers.ts +35 -0
- package/src/components/booking/availability-cache-policy.ts +10 -0
- package/src/components/booking/booking-flow-types.ts +3 -0
- package/src/components/booking/booking-flow-ui.ts +10 -0
- package/src/components/booking/use-private-shuttle-availability.ts +5 -1
- package/src/components/booking/use-standard-booking-availability.ts +36 -10
- package/src/constants/pill-values.ts +0 -8
- package/src/constants/products.ts +2 -2
- package/src/data/product-descriptions/private-tour.en.json +1 -2
- package/src/index.ts +5 -0
- package/src/lib/booking/i18n/messages/en.json +1 -0
- package/src/lib/booking/i18n/messages/fr.json +1 -0
- package/src/lib/booking/partner-pricing-profile.ts +74 -0
- package/src/lib/booking/reservation-attempt.ts +138 -0
- package/src/lib/booking-api.ts +297 -71
- package/src/lib/env.ts +13 -0
- package/src/providers/booking-dialog-provider.tsx +3 -2
- package/src/public-partners.ts +12 -1
- package/src/runtime/types.ts +4 -0
- package/src/strings/en.json +1 -2
- package/src/strings/es.json +1 -2
- package/src/strings/fr.json +1 -2
- package/test/change-booking-helpers.test.ts +181 -1
- package/test/partner-pricing-profile.test.ts +46 -0
|
@@ -75,7 +75,7 @@ interface BookingDialogProviderProps {
|
|
|
75
75
|
|
|
76
76
|
export function BookingDialogProvider({ children }: BookingDialogProviderProps) {
|
|
77
77
|
const host = useBookingHostOptional();
|
|
78
|
-
const apiUrl = host?.env.API_URL;
|
|
78
|
+
const apiUrl = host?.env.BOOKING_READ_API_URL ?? host?.env.API_URL;
|
|
79
79
|
|
|
80
80
|
const reportSuspiciousBookingProductId = useCallback(
|
|
81
81
|
(original: string, sanitized: string): void => {
|
|
@@ -84,6 +84,7 @@ export function BookingDialogProvider({ children }: BookingDialogProviderProps)
|
|
|
84
84
|
const correlationId = getOrCreateBookingCorrelationId();
|
|
85
85
|
const event = {
|
|
86
86
|
event: 'BOOKING_DIALOG_SUSPICIOUS_PRODUCT_ID',
|
|
87
|
+
clientBuildId: host?.env.BOOKING_CLIENT_BUILD_ID ?? 'source-unknown',
|
|
87
88
|
endpoint: '/booking-open',
|
|
88
89
|
correlationId,
|
|
89
90
|
originalProductId: original,
|
|
@@ -103,7 +104,7 @@ export function BookingDialogProvider({ children }: BookingDialogProviderProps)
|
|
|
103
104
|
keepalive: true,
|
|
104
105
|
}).catch(() => {});
|
|
105
106
|
},
|
|
106
|
-
[apiUrl]
|
|
107
|
+
[apiUrl, host?.env.BOOKING_CLIENT_BUILD_ID]
|
|
107
108
|
);
|
|
108
109
|
|
|
109
110
|
const [isOpen, setIsOpen] = useState(false);
|
package/src/public-partners.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* Uses NEXT_PUBLIC_* env from the consuming Next app at build time.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
parsePartnerPricingProfileOverrides,
|
|
8
|
+
type PartnerPricingProfileOverride,
|
|
9
|
+
} from './lib/booking/partner-pricing-profile';
|
|
10
|
+
|
|
6
11
|
function apiBase(): string {
|
|
7
12
|
const u = process.env.NEXT_PUBLIC_API_URL;
|
|
8
13
|
if (!u) {
|
|
@@ -51,6 +56,8 @@ export interface PublicPartner {
|
|
|
51
56
|
anonymousBookAllowed?: boolean;
|
|
52
57
|
/** B2B / partner pricing profile id when the API exposes it (optional). */
|
|
53
58
|
pricingProfileId?: string;
|
|
59
|
+
/** Month-specific profiles; first matching rule wins and pricingProfileId is the fallback. */
|
|
60
|
+
pricingProfileOverrides?: PartnerPricingProfileOverride[];
|
|
54
61
|
/** Partner cancellation-policy profile id when the API exposes it (optional). */
|
|
55
62
|
cancellationPolicyProfileId?: string;
|
|
56
63
|
}
|
|
@@ -67,7 +74,7 @@ export interface PublicStaffPortalSignInOption {
|
|
|
67
74
|
displayName: string;
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
function mapPartnerApiRow(
|
|
77
|
+
export function mapPartnerApiRow(
|
|
71
78
|
r: Record<string, unknown> & { partnerId: string; name: string },
|
|
72
79
|
): PublicPartner {
|
|
73
80
|
const pickupLocationIds = parsePickupLocationIds(r);
|
|
@@ -83,6 +90,9 @@ function mapPartnerApiRow(
|
|
|
83
90
|
? cap.pricingProfileId
|
|
84
91
|
: '';
|
|
85
92
|
const pricingProfileId = rawProfile.trim() || undefined;
|
|
93
|
+
const pricingProfileOverrides = parsePartnerPricingProfileOverrides(
|
|
94
|
+
r.pricingProfileOverrides ?? cap?.pricingProfileOverrides,
|
|
95
|
+
);
|
|
86
96
|
const rawCancellationProfile =
|
|
87
97
|
typeof r.cancellationPolicyProfileId === 'string'
|
|
88
98
|
? r.cancellationPolicyProfileId
|
|
@@ -96,6 +106,7 @@ function mapPartnerApiRow(
|
|
|
96
106
|
anonymousBookAllowed,
|
|
97
107
|
...(pickupLocationIds?.length ? { pickupLocationIds } : {}),
|
|
98
108
|
...(pricingProfileId ? { pricingProfileId } : {}),
|
|
109
|
+
...(pricingProfileOverrides.length > 0 ? { pricingProfileOverrides } : {}),
|
|
99
110
|
...(cancellationPolicyProfileId ? { cancellationPolicyProfileId } : {}),
|
|
100
111
|
};
|
|
101
112
|
}
|
package/src/runtime/types.ts
CHANGED
|
@@ -10,6 +10,10 @@ export type BookingSlotComponent = ComponentType<any>;
|
|
|
10
10
|
*/
|
|
11
11
|
export interface BookingRuntimeEnv {
|
|
12
12
|
readonly API_URL: string;
|
|
13
|
+
/** Optional same-origin base for public booking reads and client telemetry. */
|
|
14
|
+
readonly BOOKING_READ_API_URL?: string;
|
|
15
|
+
/** Immutable source/build identifier attached to browser telemetry. */
|
|
16
|
+
readonly BOOKING_CLIENT_BUILD_ID: string;
|
|
13
17
|
readonly COMPANY_ID: string;
|
|
14
18
|
readonly GOOGLE_MAPS_API_KEY: string;
|
|
15
19
|
readonly STRIPE_PUBLISHABLE_KEY: string;
|
package/src/strings/en.json
CHANGED
|
@@ -593,7 +593,6 @@
|
|
|
593
593
|
"hike": "Perfect for hiking",
|
|
594
594
|
"canoe": "Rent a canoe",
|
|
595
595
|
"lunch": "Lunch at Emerald Lake Lodge",
|
|
596
|
-
"croissant": "Croissants included",
|
|
597
596
|
"hotDrinks": "Hot drinks",
|
|
598
597
|
"blankets": "Cozy blankets",
|
|
599
598
|
"emeraldLakeEscapeTourLocations": {
|
|
@@ -1511,7 +1510,7 @@
|
|
|
1511
1510
|
{
|
|
1512
1511
|
"pagesIncluded": [],
|
|
1513
1512
|
"question": "Do you provide breakfast during your tours?",
|
|
1514
|
-
"answer": "
|
|
1513
|
+
"answer": "Our private and sunrise shuttles have a breakfast option available for add-on. Our other tours do not include breakfast, so please be sure to bring your own or purchase a snack at the Moraine Lake Lodge (open from 9 AM - 4 PM)."
|
|
1515
1514
|
},
|
|
1516
1515
|
{
|
|
1517
1516
|
"pagesIncluded": [],
|
package/src/strings/es.json
CHANGED
|
@@ -392,7 +392,6 @@
|
|
|
392
392
|
"hike": "Perfecto para senderismo",
|
|
393
393
|
"canoe": "Alquila una canoa",
|
|
394
394
|
"lunch": "Almuerzo en Emerald Lake Lodge",
|
|
395
|
-
"croissant": "Crusanes incluidos",
|
|
396
395
|
"hotDrinks": "Bebidas calientes",
|
|
397
396
|
"blankets": "Mantas calentitas",
|
|
398
397
|
"emeraldLakeEscapeTourLocations": {
|
|
@@ -1309,7 +1308,7 @@
|
|
|
1309
1308
|
{
|
|
1310
1309
|
"pagesIncluded": [],
|
|
1311
1310
|
"question": "Do you provide breakfast during your tours?",
|
|
1312
|
-
"answer": "
|
|
1311
|
+
"answer": "Our private and sunrise shuttles have a breakfast option available for add-on. Our other tours do not include breakfast, so please be sure to bring your own or purchase a snack at the Moraine Lake Lodge (open from 9 AM - 4 PM)."
|
|
1313
1312
|
},
|
|
1314
1313
|
{
|
|
1315
1314
|
"pagesIncluded": [],
|
package/src/strings/fr.json
CHANGED
|
@@ -392,7 +392,6 @@
|
|
|
392
392
|
"hike": "Idéal pour randonner",
|
|
393
393
|
"canoe": "Location de canoé",
|
|
394
394
|
"lunch": "Déjeuner aux lodges du lac Émeraude",
|
|
395
|
-
"croissant": "Croissants inclus",
|
|
396
395
|
"hotDrinks": "Boissons chaudes",
|
|
397
396
|
"blankets": "Mantes chaudes",
|
|
398
397
|
"emeraldLakeEscapeTourLocations": {
|
|
@@ -1309,7 +1308,7 @@
|
|
|
1309
1308
|
{
|
|
1310
1309
|
"pagesIncluded": [],
|
|
1311
1310
|
"question": "Do you provide breakfast during your tours?",
|
|
1312
|
-
"answer": "
|
|
1311
|
+
"answer": "Our private and sunrise shuttles have a breakfast option available for add-on. Our other tours do not include breakfast, so please be sure to bring your own or purchase a snack at the Moraine Lake Lodge (open from 9 AM - 4 PM)."
|
|
1313
1312
|
},
|
|
1314
1313
|
{
|
|
1315
1314
|
"pagesIncluded": [],
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ChangeQuoteUiSlice,
|
|
11
11
|
} from '../src/lib/booking/change-flow-pricing';
|
|
12
12
|
import {
|
|
13
|
+
createReservation,
|
|
13
14
|
mapAdminChangeBookingQuoteV2Data,
|
|
14
15
|
type Availability,
|
|
15
16
|
type ChangeBookingQuoteResponse,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
findFirstDateWithBookableAvailability,
|
|
23
24
|
selectedDateHasVisibleAvailability,
|
|
24
25
|
} from '../src/components/booking/availability-date-selection';
|
|
26
|
+
import { shouldRevalidateAvailabilityCache } from '../src/components/booking/availability-cache-policy';
|
|
25
27
|
import {
|
|
26
28
|
buildChangeBookingServerPreview,
|
|
27
29
|
labelAdminAmendmentPriceSummaryLines,
|
|
@@ -71,6 +73,11 @@ import {
|
|
|
71
73
|
resolveHydratedPrivateShuttleAvailability,
|
|
72
74
|
} from '../src/components/booking/private-shuttle-availability';
|
|
73
75
|
import { sanitizeBookingSourceUrl } from '../src/lib/booking/source-metadata';
|
|
76
|
+
import {
|
|
77
|
+
getOrCreateReservationAttempt,
|
|
78
|
+
reservationIdempotencyEnabled,
|
|
79
|
+
type ReservationAttemptStorage,
|
|
80
|
+
} from '../src/lib/booking/reservation-attempt';
|
|
74
81
|
import {
|
|
75
82
|
formatReservationHoldTime,
|
|
76
83
|
reservationHoldHasExpired,
|
|
@@ -91,7 +98,10 @@ import {
|
|
|
91
98
|
buildAdminChangePayNowCheckoutModalData,
|
|
92
99
|
shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose,
|
|
93
100
|
} from '../src/components/booking/admin-change-payment-choice-runner';
|
|
94
|
-
import {
|
|
101
|
+
import {
|
|
102
|
+
filterAdminChangeCalendarAvailabilities,
|
|
103
|
+
shouldLockExistingAddOnQuantities,
|
|
104
|
+
} from '../src/components/booking/admin-change-flow-state-helpers';
|
|
95
105
|
import { haveAddOnSelectionsChanged } from '../src/components/booking/useChangeBookingSelectionDetails';
|
|
96
106
|
import {
|
|
97
107
|
incompatibleAddOnSelections,
|
|
@@ -157,6 +167,12 @@ function test(name: string, fn: () => void | Promise<void>): void {
|
|
|
157
167
|
}
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
test('availability cache revalidates only when a covered range is stale', () => {
|
|
171
|
+
assert.equal(shouldRevalidateAvailabilityCache(true, false), false);
|
|
172
|
+
assert.equal(shouldRevalidateAvailabilityCache(true, true), true);
|
|
173
|
+
assert.equal(shouldRevalidateAvailabilityCache(false, true), false);
|
|
174
|
+
});
|
|
175
|
+
|
|
160
176
|
function calendarAvailability(dateTime: string, vacancies: number): Availability {
|
|
161
177
|
return { dateTime, vacancies, currency: 'CAD' };
|
|
162
178
|
}
|
|
@@ -407,6 +423,48 @@ test('admin availability filtering removes only starts that have passed', () =>
|
|
|
407
423
|
);
|
|
408
424
|
});
|
|
409
425
|
|
|
426
|
+
test('admin change availability filtering keeps the in-progress booking date selectable', () => {
|
|
427
|
+
const now = new Date('2026-08-01T13:00:00-06:00');
|
|
428
|
+
const rows = [
|
|
429
|
+
calendarAvailability('2026-07-31T09:00:00-06:00', 4),
|
|
430
|
+
calendarAvailability('2026-08-01T09:00:00-06:00', 4),
|
|
431
|
+
calendarAvailability('2026-08-01T11:00:00-06:00', 4),
|
|
432
|
+
calendarAvailability('2026-08-02T09:00:00-06:00', 4),
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
assert.deepEqual(
|
|
436
|
+
filterAdminChangeCalendarAvailabilities({
|
|
437
|
+
availabilities: rows,
|
|
438
|
+
originalDate: '2026-08-01',
|
|
439
|
+
companyTimezone: 'America/Edmonton',
|
|
440
|
+
now,
|
|
441
|
+
}).map((row) => row.dateTime),
|
|
442
|
+
[
|
|
443
|
+
'2026-08-01T09:00:00-06:00',
|
|
444
|
+
'2026-08-01T11:00:00-06:00',
|
|
445
|
+
'2026-08-02T09:00:00-06:00',
|
|
446
|
+
],
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('admin change availability filtering keeps the normal time guard without an original date', () => {
|
|
451
|
+
const now = new Date('2026-08-01T13:00:00-06:00');
|
|
452
|
+
const rows = [
|
|
453
|
+
calendarAvailability('2026-08-01T09:00:00-06:00', 4),
|
|
454
|
+
calendarAvailability('2026-08-01T14:00:00-06:00', 4),
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
assert.deepEqual(
|
|
458
|
+
filterAdminChangeCalendarAvailabilities({
|
|
459
|
+
availabilities: rows,
|
|
460
|
+
originalDate: null,
|
|
461
|
+
companyTimezone: 'America/Edmonton',
|
|
462
|
+
now,
|
|
463
|
+
}).map((row) => row.dateTime),
|
|
464
|
+
['2026-08-01T14:00:00-06:00'],
|
|
465
|
+
);
|
|
466
|
+
});
|
|
467
|
+
|
|
410
468
|
test('public availability filtering keeps its stricter advance cutoff', () => {
|
|
411
469
|
const now = new Date('2026-07-21T17:00:00-06:00');
|
|
412
470
|
const rows = [
|
|
@@ -2305,3 +2363,125 @@ test('private shuttle checkout receipt labels add-on variants and quantities', (
|
|
|
2305
2363
|
{ name: 'Croissants (Butter Croissant) \u00d7 2', totalAmount: 11.98 },
|
|
2306
2364
|
]);
|
|
2307
2365
|
});
|
|
2366
|
+
|
|
2367
|
+
test('reservation attempt reuses one idempotency key for an identical retry', async () => {
|
|
2368
|
+
const values = new Map<string, string>();
|
|
2369
|
+
const storage: ReservationAttemptStorage = {
|
|
2370
|
+
getItem: (key) => values.get(key) ?? null,
|
|
2371
|
+
setItem: (key, value) => values.set(key, value),
|
|
2372
|
+
removeItem: (key) => values.delete(key),
|
|
2373
|
+
};
|
|
2374
|
+
const request = {
|
|
2375
|
+
productId: 'po_test123',
|
|
2376
|
+
dateTime: '2026-08-02T08:00:00-06:00',
|
|
2377
|
+
bookingItems: [{ category: 'ADULT', count: 2 }],
|
|
2378
|
+
};
|
|
2379
|
+
const options = {
|
|
2380
|
+
storage,
|
|
2381
|
+
now: 1_000,
|
|
2382
|
+
createId: () => 'attempt_1234567890abcdef',
|
|
2383
|
+
digest: async (value: string) => `digest-${value.length}`,
|
|
2384
|
+
};
|
|
2385
|
+
|
|
2386
|
+
const first = await getOrCreateReservationAttempt(request, options);
|
|
2387
|
+
const retry = await getOrCreateReservationAttempt(request, {
|
|
2388
|
+
...options,
|
|
2389
|
+
now: 2_000,
|
|
2390
|
+
createId: () => 'attempt_should_not_be_used',
|
|
2391
|
+
});
|
|
2392
|
+
|
|
2393
|
+
assert.equal(first.idempotencyKey, 'attempt_1234567890abcdef');
|
|
2394
|
+
assert.equal(retry.idempotencyKey, first.idempotencyKey);
|
|
2395
|
+
assert.equal(retry.request.idempotencyKey, first.idempotencyKey);
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
test('completed reservation attempt is cleared before a later logical attempt', async () => {
|
|
2399
|
+
const values = new Map<string, string>();
|
|
2400
|
+
const storage: ReservationAttemptStorage = {
|
|
2401
|
+
getItem: (key) => values.get(key) ?? null,
|
|
2402
|
+
setItem: (key, value) => values.set(key, value),
|
|
2403
|
+
removeItem: (key) => values.delete(key),
|
|
2404
|
+
};
|
|
2405
|
+
const request = { productId: 'po_test123', dateTime: '2026-08-02', bookingItems: [] };
|
|
2406
|
+
const digest = async () => 'same-request';
|
|
2407
|
+
const first = await getOrCreateReservationAttempt(request, {
|
|
2408
|
+
storage,
|
|
2409
|
+
digest,
|
|
2410
|
+
createId: () => 'attempt_1234567890abcdef',
|
|
2411
|
+
});
|
|
2412
|
+
first.clear();
|
|
2413
|
+
const later = await getOrCreateReservationAttempt(request, {
|
|
2414
|
+
storage,
|
|
2415
|
+
digest,
|
|
2416
|
+
createId: () => 'attempt_fedcba0987654321',
|
|
2417
|
+
});
|
|
2418
|
+
|
|
2419
|
+
assert.equal(later.idempotencyKey, 'attempt_fedcba0987654321');
|
|
2420
|
+
});
|
|
2421
|
+
|
|
2422
|
+
test('reservation idempotency requires an explicit frontend build flag', () => {
|
|
2423
|
+
assert.equal(reservationIdempotencyEnabled(undefined), false);
|
|
2424
|
+
assert.equal(reservationIdempotencyEnabled('TRUE'), true);
|
|
2425
|
+
});
|
|
2426
|
+
|
|
2427
|
+
test('lost reserve response is recovered without submitting a second reservation', async () => {
|
|
2428
|
+
const originalFetch = globalThis.fetch;
|
|
2429
|
+
const originalFlag = process.env.NEXT_PUBLIC_RESERVATION_IDEMPOTENCY_ENABLED;
|
|
2430
|
+
const calls: Array<{ url: string; body: { data?: { idempotencyKey?: string } } }> = [];
|
|
2431
|
+
let scenario: 'network-loss' | 'gateway-timeout' = 'network-loss';
|
|
2432
|
+
process.env.NEXT_PUBLIC_RESERVATION_IDEMPOTENCY_ENABLED = 'true';
|
|
2433
|
+
globalThis.fetch = async (input, init) => {
|
|
2434
|
+
const url = String(input);
|
|
2435
|
+
const body = JSON.parse(String(init?.body ?? '{}')) as { data?: { idempotencyKey?: string } };
|
|
2436
|
+
calls.push({ url, body });
|
|
2437
|
+
if (url.endsWith('/1/reserve')) {
|
|
2438
|
+
if (scenario === 'network-loss') {
|
|
2439
|
+
throw new Error('connection closed after request was sent');
|
|
2440
|
+
}
|
|
2441
|
+
return new Response('Gateway timeout', { status: 504 });
|
|
2442
|
+
}
|
|
2443
|
+
return new Response(
|
|
2444
|
+
JSON.stringify({
|
|
2445
|
+
data: {
|
|
2446
|
+
reservationReference:
|
|
2447
|
+
scenario === 'network-loss' ? 'resRef_RECOVERED' : 'resRef_TIMEOUT_RECOVERED',
|
|
2448
|
+
reservationExpiration: '2026-08-01T18:10:00Z',
|
|
2449
|
+
},
|
|
2450
|
+
}),
|
|
2451
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
2452
|
+
);
|
|
2453
|
+
};
|
|
2454
|
+
|
|
2455
|
+
try {
|
|
2456
|
+
const result = await createReservation({
|
|
2457
|
+
productId: 'po_test123',
|
|
2458
|
+
dateTime: '2026-08-02T08:00:00-06:00',
|
|
2459
|
+
bookingItems: [{ category: 'ADULT', count: 2 }],
|
|
2460
|
+
});
|
|
2461
|
+
|
|
2462
|
+
assert.equal(result.reservationReference, 'resRef_RECOVERED');
|
|
2463
|
+
assert.equal(calls.filter((call) => call.url.endsWith('/1/reserve')).length, 1);
|
|
2464
|
+
assert.equal(calls.filter((call) => call.url.endsWith('/1/reserve/status')).length, 1);
|
|
2465
|
+
assert.match(calls[0].body.data?.idempotencyKey ?? '', /^attempt_[a-f0-9]{32}$/);
|
|
2466
|
+
assert.equal(calls[1].body.data?.idempotencyKey, calls[0].body.data?.idempotencyKey);
|
|
2467
|
+
|
|
2468
|
+
scenario = 'gateway-timeout';
|
|
2469
|
+
calls.length = 0;
|
|
2470
|
+
const timeoutResult = await createReservation({
|
|
2471
|
+
productId: 'po_test123',
|
|
2472
|
+
dateTime: '2026-08-03T08:00:00-06:00',
|
|
2473
|
+
bookingItems: [{ category: 'ADULT', count: 2 }],
|
|
2474
|
+
});
|
|
2475
|
+
|
|
2476
|
+
assert.equal(timeoutResult.reservationReference, 'resRef_TIMEOUT_RECOVERED');
|
|
2477
|
+
assert.equal(calls.filter((call) => call.url.endsWith('/1/reserve')).length, 1);
|
|
2478
|
+
assert.equal(calls.filter((call) => call.url.endsWith('/1/reserve/status')).length, 1);
|
|
2479
|
+
} finally {
|
|
2480
|
+
globalThis.fetch = originalFetch;
|
|
2481
|
+
if (originalFlag === undefined) {
|
|
2482
|
+
delete process.env.NEXT_PUBLIC_RESERVATION_IDEMPOTENCY_ENABLED;
|
|
2483
|
+
} else {
|
|
2484
|
+
process.env.NEXT_PUBLIC_RESERVATION_IDEMPOTENCY_ENABLED = originalFlag;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { mapPartnerApiRow } from '../src/public-partners';
|
|
4
|
+
import {
|
|
5
|
+
parsePartnerPricingProfileOverrides,
|
|
6
|
+
resolvePartnerPricingProfileId,
|
|
7
|
+
} from '../src/lib/booking/partner-pricing-profile';
|
|
8
|
+
|
|
9
|
+
const overrides = parsePartnerPricingProfileOverrides([
|
|
10
|
+
{
|
|
11
|
+
pricingProfileId: 'pp_banff_adventures_july_aug_sept',
|
|
12
|
+
months: [7, 8, 9],
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
for (const bookingDate of ['2026-07-01', '2026-08-02', '2026-09-30']) {
|
|
17
|
+
assert.equal(
|
|
18
|
+
resolvePartnerPricingProfileId('pp_banff_adventures', overrides, bookingDate),
|
|
19
|
+
'pp_banff_adventures_july_aug_sept',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
for (const bookingDate of ['2026-06-30', '2026-10-01']) {
|
|
23
|
+
assert.equal(
|
|
24
|
+
resolvePartnerPricingProfileId('pp_banff_adventures', overrides, bookingDate),
|
|
25
|
+
'pp_banff_adventures',
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const mapped = mapPartnerApiRow({
|
|
30
|
+
partnerId: 'par_U4md9ZBsf3uR',
|
|
31
|
+
name: 'Banff Adventures',
|
|
32
|
+
capabilities: {
|
|
33
|
+
pricingProfileId: 'pp_banff_adventures',
|
|
34
|
+
pricingProfileOverrides: [
|
|
35
|
+
{
|
|
36
|
+
pricingProfileId: 'pp_banff_adventures_july_aug_sept',
|
|
37
|
+
months: [7, 8, 9],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
assert.equal(mapped.pricingProfileId, 'pp_banff_adventures');
|
|
44
|
+
assert.deepEqual(mapped.pricingProfileOverrides, overrides);
|
|
45
|
+
|
|
46
|
+
console.log('partner pricing profile tests passed');
|