@ticketboothapp/booking 1.2.183 → 1.2.185

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.183",
3
+ "version": "1.2.185",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "exports": {
13
13
  ".": "./src/index.ts",
14
+ "./background-video": "./src/components/BackgroundVideo.tsx",
14
15
  "./booking-flow.css": "./src/components/booking/booking-flow.css",
15
16
  "./runtime": "./src/runtime/index.ts",
16
17
  "./contexts/booking-app-context": "./src/contexts/BookingAppContext.tsx",
@@ -0,0 +1,126 @@
1
+ 'use client';
2
+
3
+ import {
4
+ forwardRef,
5
+ useCallback,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ type CSSProperties,
10
+ type ForwardedRef,
11
+ type VideoHTMLAttributes,
12
+ } from 'react';
13
+
14
+ export type BackgroundVideoLoading = 'eager' | 'viewport';
15
+
16
+ export interface BackgroundVideoProps
17
+ extends Omit<VideoHTMLAttributes<HTMLVideoElement>, 'children' | 'poster' | 'preload' | 'src'> {
18
+ src: string;
19
+ webm?: string;
20
+ poster?: string;
21
+ preload?: 'auto' | 'metadata' | 'none';
22
+ loading?: BackgroundVideoLoading;
23
+ className?: string;
24
+ style?: CSSProperties;
25
+ }
26
+
27
+ function assignRef(ref: ForwardedRef<HTMLVideoElement>, value: HTMLVideoElement | null) {
28
+ if (typeof ref === 'function') ref(value);
29
+ else if (ref) ref.current = value;
30
+ }
31
+
32
+ /**
33
+ * Lightweight background video with real multi-format sources. Viewport videos
34
+ * do not request media until they are near the screen and pause again offscreen.
35
+ */
36
+ export const BackgroundVideo = forwardRef<HTMLVideoElement, BackgroundVideoProps>(
37
+ function BackgroundVideo(
38
+ {
39
+ src,
40
+ webm,
41
+ poster,
42
+ preload,
43
+ loading = 'viewport',
44
+ className,
45
+ style,
46
+ autoPlay = true,
47
+ muted = true,
48
+ loop = true,
49
+ playsInline = true,
50
+ ...videoProps
51
+ },
52
+ forwardedRef,
53
+ ) {
54
+ const containerRef = useRef<HTMLDivElement | null>(null);
55
+ const videoRef = useRef<HTMLVideoElement | null>(null);
56
+ const [shouldLoad, setShouldLoad] = useState(loading === 'eager');
57
+ const [shouldPlay, setShouldPlay] = useState(loading === 'eager');
58
+
59
+ const setVideoRef = useCallback((element: HTMLVideoElement | null) => {
60
+ videoRef.current = element;
61
+ assignRef(forwardedRef, element);
62
+ }, [forwardedRef]);
63
+
64
+ useEffect(() => {
65
+ if (loading === 'eager') {
66
+ setShouldLoad(true);
67
+ setShouldPlay(true);
68
+ return;
69
+ }
70
+
71
+ const container = containerRef.current;
72
+ if (!container || typeof IntersectionObserver === 'undefined') {
73
+ setShouldLoad(true);
74
+ setShouldPlay(true);
75
+ return;
76
+ }
77
+
78
+ const observer = new IntersectionObserver(
79
+ ([entry]) => {
80
+ if (entry.isIntersecting) setShouldLoad(true);
81
+ setShouldPlay(entry.isIntersecting);
82
+ },
83
+ { rootMargin: '300px 0px' },
84
+ );
85
+ observer.observe(container);
86
+ return () => observer.disconnect();
87
+ }, [loading]);
88
+
89
+ useEffect(() => {
90
+ const video = videoRef.current;
91
+ if (!video || !shouldLoad || !autoPlay) return;
92
+ if (shouldPlay) void video.play().catch(() => undefined);
93
+ else video.pause();
94
+ }, [autoPlay, shouldLoad, shouldPlay, src, webm]);
95
+
96
+ return (
97
+ <div
98
+ ref={containerRef}
99
+ className={[className, 'next-video-bg'].filter(Boolean).join(' ')}
100
+ style={{ width: '100%', height: '100%', display: 'grid', ...style }}
101
+ >
102
+ <video
103
+ {...videoProps}
104
+ ref={setVideoRef}
105
+ className="next-video-bg-video"
106
+ autoPlay={autoPlay && shouldPlay}
107
+ muted={muted}
108
+ loop={loop}
109
+ playsInline={playsInline}
110
+ poster={poster}
111
+ preload={shouldLoad ? (preload ?? (loading === 'eager' ? 'auto' : 'metadata')) : 'none'}
112
+ style={{
113
+ gridArea: '1 / 1',
114
+ width: '100%',
115
+ height: '100%',
116
+ minHeight: 0,
117
+ objectFit: 'cover',
118
+ }}
119
+ >
120
+ {shouldLoad && webm ? <source src={webm} type="video/webm" /> : null}
121
+ {shouldLoad ? <source src={src} type="video/mp4" /> : null}
122
+ </video>
123
+ </div>
124
+ );
125
+ },
126
+ );
@@ -1,5 +1,6 @@
1
1
  export interface BookingData {
2
2
  bookingReference: string;
3
+ pricingProfileId?: string | null;
3
4
  reservationReference?: string | null;
4
5
  dateTime?: string | null;
5
6
  totalAmount?: number | null;
@@ -196,6 +196,7 @@ const checkoutPanelPropKeys = [
196
196
  'onUpdateAdminCustomReceiptLine',
197
197
  'onRemoveAdminCustomReceiptLine',
198
198
  'onApplyAdminCustomReceiptLines',
199
+ 'onSetAdminReceiptLineOverride',
199
200
  'firstName',
200
201
  'lastName',
201
202
  'email',
@@ -337,6 +337,7 @@ export function AdminChangeBookingFlow({
337
337
  handleUpdateAdminCustomReceiptLine,
338
338
  handleRemoveAdminCustomReceiptLine,
339
339
  handleApplyAdminCustomReceiptLines: handleApplyAdminCustomReceiptLineDrafts,
340
+ handleSetAdminReceiptLineOverride,
340
341
  } = useAdminCustomReceiptLines(
341
342
  selectedChangeProductId,
342
343
  latestChangeQuote?.reversibleAdjustments,
@@ -1299,6 +1300,7 @@ export function AdminChangeBookingFlow({
1299
1300
  onUpdateAdminCustomReceiptLine: handleUpdateAdminCustomReceiptLine,
1300
1301
  onRemoveAdminCustomReceiptLine: handleRemoveAdminCustomReceiptLine,
1301
1302
  onApplyAdminCustomReceiptLines: handleApplyAdminCustomReceiptLines,
1303
+ onSetAdminReceiptLineOverride: handleSetAdminReceiptLineOverride,
1302
1304
  onFirstNameChange: setFirstName, onLastNameChange: setLastName, onEmailChange: setEmail,
1303
1305
  onClearError: () => setError(''),
1304
1306
  pickupLocations: product.pickupLocations, destinations: product.destinations, highlightedPickupLocationIds,
@@ -80,6 +80,13 @@ export interface AdminChangeCheckoutPanelProps {
80
80
  onUpdateAdminCustomReceiptLine: (id: string, patch: AdminCustomReceiptLinePatch) => void;
81
81
  onRemoveAdminCustomReceiptLine: (id: string) => void;
82
82
  onApplyAdminCustomReceiptLines: () => void;
83
+ onSetAdminReceiptLineOverride: (input: {
84
+ componentId: string;
85
+ lineKey: string;
86
+ label: string;
87
+ baseAmount: number;
88
+ targetAmount: number;
89
+ }) => void;
83
90
  firstName: string;
84
91
  lastName: string;
85
92
  email: string;
@@ -131,7 +138,6 @@ export function AdminChangeCheckoutPanel({
131
138
  changeFlowAmountDue,
132
139
  receiptSubtotal,
133
140
  receiptTax,
134
- receiptTotal,
135
141
  originalReceipt,
136
142
  priceSummaryLinesIncludeTaxRow,
137
143
  serverAmendmentLines,
@@ -172,6 +178,7 @@ export function AdminChangeCheckoutPanel({
172
178
  onUpdateAdminCustomReceiptLine,
173
179
  onRemoveAdminCustomReceiptLine,
174
180
  onApplyAdminCustomReceiptLines,
181
+ onSetAdminReceiptLineOverride,
175
182
  firstName,
176
183
  lastName,
177
184
  email,
@@ -232,7 +239,9 @@ export function AdminChangeCheckoutPanel({
232
239
  showProviderPricingInlineEditor={showProviderPricingInlineEditor}
233
240
  providerPricingUi={providerPricingUi}
234
241
  showAdminCustomLineEditor={showAdminCustomLineEditor}
235
- adminCustomReceiptLines={adminCustomReceiptLines}
242
+ adminCustomReceiptLines={adminCustomReceiptLines.filter(
243
+ (line) => !line.overrideComponentId,
244
+ )}
236
245
  hasPendingAdminCustomReceiptLineChanges={hasPendingAdminCustomReceiptLineChanges}
237
246
  pendingAdminCustomReceiptLineIds={pendingAdminCustomReceiptLineIds}
238
247
  adjustmentComponentOptions={adjustmentComponentOptions}
@@ -273,6 +282,8 @@ export function AdminChangeCheckoutPanel({
273
282
  promoEditor={promoEditor}
274
283
  promoCode={promoCode}
275
284
  adjustments={pricingAdjustments}
285
+ adminCustomReceiptLines={adminCustomReceiptLines}
286
+ onSetReceiptLineOverride={onSetAdminReceiptLineOverride}
276
287
  />
277
288
  ) : null;
278
289
 
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { useState, type ReactNode } from 'react';
2
2
  import type {
3
3
  AdminAmendmentOperationSnapshot,
4
4
  AdminAmendmentRefundDisposition,
@@ -10,6 +10,7 @@ import type { ChangeBookingFlowProps } from './booking-flow-types';
10
10
  import type { Currency } from './CurrencySwitcher';
11
11
  import { PriceSummary, type PriceSummaryLine } from './PriceSummary';
12
12
  import { collapseAdminPromoLines } from './admin-change-receipt-lines';
13
+ import type { AdminCustomReceiptLine } from './useAdminCustomReceiptLines';
13
14
  import {
14
15
  noRefundRemovedValue,
15
16
  refundDecisionsComplete,
@@ -63,6 +64,14 @@ export interface AdminChangeReceiptComparisonProps {
63
64
  refundQuoteLoading?: boolean;
64
65
  sourceProductName?: string | null;
65
66
  targetProductName?: string | null;
67
+ adminCustomReceiptLines?: AdminCustomReceiptLine[];
68
+ onSetReceiptLineOverride?: (input: {
69
+ componentId: string;
70
+ lineKey: string;
71
+ label: string;
72
+ baseAmount: number;
73
+ targetAmount: number;
74
+ }) => void;
66
75
  }
67
76
 
68
77
  function normalizeLocale(locale: string): Locale {
@@ -117,7 +126,10 @@ export function AdminChangeReceiptComparison({
117
126
  removedValueByOperationId = {},
118
127
  refundDispositionsByOperationId = {},
119
128
  refundQuoteLoading = false,
129
+ adminCustomReceiptLines = [],
130
+ onSetReceiptLineOverride,
120
131
  }: AdminChangeReceiptComparisonProps) {
132
+ const [overrideDrafts, setOverrideDrafts] = useState<Record<string, string>>({});
121
133
  const readableQuoteError = friendlyChangeBookingError(quoteError);
122
134
  const displayLocale = normalizeLocale(locale);
123
135
  const existingLines = originalLines(originalReceipt);
@@ -202,9 +214,59 @@ export function AdminChangeReceiptComparison({
202
214
  existingLines,
203
215
  familyConversionReceiptLines,
204
216
  );
205
- const summaryDisplayLines = isProductFamilyConversion
217
+ const rawSummaryDisplayLines = isProductFamilyConversion
206
218
  ? familyConversionReceiptLines
207
219
  : amendmentDisplayLines;
220
+ const overrideByComponentId = new Map(
221
+ adminCustomReceiptLines.flatMap((line) =>
222
+ line.overrideComponentId ? [[line.overrideComponentId, line] as const] : [],
223
+ ),
224
+ );
225
+ const summaryDisplayLines = rawSummaryDisplayLines
226
+ .filter((line) => !(line.kind === 'line' && line.label.startsWith('Override · ')))
227
+ .map((line) => {
228
+ const type = line.kind === 'ticket' ? 'TICKET' : String(line.type ?? '').toUpperCase();
229
+ const protectedType = ['TAX', 'PROMO_CODE', 'DISCOUNT', 'GIFT_CARD', 'PAYMENT_CREDIT', 'BOOKING_CHANGE']
230
+ .includes(type);
231
+ return {
232
+ ...line,
233
+ editable: Boolean(
234
+ !protectedType && line.componentId && line.lineKey && onSetReceiptLineOverride,
235
+ ),
236
+ };
237
+ });
238
+ const quotedOverrideAmountInputs = Object.fromEntries(
239
+ summaryDisplayLines.flatMap((line) => {
240
+ if (!line.componentId || !line.lineKey) return [];
241
+ const baseAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
242
+ const override = overrideByComponentId.get(line.componentId);
243
+ const target = override
244
+ ? baseAmount + (override.amountSign ?? 1) * Number(override.amountInput)
245
+ : baseAmount;
246
+ return [[line.lineKey, Number.isFinite(target) ? target.toFixed(2) : baseAmount.toFixed(2)]];
247
+ }),
248
+ );
249
+ const handleOverrideChange = (lineKey: string, value: string) => {
250
+ setOverrideDrafts((current) => ({ ...current, [lineKey]: value }));
251
+ };
252
+ const handleOverrideBlur = (lineKey: string) => {
253
+ const line = summaryDisplayLines.find((candidate) => candidate.lineKey === lineKey);
254
+ const targetAmount = Number(overrideDrafts[lineKey] ?? quotedOverrideAmountInputs[lineKey]);
255
+ if (!line?.componentId || !Number.isFinite(targetAmount)) return;
256
+ const baseAmount = line.kind === 'ticket' ? line.itemTotal : line.amount;
257
+ onSetReceiptLineOverride?.({
258
+ componentId: line.componentId,
259
+ lineKey,
260
+ label: line.kind === 'ticket' ? line.category : line.label,
261
+ baseAmount,
262
+ targetAmount,
263
+ });
264
+ setOverrideDrafts((current) => {
265
+ const next = { ...current };
266
+ delete next[lineKey];
267
+ return next;
268
+ });
269
+ };
208
270
  const summarySubtotal = isProductFamilyConversion
209
271
  ? familyConversionReceiptLines.reduce((sum, line) => {
210
272
  if (line.kind === 'line' && String(line.type ?? '').toUpperCase() === 'TAX') return sum;
@@ -313,6 +375,9 @@ export function AdminChangeReceiptComparison({
313
375
  subtotal={summarySubtotal}
314
376
  size="sm"
315
377
  t={t}
378
+ lineAmountInputs={{ ...quotedOverrideAmountInputs, ...overrideDrafts }}
379
+ onLineAmountInputChange={handleOverrideChange}
380
+ onLineAmountInputBlur={handleOverrideBlur}
316
381
  />
317
382
  )}
318
383
  </section>
@@ -68,7 +68,7 @@
68
68
  min-height: 100% !important;
69
69
  }
70
70
 
71
- /* Constrain next-video BackgroundPlayer output */
71
+ /* Constrain the shared background video output. */
72
72
  .videoSlot :global(video),
73
73
  .videoSlot :global(.next-video-bg-video),
74
74
  .videoSlot :global(.next-video-bg-poster),
@@ -2,7 +2,7 @@
2
2
  'use client';
3
3
 
4
4
  import { useState, useRef, useEffect, useCallback } from 'react';
5
- import BackgroundPlayer from 'next-video/background-player';
5
+ import { BackgroundVideo } from '../BackgroundVideo';
6
6
  import type { VideoSources } from '../../constants/products';
7
7
  import { resolvePublicAssetUrl, useBookingHost } from '../../runtime';
8
8
  import { useTranslations } from '../../lib/booking/i18n';
@@ -100,9 +100,11 @@ export function BookingFlowCollage({
100
100
  <div className={`${styles.collage} ${!hasGridImages ? styles.videoOnly : ''}`}>
101
101
  <div className={styles.videoSlot}>
102
102
  <div className={styles.videoWrapper}>
103
- <BackgroundPlayer
103
+ <BackgroundVideo
104
104
  ref={videoRef}
105
105
  {...videoForCollage}
106
+ loading="eager"
107
+ preload="auto"
106
108
  autoPlay
107
109
  muted
108
110
  loop
@@ -10,7 +10,7 @@ import {
10
10
  import { resolvePublicAssetUrl, useBookingHost } from '../../runtime';
11
11
  import { useLocale, useTranslations } from '../../lib/booking/i18n';
12
12
  import type { Product } from '../../constants/products';
13
- import BackgroundPlayer from 'next-video/background-player';
13
+ import { BackgroundVideo } from '../BackgroundVideo';
14
14
  import styles from './BookingProductGrid.module.css';
15
15
 
16
16
  /** Locale strings needed by the product grid tiles (Via Via host). */
@@ -48,7 +48,7 @@ const FILTER_PRODUCT_IDS: Record<string, string[]> = {
48
48
  export const FILTER_IDS = ['all', 'sunrise', 'moraine-lake', 'lake-louise', 'emerald-lake', 'private'] as const;
49
49
  export type FilterId = (typeof FILTER_IDS)[number];
50
50
 
51
- /** Only `src` / `webm` — never spread catalog `VideoSources` onto BackgroundPlayer (`longSrc`/`longWebm` are not valid DOM props). */
51
+ /** Only `src` / `webm` — never spread catalog long-source fields onto the DOM. */
52
52
  function backgroundPlayerSources(
53
53
  product: Product,
54
54
  staticAssetOrigin: string,
@@ -220,8 +220,10 @@ function BookingProductTileExpanded({
220
220
  <path d="M10 6l8 6-8 6" />
221
221
  </motion.svg>
222
222
  </button>
223
- <BackgroundPlayer
223
+ <BackgroundVideo
224
224
  {...backgroundPlayerSources(product, env.STATIC_ASSET_ORIGIN)}
225
+ loading="eager"
226
+ preload="auto"
225
227
  autoPlay
226
228
  muted
227
229
  loop
@@ -428,6 +428,7 @@ export default function ChangeBookingDialog({
428
428
  currency={receiptCurrency}
429
429
  contentRef={contentRef}
430
430
  bookingSourceAttribution={bookingSourceAttribution}
431
+ availabilityPricingProfileId={booking.pricingProfileId ?? null}
431
432
  onSuccess={() => {
432
433
  onChangeCompleted?.(newBookingPreview);
433
434
  onClose();
@@ -16,6 +16,7 @@ export type PriceSummaryLine =
16
16
  lineKey?: string;
17
17
  /** Pricing V2 amendment operation that produced this row, when applicable. */
18
18
  operationId?: string;
19
+ componentId?: string;
19
20
  editable?: boolean;
20
21
  category: string;
21
22
  qty: number;
@@ -34,6 +35,7 @@ export type PriceSummaryLine =
34
35
  | {
35
36
  kind: 'line';
36
37
  lineKey?: string;
38
+ componentId?: string;
37
39
  label: string;
38
40
  amount: number;
39
41
  /** Drives styling: discount (red, -), return add-on (green, +), default (stone). Receipt types: TICKET, FEE, RETURN_OPTION, PROMO_CODE, CANCELLATION_UPGRADE, TAX, etc. */
@@ -19,6 +19,9 @@ export type AdminCustomReceiptLine = {
19
19
  sourceAdjustmentId?: string;
20
20
  replacementAdjustmentId?: string;
21
21
  isDirty?: boolean;
22
+ overrideComponentId?: string;
23
+ overrideBaseAmount?: number;
24
+ overrideLineKey?: string;
22
25
  };
23
26
 
24
27
  type AdminCustomReceiptLinePatch = Partial<
@@ -146,6 +149,40 @@ export function useAdminCustomReceiptLines(
146
149
  setAppliedRemovedExistingAdjustmentIds(removedExistingAdjustmentIds);
147
150
  }, [adminCustomReceiptLines, removedExistingAdjustmentIds]);
148
151
 
152
+ const handleSetAdminReceiptLineOverride = useCallback((input: {
153
+ componentId: string;
154
+ lineKey: string;
155
+ label: string;
156
+ baseAmount: number;
157
+ targetAmount: number;
158
+ }) => {
159
+ const delta = Math.round((input.targetAmount - input.baseAmount) * 100) / 100;
160
+ const existing = adminCustomReceiptLines.find(
161
+ (line) => line.overrideComponentId === input.componentId,
162
+ );
163
+ const next = Math.abs(delta) < 0.005
164
+ ? adminCustomReceiptLines.filter((line) => line.id !== existing?.id)
165
+ : [
166
+ ...adminCustomReceiptLines.filter((line) => line.id !== existing?.id),
167
+ {
168
+ id: existing?.id ?? nextDraftId('admin-rcpt'),
169
+ label: `Override · ${input.label}`,
170
+ amountInput: Math.abs(delta).toFixed(2),
171
+ amountSign: (delta < 0 ? -1 : 1) as 1 | -1,
172
+ calculation: 'FIXED_AMOUNT' as const,
173
+ scope: 'SELECTED_COMPONENTS' as const,
174
+ selectedComponentIds: [input.componentId],
175
+ taxBehavior: delta < 0 ? 'PRE_TAX_DISCOUNT' as const : 'TAXABLE' as const,
176
+ reasonCode: 'LINE_ITEM_OVERRIDE',
177
+ overrideComponentId: input.componentId,
178
+ overrideBaseAmount: input.baseAmount,
179
+ overrideLineKey: input.lineKey,
180
+ },
181
+ ];
182
+ setAdminCustomReceiptLines(next);
183
+ setAppliedAdminCustomReceiptLines(next);
184
+ }, [adminCustomReceiptLines, nextDraftId]);
185
+
149
186
  useEffect(() => {
150
187
  if (previousResetKeyRef.current === resetKey) return;
151
188
  previousResetKeyRef.current = resetKey;
@@ -225,5 +262,6 @@ export function useAdminCustomReceiptLines(
225
262
  handleUpdateAdminCustomReceiptLine,
226
263
  handleRemoveAdminCustomReceiptLine,
227
264
  handleApplyAdminCustomReceiptLines,
265
+ handleSetAdminReceiptLineOverride,
228
266
  };
229
267
  }
package/src/index.ts CHANGED
@@ -62,6 +62,8 @@ export type {
62
62
  export { formatBookingRefForDisplay } from './lib/format-booking-ref';
63
63
  export { default as BookingProductGrid } from './components/booking/BookingProductGrid';
64
64
  export { DefaultTermsContent as TermsContent } from './components/booking/DefaultTermsContent';
65
+ export { BackgroundVideo } from './components/BackgroundVideo';
66
+ export type { BackgroundVideoLoading, BackgroundVideoProps } from './components/BackgroundVideo';
65
67
 
66
68
  export type {
67
69
  BookingAppMode,
@@ -212,7 +212,9 @@ export function mapQuoteLineItemsToPriceSummaryLines(
212
212
  : type;
213
213
  out.push({
214
214
  kind: 'ticket',
215
+ lineKey: isPricingV2Line(item) ? item.lineId ?? undefined : undefined,
215
216
  operationId: isPricingV2Line(item) ? item.metadata?.operationId : undefined,
217
+ componentId: isPricingV2Line(item) ? item.metadata?.componentId : undefined,
216
218
  category,
217
219
  qty: qty > 0 ? qty : 1,
218
220
  itemTotal: amount,
@@ -234,7 +236,9 @@ export function mapQuoteLineItemsToPriceSummaryLines(
234
236
  ) {
235
237
  out.push({
236
238
  kind: 'ticket',
239
+ lineKey: isPricingV2Line(item) ? item.lineId ?? undefined : undefined,
237
240
  operationId: isPricingV2Line(item) ? item.metadata?.operationId : undefined,
241
+ componentId: isPricingV2Line(item) ? item.metadata?.componentId : undefined,
238
242
  category: lettersOnly,
239
243
  qty,
240
244
  itemTotal: amount,
@@ -247,6 +251,8 @@ export function mapQuoteLineItemsToPriceSummaryLines(
247
251
  /** Align with checkout drift keys (`ChangeBookingFlow` uses `type: 'return'`). */
248
252
  out.push({
249
253
  kind: 'line',
254
+ lineKey: isPricingV2Line(item) ? item.lineId ?? undefined : undefined,
255
+ componentId: isPricingV2Line(item) ? item.metadata?.componentId : undefined,
250
256
  label: label || type || 'Line',
251
257
  amount,
252
258
  type,
@@ -240,7 +240,7 @@
240
240
  "home": {
241
241
  "hero": {
242
242
  "tagline": "Tours to Moraine Lake, Lake Louise, Emerald Lake & more for families, friends, and unforgettable days",
243
- "googleReviewsCount": "1500+ reviews",
243
+ "googleReviewsCount": "2500+ reviews",
244
244
  "centerCallout": "now open ✨"
245
245
  },
246
246
  "seasonClosure": {
@@ -39,7 +39,7 @@
39
39
  "home": {
40
40
  "hero": {
41
41
  "tagline": "Descubre Moraine Lake, Lake Louise, y mucho más. Excursiones inolvidables para todos los públicos.",
42
- "googleReviewsCount": "Mas de 1500 opioniones",
42
+ "googleReviewsCount": "Mas de 2500 opioniones",
43
43
  "centerCallout": "ahora abierto ✨"
44
44
  },
45
45
  "seasonClosure": {
@@ -39,7 +39,7 @@
39
39
  "home": {
40
40
  "hero": {
41
41
  "tagline": "Partez à la découverte du lac Moraine, du lac Louise, du lac Émeraude et plus encore ! Excursions en famille ou entre amis, pour vivre une expérience inoubliable",
42
- "googleReviewsCount": "1500+ avis",
42
+ "googleReviewsCount": "2500+ avis",
43
43
  "centerCallout": "maintenant ouvert ✨"
44
44
  },
45
45
  "seasonClosure": {
@@ -314,6 +314,28 @@ test('legacy booking-change comparison rows are omitted from the purchase breakd
314
314
  );
315
315
  });
316
316
 
317
+ test('Pricing V2 amendment rows retain line and component identity for admin overrides', () => {
318
+ const [line] = mapQuoteLineItemsToPriceSummaryLines([
319
+ {
320
+ lineId: 'component_adult:amendment:gross',
321
+ type: 'TICKET',
322
+ label: 'Refund · ADULT',
323
+ amount: -197.99,
324
+ quantity: 9,
325
+ billable: true,
326
+ metadata: {
327
+ category: 'ADULT',
328
+ operationId: 'op_move_adult',
329
+ componentId: 'component_adult',
330
+ },
331
+ },
332
+ ]);
333
+
334
+ assert.equal(line?.lineKey, 'component_adult:amendment:gross');
335
+ assert.equal(line?.componentId, 'component_adult');
336
+ assert.equal(line?.kind === 'ticket' ? line.operationId : null, 'op_move_adult');
337
+ });
338
+
317
339
  test('server preview exposes the resulting receipt separately from ledger allocation lines', () => {
318
340
  const preview = buildChangeBookingServerPreview(
319
341
  quote({