@ticketboothapp/booking 1.2.130 → 1.2.132

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.130",
3
+ "version": "1.2.132",
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,6 +167,7 @@ export function useAdminChangePriceSummary({
160
167
  feeLineItems,
161
168
  changeFlowBookedFeeUnitByNormalizedLabel,
162
169
  addOnSelections,
170
+ originalAddOnSelections,
163
171
  addOns,
164
172
  adminCustomReceiptLines,
165
173
  });
@@ -307,6 +315,10 @@ export function useAdminChangePriceSummary({
307
315
  ticketLineItemsForChangeFlowDisplay,
308
316
  feeLineItemsWithAddOns,
309
317
  effectiveSubtotalForCheckout,
318
+ promoEligibleSubtotalForCheckout,
319
+ addOnTotal,
320
+ originalAddOnSubtotal,
321
+ originalProductOptionId,
310
322
  lockedPromoCode,
311
323
  originalReceipt,
312
324
  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', 'CANCELLATION_UPGRADE']);
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,6 +139,7 @@ export function useChangeBookingPriceSummary({
132
139
  feeLineItems,
133
140
  changeFlowBookedFeeUnitByNormalizedLabel,
134
141
  addOnSelections,
142
+ originalAddOnSelections,
135
143
  addOns,
136
144
  });
137
145
 
@@ -151,6 +159,10 @@ export function useChangeBookingPriceSummary({
151
159
  ticketLineItemsForChangeFlowDisplay,
152
160
  feeLineItems,
153
161
  effectiveSubtotal,
162
+ promoEligibleSubtotal,
163
+ addOnTotal,
164
+ originalAddOnSubtotal,
165
+ originalProductOptionId,
154
166
  lockedPromoCode,
155
167
  originalReceipt,
156
168
  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', 'CANCELLATION_UPGRADE']);
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
  }
@@ -112,10 +112,21 @@ export function useStandardBookingPriceSummary({
112
112
  return Math.max(0, selectedReturnOption.vacancies ?? 0);
113
113
  }, [selectedReturnOption]);
114
114
 
115
+ const pricingAvailability = useMemo(() => {
116
+ if (!selectedAvailability || !selectedReturnOption?.rates?.length) return selectedAvailability;
117
+ const outboundVacancies = Math.max(0, selectedAvailability.vacancies ?? 0);
118
+ const returnVacancies = Math.max(0, selectedReturnOption.vacancies ?? 0);
119
+ return {
120
+ ...selectedAvailability,
121
+ vacancies: Math.min(outboundVacancies, returnVacancies),
122
+ rates: selectedReturnOption.rates,
123
+ };
124
+ }, [selectedAvailability, selectedReturnOption?.rates, selectedReturnOption?.vacancies]);
125
+
115
126
  const pricing = useMemo(
116
127
  () =>
117
128
  buildPricingFromAvailability(
118
- selectedAvailability,
129
+ pricingAvailability,
119
130
  activeOptions,
120
131
  precomputedPricesByOption,
121
132
  currency,
@@ -124,7 +135,7 @@ export function useStandardBookingPriceSummary({
124
135
  isSimplifiedPricingView,
125
136
  ),
126
137
  [
127
- selectedAvailability,
138
+ pricingAvailability,
128
139
  activeOptions,
129
140
  precomputedPricesByOption,
130
141
  currency,
@@ -145,7 +156,7 @@ export function useStandardBookingPriceSummary({
145
156
  const selectedOption = activeOptions.find(
146
157
  (option) =>
147
158
  option.optionId ===
148
- (selectedAvailability ? getAvailabilityOptionId(selectedAvailability) : ''),
159
+ (pricingAvailability ? getAvailabilityOptionId(pricingAvailability) : ''),
149
160
  );
150
161
  const basePriceCAD = selectedOption?.pricing?.[category.toUpperCase()] ?? 0;
151
162
  return computePriceBreakdown(
@@ -160,7 +171,7 @@ export function useStandardBookingPriceSummary({
160
171
  isSimplifiedPricingView,
161
172
  );
162
173
  },
163
- [pricingConfig, activeOptions, selectedAvailability, currency, hasFees, isSimplifiedPricingView],
174
+ [pricingConfig, activeOptions, pricingAvailability, currency, hasFees, isSimplifiedPricingView],
164
175
  );
165
176
 
166
177
  const orderSummary = useMemo(
@@ -235,6 +246,7 @@ export function useStandardBookingPriceSummary({
235
246
  const checkoutReturnLineAmount = returnPriceAdjustment;
236
247
  const effectiveSubtotalBeforeAddOns = subtotal;
237
248
  const effectiveSubtotal = effectiveSubtotalBeforeAddOns + addOnTotal;
249
+ const promoEligibleSubtotal = effectiveSubtotalBeforeAddOns;
238
250
 
239
251
  const feeLineItemsWithAddOns = useMemo(() => {
240
252
  const addOnLines = addOnSelections
@@ -352,7 +364,7 @@ export function useStandardBookingPriceSummary({
352
364
  totalPrice,
353
365
  } = useStandardBookingPromoDiscount({
354
366
  appliedPromoCode,
355
- selectedAvailability,
367
+ selectedAvailability: pricingAvailability,
356
368
  totalQuantity,
357
369
  productCompanyId,
358
370
  envCompanyId,
@@ -362,6 +374,7 @@ export function useStandardBookingPriceSummary({
362
374
  ticketLineItems,
363
375
  feeLineItems,
364
376
  effectiveSubtotal,
377
+ promoEligibleSubtotal,
365
378
  isTaxIncludedInPrice,
366
379
  pricingConfig,
367
380
  });
@@ -11,6 +11,8 @@ import type {
11
11
  import type { Currency } from './CurrencySwitcher';
12
12
  import { getAvailabilityOptionId } from './standard-booking-availability';
13
13
 
14
+ const normalizeMoneyZero = (value: number) => (Math.abs(value) < 0.005 ? 0 : value);
15
+
14
16
  interface UseStandardBookingPromoDiscountParams {
15
17
  appliedPromoCode: string | null;
16
18
  selectedAvailability: Availability | null;
@@ -23,6 +25,7 @@ interface UseStandardBookingPromoDiscountParams {
23
25
  ticketLineItems: OrderSummaryTicketLine[];
24
26
  feeLineItems: OrderSummaryFeeLine[];
25
27
  effectiveSubtotal: number;
28
+ promoEligibleSubtotal?: number;
26
29
  isTaxIncludedInPrice: boolean;
27
30
  pricingConfig: PricingConfig | null;
28
31
  }
@@ -39,6 +42,7 @@ export function useStandardBookingPromoDiscount({
39
42
  ticketLineItems,
40
43
  feeLineItems,
41
44
  effectiveSubtotal,
45
+ promoEligibleSubtotal = effectiveSubtotal,
42
46
  isTaxIncludedInPrice,
43
47
  pricingConfig,
44
48
  }: UseStandardBookingPromoDiscountParams) {
@@ -60,6 +64,7 @@ export function useStandardBookingPromoDiscount({
60
64
  selectedAvailability: null as Availability | null,
61
65
  ticketLineItems: [] as Array<{ category: string; qty: number; itemTotal: number }>,
62
66
  effectiveSubtotal: 0,
67
+ promoEligibleSubtotal: 0,
63
68
  displayedProductFeeTotal: 0,
64
69
  appliedPromoCode: null as string | null,
65
70
  });
@@ -89,6 +94,7 @@ export function useStandardBookingPromoDiscount({
89
94
  .join('|'),
90
95
  String(Math.round(feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0) * 100)),
91
96
  String(Math.round(effectiveSubtotal * 100)),
97
+ String(Math.round(promoEligibleSubtotal * 100)),
92
98
  ].join('::');
93
99
  }, [
94
100
  appliedPromoCode,
@@ -103,6 +109,7 @@ export function useStandardBookingPromoDiscount({
103
109
  ticketLineItems,
104
110
  feeLineItems,
105
111
  effectiveSubtotal,
112
+ promoEligibleSubtotal,
106
113
  ]);
107
114
 
108
115
  promoDiscountParamsRef.current = {
@@ -113,6 +120,7 @@ export function useStandardBookingPromoDiscount({
113
120
  itemTotal: line.itemTotal,
114
121
  })),
115
122
  effectiveSubtotal,
123
+ promoEligibleSubtotal,
116
124
  displayedProductFeeTotal: feeLineItems.reduce((sum, fee) => sum + fee.totalAmount, 0),
117
125
  appliedPromoCode,
118
126
  };
@@ -134,6 +142,7 @@ export function useStandardBookingPromoDiscount({
134
142
  ticketLineItems: lines,
135
143
  effectiveSubtotal: sub,
136
144
  displayedProductFeeTotal,
145
+ promoEligibleSubtotal: promoSub,
137
146
  appliedPromoCode: code,
138
147
  } = promoDiscountParamsRef.current;
139
148
  if (!code || !sel) return;
@@ -158,6 +167,7 @@ export function useStandardBookingPromoDiscount({
158
167
  totalCapacity: sel.totalCapacity,
159
168
  displayedTicketLines: lines,
160
169
  displayedProductFeeTotal,
170
+ promoEligibleSubtotal: promoSub,
161
171
  },
162
172
  )
163
173
  .then((res) => {
@@ -188,9 +198,9 @@ export function useStandardBookingPromoDiscount({
188
198
  const taxOnSubtotal = isTaxIncludedInPrice ? 0 : effectiveSubtotal * (pricingConfig?.taxRate ?? 0);
189
199
  const effectiveTax =
190
200
  effectivePromoDiscountAmount > 0 && !isGiftCard && !isVoucher
191
- ? (effectiveSubtotal - effectivePromoDiscountAmount) * (pricingConfig?.taxRate ?? 0)
192
- : taxOnSubtotal;
193
- const totalPrice = effectiveSubtotal + effectiveTax - effectivePromoDiscountAmount;
201
+ ? normalizeMoneyZero((effectiveSubtotal - effectivePromoDiscountAmount) * (pricingConfig?.taxRate ?? 0))
202
+ : normalizeMoneyZero(taxOnSubtotal);
203
+ const totalPrice = normalizeMoneyZero(effectiveSubtotal + effectiveTax - effectivePromoDiscountAmount);
194
204
 
195
205
  return {
196
206
  promoDiscountAmount,
@@ -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();
@@ -1669,6 +1710,8 @@ export interface ReturnOption {
1669
1710
  bookedCapacity?: number;
1670
1711
  pricesByCategory?: { retailPrices: Array<{ category: string; price: number }> };
1671
1712
  priceAdjustmentByCurrency?: Record<string, number>;
1713
+ /** Ticket rates priced using this specific return option's capacity floor. */
1714
+ rates?: AvailabilityRate[];
1672
1715
  returnLocation: string;
1673
1716
  mostPopular?: boolean;
1674
1717
  }