@voyant-travel/bookings-react 0.201.1 → 0.203.0
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/README.md +8 -8
- package/dist/admin/bookings-host.d.ts +1 -2
- package/dist/admin/bookings-host.js +2 -3
- package/dist/admin/index.d.ts +6 -44
- package/dist/admin/index.js +2 -65
- package/dist/components/booking-create-utils.d.ts +1 -1
- package/dist/components/booking-create-utils.js +5 -17
- package/dist/components/booking-dialog.d.ts +4 -19
- package/dist/components/booking-dialog.js +3 -12
- package/dist/components/booking-list.d.ts +1 -2
- package/dist/components/booking-list.js +5 -13
- package/dist/components/bookings-page.d.ts +1 -2
- package/dist/components/bookings-page.js +2 -2
- package/dist/hooks/booking-create-types.d.ts +34 -0
- package/dist/hooks/booking-create-types.js +1 -0
- package/dist/hooks/index.d.ts +2 -4
- package/dist/hooks/index.js +1 -4
- package/dist/hooks/use-booking-mutation.d.ts +3 -65
- package/dist/hooks/use-booking-mutation.js +1 -10
- package/dist/i18n/en-create-list.d.ts +0 -1
- package/dist/i18n/en-create-list.js +0 -1
- package/dist/i18n/en.d.ts +0 -1
- package/dist/i18n/messages-create-list.d.ts +0 -1
- package/dist/i18n/provider.d.ts +0 -2
- package/dist/i18n/ro-create-list.d.ts +0 -1
- package/dist/i18n/ro-create-list.js +0 -1
- package/dist/i18n/ro.d.ts +0 -1
- package/dist/journey/components/booking-journey-rules.d.ts +0 -22
- package/dist/journey/components/booking-journey-rules.js +0 -54
- package/dist/journey/components/journey-steps/payment-step.js +1 -1
- package/dist/journey/components/journey-steps/shared.d.ts +3 -3
- package/dist/journey/components/journey-steps.d.ts +1 -1
- package/dist/journey/components/journey-steps.js +1 -1
- package/dist/journey/index.d.ts +2 -12
- package/dist/journey/index.js +1 -11
- package/dist/journey/types.d.ts +3 -208
- package/dist/journey/types.js +2 -6
- package/dist/storefront/index.d.ts +0 -2
- package/dist/storefront/index.js +0 -2
- package/dist/ui.d.ts +0 -2
- package/dist/ui.js +0 -2
- package/package.json +33 -33
- package/dist/admin/booking-journey-host.d.ts +0 -23
- package/dist/admin/booking-journey-host.js +0 -280
- package/dist/admin/journey-billing-duplicate-warning.d.ts +0 -2
- package/dist/admin/journey-billing-duplicate-warning.js +0 -26
- package/dist/admin/journey-departure-picker.d.ts +0 -6
- package/dist/admin/journey-departure-picker.js +0 -91
- package/dist/admin/journey-travel-credit-picker.d.ts +0 -2
- package/dist/admin/journey-travel-credit-picker.js +0 -72
- package/dist/admin/journey-units-picker.d.ts +0 -10
- package/dist/admin/journey-units-picker.js +0 -60
- package/dist/admin/pages/booking-journey-page.d.ts +0 -11
- package/dist/admin/pages/booking-journey-page.js +0 -26
- package/dist/admin/pages/booking-new-page.d.ts +0 -15
- package/dist/admin/pages/booking-new-page.js +0 -50
- package/dist/components/booking-create-page.d.ts +0 -13
- package/dist/components/booking-create-page.js +0 -11
- package/dist/components/booking-create-sheet.d.ts +0 -34
- package/dist/components/booking-create-sheet.js +0 -815
- package/dist/hooks/use-booking-convert-mutation.d.ts +0 -80
- package/dist/hooks/use-booking-convert-mutation.js +0 -24
- package/dist/hooks/use-booking-create-mutation.d.ts +0 -336
- package/dist/hooks/use-booking-create-mutation.js +0 -43
- package/dist/hooks/use-booking-dual-create-mutation.d.ts +0 -337
- package/dist/hooks/use-booking-dual-create-mutation.js +0 -45
- package/dist/journey/components/booking-journey.d.ts +0 -2
- package/dist/journey/components/booking-journey.js +0 -485
- package/dist/storefront/storefront-booking-errors.d.ts +0 -12
- package/dist/storefront/storefront-booking-errors.js +0 -50
- package/dist/storefront/storefront-booking-journey.d.ts +0 -71
- package/dist/storefront/storefront-booking-journey.js +0 -327
- package/dist/storefront/storefront-booking-page.d.ts +0 -35
- package/dist/storefront/storefront-booking-page.js +0 -215
- package/dist/storefront/storefront-checkout-bodies.d.ts +0 -36
- package/dist/storefront/storefront-checkout-bodies.js +0 -69
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
// agent-quality: file-size exception -- owner: bookings-react; existing UI surface stays co-located until a dedicated split preserves behavior and tests.
|
|
2
|
-
"use client";
|
|
3
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
4
|
-
import { useBookingCommit, useBookingDraft, useBookingDraftShape, useBookingHold, useBookingQuote, } from "@voyant-travel/catalog-react/booking-engine";
|
|
5
|
-
import { Button } from "@voyant-travel/ui/components/button";
|
|
6
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
-
import { useBookingsUiMessagesOrDefault } from "../../i18n/index.js";
|
|
8
|
-
import { emptyDraft, totalPax } from "../lib/draft-state.js";
|
|
9
|
-
import { findPaidScheduleRowsMissingPaymentDate } from "../lib/payment-schedule.js";
|
|
10
|
-
import { JOURNEY_STEP_ORDER, } from "../types.js";
|
|
11
|
-
import { buildCommitParty, buildCommitPaymentIntent, canAdvanceFromStep, defaultMinimalShape, isStepVisible, makeHoldSignature, resolveInitialStatus, stackedStepComplete, validationErrorsForStep, warningsForStep, } from "./booking-journey-rules.js";
|
|
12
|
-
import { ConfigureStepSkeleton } from "./configure-step-skeleton.js";
|
|
13
|
-
import { ContractPreviewDialog } from "./contract-preview-dialog.js";
|
|
14
|
-
import { AccommodationStep, AddonsStep, BillingStep, DepartureStep, DocumentsStep, deriveDefaultPhoneCountry, FinalizeControls, OptionsStep, PaymentStep, ReviewStep, TravelersStep, } from "./journey-steps.js";
|
|
15
|
-
import { PriceSidePanel } from "./side-panel.js";
|
|
16
|
-
import { StackedJourney } from "./stacked-journey.js";
|
|
17
|
-
import { StepHeader } from "./step-header.js";
|
|
18
|
-
export function BookingJourney(props) {
|
|
19
|
-
const messages = useBookingsUiMessagesOrDefault();
|
|
20
|
-
// Default phone country: the explicit prop, else the scope locale's region
|
|
21
|
-
// (storefronts thread the shopper locale here even when the bookings-ui i18n
|
|
22
|
-
// provider isn't mounted). `PhoneField` fills in the i18n locale + GB fallback
|
|
23
|
-
// when this is undefined.
|
|
24
|
-
const defaultPhoneCountry = deriveDefaultPhoneCountry(props.defaultPhoneCountry, props.scope?.locale);
|
|
25
|
-
const surface = props.surface ?? "admin";
|
|
26
|
-
// Admin books on a single stacked page (nothing hidden); the storefront
|
|
27
|
-
// keeps the guided one-step-at-a-time wizard. Two deliberately separate
|
|
28
|
-
// flows — see BookingJourneyProps.layout.
|
|
29
|
-
const layout = props.layout ?? (surface === "admin" ? "stacked" : "wizard");
|
|
30
|
-
const [draft, setDraft] = useState(() => {
|
|
31
|
-
const base = emptyDraft({
|
|
32
|
-
module: props.entityModule,
|
|
33
|
-
id: props.entityId,
|
|
34
|
-
// Empty when storefront — the public engine route resolves
|
|
35
|
-
// it server-side from (entityModule, entityId).
|
|
36
|
-
sourceKind: props.sourceKind ?? "",
|
|
37
|
-
sourceConnectionId: props.sourceConnectionId,
|
|
38
|
-
sourceRef: props.sourceRef,
|
|
39
|
-
}, { buyerType: props.defaultBuyerType ?? (surface === "admin" ? "B2B" : "B2C") });
|
|
40
|
-
// Seed Configure when the caller passed pre-locked state —
|
|
41
|
-
// detail page picks departure + pax, booking flow only handles
|
|
42
|
-
// travelers + addons + payment.
|
|
43
|
-
if (props.initialConfigure) {
|
|
44
|
-
const seed = props.initialConfigure;
|
|
45
|
-
const seedPax = seed.pax;
|
|
46
|
-
base.configure = Object.assign({}, base.configure, seed, {
|
|
47
|
-
pax: { ...base.configure.pax, ...(seedPax ?? {}) },
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
if (props.initialAccommodation) {
|
|
51
|
-
const seed = props.initialAccommodation;
|
|
52
|
-
base.accommodation = Object.assign({ rooms: [], travelerAssignments: {} }, base.accommodation ?? {}, seed);
|
|
53
|
-
}
|
|
54
|
-
return base;
|
|
55
|
-
});
|
|
56
|
-
const fallbackShape = useMemo(() => props.fallbackShape ?? defaultMinimalShape(), [props.fallbackShape]);
|
|
57
|
-
// Server-side draft sync — PUT on each step transition. The shell
|
|
58
|
-
// doesn't read from the server in Phase B (drafts are recovery
|
|
59
|
-
// surface, not source of truth) but the wire is in place.
|
|
60
|
-
const draftSync = useBookingDraft({
|
|
61
|
-
surface,
|
|
62
|
-
draftId: props.draftId,
|
|
63
|
-
enableLoad: false,
|
|
64
|
-
});
|
|
65
|
-
// Live quote — debounced 250ms.
|
|
66
|
-
const quote = useBookingQuote({
|
|
67
|
-
surface,
|
|
68
|
-
draft,
|
|
69
|
-
scope: props.scope,
|
|
70
|
-
});
|
|
71
|
-
const shape = useBookingDraftShape({
|
|
72
|
-
surface,
|
|
73
|
-
quote: quote.data,
|
|
74
|
-
fallback: fallbackShape,
|
|
75
|
-
});
|
|
76
|
-
// Step navigation — only show steps the descriptor says are relevant.
|
|
77
|
-
const hideConfigure = props.hideConfigure === true;
|
|
78
|
-
const steps = useMemo(() => JOURNEY_STEP_ORDER.filter((s) => {
|
|
79
|
-
// `hideConfigure` skips the configure phase — now split across the
|
|
80
|
-
// Departure + Options steps.
|
|
81
|
-
if (hideConfigure && (s === "departure" || s === "options"))
|
|
82
|
-
return false;
|
|
83
|
-
// Internal notes + document generation are operator-only.
|
|
84
|
-
if (s === "documents" && surface !== "admin")
|
|
85
|
-
return false;
|
|
86
|
-
return isStepVisible(s, shape);
|
|
87
|
-
}), [shape, hideConfigure, surface]);
|
|
88
|
-
// The stacked admin layout drops the Review block — the side panel shows the
|
|
89
|
-
// live summary + Confirm at all times, so a separate review section is
|
|
90
|
-
// redundant.
|
|
91
|
-
const stackedSteps = useMemo(() => steps.filter((s) => s !== "review"), [steps]);
|
|
92
|
-
const [currentStep, setCurrentStep] = useState(() => steps[0] ?? "departure");
|
|
93
|
-
const [visited, setVisited] = useState(() => new Set([steps[0] ?? "departure"]));
|
|
94
|
-
// If the descriptor changes and removes the current step, reset to
|
|
95
|
-
// the first available step. (Edge case: shape goes from
|
|
96
|
-
// owned→sourced and the relevant step set narrows.)
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (!steps.includes(currentStep)) {
|
|
99
|
-
setCurrentStep(steps[0] ?? "departure");
|
|
100
|
-
}
|
|
101
|
-
}, [steps, currentStep]);
|
|
102
|
-
// PUT the draft to the server to keep the recovery surface fresh
|
|
103
|
-
// without saving on every keystroke. Wizard saves on each step
|
|
104
|
-
// transition; the stacked admin page has no steps, so it saves on
|
|
105
|
-
// each settled quote (a natural, debounced cadence). The mutation
|
|
106
|
-
// reads the latest draft + quote from the closure on each fire.
|
|
107
|
-
const saveTrigger = layout === "wizard" ? currentStep : (quote.data?.quoteId ?? "");
|
|
108
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional — fires on save trigger only
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
draftSync.save.mutate({
|
|
111
|
-
draft: { ...draft, quoteId: quote.data?.quoteId },
|
|
112
|
-
currentStep,
|
|
113
|
-
currentQuoteId: quote.data?.quoteId,
|
|
114
|
-
});
|
|
115
|
-
}, [saveTrigger]);
|
|
116
|
-
// Commit
|
|
117
|
-
const commit = useBookingCommit({
|
|
118
|
-
surface,
|
|
119
|
-
draftId: props.draftId,
|
|
120
|
-
onCommitted: props.onCommitted,
|
|
121
|
-
});
|
|
122
|
-
// Inventory hold — fired when the user advances past Configure
|
|
123
|
-
// with a slot + pax picked. Failures are non-blocking (the engine
|
|
124
|
-
// re-validates capacity at commit time anyway); we just don't
|
|
125
|
-
// want to silently let two shoppers race past Configure with one
|
|
126
|
-
// capacity unit between them.
|
|
127
|
-
const holdApi = useBookingHold({ surface });
|
|
128
|
-
const holdState = useRef({ transition: Promise.resolve() });
|
|
129
|
-
const holdSignature = makeHoldSignature(draft, props.entityModule, props.entityId);
|
|
130
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: signature change is the only trigger; refs + closure read latest values -- owner: bookings-react; existing suppression is intentional pending typed cleanup.
|
|
131
|
-
useEffect(() => {
|
|
132
|
-
// Wizard: don't hold while still on the configure steps. Stacked:
|
|
133
|
-
// everything's on one page, so the signature (slot + pax present)
|
|
134
|
-
// is the trigger.
|
|
135
|
-
if (layout === "wizard" && (currentStep === "departure" || currentStep === "options"))
|
|
136
|
-
return;
|
|
137
|
-
if (!holdSignature)
|
|
138
|
-
return;
|
|
139
|
-
if (holdState.current.signature === holdSignature)
|
|
140
|
-
return;
|
|
141
|
-
// Inquiry mode is the lead-form path — capture the lead without
|
|
142
|
-
// burning capacity. The operator follows up before any inventory
|
|
143
|
-
// is touched.
|
|
144
|
-
if (draft.payment.intent === "inquiry")
|
|
145
|
-
return;
|
|
146
|
-
holdState.current.signature = holdSignature;
|
|
147
|
-
holdState.current.transition = holdState.current.transition.then(async () => {
|
|
148
|
-
// A newer selection can arrive while this transition is queued. Skip
|
|
149
|
-
// stale work before it reaches inventory.
|
|
150
|
-
if (holdState.current.signature !== holdSignature)
|
|
151
|
-
return;
|
|
152
|
-
const previousToken = holdState.current.holdToken;
|
|
153
|
-
if (previousToken) {
|
|
154
|
-
holdState.current.holdToken = undefined;
|
|
155
|
-
await holdApi
|
|
156
|
-
.release({ entityModule: props.entityModule, holdToken: previousToken })
|
|
157
|
-
.catch(() => { });
|
|
158
|
-
}
|
|
159
|
-
// Re-check after the release: a newer transition may now be waiting.
|
|
160
|
-
if (holdState.current.signature !== holdSignature)
|
|
161
|
-
return;
|
|
162
|
-
try {
|
|
163
|
-
const result = await holdApi.place({
|
|
164
|
-
entityModule: props.entityModule,
|
|
165
|
-
entityId: props.entityId,
|
|
166
|
-
draftId: props.draftId,
|
|
167
|
-
parameters: {
|
|
168
|
-
slotId: draft.configure.departureSlotId,
|
|
169
|
-
paxCount: totalPax(draft),
|
|
170
|
-
productId: props.entityId,
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
if (holdState.current.signature === holdSignature) {
|
|
174
|
-
holdState.current.holdToken = result.holdToken;
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
// The placement completed after the selection changed. Release it
|
|
178
|
-
// before the queued replacement starts so token-wide release cannot
|
|
179
|
-
// delete that replacement.
|
|
180
|
-
await holdApi
|
|
181
|
-
.release({ entityModule: props.entityModule, holdToken: result.holdToken })
|
|
182
|
-
.catch(() => { });
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
// Non-blocking — see comment above.
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
}, [holdSignature, currentStep]);
|
|
189
|
-
const available = quote.data?.available !== false;
|
|
190
|
-
// A failed quote (e.g. the connector adapter 500s) leaves `quote.data` null,
|
|
191
|
-
// which makes `available` read as true and lets a stale/absent price slip
|
|
192
|
-
// through to Review where Confirm would silently no-op. Treat a quote error
|
|
193
|
-
// as a hard block: gate Next/Confirm and surface a recoverable banner + retry.
|
|
194
|
-
const hasQuoteError = quote.error != null;
|
|
195
|
-
// A settled quote (no thrown error, a real `quoteId`) can still be
|
|
196
|
-
// un-committable: the owned accommodation handler returns `available: true`
|
|
197
|
-
// with `invalidReason: "rates_missing"` and no pricing when the selected stay
|
|
198
|
-
// has no applicable rate plan. Committing that yields a 502 RESERVE_FAILED at
|
|
199
|
-
// /book (#2638). Treat any settled quote that is explicitly unavailable or
|
|
200
|
-
// carries an `invalidReason` as un-priceable and block contract acceptance /
|
|
201
|
-
// Confirm against it.
|
|
202
|
-
const quoteUnpriceable = quote.data != null &&
|
|
203
|
-
(quote.data.available === false ||
|
|
204
|
-
(quote.data.invalidReason != null && quote.data.invalidReason !== ""));
|
|
205
|
-
const quoteBlocked = hasQuoteError || quoteUnpriceable;
|
|
206
|
-
// The pristine baseline quote is intentionally un-priceable: a product has no
|
|
207
|
-
// price until its price driver — a room, or a traveler count — is chosen, and
|
|
208
|
-
// the engine reports that empty draft as `no_sell_amount_configured`. Suppress
|
|
209
|
-
// the un-priceable banner (and the side-panel error) for exactly that baseline
|
|
210
|
-
// so step 1 doesn't scream "can't be priced" before the buyer has picked
|
|
211
|
-
// anything. Any other reason (e.g. `rates_missing` on an already-picked stay,
|
|
212
|
-
// #2638), or the same code once a driver IS picked (a room that carries no
|
|
213
|
-
// price), still surfaces. Commit stays gated on `quoteBlocked` regardless, so
|
|
214
|
-
// an unpriced booking can never be submitted.
|
|
215
|
-
const priceDriversConfigured = (draft?.configure?.optionSelections?.length ?? 0) > 0 ||
|
|
216
|
-
Object.values(draft?.configure?.pax ?? {}).reduce((sum, n) => sum + (n ?? 0), 0) > 0;
|
|
217
|
-
const isPristineUnconfigured = quote.data?.invalidReason === "no_sell_amount_configured" && !priceDriversConfigured;
|
|
218
|
-
const showQuoteUnpriceable = quoteBlocked && !isPristineUnconfigured;
|
|
219
|
-
const quoteReady = Boolean(quote.data?.quoteId);
|
|
220
|
-
// Step navigation only hard-blocks on a thrown quote error (a transient fetch
|
|
221
|
-
// failure that a retry fixes). An un-priceable quote (e.g. `rates_missing`
|
|
222
|
-
// from a preselected room) is *corrected by navigating* — often the room/rate
|
|
223
|
-
// editor is a later step — so Next must stay enabled; commit is still gated on
|
|
224
|
-
// `quoteBlocked` below so an unpriced booking can never be submitted.
|
|
225
|
-
const canAdvance = canAdvanceFromStep(currentStep, draft, shape, available) && !hasQuoteError;
|
|
226
|
-
const warnings = warningsForStep(currentStep, draft, shape, messages);
|
|
227
|
-
// Stacked layout: there's no "current" step, but the section nav still
|
|
228
|
-
// nudges toward the first thing that isn't done yet, and the final
|
|
229
|
-
// Confirm is gated until every section passes its check.
|
|
230
|
-
const firstIncomplete = useMemo(() => stackedSteps.find((s) => !stackedStepComplete(s, draft, shape, available)) ??
|
|
231
|
-
stackedSteps[stackedSteps.length - 1] ??
|
|
232
|
-
stackedSteps[0] ??
|
|
233
|
-
"departure", [stackedSteps, draft, shape, available]);
|
|
234
|
-
const canCommit = useMemo(() => stackedSteps.every((s) => canAdvanceFromStep(s, draft, shape, available)) &&
|
|
235
|
-
quoteReady &&
|
|
236
|
-
!quoteBlocked, [stackedSteps, draft, shape, available, quoteReady, quoteBlocked]);
|
|
237
|
-
const [isAdvanceGuardPending, setIsAdvanceGuardPending] = useState(false);
|
|
238
|
-
const [advanceGuardError, setAdvanceGuardError] = useState(null);
|
|
239
|
-
// Set when Confirm can't proceed because there's no valid quote — makes the
|
|
240
|
-
// click a visible, explained block instead of a silent no-op.
|
|
241
|
-
const [confirmError, setConfirmError] = useState(null);
|
|
242
|
-
// Clear the "no valid quote" block as soon as a fresh quote settles (via
|
|
243
|
-
// retry or an auto re-quote), so the message doesn't linger once resolved.
|
|
244
|
-
useEffect(() => {
|
|
245
|
-
if (quote.data?.quoteId)
|
|
246
|
-
setConfirmError(null);
|
|
247
|
-
}, [quote.data?.quoteId]);
|
|
248
|
-
const idx = steps.indexOf(currentStep);
|
|
249
|
-
const next = steps[idx + 1];
|
|
250
|
-
const prev = steps[idx - 1];
|
|
251
|
-
const advance = async () => {
|
|
252
|
-
if (!next || !canAdvance || isAdvanceGuardPending)
|
|
253
|
-
return;
|
|
254
|
-
setAdvanceGuardError(null);
|
|
255
|
-
if (props.onBeforeStepAdvance) {
|
|
256
|
-
setIsAdvanceGuardPending(true);
|
|
257
|
-
try {
|
|
258
|
-
const guardResult = await props.onBeforeStepAdvance({
|
|
259
|
-
currentStep,
|
|
260
|
-
nextStep: next,
|
|
261
|
-
draft,
|
|
262
|
-
pricing: quote.data?.pricing ?? null,
|
|
263
|
-
quoteId: quote.data?.quoteId,
|
|
264
|
-
surface,
|
|
265
|
-
});
|
|
266
|
-
if (guardResult &&
|
|
267
|
-
typeof guardResult === "object" &&
|
|
268
|
-
"draft" in guardResult &&
|
|
269
|
-
guardResult.draft) {
|
|
270
|
-
setDraft(guardResult.draft);
|
|
271
|
-
}
|
|
272
|
-
if (guardResult === false) {
|
|
273
|
-
setAdvanceGuardError(messages.bookingJourney.validation.completeStepBeforeContinuing);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (guardResult &&
|
|
277
|
-
typeof guardResult === "object" &&
|
|
278
|
-
"allow" in guardResult &&
|
|
279
|
-
guardResult.allow === false) {
|
|
280
|
-
setAdvanceGuardError(guardResult.message ?? messages.bookingJourney.validation.completeStepBeforeContinuing);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
catch (error) {
|
|
285
|
-
setAdvanceGuardError(error instanceof Error
|
|
286
|
-
? error.message
|
|
287
|
-
: messages.bookingJourney.validation.unableToContinue);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
finally {
|
|
291
|
-
setIsAdvanceGuardPending(false);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
setCurrentStep(next);
|
|
295
|
-
setVisited((s) => new Set(s).add(next));
|
|
296
|
-
};
|
|
297
|
-
const goBack = () => {
|
|
298
|
-
if (!prev)
|
|
299
|
-
return;
|
|
300
|
-
setCurrentStep(prev);
|
|
301
|
-
};
|
|
302
|
-
const jumpTo = (step) => {
|
|
303
|
-
if (!visited.has(step) && step !== currentStep)
|
|
304
|
-
return;
|
|
305
|
-
setCurrentStep(step);
|
|
306
|
-
};
|
|
307
|
-
const [contractDialogOpen, setContractDialogOpen] = useState(false);
|
|
308
|
-
// Tracks the multi-step storefront checkout flow (book →
|
|
309
|
-
// checkout-start → redirect). The legacy in-process commit has
|
|
310
|
-
// its own `commit.isPending` so the Confirm button merges both
|
|
311
|
-
// when deciding whether to show a spinner.
|
|
312
|
-
const [isHandlingCheckout, setIsHandlingCheckout] = useState(false);
|
|
313
|
-
const contractConfig = props.contract;
|
|
314
|
-
const contractVariables = useMemo(() => {
|
|
315
|
-
if (!contractConfig)
|
|
316
|
-
return {};
|
|
317
|
-
return contractConfig.resolveVariables({ draft, pricing: quote.data?.pricing ?? null });
|
|
318
|
-
}, [contractConfig, draft, quote.data?.pricing]);
|
|
319
|
-
const commitDraft = async () => {
|
|
320
|
-
if (!quote.data?.quoteId || quoteBlocked) {
|
|
321
|
-
setConfirmError(quoteUnpriceable
|
|
322
|
-
? messages.bookingJourney.validation.pricingUnavailable
|
|
323
|
-
: messages.bookingJourney.validation.quoteUnavailable);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
await commit.mutateAsync({
|
|
327
|
-
draft: { ...draft, quoteId: quote.data.quoteId },
|
|
328
|
-
quoteId: quote.data.quoteId,
|
|
329
|
-
// The owned commit reads the buyer + travelers off `party`, not the
|
|
330
|
-
// draft — without this the create rejects with "no billing person/org".
|
|
331
|
-
party: buildCommitParty(draft),
|
|
332
|
-
initialStatus: resolveInitialStatus(draft),
|
|
333
|
-
paymentIntent: buildCommitPaymentIntent(draft),
|
|
334
|
-
});
|
|
335
|
-
};
|
|
336
|
-
const handleAccepted = async (acceptance) => {
|
|
337
|
-
if (!props.onContractAccepted) {
|
|
338
|
-
await commitDraft();
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
setIsHandlingCheckout(true);
|
|
342
|
-
setConfirmError(null);
|
|
343
|
-
try {
|
|
344
|
-
await props.onContractAccepted(acceptance, {
|
|
345
|
-
draft,
|
|
346
|
-
pricing: quote.data?.pricing ?? null,
|
|
347
|
-
quoteId: quote.data?.quoteId,
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
catch (error) {
|
|
351
|
-
// The storefront checkout handler drives /book + /checkout/start and can
|
|
352
|
-
// fail (e.g. 502 RESERVE_FAILED with reason "rates_missing"). Surface it
|
|
353
|
-
// in the checkout UI instead of dropping the customer back on Review with
|
|
354
|
-
// only a console log (#2638).
|
|
355
|
-
setConfirmError(error instanceof Error && error.message
|
|
356
|
-
? error.message
|
|
357
|
-
: messages.bookingJourney.validation.checkoutFailed);
|
|
358
|
-
}
|
|
359
|
-
finally {
|
|
360
|
-
setIsHandlingCheckout(false);
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
const onConfirm = async () => {
|
|
364
|
-
// No valid quote (never priced, or the last re-quote 500'd) → Confirm must
|
|
365
|
-
// NOT be a silent no-op. `useBookingQuote` keeps the prior quote as
|
|
366
|
-
// placeholder data on a failed refetch, so a stale `quoteId` can still be
|
|
367
|
-
// present while `quote.error` is set; block on `hasQuoteError` too so the
|
|
368
|
-
// wizard Review Confirm can never submit against a stale price. Surface a
|
|
369
|
-
// recoverable message pointing at the retry banner instead of swallowing.
|
|
370
|
-
if (!quote.data?.quoteId || quoteBlocked) {
|
|
371
|
-
setConfirmError(quoteUnpriceable
|
|
372
|
-
? messages.bookingJourney.validation.pricingUnavailable
|
|
373
|
-
: messages.bookingJourney.validation.quoteUnavailable);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (findPaidScheduleRowsMissingPaymentDate(draft.paymentSchedules) !== null) {
|
|
377
|
-
setConfirmError(messages.bookingJourney.validation.paidPaymentDateRequired);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
setConfirmError(null);
|
|
381
|
-
// 1. Contract is wired → open the dialog. Acceptance triggers
|
|
382
|
-
// onContractAccepted (the storefront's checkout-start path).
|
|
383
|
-
// 2. No contract but onContractAccepted is wired → call it
|
|
384
|
-
// directly without an acceptance payload. The storefront uses
|
|
385
|
-
// this to drive the /book + /checkout/start handoff so card
|
|
386
|
-
// intents redirect to the PSP. Skipping the dialog when no
|
|
387
|
-
// template is configured is intentional — the dialog is an
|
|
388
|
-
// optional gate, not a required step.
|
|
389
|
-
// 3. Neither wired → in-process commit (the operator dashboard's
|
|
390
|
-
// legacy path).
|
|
391
|
-
if (contractConfig) {
|
|
392
|
-
setContractDialogOpen(true);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
await handleAccepted(null);
|
|
396
|
-
};
|
|
397
|
-
const onContractAccept = async (acceptance) => {
|
|
398
|
-
setContractDialogOpen(false);
|
|
399
|
-
await handleAccepted(acceptance);
|
|
400
|
-
};
|
|
401
|
-
// Bind the picked lead + departure into the billing-extras slot so the
|
|
402
|
-
// template can run lead-aware checks (e.g. duplicate-departure warning).
|
|
403
|
-
const billingExtrasSlot = props.renderBillingExtras
|
|
404
|
-
? () => props.renderBillingExtras?.({
|
|
405
|
-
buyerType: draft.billing.buyerType,
|
|
406
|
-
personId: draft.billing.contact.personId,
|
|
407
|
-
organizationId: draft.billing.buyerType === "B2B" ? draft.billing.organizationId : undefined,
|
|
408
|
-
productId: props.entityId,
|
|
409
|
-
departureSlotId: draft.configure.departureSlotId,
|
|
410
|
-
departureDate: draft.configure.departureDate,
|
|
411
|
-
})
|
|
412
|
-
: undefined;
|
|
413
|
-
// Recoverable quote-failure banner — rendered in both layouts whenever the
|
|
414
|
-
// live quote is erroring. The manual retry re-runs the query (clearing its
|
|
415
|
-
// error on success), which re-enables Next/Confirm.
|
|
416
|
-
const retryQuote = () => {
|
|
417
|
-
setConfirmError(null);
|
|
418
|
-
void quote.refetch();
|
|
419
|
-
};
|
|
420
|
-
const quoteErrorBanner = showQuoteUnpriceable ? (_jsxs("div", { role: "alert", "aria-live": "polite", className: "flex flex-col items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-3 text-destructive text-sm", children: [_jsx("span", { children: hasQuoteError
|
|
421
|
-
? messages.bookingJourney.validation.quoteFailed
|
|
422
|
-
: messages.bookingJourney.validation.pricingUnavailable }), hasQuoteError ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: retryQuote, disabled: quote.isQuoting, children: messages.bookingJourney.validation.retryQuote })) : null] })) : null;
|
|
423
|
-
// Renders one step's content. Shared by both layouts — the wizard shows
|
|
424
|
-
// exactly one at a time; the stacked page renders them all in sections.
|
|
425
|
-
const renderStep = (step) => {
|
|
426
|
-
switch (step) {
|
|
427
|
-
case "departure":
|
|
428
|
-
// First load: the descriptor arrives with the first quote. Show a
|
|
429
|
-
// skeleton rather than the generic fallback, which would flash and
|
|
430
|
-
// then shift into the real layout.
|
|
431
|
-
return !quote.data && quote.isQuoting ? (_jsx(ConfigureStepSkeleton, {})) : (_jsx(DepartureStep, { draft: draft, setDraft: setDraft, shape: shape, productId: props.entityId, renderDeparturePicker: props.renderDeparturePicker }));
|
|
432
|
-
case "options":
|
|
433
|
-
return (_jsx(OptionsStep, { draft: draft, setDraft: setDraft, shape: shape, productId: props.entityId, renderUnitsPicker: props.renderUnitsPicker }));
|
|
434
|
-
case "billing":
|
|
435
|
-
return (_jsx(BillingStep, { draft: draft, setDraft: setDraft, shape: shape, defaultPhoneCountry: defaultPhoneCountry, renderLeadContactPicker: props.renderLeadContactPicker, renderExtras: billingExtrasSlot, errors: validationErrorsForStep("billing", draft, messages), warnings: warningsForStep("billing", draft, shape, messages) }));
|
|
436
|
-
case "travelers":
|
|
437
|
-
return (_jsx(TravelersStep, { draft: draft, setDraft: setDraft, shape: shape, defaultPhoneCountry: defaultPhoneCountry, renderTravelerContactPicker: props.renderTravelerContactPicker, errors: validationErrorsForStep("travelers", draft, messages), warnings: warningsForStep("travelers", draft, shape, messages) }));
|
|
438
|
-
case "accommodation":
|
|
439
|
-
return _jsx(AccommodationStep, { draft: draft, setDraft: setDraft, shape: shape });
|
|
440
|
-
case "addons":
|
|
441
|
-
return _jsx(AddonsStep, { draft: draft, setDraft: setDraft, shape: shape });
|
|
442
|
-
case "payment":
|
|
443
|
-
return (_jsx(PaymentStep, { draft: draft, setDraft: setDraft, shape: shape, capabilities: props.paymentCapabilities ?? {
|
|
444
|
-
acceptsCard: false,
|
|
445
|
-
acceptsHold: true,
|
|
446
|
-
acceptsTicketOnCredit: false,
|
|
447
|
-
}, renderProviderStep: props.renderPaymentProviderStep, surface: surface, pricing: quote.data?.pricing ?? null }));
|
|
448
|
-
case "documents":
|
|
449
|
-
return _jsx(DocumentsStep, { draft: draft, setDraft: setDraft });
|
|
450
|
-
case "review":
|
|
451
|
-
return (_jsx(ReviewStep, { draft: draft, setDraft: setDraft, isCommitting: commit.isPending || isHandlingCheckout, onConfirm: onConfirm,
|
|
452
|
-
// Stacked has no per-step gates, so the Confirm button enforces
|
|
453
|
-
// the whole-booking validity itself.
|
|
454
|
-
canConfirm: layout === "stacked" ? canCommit : undefined, renderExtras: props.renderReviewExtras, surface: surface, pricing: quote.data?.pricing ?? null, warnings: warningsForStep("review", draft, shape, messages) }));
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
if (layout === "stacked") {
|
|
458
|
-
return (_jsx(StackedJourney, { className: props.className, steps: stackedSteps, renderStep: renderStep, isStepComplete: (s) => stackedStepComplete(s, draft, shape, available),
|
|
459
|
-
// Surface both the in-process commit error and the "no valid quote"
|
|
460
|
-
// Confirm block so a failed confirm is never silent.
|
|
461
|
-
commitError: commit.error ?? confirmError, onCancel: props.onCancelled, onConfirm: onConfirm, isCommitting: commit.isPending || isHandlingCheckout, canConfirm: canCommit, banner: quoteErrorBanner, sidePanel: _jsx(PriceSidePanel, { pricing: quote.data?.pricing ?? null, isQuoting: quote.isQuoting, invalidReason: quote.data?.invalidReason, entitySummary: props.entitySummary, currentStep: firstIncomplete, steps: stackedSteps, shape: shape, draft: draft, className: props.sidePanelClassName,
|
|
462
|
-
// Price override and Travel Credit live with the pricing, not in Payment.
|
|
463
|
-
pricingExtras: _jsx(FinalizeControls, { draft: draft, setDraft: setDraft, pricing: quote.data?.pricing ?? null, renderTravelCreditPicker: props.renderTravelCreditPicker }) }), contractDialog: contractConfig ? (_jsx(ContractPreviewDialog, { open: contractDialogOpen, onOpenChange: setContractDialogOpen, previewUrl: contractConfig.previewUrl, acceptLanguage: contractConfig.acceptLanguage, variables: contractVariables, marketingLabel: contractConfig.marketingLabel, termsLabel: contractConfig.termsLabel, onAccept: onContractAccept })) : null }));
|
|
464
|
-
}
|
|
465
|
-
return (_jsxs("div", { className: props.className, children: [_jsxs("div", { className: "grid grid-cols-1 gap-6 md:grid-cols-8 md:items-start", children: [_jsxs("div", { className: "space-y-6 md:col-span-5", children: [_jsx(StepHeader, { current: currentStep, visited: [...visited], steps: steps, shape: shape, onJumpTo: jumpTo }), quoteErrorBanner, currentStep === "departure" ? (
|
|
466
|
-
// First load: the descriptor arrives with the first quote. Show a
|
|
467
|
-
// skeleton rather than the generic fallback, which would flash
|
|
468
|
-
// and then shift into the real layout.
|
|
469
|
-
!quote.data && quote.isQuoting ? (_jsx(ConfigureStepSkeleton, {})) : (_jsx(DepartureStep, { draft: draft, setDraft: setDraft, shape: shape, productId: props.entityId, renderDeparturePicker: props.renderDeparturePicker }))) : null, currentStep === "options" ? (_jsx(OptionsStep, { draft: draft, setDraft: setDraft, shape: shape, productId: props.entityId, renderUnitsPicker: props.renderUnitsPicker })) : null, currentStep === "billing" ? (_jsx(BillingStep, { draft: draft, setDraft: setDraft, shape: shape, defaultPhoneCountry: defaultPhoneCountry, renderLeadContactPicker: props.renderLeadContactPicker, renderExtras: billingExtrasSlot })) : null, currentStep === "travelers" ? (_jsx(TravelersStep, { draft: draft, setDraft: setDraft, shape: shape, defaultPhoneCountry: defaultPhoneCountry, renderTravelerContactPicker: props.renderTravelerContactPicker })) : null, currentStep === "accommodation" ? (_jsx(AccommodationStep, { draft: draft, setDraft: setDraft, shape: shape })) : null, currentStep === "addons" ? (_jsx(AddonsStep, { draft: draft, setDraft: setDraft, shape: shape })) : null, currentStep === "payment" ? (_jsx(PaymentStep, { draft: draft, setDraft: setDraft, shape: shape, capabilities: props.paymentCapabilities ?? {
|
|
470
|
-
acceptsCard: false,
|
|
471
|
-
acceptsHold: true,
|
|
472
|
-
acceptsTicketOnCredit: false,
|
|
473
|
-
}, renderProviderStep: props.renderPaymentProviderStep, surface: surface, pricing: quote.data?.pricing ?? null })) : null, currentStep === "review" ? (_jsx(ReviewStep, { draft: draft, setDraft: setDraft, isCommitting: commit.isPending || isHandlingCheckout, onConfirm: onConfirm,
|
|
474
|
-
// Disable Confirm when the live quote can't be priced (error or
|
|
475
|
-
// rates_missing) so contract acceptance / commit never fires
|
|
476
|
-
// against an unpriced booking (#2638).
|
|
477
|
-
canConfirm: quoteReady && !quoteBlocked, renderExtras: props.renderReviewExtras, surface: surface, pricing: quote.data?.pricing ?? null })) : null, warnings.length > 0 ? (_jsx("ul", { className: "space-y-1 rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900 text-sm dark:border-amber-700 dark:bg-amber-950 dark:text-amber-100", children: warnings.map((w) => (_jsxs("li", { children: ["\u26A0 ", w] }, w))) })) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "outline", disabled: isAdvanceGuardPending, onClick: () => {
|
|
478
|
-
if (prev)
|
|
479
|
-
goBack();
|
|
480
|
-
else
|
|
481
|
-
props.onCancelled?.();
|
|
482
|
-
}, children: messages.bookingJourney.navigation.back }), next ? (_jsx(Button, { type: "button", onClick: () => void advance(), disabled: !canAdvance || isAdvanceGuardPending, className: "ml-auto", children: isAdvanceGuardPending
|
|
483
|
-
? messages.bookingJourney.navigation.checking
|
|
484
|
-
: messages.bookingJourney.navigation.next })) : null] }), advanceGuardError ? (_jsx("p", { className: "text-destructive text-sm", role: "alert", "aria-live": "polite", children: advanceGuardError })) : null, confirmError ? (_jsx("p", { className: "text-destructive text-sm", role: "alert", "aria-live": "polite", children: confirmError })) : null, commit.error ? (_jsx("p", { className: "text-destructive text-sm", children: commit.error instanceof Error ? commit.error.message : String(commit.error) })) : null] }), _jsx("aside", { className: "md:sticky md:top-4 md:col-span-3", children: _jsx(PriceSidePanel, { pricing: quote.data?.pricing ?? null, isQuoting: quote.isQuoting, invalidReason: quote.data?.invalidReason, entitySummary: props.entitySummary, currentStep: currentStep, steps: steps, shape: shape, draft: draft, className: props.sidePanelClassName }) })] }), contractConfig ? (_jsx(ContractPreviewDialog, { open: contractDialogOpen, onOpenChange: setContractDialogOpen, previewUrl: contractConfig.previewUrl, acceptLanguage: contractConfig.acceptLanguage, variables: contractVariables, marketingLabel: contractConfig.marketingLabel, termsLabel: contractConfig.termsLabel, onAccept: onContractAccept })) : null] }));
|
|
485
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export interface StorefrontBookErrorBody {
|
|
2
|
-
error?: unknown;
|
|
3
|
-
code?: unknown;
|
|
4
|
-
requestId?: unknown;
|
|
5
|
-
details?: unknown;
|
|
6
|
-
context?: {
|
|
7
|
-
upstreamPayload?: {
|
|
8
|
-
reason?: unknown;
|
|
9
|
-
};
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
export declare function buildStorefrontBookFailureMessage(body: StorefrontBookErrorBody, requestId: string | null, fallback: string, requestReferenceTemplate: string): string;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
const SPACE = String.fromCharCode(32);
|
|
2
|
-
const PERIOD = String.fromCharCode(46);
|
|
3
|
-
const SENTENCE_SEPARATOR = PERIOD + SPACE;
|
|
4
|
-
export function buildStorefrontBookFailureMessage(body, requestId, fallback, requestReferenceTemplate) {
|
|
5
|
-
const message = firstString(body.error) ?? firstFieldError(body.details);
|
|
6
|
-
const reference = firstString(body.requestId) ?? requestId;
|
|
7
|
-
const withMessage = message ? appendSentence(fallback, message) : fallback;
|
|
8
|
-
return reference
|
|
9
|
-
? appendSentence(withMessage, requestReferenceTemplate.replace("{requestId}", reference))
|
|
10
|
-
: withMessage;
|
|
11
|
-
}
|
|
12
|
-
function appendSentence(base, sentence) {
|
|
13
|
-
const trimmedBase = base.trim();
|
|
14
|
-
const trimmedSentence = sentence.trim();
|
|
15
|
-
if (!trimmedSentence)
|
|
16
|
-
return trimmedBase;
|
|
17
|
-
const separator = /[.!?]$/.test(trimmedBase) ? SPACE : SENTENCE_SEPARATOR;
|
|
18
|
-
const suffix = /[.!?]$/.test(trimmedSentence) ? trimmedSentence : trimmedSentence + PERIOD;
|
|
19
|
-
return `${trimmedBase}${separator}${suffix}`;
|
|
20
|
-
}
|
|
21
|
-
function firstFieldError(details) {
|
|
22
|
-
const record = asObject(details);
|
|
23
|
-
const fields = asObject(record?.fields) ?? record;
|
|
24
|
-
const fieldErrors = asObject(fields?.fieldErrors);
|
|
25
|
-
if (!fieldErrors)
|
|
26
|
-
return null;
|
|
27
|
-
for (const value of Object.values(fieldErrors)) {
|
|
28
|
-
const message = firstString(value);
|
|
29
|
-
if (message)
|
|
30
|
-
return message;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
function firstString(value) {
|
|
35
|
-
if (typeof value === "string")
|
|
36
|
-
return value.trim() || null;
|
|
37
|
-
if (Array.isArray(value)) {
|
|
38
|
-
for (const item of value) {
|
|
39
|
-
const message = firstString(item);
|
|
40
|
-
if (message)
|
|
41
|
-
return message;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
function asObject(value) {
|
|
47
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
48
|
-
? value
|
|
49
|
-
: null;
|
|
50
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { type BookingEntitySummary, type BookingJourneyCheckoutContext, type ContractAcceptanceEvent } from "../journey/index.js";
|
|
2
|
-
import { type ContractSourceContext } from "./resolve-contract-variables.js";
|
|
3
|
-
export { buildStorefrontBookBody, buildStorefrontCheckoutStartBody, buildStorefrontCommitParty, } from "./storefront-checkout-bodies.js";
|
|
4
|
-
export interface StorefrontBookingJourneyMessages {
|
|
5
|
-
checkoutFailed: string;
|
|
6
|
-
requestReference: string;
|
|
7
|
-
reserveFailed: string;
|
|
8
|
-
}
|
|
9
|
-
export interface StorefrontBookingJourneyScope {
|
|
10
|
-
marketId?: string;
|
|
11
|
-
locale?: string;
|
|
12
|
-
currency?: string;
|
|
13
|
-
}
|
|
14
|
-
export type StorefrontCheckoutConfirmationKind = "bank_transfer" | "card_pending" | "hold" | "inquiry";
|
|
15
|
-
export interface StorefrontBookingJourneyProps {
|
|
16
|
-
entityModule: string;
|
|
17
|
-
entityId: string;
|
|
18
|
-
/**
|
|
19
|
-
* Source provenance — optional on the storefront. When absent,
|
|
20
|
-
* the public engine route resolves it from
|
|
21
|
-
* `(entityModule, entityId)` via the catalog plane's
|
|
22
|
-
* sourced-entry lookup. Admin surfaces still pass it explicitly
|
|
23
|
-
* via the packaged `<BookingJourneyHost />`
|
|
24
|
-
* (`@voyant-travel/bookings-react/admin/booking-journey-host`).
|
|
25
|
-
*/
|
|
26
|
-
sourceKind?: string;
|
|
27
|
-
sourceConnectionId?: string;
|
|
28
|
-
sourceRef?: string;
|
|
29
|
-
draftId: string;
|
|
30
|
-
/** Pre-locked configure inputs (departure / sailing / cabin /
|
|
31
|
-
* date-range / pax) collected on the detail page. */
|
|
32
|
-
initialConfigure: Record<string, unknown>;
|
|
33
|
-
/** Pre-locked accommodation slice (room/rate for accommodations). */
|
|
34
|
-
initialAccommodation?: Record<string, unknown>;
|
|
35
|
-
/** Optional summary of the entity being booked — surfaces in the
|
|
36
|
-
* side panel so the customer keeps context while filling out the
|
|
37
|
-
* journey. */
|
|
38
|
-
entitySummary?: BookingEntitySummary;
|
|
39
|
-
/**
|
|
40
|
-
* Resolved source provenance for the booked entity — kind /
|
|
41
|
-
* connection / ref + supplier. The `/book` route reads it off the
|
|
42
|
-
* public content endpoint's `provenance` and passes it here so the
|
|
43
|
-
* contract preview's `booking.source` block reflects sourced
|
|
44
|
-
* inventory instead of defaulting to owned/blank (voyant#2619).
|
|
45
|
-
*/
|
|
46
|
-
entitySource?: ContractSourceContext;
|
|
47
|
-
/**
|
|
48
|
-
* Per-product override for the contract template. When unset (the
|
|
49
|
-
* default), the storefront resolves the active customer-scope
|
|
50
|
-
* template via /v1/public/legal/contracts/templates/default —
|
|
51
|
-
* whichever one the operator has marked as the customer default
|
|
52
|
-
* in the legal admin. Set this when a specific product/cruise/
|
|
53
|
-
* hotel needs a non-default contract.
|
|
54
|
-
*/
|
|
55
|
-
contractTemplateSlug?: string;
|
|
56
|
-
/** Optional marketing-opt-in label — when set, an extra checkbox
|
|
57
|
-
* is rendered in the contract dialog. */
|
|
58
|
-
contractMarketingLabel?: string;
|
|
59
|
-
/** Fired after the user accepts the contract (or, when no
|
|
60
|
-
* template is configured, when they click Confirm on Review).
|
|
61
|
-
* The route handles the actual checkout-start dispatch + redirect
|
|
62
|
-
* to Netopia / bank-transfer instructions. */
|
|
63
|
-
onContractAccepted?: (acceptance: ContractAcceptanceEvent | null, context: BookingJourneyCheckoutContext) => void | Promise<void>;
|
|
64
|
-
messages?: StorefrontBookingJourneyMessages;
|
|
65
|
-
scope?: StorefrontBookingJourneyScope;
|
|
66
|
-
onNavigateToShop: () => void;
|
|
67
|
-
onNavigateToConfirmation: (bookingId: string, kind?: StorefrontCheckoutConfirmationKind) => void;
|
|
68
|
-
onRedirectToPayment?: (url: string) => void;
|
|
69
|
-
className?: string;
|
|
70
|
-
}
|
|
71
|
-
export declare function StorefrontBookingJourney({ entityModule, entityId, sourceKind, sourceConnectionId, sourceRef, draftId, initialConfigure, initialAccommodation, entitySummary, entitySource, contractTemplateSlug, contractMarketingLabel, onContractAccepted, messages, scope, onNavigateToShop, onNavigateToConfirmation, onRedirectToPayment, className, }: StorefrontBookingJourneyProps): React.ReactElement;
|