@ticketboothapp/booking 1.2.185 → 1.2.187
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 +7 -0
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +6 -0
- package/src/components/booking/AdminChangeReceiptComparison.tsx +115 -22
- package/src/components/booking/PriceBreakdown.tsx +24 -0
- package/src/components/booking/PriceSummary.tsx +19 -1
- package/src/components/booking/admin-change-provider-payload.ts +9 -5
- package/src/components/booking/admin-change-quote-request-key.ts +2 -0
- package/src/components/booking/admin-change-v2-quote-request.ts +6 -3
- package/src/components/booking/provider-dashboard-change-booking.ts +2 -0
- package/src/components/booking/useAdminChangeCheckoutController.ts +3 -0
- package/src/components/booking/useAdminChangeQuotePreview.ts +5 -0
- package/src/components/booking/useAdminCustomReceiptLines.ts +21 -0
- package/src/lib/booking-api.ts +2 -0
- package/test/change-booking-helpers.test.ts +100 -0
package/package.json
CHANGED
|
@@ -197,6 +197,8 @@ const checkoutPanelPropKeys = [
|
|
|
197
197
|
'onRemoveAdminCustomReceiptLine',
|
|
198
198
|
'onApplyAdminCustomReceiptLines',
|
|
199
199
|
'onSetAdminReceiptLineOverride',
|
|
200
|
+
'preserveExistingReceipt',
|
|
201
|
+
'onPreserveExistingReceiptChange',
|
|
200
202
|
'firstName',
|
|
201
203
|
'lastName',
|
|
202
204
|
'email',
|
|
@@ -348,6 +348,7 @@ export function AdminChangeBookingFlow({
|
|
|
348
348
|
>({});
|
|
349
349
|
const [changeQuoteLoading, setChangeQuoteLoading] = useState(false);
|
|
350
350
|
const [changeQuoteFetchError, setChangeQuoteFetchError] = useState<string | null>(null);
|
|
351
|
+
const [preserveExistingReceipt, setPreserveExistingReceipt] = useState(false);
|
|
351
352
|
/** Dedupe quote calls while user input payload is unchanged. */
|
|
352
353
|
const lastCompletedQuoteInputsKeyRef = useRef<string | null>(null);
|
|
353
354
|
const inFlightQuoteInputsKeyRef = useRef<string | null>(null);
|
|
@@ -804,6 +805,7 @@ export function AdminChangeBookingFlow({
|
|
|
804
805
|
adminCustomReceiptLines: appliedAdminCustomReceiptLines,
|
|
805
806
|
structuredAdjustments: adminStructuredAdjustments,
|
|
806
807
|
reverseAdjustmentIds: adminReverseAdjustmentIds,
|
|
808
|
+
preserveExistingReceipt,
|
|
807
809
|
useAdminFeAuthoritativeQuote,
|
|
808
810
|
}), [
|
|
809
811
|
initialValues?.bookingReference,
|
|
@@ -820,6 +822,7 @@ export function AdminChangeBookingFlow({
|
|
|
820
822
|
appliedAdminCustomReceiptLines,
|
|
821
823
|
adminStructuredAdjustments,
|
|
822
824
|
adminReverseAdjustmentIds,
|
|
825
|
+
preserveExistingReceipt,
|
|
823
826
|
useAdminFeAuthoritativeQuote,
|
|
824
827
|
]);
|
|
825
828
|
const responseRefundDecisionOperations = latestChangeQuote?.refundDecisionOperations ?? [];
|
|
@@ -931,6 +934,7 @@ export function AdminChangeBookingFlow({
|
|
|
931
934
|
adminCustomReceiptLines: appliedAdminCustomReceiptLines,
|
|
932
935
|
adminStructuredAdjustments,
|
|
933
936
|
adminReverseAdjustmentIds,
|
|
937
|
+
preserveExistingReceipt,
|
|
934
938
|
needsAdjustmentDiscovery:
|
|
935
939
|
isProviderDashboardChange && !hasHydratedExistingAdjustments,
|
|
936
940
|
refundDispositionsByOperationId: activeRefundDispositionsByOperationId,
|
|
@@ -1153,6 +1157,7 @@ export function AdminChangeBookingFlow({
|
|
|
1153
1157
|
mergedProviderAdditionalAdjustments,
|
|
1154
1158
|
adminStructuredAdjustments,
|
|
1155
1159
|
adminReverseAdjustmentIds,
|
|
1160
|
+
preserveExistingReceipt,
|
|
1156
1161
|
refundDispositionsByOperationId: activeRefundDispositionsByOperationId,
|
|
1157
1162
|
providerApplyAuthoritativeReceipt,
|
|
1158
1163
|
onSuccess,
|
|
@@ -1301,6 +1306,8 @@ export function AdminChangeBookingFlow({
|
|
|
1301
1306
|
onRemoveAdminCustomReceiptLine: handleRemoveAdminCustomReceiptLine,
|
|
1302
1307
|
onApplyAdminCustomReceiptLines: handleApplyAdminCustomReceiptLines,
|
|
1303
1308
|
onSetAdminReceiptLineOverride: handleSetAdminReceiptLineOverride,
|
|
1309
|
+
preserveExistingReceipt,
|
|
1310
|
+
onPreserveExistingReceiptChange: setPreserveExistingReceipt,
|
|
1304
1311
|
onFirstNameChange: setFirstName, onLastNameChange: setLastName, onEmailChange: setEmail,
|
|
1305
1312
|
onClearError: () => setError(''),
|
|
1306
1313
|
pickupLocations: product.pickupLocations, destinations: product.destinations, highlightedPickupLocationIds,
|
|
@@ -87,6 +87,8 @@ export interface AdminChangeCheckoutPanelProps {
|
|
|
87
87
|
baseAmount: number;
|
|
88
88
|
targetAmount: number;
|
|
89
89
|
}) => void;
|
|
90
|
+
preserveExistingReceipt: boolean;
|
|
91
|
+
onPreserveExistingReceiptChange: (preserve: boolean) => void;
|
|
90
92
|
firstName: string;
|
|
91
93
|
lastName: string;
|
|
92
94
|
email: string;
|
|
@@ -179,6 +181,8 @@ export function AdminChangeCheckoutPanel({
|
|
|
179
181
|
onRemoveAdminCustomReceiptLine,
|
|
180
182
|
onApplyAdminCustomReceiptLines,
|
|
181
183
|
onSetAdminReceiptLineOverride,
|
|
184
|
+
preserveExistingReceipt,
|
|
185
|
+
onPreserveExistingReceiptChange,
|
|
182
186
|
firstName,
|
|
183
187
|
lastName,
|
|
184
188
|
email,
|
|
@@ -282,6 +286,8 @@ export function AdminChangeCheckoutPanel({
|
|
|
282
286
|
promoEditor={promoEditor}
|
|
283
287
|
promoCode={promoCode}
|
|
284
288
|
adjustments={pricingAdjustments}
|
|
289
|
+
preserveExistingReceipt={preserveExistingReceipt}
|
|
290
|
+
onPreserveExistingReceiptChange={onPreserveExistingReceiptChange}
|
|
285
291
|
adminCustomReceiptLines={adminCustomReceiptLines}
|
|
286
292
|
onSetReceiptLineOverride={onSetAdminReceiptLineOverride}
|
|
287
293
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, type ReactNode } from 'react';
|
|
1
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
AdminAmendmentOperationSnapshot,
|
|
4
4
|
AdminAmendmentRefundDisposition,
|
|
@@ -72,6 +72,8 @@ export interface AdminChangeReceiptComparisonProps {
|
|
|
72
72
|
baseAmount: number;
|
|
73
73
|
targetAmount: number;
|
|
74
74
|
}) => void;
|
|
75
|
+
preserveExistingReceipt?: boolean;
|
|
76
|
+
onPreserveExistingReceiptChange?: (preserve: boolean) => void;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
function normalizeLocale(locale: string): Locale {
|
|
@@ -128,8 +130,13 @@ export function AdminChangeReceiptComparison({
|
|
|
128
130
|
refundQuoteLoading = false,
|
|
129
131
|
adminCustomReceiptLines = [],
|
|
130
132
|
onSetReceiptLineOverride,
|
|
133
|
+
preserveExistingReceipt = false,
|
|
134
|
+
onPreserveExistingReceiptChange,
|
|
131
135
|
}: AdminChangeReceiptComparisonProps) {
|
|
132
136
|
const [overrideDrafts, setOverrideDrafts] = useState<Record<string, string>>({});
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (preserveExistingReceipt) setOverrideDrafts({});
|
|
139
|
+
}, [preserveExistingReceipt]);
|
|
133
140
|
const readableQuoteError = friendlyChangeBookingError(quoteError);
|
|
134
141
|
const displayLocale = normalizeLocale(locale);
|
|
135
142
|
const existingLines = originalLines(originalReceipt);
|
|
@@ -222,7 +229,7 @@ export function AdminChangeReceiptComparison({
|
|
|
222
229
|
line.overrideComponentId ? [[line.overrideComponentId, line] as const] : [],
|
|
223
230
|
),
|
|
224
231
|
);
|
|
225
|
-
const
|
|
232
|
+
const allSummaryDisplayLines = rawSummaryDisplayLines
|
|
226
233
|
.filter((line) => !(line.kind === 'line' && line.label.startsWith('Override · ')))
|
|
227
234
|
.map((line) => {
|
|
228
235
|
const type = line.kind === 'ticket' ? 'TICKET' : String(line.type ?? '').toUpperCase();
|
|
@@ -235,6 +242,18 @@ export function AdminChangeReceiptComparison({
|
|
|
235
242
|
),
|
|
236
243
|
};
|
|
237
244
|
});
|
|
245
|
+
const isRemovedSummaryLine = (line: PriceSummaryLine) => {
|
|
246
|
+
if (!line.editable || !line.componentId) return false;
|
|
247
|
+
const override = overrideByComponentId.get(line.componentId);
|
|
248
|
+
if (!override) return false;
|
|
249
|
+
const baseAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
250
|
+
const targetAmount = baseAmount + override.amountSign * Number(override.amountInput);
|
|
251
|
+
return Number.isFinite(targetAmount) && Math.abs(targetAmount) < 0.005;
|
|
252
|
+
};
|
|
253
|
+
const summaryDisplayLines = allSummaryDisplayLines.filter(
|
|
254
|
+
(line) => !isRemovedSummaryLine(line),
|
|
255
|
+
);
|
|
256
|
+
const removedSummaryDisplayLines = allSummaryDisplayLines.filter(isRemovedSummaryLine);
|
|
238
257
|
const quotedOverrideAmountInputs = Object.fromEntries(
|
|
239
258
|
summaryDisplayLines.flatMap((line) => {
|
|
240
259
|
if (!line.componentId || !line.lineKey) return [];
|
|
@@ -267,6 +286,34 @@ export function AdminChangeReceiptComparison({
|
|
|
267
286
|
return next;
|
|
268
287
|
});
|
|
269
288
|
};
|
|
289
|
+
const handleOverrideRemove = (lineKey: string) => {
|
|
290
|
+
const line = summaryDisplayLines.find((candidate) => candidate.lineKey === lineKey);
|
|
291
|
+
if (!line?.componentId) return;
|
|
292
|
+
const baseAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
293
|
+
onSetReceiptLineOverride?.({
|
|
294
|
+
componentId: line.componentId,
|
|
295
|
+
lineKey,
|
|
296
|
+
label: line.kind === 'ticket' ? line.category : line.label,
|
|
297
|
+
baseAmount,
|
|
298
|
+
targetAmount: 0,
|
|
299
|
+
});
|
|
300
|
+
setOverrideDrafts((current) => {
|
|
301
|
+
const next = { ...current };
|
|
302
|
+
delete next[lineKey];
|
|
303
|
+
return next;
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
const handleOverrideRestore = (line: PriceSummaryLine) => {
|
|
307
|
+
if (!line.componentId || !line.lineKey) return;
|
|
308
|
+
const baseAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
|
|
309
|
+
onSetReceiptLineOverride?.({
|
|
310
|
+
componentId: line.componentId,
|
|
311
|
+
lineKey: line.lineKey,
|
|
312
|
+
label: line.kind === 'ticket' ? line.category : line.label,
|
|
313
|
+
baseAmount,
|
|
314
|
+
targetAmount: baseAmount,
|
|
315
|
+
});
|
|
316
|
+
};
|
|
270
317
|
const summarySubtotal = isProductFamilyConversion
|
|
271
318
|
? familyConversionReceiptLines.reduce((sum, line) => {
|
|
272
319
|
if (line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX') return sum;
|
|
@@ -327,15 +374,38 @@ export function AdminChangeReceiptComparison({
|
|
|
327
374
|
</p>
|
|
328
375
|
) : null}
|
|
329
376
|
</div>
|
|
330
|
-
<
|
|
377
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
378
|
+
{onPreserveExistingReceiptChange ? (
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
aria-pressed={preserveExistingReceipt}
|
|
382
|
+
className={`rounded-md border px-2.5 py-1.5 text-xs font-semibold transition ${
|
|
383
|
+
preserveExistingReceipt
|
|
384
|
+
? 'border-sky-700 bg-sky-700 text-white'
|
|
385
|
+
: 'border-stone-300 bg-white text-stone-700 hover:border-sky-500 hover:text-sky-800'
|
|
386
|
+
}`}
|
|
387
|
+
onClick={() => onPreserveExistingReceiptChange(!preserveExistingReceipt)}
|
|
388
|
+
>
|
|
389
|
+
Keep receipt unchanged
|
|
390
|
+
</button>
|
|
391
|
+
) : null}
|
|
392
|
+
<span className="text-xs font-medium text-stone-500">
|
|
331
393
|
{isProductFamilyConversion
|
|
332
394
|
? formatCurrencyAmount(updatedTotal ?? 0, currency, displayLocale)
|
|
333
395
|
: bookingTotalChange == null
|
|
334
396
|
? '—'
|
|
335
397
|
: formatCurrencyAmount(bookingTotalChange, currency, displayLocale)}
|
|
336
|
-
|
|
398
|
+
</span>
|
|
399
|
+
</div>
|
|
337
400
|
</div>
|
|
338
|
-
{
|
|
401
|
+
{preserveExistingReceipt ? (
|
|
402
|
+
<div className="mb-3 rounded-md border border-sky-200 bg-white px-3 py-2">
|
|
403
|
+
<p className="text-sm font-semibold text-stone-800">Receipt and balance will stay unchanged</p>
|
|
404
|
+
<p className="mt-0.5 text-xs text-stone-600">
|
|
405
|
+
Only the booking details will change. No charge, refund, or balance adjustment will be created.
|
|
406
|
+
</p>
|
|
407
|
+
</div>
|
|
408
|
+
) : promoEditor}
|
|
339
409
|
{!selectionChanged ? (
|
|
340
410
|
<div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
|
|
341
411
|
<p className="text-sm font-medium text-stone-700">No changes selected</p>
|
|
@@ -353,37 +423,60 @@ export function AdminChangeReceiptComparison({
|
|
|
353
423
|
<div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
|
|
354
424
|
<p className="text-sm font-medium text-stone-700">Loading price change…</p>
|
|
355
425
|
</div>
|
|
356
|
-
) : summaryDisplayLines.length === 0 && Math.abs(amountDue) < 0.005 && removedWithoutRefund > 0 ? (
|
|
426
|
+
) : summaryDisplayLines.length === 0 && removedSummaryDisplayLines.length === 0 && Math.abs(amountDue) < 0.005 && removedWithoutRefund > 0 ? (
|
|
357
427
|
<div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
|
|
358
428
|
<p className="text-sm font-medium text-stone-800">No refund will be created</p>
|
|
359
429
|
<p className="mt-1 text-xs text-stone-600">
|
|
360
430
|
{formatCurrencyAmount(removedWithoutRefund, currency, displayLocale)} of booking value will be removed without customer credit.
|
|
361
431
|
</p>
|
|
362
432
|
</div>
|
|
363
|
-
) : summaryDisplayLines.length === 0 && Math.abs(amountDue) < 0.005 ? (
|
|
433
|
+
) : summaryDisplayLines.length === 0 && removedSummaryDisplayLines.length === 0 && Math.abs(amountDue) < 0.005 ? (
|
|
364
434
|
<div className="rounded-md border border-dashed border-stone-300 bg-white/70 px-3 py-4 text-center">
|
|
365
435
|
<p className="text-sm font-medium text-stone-700">No pricing changes</p>
|
|
366
436
|
</div>
|
|
367
437
|
) : (
|
|
368
|
-
<
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
438
|
+
<div>
|
|
439
|
+
<PriceSummary
|
|
440
|
+
lines={summaryDisplayLines}
|
|
441
|
+
total={summaryTotal ?? amountDue}
|
|
442
|
+
totalLabel={summaryTotalLabel}
|
|
443
|
+
showPositiveTotalSign={!isProductFamilyConversion}
|
|
444
|
+
currency={currency}
|
|
445
|
+
locale={displayLocale}
|
|
446
|
+
subtotal={summarySubtotal}
|
|
447
|
+
size="sm"
|
|
448
|
+
t={t}
|
|
449
|
+
lineAmountInputs={preserveExistingReceipt ? undefined : { ...quotedOverrideAmountInputs, ...overrideDrafts }}
|
|
450
|
+
onLineAmountInputChange={preserveExistingReceipt ? undefined : handleOverrideChange}
|
|
451
|
+
onLineAmountInputBlur={preserveExistingReceipt ? undefined : handleOverrideBlur}
|
|
452
|
+
onLineRemove={preserveExistingReceipt ? undefined : handleOverrideRemove}
|
|
453
|
+
/>
|
|
454
|
+
{removedSummaryDisplayLines.length > 0 ? (
|
|
455
|
+
<div className="mt-3 border-t border-stone-200 pt-2 text-xs text-stone-500">
|
|
456
|
+
<span className="font-medium text-stone-600">Removed lines:</span>{' '}
|
|
457
|
+
{removedSummaryDisplayLines.map((line, index) => {
|
|
458
|
+
const label = line.kind === 'ticket' ? line.category : line.label;
|
|
459
|
+
return (
|
|
460
|
+
<span key={line.lineKey ?? `${label}-${index}`}>
|
|
461
|
+
{index > 0 ? ', ' : ''}{label}{' '}
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
className="font-medium text-stone-700 underline underline-offset-2 hover:text-stone-950"
|
|
465
|
+
onClick={() => handleOverrideRestore(line)}
|
|
466
|
+
>
|
|
467
|
+
Undo
|
|
468
|
+
</button>
|
|
469
|
+
</span>
|
|
470
|
+
);
|
|
471
|
+
})}
|
|
472
|
+
</div>
|
|
473
|
+
) : null}
|
|
474
|
+
</div>
|
|
382
475
|
)}
|
|
383
476
|
</section>
|
|
384
477
|
</div>
|
|
385
478
|
|
|
386
|
-
{adjustments ? (
|
|
479
|
+
{!preserveExistingReceipt && adjustments ? (
|
|
387
480
|
<section className="rounded-lg border border-amber-200 bg-amber-50/40 p-3">
|
|
388
481
|
<h3 className="mb-2 text-sm font-semibold text-stone-900">Add custom line items</h3>
|
|
389
482
|
{adjustments}
|
|
@@ -28,6 +28,7 @@ export interface PriceBreakdownProps {
|
|
|
28
28
|
onEditableChange?: (value: string) => void;
|
|
29
29
|
onEditableBlur?: () => void;
|
|
30
30
|
onEditableReset?: () => void;
|
|
31
|
+
onEditableRemove?: () => void;
|
|
31
32
|
editableLabelValue?: string;
|
|
32
33
|
onEditableLabelChange?: (value: string) => void;
|
|
33
34
|
}
|
|
@@ -76,6 +77,7 @@ export function PriceBreakdown({
|
|
|
76
77
|
onEditableChange,
|
|
77
78
|
onEditableBlur,
|
|
78
79
|
onEditableReset,
|
|
80
|
+
onEditableRemove,
|
|
79
81
|
editableLabelValue,
|
|
80
82
|
onEditableLabelChange,
|
|
81
83
|
}: PriceBreakdownProps) {
|
|
@@ -129,6 +131,17 @@ export function PriceBreakdown({
|
|
|
129
131
|
onChange={(e) => onEditableChange(e.target.value)}
|
|
130
132
|
onBlur={onEditableBlur}
|
|
131
133
|
/>
|
|
134
|
+
{onEditableRemove ? (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
className="rounded p-0.5 text-stone-400 hover:bg-red-50 hover:text-red-600"
|
|
138
|
+
onClick={onEditableRemove}
|
|
139
|
+
aria-label={`Remove ${category} line`}
|
|
140
|
+
title="Remove line"
|
|
141
|
+
>
|
|
142
|
+
<span className="inline-block h-4 w-4 text-center text-base leading-4">×</span>
|
|
143
|
+
</button>
|
|
144
|
+
) : null}
|
|
132
145
|
</div>
|
|
133
146
|
) : (
|
|
134
147
|
<span className="text-sm font-medium text-stone-700">
|
|
@@ -191,6 +204,17 @@ export function PriceBreakdown({
|
|
|
191
204
|
onChange={(e) => onEditableChange(e.target.value)}
|
|
192
205
|
onBlur={onEditableBlur}
|
|
193
206
|
/>
|
|
207
|
+
{onEditableRemove ? (
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
className="rounded p-0.5 text-stone-400 hover:bg-red-50 hover:text-red-600"
|
|
211
|
+
onClick={onEditableRemove}
|
|
212
|
+
aria-label={`Remove ${category} line`}
|
|
213
|
+
title="Remove line"
|
|
214
|
+
>
|
|
215
|
+
<span className="inline-block h-4 w-4 text-center text-base leading-4">×</span>
|
|
216
|
+
</button>
|
|
217
|
+
) : null}
|
|
194
218
|
</div>
|
|
195
219
|
) : (
|
|
196
220
|
<span
|
|
@@ -98,6 +98,8 @@ export interface PriceSummaryProps {
|
|
|
98
98
|
onLineAmountInputBlur?: (lineKey: string) => void;
|
|
99
99
|
/** Optional reset handler for inline editable line amounts. */
|
|
100
100
|
onLineAmountReset?: (lineKey: string) => void;
|
|
101
|
+
/** Optional remove handler for inline editable lines. */
|
|
102
|
+
onLineRemove?: (lineKey: string) => void;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
function getLineAmountClass(type: string | undefined, amount: number): string {
|
|
@@ -174,7 +176,6 @@ export function PriceSummary({
|
|
|
174
176
|
locale,
|
|
175
177
|
subtotal,
|
|
176
178
|
taxAmount = 0,
|
|
177
|
-
taxRate: _taxRate,
|
|
178
179
|
discountAmount = 0,
|
|
179
180
|
discountLabel,
|
|
180
181
|
size = 'sm',
|
|
@@ -194,6 +195,7 @@ export function PriceSummary({
|
|
|
194
195
|
onLineLabelInputChange,
|
|
195
196
|
onLineAmountInputBlur,
|
|
196
197
|
onLineAmountReset,
|
|
198
|
+
onLineRemove,
|
|
197
199
|
}: PriceSummaryProps) {
|
|
198
200
|
const textSize = size === 'sm' ? 'text-sm' : 'text-base';
|
|
199
201
|
const totalSize = size === 'sm' ? 'text-xl' : 'text-2xl';
|
|
@@ -247,6 +249,11 @@ export function PriceSummary({
|
|
|
247
249
|
? () => onLineAmountReset(row.lineKey!)
|
|
248
250
|
: undefined
|
|
249
251
|
}
|
|
252
|
+
onEditableRemove={
|
|
253
|
+
row.editable && isBeforeSubtotalBoundary && row.lineKey && onLineRemove
|
|
254
|
+
? () => onLineRemove(row.lineKey!)
|
|
255
|
+
: undefined
|
|
256
|
+
}
|
|
250
257
|
editableLabelValue={row.lineKey ? lineLabelInputs?.[row.lineKey] : undefined}
|
|
251
258
|
onEditableLabelChange={
|
|
252
259
|
row.editable && isBeforeSubtotalBoundary && row.lineKey && onLineLabelInputChange
|
|
@@ -313,6 +320,17 @@ export function PriceSummary({
|
|
|
313
320
|
onChange={(e) => onLineAmountInputChange(lineKey, e.target.value)}
|
|
314
321
|
onBlur={() => onLineAmountInputBlur?.(lineKey)}
|
|
315
322
|
/>
|
|
323
|
+
{isBeforeSubtotalBoundary && onLineRemove ? (
|
|
324
|
+
<button
|
|
325
|
+
type="button"
|
|
326
|
+
className="rounded p-0.5 text-stone-400 hover:bg-red-50 hover:text-red-600"
|
|
327
|
+
onClick={() => onLineRemove(lineKey)}
|
|
328
|
+
aria-label={`Remove ${label} line`}
|
|
329
|
+
title="Remove line"
|
|
330
|
+
>
|
|
331
|
+
<span className="inline-block h-4 w-4 text-center text-base leading-4">×</span>
|
|
332
|
+
</button>
|
|
333
|
+
) : null}
|
|
316
334
|
</div>
|
|
317
335
|
) : (
|
|
318
336
|
<span className={`flex-shrink-0 whitespace-nowrap font-medium ${getLineAmountClass(type, amount)}`}>
|
|
@@ -40,6 +40,7 @@ export interface BuildAdminChangeProviderPayloadParams {
|
|
|
40
40
|
mergedProviderAdditionalAdjustments: AdminChangeProviderAdditionalAdjustment[];
|
|
41
41
|
adminStructuredAdjustments: NonNullable<ProviderDashboardChangeBookingPayload['structuredAdjustments']>;
|
|
42
42
|
adminReverseAdjustmentIds?: string[];
|
|
43
|
+
preserveExistingReceipt?: boolean;
|
|
43
44
|
refundDispositionsByOperationId: NonNullable<
|
|
44
45
|
ProviderDashboardChangeBookingPayload['refundDispositionsByOperationId']
|
|
45
46
|
>;
|
|
@@ -69,6 +70,7 @@ export function buildAdminChangeProviderPayload({
|
|
|
69
70
|
mergedProviderAdditionalAdjustments,
|
|
70
71
|
adminStructuredAdjustments,
|
|
71
72
|
adminReverseAdjustmentIds = [],
|
|
73
|
+
preserveExistingReceipt = false,
|
|
72
74
|
refundDispositionsByOperationId,
|
|
73
75
|
pricingV2QuoteId,
|
|
74
76
|
pricingV2QuotedTotal,
|
|
@@ -84,6 +86,7 @@ export function buildAdminChangeProviderPayload({
|
|
|
84
86
|
: null;
|
|
85
87
|
|
|
86
88
|
return {
|
|
89
|
+
preserveExistingReceipt,
|
|
87
90
|
productId: availabilityProductOptionId,
|
|
88
91
|
parentProductId: product.productId,
|
|
89
92
|
availabilityId: selectedAvailability.availabilityId ?? null,
|
|
@@ -102,10 +105,11 @@ export function buildAdminChangeProviderPayload({
|
|
|
102
105
|
cancellationPolicyId: cancellationPolicyId ?? initialCancellationPolicyId ?? null,
|
|
103
106
|
promoCode: appliedPromoCode ?? null,
|
|
104
107
|
promoApplicationScope: appliedPromoCode ? promoApplicationScope : null,
|
|
105
|
-
newTotalAmount,
|
|
108
|
+
newTotalAmount: preserveExistingReceipt ? undefined : newTotalAmount,
|
|
106
109
|
additionalHoursCount: null,
|
|
107
110
|
pricingAdjustment:
|
|
108
|
-
|
|
111
|
+
!preserveExistingReceipt &&
|
|
112
|
+
(providerPricingOverrides.length > 0 || mergedProviderAdditionalAdjustments.length > 0)
|
|
109
113
|
? {
|
|
110
114
|
mode: 'MANUAL_LINES',
|
|
111
115
|
...(providerPricingOverrides.length > 0 ? { lineOverrides: providerPricingOverrides } : {}),
|
|
@@ -115,9 +119,9 @@ export function buildAdminChangeProviderPayload({
|
|
|
115
119
|
}
|
|
116
120
|
: undefined,
|
|
117
121
|
authoritativeReceipt: providerApplyAuthoritativeReceipt,
|
|
118
|
-
structuredAdjustments: adminStructuredAdjustments,
|
|
119
|
-
reverseAdjustmentIds: adminReverseAdjustmentIds,
|
|
120
|
-
refundDispositionsByOperationId,
|
|
122
|
+
structuredAdjustments: preserveExistingReceipt ? [] : adminStructuredAdjustments,
|
|
123
|
+
reverseAdjustmentIds: preserveExistingReceipt ? [] : adminReverseAdjustmentIds,
|
|
124
|
+
refundDispositionsByOperationId: preserveExistingReceipt ? {} : refundDispositionsByOperationId,
|
|
121
125
|
pricingV2QuoteId: pricingV2QuoteId ?? null,
|
|
122
126
|
pricingV2QuotedTotal: pricingV2QuotedTotal ?? null,
|
|
123
127
|
capacitySeatCredit: {
|
|
@@ -16,6 +16,7 @@ interface AdminChangeQuoteRequestKeyInput {
|
|
|
16
16
|
promoApplicationScope?: AdminPromoApplicationScope | null;
|
|
17
17
|
structuredAdjustments?: unknown[];
|
|
18
18
|
reverseAdjustmentIds?: string[];
|
|
19
|
+
preserveExistingReceipt?: boolean;
|
|
19
20
|
refundDispositionsByOperationId?: Record<string, string>;
|
|
20
21
|
useAdminFeAuthoritativeQuote: boolean;
|
|
21
22
|
}
|
|
@@ -74,6 +75,7 @@ export function buildAdminChangeQuoteRequestKey(input: AdminChangeQuoteRequestKe
|
|
|
74
75
|
promoApplicationScope: input.promoCode ? (input.promoApplicationScope ?? 'CHANGE_ONLY') : null,
|
|
75
76
|
structuredAdjustments: input.structuredAdjustments ?? [],
|
|
76
77
|
reverseAdjustmentIds: [...(input.reverseAdjustmentIds ?? [])].sort(),
|
|
78
|
+
preserveExistingReceipt: input.preserveExistingReceipt === true,
|
|
77
79
|
refundDispositionsByOperationId: Object.fromEntries(
|
|
78
80
|
Object.entries(input.refundDispositionsByOperationId ?? {}).sort(([a], [b]) => a.localeCompare(b)),
|
|
79
81
|
),
|
|
@@ -32,6 +32,7 @@ export interface BuildAdminChangeV2QuoteRequestParams {
|
|
|
32
32
|
promoApplicationScope?: AdminPromoApplicationScope;
|
|
33
33
|
structuredAdjustments: NonNullable<ChangeBookingQuoteRequest['structuredAdjustments']>;
|
|
34
34
|
reverseAdjustmentIds?: string[];
|
|
35
|
+
preserveExistingReceipt?: boolean;
|
|
35
36
|
includeSelectableAdjustmentComponents?: boolean;
|
|
36
37
|
refundDispositionsByOperationId: Record<string, AdminAmendmentRefundDisposition>;
|
|
37
38
|
previousPassengerCount: number;
|
|
@@ -59,6 +60,7 @@ export function buildAdminChangeV2QuoteRequest({
|
|
|
59
60
|
promoApplicationScope = 'CHANGE_ONLY',
|
|
60
61
|
structuredAdjustments,
|
|
61
62
|
reverseAdjustmentIds = [],
|
|
63
|
+
preserveExistingReceipt = false,
|
|
62
64
|
includeSelectableAdjustmentComponents = false,
|
|
63
65
|
refundDispositionsByOperationId,
|
|
64
66
|
previousPassengerCount,
|
|
@@ -80,12 +82,13 @@ export function buildAdminChangeV2QuoteRequest({
|
|
|
80
82
|
cancellationPolicyId,
|
|
81
83
|
promoCode: promoCode?.trim() || null,
|
|
82
84
|
promoApplicationScope: promoCode?.trim() ? promoApplicationScope : null,
|
|
83
|
-
structuredAdjustments,
|
|
84
|
-
reverseAdjustmentIds,
|
|
85
|
+
structuredAdjustments: preserveExistingReceipt ? [] : structuredAdjustments,
|
|
86
|
+
reverseAdjustmentIds: preserveExistingReceipt ? [] : reverseAdjustmentIds,
|
|
87
|
+
preserveExistingReceipt,
|
|
85
88
|
...(includeSelectableAdjustmentComponents
|
|
86
89
|
? { includeSelectableAdjustmentComponents: true }
|
|
87
90
|
: {}),
|
|
88
|
-
...(Object.keys(refundDispositionsByOperationId).length > 0
|
|
91
|
+
...(!preserveExistingReceipt && Object.keys(refundDispositionsByOperationId).length > 0
|
|
89
92
|
? { refundDispositionsByOperationId }
|
|
90
93
|
: {}),
|
|
91
94
|
capacitySeatCredit: {
|
|
@@ -33,6 +33,8 @@ export type AdminAmendmentStructuredAdjustment = {
|
|
|
33
33
|
|
|
34
34
|
/** Payload passed to `onChangeBooking` when applying a dashboard-managed booking change. */
|
|
35
35
|
export type ProviderDashboardChangeBookingPayload = {
|
|
36
|
+
/** Apply operational changes while preserving the existing receipt and balance exactly. */
|
|
37
|
+
preserveExistingReceipt?: boolean;
|
|
36
38
|
/** Parent catalog product id. Present when admin changes the booking to a different product. */
|
|
37
39
|
parentProductId?: string;
|
|
38
40
|
/** Product option id selected for the new booking. */
|
|
@@ -155,6 +155,7 @@ export interface UseAdminChangeCheckoutControllerParams {
|
|
|
155
155
|
mergedProviderAdditionalAdjustments: AdminChangeProviderAdditionalAdjustment[];
|
|
156
156
|
adminStructuredAdjustments: NonNullable<ProviderDashboardChangeBookingPayload['structuredAdjustments']>;
|
|
157
157
|
adminReverseAdjustmentIds: string[];
|
|
158
|
+
preserveExistingReceipt: boolean;
|
|
158
159
|
refundDispositionsByOperationId: NonNullable<
|
|
159
160
|
ProviderDashboardChangeBookingPayload['refundDispositionsByOperationId']
|
|
160
161
|
>;
|
|
@@ -258,6 +259,7 @@ export function useAdminChangeCheckoutController({
|
|
|
258
259
|
mergedProviderAdditionalAdjustments,
|
|
259
260
|
adminStructuredAdjustments,
|
|
260
261
|
adminReverseAdjustmentIds,
|
|
262
|
+
preserveExistingReceipt,
|
|
261
263
|
refundDispositionsByOperationId,
|
|
262
264
|
providerApplyAuthoritativeReceipt,
|
|
263
265
|
onSuccess,
|
|
@@ -381,6 +383,7 @@ export function useAdminChangeCheckoutController({
|
|
|
381
383
|
mergedProviderAdditionalAdjustments,
|
|
382
384
|
adminStructuredAdjustments,
|
|
383
385
|
adminReverseAdjustmentIds,
|
|
386
|
+
preserveExistingReceipt,
|
|
384
387
|
refundDispositionsByOperationId,
|
|
385
388
|
pricingV2QuoteId: authoritativeQuote?.quoteId ?? null,
|
|
386
389
|
pricingV2QuotedTotal: authoritativeQuote?.quotedTotal ?? authoritativeQuote?.serverDisplay?.total ?? null,
|
|
@@ -80,6 +80,7 @@ export interface UseAdminChangeQuotePreviewParams {
|
|
|
80
80
|
adminCustomReceiptLines: AdminCustomReceiptLine[];
|
|
81
81
|
adminStructuredAdjustments: NonNullable<ProviderDashboardChangeBookingPayload['structuredAdjustments']>;
|
|
82
82
|
adminReverseAdjustmentIds: string[];
|
|
83
|
+
preserveExistingReceipt: boolean;
|
|
83
84
|
needsAdjustmentDiscovery: boolean;
|
|
84
85
|
refundDispositionsByOperationId: Record<string, AdminAmendmentRefundDisposition>;
|
|
85
86
|
useAdminFeAuthoritativeQuote: boolean;
|
|
@@ -144,6 +145,7 @@ export function useAdminChangeQuotePreview({
|
|
|
144
145
|
adminCustomReceiptLines,
|
|
145
146
|
adminStructuredAdjustments,
|
|
146
147
|
adminReverseAdjustmentIds,
|
|
148
|
+
preserveExistingReceipt,
|
|
147
149
|
needsAdjustmentDiscovery,
|
|
148
150
|
refundDispositionsByOperationId,
|
|
149
151
|
useAdminFeAuthoritativeQuote,
|
|
@@ -238,6 +240,7 @@ export function useAdminChangeQuotePreview({
|
|
|
238
240
|
promoApplicationScope,
|
|
239
241
|
structuredAdjustments: adminStructuredAdjustments,
|
|
240
242
|
reverseAdjustmentIds: adminReverseAdjustmentIds,
|
|
243
|
+
preserveExistingReceipt,
|
|
241
244
|
refundDispositionsByOperationId,
|
|
242
245
|
useAdminFeAuthoritativeQuote,
|
|
243
246
|
}), [
|
|
@@ -255,6 +258,7 @@ export function useAdminChangeQuotePreview({
|
|
|
255
258
|
promoApplicationScope,
|
|
256
259
|
adminStructuredAdjustments,
|
|
257
260
|
adminReverseAdjustmentIds,
|
|
261
|
+
preserveExistingReceipt,
|
|
258
262
|
refundDispositionsByOperationId,
|
|
259
263
|
useAdminFeAuthoritativeQuote,
|
|
260
264
|
]);
|
|
@@ -526,6 +530,7 @@ export function useAdminChangeQuotePreview({
|
|
|
526
530
|
promoApplicationScope,
|
|
527
531
|
structuredAdjustments: adminStructuredAdjustments,
|
|
528
532
|
reverseAdjustmentIds: adminReverseAdjustmentIds,
|
|
533
|
+
preserveExistingReceipt,
|
|
529
534
|
includeSelectableAdjustmentComponents:
|
|
530
535
|
needsAdjustmentDiscovery || adminCustomReceiptLines.some(
|
|
531
536
|
(line) => line.scope === 'SELECTED_COMPONENTS',
|
|
@@ -43,6 +43,10 @@ export function lineFromActiveAdjustment(
|
|
|
43
43
|
replacementAdjustmentId: string,
|
|
44
44
|
): AdminCustomReceiptLine {
|
|
45
45
|
const percentage = adjustment.mode !== 'FIXED_AMOUNT';
|
|
46
|
+
const overrideComponentId =
|
|
47
|
+
adjustment.reasonCode === 'LINE_ITEM_OVERRIDE' && adjustment.affectedComponentIds.length === 1
|
|
48
|
+
? adjustment.affectedComponentIds[0]
|
|
49
|
+
: undefined;
|
|
46
50
|
return {
|
|
47
51
|
id: `existing:${adjustment.adjustmentId}`,
|
|
48
52
|
label:
|
|
@@ -62,6 +66,7 @@ export function lineFromActiveAdjustment(
|
|
|
62
66
|
sourceAdjustmentId: adjustment.adjustmentId,
|
|
63
67
|
replacementAdjustmentId,
|
|
64
68
|
isDirty: false,
|
|
69
|
+
overrideComponentId,
|
|
65
70
|
};
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -160,6 +165,8 @@ export function useAdminCustomReceiptLines(
|
|
|
160
165
|
const existing = adminCustomReceiptLines.find(
|
|
161
166
|
(line) => line.overrideComponentId === input.componentId,
|
|
162
167
|
);
|
|
168
|
+
const sourceAdjustmentId = existing?.sourceAdjustmentId;
|
|
169
|
+
const restoringQuotedLine = Math.abs(delta) < 0.005;
|
|
163
170
|
const next = Math.abs(delta) < 0.005
|
|
164
171
|
? adminCustomReceiptLines.filter((line) => line.id !== existing?.id)
|
|
165
172
|
: [
|
|
@@ -177,8 +184,22 @@ export function useAdminCustomReceiptLines(
|
|
|
177
184
|
overrideComponentId: input.componentId,
|
|
178
185
|
overrideBaseAmount: input.baseAmount,
|
|
179
186
|
overrideLineKey: input.lineKey,
|
|
187
|
+
...(existing?.sourceAdjustmentId
|
|
188
|
+
? {
|
|
189
|
+
sourceAdjustmentId: existing.sourceAdjustmentId,
|
|
190
|
+
replacementAdjustmentId: existing.replacementAdjustmentId,
|
|
191
|
+
isDirty: true,
|
|
192
|
+
}
|
|
193
|
+
: {}),
|
|
180
194
|
},
|
|
181
195
|
];
|
|
196
|
+
if (sourceAdjustmentId) {
|
|
197
|
+
const updateRemovedIds = (current: string[]) => restoringQuotedLine
|
|
198
|
+
? Array.from(new Set([...current, sourceAdjustmentId]))
|
|
199
|
+
: current.filter((id) => id !== sourceAdjustmentId);
|
|
200
|
+
setRemovedExistingAdjustmentIds(updateRemovedIds);
|
|
201
|
+
setAppliedRemovedExistingAdjustmentIds(updateRemovedIds);
|
|
202
|
+
}
|
|
182
203
|
setAdminCustomReceiptLines(next);
|
|
183
204
|
setAppliedAdminCustomReceiptLines(next);
|
|
184
205
|
}, [adminCustomReceiptLines, nextDraftId]);
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -1475,6 +1475,8 @@ export interface ChangeBookingQuoteRequest {
|
|
|
1475
1475
|
}>;
|
|
1476
1476
|
/** Existing immutable adjustment ids to reverse as part of this amendment. */
|
|
1477
1477
|
reverseAdjustmentIds?: string[];
|
|
1478
|
+
/** Admin only: change operations without changing the receipt or outstanding balance. */
|
|
1479
|
+
preserveExistingReceipt?: boolean;
|
|
1478
1480
|
}
|
|
1479
1481
|
|
|
1480
1482
|
/** FE-authored receipt payload for admin quote path (major units, booking currency). */
|
|
@@ -234,6 +234,31 @@ test('existing admin adjustment hydrates for immutable reverse-and-replace editi
|
|
|
234
234
|
assert.deepEqual(reverseAdjustmentIdsForAdminCustomLines([], ['admin-rcpt-1']), ['admin-rcpt-1']);
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
test('saved line-item override hydrates its targeted receipt component', () => {
|
|
238
|
+
const line = lineFromActiveAdjustment({
|
|
239
|
+
adjustmentId: 'override-adult-1',
|
|
240
|
+
operationId: 'op-override-adult-1',
|
|
241
|
+
mode: 'FIXED_AMOUNT',
|
|
242
|
+
scope: 'SELECTED_COMPONENTS',
|
|
243
|
+
basisCents: 19_799,
|
|
244
|
+
amountCents: 19_799,
|
|
245
|
+
taxDeltaCents: 990,
|
|
246
|
+
affectedComponentIds: ['adult-component'],
|
|
247
|
+
taxBehavior: 'TAXABLE',
|
|
248
|
+
reasonCode: 'LINE_ITEM_OVERRIDE',
|
|
249
|
+
reason: 'Override · Refund · ADULT',
|
|
250
|
+
createdAt: '2026-08-09T00:00:00Z',
|
|
251
|
+
}, 'replacement-override-adult-1');
|
|
252
|
+
|
|
253
|
+
assert.equal(line.overrideComponentId, 'adult-component');
|
|
254
|
+
assert.deepEqual(line.selectedComponentIds, ['adult-component']);
|
|
255
|
+
assert.equal(line.sourceAdjustmentId, 'override-adult-1');
|
|
256
|
+
assert.deepEqual(
|
|
257
|
+
reverseAdjustmentIdsForAdminCustomLines([{ ...line, isDirty: true }]),
|
|
258
|
+
['override-adult-1'],
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
237
262
|
test('private resource quantity is not carried into a regular-tour conversion', () => {
|
|
238
263
|
const quantities = deriveCompatibleInitialBookingQuantities(
|
|
239
264
|
[{ category: 'RESOURCE', count: 1 }],
|
|
@@ -783,6 +808,38 @@ test('admin preview and checkout share the Pricing V2 request shape', () => {
|
|
|
783
808
|
assert.equal('clientProposedTotal' in request, false);
|
|
784
809
|
});
|
|
785
810
|
|
|
811
|
+
test('preserve-receipt quote is a first-class backend mode and excludes price edits', () => {
|
|
812
|
+
const request = buildAdminChangeV2QuoteRequest({
|
|
813
|
+
bookingReference: 'VFSCAEKQ',
|
|
814
|
+
lastName: 'Tauro',
|
|
815
|
+
parentProductId: 'product_1',
|
|
816
|
+
optionId: 'option_2',
|
|
817
|
+
selectedAvailability: {
|
|
818
|
+
availabilityId: 'availability_2',
|
|
819
|
+
productId: 'option_2',
|
|
820
|
+
dateTime: '2026-07-29T08:35:00-06:00',
|
|
821
|
+
vacancies: 10,
|
|
822
|
+
} as Parameters<typeof buildAdminChangeV2QuoteRequest>[0]['selectedAvailability'],
|
|
823
|
+
pickupLocationId: 'pickup_1',
|
|
824
|
+
returnAvailabilityId: null,
|
|
825
|
+
bookingItems: [{ category: 'ADULT', count: 2 }],
|
|
826
|
+
addOnSelections: [],
|
|
827
|
+
cancellationPolicyId: 'policy_1',
|
|
828
|
+
structuredAdjustments: [{ adjustmentId: 'ignored_draft' }],
|
|
829
|
+
reverseAdjustmentIds: ['ignored_reversal'],
|
|
830
|
+
preserveExistingReceipt: true,
|
|
831
|
+
refundDispositionsByOperationId: { ignored_removal: 'PENDING_REFUND' },
|
|
832
|
+
previousPassengerCount: 2,
|
|
833
|
+
previousAvailabilityId: 'availability_1',
|
|
834
|
+
previousReturnAvailabilityId: null,
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
assert.equal(request.preserveExistingReceipt, true);
|
|
838
|
+
assert.deepEqual(request.structuredAdjustments, []);
|
|
839
|
+
assert.deepEqual(request.reverseAdjustmentIds, []);
|
|
840
|
+
assert.equal(request.refundDispositionsByOperationId, undefined);
|
|
841
|
+
});
|
|
842
|
+
|
|
786
843
|
test('admin Pricing V2 quote mapper preserves the server quote identity', async () => {
|
|
787
844
|
const request = buildAdminChangeV2QuoteRequest({
|
|
788
845
|
bookingReference: 'VFSCAEKQ',
|
|
@@ -909,6 +966,49 @@ test('admin no-charge provider payload carries the selected pickup itinerary', (
|
|
|
909
966
|
assert.equal(payload?.promoApplicationScope, null);
|
|
910
967
|
});
|
|
911
968
|
|
|
969
|
+
test('preserve-receipt provider payload carries the mode without price mutations', () => {
|
|
970
|
+
const payload = buildAdminChangeProviderPayload({
|
|
971
|
+
selectedAvailability: {
|
|
972
|
+
availabilityId: 'availability_2',
|
|
973
|
+
productId: 'option_2',
|
|
974
|
+
dateTime: '2026-07-29T08:35:00-06:00',
|
|
975
|
+
vacancies: 10,
|
|
976
|
+
} as Parameters<typeof buildAdminChangeProviderPayload>[0]['selectedAvailability'],
|
|
977
|
+
availabilityProductOptionId: 'option_2',
|
|
978
|
+
bookingItems: [{ category: 'ADULT', count: 2 }],
|
|
979
|
+
product: { productId: 'product_1', name: 'Changed tour' } as Parameters<
|
|
980
|
+
typeof buildAdminChangeProviderPayload
|
|
981
|
+
>[0]['product'],
|
|
982
|
+
pickupLocationId: null,
|
|
983
|
+
selectedReturnOption: null,
|
|
984
|
+
addOnSelections: [],
|
|
985
|
+
cancellationPolicyId: null,
|
|
986
|
+
appliedPromoCode: null,
|
|
987
|
+
newTotalAmount: 999,
|
|
988
|
+
providerPricingOverrides: [{ lineKey: 'ticket', amount: 999 }],
|
|
989
|
+
mergedProviderAdditionalAdjustments: [{ label: 'Draft fee', amount: 10 }],
|
|
990
|
+
adminStructuredAdjustments: [{
|
|
991
|
+
adjustmentId: 'draft',
|
|
992
|
+
mode: 'FIXED_AMOUNT',
|
|
993
|
+
fixedAmount: 10,
|
|
994
|
+
scope: 'CURRENT_ACTIVE_BOOKING',
|
|
995
|
+
taxBehavior: 'NON_TAXABLE',
|
|
996
|
+
reasonCode: 'STAFF_ADJUSTMENT',
|
|
997
|
+
}],
|
|
998
|
+
adminReverseAdjustmentIds: ['old_adjustment'],
|
|
999
|
+
preserveExistingReceipt: true,
|
|
1000
|
+
refundDispositionsByOperationId: { removal: 'PENDING_REFUND' },
|
|
1001
|
+
previousPassengerCount: 2,
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
assert.equal(payload?.preserveExistingReceipt, true);
|
|
1005
|
+
assert.equal(payload?.newTotalAmount, undefined);
|
|
1006
|
+
assert.equal(payload?.pricingAdjustment, undefined);
|
|
1007
|
+
assert.deepEqual(payload?.structuredAdjustments, []);
|
|
1008
|
+
assert.deepEqual(payload?.reverseAdjustmentIds, []);
|
|
1009
|
+
assert.deepEqual(payload?.refundDispositionsByOperationId, {});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
912
1012
|
test('public paid change preview shows amendment-only charges before combined tax', () => {
|
|
913
1013
|
const summary = buildPublicChangeAmountDueSummary([
|
|
914
1014
|
{
|