@ticketboothapp/booking 1.2.83 → 1.2.85

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.83",
3
+ "version": "1.2.85",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -152,14 +152,39 @@ function omitZeroAmountPromoDiscountSummaryLines(lines: PriceSummaryLine[]): Pri
152
152
  });
153
153
  }
154
154
 
155
- function mapSummaryLinesToFeReceiptLineItems(lines: PriceSummaryLine[]): NonNullable<AdminFeAuthoritativeReceipt['lineItems']> {
155
+ function resolveTicketQtyFromQuantities(
156
+ category: string,
157
+ quantities: Record<string, number>,
158
+ ): number | undefined {
159
+ const c = category.trim();
160
+ if (Object.prototype.hasOwnProperty.call(quantities, c)) return quantities[c];
161
+ const up = c.toUpperCase();
162
+ for (const [key, val] of Object.entries(quantities)) {
163
+ if (key.toUpperCase() === up) return val;
164
+ }
165
+ return undefined;
166
+ }
167
+
168
+ /** Receipt TICKET rows sent on admin quote must follow picker {@link quantities}: BE apply uses these lines for headcount when feReceipt is authoritative — server `priceSummaryLines` can lag counts. */
169
+ function mapSummaryLinesToFeReceiptLineItems(
170
+ lines: PriceSummaryLine[],
171
+ quantities: Record<string, number>,
172
+ ): NonNullable<AdminFeAuthoritativeReceipt['lineItems']> {
156
173
  return lines.map((line) => {
157
174
  if (line.kind === 'ticket') {
175
+ const uiQty = resolveTicketQtyFromQuantities(line.category, quantities);
176
+ const lineQty = Math.max(0, line.qty);
177
+ const effectiveQty =
178
+ uiQty != null && uiQty >= 0 ? Math.round(uiQty) : lineQty;
179
+ let amount = roundMoney(line.itemTotal);
180
+ if (lineQty > 0 && effectiveQty !== lineQty) {
181
+ amount = roundMoney((line.itemTotal * effectiveQty) / lineQty);
182
+ }
158
183
  return {
159
184
  label: line.category,
160
- amount: roundMoney(line.itemTotal),
185
+ amount,
161
186
  type: 'TICKET',
162
- quantity: line.qty,
187
+ quantity: Math.max(0, effectiveQty),
163
188
  };
164
189
  }
165
190
  const normalizedType = String(line.type ?? '').trim().toUpperCase();
@@ -3828,7 +3853,7 @@ export function AdminChangeBookingFlow({
3828
3853
  const displayChangeFlowProposedTotalWithEditableLines = roundMoney(
3829
3854
  displayChangeFlowProposedTotal + editableSummaryPreSubtotalDelta
3830
3855
  );
3831
- /** Tax-included currencies: ticket row amounts match authoritative new-booking total (quote/display), not raw catalog/floor rollup. */
3856
+ /** Tax-included: scale ticket row $ on the PriceSummary so ADULT lines match the footer total (quote/display). Payment breakdown still uses {@link ticketLineItemsForChangeFlowDisplay} — it must reconcile to amount-due, not full new-booking total. */
3832
3857
  const taxIncludedReconciledCheckoutPriceSummaryLines = useMemo(() => {
3833
3858
  if (!isTaxIncludedInPrice || !originalReceipt || showProviderPricingInlineEditor) {
3834
3859
  return editableCheckoutPriceSummaryLines;
@@ -3847,11 +3872,39 @@ export function AdminChangeBookingFlow({
3847
3872
  effectivePromoDiscountAmount,
3848
3873
  ]);
3849
3874
 
3875
+ /** Stripe modal / admin pay modal must use the same ticket row amounts as PriceSummary; unreconciled catalog lines caused ADULT € vs total mismatch (tax-included). PI `buildCheckoutBreakdown` stays on catalog ticket rows. */
3876
+ const checkoutModalTicketLinesDisplay = useMemo((): CheckoutModalLineItem[] => {
3877
+ return taxIncludedReconciledCheckoutPriceSummaryLines
3878
+ .filter((l): l is Extract<PriceSummaryLine, { kind: 'ticket' }> => l.kind === 'ticket')
3879
+ .map((line) => {
3880
+ const rate = pricing.find((r) => r.category === line.category);
3881
+ const breakdown = getPriceBreakdown(
3882
+ line.category,
3883
+ rate?.priceCAD ?? 0,
3884
+ rate?.baseInDisplayCurrency,
3885
+ rate?.appliedAdjustments ?? [],
3886
+ );
3887
+ const qty = Math.max(0, line.qty);
3888
+ return {
3889
+ line: {
3890
+ category: line.category,
3891
+ qty,
3892
+ itemTotal: line.itemTotal,
3893
+ pricePerUnit: qty > 0 ? roundMoney(line.itemTotal / qty) : 0,
3894
+ },
3895
+ breakdown,
3896
+ };
3897
+ });
3898
+ }, [taxIncludedReconciledCheckoutPriceSummaryLines, pricing, getPriceBreakdown]);
3899
+
3850
3900
  const adminFeAuthoritativeReceipt = useMemo((): AdminFeAuthoritativeReceipt => {
3851
3901
  const hasTaxLine = taxIncludedReconciledCheckoutPriceSummaryLines.some(
3852
3902
  (line) => line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX'
3853
3903
  );
3854
- const lineItems = mapSummaryLinesToFeReceiptLineItems(taxIncludedReconciledCheckoutPriceSummaryLines);
3904
+ const lineItems = mapSummaryLinesToFeReceiptLineItems(
3905
+ taxIncludedReconciledCheckoutPriceSummaryLines,
3906
+ quantities,
3907
+ );
3855
3908
  if (!hasTaxLine && Math.abs(displayChangeFlowTax) >= 0.0005) {
3856
3909
  lineItems.push({
3857
3910
  label: t('booking.tax') !== 'booking.tax' ? t('booking.tax') : 'Taxes and fees',
@@ -3875,6 +3928,7 @@ export function AdminChangeBookingFlow({
3875
3928
  };
3876
3929
  }, [
3877
3930
  taxIncludedReconciledCheckoutPriceSummaryLines,
3931
+ quantities,
3878
3932
  displayChangeFlowTax,
3879
3933
  displayChangeFlowSubtotal,
3880
3934
  editableSummaryPreSubtotalDelta,
@@ -5034,11 +5088,8 @@ export function AdminChangeBookingFlow({
5034
5088
  }),
5035
5089
  )
5036
5090
  : totalPrice;
5037
- const ticketRowsForCheckoutBreakdown = taxIncludedReconciledCheckoutPriceSummaryLines.filter(
5038
- (l): l is Extract<PriceSummaryLine, { kind: 'ticket' }> => l.kind === 'ticket',
5039
- );
5040
5091
  const lines = [
5041
- ...ticketRowsForCheckoutBreakdown.map((line) => ({
5092
+ ...ticketLineItemsForChangeFlowDisplay.map((line) => ({
5042
5093
  label: line.category,
5043
5094
  amount: line.itemTotal,
5044
5095
  type: 'TICKET' as const,
@@ -5181,25 +5232,7 @@ export function AdminChangeBookingFlow({
5181
5232
  availabilityProductOptionId,
5182
5233
  itineraryDisplay: itineraryDisplay ?? undefined,
5183
5234
  clientSecret: paymentIntent.clientSecret ?? '',
5184
- ticketLinesForModal: ticketRowsForCheckoutBreakdown.map((line) => {
5185
- const rate = pricing.find((r) => r.category === line.category);
5186
- const breakdown = getPriceBreakdown(
5187
- line.category,
5188
- rate?.priceCAD ?? 0,
5189
- rate?.baseInDisplayCurrency,
5190
- rate?.appliedAdjustments ?? []
5191
- );
5192
- const qty = Math.max(0, line.qty);
5193
- return {
5194
- line: {
5195
- category: line.category,
5196
- qty,
5197
- itemTotal: line.itemTotal,
5198
- pricePerUnit: qty > 0 ? roundMoney(line.itemTotal / qty) : 0,
5199
- },
5200
- breakdown,
5201
- };
5202
- }),
5235
+ ticketLinesForModal: checkoutModalTicketLinesDisplay,
5203
5236
  feeLineItems: feeLineItemsWithAddOns,
5204
5237
  returnPriceAdjustment: checkoutReturnLineAmount,
5205
5238
  cancellationPolicyFee,
@@ -5217,26 +5250,6 @@ export function AdminChangeBookingFlow({
5217
5250
  return;
5218
5251
  }
5219
5252
 
5220
- const ticketLinesForModal: CheckoutModalLineItem[] = ticketRowsForCheckoutBreakdown.map((line) => {
5221
- const rate = pricing.find((r) => r.category === line.category);
5222
- const breakdown = getPriceBreakdown(
5223
- line.category,
5224
- rate?.priceCAD ?? 0,
5225
- rate?.baseInDisplayCurrency,
5226
- rate?.appliedAdjustments ?? []
5227
- );
5228
- const qty = Math.max(0, line.qty);
5229
- return {
5230
- line: {
5231
- category: line.category,
5232
- qty,
5233
- itemTotal: line.itemTotal,
5234
- pricePerUnit: qty > 0 ? roundMoney(line.itemTotal / qty) : 0,
5235
- },
5236
- breakdown,
5237
- };
5238
- });
5239
-
5240
5253
  setCheckoutClientSecret(paymentIntent.clientSecret ?? '');
5241
5254
  setCheckoutModalData({
5242
5255
  reservationReference: changeBookingReferenceForPaidFlow ?? '',
@@ -5269,7 +5282,7 @@ export function AdminChangeBookingFlow({
5269
5282
  return `${origin}/manage-booking?ref=${ref}&lastName=${ln}&${fromQ}${intentQ}`;
5270
5283
  })()
5271
5284
  : undefined,
5272
- ticketLines: ticketLinesForModal,
5285
+ ticketLines: checkoutModalTicketLinesDisplay,
5273
5286
  feeLineItems: feeLineItemsWithAddOns,
5274
5287
  returnPriceAdjustment: checkoutReturnLineAmount,
5275
5288
  cancellationPolicyFee,
@@ -3564,6 +3564,30 @@ export function ChangeBookingFlow({
3564
3564
  effectivePromoDiscountAmount,
3565
3565
  ]);
3566
3566
 
3567
+ const checkoutModalTicketLinesDisplay = useMemo((): CheckoutModalLineItem[] => {
3568
+ return taxIncludedSelfServeReconciledPriceSummaryLines
3569
+ .filter((l): l is Extract<PriceSummaryLine, { kind: 'ticket' }> => l.kind === 'ticket')
3570
+ .map((line) => {
3571
+ const rate = pricing.find((r) => r.category === line.category);
3572
+ const breakdown = getPriceBreakdown(
3573
+ line.category,
3574
+ rate?.priceCAD ?? 0,
3575
+ rate?.baseInDisplayCurrency,
3576
+ rate?.appliedAdjustments ?? [],
3577
+ );
3578
+ const qty = Math.max(0, line.qty);
3579
+ return {
3580
+ line: {
3581
+ category: line.category,
3582
+ qty,
3583
+ itemTotal: line.itemTotal,
3584
+ pricePerUnit: qty > 0 ? roundMoney(line.itemTotal / qty) : 0,
3585
+ },
3586
+ breakdown,
3587
+ };
3588
+ });
3589
+ }, [taxIncludedSelfServeReconciledPriceSummaryLines, pricing, getPriceBreakdown]);
3590
+
3567
3591
  const changeFlowClientEstimateDue = (() => {
3568
3592
  if (!originalReceipt) return totalPrice;
3569
3593
  // Customer self-serve: amount due comes from POST .../change/quote (`amountDueCents` / priceDiff), not FE delta math.
@@ -4466,11 +4490,8 @@ export function ChangeBookingFlow({
4466
4490
  originalReceiptTotal: originalReceipt?.total ?? 0,
4467
4491
  audience: 'customer',
4468
4492
  });
4469
- const ticketRowsForCheckoutBreakdown = taxIncludedSelfServeReconciledPriceSummaryLines.filter(
4470
- (l): l is Extract<PriceSummaryLine, { kind: 'ticket' }> => l.kind === 'ticket',
4471
- );
4472
4493
  const lines = [
4473
- ...ticketRowsForCheckoutBreakdown.map((line) => ({
4494
+ ...ticketLineItemsForChangeFlowDisplay.map((line) => ({
4474
4495
  label: line.category,
4475
4496
  amount: line.itemTotal,
4476
4497
  type: 'TICKET' as const,
@@ -4555,26 +4576,6 @@ export function ChangeBookingFlow({
4555
4576
  })()
4556
4577
  );
4557
4578
 
4558
- const ticketLinesForModal: CheckoutModalLineItem[] = ticketRowsForCheckoutBreakdown.map((line) => {
4559
- const rate = pricing.find((r) => r.category === line.category);
4560
- const breakdown = getPriceBreakdown(
4561
- line.category,
4562
- rate?.priceCAD ?? 0,
4563
- rate?.baseInDisplayCurrency,
4564
- rate?.appliedAdjustments ?? []
4565
- );
4566
- const qty = Math.max(0, line.qty);
4567
- return {
4568
- line: {
4569
- category: line.category,
4570
- qty,
4571
- itemTotal: line.itemTotal,
4572
- pricePerUnit: qty > 0 ? roundMoney(line.itemTotal / qty) : 0,
4573
- },
4574
- breakdown,
4575
- };
4576
- });
4577
-
4578
4579
  setCheckoutClientSecret(paymentIntent.clientSecret ?? '');
4579
4580
  setCheckoutModalData({
4580
4581
  reservationReference: changeBookingReferenceForPaidFlow ?? '',
@@ -4598,7 +4599,7 @@ export function ChangeBookingFlow({
4598
4599
  return `${origin}/manage-booking?ref=${ref}&lastName=${ln}&${fromQ}${intentQ}`;
4599
4600
  })()
4600
4601
  : undefined,
4601
- ticketLines: ticketLinesForModal,
4602
+ ticketLines: checkoutModalTicketLinesDisplay,
4602
4603
  feeLineItems: feeLineItemsWithAddOns,
4603
4604
  returnPriceAdjustment: checkoutReturnLineAmount,
4604
4605
  cancellationPolicyFee,