@ticketboothapp/booking 1.2.165 → 1.2.167
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 +2 -0
- package/src/components/booking/AdminChangeBookingFlow.tsx +97 -32
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +6 -0
- package/src/components/booking/AdminChangePromoEditor.tsx +97 -0
- package/src/components/booking/AdminChangeReceiptComparison.tsx +9 -2
- package/src/components/booking/NewBookingFlow.tsx +14 -2
- package/src/components/booking/PriceBreakdown.tsx +66 -15
- package/src/components/booking/PriceSummary.tsx +12 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +25 -1
- package/src/components/booking/admin-change-payment-choice-runner.ts +12 -0
- package/src/components/booking/admin-change-provider-payload.ts +9 -1
- package/src/components/booking/admin-change-quote-request-key.ts +6 -6
- package/src/components/booking/admin-change-receipt-lines.ts +49 -0
- package/src/components/booking/admin-change-v2-quote-request.ts +101 -0
- package/src/components/booking/admin-refund-decision-context.ts +85 -0
- package/src/components/booking/availability-date-selection.ts +16 -0
- package/src/components/booking/provider-dashboard-change-booking.ts +2 -1
- package/src/components/booking/use-standard-booking-availability.ts +5 -2
- package/src/components/booking/useAdminChangeCheckoutController.ts +90 -11
- package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +2 -2
- package/src/components/booking/useAdminChangeQuotePreview.ts +47 -37
- package/src/components/booking/useBookingAvailabilityAddOns.ts +7 -2
- package/src/components/booking/useBookingPromoAndQuantityController.ts +16 -2
- package/src/components/booking/useChangeBookingAutoSelections.ts +60 -17
- package/src/components/booking/useStandardBookingAutoSelections.ts +55 -7
- package/src/lib/booking/booking-cutoffs.ts +22 -0
- package/src/lib/booking/change-booking-server-preview.ts +75 -5
- package/src/lib/booking-api.ts +12 -1
- package/test/change-booking-helpers.test.ts +466 -3
|
@@ -11,12 +11,29 @@ import {
|
|
|
11
11
|
} from '../src/lib/booking/change-flow-pricing';
|
|
12
12
|
import {
|
|
13
13
|
mapAdminChangeBookingQuoteV2Data,
|
|
14
|
+
type Availability,
|
|
14
15
|
type ChangeBookingQuoteResponse,
|
|
15
16
|
} from '../src/lib/booking-api';
|
|
17
|
+
import {
|
|
18
|
+
filterAvailabilitiesAfterCurrentTime,
|
|
19
|
+
filterAvailabilitiesAfterPublicBookingCutoff,
|
|
20
|
+
} from '../src/lib/booking/booking-cutoffs';
|
|
21
|
+
import {
|
|
22
|
+
findFirstDateWithBookableAvailability,
|
|
23
|
+
selectedDateHasVisibleAvailability,
|
|
24
|
+
} from '../src/components/booking/availability-date-selection';
|
|
25
|
+
import {
|
|
26
|
+
buildChangeBookingServerPreview,
|
|
27
|
+
mapQuoteLineItemsToPriceSummaryLines,
|
|
28
|
+
} from '../src/lib/booking/change-booking-server-preview';
|
|
16
29
|
import {
|
|
17
30
|
buildAdminChangeQuoteRequestKey,
|
|
18
31
|
canReuseCompletedAdminChangeQuote,
|
|
19
32
|
} from '../src/components/booking/admin-change-quote-request-key';
|
|
33
|
+
import {
|
|
34
|
+
buildAdminChangeV2QuoteRequest,
|
|
35
|
+
quoteAdminChangeV2ForUi,
|
|
36
|
+
} from '../src/components/booking/admin-change-v2-quote-request';
|
|
20
37
|
import {
|
|
21
38
|
noRefundRemovedValue,
|
|
22
39
|
normalizeReturnPriceTreatment,
|
|
@@ -24,6 +41,10 @@ import {
|
|
|
24
41
|
releaseRefundDispositionOptions,
|
|
25
42
|
returnPriceTreatmentOptions,
|
|
26
43
|
} from '../src/components/booking/admin-refund-disposition';
|
|
44
|
+
import {
|
|
45
|
+
mergeAdminRefundDecisionContext,
|
|
46
|
+
selectRefundDispositionsForOperations,
|
|
47
|
+
} from '../src/components/booking/admin-refund-decision-context';
|
|
27
48
|
import {
|
|
28
49
|
applyResourceAdjustments,
|
|
29
50
|
privateShuttlePriceChanged,
|
|
@@ -37,9 +58,11 @@ import {
|
|
|
37
58
|
} from '../src/components/booking/reservation-hold';
|
|
38
59
|
import { buildAdminChangeProviderPayload } from '../src/components/booking/admin-change-provider-payload';
|
|
39
60
|
import { buildPublicChangeAmountDueSummary } from '../src/components/booking/change-booking-flow-helpers';
|
|
61
|
+
import { collapseAdminPromoLines } from '../src/components/booking/admin-change-receipt-lines';
|
|
40
62
|
import {
|
|
41
63
|
buildAdminChangePaymentChoiceData,
|
|
42
64
|
buildAdminChangePayNowCheckoutModalData,
|
|
65
|
+
shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose,
|
|
43
66
|
} from '../src/components/booking/admin-change-payment-choice-runner';
|
|
44
67
|
|
|
45
68
|
function quote(overrides: Partial<ChangeBookingQuoteResponse>): ChangeBookingQuoteResponse {
|
|
@@ -59,16 +82,103 @@ function quoteSlice(overrides: Partial<ChangeQuoteUiSlice>): ChangeQuoteUiSlice
|
|
|
59
82
|
};
|
|
60
83
|
}
|
|
61
84
|
|
|
62
|
-
function test(name: string, fn: () => void): void {
|
|
85
|
+
function test(name: string, fn: () => void | Promise<void>): void {
|
|
63
86
|
try {
|
|
64
|
-
fn();
|
|
65
|
-
|
|
87
|
+
const result = fn();
|
|
88
|
+
if (result instanceof Promise) {
|
|
89
|
+
void result.then(
|
|
90
|
+
() => console.log(`ok - ${name}`),
|
|
91
|
+
(error) => {
|
|
92
|
+
console.error(`not ok - ${name}`);
|
|
93
|
+
throw error;
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`ok - ${name}`);
|
|
98
|
+
}
|
|
66
99
|
} catch (error) {
|
|
67
100
|
console.error(`not ok - ${name}`);
|
|
68
101
|
throw error;
|
|
69
102
|
}
|
|
70
103
|
}
|
|
71
104
|
|
|
105
|
+
function calendarAvailability(dateTime: string, vacancies: number): Availability {
|
|
106
|
+
return { dateTime, vacancies, currency: 'CAD' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test('admin amendment display combines one promo allocation across component rows', () => {
|
|
110
|
+
const lines = collapseAdminPromoLines([
|
|
111
|
+
{ kind: 'ticket', category: 'ADULT', qty: 1, itemTotal: 170.49 },
|
|
112
|
+
{ kind: 'line', label: 'Moraine Lake Road Access Fee', amount: 15.99, type: 'FEE' },
|
|
113
|
+
{ kind: 'line', label: 'Discount · Moraine Lake Road Access Fee', amount: -2.4, type: 'DISCOUNT' },
|
|
114
|
+
{ kind: 'line', label: 'Discount · ADULT', amount: -25.57, type: 'DISCOUNT' },
|
|
115
|
+
], 'banffblog15');
|
|
116
|
+
|
|
117
|
+
assert.deepEqual(lines, [
|
|
118
|
+
{ kind: 'ticket', category: 'ADULT', qty: 1, itemTotal: 170.49 },
|
|
119
|
+
{ kind: 'line', label: 'Moraine Lake Road Access Fee', amount: 15.99, type: 'FEE' },
|
|
120
|
+
{ kind: 'line', label: 'Promo: BANFFBLOG15', amount: -27.97, type: 'PROMO_CODE' },
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('admin amendment display leaves unrelated manual discounts separate', () => {
|
|
125
|
+
const lines = collapseAdminPromoLines([
|
|
126
|
+
{ kind: 'line', label: 'Promo retained booking value', amount: -10, type: 'DISCOUNT' },
|
|
127
|
+
{ kind: 'line', label: 'Manager courtesy', amount: -5, type: 'DISCOUNT' },
|
|
128
|
+
{ kind: 'line', label: 'Discount · ADULT', amount: -20, type: 'PROMO_CODE' },
|
|
129
|
+
], 'SAVE20');
|
|
130
|
+
|
|
131
|
+
assert.deepEqual(lines, [
|
|
132
|
+
{ kind: 'line', label: 'Promo: SAVE20', amount: -30, type: 'PROMO_CODE' },
|
|
133
|
+
{ kind: 'line', label: 'Manager courtesy', amount: -5, type: 'DISCOUNT' },
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('admin availability filtering removes only starts that have passed', () => {
|
|
138
|
+
const now = new Date('2026-07-21T17:00:00-06:00');
|
|
139
|
+
const rows = [
|
|
140
|
+
calendarAvailability('2026-07-21T16:59:00-06:00', 4),
|
|
141
|
+
calendarAvailability('2026-07-21T17:01:00-06:00', 4),
|
|
142
|
+
calendarAvailability('2026-07-22T08:00:00-06:00', 4),
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
assert.deepEqual(
|
|
146
|
+
filterAvailabilitiesAfterCurrentTime(rows, now).map((row) => row.dateTime),
|
|
147
|
+
['2026-07-21T17:01:00-06:00', '2026-07-22T08:00:00-06:00'],
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('public availability filtering keeps its stricter advance cutoff', () => {
|
|
152
|
+
const now = new Date('2026-07-21T17:00:00-06:00');
|
|
153
|
+
const rows = [
|
|
154
|
+
calendarAvailability('2026-07-21T17:30:00-06:00', 4),
|
|
155
|
+
calendarAvailability('2026-07-21T18:30:00-06:00', 4),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
assert.deepEqual(
|
|
159
|
+
filterAvailabilitiesAfterPublicBookingCutoff(rows, now, 60).map((row) => row.dateTime),
|
|
160
|
+
['2026-07-21T18:30:00-06:00'],
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('calendar auto-selection advances when the selected day has no visible starts', () => {
|
|
165
|
+
const rowsByDate: Record<string, Availability[]> = {
|
|
166
|
+
'2026-07-22': [calendarAvailability('2026-07-22T08:00:00-06:00', 0)],
|
|
167
|
+
'2026-07-23': [calendarAvailability('2026-07-23T08:00:00-06:00', 3)],
|
|
168
|
+
};
|
|
169
|
+
const getRowsForDate = (date: string) => rowsByDate[date] ?? [];
|
|
170
|
+
|
|
171
|
+
assert.equal(selectedDateHasVisibleAvailability('2026-07-21', getRowsForDate), false);
|
|
172
|
+
assert.equal(
|
|
173
|
+
findFirstDateWithBookableAvailability(
|
|
174
|
+
['2026-07-22', '2026-07-23'],
|
|
175
|
+
getRowsForDate,
|
|
176
|
+
(availability) => availability.vacancies > 0,
|
|
177
|
+
),
|
|
178
|
+
'2026-07-23',
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
72
182
|
test('admin payment choice countdown expires exactly at the reservation deadline', () => {
|
|
73
183
|
const expiration = '2026-07-16T15:46:44+00:00';
|
|
74
184
|
assert.equal(
|
|
@@ -120,6 +230,16 @@ test('admin existing-booking pay-now review keeps previous, updated, and exact d
|
|
|
120
230
|
assert.equal(checkout.finalizeExistingChangeInline, true);
|
|
121
231
|
assert.equal(choice.confirmWithoutPaymentLabel, 'Pay later');
|
|
122
232
|
assert.equal(choice.preferConfirmWithoutPayment, true);
|
|
233
|
+
assert.equal(
|
|
234
|
+
shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose(true, true, choice),
|
|
235
|
+
true,
|
|
236
|
+
);
|
|
237
|
+
assert.equal(
|
|
238
|
+
shouldRestoreAdminChangePaymentChoiceAfterCheckoutClose(true, true, {
|
|
239
|
+
pricingQuoteId: ' ',
|
|
240
|
+
}),
|
|
241
|
+
false,
|
|
242
|
+
);
|
|
123
243
|
assert.equal(
|
|
124
244
|
buildAdminChangePayNowCheckoutModalData(
|
|
125
245
|
{ ...choice, finalizeExistingChangeInline: undefined },
|
|
@@ -205,6 +325,93 @@ test('admin quote dedupe refetches unchanged inputs after the published quote is
|
|
|
205
325
|
assert.equal(canReuseCompletedAdminChangeQuote(inputKey, 'different-inputs', true), false);
|
|
206
326
|
});
|
|
207
327
|
|
|
328
|
+
test('admin preview and checkout share the Pricing V2 request shape', () => {
|
|
329
|
+
const request = buildAdminChangeV2QuoteRequest({
|
|
330
|
+
bookingReference: ' VFSCAEKQ ',
|
|
331
|
+
lastName: ' Tauro ',
|
|
332
|
+
parentProductId: 'product_1',
|
|
333
|
+
optionId: 'option_1',
|
|
334
|
+
selectedAvailability: {
|
|
335
|
+
availabilityId: 'availability_1',
|
|
336
|
+
productId: 'option_1',
|
|
337
|
+
dateTime: '2026-07-28T08:35:00-06:00',
|
|
338
|
+
vacancies: 10,
|
|
339
|
+
} as Parameters<typeof buildAdminChangeV2QuoteRequest>[0]['selectedAvailability'],
|
|
340
|
+
pickupLocationId: 'pickup_1',
|
|
341
|
+
returnAvailabilityId: null,
|
|
342
|
+
bookingItems: [{ category: 'ADULT', count: 3 }],
|
|
343
|
+
addOnSelections: [],
|
|
344
|
+
cancellationPolicyId: 'policy_1',
|
|
345
|
+
promoCode: ' banffblog15 ',
|
|
346
|
+
promoApplicationScope: 'ENTIRE_BOOKING',
|
|
347
|
+
structuredAdjustments: [{ type: 'CUSTOM', amount: 5 }],
|
|
348
|
+
refundDispositionsByOperationId: { remove_1: 'PENDING_REFUND' },
|
|
349
|
+
previousPassengerCount: 2,
|
|
350
|
+
previousAvailabilityId: 'availability_old',
|
|
351
|
+
previousReturnAvailabilityId: null,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
assert.equal(request.bookingReference, 'VFSCAEKQ');
|
|
355
|
+
assert.equal(request.lastName, 'Tauro');
|
|
356
|
+
assert.equal(request.newProductId, 'option_1');
|
|
357
|
+
assert.deepEqual(request.newPassengerCounts, [{ category: 'ADULT', count: 3 }]);
|
|
358
|
+
assert.equal(request.promoCode, 'banffblog15');
|
|
359
|
+
assert.equal(request.promoApplicationScope, 'ENTIRE_BOOKING');
|
|
360
|
+
assert.deepEqual(request.refundDispositionsByOperationId, { remove_1: 'PENDING_REFUND' });
|
|
361
|
+
assert.deepEqual(request.capacitySeatCredit, {
|
|
362
|
+
enabled: true,
|
|
363
|
+
previousPassengerCount: 2,
|
|
364
|
+
previousAvailabilityId: 'availability_old',
|
|
365
|
+
previousReturnAvailabilityId: null,
|
|
366
|
+
});
|
|
367
|
+
assert.equal('clientProposedTotal' in request, false);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('admin checkout maps the fresh Pricing V2 quote id instead of a legacy response', async () => {
|
|
371
|
+
const request = buildAdminChangeV2QuoteRequest({
|
|
372
|
+
bookingReference: 'VFSCAEKQ',
|
|
373
|
+
lastName: 'Tauro',
|
|
374
|
+
parentProductId: 'product_1',
|
|
375
|
+
optionId: 'option_1',
|
|
376
|
+
selectedAvailability: {
|
|
377
|
+
availabilityId: 'availability_1',
|
|
378
|
+
productId: 'option_1',
|
|
379
|
+
dateTime: '2026-07-28T08:35:00-06:00',
|
|
380
|
+
vacancies: 10,
|
|
381
|
+
} as Parameters<typeof buildAdminChangeV2QuoteRequest>[0]['selectedAvailability'],
|
|
382
|
+
pickupLocationId: 'pickup_1',
|
|
383
|
+
returnAvailabilityId: null,
|
|
384
|
+
bookingItems: [{ category: 'ADULT', count: 3 }],
|
|
385
|
+
addOnSelections: [],
|
|
386
|
+
cancellationPolicyId: 'policy_1',
|
|
387
|
+
structuredAdjustments: [],
|
|
388
|
+
refundDispositionsByOperationId: {},
|
|
389
|
+
previousPassengerCount: 2,
|
|
390
|
+
previousAvailabilityId: 'availability_old',
|
|
391
|
+
previousReturnAvailabilityId: null,
|
|
392
|
+
});
|
|
393
|
+
const mergedQuote = await quoteAdminChangeV2ForUi(
|
|
394
|
+
request,
|
|
395
|
+
{ total: 768.11, subtotal: 707.94, tax: 60.17 },
|
|
396
|
+
'CAD',
|
|
397
|
+
async () => quote({
|
|
398
|
+
pricingQuote: {
|
|
399
|
+
quoteId: 'quote_v2_checkout',
|
|
400
|
+
currency: 'CAD',
|
|
401
|
+
payableTotal: 768.11,
|
|
402
|
+
amountToCharge: 256.03,
|
|
403
|
+
balanceDelta: 256.03,
|
|
404
|
+
lines: [],
|
|
405
|
+
},
|
|
406
|
+
balanceDeltaMajorUnits: 256.03,
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
assert.equal(mergedQuote.quoteId, 'quote_v2_checkout');
|
|
411
|
+
assert.equal(mergedQuote.priceDiff, 256.03);
|
|
412
|
+
assert.equal(mergedQuote.quotedTotal, 768.11);
|
|
413
|
+
});
|
|
414
|
+
|
|
208
415
|
test('admin no-charge provider payload carries the selected pickup itinerary', () => {
|
|
209
416
|
const itineraryDisplay: NonNullable<
|
|
210
417
|
Parameters<typeof buildAdminChangeProviderPayload>[0]['itineraryDisplay']
|
|
@@ -234,6 +441,7 @@ test('admin no-charge provider payload carries the selected pickup itinerary', (
|
|
|
234
441
|
cancellationPolicyId: 'standard',
|
|
235
442
|
initialCancellationPolicyId: 'standard',
|
|
236
443
|
appliedPromoCode: null,
|
|
444
|
+
promoApplicationScope: 'CHANGE_ONLY',
|
|
237
445
|
newTotalAmount: 202.87,
|
|
238
446
|
providerPricingOverrides: [],
|
|
239
447
|
mergedProviderAdditionalAdjustments: [],
|
|
@@ -254,6 +462,7 @@ test('admin no-charge provider payload carries the selected pickup itinerary', (
|
|
|
254
462
|
assert.deepEqual(payload?.addOnSelections, []);
|
|
255
463
|
assert.equal(payload?.pricingV2QuoteId, 'quote_reviewed');
|
|
256
464
|
assert.equal(payload?.pricingV2QuotedTotal, 202.87);
|
|
465
|
+
assert.equal(payload?.promoApplicationScope, null);
|
|
257
466
|
});
|
|
258
467
|
|
|
259
468
|
test('public paid change preview shows amendment-only charges before combined tax', () => {
|
|
@@ -348,6 +557,249 @@ test('admin removal quote preserves explicit release refund decisions for the pr
|
|
|
348
557
|
);
|
|
349
558
|
});
|
|
350
559
|
|
|
560
|
+
test('admin refund decisions remain stable when successive quotes return only unresolved operations', () => {
|
|
561
|
+
const removeReturn = {
|
|
562
|
+
operationId: 'op_remove_return',
|
|
563
|
+
type: 'REMOVE_RETURN',
|
|
564
|
+
componentKey: 'return:rt_morning',
|
|
565
|
+
quantityDelta: -1,
|
|
566
|
+
};
|
|
567
|
+
const removeAdult = {
|
|
568
|
+
operationId: 'op_remove_adult',
|
|
569
|
+
type: 'REMOVE_QUANTITY',
|
|
570
|
+
componentKey: 'ticket:ADULT',
|
|
571
|
+
quantityDelta: -1,
|
|
572
|
+
};
|
|
573
|
+
const allowed = ['PENDING_REFUND', 'NO_REFUND'] as const;
|
|
574
|
+
|
|
575
|
+
const initial = mergeAdminRefundDecisionContext({
|
|
576
|
+
current: null,
|
|
577
|
+
selectionKey: 'selection-a',
|
|
578
|
+
operations: [removeReturn, removeAdult],
|
|
579
|
+
allowedByOperationId: {
|
|
580
|
+
[removeReturn.operationId]: [...allowed],
|
|
581
|
+
[removeAdult.operationId]: [...allowed],
|
|
582
|
+
},
|
|
583
|
+
removedValueByOperationId: {
|
|
584
|
+
[removeReturn.operationId]: 42.32,
|
|
585
|
+
[removeAdult.operationId]: 220.83,
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
const afterReturnDecision = mergeAdminRefundDecisionContext({
|
|
589
|
+
current: initial,
|
|
590
|
+
selectionKey: 'selection-a',
|
|
591
|
+
// The server now returns only the still-unanswered quantity removal.
|
|
592
|
+
operations: [removeAdult],
|
|
593
|
+
allowedByOperationId: { [removeAdult.operationId]: [...allowed] },
|
|
594
|
+
removedValueByOperationId: { [removeAdult.operationId]: 220.83 },
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
assert.deepEqual(
|
|
598
|
+
afterReturnDecision?.operations.map((operation) => operation.operationId),
|
|
599
|
+
[removeReturn.operationId, removeAdult.operationId],
|
|
600
|
+
);
|
|
601
|
+
assert.deepEqual(
|
|
602
|
+
selectRefundDispositionsForOperations(afterReturnDecision, [], {
|
|
603
|
+
[removeReturn.operationId]: 'PENDING_REFUND',
|
|
604
|
+
}),
|
|
605
|
+
{ [removeReturn.operationId]: 'PENDING_REFUND' },
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const complete = mergeAdminRefundDecisionContext({
|
|
609
|
+
current: afterReturnDecision,
|
|
610
|
+
selectionKey: 'selection-a',
|
|
611
|
+
operations: [],
|
|
612
|
+
allowedByOperationId: {},
|
|
613
|
+
removedValueByOperationId: {},
|
|
614
|
+
});
|
|
615
|
+
assert.deepEqual(
|
|
616
|
+
selectRefundDispositionsForOperations(complete, [], {
|
|
617
|
+
[removeReturn.operationId]: 'PENDING_REFUND',
|
|
618
|
+
[removeAdult.operationId]: 'NO_REFUND',
|
|
619
|
+
}),
|
|
620
|
+
{
|
|
621
|
+
[removeReturn.operationId]: 'PENDING_REFUND',
|
|
622
|
+
[removeAdult.operationId]: 'NO_REFUND',
|
|
623
|
+
},
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
const nextSelection = mergeAdminRefundDecisionContext({
|
|
627
|
+
current: complete,
|
|
628
|
+
selectionKey: 'selection-b',
|
|
629
|
+
operations: [removeAdult],
|
|
630
|
+
allowedByOperationId: { [removeAdult.operationId]: [...allowed] },
|
|
631
|
+
removedValueByOperationId: { [removeAdult.operationId]: 220.83 },
|
|
632
|
+
});
|
|
633
|
+
assert.deepEqual(
|
|
634
|
+
nextSelection?.operations.map((operation) => operation.operationId),
|
|
635
|
+
[removeAdult.operationId],
|
|
636
|
+
);
|
|
637
|
+
assert.deepEqual(
|
|
638
|
+
selectRefundDispositionsForOperations(nextSelection, [], {
|
|
639
|
+
[removeReturn.operationId]: 'PENDING_REFUND',
|
|
640
|
+
[removeAdult.operationId]: 'NO_REFUND',
|
|
641
|
+
}),
|
|
642
|
+
{ [removeAdult.operationId]: 'NO_REFUND' },
|
|
643
|
+
);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('admin move-service quote labels only its ticket price difference', () => {
|
|
647
|
+
const moveOperation = {
|
|
648
|
+
operationId: 'op_move_service',
|
|
649
|
+
type: 'MOVE_SERVICE',
|
|
650
|
+
componentKey: 'service',
|
|
651
|
+
};
|
|
652
|
+
const addOperation = {
|
|
653
|
+
operationId: 'op_add_adult',
|
|
654
|
+
type: 'ADD_QUANTITY',
|
|
655
|
+
componentKey: 'ticket:ADULT',
|
|
656
|
+
quantityDelta: 1,
|
|
657
|
+
};
|
|
658
|
+
const mapped = mapAdminChangeBookingQuoteV2Data({
|
|
659
|
+
oldReceipt: {
|
|
660
|
+
currency: 'CAD',
|
|
661
|
+
grossSubtotal: 247.96,
|
|
662
|
+
taxAmount: 21.08,
|
|
663
|
+
payableTotal: 269.04,
|
|
664
|
+
lines: [
|
|
665
|
+
{
|
|
666
|
+
type: 'TICKET',
|
|
667
|
+
label: 'ADULT',
|
|
668
|
+
quantity: 2,
|
|
669
|
+
amount: 215.98,
|
|
670
|
+
billable: true,
|
|
671
|
+
priceBasis: {
|
|
672
|
+
baseUnitAmount: 139.99,
|
|
673
|
+
finalUnitAmount: 107.99,
|
|
674
|
+
currency: 'CAD',
|
|
675
|
+
adjustments: [
|
|
676
|
+
{
|
|
677
|
+
type: 'DYNAMIC_PRICING',
|
|
678
|
+
id: 'rule_early_booking',
|
|
679
|
+
name: 'Early booking discount',
|
|
680
|
+
amount: -32,
|
|
681
|
+
adjustmentType: 'FIXED',
|
|
682
|
+
adjustmentValue: 32,
|
|
683
|
+
},
|
|
684
|
+
],
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
},
|
|
689
|
+
newQuote: {
|
|
690
|
+
currency: 'CAD',
|
|
691
|
+
payableTotal: 455.64,
|
|
692
|
+
balanceDelta: 186.60,
|
|
693
|
+
amendment: { operations: [moveOperation, addOperation] },
|
|
694
|
+
lines: [
|
|
695
|
+
{
|
|
696
|
+
type: 'TICKET',
|
|
697
|
+
label: 'ADULT',
|
|
698
|
+
quantity: 2,
|
|
699
|
+
amount: 48,
|
|
700
|
+
billable: true,
|
|
701
|
+
metadata: { category: 'ADULT', operationId: moveOperation.operationId },
|
|
702
|
+
priceBasis: {
|
|
703
|
+
baseUnitAmount: 149.99,
|
|
704
|
+
finalUnitAmount: 131.99,
|
|
705
|
+
currency: 'CAD',
|
|
706
|
+
adjustments: [
|
|
707
|
+
{
|
|
708
|
+
type: 'dynamic',
|
|
709
|
+
id: 'rule_new_date',
|
|
710
|
+
name: 'New-date adjustment',
|
|
711
|
+
amount: -18,
|
|
712
|
+
adjustmentType: 'fixed',
|
|
713
|
+
adjustmentValue: -18,
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
type: 'TICKET',
|
|
720
|
+
label: 'ADULT',
|
|
721
|
+
quantity: 1,
|
|
722
|
+
amount: 124,
|
|
723
|
+
billable: true,
|
|
724
|
+
metadata: { category: 'ADULT', operationId: addOperation.operationId },
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
},
|
|
728
|
+
canApply: true,
|
|
729
|
+
});
|
|
730
|
+
assert.deepEqual(mapped.originalReceipt?.lineItems?.[0]?.priceBasis, {
|
|
731
|
+
baseUnitAmount: 139.99,
|
|
732
|
+
finalUnitAmount: 107.99,
|
|
733
|
+
currency: 'CAD',
|
|
734
|
+
adjustments: [
|
|
735
|
+
{
|
|
736
|
+
type: 'DYNAMIC_PRICING',
|
|
737
|
+
id: 'rule_early_booking',
|
|
738
|
+
name: 'Early booking discount',
|
|
739
|
+
amount: -32,
|
|
740
|
+
adjustmentType: 'FIXED',
|
|
741
|
+
adjustmentValue: 32,
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
});
|
|
745
|
+
const existingPurchaseLines = mapQuoteLineItemsToPriceSummaryLines(
|
|
746
|
+
mapped.originalReceipt?.lineItems,
|
|
747
|
+
);
|
|
748
|
+
assert.deepEqual(
|
|
749
|
+
existingPurchaseLines[0]?.kind === 'ticket'
|
|
750
|
+
? existingPurchaseLines[0].breakdown?.lineItems
|
|
751
|
+
: null,
|
|
752
|
+
[
|
|
753
|
+
{ type: 'base', name: 'Base price', amountInDisplayCurrency: 139.99 },
|
|
754
|
+
{
|
|
755
|
+
type: 'adjustment',
|
|
756
|
+
id: 'rule_early_booking',
|
|
757
|
+
name: 'Early booking discount',
|
|
758
|
+
amountInDisplayCurrency: -32,
|
|
759
|
+
adjustmentType: 'FIXED',
|
|
760
|
+
adjustmentValue: 32,
|
|
761
|
+
sourceType: 'dynamic',
|
|
762
|
+
},
|
|
763
|
+
],
|
|
764
|
+
);
|
|
765
|
+
const preview = buildChangeBookingServerPreview(
|
|
766
|
+
mapped,
|
|
767
|
+
{ total: 455.64, subtotal: 419.94, tax: 35.70 },
|
|
768
|
+
'CAD',
|
|
769
|
+
);
|
|
770
|
+
const lines = preview?.priceSummaryLines ?? [];
|
|
771
|
+
|
|
772
|
+
assert.equal(lines[0]?.kind, 'ticket');
|
|
773
|
+
assert.equal(lines[0]?.kind === 'ticket' ? lines[0].category : null, 'ADULT — new date price difference');
|
|
774
|
+
assert.deepEqual(
|
|
775
|
+
lines[0]?.kind === 'ticket' ? lines[0].unitPriceComparison : null,
|
|
776
|
+
{
|
|
777
|
+
previousLabel: 'Existing adult price',
|
|
778
|
+
updatedLabel: 'New-date adult price',
|
|
779
|
+
differenceLabel: 'Difference per adult',
|
|
780
|
+
previousUnitAmount: 107.99,
|
|
781
|
+
updatedUnitAmount: 131.99,
|
|
782
|
+
differenceUnitAmount: 24,
|
|
783
|
+
},
|
|
784
|
+
);
|
|
785
|
+
assert.deepEqual(
|
|
786
|
+
lines[0]?.kind === 'ticket' ? lines[0].breakdown?.lineItems : null,
|
|
787
|
+
[
|
|
788
|
+
{ type: 'base', name: 'Base price', amountInDisplayCurrency: 149.99 },
|
|
789
|
+
{
|
|
790
|
+
type: 'adjustment',
|
|
791
|
+
id: 'rule_new_date',
|
|
792
|
+
name: 'New-date adjustment',
|
|
793
|
+
amountInDisplayCurrency: -18,
|
|
794
|
+
adjustmentType: 'fixed',
|
|
795
|
+
adjustmentValue: -18,
|
|
796
|
+
sourceType: 'dynamic',
|
|
797
|
+
},
|
|
798
|
+
],
|
|
799
|
+
);
|
|
800
|
+
assert.equal(lines[1]?.kind === 'ticket' ? lines[1].category : null, 'ADULT');
|
|
801
|
+
});
|
|
802
|
+
|
|
351
803
|
test('admin return price treatment defaults to keep floor and maps optional reprice choices', () => {
|
|
352
804
|
const operation = {
|
|
353
805
|
operationId: 'op_change_return',
|
|
@@ -442,9 +894,20 @@ test('admin cancellation policy and structured adjustments are authoritative quo
|
|
|
442
894
|
...base,
|
|
443
895
|
structuredAdjustments: [{ adjustmentId: 'adj_1', mode: 'FIXED_AMOUNT', fixedAmount: 10 }],
|
|
444
896
|
});
|
|
897
|
+
const wholeBookingPromo = buildAdminChangeQuoteRequestKey({
|
|
898
|
+
...base,
|
|
899
|
+
promoCode: 'BANFFBLOG15',
|
|
900
|
+
promoApplicationScope: 'ENTIRE_BOOKING',
|
|
901
|
+
});
|
|
902
|
+
const changeOnlyPromo = buildAdminChangeQuoteRequestKey({
|
|
903
|
+
...base,
|
|
904
|
+
promoCode: 'BANFFBLOG15',
|
|
905
|
+
promoApplicationScope: 'CHANGE_ONLY',
|
|
906
|
+
});
|
|
445
907
|
|
|
446
908
|
assert.notEqual(original, policyChanged);
|
|
447
909
|
assert.notEqual(original, adjustmentChanged);
|
|
910
|
+
assert.notEqual(changeOnlyPromo, wholeBookingPromo);
|
|
448
911
|
});
|
|
449
912
|
|
|
450
913
|
test('private shuttle pricing replaces the selected calendar summary with hydrated rate details', () => {
|