@ticketboothapp/booking 1.2.166 → 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 +95 -30
- 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-provider-payload.ts +9 -1
- package/src/components/booking/admin-change-quote-request-key.ts +14 -1
- 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 +40 -1
- package/src/components/booking/useAdminChangeQuoteDisplayState.tsx +2 -2
- package/src/components/booking/useAdminChangeQuotePreview.ts +72 -43
- 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 -4
|
@@ -11,9 +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';
|
|
16
|
-
import {
|
|
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';
|
|
29
|
+
import {
|
|
30
|
+
buildAdminChangeQuoteRequestKey,
|
|
31
|
+
canReuseCompletedAdminChangeQuote,
|
|
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';
|
|
17
37
|
import {
|
|
18
38
|
noRefundRemovedValue,
|
|
19
39
|
normalizeReturnPriceTreatment,
|
|
@@ -21,6 +41,10 @@ import {
|
|
|
21
41
|
releaseRefundDispositionOptions,
|
|
22
42
|
returnPriceTreatmentOptions,
|
|
23
43
|
} from '../src/components/booking/admin-refund-disposition';
|
|
44
|
+
import {
|
|
45
|
+
mergeAdminRefundDecisionContext,
|
|
46
|
+
selectRefundDispositionsForOperations,
|
|
47
|
+
} from '../src/components/booking/admin-refund-decision-context';
|
|
24
48
|
import {
|
|
25
49
|
applyResourceAdjustments,
|
|
26
50
|
privateShuttlePriceChanged,
|
|
@@ -34,6 +58,7 @@ import {
|
|
|
34
58
|
} from '../src/components/booking/reservation-hold';
|
|
35
59
|
import { buildAdminChangeProviderPayload } from '../src/components/booking/admin-change-provider-payload';
|
|
36
60
|
import { buildPublicChangeAmountDueSummary } from '../src/components/booking/change-booking-flow-helpers';
|
|
61
|
+
import { collapseAdminPromoLines } from '../src/components/booking/admin-change-receipt-lines';
|
|
37
62
|
import {
|
|
38
63
|
buildAdminChangePaymentChoiceData,
|
|
39
64
|
buildAdminChangePayNowCheckoutModalData,
|
|
@@ -57,16 +82,103 @@ function quoteSlice(overrides: Partial<ChangeQuoteUiSlice>): ChangeQuoteUiSlice
|
|
|
57
82
|
};
|
|
58
83
|
}
|
|
59
84
|
|
|
60
|
-
function test(name: string, fn: () => void): void {
|
|
85
|
+
function test(name: string, fn: () => void | Promise<void>): void {
|
|
61
86
|
try {
|
|
62
|
-
fn();
|
|
63
|
-
|
|
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
|
+
}
|
|
64
99
|
} catch (error) {
|
|
65
100
|
console.error(`not ok - ${name}`);
|
|
66
101
|
throw error;
|
|
67
102
|
}
|
|
68
103
|
}
|
|
69
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
|
+
|
|
70
182
|
test('admin payment choice countdown expires exactly at the reservation deadline', () => {
|
|
71
183
|
const expiration = '2026-07-16T15:46:44+00:00';
|
|
72
184
|
assert.equal(
|
|
@@ -206,6 +318,100 @@ test('admin quote response display state cannot invalidate its request identity'
|
|
|
206
318
|
assert.equal(keyAfterResponse, keyBeforeResponse);
|
|
207
319
|
});
|
|
208
320
|
|
|
321
|
+
test('admin quote dedupe refetches unchanged inputs after the published quote is cleared', () => {
|
|
322
|
+
const inputKey = 'same-admin-change-inputs';
|
|
323
|
+
assert.equal(canReuseCompletedAdminChangeQuote(inputKey, inputKey, true), true);
|
|
324
|
+
assert.equal(canReuseCompletedAdminChangeQuote(inputKey, inputKey, false), false);
|
|
325
|
+
assert.equal(canReuseCompletedAdminChangeQuote(inputKey, 'different-inputs', true), false);
|
|
326
|
+
});
|
|
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
|
+
|
|
209
415
|
test('admin no-charge provider payload carries the selected pickup itinerary', () => {
|
|
210
416
|
const itineraryDisplay: NonNullable<
|
|
211
417
|
Parameters<typeof buildAdminChangeProviderPayload>[0]['itineraryDisplay']
|
|
@@ -235,6 +441,7 @@ test('admin no-charge provider payload carries the selected pickup itinerary', (
|
|
|
235
441
|
cancellationPolicyId: 'standard',
|
|
236
442
|
initialCancellationPolicyId: 'standard',
|
|
237
443
|
appliedPromoCode: null,
|
|
444
|
+
promoApplicationScope: 'CHANGE_ONLY',
|
|
238
445
|
newTotalAmount: 202.87,
|
|
239
446
|
providerPricingOverrides: [],
|
|
240
447
|
mergedProviderAdditionalAdjustments: [],
|
|
@@ -255,6 +462,7 @@ test('admin no-charge provider payload carries the selected pickup itinerary', (
|
|
|
255
462
|
assert.deepEqual(payload?.addOnSelections, []);
|
|
256
463
|
assert.equal(payload?.pricingV2QuoteId, 'quote_reviewed');
|
|
257
464
|
assert.equal(payload?.pricingV2QuotedTotal, 202.87);
|
|
465
|
+
assert.equal(payload?.promoApplicationScope, null);
|
|
258
466
|
});
|
|
259
467
|
|
|
260
468
|
test('public paid change preview shows amendment-only charges before combined tax', () => {
|
|
@@ -349,6 +557,249 @@ test('admin removal quote preserves explicit release refund decisions for the pr
|
|
|
349
557
|
);
|
|
350
558
|
});
|
|
351
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
|
+
|
|
352
803
|
test('admin return price treatment defaults to keep floor and maps optional reprice choices', () => {
|
|
353
804
|
const operation = {
|
|
354
805
|
operationId: 'op_change_return',
|
|
@@ -443,9 +894,20 @@ test('admin cancellation policy and structured adjustments are authoritative quo
|
|
|
443
894
|
...base,
|
|
444
895
|
structuredAdjustments: [{ adjustmentId: 'adj_1', mode: 'FIXED_AMOUNT', fixedAmount: 10 }],
|
|
445
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
|
+
});
|
|
446
907
|
|
|
447
908
|
assert.notEqual(original, policyChanged);
|
|
448
909
|
assert.notEqual(original, adjustmentChanged);
|
|
910
|
+
assert.notEqual(changeOnlyPromo, wholeBookingPromo);
|
|
449
911
|
});
|
|
450
912
|
|
|
451
913
|
test('private shuttle pricing replaces the selected calendar summary with hydrated rate details', () => {
|