@ticketboothapp/booking 1.2.131 → 1.2.133

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.131",
3
+ "version": "1.2.133",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -517,6 +517,8 @@ export function AdminChangeBookingFlow({
517
517
  feeLineItems,
518
518
  changeFlowBookedFeeUnitByNormalizedLabel,
519
519
  addOnSelections,
520
+ originalAddOnSelections: initialValues?.addOnSelections ?? null,
521
+ originalProductOptionId: initialValues?.productOptionId ?? null,
520
522
  addOns,
521
523
  adminCustomReceiptLines,
522
524
  quantities,
@@ -528,6 +528,8 @@ export function ChangeBookingFlow({
528
528
  feeLineItems,
529
529
  changeFlowBookedFeeUnitByNormalizedLabel,
530
530
  addOnSelections,
531
+ originalAddOnSelections: initialValues?.addOnSelections ?? null,
532
+ originalProductOptionId: initialValues?.productOptionId ?? null,
531
533
  addOns,
532
534
  quantities,
533
535
  pricing,
@@ -59,6 +59,8 @@ export interface UseAdminChangePriceSummaryParams {
59
59
  feeLineItems: OrderSummaryFeeLine[];
60
60
  changeFlowBookedFeeUnitByNormalizedLabel: Map<string, number>;
61
61
  addOnSelections: AddOnSelection[];
62
+ originalAddOnSelections?: AddOnSelection[] | null;
63
+ originalProductOptionId?: string | null;
62
64
  addOns: AddOn[];
63
65
  adminCustomReceiptLines: AdminCustomReceiptLine[];
64
66
  quantities: Record<string, number>;
@@ -106,6 +108,8 @@ export function useAdminChangePriceSummary({
106
108
  feeLineItems,
107
109
  changeFlowBookedFeeUnitByNormalizedLabel,
108
110
  addOnSelections,
111
+ originalAddOnSelections,
112
+ originalProductOptionId,
109
113
  addOns,
110
114
  adminCustomReceiptLines,
111
115
  quantities,
@@ -138,6 +142,9 @@ export function useAdminChangePriceSummary({
138
142
  effectiveSubtotal,
139
143
  adminCustomAdjustmentTotal,
140
144
  effectiveSubtotalForCheckout,
145
+ promoEligibleSubtotalForCheckout,
146
+ addOnTotal,
147
+ originalAddOnSubtotal,
141
148
  feeLineItemsForChangeFlowDisplay,
142
149
  feeLineItemsWithAddOns,
143
150
  } = useAdminChangeProtectedPricing({
@@ -160,9 +167,14 @@ export function useAdminChangePriceSummary({
160
167
  feeLineItems,
161
168
  changeFlowBookedFeeUnitByNormalizedLabel,
162
169
  addOnSelections,
170
+ originalAddOnSelections,
163
171
  addOns,
164
172
  adminCustomReceiptLines,
165
173
  });
174
+ const promoEligibleSubtotalForCheckoutExcludingCancellation = Math.max(
175
+ 0,
176
+ promoEligibleSubtotalForCheckout - cancellationPolicyFee,
177
+ );
166
178
 
167
179
  const providerPricingUi = flowUi?.providerDashboardChangePricingUi;
168
180
  const providerQuotedLines = useMemo(
@@ -307,6 +319,10 @@ export function useAdminChangePriceSummary({
307
319
  ticketLineItemsForChangeFlowDisplay,
308
320
  feeLineItemsWithAddOns,
309
321
  effectiveSubtotalForCheckout,
322
+ promoEligibleSubtotalForCheckout: promoEligibleSubtotalForCheckoutExcludingCancellation,
323
+ addOnTotal,
324
+ originalAddOnSubtotal,
325
+ originalProductOptionId,
310
326
  lockedPromoCode,
311
327
  originalReceipt,
312
328
  isTaxIncludedInPrice,
@@ -24,12 +24,29 @@ interface UseAdminChangePromoDiscountParams {
24
24
  ticketLineItemsForChangeFlowDisplay: OrderSummaryTicketLine[];
25
25
  feeLineItemsWithAddOns: OrderSummaryFeeLine[];
26
26
  effectiveSubtotalForCheckout: number;
27
+ promoEligibleSubtotalForCheckout?: number;
28
+ addOnTotal?: number;
29
+ originalAddOnSubtotal?: number;
30
+ originalProductOptionId?: string | null;
27
31
  lockedPromoCode: string | null;
28
32
  originalReceipt: ChangeBookingFlowProps['originalReceipt'];
29
33
  isTaxIncludedInPrice: boolean;
30
34
  pricingConfig: PricingConfig | null;
31
35
  }
32
36
 
37
+ function originalReceiptGrossSubtotalBeforePromo(
38
+ originalReceipt: ChangeBookingFlowProps['originalReceipt'],
39
+ ): number {
40
+ const chargeTypes = new Set(['TICKET', 'FEE', 'RETURN_OPTION']);
41
+ return (originalReceipt?.lineItems ?? []).reduce((sum, line) => {
42
+ const amount = Number(line.amount) || 0;
43
+ if (amount <= 0) return sum;
44
+ const type = String(line.type ?? '').trim().toUpperCase();
45
+ if (!chargeTypes.has(type)) return sum;
46
+ return sum + amount;
47
+ }, 0);
48
+ }
49
+
33
50
  export function useAdminChangePromoDiscount({
34
51
  appliedPromoCode,
35
52
  selectedAvailability,
@@ -42,6 +59,10 @@ export function useAdminChangePromoDiscount({
42
59
  ticketLineItemsForChangeFlowDisplay,
43
60
  feeLineItemsWithAddOns,
44
61
  effectiveSubtotalForCheckout,
62
+ promoEligibleSubtotalForCheckout = effectiveSubtotalForCheckout,
63
+ addOnTotal = Math.max(0, effectiveSubtotalForCheckout - promoEligibleSubtotalForCheckout),
64
+ originalAddOnSubtotal = 0,
65
+ originalProductOptionId,
45
66
  lockedPromoCode,
46
67
  originalReceipt,
47
68
  isTaxIncludedInPrice,
@@ -65,20 +86,40 @@ export function useAdminChangePromoDiscount({
65
86
  selectedAvailability: null as Availability | null,
66
87
  ticketLineItems: [] as Array<{ category: string; qty: number; itemTotal: number }>,
67
88
  effectiveSubtotal: 0,
89
+ promoEligibleSubtotal: 0,
68
90
  displayedProductFeeTotal: 0,
69
91
  appliedPromoCode: null as string | null,
70
- changeBookingPromo: null as { priorAmount?: number } | null,
92
+ changeBookingPromo: null as {
93
+ priorAmount?: number;
94
+ originalFullSubtotal?: number;
95
+ originalAddOnSubtotal?: number;
96
+ newFullSubtotal?: number;
97
+ newAddOnSubtotal?: number;
98
+ } | null,
71
99
  });
72
100
 
73
101
  const originalReceiptPromoAdjustment = useMemo(
74
102
  () => deriveOriginalReceiptPromoAdjustment(originalReceipt),
75
103
  [originalReceipt],
76
104
  );
105
+ const originalFullSubtotalBeforePromo = useMemo(
106
+ () => originalReceiptGrossSubtotalBeforePromo(originalReceipt),
107
+ [originalReceipt],
108
+ );
77
109
 
78
110
  const selectedAvailabilityOptionId = useMemo(
79
111
  () => (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''),
80
112
  [selectedAvailability],
81
113
  );
114
+ const includeLegacyPromoRetentionContext = useMemo(() => {
115
+ const originalOptionId = originalProductOptionId?.trim();
116
+ return Boolean(
117
+ lockedPromoCode &&
118
+ originalOptionId &&
119
+ selectedAvailabilityOptionId &&
120
+ selectedAvailabilityOptionId === originalOptionId,
121
+ );
122
+ }, [lockedPromoCode, originalProductOptionId, selectedAvailabilityOptionId]);
82
123
 
83
124
  const promoDiscountFetchKey = useMemo(() => {
84
125
  if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
@@ -100,6 +141,10 @@ export function useAdminChangePromoDiscount({
100
141
  .join('|'),
101
142
  String(Math.round(feeLineItemsWithAddOns.reduce((sum, fee) => sum + fee.totalAmount, 0) * 100)),
102
143
  String(Math.round(effectiveSubtotalForCheckout * 100)),
144
+ String(Math.round(promoEligibleSubtotalForCheckout * 100)),
145
+ String(Math.round(addOnTotal * 100)),
146
+ includeLegacyPromoRetentionContext ? String(Math.round(originalFullSubtotalBeforePromo * 100)) : '',
147
+ includeLegacyPromoRetentionContext ? String(Math.round(originalAddOnSubtotal * 100)) : '',
103
148
  lockedPromoCode ? String(Math.round(originalReceiptPromoAdjustment.amount * 100)) : '',
104
149
  ].join('::');
105
150
  }, [
@@ -115,6 +160,11 @@ export function useAdminChangePromoDiscount({
115
160
  ticketLineItemsForChangeFlowDisplay,
116
161
  feeLineItemsWithAddOns,
117
162
  effectiveSubtotalForCheckout,
163
+ promoEligibleSubtotalForCheckout,
164
+ addOnTotal,
165
+ includeLegacyPromoRetentionContext,
166
+ originalFullSubtotalBeforePromo,
167
+ originalAddOnSubtotal,
118
168
  lockedPromoCode,
119
169
  originalReceiptPromoAdjustment.amount,
120
170
  ]);
@@ -127,10 +177,17 @@ export function useAdminChangePromoDiscount({
127
177
  itemTotal: line.itemTotal,
128
178
  })),
129
179
  effectiveSubtotal: effectiveSubtotalForCheckout,
180
+ promoEligibleSubtotal: promoEligibleSubtotalForCheckout,
130
181
  displayedProductFeeTotal: feeLineItemsWithAddOns.reduce((sum, fee) => sum + fee.totalAmount, 0),
131
182
  appliedPromoCode,
132
183
  changeBookingPromo: lockedPromoCode
133
- ? { priorAmount: originalReceiptPromoAdjustment.amount }
184
+ ? {
185
+ priorAmount: originalReceiptPromoAdjustment.amount,
186
+ originalFullSubtotal: includeLegacyPromoRetentionContext ? originalFullSubtotalBeforePromo : undefined,
187
+ originalAddOnSubtotal: includeLegacyPromoRetentionContext ? originalAddOnSubtotal : undefined,
188
+ newFullSubtotal: includeLegacyPromoRetentionContext ? effectiveSubtotalForCheckout : undefined,
189
+ newAddOnSubtotal: includeLegacyPromoRetentionContext ? addOnTotal : undefined,
190
+ }
134
191
  : null,
135
192
  };
136
193
 
@@ -151,6 +208,7 @@ export function useAdminChangePromoDiscount({
151
208
  ticketLineItems: lines,
152
209
  effectiveSubtotal: sub,
153
210
  displayedProductFeeTotal,
211
+ promoEligibleSubtotal: promoSub,
154
212
  appliedPromoCode: code,
155
213
  changeBookingPromo,
156
214
  } = promoDiscountParamsRef.current;
@@ -177,10 +235,16 @@ export function useAdminChangePromoDiscount({
177
235
  priorPromoDiscountAmount: changeBookingPromo.priorAmount,
178
236
  displayedTicketLines: lines,
179
237
  displayedProductFeeTotal,
238
+ promoEligibleSubtotal: promoSub,
239
+ legacyPromoOriginalFullSubtotal: changeBookingPromo.originalFullSubtotal,
240
+ legacyPromoOriginalAddOnSubtotal: changeBookingPromo.originalAddOnSubtotal,
241
+ legacyPromoNewFullSubtotal: changeBookingPromo.newFullSubtotal,
242
+ legacyPromoNewAddOnSubtotal: changeBookingPromo.newAddOnSubtotal,
180
243
  }
181
244
  : {
182
245
  displayedTicketLines: lines,
183
246
  displayedProductFeeTotal,
247
+ promoEligibleSubtotal: promoSub,
184
248
  },
185
249
  )
186
250
  .then((res) => {
@@ -17,6 +17,23 @@ import { normalizeLineLabelForCompare } from './change-booking-flow-helpers';
17
17
  type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
18
18
  type AdminCustomReceiptLine = { label: string; amountInput: string; amountSign?: number };
19
19
 
20
+ function addOnSelectionTotal(selections: AddOnSelection[] | null | undefined, addOns: AddOn[]): number {
21
+ let sum = 0;
22
+ for (const sel of selections ?? []) {
23
+ const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
24
+ if (!addOn) continue;
25
+ const basePrice = addOn.price ?? 0;
26
+ const hasVariant =
27
+ (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
28
+ sel.variantId;
29
+ const variantAdjustment = hasVariant
30
+ ? (addOn.variants?.find((v) => v.id === sel.variantId)?.priceAdjustment ?? 0)
31
+ : 0;
32
+ sum += (basePrice + variantAdjustment) * (sel.quantity ?? 1);
33
+ }
34
+ return sum;
35
+ }
36
+
20
37
  interface UseAdminChangeProtectedPricingParams {
21
38
  selectedAvailability: Availability | null;
22
39
  selectedReturnOption: ReturnOption | null;
@@ -37,6 +54,7 @@ interface UseAdminChangeProtectedPricingParams {
37
54
  feeLineItems: OrderSummaryFeeLine[];
38
55
  changeFlowBookedFeeUnitByNormalizedLabel: Map<string, number>;
39
56
  addOnSelections: AddOnSelection[];
57
+ originalAddOnSelections?: AddOnSelection[] | null;
40
58
  addOns: AddOn[];
41
59
  adminCustomReceiptLines: AdminCustomReceiptLine[];
42
60
  }
@@ -61,25 +79,18 @@ export function useAdminChangeProtectedPricing({
61
79
  feeLineItems,
62
80
  changeFlowBookedFeeUnitByNormalizedLabel,
63
81
  addOnSelections,
82
+ originalAddOnSelections,
64
83
  addOns,
65
84
  adminCustomReceiptLines,
66
85
  }: UseAdminChangeProtectedPricingParams) {
67
- const addOnTotal = useMemo(() => {
68
- let sum = 0;
69
- for (const sel of addOnSelections) {
70
- const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
71
- if (!addOn) continue;
72
- const basePrice = addOn.price ?? 0;
73
- const hasVariant =
74
- (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
75
- sel.variantId;
76
- const variantAdjustment = hasVariant
77
- ? (addOn.variants?.find((v) => v.id === sel.variantId)?.priceAdjustment ?? 0)
78
- : 0;
79
- sum += (basePrice + variantAdjustment) * (sel.quantity ?? 1);
80
- }
81
- return sum;
82
- }, [addOnSelections, addOns]);
86
+ const addOnTotal = useMemo(
87
+ () => addOnSelectionTotal(addOnSelections, addOns),
88
+ [addOnSelections, addOns],
89
+ );
90
+ const originalAddOnSubtotal = useMemo(
91
+ () => addOnSelectionTotal(originalAddOnSelections, addOns),
92
+ [originalAddOnSelections, addOns],
93
+ );
83
94
 
84
95
  const currentTicketSubtotal = useMemo(
85
96
  () => ticketLineItems.reduce((sum, line) => sum + Math.max(0, Number(line.itemTotal) || 0), 0),
@@ -215,6 +226,7 @@ export function useAdminChangeProtectedPricing({
215
226
  [adminCustomReceiptLines],
216
227
  );
217
228
  const effectiveSubtotalForCheckout = roundMoney(effectiveSubtotal + adminCustomAdjustmentTotal);
229
+ const promoEligibleSubtotalForCheckout = roundMoney(effectiveSubtotalBeforeAddOns + adminCustomAdjustmentTotal);
218
230
 
219
231
  const feeLineItemsForChangeFlowDisplay = useMemo(() => {
220
232
  if (!changeFlowApplyReceiptPaidFloors || totalQuantity <= 0) return feeLineItems;
@@ -274,6 +286,9 @@ export function useAdminChangeProtectedPricing({
274
286
  effectiveSubtotal,
275
287
  adminCustomAdjustmentTotal,
276
288
  effectiveSubtotalForCheckout,
289
+ promoEligibleSubtotalForCheckout,
290
+ addOnTotal,
291
+ originalAddOnSubtotal,
277
292
  feeLineItemsForChangeFlowDisplay,
278
293
  feeLineItemsWithAddOns,
279
294
  };
@@ -48,6 +48,8 @@ interface UseChangeBookingPriceSummaryParams {
48
48
  feeLineItems: OrderSummaryFeeLine[];
49
49
  changeFlowBookedFeeUnitByNormalizedLabel: Map<string, number>;
50
50
  addOnSelections: AddOnSelection[];
51
+ originalAddOnSelections?: AddOnSelection[] | null;
52
+ originalProductOptionId?: string | null;
51
53
  addOns: AddOn[];
52
54
  quantities: Record<string, number>;
53
55
  pricing: PricingRate[];
@@ -88,6 +90,8 @@ export function useChangeBookingPriceSummary({
88
90
  feeLineItems,
89
91
  changeFlowBookedFeeUnitByNormalizedLabel,
90
92
  addOnSelections,
93
+ originalAddOnSelections,
94
+ originalProductOptionId,
91
95
  addOns,
92
96
  quantities,
93
97
  pricing,
@@ -113,6 +117,9 @@ export function useChangeBookingPriceSummary({
113
117
  ticketLineItemsForChangeFlowDisplay,
114
118
  effectiveSubtotal,
115
119
  feeLineItemsWithAddOns,
120
+ promoEligibleSubtotal,
121
+ addOnTotal,
122
+ originalAddOnSubtotal,
116
123
  } = useChangeBookingProtectedPricing({
117
124
  selectedAvailability,
118
125
  selectedReturnOption,
@@ -132,8 +139,13 @@ export function useChangeBookingPriceSummary({
132
139
  feeLineItems,
133
140
  changeFlowBookedFeeUnitByNormalizedLabel,
134
141
  addOnSelections,
142
+ originalAddOnSelections,
135
143
  addOns,
136
144
  });
145
+ const promoEligibleSubtotalExcludingCancellation = Math.max(
146
+ 0,
147
+ promoEligibleSubtotal - cancellationPolicyFee,
148
+ );
137
149
 
138
150
  const {
139
151
  effectivePromoDiscountAmount,
@@ -151,6 +163,10 @@ export function useChangeBookingPriceSummary({
151
163
  ticketLineItemsForChangeFlowDisplay,
152
164
  feeLineItems,
153
165
  effectiveSubtotal,
166
+ promoEligibleSubtotal: promoEligibleSubtotalExcludingCancellation,
167
+ addOnTotal,
168
+ originalAddOnSubtotal,
169
+ originalProductOptionId,
154
170
  lockedPromoCode,
155
171
  originalReceipt,
156
172
  isTaxIncludedInPrice,
@@ -21,12 +21,29 @@ interface UseChangeBookingPromoDiscountParams {
21
21
  ticketLineItemsForChangeFlowDisplay: OrderSummaryTicketLine[];
22
22
  feeLineItems: OrderSummaryFeeLine[];
23
23
  effectiveSubtotal: number;
24
+ promoEligibleSubtotal?: number;
25
+ addOnTotal?: number;
26
+ originalAddOnSubtotal?: number;
27
+ originalProductOptionId?: string | null;
24
28
  lockedPromoCode: string | null;
25
29
  originalReceipt: ChangeBookingFlowProps['originalReceipt'];
26
30
  isTaxIncludedInPrice: boolean;
27
31
  pricingConfig: PricingConfig | null;
28
32
  }
29
33
 
34
+ function originalReceiptGrossSubtotalBeforePromo(
35
+ originalReceipt: ChangeBookingFlowProps['originalReceipt'],
36
+ ): number {
37
+ const chargeTypes = new Set(['TICKET', 'FEE', 'RETURN_OPTION']);
38
+ return (originalReceipt?.lineItems ?? []).reduce((sum, line) => {
39
+ const amount = Number(line.amount) || 0;
40
+ if (amount <= 0) return sum;
41
+ const type = String(line.type ?? '').trim().toUpperCase();
42
+ if (!chargeTypes.has(type)) return sum;
43
+ return sum + amount;
44
+ }, 0);
45
+ }
46
+
30
47
  export function useChangeBookingPromoDiscount({
31
48
  appliedPromoCode,
32
49
  selectedAvailability,
@@ -39,6 +56,10 @@ export function useChangeBookingPromoDiscount({
39
56
  ticketLineItemsForChangeFlowDisplay,
40
57
  feeLineItems,
41
58
  effectiveSubtotal,
59
+ promoEligibleSubtotal = effectiveSubtotal,
60
+ addOnTotal = Math.max(0, effectiveSubtotal - promoEligibleSubtotal),
61
+ originalAddOnSubtotal = 0,
62
+ originalProductOptionId,
42
63
  lockedPromoCode,
43
64
  originalReceipt,
44
65
  isTaxIncludedInPrice,
@@ -62,15 +83,36 @@ export function useChangeBookingPromoDiscount({
62
83
  selectedAvailability: null as Availability | null,
63
84
  ticketLineItems: [] as Array<{ category: string; qty: number; itemTotal: number }>,
64
85
  effectiveSubtotal: 0,
86
+ promoEligibleSubtotal: 0,
65
87
  displayedProductFeeTotal: 0,
66
88
  appliedPromoCode: null as string | null,
67
- changeBookingPromo: null as { priorAmount?: number } | null,
89
+ changeBookingPromo: null as {
90
+ priorAmount?: number;
91
+ originalFullSubtotal?: number;
92
+ originalAddOnSubtotal?: number;
93
+ newFullSubtotal?: number;
94
+ newAddOnSubtotal?: number;
95
+ } | null,
68
96
  });
69
97
 
98
+ const originalFullSubtotalBeforePromo = useMemo(
99
+ () => originalReceiptGrossSubtotalBeforePromo(originalReceipt),
100
+ [originalReceipt],
101
+ );
102
+
70
103
  const selectedAvailabilityOptionId = useMemo(
71
104
  () => (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''),
72
105
  [selectedAvailability],
73
106
  );
107
+ const includeLegacyPromoRetentionContext = useMemo(() => {
108
+ const originalOptionId = originalProductOptionId?.trim();
109
+ return Boolean(
110
+ lockedPromoCode &&
111
+ originalOptionId &&
112
+ selectedAvailabilityOptionId &&
113
+ selectedAvailabilityOptionId === originalOptionId,
114
+ );
115
+ }, [lockedPromoCode, originalProductOptionId, selectedAvailabilityOptionId]);
74
116
 
75
117
  const promoDiscountFetchKey = useMemo(() => {
76
118
  if (!appliedPromoCode || !selectedAvailability || totalQuantity === 0) return '';
@@ -92,6 +134,10 @@ export function useChangeBookingPromoDiscount({
92
134
  .join('|'),
93
135
  String(Math.round(feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0) * 100)),
94
136
  String(Math.round(effectiveSubtotal * 100)),
137
+ String(Math.round(promoEligibleSubtotal * 100)),
138
+ String(Math.round(addOnTotal * 100)),
139
+ includeLegacyPromoRetentionContext ? String(Math.round(originalFullSubtotalBeforePromo * 100)) : '',
140
+ includeLegacyPromoRetentionContext ? String(Math.round(originalAddOnSubtotal * 100)) : '',
95
141
  lockedPromoCode ? String(Math.round((originalReceipt?.promoAmount ?? 0) * 100)) : '',
96
142
  ].join('::');
97
143
  }, [
@@ -107,6 +153,11 @@ export function useChangeBookingPromoDiscount({
107
153
  ticketLineItemsForChangeFlowDisplay,
108
154
  feeLineItems,
109
155
  effectiveSubtotal,
156
+ promoEligibleSubtotal,
157
+ addOnTotal,
158
+ includeLegacyPromoRetentionContext,
159
+ originalFullSubtotalBeforePromo,
160
+ originalAddOnSubtotal,
110
161
  lockedPromoCode,
111
162
  originalReceipt?.promoAmount,
112
163
  ]);
@@ -119,10 +170,17 @@ export function useChangeBookingPromoDiscount({
119
170
  itemTotal: line.itemTotal,
120
171
  })),
121
172
  effectiveSubtotal,
173
+ promoEligibleSubtotal,
122
174
  displayedProductFeeTotal: feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0),
123
175
  appliedPromoCode,
124
176
  changeBookingPromo: lockedPromoCode
125
- ? { priorAmount: originalReceipt?.promoAmount }
177
+ ? {
178
+ priorAmount: originalReceipt?.promoAmount,
179
+ originalFullSubtotal: includeLegacyPromoRetentionContext ? originalFullSubtotalBeforePromo : undefined,
180
+ originalAddOnSubtotal: includeLegacyPromoRetentionContext ? originalAddOnSubtotal : undefined,
181
+ newFullSubtotal: includeLegacyPromoRetentionContext ? effectiveSubtotal : undefined,
182
+ newAddOnSubtotal: includeLegacyPromoRetentionContext ? addOnTotal : undefined,
183
+ }
126
184
  : null,
127
185
  };
128
186
 
@@ -143,6 +201,7 @@ export function useChangeBookingPromoDiscount({
143
201
  ticketLineItems: lines,
144
202
  effectiveSubtotal: sub,
145
203
  displayedProductFeeTotal,
204
+ promoEligibleSubtotal: promoSub,
146
205
  appliedPromoCode: code,
147
206
  changeBookingPromo,
148
207
  } = promoDiscountParamsRef.current;
@@ -169,10 +228,16 @@ export function useChangeBookingPromoDiscount({
169
228
  priorPromoDiscountAmount: changeBookingPromo.priorAmount,
170
229
  displayedTicketLines: lines,
171
230
  displayedProductFeeTotal,
231
+ promoEligibleSubtotal: promoSub,
232
+ legacyPromoOriginalFullSubtotal: changeBookingPromo.originalFullSubtotal,
233
+ legacyPromoOriginalAddOnSubtotal: changeBookingPromo.originalAddOnSubtotal,
234
+ legacyPromoNewFullSubtotal: changeBookingPromo.newFullSubtotal,
235
+ legacyPromoNewAddOnSubtotal: changeBookingPromo.newAddOnSubtotal,
172
236
  }
173
237
  : {
174
238
  displayedTicketLines: lines,
175
239
  displayedProductFeeTotal,
240
+ promoEligibleSubtotal: promoSub,
176
241
  },
177
242
  )
178
243
  .then((res) => {
@@ -15,6 +15,23 @@ import { normalizeLineLabelForCompare } from './change-booking-flow-helpers';
15
15
 
16
16
  type AddOnSelection = { addOnId: string; variantId?: string; quantity?: number };
17
17
 
18
+ function addOnSelectionTotal(selections: AddOnSelection[] | null | undefined, addOns: AddOn[]): number {
19
+ let sum = 0;
20
+ for (const sel of selections ?? []) {
21
+ const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
22
+ if (!addOn) continue;
23
+ const basePrice = addOn.price ?? 0;
24
+ const hasVariant =
25
+ (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
26
+ sel.variantId;
27
+ const variantAdjustment = hasVariant
28
+ ? (addOn.variants?.find((v) => v.id === sel.variantId)?.priceAdjustment ?? 0)
29
+ : 0;
30
+ sum += (basePrice + variantAdjustment) * (sel.quantity ?? 1);
31
+ }
32
+ return sum;
33
+ }
34
+
18
35
  interface UseChangeBookingProtectedPricingParams {
19
36
  selectedAvailability: Availability | null;
20
37
  selectedReturnOption: ReturnOption | null;
@@ -34,6 +51,7 @@ interface UseChangeBookingProtectedPricingParams {
34
51
  feeLineItems: OrderSummaryFeeLine[];
35
52
  changeFlowBookedFeeUnitByNormalizedLabel: Map<string, number>;
36
53
  addOnSelections: AddOnSelection[];
54
+ originalAddOnSelections?: AddOnSelection[] | null;
37
55
  addOns: AddOn[];
38
56
  }
39
57
 
@@ -56,6 +74,7 @@ export function useChangeBookingProtectedPricing({
56
74
  feeLineItems,
57
75
  changeFlowBookedFeeUnitByNormalizedLabel,
58
76
  addOnSelections,
77
+ originalAddOnSelections,
59
78
  addOns,
60
79
  }: UseChangeBookingProtectedPricingParams) {
61
80
  const changeFlowProtectedTicketSubtotal = useMemo(() => {
@@ -87,22 +106,14 @@ export function useChangeBookingProtectedPricing({
87
106
  changeFlowProtectedReceiptPricing,
88
107
  ]);
89
108
 
90
- const addOnTotal = useMemo(() => {
91
- let sum = 0;
92
- for (const sel of addOnSelections) {
93
- const addOn = addOns.find((a) => a.addOnId === sel.addOnId);
94
- if (!addOn) continue;
95
- const basePrice = addOn.price ?? 0;
96
- const hasVariant =
97
- (addOn.variantType === 'single_choice' || addOn.variantType === 'multi_quantity') &&
98
- sel.variantId;
99
- const variantAdjustment = hasVariant
100
- ? (addOn.variants?.find((v) => v.id === sel.variantId)?.priceAdjustment ?? 0)
101
- : 0;
102
- sum += (basePrice + variantAdjustment) * (sel.quantity ?? 1);
103
- }
104
- return sum;
105
- }, [addOnSelections, addOns]);
109
+ const addOnTotal = useMemo(
110
+ () => addOnSelectionTotal(addOnSelections, addOns),
111
+ [addOnSelections, addOns],
112
+ );
113
+ const originalAddOnSubtotal = useMemo(
114
+ () => addOnSelectionTotal(originalAddOnSelections, addOns),
115
+ [originalAddOnSelections, addOns],
116
+ );
106
117
 
107
118
  const currentTicketSubtotal = useMemo(
108
119
  () => ticketLineItems.reduce((sum, line) => sum + Math.max(0, Number(line.itemTotal) || 0), 0),
@@ -220,6 +231,7 @@ export function useChangeBookingProtectedPricing({
220
231
  changeFlowProtectedReturnAdjustment
221
232
  : subtotal;
222
233
  const effectiveSubtotal = effectiveSubtotalBeforeAddOns + addOnTotal;
234
+ const promoEligibleSubtotal = effectiveSubtotalBeforeAddOns;
223
235
 
224
236
  const feeLineItemsWithAddOns = useMemo(() => {
225
237
  const addOnLines = addOnSelections
@@ -251,6 +263,9 @@ export function useChangeBookingProtectedPricing({
251
263
  checkoutReturnLineAmount,
252
264
  ticketLineItemsForChangeFlowDisplay,
253
265
  effectiveSubtotal,
266
+ promoEligibleSubtotal,
267
+ addOnTotal,
268
+ originalAddOnSubtotal,
254
269
  feeLineItemsWithAddOns,
255
270
  };
256
271
  }
@@ -246,6 +246,7 @@ export function useStandardBookingPriceSummary({
246
246
  const checkoutReturnLineAmount = returnPriceAdjustment;
247
247
  const effectiveSubtotalBeforeAddOns = subtotal;
248
248
  const effectiveSubtotal = effectiveSubtotalBeforeAddOns + addOnTotal;
249
+ const promoEligibleSubtotal = Math.max(0, effectiveSubtotalBeforeAddOns - cancellationPolicyFee);
249
250
 
250
251
  const feeLineItemsWithAddOns = useMemo(() => {
251
252
  const addOnLines = addOnSelections
@@ -373,6 +374,7 @@ export function useStandardBookingPriceSummary({
373
374
  ticketLineItems,
374
375
  feeLineItems,
375
376
  effectiveSubtotal,
377
+ promoEligibleSubtotal,
376
378
  isTaxIncludedInPrice,
377
379
  pricingConfig,
378
380
  });
@@ -25,6 +25,7 @@ interface UseStandardBookingPromoDiscountParams {
25
25
  ticketLineItems: OrderSummaryTicketLine[];
26
26
  feeLineItems: OrderSummaryFeeLine[];
27
27
  effectiveSubtotal: number;
28
+ promoEligibleSubtotal?: number;
28
29
  isTaxIncludedInPrice: boolean;
29
30
  pricingConfig: PricingConfig | null;
30
31
  }
@@ -41,6 +42,7 @@ export function useStandardBookingPromoDiscount({
41
42
  ticketLineItems,
42
43
  feeLineItems,
43
44
  effectiveSubtotal,
45
+ promoEligibleSubtotal = effectiveSubtotal,
44
46
  isTaxIncludedInPrice,
45
47
  pricingConfig,
46
48
  }: UseStandardBookingPromoDiscountParams) {
@@ -62,6 +64,7 @@ export function useStandardBookingPromoDiscount({
62
64
  selectedAvailability: null as Availability | null,
63
65
  ticketLineItems: [] as Array<{ category: string; qty: number; itemTotal: number }>,
64
66
  effectiveSubtotal: 0,
67
+ promoEligibleSubtotal: 0,
65
68
  displayedProductFeeTotal: 0,
66
69
  appliedPromoCode: null as string | null,
67
70
  });
@@ -91,6 +94,7 @@ export function useStandardBookingPromoDiscount({
91
94
  .join('|'),
92
95
  String(Math.round(feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0) * 100)),
93
96
  String(Math.round(effectiveSubtotal * 100)),
97
+ String(Math.round(promoEligibleSubtotal * 100)),
94
98
  ].join('::');
95
99
  }, [
96
100
  appliedPromoCode,
@@ -105,6 +109,7 @@ export function useStandardBookingPromoDiscount({
105
109
  ticketLineItems,
106
110
  feeLineItems,
107
111
  effectiveSubtotal,
112
+ promoEligibleSubtotal,
108
113
  ]);
109
114
 
110
115
  promoDiscountParamsRef.current = {
@@ -115,6 +120,7 @@ export function useStandardBookingPromoDiscount({
115
120
  itemTotal: line.itemTotal,
116
121
  })),
117
122
  effectiveSubtotal,
123
+ promoEligibleSubtotal,
118
124
  displayedProductFeeTotal: feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0),
119
125
  appliedPromoCode,
120
126
  };
@@ -136,6 +142,7 @@ export function useStandardBookingPromoDiscount({
136
142
  ticketLineItems: lines,
137
143
  effectiveSubtotal: sub,
138
144
  displayedProductFeeTotal,
145
+ promoEligibleSubtotal: promoSub,
139
146
  appliedPromoCode: code,
140
147
  } = promoDiscountParamsRef.current;
141
148
  if (!code || !sel) return;
@@ -160,6 +167,7 @@ export function useStandardBookingPromoDiscount({
160
167
  totalCapacity: sel.totalCapacity,
161
168
  displayedTicketLines: lines,
162
169
  displayedProductFeeTotal,
170
+ promoEligibleSubtotal: promoSub,
163
171
  },
164
172
  )
165
173
  .then((res) => {
@@ -980,6 +980,12 @@ export interface GetPromoDiscountBookingChangeOptions {
980
980
  displayedTicketLines?: Array<{ category: string; qty: number; itemTotal: number }>;
981
981
  /** Displayed product fee total from the cart, excluding add-ons, return, and cancellation lines. */
982
982
  displayedProductFeeTotal?: number;
983
+ /** Promo-eligible subtotal excluding add-ons; normal promos use this, gift cards/vouchers use subtotal. */
984
+ promoEligibleSubtotal?: number;
985
+ legacyPromoOriginalFullSubtotal?: number;
986
+ legacyPromoOriginalAddOnSubtotal?: number;
987
+ legacyPromoNewFullSubtotal?: number;
988
+ legacyPromoNewAddOnSubtotal?: number;
983
989
  }
984
990
 
985
991
  export async function getPromoDiscount(
@@ -1004,6 +1010,13 @@ export async function getPromoDiscount(
1004
1010
  });
1005
1011
  if (dateTime) params.set('dateTime', dateTime);
1006
1012
  if (subtotal != null && subtotal > 0) params.set('subtotal', String(subtotal));
1013
+ if (
1014
+ bookingChange?.promoEligibleSubtotal != null &&
1015
+ Number.isFinite(bookingChange.promoEligibleSubtotal) &&
1016
+ bookingChange.promoEligibleSubtotal >= 0
1017
+ ) {
1018
+ params.set('promoEligibleSubtotal', String(bookingChange.promoEligibleSubtotal));
1019
+ }
1007
1020
  if (
1008
1021
  bookingChange?.availabilityCount != null &&
1009
1022
  Number.isFinite(bookingChange.availabilityCount)
@@ -1039,6 +1052,34 @@ export async function getPromoDiscount(
1039
1052
  ) {
1040
1053
  params.set('priorPromoDiscountAmount', String(bookingChange.priorPromoDiscountAmount));
1041
1054
  }
1055
+ if (
1056
+ bookingChange?.legacyPromoOriginalFullSubtotal != null &&
1057
+ Number.isFinite(bookingChange.legacyPromoOriginalFullSubtotal) &&
1058
+ bookingChange.legacyPromoOriginalFullSubtotal >= 0
1059
+ ) {
1060
+ params.set('legacyPromoOriginalFullSubtotal', String(bookingChange.legacyPromoOriginalFullSubtotal));
1061
+ }
1062
+ if (
1063
+ bookingChange?.legacyPromoOriginalAddOnSubtotal != null &&
1064
+ Number.isFinite(bookingChange.legacyPromoOriginalAddOnSubtotal) &&
1065
+ bookingChange.legacyPromoOriginalAddOnSubtotal >= 0
1066
+ ) {
1067
+ params.set('legacyPromoOriginalAddOnSubtotal', String(bookingChange.legacyPromoOriginalAddOnSubtotal));
1068
+ }
1069
+ if (
1070
+ bookingChange?.legacyPromoNewFullSubtotal != null &&
1071
+ Number.isFinite(bookingChange.legacyPromoNewFullSubtotal) &&
1072
+ bookingChange.legacyPromoNewFullSubtotal >= 0
1073
+ ) {
1074
+ params.set('legacyPromoNewFullSubtotal', String(bookingChange.legacyPromoNewFullSubtotal));
1075
+ }
1076
+ if (
1077
+ bookingChange?.legacyPromoNewAddOnSubtotal != null &&
1078
+ Number.isFinite(bookingChange.legacyPromoNewAddOnSubtotal) &&
1079
+ bookingChange.legacyPromoNewAddOnSubtotal >= 0
1080
+ ) {
1081
+ params.set('legacyPromoNewAddOnSubtotal', String(bookingChange.legacyPromoNewAddOnSubtotal));
1082
+ }
1042
1083
  const res = await fetchBookingGetWithRetry(`${API_BASE}/1/get-promo-discount?${params}`);
1043
1084
  if (!res.ok) {
1044
1085
  const err = await res.json();