@ticketboothapp/booking 1.2.70 → 1.2.72

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.70",
3
+ "version": "1.2.72",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -826,17 +826,12 @@ export function AdminChangeBookingFlow({
826
826
  /** Any change from an existing booking (public or provider). */
827
827
  const isChangeBookingContext = Boolean(initialValues?.bookingReference?.trim());
828
828
  /**
829
- * When the provider passes `onChangeBooking` we normally apply the change immediately on checkout.
830
- * In provider-dashboard + admin + change mode we need the "Pay now / confirm without payment" step
831
- * first, so we run the public change-intent quote + PI, then call `onChangeBooking` from
832
- * `handleConfirmWithoutPayment` (or after paying in the checkout modal).
829
+ * Public `POST .../change/quote` path whenever we are editing an existing booking including when the host passes
830
+ * `onChangeBooking`. Provider apply (`onChangeBooking`) runs only after Pay now / Confirm without payment, never from
831
+ * Continue alone.
833
832
  */
834
- const deferProviderApplyToAdminPaymentChoice =
835
- isAdmin &&
836
- isChangeBookingContext &&
837
- bookingAppMode === 'provider-dashboard' &&
838
- isProviderDashboardChange;
839
- const isCustomerSelfServeChange = !isProviderDashboardChange || deferProviderApplyToAdminPaymentChoice;
833
+ const isCustomerSelfServeChange =
834
+ !isProviderDashboardChange || isChangeBookingContext;
840
835
  /** Do not render catalog-/FE-derived dollar amounts in UI until `quoteChangeBooking` returns `serverDisplay`. */
841
836
  const suppressSelfServeCurrencyUi = isCustomerSelfServeChange;
842
837
 
@@ -1722,14 +1717,14 @@ export function AdminChangeBookingFlow({
1722
1717
  }, [initialValues?.productId, product.productId]);
1723
1718
 
1724
1719
  /**
1725
- * Receipt pricing on protected seats/fees — **customer self-serve only**: Rule A (exact receipt unit when same calendar day
1726
- * + same product option) vs Rule B (`max(receipt, live)` when date or option changes). Provider dashboard uses live catalog.
1720
+ * Receipt pricing on protected seats/fees: Rule A / B per `change-flow-pricing.ts`, same as public self-serve whenever
1721
+ * `POST .../change/quote` runs (`isCustomerSelfServeChange`, including provider dashboard with `onChangeBooking`).
1727
1722
  */
1728
1723
  const changeFlowApplyReceiptPaidFloors = useMemo(
1729
1724
  () =>
1730
- !isProviderDashboardChange &&
1725
+ isCustomerSelfServeChange &&
1731
1726
  changeFlowBookingParentProductIdForFloors === product.productId.trim(),
1732
- [isProviderDashboardChange, changeFlowBookingParentProductIdForFloors, product.productId],
1727
+ [isCustomerSelfServeChange, changeFlowBookingParentProductIdForFloors, product.productId],
1733
1728
  );
1734
1729
 
1735
1730
  useEffect(() => {
@@ -2585,7 +2580,7 @@ export function AdminChangeBookingFlow({
2585
2580
  latestChangeQuote?.serverPreview?.returnOptionPriceByReturnAvailabilityId,
2586
2581
  ]);
2587
2582
 
2588
- /** Order-summary return row uses self-serve floors via [selectedReturnOptionWithFloor]; provider stays catalog-only. */
2583
+ /** Order-summary return row uses self-serve floors via [selectedReturnOptionWithFloor] when `isCustomerSelfServeChange`. */
2589
2584
  const returnOptionForOrderSummary = useMemo(
2590
2585
  () => selectedReturnOptionWithFloor,
2591
2586
  [selectedReturnOptionWithFloor],
@@ -2839,7 +2834,7 @@ export function AdminChangeBookingFlow({
2839
2834
  */
2840
2835
  const changeFlowProtectedReturnAdjustment = useMemo(() => {
2841
2836
  if (totalQuantity <= 0) return returnPriceAdjustment;
2842
- if (isProviderDashboardChange) return returnPriceAdjustment;
2837
+ if (!isCustomerSelfServeChange) return returnPriceAdjustment;
2843
2838
  if (effectiveChangeFlowReturnUnitFloorPerPerson == null) return returnPriceAdjustment;
2844
2839
  const livePerPerson =
2845
2840
  returnOptionCatalogPerPerson ?? (selectedReturnOption?.priceAdjustmentByCurrency?.[currency] ?? 0);
@@ -2853,13 +2848,13 @@ export function AdminChangeBookingFlow({
2853
2848
  }, [
2854
2849
  totalQuantity,
2855
2850
  returnPriceAdjustment,
2856
- isProviderDashboardChange,
2857
2851
  effectiveChangeFlowReturnUnitFloorPerPerson,
2858
2852
  selectedReturnOption,
2859
2853
  returnOptionCatalogPerPerson,
2860
2854
  currency,
2861
2855
  changeFlowInitialTicketCount,
2862
2856
  changeFlowReturnPricingRuleA,
2857
+ isCustomerSelfServeChange,
2863
2858
  ]);
2864
2859
 
2865
2860
  /** Return row amount for PriceSummary, Stripe breakdown, and CheckoutModal (catalog vs protected same-product-option). */
@@ -3110,6 +3105,7 @@ export function AdminChangeBookingFlow({
3110
3105
  ticketLineItems: [] as Array<{ category: string; qty: number }>,
3111
3106
  effectiveSubtotal: 0,
3112
3107
  appliedPromoCode: null as string | null,
3108
+ changeBookingPromo: null as { priorAmount?: number } | null,
3113
3109
  });
3114
3110
 
3115
3111
  const promoDiscountFetchKey = useMemo(() => {
@@ -3128,6 +3124,7 @@ export function AdminChangeBookingFlow({
3128
3124
  currency,
3129
3125
  quantitiesSignature,
3130
3126
  String(Math.round(effectiveSubtotal * 100)),
3127
+ lockedPromoCode ? String(Math.round((originalReceipt?.promoAmount ?? 0) * 100)) : '',
3131
3128
  ].join('::');
3132
3129
  }, [
3133
3130
  appliedPromoCode,
@@ -3140,6 +3137,8 @@ export function AdminChangeBookingFlow({
3140
3137
  currency,
3141
3138
  quantitiesSignature,
3142
3139
  effectiveSubtotal,
3140
+ lockedPromoCode,
3141
+ originalReceipt?.promoAmount,
3143
3142
  ]);
3144
3143
 
3145
3144
  promoDiscountParamsRef.current = {
@@ -3147,6 +3146,9 @@ export function AdminChangeBookingFlow({
3147
3146
  ticketLineItems: ticketLineItems.map((l) => ({ category: l.category, qty: l.qty })),
3148
3147
  effectiveSubtotal,
3149
3148
  appliedPromoCode,
3149
+ changeBookingPromo: lockedPromoCode
3150
+ ? { priorAmount: originalReceipt?.promoAmount }
3151
+ : null,
3150
3152
  };
3151
3153
 
3152
3154
  useEffect(() => {
@@ -3166,6 +3168,7 @@ export function AdminChangeBookingFlow({
3166
3168
  ticketLineItems: lines,
3167
3169
  effectiveSubtotal: sub,
3168
3170
  appliedPromoCode: code,
3171
+ changeBookingPromo,
3169
3172
  } = promoDiscountParamsRef.current;
3170
3173
  if (!code || !sel) return;
3171
3174
  const companyId = product.companyId ?? env.COMPANY_ID;
@@ -3183,7 +3186,13 @@ export function AdminChangeBookingFlow({
3183
3186
  currency,
3184
3187
  items,
3185
3188
  sel.dateTime,
3186
- sub
3189
+ sub,
3190
+ changeBookingPromo
3191
+ ? {
3192
+ forBookingChange: true,
3193
+ priorPromoDiscountAmount: changeBookingPromo.priorAmount,
3194
+ }
3195
+ : undefined
3187
3196
  )
3188
3197
  .then((res) => {
3189
3198
  if (cancelled) return;
@@ -3209,7 +3218,7 @@ export function AdminChangeBookingFlow({
3209
3218
 
3210
3219
  // Percentage/fixed promos: tax on discounted amount (promo before GST per CRA guidance).
3211
3220
  // Vouchers and gift cards: tax on full subtotal (voucher discount includes tax on free portion; gift card is payment).
3212
- // Change booking: same as normal — discount from get-promo-discount on the new selection (promo code from booking).
3221
+ // Change booking: get-promo-discount uses forBookingChange + prior promo from receipt so expired codes stay locked (BE).
3213
3222
  const lockedPromoFallbackAmount =
3214
3223
  lockedPromoCode
3215
3224
  ? Math.max(0, originalReceipt?.promoAmount ?? 0)
@@ -4476,49 +4485,6 @@ export function AdminChangeBookingFlow({
4476
4485
  return;
4477
4486
  }
4478
4487
 
4479
- if (onChangeBooking && !deferProviderApplyToAdminPaymentChoice) {
4480
- const pickupForChange = pickupLocationId
4481
- ? product.pickupLocations?.find((loc) => loc.id === pickupLocationId)
4482
- : null;
4483
- await onChangeBooking({
4484
- productId: availabilityProductOptionId,
4485
- dateTime: selectedAvailability.dateTime,
4486
- bookingItems,
4487
- returnAvailabilityId: selectedReturnOption?.returnAvailabilityId ?? null,
4488
- pickupLocationId: pickupLocationId ?? null,
4489
- travelerHotel: pickupForChange?.name ?? null,
4490
- startTime: selectedAvailability.dateTime ?? null,
4491
- passengerCount: null,
4492
- childSafetySeatsCount: null,
4493
- foodRestrictions: null,
4494
- addOnSelections: addOnSelections.length > 0 ? addOnSelections : null,
4495
- cancellationPolicyId: cancellationPolicyId ?? initialValues?.cancellationPolicyId ?? null,
4496
- promoCode: appliedPromoCode ?? null,
4497
- newTotalAmount: displayChangeFlowProposedTotal,
4498
- additionalHoursCount: null,
4499
- pricingAdjustment:
4500
- providerPricingOverrides.length > 0 || providerAdditionalAdjustments.length > 0
4501
- ? {
4502
- mode: 'MANUAL_LINES',
4503
- lineOverrides: providerPricingOverrides,
4504
- additionalAdjustments: providerAdditionalAdjustments,
4505
- }
4506
- : undefined,
4507
- capacitySeatCredit: {
4508
- enabled: true,
4509
- previousPassengerCount: changeFlowInitialTicketCount,
4510
- previousAvailabilityId: initialValues?.availabilityId ?? null,
4511
- previousReturnAvailabilityId: initialValues?.returnAvailabilityId ?? null,
4512
- },
4513
- });
4514
- const refAfterChange = initialValues?.bookingReference?.trim();
4515
- if (refAfterChange) {
4516
- onSuccess?.({ reservationReference: refAfterChange });
4517
- }
4518
- setLoading(false);
4519
- return;
4520
- }
4521
-
4522
4488
  const bookingSourceContext = buildBookingSourceContext(bookingSourceAttribution, {
4523
4489
  clientChannelSource: inferClientBookingSourceFromProductIds(
4524
4490
  product.productId,
@@ -2995,6 +2995,8 @@ export function ChangeBookingFlow({
2995
2995
  ticketLineItems: [] as Array<{ category: string; qty: number }>,
2996
2996
  effectiveSubtotal: 0,
2997
2997
  appliedPromoCode: null as string | null,
2998
+ /** Change flow: pass BE stored promo discount for expired / fixed promo locking. */
2999
+ changeBookingPromo: null as { priorAmount?: number } | null,
2998
3000
  });
2999
3001
 
3000
3002
  const promoDiscountFetchKey = useMemo(() => {
@@ -3013,6 +3015,7 @@ export function ChangeBookingFlow({
3013
3015
  currency,
3014
3016
  quantitiesSignature,
3015
3017
  String(Math.round(effectiveSubtotal * 100)),
3018
+ lockedPromoCode ? String(Math.round((originalReceipt?.promoAmount ?? 0) * 100)) : '',
3016
3019
  ].join('::');
3017
3020
  }, [
3018
3021
  appliedPromoCode,
@@ -3025,6 +3028,8 @@ export function ChangeBookingFlow({
3025
3028
  currency,
3026
3029
  quantitiesSignature,
3027
3030
  effectiveSubtotal,
3031
+ lockedPromoCode,
3032
+ originalReceipt?.promoAmount,
3028
3033
  ]);
3029
3034
 
3030
3035
  promoDiscountParamsRef.current = {
@@ -3032,6 +3037,9 @@ export function ChangeBookingFlow({
3032
3037
  ticketLineItems: ticketLineItems.map((l) => ({ category: l.category, qty: l.qty })),
3033
3038
  effectiveSubtotal,
3034
3039
  appliedPromoCode,
3040
+ changeBookingPromo: lockedPromoCode
3041
+ ? { priorAmount: originalReceipt?.promoAmount }
3042
+ : null,
3035
3043
  };
3036
3044
 
3037
3045
  useEffect(() => {
@@ -3051,6 +3059,7 @@ export function ChangeBookingFlow({
3051
3059
  ticketLineItems: lines,
3052
3060
  effectiveSubtotal: sub,
3053
3061
  appliedPromoCode: code,
3062
+ changeBookingPromo,
3054
3063
  } = promoDiscountParamsRef.current;
3055
3064
  if (!code || !sel) return;
3056
3065
  const companyId = product.companyId ?? env.COMPANY_ID;
@@ -3068,7 +3077,13 @@ export function ChangeBookingFlow({
3068
3077
  currency,
3069
3078
  items,
3070
3079
  sel.dateTime,
3071
- sub
3080
+ sub,
3081
+ changeBookingPromo
3082
+ ? {
3083
+ forBookingChange: true,
3084
+ priorPromoDiscountAmount: changeBookingPromo.priorAmount,
3085
+ }
3086
+ : undefined
3072
3087
  )
3073
3088
  .then((res) => {
3074
3089
  if (cancelled) return;
@@ -3094,7 +3109,7 @@ export function ChangeBookingFlow({
3094
3109
 
3095
3110
  // Percentage/fixed promos: tax on discounted amount (promo before GST per CRA guidance).
3096
3111
  // Vouchers and gift cards: tax on full subtotal (voucher discount includes tax on free portion; gift card is payment).
3097
- // Change booking: same as normal discount from get-promo-discount on the new selection (promo code from booking).
3112
+ // Change booking: get-promo-discount uses forBookingChange + receipt prior promo (BE locks expired % / fixed / voucher).
3098
3113
  const lockedPromoFallbackAmount =
3099
3114
  lockedPromoCode
3100
3115
  ? Math.max(0, originalReceipt?.promoAmount ?? 0)
@@ -5,9 +5,9 @@
5
5
  * channel) on top of these formulas.
6
6
  *
7
7
  * ### 1. When receipt “paid floors” apply
8
- * Only **customer self-serve** change flow, when the booking’s **parent catalog product** matches the **loaded product**
9
- * (`changeFlowApplyReceiptPaidFloors` in BookingFlow). **Provider dashboard** change flow uses **live catalog only** (no
10
- * floors) so a cheaper date/return yields a lower total / refund via signed balance.
8
+ * When the booking’s **parent catalog product** matches the **loaded product** and the flow uses the public
9
+ * `POST .../change/quote` path (`isCustomerSelfServeChange` in AdminChangeBookingFlow, including provider dashboard with
10
+ * `onChangeBooking`).
11
11
  *
12
12
  * ### 2. Tickets (per category)
13
13
  * **Unchanged itinerary** (same calendar departure date **and** same product option as the booking): among seats up to the
@@ -26,12 +26,12 @@
26
26
  * BE `PublicChangeBookingQuotePricing`: **baseline party** (originally booked headcount) vs **incremental** seats. **Rule A**
27
27
  * (same `returnAvailabilityId` as the booking **and** unchanged outbound itinerary): protected pay exact locked per person;
28
28
  * incremental pay **live catalog** return only ($0 when free). **Else Rule B:** protected pay **`max(floor, live)`**;
29
- * incremental pay **live** only. Provider dashboard: **live catalog return only** (no floor).
29
+ * incremental pay **live** only.
30
30
  *
31
31
  * ### 5. Return option — **picker UI only** (BookingFlow)
32
32
  * **Self-serve:** return cards use the floored per-person price when a floor exists (aligned with §4), for every return
33
33
  * slot, regardless of date/option change.
34
- * **Provider dashboard:** cards show **catalog** prices only (no floor).
34
+ * With `POST .../change/quote`, return cards follow the same floor behavior as self-serve.
35
35
  *
36
36
  * ### 6. Quote “new booking” total & balance
37
37
  * **FE proposed total** = full cart math (subtotal + tax − promo), cent-rounded; optional **1¢ reconcile** to old receipt
@@ -651,6 +651,13 @@ export interface GetPromoDiscountResponse {
651
651
  isVoucher?: boolean;
652
652
  }
653
653
 
654
+ /** Booking change: server locks discount using stored receipt promo when windows expire / fixed codes. */
655
+ export interface GetPromoDiscountBookingChangeOptions {
656
+ forBookingChange?: boolean;
657
+ /** Stored promo discount from the existing booking (major units, same currency as quote). */
658
+ priorPromoDiscountAmount?: number;
659
+ }
660
+
654
661
  export async function getPromoDiscount(
655
662
  promoCode: string,
656
663
  companyId: string,
@@ -659,7 +666,8 @@ export async function getPromoDiscount(
659
666
  currency: string,
660
667
  items: Array<{ category: string; qty: number }>,
661
668
  dateTime?: string,
662
- subtotal?: number
669
+ subtotal?: number,
670
+ bookingChange?: GetPromoDiscountBookingChangeOptions
663
671
  ): Promise<GetPromoDiscountResponse> {
664
672
  const itemsStr = items.map((i) => `${i.category}:${i.qty}`).join(',');
665
673
  const params = new URLSearchParams({
@@ -672,6 +680,15 @@ export async function getPromoDiscount(
672
680
  });
673
681
  if (dateTime) params.set('dateTime', dateTime);
674
682
  if (subtotal != null && subtotal > 0) params.set('subtotal', String(subtotal));
683
+ if (bookingChange?.forBookingChange === true) {
684
+ params.set('forBookingChange', 'true');
685
+ }
686
+ if (
687
+ bookingChange?.priorPromoDiscountAmount != null &&
688
+ Number.isFinite(bookingChange.priorPromoDiscountAmount)
689
+ ) {
690
+ params.set('priorPromoDiscountAmount', String(bookingChange.priorPromoDiscountAmount));
691
+ }
675
692
  const res = await fetchBookingGetWithRetry(`${API_BASE}/1/get-promo-discount?${params}`);
676
693
  if (!res.ok) {
677
694
  const err = await res.json();