@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
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AdminPromoApplicationScope,
|
|
3
|
+
Availability,
|
|
4
|
+
Product,
|
|
5
|
+
ReturnOption,
|
|
6
|
+
} from '../../lib/booking-api';
|
|
2
7
|
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
3
8
|
|
|
4
9
|
export type AdminChangeBookingItem = { category: string; count: number };
|
|
@@ -29,6 +34,7 @@ export interface BuildAdminChangeProviderPayloadParams {
|
|
|
29
34
|
cancellationPolicyId: string | null;
|
|
30
35
|
initialCancellationPolicyId?: string | null;
|
|
31
36
|
appliedPromoCode: string | null;
|
|
37
|
+
promoApplicationScope?: AdminPromoApplicationScope;
|
|
32
38
|
newTotalAmount: number;
|
|
33
39
|
providerPricingOverrides: AdminChangeProviderLineOverride[];
|
|
34
40
|
mergedProviderAdditionalAdjustments: AdminChangeProviderAdditionalAdjustment[];
|
|
@@ -56,6 +62,7 @@ export function buildAdminChangeProviderPayload({
|
|
|
56
62
|
cancellationPolicyId,
|
|
57
63
|
initialCancellationPolicyId,
|
|
58
64
|
appliedPromoCode,
|
|
65
|
+
promoApplicationScope = 'CHANGE_ONLY',
|
|
59
66
|
newTotalAmount,
|
|
60
67
|
providerPricingOverrides,
|
|
61
68
|
mergedProviderAdditionalAdjustments,
|
|
@@ -92,6 +99,7 @@ export function buildAdminChangeProviderPayload({
|
|
|
92
99
|
addOnSelections,
|
|
93
100
|
cancellationPolicyId: cancellationPolicyId ?? initialCancellationPolicyId ?? null,
|
|
94
101
|
promoCode: appliedPromoCode ?? null,
|
|
102
|
+
promoApplicationScope: appliedPromoCode ? promoApplicationScope : null,
|
|
95
103
|
newTotalAmount,
|
|
96
104
|
additionalHoursCount: null,
|
|
97
105
|
pricingAdjustment:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Availability } from '../../lib/booking-api';
|
|
1
|
+
import type { AdminPromoApplicationScope, Availability } from '../../lib/booking-api';
|
|
2
2
|
import { getAvailabilityOptionId } from './change-booking-flow-helpers';
|
|
3
3
|
|
|
4
4
|
interface AdminChangeQuoteRequestKeyInput {
|
|
@@ -12,6 +12,8 @@ interface AdminChangeQuoteRequestKeyInput {
|
|
|
12
12
|
addOnSelections: Array<{ addOnId: string; variantId?: string; quantity?: number }>;
|
|
13
13
|
adminCustomReceiptLines: unknown[];
|
|
14
14
|
cancellationPolicyId?: string | null;
|
|
15
|
+
promoCode?: string | null;
|
|
16
|
+
promoApplicationScope?: AdminPromoApplicationScope | null;
|
|
15
17
|
structuredAdjustments?: unknown[];
|
|
16
18
|
refundDispositionsByOperationId?: Record<string, string>;
|
|
17
19
|
useAdminFeAuthoritativeQuote: boolean;
|
|
@@ -36,6 +38,8 @@ export function buildAdminChangeQuoteRequestKey(input: AdminChangeQuoteRequestKe
|
|
|
36
38
|
addOnSelections: input.addOnSelections,
|
|
37
39
|
adminCustomReceiptLines: input.adminCustomReceiptLines,
|
|
38
40
|
cancellationPolicyId: input.cancellationPolicyId ?? null,
|
|
41
|
+
promoCode: input.promoCode?.trim().toUpperCase() ?? null,
|
|
42
|
+
promoApplicationScope: input.promoCode ? (input.promoApplicationScope ?? 'CHANGE_ONLY') : null,
|
|
39
43
|
structuredAdjustments: input.structuredAdjustments ?? [],
|
|
40
44
|
refundDispositionsByOperationId: Object.fromEntries(
|
|
41
45
|
Object.entries(input.refundDispositionsByOperationId ?? {}).sort(([a], [b]) => a.localeCompare(b)),
|
|
@@ -43,3 +47,12 @@ export function buildAdminChangeQuoteRequestKey(input: AdminChangeQuoteRequestKe
|
|
|
43
47
|
useAdminFeAuthoritativeQuote: input.useAdminFeAuthoritativeQuote,
|
|
44
48
|
});
|
|
45
49
|
}
|
|
50
|
+
|
|
51
|
+
/** A completed request is reusable only while its quote is still published in UI state. */
|
|
52
|
+
export function canReuseCompletedAdminChangeQuote(
|
|
53
|
+
inputKey: string,
|
|
54
|
+
lastCompletedInputKey: string | null,
|
|
55
|
+
hasPublishedQuote: boolean,
|
|
56
|
+
): boolean {
|
|
57
|
+
return Boolean(inputKey) && lastCompletedInputKey === inputKey && hasPublishedQuote;
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PriceSummaryLine } from './PriceSummary';
|
|
2
|
+
|
|
3
|
+
function isSelectedPromoLine(line: PriceSummaryLine, promoCode: string): boolean {
|
|
4
|
+
if (line.kind !== 'line') return false;
|
|
5
|
+
const type = String(line.type ?? '').trim().toUpperCase();
|
|
6
|
+
if (type === 'PROMO_CODE') return true;
|
|
7
|
+
if (type !== 'DISCOUNT') return false;
|
|
8
|
+
|
|
9
|
+
const label = line.label.trim().toUpperCase();
|
|
10
|
+
return label.startsWith('DISCOUNT ·') ||
|
|
11
|
+
label.startsWith('PROMO ') ||
|
|
12
|
+
label.includes(promoCode);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Keep component-level promo allocations in the server quote/ledger while presenting
|
|
17
|
+
* the selected promo as one customer-facing row in the admin amendment summary.
|
|
18
|
+
*/
|
|
19
|
+
export function collapseAdminPromoLines(
|
|
20
|
+
lines: PriceSummaryLine[],
|
|
21
|
+
promoCode: string | null | undefined,
|
|
22
|
+
): PriceSummaryLine[] {
|
|
23
|
+
const normalizedCode = promoCode?.trim().toUpperCase() ?? '';
|
|
24
|
+
if (!normalizedCode) return lines;
|
|
25
|
+
|
|
26
|
+
const promoIndexes = lines
|
|
27
|
+
.map((line, index) => isSelectedPromoLine(line, normalizedCode) ? index : -1)
|
|
28
|
+
.filter((index) => index >= 0);
|
|
29
|
+
if (promoIndexes.length === 0) return lines;
|
|
30
|
+
|
|
31
|
+
const promoIndexSet = new Set(promoIndexes);
|
|
32
|
+
const firstPromoIndex = promoIndexes[0];
|
|
33
|
+
const promoAmount = Math.round(promoIndexes.reduce((sum, index) => {
|
|
34
|
+
const line = lines[index];
|
|
35
|
+
return sum + (line.kind === 'line' ? line.amount : 0);
|
|
36
|
+
}, 0) * 100) / 100;
|
|
37
|
+
|
|
38
|
+
return lines.flatMap((line, index) => {
|
|
39
|
+
if (index === firstPromoIndex) {
|
|
40
|
+
return [{
|
|
41
|
+
kind: 'line' as const,
|
|
42
|
+
label: `Promo: ${normalizedCode}`,
|
|
43
|
+
amount: promoAmount,
|
|
44
|
+
type: 'PROMO_CODE',
|
|
45
|
+
}];
|
|
46
|
+
}
|
|
47
|
+
return promoIndexSet.has(index) ? [] : [line];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AdminAmendmentRefundDisposition,
|
|
3
|
+
AdminPromoApplicationScope,
|
|
4
|
+
Availability,
|
|
5
|
+
ChangeBookingQuoteRequest,
|
|
6
|
+
ChangeBookingQuoteResponse,
|
|
7
|
+
} from '../../lib/booking-api';
|
|
8
|
+
import { quoteAdminChangeBookingV2 } from '../../lib/booking-api';
|
|
9
|
+
import type { Currency } from './CurrencySwitcher';
|
|
10
|
+
import {
|
|
11
|
+
sliceAndMergeChangeQuoteForUi,
|
|
12
|
+
type ChangeBookingMergedQuoteState,
|
|
13
|
+
} from './change-booking-quote-state';
|
|
14
|
+
|
|
15
|
+
export type AdminChangeV2QuoteRequest = ChangeBookingQuoteRequest & {
|
|
16
|
+
cancellationPolicyId?: string | null;
|
|
17
|
+
structuredAdjustments?: unknown[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface BuildAdminChangeV2QuoteRequestParams {
|
|
21
|
+
bookingReference: string;
|
|
22
|
+
lastName: string;
|
|
23
|
+
parentProductId: string;
|
|
24
|
+
optionId: string;
|
|
25
|
+
selectedAvailability: Availability;
|
|
26
|
+
pickupLocationId: string | null;
|
|
27
|
+
returnAvailabilityId: string | null;
|
|
28
|
+
bookingItems: Array<{ category: string; count: number }>;
|
|
29
|
+
addOnSelections: Array<{ addOnId: string; variantId?: string; quantity?: number }>;
|
|
30
|
+
cancellationPolicyId: string | null;
|
|
31
|
+
promoCode?: string | null;
|
|
32
|
+
promoApplicationScope?: AdminPromoApplicationScope;
|
|
33
|
+
structuredAdjustments: unknown[];
|
|
34
|
+
refundDispositionsByOperationId: Record<string, AdminAmendmentRefundDisposition>;
|
|
35
|
+
previousPassengerCount: number;
|
|
36
|
+
previousAvailabilityId: string | null;
|
|
37
|
+
previousReturnAvailabilityId: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Canonical server-authoritative provider/admin quote request.
|
|
42
|
+
* Preview and checkout must use the same endpoint and request shape so checkout cannot
|
|
43
|
+
* replace a valid Pricing V2 quote with a legacy response that has no quote id.
|
|
44
|
+
*/
|
|
45
|
+
export function buildAdminChangeV2QuoteRequest({
|
|
46
|
+
bookingReference,
|
|
47
|
+
lastName,
|
|
48
|
+
parentProductId,
|
|
49
|
+
optionId,
|
|
50
|
+
selectedAvailability,
|
|
51
|
+
pickupLocationId,
|
|
52
|
+
returnAvailabilityId,
|
|
53
|
+
bookingItems,
|
|
54
|
+
addOnSelections,
|
|
55
|
+
cancellationPolicyId,
|
|
56
|
+
promoCode,
|
|
57
|
+
promoApplicationScope = 'CHANGE_ONLY',
|
|
58
|
+
structuredAdjustments,
|
|
59
|
+
refundDispositionsByOperationId,
|
|
60
|
+
previousPassengerCount,
|
|
61
|
+
previousAvailabilityId,
|
|
62
|
+
previousReturnAvailabilityId,
|
|
63
|
+
}: BuildAdminChangeV2QuoteRequestParams): AdminChangeV2QuoteRequest {
|
|
64
|
+
return {
|
|
65
|
+
bookingReference: bookingReference.trim(),
|
|
66
|
+
lastName: lastName.trim(),
|
|
67
|
+
newProductId: optionId,
|
|
68
|
+
newParentProductId: parentProductId,
|
|
69
|
+
newDateTime: selectedAvailability.dateTime,
|
|
70
|
+
newAvailabilityId: selectedAvailability.availabilityId || null,
|
|
71
|
+
newPickupLocationId: pickupLocationId || null,
|
|
72
|
+
newReturnAvailabilityId: returnAvailabilityId,
|
|
73
|
+
newPassengerCounts: bookingItems,
|
|
74
|
+
// Admin owns the complete target selection. [] intentionally clears all add-ons.
|
|
75
|
+
newAddOnSelections: addOnSelections,
|
|
76
|
+
cancellationPolicyId,
|
|
77
|
+
promoCode: promoCode?.trim() || null,
|
|
78
|
+
promoApplicationScope: promoCode?.trim() ? promoApplicationScope : null,
|
|
79
|
+
structuredAdjustments,
|
|
80
|
+
...(Object.keys(refundDispositionsByOperationId).length > 0
|
|
81
|
+
? { refundDispositionsByOperationId }
|
|
82
|
+
: {}),
|
|
83
|
+
capacitySeatCredit: {
|
|
84
|
+
enabled: true,
|
|
85
|
+
previousPassengerCount,
|
|
86
|
+
previousAvailabilityId,
|
|
87
|
+
previousReturnAvailabilityId,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function quoteAdminChangeV2ForUi(
|
|
93
|
+
request: AdminChangeV2QuoteRequest,
|
|
94
|
+
fallbackCart: { total: number; subtotal: number; tax: number },
|
|
95
|
+
currency: Currency,
|
|
96
|
+
quoteFn: (request: ChangeBookingQuoteRequest) => Promise<ChangeBookingQuoteResponse> =
|
|
97
|
+
quoteAdminChangeBookingV2,
|
|
98
|
+
): Promise<ChangeBookingMergedQuoteState> {
|
|
99
|
+
const quote = await quoteFn(request);
|
|
100
|
+
return sliceAndMergeChangeQuoteForUi(quote, fallbackCart, currency).mergedQuote;
|
|
101
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AdminAmendmentOperationSnapshot,
|
|
3
|
+
AdminAmendmentRefundDisposition,
|
|
4
|
+
} from '../../lib/booking-api';
|
|
5
|
+
|
|
6
|
+
export interface AdminRefundDecisionContext {
|
|
7
|
+
selectionKey: string;
|
|
8
|
+
operations: AdminAmendmentOperationSnapshot[];
|
|
9
|
+
allowedByOperationId: Record<string, AdminAmendmentRefundDisposition[]>;
|
|
10
|
+
removedValueByOperationId: Record<string, number>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface MergeAdminRefundDecisionContextParams {
|
|
14
|
+
current: AdminRefundDecisionContext | null;
|
|
15
|
+
selectionKey: string;
|
|
16
|
+
operations: AdminAmendmentOperationSnapshot[];
|
|
17
|
+
allowedByOperationId: Record<string, AdminAmendmentRefundDisposition[]>;
|
|
18
|
+
removedValueByOperationId: Record<string, number>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Refund-decision quote responses are incremental: after one operation is answered,
|
|
23
|
+
* the server can return only the operations that still need an answer. Keep all of
|
|
24
|
+
* the operations for the current booking selection so an answered operation does
|
|
25
|
+
* not disappear from the next request and get requested again.
|
|
26
|
+
*/
|
|
27
|
+
export function mergeAdminRefundDecisionContext({
|
|
28
|
+
current,
|
|
29
|
+
selectionKey,
|
|
30
|
+
operations,
|
|
31
|
+
allowedByOperationId,
|
|
32
|
+
removedValueByOperationId,
|
|
33
|
+
}: MergeAdminRefundDecisionContextParams): AdminRefundDecisionContext | null {
|
|
34
|
+
const sameSelection = current?.selectionKey === selectionKey ? current : null;
|
|
35
|
+
if (!sameSelection && operations.length === 0) return null;
|
|
36
|
+
|
|
37
|
+
const operationsById = new Map<string, AdminAmendmentOperationSnapshot>();
|
|
38
|
+
for (const operation of sameSelection?.operations ?? []) {
|
|
39
|
+
operationsById.set(operation.operationId, operation);
|
|
40
|
+
}
|
|
41
|
+
for (const operation of operations) {
|
|
42
|
+
operationsById.set(operation.operationId, operation);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const relevantOperationIds = new Set(operationsById.keys());
|
|
46
|
+
const mergedAllowed = {
|
|
47
|
+
...(sameSelection?.allowedByOperationId ?? {}),
|
|
48
|
+
...allowedByOperationId,
|
|
49
|
+
};
|
|
50
|
+
const mergedRemovedValues = {
|
|
51
|
+
...(sameSelection?.removedValueByOperationId ?? {}),
|
|
52
|
+
...removedValueByOperationId,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
selectionKey,
|
|
57
|
+
operations: [...operationsById.values()],
|
|
58
|
+
allowedByOperationId: Object.fromEntries(
|
|
59
|
+
Object.entries(mergedAllowed).filter(([operationId]) =>
|
|
60
|
+
relevantOperationIds.has(operationId),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
removedValueByOperationId: Object.fromEntries(
|
|
64
|
+
Object.entries(mergedRemovedValues).filter(([operationId]) =>
|
|
65
|
+
relevantOperationIds.has(operationId),
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function selectRefundDispositionsForOperations(
|
|
72
|
+
context: AdminRefundDecisionContext | null,
|
|
73
|
+
additionalOperations: AdminAmendmentOperationSnapshot[],
|
|
74
|
+
dispositionsByOperationId: Record<string, AdminAmendmentRefundDisposition>,
|
|
75
|
+
): Record<string, AdminAmendmentRefundDisposition> {
|
|
76
|
+
const relevantOperationIds = new Set([
|
|
77
|
+
...(context?.operations ?? []).map((operation) => operation.operationId),
|
|
78
|
+
...additionalOperations.map((operation) => operation.operationId),
|
|
79
|
+
]);
|
|
80
|
+
return Object.fromEntries(
|
|
81
|
+
Object.entries(dispositionsByOperationId).filter(([operationId]) =>
|
|
82
|
+
relevantOperationIds.has(operationId),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Availability } from '../../lib/booking-api';
|
|
2
|
+
|
|
3
|
+
export function findFirstDateWithBookableAvailability(
|
|
4
|
+
dates: string[],
|
|
5
|
+
getRowsForDate: (date: string) => Availability[],
|
|
6
|
+
isBookable: (availability: Availability) => boolean,
|
|
7
|
+
): string | undefined {
|
|
8
|
+
return dates.find((date) => getRowsForDate(date).some(isBookable));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function selectedDateHasVisibleAvailability(
|
|
12
|
+
selectedDate: string,
|
|
13
|
+
getRowsForDate: (date: string) => Availability[],
|
|
14
|
+
): boolean {
|
|
15
|
+
return selectedDate !== '' && getRowsForDate(selectedDate).length > 0;
|
|
16
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Currency } from './CurrencySwitcher';
|
|
2
|
-
import type { ItineraryDisplayStep } from '../../lib/booking-api';
|
|
2
|
+
import type { AdminPromoApplicationScope, ItineraryDisplayStep } from '../../lib/booking-api';
|
|
3
3
|
|
|
4
4
|
export type AdminAmendmentAdjustmentMode =
|
|
5
5
|
| 'FIXED_AMOUNT'
|
|
@@ -52,6 +52,7 @@ export type ProviderDashboardChangeBookingPayload = {
|
|
|
52
52
|
addOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }> | null;
|
|
53
53
|
cancellationPolicyId?: string | null;
|
|
54
54
|
promoCode?: string | null;
|
|
55
|
+
promoApplicationScope?: AdminPromoApplicationScope | null;
|
|
55
56
|
newTotalAmount?: number;
|
|
56
57
|
additionalHoursCount?: number | null;
|
|
57
58
|
pricingAdjustment?: {
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
type Product,
|
|
19
19
|
type ReturnOption,
|
|
20
20
|
} from '../../lib/booking-api';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
filterAvailabilitiesAfterCurrentTime,
|
|
23
|
+
filterAvailabilitiesAfterPublicBookingCutoff,
|
|
24
|
+
} from '../../lib/booking/booking-cutoffs';
|
|
22
25
|
import { shouldReplacePricingConfig } from '../../lib/booking/pricing-config';
|
|
23
26
|
import { useAvailabilitiesCache, buildAvailabilitiesCacheKey } from '../../contexts/AvailabilitiesCacheContext';
|
|
24
27
|
import {
|
|
@@ -628,7 +631,7 @@ export function useStandardBookingAvailability({
|
|
|
628
631
|
});
|
|
629
632
|
|
|
630
633
|
return isAdmin
|
|
631
|
-
? currentOrFutureAvailabilities
|
|
634
|
+
? filterAvailabilitiesAfterCurrentTime(currentOrFutureAvailabilities, bookingCutoffNow)
|
|
632
635
|
: filterAvailabilitiesAfterPublicBookingCutoff(
|
|
633
636
|
currentOrFutureAvailabilities,
|
|
634
637
|
bookingCutoffNow,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type AdminAmendmentOperationSnapshot,
|
|
14
14
|
type AdminAmendmentRefundDisposition,
|
|
15
15
|
type AdminFeAuthoritativeReceipt,
|
|
16
|
+
type AdminPromoApplicationScope,
|
|
16
17
|
type Availability,
|
|
17
18
|
type ChangeBookingQuotePricingDriftDetail,
|
|
18
19
|
type ChangeBookingQuoteTicketPricingTrace,
|
|
@@ -67,6 +68,10 @@ import {
|
|
|
67
68
|
confirmFreeAdminCustomerChange,
|
|
68
69
|
quoteAdminCustomerChangeForCheckout,
|
|
69
70
|
} from './admin-change-customer-quote-runner';
|
|
71
|
+
import {
|
|
72
|
+
buildAdminChangeV2QuoteRequest,
|
|
73
|
+
quoteAdminChangeV2ForUi,
|
|
74
|
+
} from './admin-change-v2-quote-request';
|
|
70
75
|
import {
|
|
71
76
|
reservationHoldHasExpired,
|
|
72
77
|
RESERVATION_HOLD_EXPIRED_MESSAGE,
|
|
@@ -136,6 +141,7 @@ export interface UseAdminChangeCheckoutControllerParams {
|
|
|
136
141
|
onChangeBooking?: ChangeBookingFlowProps['onChangeBooking'];
|
|
137
142
|
cancellationPolicyId: string | null;
|
|
138
143
|
appliedPromoCode: string | null;
|
|
144
|
+
promoApplicationScope: AdminPromoApplicationScope;
|
|
139
145
|
displayChangeFlowProposedTotalWithEditableLines: number;
|
|
140
146
|
providerPricingOverrides: AdminChangeProviderLineOverride[];
|
|
141
147
|
mergedProviderAdditionalAdjustments: AdminChangeProviderAdditionalAdjustment[];
|
|
@@ -237,6 +243,7 @@ export function useAdminChangeCheckoutController({
|
|
|
237
243
|
onChangeBooking,
|
|
238
244
|
cancellationPolicyId,
|
|
239
245
|
appliedPromoCode,
|
|
246
|
+
promoApplicationScope,
|
|
240
247
|
displayChangeFlowProposedTotalWithEditableLines,
|
|
241
248
|
providerPricingOverrides,
|
|
242
249
|
mergedProviderAdditionalAdjustments,
|
|
@@ -358,6 +365,7 @@ export function useAdminChangeCheckoutController({
|
|
|
358
365
|
cancellationPolicyId,
|
|
359
366
|
initialCancellationPolicyId: initialValues?.cancellationPolicyId ?? null,
|
|
360
367
|
appliedPromoCode,
|
|
368
|
+
promoApplicationScope,
|
|
361
369
|
newTotalAmount: displayChangeFlowProposedTotalWithEditableLines,
|
|
362
370
|
providerPricingOverrides,
|
|
363
371
|
mergedProviderAdditionalAdjustments,
|
|
@@ -461,7 +469,38 @@ export function useAdminChangeCheckoutController({
|
|
|
461
469
|
let confirmedChangeAmountDueForCheckout: number | null = null;
|
|
462
470
|
let authoritativeChangeQuoteForCheckout = latestChangeQuote;
|
|
463
471
|
|
|
464
|
-
if (
|
|
472
|
+
if (isProviderDashboardChange && isChangeBookingContext) {
|
|
473
|
+
const bookingReference = initialValues?.bookingReference?.trim();
|
|
474
|
+
if (!bookingReference) throw new Error('Missing booking reference.');
|
|
475
|
+
authoritativeChangeQuoteForCheckout = await quoteAdminChangeV2ForUi(
|
|
476
|
+
buildAdminChangeV2QuoteRequest({
|
|
477
|
+
bookingReference,
|
|
478
|
+
lastName,
|
|
479
|
+
parentProductId: product.productId,
|
|
480
|
+
optionId: availabilityProductOptionId,
|
|
481
|
+
selectedAvailability,
|
|
482
|
+
pickupLocationId,
|
|
483
|
+
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
484
|
+
bookingItems,
|
|
485
|
+
addOnSelections,
|
|
486
|
+
cancellationPolicyId,
|
|
487
|
+
promoCode: appliedPromoCode,
|
|
488
|
+
promoApplicationScope,
|
|
489
|
+
structuredAdjustments: adminStructuredAdjustments,
|
|
490
|
+
refundDispositionsByOperationId,
|
|
491
|
+
previousPassengerCount: changeFlowInitialTicketCount,
|
|
492
|
+
previousAvailabilityId: initialValues?.availabilityId ?? null,
|
|
493
|
+
previousReturnAvailabilityId: initialValues?.returnAvailabilityId ?? null,
|
|
494
|
+
}),
|
|
495
|
+
{
|
|
496
|
+
total: changeFlowNewBookingTotal,
|
|
497
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
498
|
+
tax: effectiveTax,
|
|
499
|
+
},
|
|
500
|
+
currency,
|
|
501
|
+
);
|
|
502
|
+
setLatestChangeQuote(authoritativeChangeQuoteForCheckout);
|
|
503
|
+
} else if (isCustomerSelfServeChange) {
|
|
465
504
|
const quoteResult = await quoteAdminCustomerChangeForCheckout({
|
|
466
505
|
bookingReference: initialValues?.bookingReference,
|
|
467
506
|
lastName,
|
|
@@ -326,9 +326,9 @@ export function useAdminChangeQuoteDisplayState({
|
|
|
326
326
|
productChanged: selectedParentProductChanged,
|
|
327
327
|
});
|
|
328
328
|
const hasChangeSelection =
|
|
329
|
-
isProviderDashboardChange
|
|
329
|
+
(isProviderDashboardChange
|
|
330
330
|
? changeSelectionDetails.hasOperationalChangesFromInitial
|
|
331
|
-
: changeSelectionDetails.hasChangesFromInitial;
|
|
331
|
+
: changeSelectionDetails.hasChangesFromInitial) || Boolean(appliedPromoCode);
|
|
332
332
|
|
|
333
333
|
const {
|
|
334
334
|
providerTotalsPreview,
|
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
} from 'react';
|
|
9
9
|
import {
|
|
10
10
|
quoteChangeBooking,
|
|
11
|
-
quoteAdminChangeBookingV2,
|
|
12
11
|
type AdminAmendmentRefundDisposition,
|
|
12
|
+
type AdminPromoApplicationScope,
|
|
13
13
|
type AdminFeAuthoritativeReceipt,
|
|
14
14
|
type Availability,
|
|
15
15
|
type ItineraryDisplayStep,
|
|
@@ -27,7 +27,14 @@ import {
|
|
|
27
27
|
} from './change-booking-flow-helpers';
|
|
28
28
|
import type { Currency } from './CurrencySwitcher';
|
|
29
29
|
import type { AdminChangeLatestQuote } from './useAdminChangeCheckoutController';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
buildAdminChangeQuoteRequestKey,
|
|
32
|
+
canReuseCompletedAdminChangeQuote,
|
|
33
|
+
} from './admin-change-quote-request-key';
|
|
34
|
+
import {
|
|
35
|
+
buildAdminChangeV2QuoteRequest,
|
|
36
|
+
quoteAdminChangeV2ForUi,
|
|
37
|
+
} from './admin-change-v2-quote-request';
|
|
31
38
|
import type { ProviderDashboardChangeBookingPayload } from './provider-dashboard-change-booking';
|
|
32
39
|
|
|
33
40
|
type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
|
|
@@ -60,6 +67,8 @@ export interface UseAdminChangeQuotePreviewParams {
|
|
|
60
67
|
quantities: Record<string, number>;
|
|
61
68
|
addOnSelections: AddOnSelection[];
|
|
62
69
|
cancellationPolicyId: string | null;
|
|
70
|
+
appliedPromoCode: string | null;
|
|
71
|
+
promoApplicationScope: AdminPromoApplicationScope;
|
|
63
72
|
adminCustomReceiptLines: AdminCustomReceiptLine[];
|
|
64
73
|
adminStructuredAdjustments: NonNullable<ProviderDashboardChangeBookingPayload['structuredAdjustments']>;
|
|
65
74
|
refundDispositionsByOperationId: Record<string, AdminAmendmentRefundDisposition>;
|
|
@@ -119,6 +128,8 @@ export function useAdminChangeQuotePreview({
|
|
|
119
128
|
quantities,
|
|
120
129
|
addOnSelections,
|
|
121
130
|
cancellationPolicyId,
|
|
131
|
+
appliedPromoCode,
|
|
132
|
+
promoApplicationScope,
|
|
122
133
|
adminCustomReceiptLines,
|
|
123
134
|
adminStructuredAdjustments,
|
|
124
135
|
refundDispositionsByOperationId,
|
|
@@ -167,6 +178,7 @@ export function useAdminChangeQuotePreview({
|
|
|
167
178
|
latestChangeQuote?.canProceed === false &&
|
|
168
179
|
(latestChangeQuote.refundDecisionOperations?.length ?? 0) === 0;
|
|
169
180
|
const refundDecisionRequired = (latestChangeQuote?.refundDecisionOperations?.length ?? 0) > 0;
|
|
181
|
+
const hasPublishedChangeQuote = latestChangeQuote !== null;
|
|
170
182
|
const changeQuoteInputsKey = useMemo(() => buildAdminChangeQuoteRequestKey({
|
|
171
183
|
bookingReference: initialValues?.bookingReference,
|
|
172
184
|
lastName,
|
|
@@ -178,6 +190,8 @@ export function useAdminChangeQuotePreview({
|
|
|
178
190
|
addOnSelections,
|
|
179
191
|
adminCustomReceiptLines,
|
|
180
192
|
cancellationPolicyId,
|
|
193
|
+
promoCode: appliedPromoCode,
|
|
194
|
+
promoApplicationScope,
|
|
181
195
|
structuredAdjustments: adminStructuredAdjustments,
|
|
182
196
|
refundDispositionsByOperationId,
|
|
183
197
|
useAdminFeAuthoritativeQuote,
|
|
@@ -192,6 +206,8 @@ export function useAdminChangeQuotePreview({
|
|
|
192
206
|
addOnSelections,
|
|
193
207
|
adminCustomReceiptLines,
|
|
194
208
|
cancellationPolicyId,
|
|
209
|
+
appliedPromoCode,
|
|
210
|
+
promoApplicationScope,
|
|
195
211
|
adminStructuredAdjustments,
|
|
196
212
|
refundDispositionsByOperationId,
|
|
197
213
|
useAdminFeAuthoritativeQuote,
|
|
@@ -208,6 +224,14 @@ export function useAdminChangeQuotePreview({
|
|
|
208
224
|
const changeCheckoutButtonLabel = (() => {
|
|
209
225
|
if (!hasEffectiveChangeSelection) return undefined;
|
|
210
226
|
if (isProviderDashboardChange) {
|
|
227
|
+
if (changeQuoteLoading || (!latestChangeQuote && !changeQuoteFetchError)) {
|
|
228
|
+
const tr = t('booking.updatingPrice');
|
|
229
|
+
return tr !== 'booking.updatingPrice' ? tr : 'Updating price\u2026';
|
|
230
|
+
}
|
|
231
|
+
if (changeQuoteFetchError) {
|
|
232
|
+
const tr = t('booking.changeBookingRetry');
|
|
233
|
+
return tr !== 'booking.changeBookingRetry' ? tr : 'Change booking (retry)';
|
|
234
|
+
}
|
|
211
235
|
if (isChangeQuoteBlocked) {
|
|
212
236
|
const tr = t('booking.changeBookingNotAvailable');
|
|
213
237
|
return tr !== 'booking.changeBookingNotAvailable' ? tr : 'Change not available';
|
|
@@ -215,8 +239,8 @@ export function useAdminChangeQuotePreview({
|
|
|
215
239
|
// Prefer authoritative V2 signed due (charge positive / refund negative). Do not use the
|
|
216
240
|
// FE cart estimate once a server quote exists — receipt total deltas can disagree with
|
|
217
241
|
// floored/settlement amounts (DEFECT-PV2-018 follow-up).
|
|
218
|
-
const serverDue = latestChangeQuote?.serverPreview?.amountDue;
|
|
219
|
-
const due = Math.round(
|
|
242
|
+
const serverDue = latestChangeQuote?.serverPreview?.amountDue ?? latestChangeQuote?.priceDiff ?? 0;
|
|
243
|
+
const due = Math.round(serverDue * 100) / 100;
|
|
220
244
|
return due > 0
|
|
221
245
|
? `Change booking (${formatCurrencyAmount(due, currency, locale as 'en' | 'fr')})`
|
|
222
246
|
: due < 0
|
|
@@ -398,11 +422,15 @@ export function useAdminChangeQuotePreview({
|
|
|
398
422
|
return;
|
|
399
423
|
}
|
|
400
424
|
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
425
|
+
if (changeQuoteInputsKey && inFlightQuoteInputsKeyRef.current === changeQuoteInputsKey) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (canReuseCompletedAdminChangeQuote(
|
|
430
|
+
changeQuoteInputsKey,
|
|
431
|
+
lastCompletedQuoteInputsKeyRef.current,
|
|
432
|
+
hasPublishedChangeQuote,
|
|
433
|
+
)) {
|
|
406
434
|
setChangeQuoteLoading(false);
|
|
407
435
|
return;
|
|
408
436
|
}
|
|
@@ -429,44 +457,44 @@ export function useAdminChangeQuotePreview({
|
|
|
429
457
|
.filter(([, count]) => count > 0)
|
|
430
458
|
.map(([category, count]) => ({ category, count }));
|
|
431
459
|
try {
|
|
432
|
-
const quoteRequestBase = {
|
|
460
|
+
const quoteRequestBase = buildAdminChangeV2QuoteRequest({
|
|
433
461
|
bookingReference: bookingReferenceForQuote,
|
|
434
|
-
lastName
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
462
|
+
lastName,
|
|
463
|
+
parentProductId: product.productId,
|
|
464
|
+
optionId,
|
|
465
|
+
selectedAvailability,
|
|
466
|
+
pickupLocationId,
|
|
467
|
+
returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
|
|
468
|
+
bookingItems,
|
|
469
|
+
addOnSelections,
|
|
470
|
+
cancellationPolicyId: cancellationPolicyId ?? null,
|
|
471
|
+
promoCode: appliedPromoCode,
|
|
472
|
+
promoApplicationScope,
|
|
445
473
|
structuredAdjustments: adminStructuredAdjustments,
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
474
|
+
refundDispositionsByOperationId,
|
|
475
|
+
previousPassengerCount: changeFlowInitialTicketCount,
|
|
476
|
+
previousAvailabilityId: initialValues.availabilityId ?? null,
|
|
477
|
+
previousReturnAvailabilityId: initialValues.returnAvailabilityId ?? null,
|
|
478
|
+
});
|
|
479
|
+
const mergedQuote = useAdminFeAuthoritativeQuote
|
|
480
|
+
? await quoteAdminChangeV2ForUi(
|
|
481
|
+
quoteRequestBase,
|
|
482
|
+
{
|
|
483
|
+
total: changeFlowNewBookingTotal,
|
|
484
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
485
|
+
tax: effectiveTax,
|
|
486
|
+
},
|
|
487
|
+
currency,
|
|
488
|
+
)
|
|
489
|
+
: sliceAndMergeChangeQuoteForUi(await quoteChangeBooking({
|
|
459
490
|
...quoteRequestBase,
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
491
|
+
clientProposedTotal: changeFlowNewBookingTotal,
|
|
492
|
+
}), {
|
|
493
|
+
total: changeFlowNewBookingTotal,
|
|
494
|
+
subtotal: effectiveSubtotalForCheckout,
|
|
495
|
+
tax: effectiveTax,
|
|
496
|
+
}, currency).mergedQuote;
|
|
464
497
|
if (seq !== changeQuoteRequestSeq.current) return;
|
|
465
|
-
const mergedQuote = sliceAndMergeChangeQuoteForUi(quote, {
|
|
466
|
-
total: changeFlowNewBookingTotal,
|
|
467
|
-
subtotal: effectiveSubtotalForCheckout,
|
|
468
|
-
tax: effectiveTax,
|
|
469
|
-
}, currency).mergedQuote;
|
|
470
498
|
// Commit the completed identity before publishing its result. This keeps a
|
|
471
499
|
// render caused by setLatestChangeQuote from being interpreted as a new request.
|
|
472
500
|
lastCompletedQuoteInputsKeyRef.current = changeQuoteInputsKey;
|
|
@@ -497,6 +525,7 @@ export function useAdminChangeQuotePreview({
|
|
|
497
525
|
hasChangeSelection,
|
|
498
526
|
missingRequiredReturnSelection,
|
|
499
527
|
changeQuoteInputsKey,
|
|
528
|
+
hasPublishedChangeQuote,
|
|
500
529
|
]);
|
|
501
530
|
|
|
502
531
|
return {
|