@ticketboothapp/booking 1.2.167 → 1.2.169
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/AdminChangeBookingContent.tsx +13 -1
- package/src/components/booking/AdminChangeBookingFlow.tsx +180 -65
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +43 -13
- package/src/components/booking/AdminChangePricingPanel.tsx +400 -134
- package/src/components/booking/AdminChangeReceiptComparison.tsx +166 -145
- package/src/components/booking/ChangeBookingFlow.tsx +7 -3
- package/src/components/booking/ChangeBookingQuoteStatusPlaceholder.tsx +7 -2
- package/src/components/booking/ChangeBookingSelectionControlsPanel.tsx +6 -0
- package/src/components/booking/ChangeBookingTicketsAndAddOnsPanel.tsx +80 -0
- package/src/components/booking/PickupLocationSelector.module.css +22 -0
- package/src/components/booking/PickupLocationSelector.tsx +3 -3
- package/src/components/booking/PriceSummary.tsx +4 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +27 -13
- package/src/components/booking/PrivateShuttleCheckoutSection.tsx +95 -54
- package/src/components/booking/admin-adjustment-components.ts +44 -0
- package/src/components/booking/admin-adjustment-tax-behavior.ts +50 -0
- package/src/components/booking/admin-change-flow-state-helpers.ts +10 -0
- package/src/components/booking/admin-change-provider-payload.ts +3 -0
- package/src/components/booking/admin-change-quote-request-key.ts +27 -0
- package/src/components/booking/admin-change-v2-quote-request.ts +10 -2
- package/src/components/booking/admin-refund-decision-context.ts +37 -0
- package/src/components/booking/admin-refund-disposition.ts +71 -2
- package/src/components/booking/booking-flow-types.ts +4 -0
- package/src/components/booking/booking-flow.css +5 -0
- package/src/components/booking/change-booking-error-message.ts +137 -0
- package/src/components/booking/change-booking-flow-helpers.ts +77 -2
- package/src/components/booking/change-booking-quote-guards.ts +4 -1
- package/src/components/booking/change-booking-quote-state.ts +44 -1
- package/src/components/booking/incompatible-add-on-selections.ts +19 -0
- package/src/components/booking/private-shuttle-availability.ts +14 -0
- package/src/components/booking/private-shuttle-cancellation-policy.ts +9 -0
- package/src/components/booking/private-shuttle-checkout-controller.ts +5 -0
- package/src/components/booking/private-shuttle-provider-change-runner.ts +21 -0
- package/src/components/booking/private-shuttle-reservation-runner.ts +5 -3
- package/src/components/booking/provider-dashboard-change-booking.ts +16 -1
- package/src/components/booking/useAdminChangeCheckoutController.ts +16 -0
- package/src/components/booking/useAdminChangeProductReset.ts +6 -0
- package/src/components/booking/useAdminChangeProtectedPricing.ts +4 -0
- package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +6 -4
- package/src/components/booking/useAdminChangeQuotePreview.ts +76 -20
- package/src/components/booking/useAdminCustomReceiptLines.ts +171 -10
- package/src/components/booking/useAdminProviderPricingAdjustments.ts +50 -19
- package/src/components/booking/useBookingAvailabilityAddOns.ts +19 -2
- package/src/components/booking/useChangeBookingAddOnFloorController.ts +15 -9
- package/src/components/booking/useChangeBookingInitialHydration.ts +10 -7
- package/src/components/booking/useChangeBookingQuotePreview.ts +3 -1
- package/src/components/booking/useChangeBookingSelectionDetails.ts +23 -17
- package/src/lib/booking/change-booking-server-preview.ts +26 -9
- package/src/lib/booking/change-flow-pricing.ts +12 -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-api.ts +64 -0
- package/test/change-booking-helpers.test.ts +853 -3
- package/src/components/booking/useAdminChangePricingDebugPanel.tsx +0 -178
|
@@ -24,12 +24,24 @@ import {
|
|
|
24
24
|
} from '../src/components/booking/availability-date-selection';
|
|
25
25
|
import {
|
|
26
26
|
buildChangeBookingServerPreview,
|
|
27
|
+
labelAdminAmendmentPriceSummaryLines,
|
|
27
28
|
mapQuoteLineItemsToPriceSummaryLines,
|
|
28
29
|
} from '../src/lib/booking/change-booking-server-preview';
|
|
29
30
|
import {
|
|
30
31
|
buildAdminChangeQuoteRequestKey,
|
|
32
|
+
canRequestAdminChangeQuote,
|
|
31
33
|
canReuseCompletedAdminChangeQuote,
|
|
32
34
|
} from '../src/components/booking/admin-change-quote-request-key';
|
|
35
|
+
import {
|
|
36
|
+
authoritativeItineraryDisplayFromChangeQuote,
|
|
37
|
+
sliceAndMergeChangeQuoteForUi,
|
|
38
|
+
} from '../src/components/booking/change-booking-quote-state';
|
|
39
|
+
import { selectableAdminAdjustmentComponentsFromQuote } from '../src/components/booking/admin-adjustment-components';
|
|
40
|
+
import {
|
|
41
|
+
adminAdjustmentTaxBehaviorOptions,
|
|
42
|
+
normalizeAdminAdjustmentScopeForSign,
|
|
43
|
+
normalizeAdminAdjustmentTaxBehavior,
|
|
44
|
+
} from '../src/components/booking/admin-adjustment-tax-behavior';
|
|
33
45
|
import {
|
|
34
46
|
buildAdminChangeV2QuoteRequest,
|
|
35
47
|
quoteAdminChangeV2ForUi,
|
|
@@ -37,16 +49,21 @@ import {
|
|
|
37
49
|
import {
|
|
38
50
|
noRefundRemovedValue,
|
|
39
51
|
normalizeReturnPriceTreatment,
|
|
52
|
+
adminRemovalOperationLabel,
|
|
53
|
+
refundTreatmentDisplayValue,
|
|
40
54
|
refundDecisionsComplete,
|
|
41
55
|
releaseRefundDispositionOptions,
|
|
42
56
|
returnPriceTreatmentOptions,
|
|
43
57
|
} from '../src/components/booking/admin-refund-disposition';
|
|
44
58
|
import {
|
|
59
|
+
defaultKeepOriginalReturnPriceDispositions,
|
|
60
|
+
defaultPendingRefundDispositions,
|
|
45
61
|
mergeAdminRefundDecisionContext,
|
|
46
62
|
selectRefundDispositionsForOperations,
|
|
47
63
|
} from '../src/components/booking/admin-refund-decision-context';
|
|
48
64
|
import {
|
|
49
65
|
applyResourceAdjustments,
|
|
66
|
+
privateShuttleInitialDateKey,
|
|
50
67
|
privateShuttlePriceChanged,
|
|
51
68
|
resolveHydratedPrivateShuttleAvailability,
|
|
52
69
|
} from '../src/components/booking/private-shuttle-availability';
|
|
@@ -57,13 +74,36 @@ import {
|
|
|
57
74
|
reservationHoldSecondsRemaining,
|
|
58
75
|
} from '../src/components/booking/reservation-hold';
|
|
59
76
|
import { buildAdminChangeProviderPayload } from '../src/components/booking/admin-change-provider-payload';
|
|
60
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
buildPublicChangeAmountDueSummary,
|
|
79
|
+
deriveCompatibleInitialBookingQuantities,
|
|
80
|
+
isProductFamilyReceiptConversion,
|
|
81
|
+
resolveChangeBookingCancellationPolicyId,
|
|
82
|
+
resolveInitialAvailabilityFromBooking,
|
|
83
|
+
} from '../src/components/booking/change-booking-flow-helpers';
|
|
61
84
|
import { collapseAdminPromoLines } from '../src/components/booking/admin-change-receipt-lines';
|
|
62
85
|
import {
|
|
63
86
|
buildAdminChangePaymentChoiceData,
|
|
64
87
|
buildAdminChangePayNowCheckoutModalData,
|
|
65
88
|
shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose,
|
|
66
89
|
} from '../src/components/booking/admin-change-payment-choice-runner';
|
|
90
|
+
import { shouldLockExistingAddOnQuantities } from '../src/components/booking/admin-change-flow-state-helpers';
|
|
91
|
+
import { haveAddOnSelectionsChanged } from '../src/components/booking/useChangeBookingSelectionDetails';
|
|
92
|
+
import {
|
|
93
|
+
incompatibleAddOnSelections,
|
|
94
|
+
removeAddOnSelections,
|
|
95
|
+
} from '../src/components/booking/incompatible-add-on-selections';
|
|
96
|
+
import {
|
|
97
|
+
friendlyChangeBookingError,
|
|
98
|
+
incompatibleAddOnChangeError,
|
|
99
|
+
} from '../src/components/booking/change-booking-error-message';
|
|
100
|
+
import { applyPrivateShuttleProviderChange } from '../src/components/booking/private-shuttle-provider-change-runner';
|
|
101
|
+
import { resolvePrivateShuttleCancellationPolicyId } from '../src/components/booking/private-shuttle-cancellation-policy';
|
|
102
|
+
import { hasEffectiveAdminChangeSelection } from '../src/components/booking/useAdminProviderPricingAdjustments';
|
|
103
|
+
import {
|
|
104
|
+
lineFromActiveAdjustment,
|
|
105
|
+
reverseAdjustmentIdsForAdminCustomLines,
|
|
106
|
+
} from '../src/components/booking/useAdminCustomReceiptLines';
|
|
67
107
|
|
|
68
108
|
function quote(overrides: Partial<ChangeBookingQuoteResponse>): ChangeBookingQuoteResponse {
|
|
69
109
|
return {
|
|
@@ -106,6 +146,195 @@ function calendarAvailability(dateTime: string, vacancies: number): Availability
|
|
|
106
146
|
return { dateTime, vacancies, currency: 'CAD' };
|
|
107
147
|
}
|
|
108
148
|
|
|
149
|
+
test('complete percentage-only admin adjustment requests a server quote', () => {
|
|
150
|
+
assert.equal(
|
|
151
|
+
hasEffectiveAdminChangeSelection({
|
|
152
|
+
hasChangeSelection: false,
|
|
153
|
+
providerPricingOverrideCount: 0,
|
|
154
|
+
providerHasAdditionalAdjustments: false,
|
|
155
|
+
isProviderDashboardChange: false,
|
|
156
|
+
editableSummaryPreSubtotalDelta: 0,
|
|
157
|
+
adminStructuredAdjustmentCount: 1,
|
|
158
|
+
}),
|
|
159
|
+
true,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('reversing an existing admin adjustment is an effective change', () => {
|
|
164
|
+
assert.equal(
|
|
165
|
+
hasEffectiveAdminChangeSelection({
|
|
166
|
+
hasChangeSelection: false,
|
|
167
|
+
providerPricingOverrideCount: 0,
|
|
168
|
+
providerHasAdditionalAdjustments: false,
|
|
169
|
+
isProviderDashboardChange: true,
|
|
170
|
+
editableSummaryPreSubtotalDelta: 0,
|
|
171
|
+
adminStructuredAdjustmentCount: 0,
|
|
172
|
+
adminReverseAdjustmentCount: 1,
|
|
173
|
+
}),
|
|
174
|
+
true,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('existing admin adjustment hydrates for immutable reverse-and-replace editing', () => {
|
|
179
|
+
const line = lineFromActiveAdjustment({
|
|
180
|
+
adjustmentId: 'admin-rcpt-1',
|
|
181
|
+
operationId: 'op-1',
|
|
182
|
+
mode: 'FIXED_AMOUNT',
|
|
183
|
+
scope: 'CURRENT_ACTIVE_BOOKING',
|
|
184
|
+
basisCents: 0,
|
|
185
|
+
amountCents: 2_000,
|
|
186
|
+
taxDeltaCents: 170,
|
|
187
|
+
affectedComponentIds: [],
|
|
188
|
+
taxBehavior: 'TAXABLE',
|
|
189
|
+
reasonCode: 'STAFF_ADJUSTMENT',
|
|
190
|
+
reason: 'Local test service fee',
|
|
191
|
+
createdAt: '2026-07-27T00:00:00Z',
|
|
192
|
+
}, 'admin-edit-session-1');
|
|
193
|
+
|
|
194
|
+
assert.equal(line.label, 'Local test service fee');
|
|
195
|
+
assert.equal(line.amountInput, '20.00');
|
|
196
|
+
assert.equal(line.sourceAdjustmentId, 'admin-rcpt-1');
|
|
197
|
+
assert.equal(line.replacementAdjustmentId, 'admin-edit-session-1');
|
|
198
|
+
assert.deepEqual(reverseAdjustmentIdsForAdminCustomLines([line]), []);
|
|
199
|
+
assert.deepEqual(
|
|
200
|
+
reverseAdjustmentIdsForAdminCustomLines([{ ...line, isDirty: true }]),
|
|
201
|
+
['admin-rcpt-1'],
|
|
202
|
+
);
|
|
203
|
+
assert.deepEqual(reverseAdjustmentIdsForAdminCustomLines([], ['admin-rcpt-1']), ['admin-rcpt-1']);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('private resource quantity is not carried into a regular-tour conversion', () => {
|
|
207
|
+
const quantities = deriveCompatibleInitialBookingQuantities(
|
|
208
|
+
[{ category: 'RESOURCE', count: 1 }],
|
|
209
|
+
{
|
|
210
|
+
dateTime: '2026-08-10T09:00:00-06:00',
|
|
211
|
+
vacancies: 10,
|
|
212
|
+
currency: 'CAD',
|
|
213
|
+
productType: 'STANDARD',
|
|
214
|
+
rates: [
|
|
215
|
+
{ rateId: 'adult', category: 'ADULT', available: 10 },
|
|
216
|
+
{ rateId: 'child', category: 'CHILD', available: 10 },
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
assert.deepEqual(quantities, {});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('compatible original ticket quantities are preserved for a regular-tour change', () => {
|
|
225
|
+
const quantities = deriveCompatibleInitialBookingQuantities(
|
|
226
|
+
[
|
|
227
|
+
{ category: 'ADULT', count: 2 },
|
|
228
|
+
{ category: 'CHILD', count: 1 },
|
|
229
|
+
],
|
|
230
|
+
{
|
|
231
|
+
dateTime: '2026-08-10T09:00:00-06:00',
|
|
232
|
+
vacancies: 10,
|
|
233
|
+
currency: 'CAD',
|
|
234
|
+
productType: 'STANDARD',
|
|
235
|
+
rates: [
|
|
236
|
+
{ rateId: 'adult', category: 'ADULT', available: 10 },
|
|
237
|
+
{ rateId: 'child', category: 'CHILD', available: 10 },
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
assert.deepEqual(quantities, { ADULT: 2, CHILD: 1 });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('family conversion selects the target cancellation policy default', () => {
|
|
246
|
+
const policies = [
|
|
247
|
+
{ id: 'standard', label: 'Standard', feeByCurrency: { CAD: 0 } },
|
|
248
|
+
{ id: 'flexible', label: 'Flexible', feeByCurrency: { CAD: 25 } },
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
assert.equal(
|
|
252
|
+
resolveChangeBookingCancellationPolicyId('private_shuttle', policies, 'CAD', true),
|
|
253
|
+
'standard',
|
|
254
|
+
);
|
|
255
|
+
assert.equal(
|
|
256
|
+
resolveChangeBookingCancellationPolicyId('flexible', policies, 'CAD', false),
|
|
257
|
+
'flexible',
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('resource and passenger receipts are recognized as a product-family conversion', () => {
|
|
262
|
+
assert.equal(
|
|
263
|
+
isProductFamilyReceiptConversion(
|
|
264
|
+
[{ kind: 'ticket', category: 'RESOURCE', qty: 1, itemTotal: 2199.99 }],
|
|
265
|
+
[{ kind: 'ticket', category: 'ADULT', qty: 1, itemTotal: 120.99 }],
|
|
266
|
+
),
|
|
267
|
+
true,
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('server preview exposes the resulting receipt separately from ledger allocation lines', () => {
|
|
272
|
+
const preview = buildChangeBookingServerPreview(
|
|
273
|
+
quote({
|
|
274
|
+
pricingQuote: {
|
|
275
|
+
currency: 'CAD',
|
|
276
|
+
payableTotal: 148.62,
|
|
277
|
+
netSubtotalBeforeTax: 136.98,
|
|
278
|
+
taxAmount: 11.64,
|
|
279
|
+
amountPreviouslyPaid: 836.85,
|
|
280
|
+
amountToCharge: 0,
|
|
281
|
+
refundCandidate: 688.23,
|
|
282
|
+
lines: [
|
|
283
|
+
{
|
|
284
|
+
type: 'BOOKING_CHANGE',
|
|
285
|
+
label: 'Balance reduction · RESOURCE',
|
|
286
|
+
amount: -1670.89,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
amendment: {
|
|
290
|
+
updatedReceipt: {
|
|
291
|
+
total: 148.62,
|
|
292
|
+
lineItems: [
|
|
293
|
+
{ type: 'TICKET', label: 'ADULT', amount: 120.99, quantity: 1 },
|
|
294
|
+
{ type: 'FEE', label: 'Moraine Lake Road Access Fee', amount: 15.99 },
|
|
295
|
+
{ type: 'TAX', label: 'Taxes and fees', amount: 11.64 },
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
refundCandidate: 688.23,
|
|
301
|
+
}),
|
|
302
|
+
{ total: 148.62, subtotal: 136.98, tax: 11.64 },
|
|
303
|
+
'CAD',
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
assert.deepEqual(
|
|
307
|
+
preview?.resultingReceiptLines?.map((line) =>
|
|
308
|
+
line.kind === 'ticket' ? line.category : line.label,
|
|
309
|
+
),
|
|
310
|
+
['ADULT', 'Moraine Lake Road Access Fee', 'Taxes and fees'],
|
|
311
|
+
);
|
|
312
|
+
assert.equal(preview?.amountDue, -688.23);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('existing add-on quantities are locked only for customer self-service changes', () => {
|
|
316
|
+
assert.equal(
|
|
317
|
+
shouldLockExistingAddOnQuantities({ isCustomerSelfServeChange: true, isAdmin: false }),
|
|
318
|
+
true,
|
|
319
|
+
);
|
|
320
|
+
assert.equal(
|
|
321
|
+
shouldLockExistingAddOnQuantities({ isCustomerSelfServeChange: true, isAdmin: true }),
|
|
322
|
+
false,
|
|
323
|
+
);
|
|
324
|
+
assert.equal(
|
|
325
|
+
shouldLockExistingAddOnQuantities({ isCustomerSelfServeChange: false, isAdmin: true }),
|
|
326
|
+
false,
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('removing every existing add-on is detected as a booking change', () => {
|
|
331
|
+
const initialAddOnQtyByKey = new Map([
|
|
332
|
+
['addon_el_lunch::blackforest_ham_sandwich_gatorade', 1],
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
assert.equal(haveAddOnSelectionsChanged(initialAddOnQtyByKey, []), true);
|
|
336
|
+
});
|
|
337
|
+
|
|
109
338
|
test('admin amendment display combines one promo allocation across component rows', () => {
|
|
110
339
|
const lines = collapseAdminPromoLines([
|
|
111
340
|
{ kind: 'ticket', category: 'ADULT', qty: 1, itemTotal: 170.49 },
|
|
@@ -179,6 +408,48 @@ test('calendar auto-selection advances when the selected day has no visible star
|
|
|
179
408
|
);
|
|
180
409
|
});
|
|
181
410
|
|
|
411
|
+
test('product change falls back to the most-popular available pickup', () => {
|
|
412
|
+
const times: Availability[] = [
|
|
413
|
+
{
|
|
414
|
+
availabilityId: 'availability_725',
|
|
415
|
+
productOptionId: 'option_725',
|
|
416
|
+
dateTime: '2026-07-30T07:25:00-06:00',
|
|
417
|
+
vacancies: 10,
|
|
418
|
+
currency: 'CAD',
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
availabilityId: 'availability_900',
|
|
422
|
+
productOptionId: 'option_900',
|
|
423
|
+
dateTime: '2026-07-30T09:00:00-06:00',
|
|
424
|
+
vacancies: 25,
|
|
425
|
+
currency: 'CAD',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
availabilityId: 'availability_1300',
|
|
429
|
+
productOptionId: 'option_1300',
|
|
430
|
+
dateTime: '2026-07-30T13:00:00-06:00',
|
|
431
|
+
vacancies: 13,
|
|
432
|
+
currency: 'CAD',
|
|
433
|
+
},
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
const result = resolveInitialAvailabilityFromBooking(
|
|
437
|
+
times,
|
|
438
|
+
new Date('2026-07-30T08:00:00-06:00'),
|
|
439
|
+
'America/Edmonton',
|
|
440
|
+
'old_product_availability',
|
|
441
|
+
null,
|
|
442
|
+
[{ category: 'ADULT', count: 1 }],
|
|
443
|
+
null,
|
|
444
|
+
'CAD',
|
|
445
|
+
235.98,
|
|
446
|
+
'option_900',
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
assert.equal(result.defer, false);
|
|
450
|
+
assert.equal(result.selection?.availabilityId, 'availability_900');
|
|
451
|
+
});
|
|
452
|
+
|
|
182
453
|
test('admin payment choice countdown expires exactly at the reservation deadline', () => {
|
|
183
454
|
const expiration = '2026-07-16T15:46:44+00:00';
|
|
184
455
|
assert.equal(
|
|
@@ -325,6 +596,26 @@ test('admin quote dedupe refetches unchanged inputs after the published quote is
|
|
|
325
596
|
assert.equal(canReuseCompletedAdminChangeQuote(inputKey, 'different-inputs', true), false);
|
|
326
597
|
});
|
|
327
598
|
|
|
599
|
+
test('admin quote gate allows adjustment-only changes', () => {
|
|
600
|
+
assert.equal(canRequestAdminChangeQuote({
|
|
601
|
+
isCustomerSelfServeChange: true,
|
|
602
|
+
hasEffectiveChangeSelection: true,
|
|
603
|
+
hasSelectedAvailability: true,
|
|
604
|
+
bookingReference: 'DIFG66OF',
|
|
605
|
+
lastName: 'Tauro',
|
|
606
|
+
missingRequiredReturnSelection: false,
|
|
607
|
+
}), true);
|
|
608
|
+
|
|
609
|
+
assert.equal(canRequestAdminChangeQuote({
|
|
610
|
+
isCustomerSelfServeChange: true,
|
|
611
|
+
hasEffectiveChangeSelection: false,
|
|
612
|
+
hasSelectedAvailability: true,
|
|
613
|
+
bookingReference: 'DIFG66OF',
|
|
614
|
+
lastName: 'Tauro',
|
|
615
|
+
missingRequiredReturnSelection: false,
|
|
616
|
+
}), false);
|
|
617
|
+
});
|
|
618
|
+
|
|
328
619
|
test('admin preview and checkout share the Pricing V2 request shape', () => {
|
|
329
620
|
const request = buildAdminChangeV2QuoteRequest({
|
|
330
621
|
bookingReference: ' VFSCAEKQ ',
|
|
@@ -345,6 +636,7 @@ test('admin preview and checkout share the Pricing V2 request shape', () => {
|
|
|
345
636
|
promoCode: ' banffblog15 ',
|
|
346
637
|
promoApplicationScope: 'ENTIRE_BOOKING',
|
|
347
638
|
structuredAdjustments: [{ type: 'CUSTOM', amount: 5 }],
|
|
639
|
+
includeSelectableAdjustmentComponents: true,
|
|
348
640
|
refundDispositionsByOperationId: { remove_1: 'PENDING_REFUND' },
|
|
349
641
|
previousPassengerCount: 2,
|
|
350
642
|
previousAvailabilityId: 'availability_old',
|
|
@@ -358,6 +650,7 @@ test('admin preview and checkout share the Pricing V2 request shape', () => {
|
|
|
358
650
|
assert.equal(request.promoCode, 'banffblog15');
|
|
359
651
|
assert.equal(request.promoApplicationScope, 'ENTIRE_BOOKING');
|
|
360
652
|
assert.deepEqual(request.refundDispositionsByOperationId, { remove_1: 'PENDING_REFUND' });
|
|
653
|
+
assert.equal(request.includeSelectableAdjustmentComponents, true);
|
|
361
654
|
assert.deepEqual(request.capacitySeatCredit, {
|
|
362
655
|
enabled: true,
|
|
363
656
|
previousPassengerCount: 2,
|
|
@@ -517,8 +810,22 @@ test('admin removal quote preserves explicit release refund decisions for the pr
|
|
|
517
810
|
quantityDelta: -1,
|
|
518
811
|
};
|
|
519
812
|
const mapped = mapAdminChangeBookingQuoteV2Data({
|
|
520
|
-
oldReceipt: {
|
|
521
|
-
|
|
813
|
+
oldReceipt: {
|
|
814
|
+
currency: 'CAD',
|
|
815
|
+
grossSubtotal: 357.46,
|
|
816
|
+
paymentCreditTotal: 50,
|
|
817
|
+
taxAmount: 30.38,
|
|
818
|
+
payableTotal: 387.84,
|
|
819
|
+
},
|
|
820
|
+
newQuote: {
|
|
821
|
+
currency: 'CAD',
|
|
822
|
+
payableTotal: 193.92,
|
|
823
|
+
paymentCreditTotal: 25,
|
|
824
|
+
amountPreviouslyPaid: 148.62,
|
|
825
|
+
balanceDelta: 0,
|
|
826
|
+
amountToCharge: 0,
|
|
827
|
+
refundCandidate: 0,
|
|
828
|
+
},
|
|
522
829
|
canApply: false,
|
|
523
830
|
reasonIfBlocked: 'admin_refund_disposition_required',
|
|
524
831
|
refundDecisionOperations: [operation],
|
|
@@ -538,6 +845,10 @@ test('admin removal quote preserves explicit release refund decisions for the pr
|
|
|
538
845
|
);
|
|
539
846
|
|
|
540
847
|
assert.deepEqual(slice.refundDecisionOperations, [operation]);
|
|
848
|
+
assert.equal(slice.amountPreviouslyPaid, 148.62);
|
|
849
|
+
assert.equal(slice.currentPaymentCreditTotal, 50);
|
|
850
|
+
assert.equal(slice.projectedPaymentCreditTotal, 25);
|
|
851
|
+
assert.equal(slice.refundCandidate, 0);
|
|
541
852
|
assert.equal(slice.removedValueByOperationId?.[operation.operationId], 193.92);
|
|
542
853
|
assert.deepEqual(
|
|
543
854
|
releaseRefundDispositionOptions(
|
|
@@ -643,11 +954,62 @@ test('admin refund decisions remain stable when successive quotes return only un
|
|
|
643
954
|
);
|
|
644
955
|
});
|
|
645
956
|
|
|
957
|
+
test('admin removal decisions default every required operation to pending refund', () => {
|
|
958
|
+
const removeReturn = {
|
|
959
|
+
operationId: 'op_remove_return',
|
|
960
|
+
type: 'REMOVE_RETURN',
|
|
961
|
+
componentKey: 'return:rt_morning',
|
|
962
|
+
quantityDelta: -1,
|
|
963
|
+
};
|
|
964
|
+
const removeAdult = {
|
|
965
|
+
operationId: 'op_remove_adult',
|
|
966
|
+
type: 'REMOVE_QUANTITY',
|
|
967
|
+
componentKey: 'ticket:ADULT',
|
|
968
|
+
quantityDelta: -1,
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
assert.deepEqual(
|
|
972
|
+
defaultPendingRefundDispositions(
|
|
973
|
+
[removeReturn, removeAdult],
|
|
974
|
+
{
|
|
975
|
+
unrelated_operation: 'NO_REFUND',
|
|
976
|
+
[removeAdult.operationId]: 'NO_REFUND',
|
|
977
|
+
},
|
|
978
|
+
),
|
|
979
|
+
{
|
|
980
|
+
unrelated_operation: 'NO_REFUND',
|
|
981
|
+
[removeReturn.operationId]: 'PENDING_REFUND',
|
|
982
|
+
[removeAdult.operationId]: 'PENDING_REFUND',
|
|
983
|
+
},
|
|
984
|
+
);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
test('admin return-price decisions always default to keeping the original paid floor', () => {
|
|
988
|
+
const returnOperation = {
|
|
989
|
+
operationId: 'op_change_return',
|
|
990
|
+
type: 'CHANGE_RETURN',
|
|
991
|
+
componentKey: 'return:rt_early',
|
|
992
|
+
quantityDelta: 0,
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
assert.deepEqual(
|
|
996
|
+
defaultKeepOriginalReturnPriceDispositions(
|
|
997
|
+
[returnOperation],
|
|
998
|
+
{
|
|
999
|
+
op_remove_adult: 'PENDING_REFUND',
|
|
1000
|
+
[returnOperation.operationId]: 'NO_REFUND',
|
|
1001
|
+
},
|
|
1002
|
+
),
|
|
1003
|
+
{ op_remove_adult: 'PENDING_REFUND' },
|
|
1004
|
+
);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
646
1007
|
test('admin move-service quote labels only its ticket price difference', () => {
|
|
647
1008
|
const moveOperation = {
|
|
648
1009
|
operationId: 'op_move_service',
|
|
649
1010
|
type: 'MOVE_SERVICE',
|
|
650
1011
|
componentKey: 'service',
|
|
1012
|
+
changedFields: ['availabilityId', 'dateTime'],
|
|
651
1013
|
};
|
|
652
1014
|
const addOperation = {
|
|
653
1015
|
operationId: 'op_add_adult',
|
|
@@ -830,6 +1192,46 @@ test('admin return price treatment defaults to keep floor and maps optional repr
|
|
|
830
1192
|
);
|
|
831
1193
|
});
|
|
832
1194
|
|
|
1195
|
+
test('admin cross-tour move labels its ticket delta as a tour price difference', () => {
|
|
1196
|
+
const [line] = labelAdminAmendmentPriceSummaryLines(
|
|
1197
|
+
[{
|
|
1198
|
+
kind: 'ticket',
|
|
1199
|
+
operationId: 'op_move_tour',
|
|
1200
|
+
category: 'Refund · ADULT',
|
|
1201
|
+
qty: 1,
|
|
1202
|
+
itemTotal: -99,
|
|
1203
|
+
}],
|
|
1204
|
+
[{
|
|
1205
|
+
operationId: 'op_move_tour',
|
|
1206
|
+
type: 'MOVE_SERVICE',
|
|
1207
|
+
componentKey: 'service',
|
|
1208
|
+
changedFields: ['productId', 'availabilityId', 'dateTime'],
|
|
1209
|
+
}],
|
|
1210
|
+
[{
|
|
1211
|
+
kind: 'ticket',
|
|
1212
|
+
category: 'ADULT',
|
|
1213
|
+
qty: 1,
|
|
1214
|
+
itemTotal: 219.99,
|
|
1215
|
+
}],
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
assert.equal(
|
|
1219
|
+
line?.kind === 'ticket' ? line.category : null,
|
|
1220
|
+
'Refund · ADULT — tour price difference',
|
|
1221
|
+
);
|
|
1222
|
+
assert.deepEqual(
|
|
1223
|
+
line?.kind === 'ticket' ? line.unitPriceComparison : null,
|
|
1224
|
+
{
|
|
1225
|
+
previousLabel: 'Existing adult price',
|
|
1226
|
+
updatedLabel: 'New-tour adult price',
|
|
1227
|
+
differenceLabel: 'Difference per adult',
|
|
1228
|
+
previousUnitAmount: 219.99,
|
|
1229
|
+
updatedUnitAmount: 120.99,
|
|
1230
|
+
differenceUnitAmount: -99,
|
|
1231
|
+
},
|
|
1232
|
+
);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
833
1235
|
test('unsupported immediate refund cannot become a return price treatment', () => {
|
|
834
1236
|
assert.equal(normalizeReturnPriceTreatment(undefined), 'KEEP_FLOOR');
|
|
835
1237
|
assert.equal(normalizeReturnPriceTreatment('REFUND_TO_ORIGINAL_PAYMENT'), 'KEEP_FLOOR');
|
|
@@ -951,6 +1353,142 @@ test('private shuttle pricing replaces the selected calendar summary with hydrat
|
|
|
951
1353
|
assert.equal(applyResourceAdjustments(1699.99, resolved, 'CAD'), 2099.99);
|
|
952
1354
|
});
|
|
953
1355
|
|
|
1356
|
+
test('private conversion keeps the existing booking day in the company timezone', () => {
|
|
1357
|
+
assert.equal(
|
|
1358
|
+
privateShuttleInitialDateKey('2026-07-28T01:05:00Z', 'America/Edmonton'),
|
|
1359
|
+
'2026-07-27',
|
|
1360
|
+
);
|
|
1361
|
+
assert.equal(
|
|
1362
|
+
privateShuttleInitialDateKey('2026-07-27', 'America/Edmonton'),
|
|
1363
|
+
'2026-07-27',
|
|
1364
|
+
);
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
test('private conversion replaces an incompatible source cancellation policy with the target default', () => {
|
|
1368
|
+
assert.equal(
|
|
1369
|
+
resolvePrivateShuttleCancellationPolicyId('standard_flexible', ['private_shuttle']),
|
|
1370
|
+
'private_shuttle',
|
|
1371
|
+
);
|
|
1372
|
+
assert.equal(
|
|
1373
|
+
resolvePrivateShuttleCancellationPolicyId('private_shuttle', ['private_shuttle']),
|
|
1374
|
+
'private_shuttle',
|
|
1375
|
+
);
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
test('admin standard-to-private conversion sends complete private target identity and state', async () => {
|
|
1379
|
+
let captured: Parameters<typeof applyPrivateShuttleProviderChange>[0] extends {
|
|
1380
|
+
onChangeBooking: (data: infer T) => Promise<void>;
|
|
1381
|
+
}
|
|
1382
|
+
? T
|
|
1383
|
+
: never;
|
|
1384
|
+
|
|
1385
|
+
await applyPrivateShuttleProviderChange({
|
|
1386
|
+
onChangeBooking: async (data) => {
|
|
1387
|
+
captured = data;
|
|
1388
|
+
},
|
|
1389
|
+
parentProductId: 'p_private',
|
|
1390
|
+
selectedOption: 'po_private',
|
|
1391
|
+
selectedAvailabilityId: 'a_private',
|
|
1392
|
+
selectedDate: '2026-07-29',
|
|
1393
|
+
selectedStartTime: '09:15',
|
|
1394
|
+
billableResourceCount: 2,
|
|
1395
|
+
pickupLocationId: 'pickup_hotel',
|
|
1396
|
+
selectedPickupLocationName: 'Banff Hotel',
|
|
1397
|
+
initialBooking: {
|
|
1398
|
+
bookingReference: 'bookRef_TEST1234',
|
|
1399
|
+
productId: 'po_standard',
|
|
1400
|
+
availabilityId: 'a_standard',
|
|
1401
|
+
dateTime: '2026-07-29T08:00:00-06:00',
|
|
1402
|
+
bookingItems: [{ category: 'ADULT', count: 5 }],
|
|
1403
|
+
returnAvailabilityId: 'ret_standard',
|
|
1404
|
+
},
|
|
1405
|
+
passengerCount: 5,
|
|
1406
|
+
childSafetySeatsCount: 1,
|
|
1407
|
+
foodRestrictions: 'No nuts',
|
|
1408
|
+
addOnSelections: [],
|
|
1409
|
+
cancellationPolicyId: 'flexible',
|
|
1410
|
+
activePromoCode: null,
|
|
1411
|
+
totalPrice: 1200,
|
|
1412
|
+
additionalHoursCount: 1,
|
|
1413
|
+
draftItineraryDestinations: ['moraine_lake'],
|
|
1414
|
+
draftItineraryPlanningNotes: 'Photo stop requested',
|
|
1415
|
+
itineraryDisplayItems: [
|
|
1416
|
+
{ time: '9:15 AM', label: 'Banff Hotel' },
|
|
1417
|
+
{ time: null, label: 'Moraine Lake', isProposedStops: true },
|
|
1418
|
+
{ time: '3:15 PM', label: 'Banff Hotel' },
|
|
1419
|
+
],
|
|
1420
|
+
isAdmin: true,
|
|
1421
|
+
providerChangeAuthoritativeReceipt: undefined,
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
assert.equal(captured!.parentProductId, 'p_private');
|
|
1425
|
+
assert.equal(captured!.productId, 'po_private');
|
|
1426
|
+
assert.equal(captured!.availabilityId, 'a_private');
|
|
1427
|
+
assert.deepEqual(captured!.bookingItems, [{ category: 'RESOURCE', count: 2 }]);
|
|
1428
|
+
assert.equal(captured!.returnAvailabilityId, null);
|
|
1429
|
+
assert.equal(captured!.passengerCount, 5);
|
|
1430
|
+
assert.equal(captured!.startTime, '2026-07-29T09:15:00-06:00');
|
|
1431
|
+
assert.deepEqual(captured!.draftItinerary, {
|
|
1432
|
+
destinations: ['moraine_lake'],
|
|
1433
|
+
planningNotes: 'Photo stop requested',
|
|
1434
|
+
});
|
|
1435
|
+
assert.deepEqual(captured!.itineraryDisplay?.map((step) => step.stepType), [
|
|
1436
|
+
'pickup',
|
|
1437
|
+
'draft',
|
|
1438
|
+
'drop_off',
|
|
1439
|
+
]);
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
test('private conversion stores the first actual location as pickup when proposed stops come first', async () => {
|
|
1443
|
+
let captured: Parameters<typeof applyPrivateShuttleProviderChange>[0] extends {
|
|
1444
|
+
onChangeBooking: (data: infer T) => Promise<void>;
|
|
1445
|
+
}
|
|
1446
|
+
? T
|
|
1447
|
+
: never;
|
|
1448
|
+
|
|
1449
|
+
await applyPrivateShuttleProviderChange({
|
|
1450
|
+
onChangeBooking: async (data) => {
|
|
1451
|
+
captured = data;
|
|
1452
|
+
},
|
|
1453
|
+
parentProductId: 'p_private',
|
|
1454
|
+
selectedOption: 'po_private',
|
|
1455
|
+
selectedAvailabilityId: 'a_private',
|
|
1456
|
+
selectedDate: '2026-07-29',
|
|
1457
|
+
selectedStartTime: '07:00',
|
|
1458
|
+
billableResourceCount: 1,
|
|
1459
|
+
pickupLocationId: 'pickup_hotel',
|
|
1460
|
+
selectedPickupLocationName: 'Banff Hotel',
|
|
1461
|
+
initialBooking: {
|
|
1462
|
+
bookingReference: 'bookRef_TEST1234',
|
|
1463
|
+
productId: 'po_standard',
|
|
1464
|
+
availabilityId: 'a_standard',
|
|
1465
|
+
dateTime: '2026-07-29T08:00:00-06:00',
|
|
1466
|
+
bookingItems: [{ category: 'ADULT', count: 1 }],
|
|
1467
|
+
},
|
|
1468
|
+
passengerCount: 1,
|
|
1469
|
+
childSafetySeatsCount: 0,
|
|
1470
|
+
foodRestrictions: '',
|
|
1471
|
+
addOnSelections: [],
|
|
1472
|
+
cancellationPolicyId: 'private_shuttle',
|
|
1473
|
+
activePromoCode: null,
|
|
1474
|
+
totalPrice: 1200,
|
|
1475
|
+
additionalHoursCount: 0,
|
|
1476
|
+
draftItineraryDestinations: ['moraine_lake', 'lake_louise'],
|
|
1477
|
+
draftItineraryPlanningNotes: '',
|
|
1478
|
+
itineraryDisplayItems: [
|
|
1479
|
+
{ time: null, label: 'Moraine Lake, Lake Louise', isProposedStops: true },
|
|
1480
|
+
{ time: '7:00 AM', label: 'Banff Hotel' },
|
|
1481
|
+
],
|
|
1482
|
+
isAdmin: true,
|
|
1483
|
+
providerChangeAuthoritativeReceipt: undefined,
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
assert.deepEqual(captured!.itineraryDisplay, [
|
|
1487
|
+
{ stepType: 'draft', time: 'Proposed stops', place: 'Moraine Lake, Lake Louise' },
|
|
1488
|
+
{ stepType: 'pickup', time: '7:00 AM', place: 'Banff Hotel' },
|
|
1489
|
+
]);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
954
1492
|
test('private shuttle checkout detects a quote total that differs from the displayed total', () => {
|
|
955
1493
|
assert.equal(privateShuttlePriceChanged(2247, 2681), true);
|
|
956
1494
|
assert.equal(privateShuttlePriceChanged(2681, 2681.0), false);
|
|
@@ -1260,3 +1798,315 @@ test('builds paid checkout modal data with trimmed customer and change totals',
|
|
|
1260
1798
|
differenceTotal: 12,
|
|
1261
1799
|
});
|
|
1262
1800
|
});
|
|
1801
|
+
|
|
1802
|
+
test('keeps an incompatible add-on visible until the admin explicitly removes it', () => {
|
|
1803
|
+
const lunch = {
|
|
1804
|
+
addOnId: 'addon_el_lunch',
|
|
1805
|
+
name: 'Emerald Lake Lunch Package',
|
|
1806
|
+
price: 28,
|
|
1807
|
+
currency: 'CAD',
|
|
1808
|
+
preCheckout: true,
|
|
1809
|
+
postCheckout: false,
|
|
1810
|
+
variantType: 'multi_quantity' as const,
|
|
1811
|
+
variants: [
|
|
1812
|
+
{
|
|
1813
|
+
id: 'blackforest_ham_sandwich_gatorade',
|
|
1814
|
+
label: 'Blackforest Ham Sandwich + Gatorade',
|
|
1815
|
+
priceAdjustment: 0,
|
|
1816
|
+
},
|
|
1817
|
+
],
|
|
1818
|
+
productOptionIds: ['po_big_three'],
|
|
1819
|
+
};
|
|
1820
|
+
const selections = [
|
|
1821
|
+
{
|
|
1822
|
+
addOnId: lunch.addOnId,
|
|
1823
|
+
variantId: lunch.variants[0].id,
|
|
1824
|
+
quantity: 1,
|
|
1825
|
+
},
|
|
1826
|
+
];
|
|
1827
|
+
|
|
1828
|
+
assert.deepEqual(incompatibleAddOnSelections(selections, [], false), []);
|
|
1829
|
+
assert.deepEqual(incompatibleAddOnSelections(selections, [lunch], true), []);
|
|
1830
|
+
assert.deepEqual(incompatibleAddOnSelections(selections, [], true), selections);
|
|
1831
|
+
assert.deepEqual(removeAddOnSelections(selections, lunch.addOnId), []);
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
test('removing one incompatible add-on preserves other target selections', () => {
|
|
1835
|
+
const selections = [
|
|
1836
|
+
{ addOnId: 'addon_el_lunch', variantId: 'ham', quantity: 1 },
|
|
1837
|
+
{ addOnId: 'addon_target_supported', quantity: 2 },
|
|
1838
|
+
];
|
|
1839
|
+
|
|
1840
|
+
assert.deepEqual(removeAddOnSelections(selections, 'addon_el_lunch'), [
|
|
1841
|
+
{ addOnId: 'addon_target_supported', quantity: 2 },
|
|
1842
|
+
]);
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
test('labels a service move with the source and target tour names', () => {
|
|
1846
|
+
assert.equal(
|
|
1847
|
+
adminRemovalOperationLabel(
|
|
1848
|
+
{
|
|
1849
|
+
operationId: 'op_move',
|
|
1850
|
+
type: 'MOVE_SERVICE',
|
|
1851
|
+
componentKey: 'service',
|
|
1852
|
+
oldIdentity: 'p_big_three:a_source:2026-07-31',
|
|
1853
|
+
targetIdentity: 'po_moraine:a_target:2026-07-31',
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
sourceProductName: 'Big 3 Lakes Tour: Moraine, Emerald, Louise',
|
|
1857
|
+
targetProductName: 'Moraine Lake Adventure',
|
|
1858
|
+
},
|
|
1859
|
+
),
|
|
1860
|
+
'Change tour: Big 3 Lakes → Moraine Lake Adventure',
|
|
1861
|
+
);
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
test('labels a whole-booking promo adjustment without exposing its internal id', () => {
|
|
1865
|
+
const label = adminRemovalOperationLabel(
|
|
1866
|
+
{
|
|
1867
|
+
operationId: 'op_promo_adjustment',
|
|
1868
|
+
type: 'ADD_MANUAL_ADJUSTMENT',
|
|
1869
|
+
componentKey: 'manual_adjustment:promo_53ef0d9fe839aa8cd88e9b0f',
|
|
1870
|
+
targetIdentity: '-3035',
|
|
1871
|
+
},
|
|
1872
|
+
{ promoCode: 'banffblog15' },
|
|
1873
|
+
);
|
|
1874
|
+
|
|
1875
|
+
assert.equal(label, 'Apply promo to entire booking: BANFFBLOG15');
|
|
1876
|
+
assert.doesNotMatch(label, /promo_[a-f0-9]+/i);
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
test('limits adjustment tax treatments by charge or credit direction', () => {
|
|
1880
|
+
assert.deepEqual(
|
|
1881
|
+
adminAdjustmentTaxBehaviorOptions(1).map((option) => option.value),
|
|
1882
|
+
['TAXABLE', 'NON_TAXABLE'],
|
|
1883
|
+
);
|
|
1884
|
+
assert.deepEqual(
|
|
1885
|
+
adminAdjustmentTaxBehaviorOptions(-1).map((option) => option.value),
|
|
1886
|
+
['PRE_TAX_DISCOUNT', 'POST_TAX_DISCOUNT'],
|
|
1887
|
+
);
|
|
1888
|
+
assert.equal(normalizeAdminAdjustmentTaxBehavior(-1, 'TAXABLE'), 'PRE_TAX_DISCOUNT');
|
|
1889
|
+
assert.equal(
|
|
1890
|
+
normalizeAdminAdjustmentTaxBehavior(-1, 'POST_TAX_DISCOUNT'),
|
|
1891
|
+
'POST_TAX_DISCOUNT',
|
|
1892
|
+
);
|
|
1893
|
+
assert.equal(normalizeAdminAdjustmentTaxBehavior(1, 'PRE_TAX_DISCOUNT'), 'TAXABLE');
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
test('switching a new custom line to credit defaults its scope to active tickets', () => {
|
|
1897
|
+
assert.equal(
|
|
1898
|
+
normalizeAdminAdjustmentScopeForSign(-1, 'POSITIVE_AMENDMENT_VALUE'),
|
|
1899
|
+
'CURRENT_ACTIVE_TICKETS',
|
|
1900
|
+
);
|
|
1901
|
+
assert.equal(
|
|
1902
|
+
normalizeAdminAdjustmentScopeForSign(-1, 'CURRENT_ACTIVE_BOOKING'),
|
|
1903
|
+
'CURRENT_ACTIVE_BOOKING',
|
|
1904
|
+
);
|
|
1905
|
+
assert.equal(
|
|
1906
|
+
normalizeAdminAdjustmentScopeForSign(1, 'POSITIVE_AMENDMENT_VALUE'),
|
|
1907
|
+
'POSITIVE_AMENDMENT_VALUE',
|
|
1908
|
+
);
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
test('labels a custom adjustment refund decision with its operator-entered description', () => {
|
|
1912
|
+
const label = adminRemovalOperationLabel(
|
|
1913
|
+
{
|
|
1914
|
+
operationId: 'op_admin_discount',
|
|
1915
|
+
type: 'ADD_MANUAL_ADJUSTMENT',
|
|
1916
|
+
componentKey: 'manual_adjustment:admin-rcpt-1',
|
|
1917
|
+
targetIdentity: '-1000',
|
|
1918
|
+
},
|
|
1919
|
+
{ manualAdjustmentLabelsById: { 'admin-rcpt-1': 'Local active-ticket discount' } },
|
|
1920
|
+
);
|
|
1921
|
+
|
|
1922
|
+
assert.equal(label, 'Local active-ticket discount');
|
|
1923
|
+
assert.doesNotMatch(label, /admin-rcpt/i);
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
test('labels an adjustment reversal from server metadata without exposing its internal id', () => {
|
|
1927
|
+
const label = adminRemovalOperationLabel({
|
|
1928
|
+
operationId: 'op_reverse_adjustment',
|
|
1929
|
+
type: 'REVERSE_MANUAL_ADJUSTMENT',
|
|
1930
|
+
componentKey: 'manual_adjustment:admin-edit-ms3ea4jv-1',
|
|
1931
|
+
oldIdentity: '2500',
|
|
1932
|
+
metadata: { displayName: 'Edited service fee' },
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
assert.equal(label, 'Remove adjustment: Edited service fee');
|
|
1936
|
+
assert.doesNotMatch(label, /admin-edit/i);
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
test('shows the true refundable amount after an unpaid balance is reduced', () => {
|
|
1940
|
+
const operation = {
|
|
1941
|
+
operationId: 'op_reverse_adjustment',
|
|
1942
|
+
type: 'REVERSE_MANUAL_ADJUSTMENT',
|
|
1943
|
+
componentKey: 'manual_adjustment:admin-edit-ms3ea4jv-1',
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
assert.equal(
|
|
1947
|
+
refundTreatmentDisplayValue(
|
|
1948
|
+
operation,
|
|
1949
|
+
[operation],
|
|
1950
|
+
{ op_reverse_adjustment: 27.13 },
|
|
1951
|
+
5,
|
|
1952
|
+
),
|
|
1953
|
+
5,
|
|
1954
|
+
);
|
|
1955
|
+
assert.equal(
|
|
1956
|
+
noRefundRemovedValue(
|
|
1957
|
+
[operation],
|
|
1958
|
+
{ op_reverse_adjustment: 'NO_REFUND' },
|
|
1959
|
+
{ op_reverse_adjustment: 27.13 },
|
|
1960
|
+
),
|
|
1961
|
+
27.13,
|
|
1962
|
+
);
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
test('translates booking-change reason codes into operator-facing messages', () => {
|
|
1966
|
+
assert.equal(
|
|
1967
|
+
friendlyChangeBookingError('pricing_v2_unsupported_features'),
|
|
1968
|
+
'Some selected booking options do not work together. Review the highlighted selections before continuing.',
|
|
1969
|
+
);
|
|
1970
|
+
assert.equal(
|
|
1971
|
+
friendlyChangeBookingError('quote_expired'),
|
|
1972
|
+
'This booking or price changed after the page was opened. Refresh it and try again.',
|
|
1973
|
+
);
|
|
1974
|
+
assert.equal(
|
|
1975
|
+
friendlyChangeBookingError('admin_adjustment_refund_disposition_required:op_adjustment'),
|
|
1976
|
+
'Choose how to handle the removed adjustment value before continuing.',
|
|
1977
|
+
);
|
|
1978
|
+
assert.equal(
|
|
1979
|
+
friendlyChangeBookingError('promo_change_only_no_eligible_new_value'),
|
|
1980
|
+
'This promo only applies to new charges. Make a priced change, or choose Entire active booking.',
|
|
1981
|
+
);
|
|
1982
|
+
assert.equal(
|
|
1983
|
+
friendlyChangeBookingError('structured_adjustment_basis_is_zero'),
|
|
1984
|
+
'This credit needs a value to apply to. Make a priced booking change, or choose All active tickets, Entire active booking, or Selected components.',
|
|
1985
|
+
);
|
|
1986
|
+
assert.equal(
|
|
1987
|
+
friendlyChangeBookingError('Remove the unavailable add-on before recalculating this amendment.'),
|
|
1988
|
+
'Remove the unavailable add-on before recalculating this amendment.',
|
|
1989
|
+
);
|
|
1990
|
+
assert.doesNotMatch(
|
|
1991
|
+
friendlyChangeBookingError('pricing_v2_future_unsupported_case'),
|
|
1992
|
+
/pricing_v2|_/,
|
|
1993
|
+
);
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
test('names the incompatible add-on and target tour in the operator error', () => {
|
|
1997
|
+
assert.equal(
|
|
1998
|
+
incompatibleAddOnChangeError(
|
|
1999
|
+
['Emerald Lake Lunch Package'],
|
|
2000
|
+
'Moraine Lake Adventure',
|
|
2001
|
+
),
|
|
2002
|
+
"Emerald Lake Lunch Package isn't available with Moraine Lake Adventure. Remove the add-on or choose a different tour.",
|
|
2003
|
+
);
|
|
2004
|
+
assert.equal(
|
|
2005
|
+
incompatibleAddOnChangeError(['Lunch', 'Photo package'], 'Target Tour'),
|
|
2006
|
+
"Lunch and Photo package aren't available with Target Tour. Remove these add-ons or choose a different tour.",
|
|
2007
|
+
);
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
test('uses the authoritative quote itinerary with confirmed dependent sessions', () => {
|
|
2011
|
+
const itinerary = [
|
|
2012
|
+
{ stepType: 'pickup', time: '8:05 AM', place: 'Banff Boundary Lodge' },
|
|
2013
|
+
{
|
|
2014
|
+
stepType: 'other',
|
|
2015
|
+
time: '11:00 AM - 11:30 AM',
|
|
2016
|
+
place: 'Friends, Family, & Couples add-on · Photographer: Will Lambert · 30 minutes',
|
|
2017
|
+
},
|
|
2018
|
+
{ stepType: 'drop_off', time: '7:00 PM', place: 'Banff Boundary Lodge' },
|
|
2019
|
+
];
|
|
2020
|
+
const response = quote({
|
|
2021
|
+
pricingQuote: {
|
|
2022
|
+
payableTotal: 212.64,
|
|
2023
|
+
amendment: {
|
|
2024
|
+
targetState: {
|
|
2025
|
+
nonPriceState: {
|
|
2026
|
+
itinerary: JSON.stringify(itinerary),
|
|
2027
|
+
},
|
|
2028
|
+
},
|
|
2029
|
+
},
|
|
2030
|
+
},
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
assert.deepEqual(authoritativeItineraryDisplayFromChangeQuote(response), itinerary);
|
|
2034
|
+
assert.deepEqual(
|
|
2035
|
+
sliceAndMergeChangeQuoteForUi(
|
|
2036
|
+
response,
|
|
2037
|
+
{ total: 212.64, subtotal: 195.98, tax: 16.66 },
|
|
2038
|
+
'CAD',
|
|
2039
|
+
).mergedQuote.authoritativeItineraryDisplay,
|
|
2040
|
+
itinerary,
|
|
2041
|
+
);
|
|
2042
|
+
});
|
|
2043
|
+
|
|
2044
|
+
test('falls back when the authoritative quote itinerary is malformed', () => {
|
|
2045
|
+
assert.equal(
|
|
2046
|
+
authoritativeItineraryDisplayFromChangeQuote(quote({
|
|
2047
|
+
pricingQuote: {
|
|
2048
|
+
amendment: {
|
|
2049
|
+
targetState: {
|
|
2050
|
+
nonPriceState: {
|
|
2051
|
+
itinerary: '{not-json',
|
|
2052
|
+
},
|
|
2053
|
+
},
|
|
2054
|
+
},
|
|
2055
|
+
},
|
|
2056
|
+
})),
|
|
2057
|
+
null,
|
|
2058
|
+
);
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
test('exposes only authoritative selectable amendment components to admin adjustments', () => {
|
|
2062
|
+
const response = quote({
|
|
2063
|
+
pricingQuote: {
|
|
2064
|
+
amendment: {
|
|
2065
|
+
updatedReceipt: {
|
|
2066
|
+
total: 256.04,
|
|
2067
|
+
lineItems: [
|
|
2068
|
+
{
|
|
2069
|
+
type: 'TICKET',
|
|
2070
|
+
label: 'ADULT',
|
|
2071
|
+
amount: 219.99,
|
|
2072
|
+
identity: { componentId: 'ticket_adult_1' },
|
|
2073
|
+
},
|
|
2074
|
+
{
|
|
2075
|
+
type: 'FEE',
|
|
2076
|
+
label: 'Moraine Lake Road Access Fee',
|
|
2077
|
+
amount: 15.99,
|
|
2078
|
+
identity: { componentId: 'fee_road_access' },
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
type: 'TAX',
|
|
2082
|
+
label: 'Taxes and fees',
|
|
2083
|
+
amount: 20.06,
|
|
2084
|
+
identity: { componentId: 'tax_1' },
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
type: 'PROMO_CODE',
|
|
2088
|
+
label: 'Promo',
|
|
2089
|
+
amount: -15,
|
|
2090
|
+
identity: { componentId: 'incentive_1' },
|
|
2091
|
+
},
|
|
2092
|
+
],
|
|
2093
|
+
},
|
|
2094
|
+
},
|
|
2095
|
+
},
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
assert.deepEqual(selectableAdminAdjustmentComponentsFromQuote(response), [
|
|
2099
|
+
{
|
|
2100
|
+
componentId: 'ticket_adult_1',
|
|
2101
|
+
label: 'ADULT',
|
|
2102
|
+
amount: 219.99,
|
|
2103
|
+
type: 'TICKET',
|
|
2104
|
+
},
|
|
2105
|
+
{
|
|
2106
|
+
componentId: 'fee_road_access',
|
|
2107
|
+
label: 'Moraine Lake Road Access Fee',
|
|
2108
|
+
amount: 15.99,
|
|
2109
|
+
type: 'FEE',
|
|
2110
|
+
},
|
|
2111
|
+
]);
|
|
2112
|
+
});
|