@ticketboothapp/booking 1.2.155 → 1.2.156

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.155",
3
+ "version": "1.2.156",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -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
- /** Seed a disposition once per booking-selection key; operator can still override the dropdown. */
737
- const refundDispositionAutoDefaultKeyRef = useRef<string | null>(null);
738
- useEffect(() => {
739
- if (!activeRefundDecisionContext || activeRefundDecisionContext.operations.length === 0) return;
740
- if (refundDispositionAutoDefaultKeyRef.current === activeRefundDecisionContext.selectionKey) {
741
- return;
742
- }
743
- refundDispositionAutoDefaultKeyRef.current = activeRefundDecisionContext.selectionKey;
744
-
745
- const sameProductAndOutboundSelection =
746
- !changeSelectionDetails.dateChanged &&
747
- !changeSelectionDetails.productChanged &&
748
- !changeSelectionDetails.optionChanged &&
749
- !changeSelectionDetails.countsChanged &&
750
- !changeSelectionDetails.addOnsChanged;
751
- const defaults = buildDefaultRefundDispositionsByOperationId({
752
- operations: activeRefundDecisionContext.operations,
753
- allowedByOperationId: activeRefundDecisionContext.allowedByOperationId,
754
- sameProductAndOutboundSelection,
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
- allowedRefundDispositionsByOperationId:
1104
- activeRefundDecisionContext?.allowedByOperationId ?? {},
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}
@@ -14,7 +14,9 @@ import {
14
14
  noRefundRemovedValue,
15
15
  refundDecisionsComplete,
16
16
  releaseRefundDispositionOptions,
17
+ returnPriceTreatmentOptions,
17
18
  type AdminReleaseRefundDisposition,
19
+ type AdminReturnPriceTreatment,
18
20
  } from './admin-refund-disposition';
19
21
 
20
22
  type TranslationFn = (key: string, params?: Record<string, string>) => string;
@@ -37,6 +39,7 @@ export interface AdminChangeReceiptComparisonProps {
37
39
  taxRate?: number;
38
40
  adjustments?: ReactNode;
39
41
  refundDecisionOperations?: AdminAmendmentOperationSnapshot[];
42
+ returnPriceTreatmentOperations?: AdminAmendmentOperationSnapshot[];
40
43
  allowedRefundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition[]>;
41
44
  removedValueByOperationId?: Record<string, number>;
42
45
  refundDispositionsByOperationId?: Record<string, AdminAmendmentRefundDisposition>;
@@ -86,6 +89,7 @@ export function AdminChangeReceiptComparison({
86
89
  taxRate,
87
90
  adjustments,
88
91
  refundDecisionOperations = [],
92
+ returnPriceTreatmentOperations = [],
89
93
  allowedRefundDispositionsByOperationId = {},
90
94
  removedValueByOperationId = {},
91
95
  refundDispositionsByOperationId = {},
@@ -97,11 +101,17 @@ export function AdminChangeReceiptComparison({
97
101
  const originalCurrency = originalReceipt.currency ?? currency;
98
102
  const quoteReady = amendmentLines != null && amountDue != null && updatedTotal != null;
99
103
  const hasRefundDecisions = refundDecisionOperations.length > 0;
104
+ const hasReturnPriceTreatments = returnPriceTreatmentOperations.length > 0;
100
105
  const refundSelectionsComplete = refundDecisionsComplete(
101
106
  refundDecisionOperations,
102
107
  refundDispositionsByOperationId,
103
108
  );
104
109
  const settlementPending = hasRefundDecisions && (!refundSelectionsComplete || refundQuoteLoading);
110
+ const returnRepricePending = hasReturnPriceTreatments &&
111
+ returnPriceTreatmentOperations.some(
112
+ (operation) => refundDispositionsByOperationId[operation.operationId] != null,
113
+ ) &&
114
+ refundQuoteLoading;
105
115
  const displayedAmountDue = settlementPending ? null : amountDue;
106
116
  const removedWithoutRefund = noRefundRemovedValue(
107
117
  refundDecisionOperations,
@@ -150,12 +160,60 @@ export function AdminChangeReceiptComparison({
150
160
 
151
161
  return (
152
162
  <div className="min-w-0 space-y-3 overflow-visible">
163
+ {hasReturnPriceTreatments ? (
164
+ <section className="rounded-lg border border-sky-300 bg-sky-50 p-3" aria-label="Return price treatment">
165
+ <div className="mb-3">
166
+ <h3 className="text-sm font-semibold text-stone-900">Return price</h3>
167
+ <p className="mt-1 text-xs text-stone-600">
168
+ By default the original paid return price stays on the booking. You can optionally reduce it and choose refund treatment.
169
+ </p>
170
+ </div>
171
+ <div className="space-y-3">
172
+ {returnPriceTreatmentOperations.map((operation) => {
173
+ const options = returnPriceTreatmentOptions(
174
+ allowedRefundDispositionsByOperationId[operation.operationId],
175
+ );
176
+ const selectedValue: AdminReturnPriceTreatment =
177
+ refundDispositionsByOperationId[operation.operationId] ?? 'KEEP_FLOOR';
178
+ const selected = options.find((option) => option.value === selectedValue);
179
+ return (
180
+ <div key={operation.operationId} className="rounded-md border border-sky-200 bg-white p-3">
181
+ <div className="mb-2 text-sm font-medium capitalize text-stone-900">
182
+ {adminRemovalOperationLabel(operation)}
183
+ </div>
184
+ <select
185
+ className="w-full rounded-md border border-stone-300 bg-white px-3 py-2 text-sm text-stone-900"
186
+ value={selectedValue}
187
+ onChange={(event) => {
188
+ const value = event.target.value as AdminReturnPriceTreatment;
189
+ onRefundDispositionChange?.(
190
+ operation.operationId,
191
+ value === 'KEEP_FLOOR' ? null : value,
192
+ );
193
+ }}
194
+ aria-label={`Return price treatment for ${adminRemovalOperationLabel(operation)}`}
195
+ >
196
+ {options.map((option) => (
197
+ <option key={option.value} value={option.value}>{option.label}</option>
198
+ ))}
199
+ </select>
200
+ {selected ? <p className="mt-2 text-xs text-stone-600">{selected.description}</p> : null}
201
+ </div>
202
+ );
203
+ })}
204
+ </div>
205
+ {returnRepricePending ? (
206
+ <p className="mt-3 text-xs font-medium text-sky-800">Recalculating settlement…</p>
207
+ ) : null}
208
+ </section>
209
+ ) : null}
210
+
153
211
  {hasRefundDecisions ? (
154
212
  <section className="rounded-lg border border-amber-300 bg-amber-50 p-3" aria-label="Refund treatment">
155
213
  <div className="mb-3">
156
214
  <h3 className="text-sm font-semibold text-stone-900">Choose refund treatment</h3>
157
215
  <p className="mt-1 text-xs text-stone-600">
158
- Removed paid value is never refunded automatically. Choose an explicit treatment (return-only changes default to no refund), then the server will recalculate the settlement.
216
+ Removed paid value is never refunded automatically. Choose an explicit treatment, then the server will recalculate the settlement.
159
217
  </p>
160
218
  </div>
161
219
  <div className="space-y-3">
@@ -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
- /** Server ops whose removed value is only a return-time substitution. */
31
- const RETURN_ONLY_REFUND_OPERATION_TYPES = new Set(['CHANGE_RETURN']);
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,21 @@ export function releaseRefundDispositionOptions(
37
49
  return RELEASE_OPTIONS.filter((option) => allowed.includes(option.value));
38
50
  }
39
51
 
40
- /**
41
- * True when every refund-decision operation is a return substitution.
42
- * Combined with an unchanged product/outbound selection, this is the common
43
- * "cheaper/later return" admin case that should default to no refund.
44
- */
45
- export function isReturnOnlyRefundDecision(
46
- operations: readonly AdminAmendmentOperationSnapshot[],
47
- ): boolean {
48
- return (
49
- operations.length > 0 &&
50
- operations.every((operation) => RETURN_ONLY_REFUND_OPERATION_TYPES.has(operation.type))
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;
72
- }
73
-
74
- export function buildDefaultRefundDispositionsByOperationId(args: {
75
- operations: readonly AdminAmendmentOperationSnapshot[];
76
- allowedByOperationId?: Readonly<
77
- Record<string, readonly AdminAmendmentRefundDisposition[] | undefined>
78
- >;
79
- sameProductAndOutboundSelection: boolean;
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
- );
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
+ ];
91
67
  }
92
68
 
93
69
  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 } : {}),
@@ -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,10 @@ 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
- preferredAdminRefundDisposition,
22
19
  refundDecisionsComplete,
23
20
  releaseRefundDispositionOptions,
21
+ returnPriceTreatmentOptions,
24
22
  } from '../src/components/booking/admin-refund-disposition';
25
23
  import {
26
24
  applyResourceAdjustments,
@@ -189,61 +187,33 @@ test('admin removal quote preserves explicit release refund decisions for the pr
189
187
  );
190
188
  });
191
189
 
192
- test('admin refund disposition defaults only for return-only no refund', () => {
193
- const changeReturn = {
190
+ test('admin return price treatment defaults to keep floor and maps optional reprice choices', () => {
191
+ const operation = {
194
192
  operationId: 'op_change_return',
195
193
  type: 'CHANGE_RETURN',
196
194
  componentKey: 'return',
197
195
  };
198
- const removeAdult = {
199
- operationId: 'op_remove_adult',
200
- type: 'REMOVE_QUANTITY',
201
- componentKey: 'ticket:ADULT',
202
- quantityDelta: -1,
203
- };
204
-
205
- assert.equal(isReturnOnlyRefundDecision([changeReturn]), true);
206
- assert.equal(isReturnOnlyRefundDecision([changeReturn, removeAdult]), false);
207
- assert.equal(
208
- preferredAdminRefundDisposition({
209
- operations: [changeReturn],
210
- sameProductAndOutboundSelection: true,
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' },
196
+ const mapped = mapAdminChangeBookingQuoteV2Data({
197
+ oldReceipt: { currency: 'CAD', grossSubtotal: 214.48, taxAmount: 18.23, payableTotal: 232.71 },
198
+ newQuote: { currency: 'CAD', payableTotal: 232.71, balanceDelta: 0, amountToCharge: 0, refundCandidate: 0 },
199
+ canApply: true,
200
+ returnPriceTreatmentOperations: [operation],
201
+ allowedRefundDispositionsByOperationId: {
202
+ [operation.operationId]: ['PENDING_REFUND', 'NO_REFUND'],
203
+ },
204
+ });
205
+ const slice = sliceChangeQuoteForUi(
206
+ mapped,
207
+ { total: 232.71, subtotal: 214.48, tax: 18.23 },
208
+ 'CAD',
237
209
  );
210
+
211
+ assert.deepEqual(slice.returnPriceTreatmentOperations, [operation]);
238
212
  assert.deepEqual(
239
- buildDefaultRefundDispositionsByOperationId({
240
- operations: [removeAdult],
241
- sameProductAndOutboundSelection: false,
242
- allowedByOperationId: {
243
- [removeAdult.operationId]: ['PENDING_REFUND', 'NO_REFUND'],
244
- },
245
- }),
246
- {},
213
+ returnPriceTreatmentOptions(
214
+ slice.allowedRefundDispositionsByOperationId?.[operation.operationId],
215
+ ).map((option) => option.value),
216
+ ['KEEP_FLOOR', 'PENDING_REFUND', 'NO_REFUND'],
247
217
  );
248
218
  });
249
219