@ticketboothapp/booking 1.2.155 → 1.2.158
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 +1 -0
- package/src/components/booking/AdminChangeBookingFlow.tsx +27 -45
- package/src/components/booking/AdminChangeCheckoutPanel.tsx +3 -0
- package/src/components/booking/AdminChangeReceiptComparison.tsx +61 -1
- package/src/components/booking/CheckoutModal.tsx +1 -1
- package/src/components/booking/PrivateShuttleCheckoutSection.tsx +1 -1
- package/src/components/booking/admin-refund-disposition.ts +35 -51
- package/src/components/booking/useAdminChangeCheckoutController.ts +1 -0
- package/src/lib/booking/change-flow-pricing.ts +2 -0
- package/src/lib/booking/i18n/messages/en.json +1 -1
- package/src/lib/booking-api.ts +12 -5
- package/test/change-booking-helpers.test.ts +30 -52
package/package.json
CHANGED
|
@@ -160,6 +160,7 @@ const checkoutPanelPropKeys = [
|
|
|
160
160
|
'serverUpdatedTotal',
|
|
161
161
|
'serverQuoteError',
|
|
162
162
|
'refundDecisionOperations',
|
|
163
|
+
'returnPriceTreatmentOperations',
|
|
163
164
|
'allowedRefundDispositionsByOperationId',
|
|
164
165
|
'removedValueByOperationId',
|
|
165
166
|
'refundDispositionsByOperationId',
|
|
@@ -56,10 +56,7 @@ import { useAdminChangeProductReset } from './useAdminChangeProductReset';
|
|
|
56
56
|
import { useBookingQuantityCapTrim } from './useBookingQuantityCapTrim';
|
|
57
57
|
import { useBookingViewItemAnalytics } from './useBookingViewItemAnalytics';
|
|
58
58
|
import { buildAdminChangeQuoteRequestKey } from './admin-change-quote-request-key';
|
|
59
|
-
import {
|
|
60
|
-
buildDefaultRefundDispositionsByOperationId,
|
|
61
|
-
type AdminReleaseRefundDisposition,
|
|
62
|
-
} from './admin-refund-disposition';
|
|
59
|
+
import type { AdminReleaseRefundDisposition } from './admin-refund-disposition';
|
|
63
60
|
|
|
64
61
|
type AdminRefundDecisionContext = {
|
|
65
62
|
selectionKey: string;
|
|
@@ -709,6 +706,8 @@ export function AdminChangeBookingFlow({
|
|
|
709
706
|
useAdminFeAuthoritativeQuote,
|
|
710
707
|
]);
|
|
711
708
|
const responseRefundDecisionOperations = latestChangeQuote?.refundDecisionOperations ?? [];
|
|
709
|
+
const responseReturnPriceTreatmentOperations =
|
|
710
|
+
latestChangeQuote?.returnPriceTreatmentOperations ?? [];
|
|
712
711
|
useEffect(() => {
|
|
713
712
|
if (responseRefundDecisionOperations.length === 0) return;
|
|
714
713
|
setRefundDecisionContext({
|
|
@@ -733,45 +732,25 @@ export function AdminChangeBookingFlow({
|
|
|
733
732
|
: refundDecisionContext?.selectionKey === refundDecisionSelectionKey
|
|
734
733
|
? refundDecisionContext
|
|
735
734
|
: null;
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
});
|
|
756
|
-
if (Object.keys(defaults).length === 0) return;
|
|
757
|
-
|
|
758
|
-
setRefundDispositionsByOperationId((current) => {
|
|
759
|
-
let changed = false;
|
|
760
|
-
const next = { ...current };
|
|
761
|
-
for (const [operationId, disposition] of Object.entries(defaults)) {
|
|
762
|
-
if (next[operationId] != null) continue;
|
|
763
|
-
next[operationId] = disposition;
|
|
764
|
-
changed = true;
|
|
765
|
-
}
|
|
766
|
-
return changed ? next : current;
|
|
767
|
-
});
|
|
768
|
-
}, [activeRefundDecisionContext, changeSelectionDetails]);
|
|
769
|
-
const activeRefundDispositionsByOperationId = useMemo(() => Object.fromEntries(
|
|
770
|
-
(activeRefundDecisionContext?.operations ?? []).flatMap((operation) => {
|
|
771
|
-
const disposition = refundDispositionsByOperationId[operation.operationId];
|
|
772
|
-
return disposition ? [[operation.operationId, disposition] as const] : [];
|
|
773
|
-
}),
|
|
774
|
-
), [activeRefundDecisionContext?.operations, refundDispositionsByOperationId]);
|
|
735
|
+
const activeReturnPriceTreatmentOperations =
|
|
736
|
+
responseReturnPriceTreatmentOperations.length > 0
|
|
737
|
+
? responseReturnPriceTreatmentOperations
|
|
738
|
+
: [];
|
|
739
|
+
const activeRefundDispositionsByOperationId = useMemo(() => {
|
|
740
|
+
const relevantOperationIds = new Set([
|
|
741
|
+
...(activeRefundDecisionContext?.operations ?? []).map((operation) => operation.operationId),
|
|
742
|
+
...activeReturnPriceTreatmentOperations.map((operation) => operation.operationId),
|
|
743
|
+
]);
|
|
744
|
+
return Object.fromEntries(
|
|
745
|
+
Object.entries(refundDispositionsByOperationId).filter(([operationId]) =>
|
|
746
|
+
relevantOperationIds.has(operationId),
|
|
747
|
+
),
|
|
748
|
+
);
|
|
749
|
+
}, [
|
|
750
|
+
activeRefundDecisionContext?.operations,
|
|
751
|
+
activeReturnPriceTreatmentOperations,
|
|
752
|
+
refundDispositionsByOperationId,
|
|
753
|
+
]);
|
|
775
754
|
const handleRefundDispositionChange = useCallback((
|
|
776
755
|
operationId: string,
|
|
777
756
|
disposition: AdminReleaseRefundDisposition | null,
|
|
@@ -1100,8 +1079,11 @@ export function AdminChangeBookingFlow({
|
|
|
1100
1079
|
? (latestChangeQuote?.reasonIfBlocked ?? 'This change cannot be priced right now.')
|
|
1101
1080
|
: null),
|
|
1102
1081
|
refundDecisionOperations: activeRefundDecisionContext?.operations ?? [],
|
|
1103
|
-
|
|
1104
|
-
|
|
1082
|
+
returnPriceTreatmentOperations: activeReturnPriceTreatmentOperations,
|
|
1083
|
+
allowedRefundDispositionsByOperationId: {
|
|
1084
|
+
...(activeRefundDecisionContext?.allowedByOperationId ?? {}),
|
|
1085
|
+
...(latestChangeQuote?.allowedRefundDispositionsByOperationId ?? {}),
|
|
1086
|
+
},
|
|
1105
1087
|
removedValueByOperationId:
|
|
1106
1088
|
activeRefundDecisionContext?.removedValueByOperationId ?? {},
|
|
1107
1089
|
refundDispositionsByOperationId: activeRefundDispositionsByOperationId,
|
|
@@ -41,6 +41,7 @@ export interface AdminChangeCheckoutPanelProps {
|
|
|
41
41
|
serverUpdatedTotal: number | null;
|
|
42
42
|
serverQuoteError?: string | null;
|
|
43
43
|
refundDecisionOperations?: AdminAmendmentOperationSnapshot[];
|
|
44
|
+
returnPriceTreatmentOperations?: AdminAmendmentOperationSnapshot[];
|
|
44
45
|
allowedRefundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition[]>;
|
|
45
46
|
removedValueByOperationId?: Record<string, number>;
|
|
46
47
|
refundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition>;
|
|
@@ -123,6 +124,7 @@ export function AdminChangeCheckoutPanel({
|
|
|
123
124
|
serverUpdatedTotal,
|
|
124
125
|
serverQuoteError,
|
|
125
126
|
refundDecisionOperations,
|
|
127
|
+
returnPriceTreatmentOperations,
|
|
126
128
|
allowedRefundDispositionsByOperationId,
|
|
127
129
|
removedValueByOperationId,
|
|
128
130
|
refundDispositionsByOperationId,
|
|
@@ -216,6 +218,7 @@ export function AdminChangeCheckoutPanel({
|
|
|
216
218
|
selectionChanged={hasEffectiveChangeSelection}
|
|
217
219
|
quoteError={serverQuoteError}
|
|
218
220
|
refundDecisionOperations={refundDecisionOperations}
|
|
221
|
+
returnPriceTreatmentOperations={returnPriceTreatmentOperations}
|
|
219
222
|
allowedRefundDispositionsByOperationId={allowedRefundDispositionsByOperationId}
|
|
220
223
|
removedValueByOperationId={removedValueByOperationId}
|
|
221
224
|
refundDispositionsByOperationId={refundDispositionsByOperationId}
|
|
@@ -12,9 +12,12 @@ import { PriceSummary, type PriceSummaryLine } from './PriceSummary';
|
|
|
12
12
|
import {
|
|
13
13
|
adminRemovalOperationLabel,
|
|
14
14
|
noRefundRemovedValue,
|
|
15
|
+
normalizeReturnPriceTreatment,
|
|
15
16
|
refundDecisionsComplete,
|
|
16
17
|
releaseRefundDispositionOptions,
|
|
18
|
+
returnPriceTreatmentOptions,
|
|
17
19
|
type AdminReleaseRefundDisposition,
|
|
20
|
+
type AdminReturnPriceTreatment,
|
|
18
21
|
} from './admin-refund-disposition';
|
|
19
22
|
|
|
20
23
|
type TranslationFn = (key: string, params?: Record<string, string>) => string;
|
|
@@ -37,6 +40,7 @@ export interface AdminChangeReceiptComparisonProps {
|
|
|
37
40
|
taxRate?: number;
|
|
38
41
|
adjustments?: ReactNode;
|
|
39
42
|
refundDecisionOperations?: AdminAmendmentOperationSnapshot[];
|
|
43
|
+
returnPriceTreatmentOperations?: AdminAmendmentOperationSnapshot[];
|
|
40
44
|
allowedRefundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition[]>;
|
|
41
45
|
removedValueByOperationId?: Record<string, number>;
|
|
42
46
|
refundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition>;
|
|
@@ -86,6 +90,7 @@ export function AdminChangeReceiptComparison({
|
|
|
86
90
|
taxRate,
|
|
87
91
|
adjustments,
|
|
88
92
|
refundDecisionOperations = [],
|
|
93
|
+
returnPriceTreatmentOperations = [],
|
|
89
94
|
allowedRefundDispositionsByOperationId = {},
|
|
90
95
|
removedValueByOperationId = {},
|
|
91
96
|
refundDispositionsByOperationId = {},
|
|
@@ -97,11 +102,17 @@ export function AdminChangeReceiptComparison({
|
|
|
97
102
|
const originalCurrency = originalReceipt.currency ?? currency;
|
|
98
103
|
const quoteReady = amendmentLines != null && amountDue != null && updatedTotal != null;
|
|
99
104
|
const hasRefundDecisions = refundDecisionOperations.length > 0;
|
|
105
|
+
const hasReturnPriceTreatments = returnPriceTreatmentOperations.length > 0;
|
|
100
106
|
const refundSelectionsComplete = refundDecisionsComplete(
|
|
101
107
|
refundDecisionOperations,
|
|
102
108
|
refundDispositionsByOperationId,
|
|
103
109
|
);
|
|
104
110
|
const settlementPending = hasRefundDecisions && (!refundSelectionsComplete || refundQuoteLoading);
|
|
111
|
+
const returnRepricePending = hasReturnPriceTreatments &&
|
|
112
|
+
returnPriceTreatmentOperations.some(
|
|
113
|
+
(operation) => refundDispositionsByOperationId[operation.operationId] != null,
|
|
114
|
+
) &&
|
|
115
|
+
refundQuoteLoading;
|
|
105
116
|
const displayedAmountDue = settlementPending ? null : amountDue;
|
|
106
117
|
const removedWithoutRefund = noRefundRemovedValue(
|
|
107
118
|
refundDecisionOperations,
|
|
@@ -150,12 +161,61 @@ export function AdminChangeReceiptComparison({
|
|
|
150
161
|
|
|
151
162
|
return (
|
|
152
163
|
<div className="min-w-0 space-y-3 overflow-visible">
|
|
164
|
+
{hasReturnPriceTreatments ? (
|
|
165
|
+
<section className="rounded-lg border border-sky-300 bg-sky-50 p-3" aria-label="Return price treatment">
|
|
166
|
+
<div className="mb-3">
|
|
167
|
+
<h3 className="text-sm font-semibold text-stone-900">Return price</h3>
|
|
168
|
+
<p className="mt-1 text-xs text-stone-600">
|
|
169
|
+
By default the original paid return price stays on the booking. You can optionally reduce it and choose refund treatment.
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="space-y-3">
|
|
173
|
+
{returnPriceTreatmentOperations.map((operation) => {
|
|
174
|
+
const options = returnPriceTreatmentOptions(
|
|
175
|
+
allowedRefundDispositionsByOperationId[operation.operationId],
|
|
176
|
+
);
|
|
177
|
+
const selectedValue: AdminReturnPriceTreatment = normalizeReturnPriceTreatment(
|
|
178
|
+
refundDispositionsByOperationId[operation.operationId],
|
|
179
|
+
);
|
|
180
|
+
const selected = options.find((option) => option.value === selectedValue);
|
|
181
|
+
return (
|
|
182
|
+
<div key={operation.operationId} className="rounded-md border border-sky-200 bg-white p-3">
|
|
183
|
+
<div className="mb-2 text-sm font-medium capitalize text-stone-900">
|
|
184
|
+
{adminRemovalOperationLabel(operation)}
|
|
185
|
+
</div>
|
|
186
|
+
<select
|
|
187
|
+
className="w-full rounded-md border border-stone-300 bg-white px-3 py-2 text-sm text-stone-900"
|
|
188
|
+
value={selectedValue}
|
|
189
|
+
onChange={(event) => {
|
|
190
|
+
const value = event.target.value as AdminReturnPriceTreatment;
|
|
191
|
+
onRefundDispositionChange?.(
|
|
192
|
+
operation.operationId,
|
|
193
|
+
value === 'KEEP_FLOOR' ? null : value,
|
|
194
|
+
);
|
|
195
|
+
}}
|
|
196
|
+
aria-label={`Return price treatment for ${adminRemovalOperationLabel(operation)}`}
|
|
197
|
+
>
|
|
198
|
+
{options.map((option) => (
|
|
199
|
+
<option key={option.value} value={option.value}>{option.label}</option>
|
|
200
|
+
))}
|
|
201
|
+
</select>
|
|
202
|
+
{selected ? <p className="mt-2 text-xs text-stone-600">{selected.description}</p> : null}
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
})}
|
|
206
|
+
</div>
|
|
207
|
+
{returnRepricePending ? (
|
|
208
|
+
<p className="mt-3 text-xs font-medium text-sky-800">Recalculating settlement…</p>
|
|
209
|
+
) : null}
|
|
210
|
+
</section>
|
|
211
|
+
) : null}
|
|
212
|
+
|
|
153
213
|
{hasRefundDecisions ? (
|
|
154
214
|
<section className="rounded-lg border border-amber-300 bg-amber-50 p-3" aria-label="Refund treatment">
|
|
155
215
|
<div className="mb-3">
|
|
156
216
|
<h3 className="text-sm font-semibold text-stone-900">Choose refund treatment</h3>
|
|
157
217
|
<p className="mt-1 text-xs text-stone-600">
|
|
158
|
-
Removed paid value is never refunded automatically. Choose an explicit treatment
|
|
218
|
+
Removed paid value is never refunded automatically. Choose an explicit treatment, then the server will recalculate the settlement.
|
|
159
219
|
</p>
|
|
160
220
|
</div>
|
|
161
221
|
<div className="space-y-3">
|
|
@@ -411,7 +411,7 @@ export function CheckoutModal({
|
|
|
411
411
|
{balanceChargeDaysBefore > 0
|
|
412
412
|
? (t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore }) !== 'booking.balanceChargeNotice'
|
|
413
413
|
? t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore })
|
|
414
|
-
: `The remaining balance
|
|
414
|
+
: `The remaining balance is due ${balanceChargeDaysBefore} days before your booking. You can pay it from your Manage Booking page.`)
|
|
415
415
|
: (t('booking.balancePayEarlier') !== 'booking.balancePayEarlier'
|
|
416
416
|
? t('booking.balancePayEarlier')
|
|
417
417
|
: 'You can pay the remaining balance anytime from your Manage Booking page.')}
|
|
@@ -179,7 +179,7 @@ export function PrivateShuttleCheckoutSection({
|
|
|
179
179
|
{balanceChargeDaysBefore && balanceChargeDaysBefore > 0
|
|
180
180
|
? (t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore }) !== 'booking.balanceChargeNotice'
|
|
181
181
|
? t('booking.balanceChargeNotice', { days: balanceChargeDaysBefore })
|
|
182
|
-
: `The remaining balance
|
|
182
|
+
: `The remaining balance is due ${balanceChargeDaysBefore} days before your booking. You can pay it from your Manage Booking page.`)
|
|
183
183
|
: (t('booking.balancePayEarlier') !== 'booking.balancePayEarlier'
|
|
184
184
|
? t('booking.balancePayEarlier')
|
|
185
185
|
: 'You can pay the remaining balance anytime from your Manage Booking page.')}
|
|
@@ -8,12 +8,21 @@ export type AdminReleaseRefundDisposition = Exclude<
|
|
|
8
8
|
'REFUND_TO_ORIGINAL_PAYMENT'
|
|
9
9
|
>;
|
|
10
10
|
|
|
11
|
+
/** Local UI value: omit disposition on the quote request to keep the paid-return floor. */
|
|
12
|
+
export type AdminReturnPriceTreatment = 'KEEP_FLOOR' | AdminReleaseRefundDisposition;
|
|
13
|
+
|
|
11
14
|
export type AdminRefundDispositionOption = {
|
|
12
15
|
value: AdminReleaseRefundDisposition;
|
|
13
16
|
label: string;
|
|
14
17
|
description: string;
|
|
15
18
|
};
|
|
16
19
|
|
|
20
|
+
export type AdminReturnPriceTreatmentOption = {
|
|
21
|
+
value: AdminReturnPriceTreatment;
|
|
22
|
+
label: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
17
26
|
const RELEASE_OPTIONS: AdminRefundDispositionOption[] = [
|
|
18
27
|
{
|
|
19
28
|
value: 'PENDING_REFUND',
|
|
@@ -27,8 +36,11 @@ const RELEASE_OPTIONS: AdminRefundDispositionOption[] = [
|
|
|
27
36
|
},
|
|
28
37
|
];
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
const KEEP_FLOOR_OPTION: AdminReturnPriceTreatmentOption = {
|
|
40
|
+
value: 'KEEP_FLOOR',
|
|
41
|
+
label: 'Keep original return price',
|
|
42
|
+
description: 'Change the return time but keep what was already paid for the return on the booking total.',
|
|
43
|
+
};
|
|
32
44
|
|
|
33
45
|
export function releaseRefundDispositionOptions(
|
|
34
46
|
allowed: readonly AdminAmendmentRefundDisposition[] | undefined,
|
|
@@ -37,57 +49,29 @@ export function releaseRefundDispositionOptions(
|
|
|
37
49
|
return RELEASE_OPTIONS.filter((option) => allowed.includes(option.value));
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* Only auto-default the low-stakes return-only case (same product/outbound → NO_REFUND).
|
|
56
|
-
* Other removals stay undecided so the operator must choose explicitly.
|
|
57
|
-
*/
|
|
58
|
-
export function preferredAdminRefundDisposition(args: {
|
|
59
|
-
operations: readonly AdminAmendmentOperationSnapshot[];
|
|
60
|
-
/** Same parent product, option, date/time, passengers, and add-ons — only return differs. */
|
|
61
|
-
sameProductAndOutboundSelection: boolean;
|
|
62
|
-
allowed?: readonly AdminAmendmentRefundDisposition[];
|
|
63
|
-
}): AdminReleaseRefundDisposition | null {
|
|
64
|
-
if (
|
|
65
|
-
!args.sameProductAndOutboundSelection ||
|
|
66
|
-
!isReturnOnlyRefundDecision(args.operations)
|
|
67
|
-
) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
const options = releaseRefundDispositionOptions(args.allowed);
|
|
71
|
-
return options.some((option) => option.value === 'NO_REFUND') ? 'NO_REFUND' : null;
|
|
52
|
+
export function returnPriceTreatmentOptions(
|
|
53
|
+
allowed: readonly AdminAmendmentRefundDisposition[] | undefined,
|
|
54
|
+
): AdminReturnPriceTreatmentOption[] {
|
|
55
|
+
return [
|
|
56
|
+
KEEP_FLOOR_OPTION,
|
|
57
|
+
...releaseRefundDispositionOptions(allowed).map((option) => ({
|
|
58
|
+
value: option.value,
|
|
59
|
+
label: option.value === 'NO_REFUND'
|
|
60
|
+
? 'Reduce return price — no refund'
|
|
61
|
+
: 'Reduce return price — pending refund',
|
|
62
|
+
description: option.value === 'NO_REFUND'
|
|
63
|
+
? 'Lower the booking total to the cheaper return price without creating a customer credit.'
|
|
64
|
+
: 'Lower the booking total and record a pending refund for the removed return value.',
|
|
65
|
+
})),
|
|
66
|
+
];
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
export function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}): Record<string, AdminReleaseRefundDisposition> {
|
|
81
|
-
return Object.fromEntries(
|
|
82
|
-
args.operations.flatMap((operation) => {
|
|
83
|
-
const disposition = preferredAdminRefundDisposition({
|
|
84
|
-
operations: args.operations,
|
|
85
|
-
sameProductAndOutboundSelection: args.sameProductAndOutboundSelection,
|
|
86
|
-
allowed: args.allowedByOperationId?.[operation.operationId],
|
|
87
|
-
});
|
|
88
|
-
return disposition ? [[operation.operationId, disposition] as const] : [];
|
|
89
|
-
}),
|
|
90
|
-
);
|
|
69
|
+
export function normalizeReturnPriceTreatment(
|
|
70
|
+
disposition: AdminAmendmentRefundDisposition | undefined,
|
|
71
|
+
): AdminReturnPriceTreatment {
|
|
72
|
+
return disposition === 'PENDING_REFUND' || disposition === 'NO_REFUND'
|
|
73
|
+
? disposition
|
|
74
|
+
: 'KEEP_FLOOR';
|
|
91
75
|
}
|
|
92
76
|
|
|
93
77
|
export function adminRemovalOperationLabel(operation: AdminAmendmentOperationSnapshot): string {
|
|
@@ -85,6 +85,7 @@ export interface AdminChangeLatestQuote {
|
|
|
85
85
|
quotedTotal?: number;
|
|
86
86
|
paymentCreditTotal?: number;
|
|
87
87
|
refundDecisionOperations?: AdminAmendmentOperationSnapshot[];
|
|
88
|
+
returnPriceTreatmentOperations?: AdminAmendmentOperationSnapshot[];
|
|
88
89
|
allowedRefundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition[]>;
|
|
89
90
|
removedValueByOperationId?: Record<string, number>;
|
|
90
91
|
serverDisplay?: { total: number; subtotal: number; tax: number };
|
|
@@ -257,6 +257,7 @@ export interface ChangeQuoteUiSlice {
|
|
|
257
257
|
quotedTotal?: number;
|
|
258
258
|
paymentCreditTotal?: number;
|
|
259
259
|
refundDecisionOperations?: ChangeBookingQuoteResponse['refundDecisionOperations'];
|
|
260
|
+
returnPriceTreatmentOperations?: ChangeBookingQuoteResponse['returnPriceTreatmentOperations'];
|
|
260
261
|
allowedRefundDispositionsByOperationId?: ChangeBookingQuoteResponse['allowedRefundDispositionsByOperationId'];
|
|
261
262
|
removedValueByOperationId?: ChangeBookingQuoteResponse['removedValueByOperationId'];
|
|
262
263
|
serverDisplay?: { total: number; subtotal: number; tax: number };
|
|
@@ -297,6 +298,7 @@ export function sliceChangeQuoteForUi(
|
|
|
297
298
|
quotedTotal: pricingQuote?.payableTotal ?? pricingQuote?.totalAmount ?? quote.proposed?.total ?? quote.newReceipt?.total,
|
|
298
299
|
paymentCreditTotal: pricingQuote?.paymentCreditTotal ?? quote.paymentCreditTotal,
|
|
299
300
|
refundDecisionOperations: quote.refundDecisionOperations,
|
|
301
|
+
returnPriceTreatmentOperations: quote.returnPriceTreatmentOperations,
|
|
300
302
|
allowedRefundDispositionsByOperationId: quote.allowedRefundDispositionsByOperationId,
|
|
301
303
|
removedValueByOperationId: quote.removedValueByOperationId,
|
|
302
304
|
...(serverDisplay ? { serverDisplay } : {}),
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
"included": "Included",
|
|
154
154
|
"flexibleCancellation": "Flexible cancellation",
|
|
155
155
|
"depositPaymentNotice": "You're paying the deposit today.",
|
|
156
|
-
"balanceChargeNotice": "The remaining balance
|
|
156
|
+
"balanceChargeNotice": "The remaining balance is due {days} days before your booking. You can pay it from your Manage Booking page.",
|
|
157
157
|
"balancePayEarlier": "You can pay the remaining balance anytime from your Manage Booking page.",
|
|
158
158
|
"remainingBalance": "Remaining Balance"
|
|
159
159
|
},
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -1291,11 +1291,7 @@ export interface ChangeBookingQuoteRequest {
|
|
|
1291
1291
|
previousAvailabilityId?: string | null;
|
|
1292
1292
|
previousReturnAvailabilityId?: string | null;
|
|
1293
1293
|
} | null;
|
|
1294
|
-
/**
|
|
1295
|
-
* Explicit admin treatment for server-classified removed paid value.
|
|
1296
|
-
* The dashboard may pre-select NO_REFUND for return-only changes (still overridable);
|
|
1297
|
-
* other removals stay undecided until chosen. The value is always sent explicitly on quote/apply.
|
|
1298
|
-
*/
|
|
1294
|
+
/** Explicit admin treatment for server-classified removed paid value. Never defaulted by the client. */
|
|
1299
1295
|
refundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition>;
|
|
1300
1296
|
}
|
|
1301
1297
|
|
|
@@ -1339,6 +1335,11 @@ export interface AdminChangeBookingQuoteV2Data {
|
|
|
1339
1335
|
reasonIfBlocked?: string | null;
|
|
1340
1336
|
unsupportedFeatures?: string[];
|
|
1341
1337
|
refundDecisionOperations?: AdminAmendmentOperationSnapshot[];
|
|
1338
|
+
/**
|
|
1339
|
+
* CHANGE_RETURN ops. Omit disposition to keep the paid-return floor; send
|
|
1340
|
+
* PENDING_REFUND / NO_REFUND to reprice and settle removed value.
|
|
1341
|
+
*/
|
|
1342
|
+
returnPriceTreatmentOperations?: AdminAmendmentOperationSnapshot[];
|
|
1342
1343
|
allowedRefundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition[]>;
|
|
1343
1344
|
removedValueByOperationId?: Record<string, number>;
|
|
1344
1345
|
}
|
|
@@ -1479,6 +1480,11 @@ export interface ChangeBookingQuoteResponse {
|
|
|
1479
1480
|
reasonIfBlocked?: string;
|
|
1480
1481
|
/** Admin-only removal decisions required before this quote can be applied. */
|
|
1481
1482
|
refundDecisionOperations?: AdminAmendmentOperationSnapshot[];
|
|
1483
|
+
/**
|
|
1484
|
+
* Admin CHANGE_RETURN ops. Default (no disposition) keeps the paid return floor;
|
|
1485
|
+
* PENDING_REFUND / NO_REFUND reprices the return component.
|
|
1486
|
+
*/
|
|
1487
|
+
returnPriceTreatmentOperations?: AdminAmendmentOperationSnapshot[];
|
|
1482
1488
|
/** Server-authorized choices. Release UI intentionally filters out immediate refund. */
|
|
1483
1489
|
allowedRefundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition[]>;
|
|
1484
1490
|
/** Original paid basis plus allocated refundable tax for each removal operation. */
|
|
@@ -1669,6 +1675,7 @@ export function mapAdminChangeBookingQuoteV2Data(
|
|
|
1669
1675
|
canProceed: data.canApply,
|
|
1670
1676
|
reasonIfBlocked: data.reasonIfBlocked ?? undefined,
|
|
1671
1677
|
refundDecisionOperations: data.refundDecisionOperations ?? [],
|
|
1678
|
+
returnPriceTreatmentOperations: data.returnPriceTreatmentOperations ?? [],
|
|
1672
1679
|
allowedRefundDispositionsByOperationId:
|
|
1673
1680
|
data.allowedRefundDispositionsByOperationId ?? {},
|
|
1674
1681
|
removedValueByOperationId: data.removedValueByOperationId ?? {},
|
|
@@ -15,12 +15,11 @@ import {
|
|
|
15
15
|
} from '../src/lib/booking-api';
|
|
16
16
|
import { buildAdminChangeQuoteRequestKey } from '../src/components/booking/admin-change-quote-request-key';
|
|
17
17
|
import {
|
|
18
|
-
buildDefaultRefundDispositionsByOperationId,
|
|
19
|
-
isReturnOnlyRefundDecision,
|
|
20
18
|
noRefundRemovedValue,
|
|
21
|
-
|
|
19
|
+
normalizeReturnPriceTreatment,
|
|
22
20
|
refundDecisionsComplete,
|
|
23
21
|
releaseRefundDispositionOptions,
|
|
22
|
+
returnPriceTreatmentOptions,
|
|
24
23
|
} from '../src/components/booking/admin-refund-disposition';
|
|
25
24
|
import {
|
|
26
25
|
applyResourceAdjustments,
|
|
@@ -189,64 +188,43 @@ test('admin removal quote preserves explicit release refund decisions for the pr
|
|
|
189
188
|
);
|
|
190
189
|
});
|
|
191
190
|
|
|
192
|
-
test('admin
|
|
193
|
-
const
|
|
191
|
+
test('admin return price treatment defaults to keep floor and maps optional reprice choices', () => {
|
|
192
|
+
const operation = {
|
|
194
193
|
operationId: 'op_change_return',
|
|
195
194
|
type: 'CHANGE_RETURN',
|
|
196
195
|
componentKey: 'return',
|
|
197
196
|
};
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}),
|
|
212
|
-
'NO_REFUND',
|
|
213
|
-
);
|
|
214
|
-
assert.equal(
|
|
215
|
-
preferredAdminRefundDisposition({
|
|
216
|
-
operations: [changeReturn],
|
|
217
|
-
sameProductAndOutboundSelection: false,
|
|
218
|
-
}),
|
|
219
|
-
null,
|
|
220
|
-
);
|
|
221
|
-
assert.equal(
|
|
222
|
-
preferredAdminRefundDisposition({
|
|
223
|
-
operations: [removeAdult],
|
|
224
|
-
sameProductAndOutboundSelection: true,
|
|
225
|
-
}),
|
|
226
|
-
null,
|
|
227
|
-
);
|
|
228
|
-
assert.deepEqual(
|
|
229
|
-
buildDefaultRefundDispositionsByOperationId({
|
|
230
|
-
operations: [changeReturn],
|
|
231
|
-
sameProductAndOutboundSelection: true,
|
|
232
|
-
allowedByOperationId: {
|
|
233
|
-
[changeReturn.operationId]: ['PENDING_REFUND', 'NO_REFUND'],
|
|
234
|
-
},
|
|
235
|
-
}),
|
|
236
|
-
{ [changeReturn.operationId]: 'NO_REFUND' },
|
|
197
|
+
const mapped = mapAdminChangeBookingQuoteV2Data({
|
|
198
|
+
oldReceipt: { currency: 'CAD', grossSubtotal: 214.48, taxAmount: 18.23, payableTotal: 232.71 },
|
|
199
|
+
newQuote: { currency: 'CAD', payableTotal: 232.71, balanceDelta: 0, amountToCharge: 0, refundCandidate: 0 },
|
|
200
|
+
canApply: true,
|
|
201
|
+
returnPriceTreatmentOperations: [operation],
|
|
202
|
+
allowedRefundDispositionsByOperationId: {
|
|
203
|
+
[operation.operationId]: ['PENDING_REFUND', 'NO_REFUND'],
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const slice = sliceChangeQuoteForUi(
|
|
207
|
+
mapped,
|
|
208
|
+
{ total: 232.71, subtotal: 214.48, tax: 18.23 },
|
|
209
|
+
'CAD',
|
|
237
210
|
);
|
|
211
|
+
|
|
212
|
+
assert.deepEqual(slice.returnPriceTreatmentOperations, [operation]);
|
|
238
213
|
assert.deepEqual(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
[removeAdult.operationId]: ['PENDING_REFUND', 'NO_REFUND'],
|
|
244
|
-
},
|
|
245
|
-
}),
|
|
246
|
-
{},
|
|
214
|
+
returnPriceTreatmentOptions(
|
|
215
|
+
slice.allowedRefundDispositionsByOperationId?.[operation.operationId],
|
|
216
|
+
).map((option) => option.value),
|
|
217
|
+
['KEEP_FLOOR', 'PENDING_REFUND', 'NO_REFUND'],
|
|
247
218
|
);
|
|
248
219
|
});
|
|
249
220
|
|
|
221
|
+
test('unsupported immediate refund cannot become a return price treatment', () => {
|
|
222
|
+
assert.equal(normalizeReturnPriceTreatment(undefined), 'KEEP_FLOOR');
|
|
223
|
+
assert.equal(normalizeReturnPriceTreatment('REFUND_TO_ORIGINAL_PAYMENT'), 'KEEP_FLOOR');
|
|
224
|
+
assert.equal(normalizeReturnPriceTreatment('PENDING_REFUND'), 'PENDING_REFUND');
|
|
225
|
+
assert.equal(normalizeReturnPriceTreatment('NO_REFUND'), 'NO_REFUND');
|
|
226
|
+
});
|
|
227
|
+
|
|
250
228
|
test('changing admin refund treatment creates a distinct authoritative quote request', () => {
|
|
251
229
|
const base = {
|
|
252
230
|
bookingReference: 'DE8H04DX',
|