@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,327 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* Storefront-flavored wrapper around `<BookingJourney />` —
|
|
5
|
-
* customer-facing, no CRM picker, B2C billing default, post-commit
|
|
6
|
-
* navigation to a confirmation page.
|
|
7
|
-
*
|
|
8
|
-
* Uses `surface="public"` so the engine hits `/v1/public/catalog/*`.
|
|
9
|
-
* Per booking-journey-architecture §8.1 + §10 Phase B.
|
|
10
|
-
*
|
|
11
|
-
* Package-owned so Node starters and dedicated storefronts share the same
|
|
12
|
-
* checkout behavior while route trees remain application-owned.
|
|
13
|
-
*/
|
|
14
|
-
import { useQuery } from "@tanstack/react-query";
|
|
15
|
-
import { computePaymentSchedule, noDepositPolicy, } from "@voyant-travel/finance/payment-policy";
|
|
16
|
-
import { useVoyantReactContext } from "@voyant-travel/react";
|
|
17
|
-
import { BookingJourney, } from "../journey/index.js";
|
|
18
|
-
import { resolveContractVariables, } from "./resolve-contract-variables.js";
|
|
19
|
-
import { buildStorefrontBookFailureMessage, } from "./storefront-booking-errors.js";
|
|
20
|
-
import { buildStorefrontBookBody, buildStorefrontCheckoutStartBody, } from "./storefront-checkout-bodies.js";
|
|
21
|
-
export { buildStorefrontBookBody, buildStorefrontCheckoutStartBody, buildStorefrontCommitParty, } from "./storefront-checkout-bodies.js";
|
|
22
|
-
/**
|
|
23
|
-
* Marker for checkout failures we've already turned into a localized,
|
|
24
|
-
* customer-facing message. Native errors (a `fetch` network drop, a
|
|
25
|
-
* `Response.json()` parse of an HTML 502) are NOT this type, so the outer catch
|
|
26
|
-
* can wrap them in the generic message instead of showing raw browser/parser
|
|
27
|
-
* text to the shopper (voyant#2638).
|
|
28
|
-
*/
|
|
29
|
-
class CheckoutError extends Error {
|
|
30
|
-
}
|
|
31
|
-
const defaultMessages = {
|
|
32
|
-
checkoutFailed: "We couldn't complete your booking. Please review your selection or try again.",
|
|
33
|
-
requestReference: "Reference: {requestId}",
|
|
34
|
-
reserveFailed: "This selection is no longer available. Adjust it and try again.",
|
|
35
|
-
};
|
|
36
|
-
export function StorefrontBookingJourney({ entityModule, entityId, sourceKind, sourceConnectionId, sourceRef, draftId, initialConfigure, initialAccommodation, entitySummary, entitySource, contractTemplateSlug, contractMarketingLabel, onContractAccepted, messages = defaultMessages, scope = {}, onNavigateToShop, onNavigateToConfirmation, onRedirectToPayment = (url) => window.location.assign(url), className, }) {
|
|
37
|
-
const { baseUrl, fetcher } = useVoyantReactContext();
|
|
38
|
-
// Carry the shopper's selected market/currency/locale (voyant#2643) into the
|
|
39
|
-
// journey's live quote so checkout prices in the same scope as browse/detail,
|
|
40
|
-
// not the default. The `(storefront)` layout provides the scope; unselected
|
|
41
|
-
// fields stay undefined and the quote falls back to the surface default.
|
|
42
|
-
// Resolve the contract template the journey will preview. The
|
|
43
|
-
// per-product override wins when set; otherwise we fetch
|
|
44
|
-
// whatever the operator marked as the active customer-scope
|
|
45
|
-
// template in `legal/contract_templates`. A 404 means no template
|
|
46
|
-
// has been seeded — the journey skips the preview dialog and
|
|
47
|
-
// commits without a contract.
|
|
48
|
-
const resolvedSlug = useResolvedContractSlug(contractTemplateSlug, baseUrl, fetcher);
|
|
49
|
-
const operatorProfile = usePublicOperatorProfile(baseUrl, fetcher);
|
|
50
|
-
const resolvedPolicy = useResolvedPaymentPolicy(entityModule, entityId, baseUrl, fetcher);
|
|
51
|
-
// Storefront-specific slot wiring. NO CRM picker — customers fill
|
|
52
|
-
// an inline contact form, which is the BookingJourney's default
|
|
53
|
-
// when `renderLeadContactPicker` is absent. Operators who later
|
|
54
|
-
// sign in could swap to the CRM picker mid-journey via an
|
|
55
|
-
// upgrade-path hook (Phase E follow-up).
|
|
56
|
-
const slots = {
|
|
57
|
-
onCommitted(result) {
|
|
58
|
-
onNavigateToConfirmation(result.bookingId);
|
|
59
|
-
},
|
|
60
|
-
onCancelled() {
|
|
61
|
-
onNavigateToShop();
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
// Default checkout-start handler — when the caller doesn't supply
|
|
65
|
-
// its own `onContractAccepted`, we run the standard storefront
|
|
66
|
-
// checkout flow:
|
|
67
|
-
//
|
|
68
|
-
// 1. POST /v1/public/catalog/book with the draft id → bookingId
|
|
69
|
-
// 2. POST /v1/public/catalog/checkout/start with the bookingId,
|
|
70
|
-
// the payment intent, and the captured acceptance.
|
|
71
|
-
// 3. Route the customer based on the response: card → 302 to
|
|
72
|
-
// Netopia; bank_transfer → instructions page; inquiry →
|
|
73
|
-
// thanks page.
|
|
74
|
-
//
|
|
75
|
-
const defaultCheckoutHandler = async (acceptance, context) => {
|
|
76
|
-
try {
|
|
77
|
-
// Step 1 — book the entity. Send the live scoped quote id explicitly
|
|
78
|
-
// (the server prefers `quoteId` over resolving the draft's stored
|
|
79
|
-
// `currentQuoteId`), so a market/currency change made mid-journey books
|
|
80
|
-
// the price the shopper is actually looking at rather than a stale one
|
|
81
|
-
// (voyant#2643). Falls back to draft resolution when no live quote yet.
|
|
82
|
-
const bookRes = await fetcher(apiPath(baseUrl, "/v1/public/catalog/book"), {
|
|
83
|
-
method: "POST",
|
|
84
|
-
credentials: "include",
|
|
85
|
-
headers: { "content-type": "application/json" },
|
|
86
|
-
body: JSON.stringify(buildStorefrontBookBody({
|
|
87
|
-
draftId,
|
|
88
|
-
quoteId: context.quoteId,
|
|
89
|
-
draft: context.draft,
|
|
90
|
-
acceptedAt: acceptance?.acceptedAt,
|
|
91
|
-
})),
|
|
92
|
-
});
|
|
93
|
-
if (!bookRes.ok) {
|
|
94
|
-
const errBody = (await bookRes.json().catch(() => ({})));
|
|
95
|
-
console.error("[storefront] /book failed", errBody);
|
|
96
|
-
// Reserve failures (e.g. 502 RESERVE_FAILED with reason
|
|
97
|
-
// "rates_missing") must reach the customer — throw so the journey
|
|
98
|
-
// surfaces a visible error instead of silently dropping back to
|
|
99
|
-
// Review (voyant#2638). A missing-rate / availability reason gets the
|
|
100
|
-
// "adjust your selection" copy; everything else the generic message.
|
|
101
|
-
// The engine error serializer nests the upstream payload under
|
|
102
|
-
// `context.upstreamPayload` (ReserveFailedError), not at top level.
|
|
103
|
-
const reason = typeof errBody.context?.upstreamPayload?.reason === "string"
|
|
104
|
-
? errBody.context.upstreamPayload.reason
|
|
105
|
-
: undefined;
|
|
106
|
-
throw new CheckoutError(reason === "rates_missing"
|
|
107
|
-
? messages.reserveFailed
|
|
108
|
-
: buildStorefrontBookFailureMessage(errBody, bookRes.headers.get("x-request-id"), messages.checkoutFailed, messages.requestReference));
|
|
109
|
-
}
|
|
110
|
-
const bookJson = (await bookRes.json());
|
|
111
|
-
const bookingId = bookJson.bookingId;
|
|
112
|
-
if (!bookingId) {
|
|
113
|
-
console.error("[storefront] /book returned no bookingId", bookJson);
|
|
114
|
-
throw new CheckoutError(messages.checkoutFailed);
|
|
115
|
-
}
|
|
116
|
-
// Step 2 — start checkout with the payment method selected in
|
|
117
|
-
// the journey's Payment step. Card goes to the PSP; bank
|
|
118
|
-
// transfer returns IBAN/reference instructions; inquiry skips
|
|
119
|
-
// inventory/payment.
|
|
120
|
-
const startRes = await fetcher(apiPath(baseUrl, "/v1/public/catalog/checkout/start"), {
|
|
121
|
-
method: "POST",
|
|
122
|
-
credentials: "include",
|
|
123
|
-
headers: { "content-type": "application/json" },
|
|
124
|
-
body: JSON.stringify(buildStorefrontCheckoutStartBody({
|
|
125
|
-
bookingId,
|
|
126
|
-
draft: context.draft,
|
|
127
|
-
acceptance,
|
|
128
|
-
returnOrigin: window.location.origin,
|
|
129
|
-
})),
|
|
130
|
-
});
|
|
131
|
-
const json = (await startRes.json());
|
|
132
|
-
if ("error" in json) {
|
|
133
|
-
console.error("[storefront] /checkout/start error", json);
|
|
134
|
-
throw new CheckoutError(messages.checkoutFailed);
|
|
135
|
-
}
|
|
136
|
-
switch (json.kind) {
|
|
137
|
-
case "card_redirect":
|
|
138
|
-
if (json.redirectUrl) {
|
|
139
|
-
onRedirectToPayment(json.redirectUrl);
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
onNavigateToConfirmation(json.bookingId, "card_pending");
|
|
143
|
-
}
|
|
144
|
-
break;
|
|
145
|
-
case "bank_transfer_instructions":
|
|
146
|
-
// Hand the instructions to the confirmation page so the
|
|
147
|
-
// customer leaves with the IBAN + reference visible. Stored
|
|
148
|
-
// in sessionStorage to keep the URL clean — refreshing the
|
|
149
|
-
// page replays the same instructions.
|
|
150
|
-
if (typeof sessionStorage !== "undefined") {
|
|
151
|
-
sessionStorage.setItem(`voyant.checkout.${json.bookingId}`, JSON.stringify(json));
|
|
152
|
-
}
|
|
153
|
-
onNavigateToConfirmation(json.bookingId, "bank_transfer");
|
|
154
|
-
break;
|
|
155
|
-
case "inquiry_received":
|
|
156
|
-
onNavigateToConfirmation(json.bookingId, "inquiry");
|
|
157
|
-
break;
|
|
158
|
-
case "hold_placed":
|
|
159
|
-
onNavigateToConfirmation(json.bookingId, "hold");
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
catch (err) {
|
|
164
|
-
console.error("[storefront] checkout flow failed", err);
|
|
165
|
-
// Re-throw so <BookingJourney /> can render a visible checkout error
|
|
166
|
-
// (voyant#2638). Preserve our own localized CheckoutError messages; wrap
|
|
167
|
-
// anything else (native fetch/network error, JSON parse of an HTML 502)
|
|
168
|
-
// in the generic message so raw browser/parser text never reaches the UI.
|
|
169
|
-
throw err instanceof CheckoutError ? err : new Error(messages.checkoutFailed);
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
return (_jsx(BookingJourney, { surface: "public", scope: { market: scope.marketId, locale: scope.locale, currency: scope.currency }, entityModule: entityModule, entityId: entityId, sourceKind: sourceKind, sourceConnectionId: sourceConnectionId, sourceRef: sourceRef, draftId: draftId, defaultBuyerType: "B2C", hideConfigure: true, initialConfigure: initialConfigure, initialAccommodation: initialAccommodation, entitySummary: entitySummary, contract: resolvedSlug
|
|
173
|
-
? {
|
|
174
|
-
templateSlug: resolvedSlug,
|
|
175
|
-
previewUrl: apiPath(baseUrl, `/v1/public/legal/contracts/templates/by-slug/${encodeURIComponent(resolvedSlug)}/preview`),
|
|
176
|
-
acceptLanguage: typeof navigator !== "undefined" ? navigator.language : undefined,
|
|
177
|
-
resolveVariables: ({ draft, pricing }) => {
|
|
178
|
-
// Use the server-resolved cascade when the public
|
|
179
|
-
// /v1/public/payment-policy/resolve endpoint has
|
|
180
|
-
// come back; while the request is in flight, fall
|
|
181
|
-
// back to the operator default so the customer sees
|
|
182
|
-
// a sensible preview rather than an empty schedule.
|
|
183
|
-
const policy = resolvedPolicy?.policy ??
|
|
184
|
-
operatorProfile?.customerPaymentPolicy ??
|
|
185
|
-
noDepositPolicy;
|
|
186
|
-
const source = resolvedPolicy?.source ?? "operator_default";
|
|
187
|
-
const schedule = pricing
|
|
188
|
-
? computePaymentSchedule({
|
|
189
|
-
totalCents: pricing.total,
|
|
190
|
-
currency: pricing.currency,
|
|
191
|
-
departureDate: entitySummary?.startDate ?? null,
|
|
192
|
-
}, policy)
|
|
193
|
-
: [];
|
|
194
|
-
return resolveContractVariables(draft, {
|
|
195
|
-
entityModule,
|
|
196
|
-
entityId,
|
|
197
|
-
entitySummary,
|
|
198
|
-
pricing,
|
|
199
|
-
operatorInfo: operatorProfile,
|
|
200
|
-
paymentSchedule: schedule,
|
|
201
|
-
paymentPolicySource: source,
|
|
202
|
-
source: entitySource,
|
|
203
|
-
});
|
|
204
|
-
},
|
|
205
|
-
...(contractMarketingLabel ? { marketingLabel: contractMarketingLabel } : {}),
|
|
206
|
-
}
|
|
207
|
-
: undefined, onContractAccepted: onContractAccepted ?? defaultCheckoutHandler, paymentCapabilities: {
|
|
208
|
-
// Storefront defaults — covers the three customer-facing
|
|
209
|
-
// payment paths a typical tour operator wants. The deployment
|
|
210
|
-
// can override per-product (e.g. high-ticket cruises that
|
|
211
|
-
// should always go through inquiry first).
|
|
212
|
-
//
|
|
213
|
-
// - acceptsCard: real-time card via the configured PSP
|
|
214
|
-
// (renderPaymentProviderStep supplies the widget — when
|
|
215
|
-
// absent we fall back to a "we'll email a link" message).
|
|
216
|
-
// - acceptsBankTransfer: lock inventory, send bank details
|
|
217
|
-
// out of band, reconcile on receipt of funds.
|
|
218
|
-
// - acceptsInquiry: lead-only path. No inventory hold, no
|
|
219
|
-
// charge — operator follows up manually. Useful for
|
|
220
|
-
// custom itineraries / availability-on-request products.
|
|
221
|
-
acceptsCard: true,
|
|
222
|
-
acceptsBankTransfer: true,
|
|
223
|
-
acceptsHold: false,
|
|
224
|
-
acceptsTicketOnCredit: false,
|
|
225
|
-
acceptsInquiry: true,
|
|
226
|
-
}, className: className, ...slots }));
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Resolve which contract template to preview at the Review step.
|
|
230
|
-
*
|
|
231
|
-
* Order:
|
|
232
|
-
* 1. The per-product `contractTemplateSlug` override the caller
|
|
233
|
-
* passed in (skips the network call).
|
|
234
|
-
* 2. The active customer-scope template returned by
|
|
235
|
-
* `GET /v1/public/legal/contracts/templates/default?scope=customer`.
|
|
236
|
-
*
|
|
237
|
-
* Returns `undefined` while the request is in flight or when the
|
|
238
|
-
* deployment hasn't seeded a customer template — the journey then
|
|
239
|
-
* skips the dialog and routes Confirm straight to the
|
|
240
|
-
* checkout-start handler.
|
|
241
|
-
*/
|
|
242
|
-
function useResolvedContractSlug(override, baseUrl, fetcher) {
|
|
243
|
-
const language = typeof navigator !== "undefined" ? navigator.language?.split("-")[0] : undefined;
|
|
244
|
-
const { data } = useQuery({
|
|
245
|
-
queryKey: ["public-legal-default-template", "customer", language ?? "en"],
|
|
246
|
-
queryFn: async () => {
|
|
247
|
-
const params = new URLSearchParams({ scope: "customer" });
|
|
248
|
-
if (language)
|
|
249
|
-
params.set("language", language);
|
|
250
|
-
const res = await fetcher(apiPath(baseUrl, `/v1/public/legal/contracts/templates/default?${params.toString()}`), { credentials: "include" });
|
|
251
|
-
if (!res.ok)
|
|
252
|
-
return null;
|
|
253
|
-
const json = (await res.json());
|
|
254
|
-
return json.data?.slug ?? null;
|
|
255
|
-
},
|
|
256
|
-
enabled: !override,
|
|
257
|
-
staleTime: 5 * 60 * 1000,
|
|
258
|
-
});
|
|
259
|
-
if (override)
|
|
260
|
-
return override;
|
|
261
|
-
return data ?? undefined;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Fetch the operator profile (name / legal name / address / license /
|
|
265
|
-
* default customer payment policy) from the public settings endpoint.
|
|
266
|
-
* The result is cached for 5 minutes — operator
|
|
267
|
-
* details rarely change, and stale-while-revalidate is fine for the
|
|
268
|
-
* contract preview UI.
|
|
269
|
-
*
|
|
270
|
-
* Returns `undefined` while the request is in flight or when the
|
|
271
|
-
* operator hasn't filled in Settings -> Organization yet — the contract
|
|
272
|
-
* preview then renders the operator block with `-` placeholders (the
|
|
273
|
-
* template renderer's missing-value substitution kicks in).
|
|
274
|
-
*/
|
|
275
|
-
function usePublicOperatorProfile(baseUrl, fetcher) {
|
|
276
|
-
const { data } = useQuery({
|
|
277
|
-
queryKey: ["public-operator-profile"],
|
|
278
|
-
queryFn: async () => {
|
|
279
|
-
const res = await fetcher(apiPath(baseUrl, "/v1/public/operator-profile"), {
|
|
280
|
-
credentials: "include",
|
|
281
|
-
});
|
|
282
|
-
if (!res.ok)
|
|
283
|
-
return null;
|
|
284
|
-
const json = (await res.json());
|
|
285
|
-
return json.data ?? null;
|
|
286
|
-
},
|
|
287
|
-
staleTime: 5 * 60 * 1000,
|
|
288
|
-
});
|
|
289
|
-
return data ?? undefined;
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Server-side cascade resolution for the storefront preview.
|
|
293
|
-
*
|
|
294
|
-
* Calls `POST /v1/public/payment-policy/resolve` with the entity
|
|
295
|
-
* coordinates and returns the resolved policy + which cascade layer
|
|
296
|
-
* supplied it (operator_default | supplier | category | listing).
|
|
297
|
-
*
|
|
298
|
-
* Resolves at entity granularity only — the journey's later
|
|
299
|
-
* sailing / cabin / rate-plan selections refine the cascade
|
|
300
|
-
* server-side at booking-confirmed time. Storefront preview shows
|
|
301
|
-
* the entity-level result, which is correct for the common case
|
|
302
|
-
* (single sailing per cruise, single rate plan picked at booking
|
|
303
|
-
* time, etc.) and gracefully degrades to a less-specific layer when
|
|
304
|
-
* the journey hasn't picked yet.
|
|
305
|
-
*/
|
|
306
|
-
function useResolvedPaymentPolicy(entityModule, entityId, baseUrl, fetcher) {
|
|
307
|
-
const { data } = useQuery({
|
|
308
|
-
queryKey: ["public-payment-policy", entityModule, entityId],
|
|
309
|
-
queryFn: async () => {
|
|
310
|
-
const res = await fetcher(apiPath(baseUrl, "/v1/public/payment-policy/resolve"), {
|
|
311
|
-
method: "POST",
|
|
312
|
-
credentials: "include",
|
|
313
|
-
headers: { "content-type": "application/json" },
|
|
314
|
-
body: JSON.stringify({ entityModule, entityId }),
|
|
315
|
-
});
|
|
316
|
-
if (!res.ok)
|
|
317
|
-
return null;
|
|
318
|
-
const json = (await res.json());
|
|
319
|
-
return json.data ?? null;
|
|
320
|
-
},
|
|
321
|
-
staleTime: 5 * 60 * 1000,
|
|
322
|
-
});
|
|
323
|
-
return data ?? undefined;
|
|
324
|
-
}
|
|
325
|
-
function apiPath(baseUrl, path) {
|
|
326
|
-
return `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
327
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { type StorefrontBookingJourneyMessages, type StorefrontBookingJourneyProps } from "./storefront-booking-journey.js";
|
|
3
|
-
export declare const storefrontBookingSearchSchema: z.ZodObject<{
|
|
4
|
-
departureSlotId: z.ZodOptional<z.ZodString>;
|
|
5
|
-
cabinCategoryId: z.ZodOptional<z.ZodString>;
|
|
6
|
-
cabinNumberId: z.ZodOptional<z.ZodString>;
|
|
7
|
-
airArrangement: z.ZodOptional<z.ZodEnum<{
|
|
8
|
-
none: "none";
|
|
9
|
-
cruise_line: "cruise_line";
|
|
10
|
-
independent: "independent";
|
|
11
|
-
}>>;
|
|
12
|
-
checkIn: z.ZodOptional<z.ZodString>;
|
|
13
|
-
checkOut: z.ZodOptional<z.ZodString>;
|
|
14
|
-
roomTypeId: z.ZodOptional<z.ZodString>;
|
|
15
|
-
ratePlanId: z.ZodOptional<z.ZodString>;
|
|
16
|
-
board: z.ZodOptional<z.ZodString>;
|
|
17
|
-
adult: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
18
|
-
child: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
19
|
-
infant: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
20
|
-
draftId: z.ZodOptional<z.ZodString>;
|
|
21
|
-
}, z.core.$strip>;
|
|
22
|
-
export type StorefrontBookingSearch = z.infer<typeof storefrontBookingSearchSchema>;
|
|
23
|
-
export interface StorefrontBookingPageMessages extends StorefrontBookingJourneyMessages {
|
|
24
|
-
marketingLabel?: string;
|
|
25
|
-
}
|
|
26
|
-
export type StorefrontBookingPageExtensions = Pick<StorefrontBookingJourneyProps, "className" | "contractMarketingLabel" | "contractTemplateSlug" | "onContractAccepted" | "onRedirectToPayment">;
|
|
27
|
-
export interface StorefrontBookingPageProps {
|
|
28
|
-
entityModule: string;
|
|
29
|
-
entityId: string;
|
|
30
|
-
search: StorefrontBookingSearch;
|
|
31
|
-
messages: StorefrontBookingPageMessages;
|
|
32
|
-
extensions?: StorefrontBookingPageExtensions;
|
|
33
|
-
}
|
|
34
|
-
/** Customer booking page shared by Node applications and dedicated storefronts. */
|
|
35
|
-
export declare function StorefrontBookingPage({ entityModule, entityId, search, messages, extensions, }: StorefrontBookingPageProps): React.ReactElement;
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useQuery } from "@tanstack/react-query";
|
|
3
|
-
import { useStorefrontUi } from "@voyant-travel/storefront-react/storefront";
|
|
4
|
-
import { useMemo } from "react";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { useBookingsUiI18nOrDefault } from "../i18n/provider.js";
|
|
7
|
-
import { StorefrontBookingJourney, } from "./storefront-booking-journey.js";
|
|
8
|
-
export const storefrontBookingSearchSchema = z.object({
|
|
9
|
-
departureSlotId: z.string().optional(),
|
|
10
|
-
cabinCategoryId: z.string().optional(),
|
|
11
|
-
cabinNumberId: z.string().optional(),
|
|
12
|
-
airArrangement: z.enum(["cruise_line", "independent", "none"]).optional(),
|
|
13
|
-
checkIn: z.string().optional(),
|
|
14
|
-
checkOut: z.string().optional(),
|
|
15
|
-
roomTypeId: z.string().optional(),
|
|
16
|
-
ratePlanId: z.string().optional(),
|
|
17
|
-
board: z.string().optional(),
|
|
18
|
-
adult: z.coerce.number().int().min(0).optional(),
|
|
19
|
-
child: z.coerce.number().int().min(0).optional(),
|
|
20
|
-
infant: z.coerce.number().int().min(0).optional(),
|
|
21
|
-
draftId: z.string().optional(),
|
|
22
|
-
});
|
|
23
|
-
/** Customer booking page shared by Node applications and dedicated storefronts. */
|
|
24
|
-
export function StorefrontBookingPage({ entityModule, entityId, search, messages, extensions, }) {
|
|
25
|
-
const { apiUrl, navigate, scope } = useStorefrontUi();
|
|
26
|
-
const draftId = useMemo(() => search.draftId ?? generateDraftId(), [search.draftId]);
|
|
27
|
-
const initialConfigure = buildInitialConfigure(search);
|
|
28
|
-
const initialAccommodation = buildInitialAccommodation(search);
|
|
29
|
-
const { summary, source } = useEntityContent(apiUrl, entityModule, entityId, search);
|
|
30
|
-
return (_jsx(StorefrontBookingJourney, { entityModule: entityModule, entityId: entityId, draftId: draftId, initialConfigure: initialConfigure, initialAccommodation: initialAccommodation, entitySummary: summary, entitySource: source, messages: messages, scope: scope, onNavigateToShop: () => navigate({ to: "/shop" }), onNavigateToConfirmation: (bookingId, kind) => navigate({
|
|
31
|
-
to: "/shop/confirmation/$bookingId",
|
|
32
|
-
params: { bookingId },
|
|
33
|
-
...(kind ? { search: { kind } } : {}),
|
|
34
|
-
}), ...(messages.marketingLabel !== undefined
|
|
35
|
-
? { contractMarketingLabel: messages.marketingLabel }
|
|
36
|
-
: {}), ...extensions }));
|
|
37
|
-
}
|
|
38
|
-
function buildInitialConfigure(search) {
|
|
39
|
-
const configure = {
|
|
40
|
-
pax: {
|
|
41
|
-
adult: search.adult ?? 1,
|
|
42
|
-
child: search.child ?? 0,
|
|
43
|
-
infant: search.infant ?? 0,
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
if (search.departureSlotId)
|
|
47
|
-
configure.departureSlotId = search.departureSlotId;
|
|
48
|
-
if (search.cabinCategoryId)
|
|
49
|
-
configure.cabinCategoryId = search.cabinCategoryId;
|
|
50
|
-
if (search.cabinNumberId)
|
|
51
|
-
configure.cabinNumberId = search.cabinNumberId;
|
|
52
|
-
if (search.airArrangement)
|
|
53
|
-
configure.airArrangement = search.airArrangement;
|
|
54
|
-
if (search.checkIn && search.checkOut) {
|
|
55
|
-
configure.dateRange = { checkIn: search.checkIn, checkOut: search.checkOut };
|
|
56
|
-
}
|
|
57
|
-
if (search.roomTypeId)
|
|
58
|
-
configure.roomTypeId = search.roomTypeId;
|
|
59
|
-
if (search.ratePlanId)
|
|
60
|
-
configure.ratePlanId = search.ratePlanId;
|
|
61
|
-
if (search.board)
|
|
62
|
-
configure.board = search.board;
|
|
63
|
-
return configure;
|
|
64
|
-
}
|
|
65
|
-
function buildInitialAccommodation(search) {
|
|
66
|
-
if (!search.roomTypeId)
|
|
67
|
-
return undefined;
|
|
68
|
-
return {
|
|
69
|
-
rooms: [
|
|
70
|
-
{
|
|
71
|
-
optionUnitId: search.roomTypeId,
|
|
72
|
-
quantity: 1,
|
|
73
|
-
...(search.ratePlanId ? { ratePlanId: search.ratePlanId } : {}),
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
travelerAssignments: {},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function useEntityContent(apiUrl, entityModule, entityId, search) {
|
|
80
|
-
const { locale } = useBookingsUiI18nOrDefault();
|
|
81
|
-
const url = entityContentUrl(apiUrl, entityModule, entityId);
|
|
82
|
-
const { data } = useQuery({
|
|
83
|
-
queryKey: ["public-entity-summary", entityModule, entityId],
|
|
84
|
-
queryFn: async () => {
|
|
85
|
-
if (!url)
|
|
86
|
-
return null;
|
|
87
|
-
const response = await fetch(url, { credentials: "include" });
|
|
88
|
-
if (!response.ok)
|
|
89
|
-
return null;
|
|
90
|
-
const json = (await response.json());
|
|
91
|
-
return json.data ?? null;
|
|
92
|
-
},
|
|
93
|
-
enabled: Boolean(url),
|
|
94
|
-
staleTime: 60_000,
|
|
95
|
-
});
|
|
96
|
-
const content = data?.content ?? null;
|
|
97
|
-
const source = useMemo(() => resolveEntitySource(entityModule, data?.provenance, content), [entityModule, data?.provenance, content]);
|
|
98
|
-
const summary = useMemo(() => resolveEntitySummary(entityModule, content, search, locale), [content, entityModule, search, locale]);
|
|
99
|
-
return { summary, source };
|
|
100
|
-
}
|
|
101
|
-
function entityContentUrl(apiUrl, entityModule, entityId) {
|
|
102
|
-
const encodedId = encodeURIComponent(entityId);
|
|
103
|
-
if (entityModule === "cruises")
|
|
104
|
-
return `${apiUrl}/v1/public/cruises/${encodedId}/content`;
|
|
105
|
-
if (entityModule === "accommodations") {
|
|
106
|
-
return `${apiUrl}/v1/public/accommodations/${encodedId}/content`;
|
|
107
|
-
}
|
|
108
|
-
if (entityModule === "products")
|
|
109
|
-
return `${apiUrl}/v1/public/products/${encodedId}/content`;
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
function resolveEntitySummary(entityModule, content, search, locale) {
|
|
113
|
-
if (!content)
|
|
114
|
-
return undefined;
|
|
115
|
-
if (entityModule === "products")
|
|
116
|
-
return resolveProductSummary(content, search, locale);
|
|
117
|
-
if (entityModule === "cruises")
|
|
118
|
-
return resolveCruiseSummary(content, search, locale);
|
|
119
|
-
if (entityModule === "accommodations") {
|
|
120
|
-
return resolveAccommodationSummary(content, search, locale);
|
|
121
|
-
}
|
|
122
|
-
return undefined;
|
|
123
|
-
}
|
|
124
|
-
function resolveProductSummary(content, search, locale) {
|
|
125
|
-
const subtitle = [
|
|
126
|
-
content.product.duration_days
|
|
127
|
-
? `${content.product.duration_days} day${content.product.duration_days === 1 ? "" : "s"}`
|
|
128
|
-
: null,
|
|
129
|
-
content.product.country ?? null,
|
|
130
|
-
].filter(Boolean);
|
|
131
|
-
const departure = content.departures?.find((item) => item.id === search.departureSlotId);
|
|
132
|
-
return {
|
|
133
|
-
name: content.product.name,
|
|
134
|
-
subtitle: subtitle.join(" · ") || undefined,
|
|
135
|
-
heroImageUrl: content.product.hero_image_url ?? content.media?.[0]?.url ?? undefined,
|
|
136
|
-
vertical: "products",
|
|
137
|
-
whenLabel: departure ? formatDate(departure.starts_at, locale) : undefined,
|
|
138
|
-
locationLabel: content.product.departure_city ?? content.product.country ?? undefined,
|
|
139
|
-
startDate: departure?.starts_at ?? undefined,
|
|
140
|
-
endDate: departure?.ends_at ?? undefined,
|
|
141
|
-
destination: content.product.country ?? content.product.departure_city ?? undefined,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function resolveCruiseSummary(content, search, locale) {
|
|
145
|
-
const sailing = content.sailings.find((item) => item.id === search.departureSlotId);
|
|
146
|
-
const subtitle = [
|
|
147
|
-
content.cruise.duration_nights
|
|
148
|
-
? `${content.cruise.duration_nights} night${content.cruise.duration_nights === 1 ? "" : "s"}`
|
|
149
|
-
: null,
|
|
150
|
-
content.ship?.name ?? null,
|
|
151
|
-
].filter(Boolean);
|
|
152
|
-
const route = sailing
|
|
153
|
-
? sailing.embarkation_port && sailing.disembarkation_port
|
|
154
|
-
? `${sailing.embarkation_port} → ${sailing.disembarkation_port}`
|
|
155
|
-
: (sailing.embarkation_port ?? null)
|
|
156
|
-
: null;
|
|
157
|
-
return {
|
|
158
|
-
name: content.cruise.name,
|
|
159
|
-
subtitle: subtitle.join(" · ") || undefined,
|
|
160
|
-
heroImageUrl: content.cruise.hero_image_url ?? undefined,
|
|
161
|
-
vertical: "cruises",
|
|
162
|
-
whenLabel: sailing ? formatDate(sailing.start_date, locale) : undefined,
|
|
163
|
-
locationLabel: route ?? undefined,
|
|
164
|
-
startDate: sailing?.start_date ?? undefined,
|
|
165
|
-
endDate: sailing?.end_date ?? undefined,
|
|
166
|
-
destination: route ?? undefined,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
function resolveAccommodationSummary(content, search, locale) {
|
|
170
|
-
const stars = content.hotel.star_rating ? "★".repeat(Math.floor(content.hotel.star_rating)) : null;
|
|
171
|
-
return {
|
|
172
|
-
name: content.hotel.name,
|
|
173
|
-
subtitle: stars ?? undefined,
|
|
174
|
-
heroImageUrl: content.hotel.hero_image_url ?? undefined,
|
|
175
|
-
vertical: "accommodations",
|
|
176
|
-
whenLabel: search.checkIn && search.checkOut
|
|
177
|
-
? `${formatDate(search.checkIn, locale)} → ${formatDate(search.checkOut, locale)}`
|
|
178
|
-
: undefined,
|
|
179
|
-
startDate: search.checkIn ?? undefined,
|
|
180
|
-
endDate: search.checkOut ?? undefined,
|
|
181
|
-
destination: content.hotel.city ?? content.hotel.country ?? undefined,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
function resolveEntitySource(entityModule, provenance, content) {
|
|
185
|
-
if (!provenance)
|
|
186
|
-
return undefined;
|
|
187
|
-
const supplierName = entityModule === "products" && content
|
|
188
|
-
? (content.product.supplier ?? "")
|
|
189
|
-
: "";
|
|
190
|
-
return {
|
|
191
|
-
kind: provenance.source_kind,
|
|
192
|
-
connectionId: provenance.source_connection_id ?? "",
|
|
193
|
-
ref: provenance.source_ref ?? "",
|
|
194
|
-
supplier: { id: "", name: supplierName },
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
function formatDate(iso, locale) {
|
|
198
|
-
try {
|
|
199
|
-
return new Date(iso).toLocaleDateString(locale, {
|
|
200
|
-
weekday: "short",
|
|
201
|
-
day: "numeric",
|
|
202
|
-
month: "short",
|
|
203
|
-
year: "numeric",
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
return iso;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
function generateDraftId() {
|
|
211
|
-
if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.randomUUID) {
|
|
212
|
-
return `bdrf_${globalThis.crypto.randomUUID().replace(/-/g, "")}`;
|
|
213
|
-
}
|
|
214
|
-
return `bdrf_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
215
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { ContractAcceptanceEvent, Draft } from "../journey/index.js";
|
|
2
|
-
export declare function buildStorefrontBookBody(input: {
|
|
3
|
-
draftId: string;
|
|
4
|
-
quoteId?: string;
|
|
5
|
-
draft: Draft;
|
|
6
|
-
acceptedAt?: string;
|
|
7
|
-
}): {
|
|
8
|
-
draftId: string;
|
|
9
|
-
quoteId: string | undefined;
|
|
10
|
-
party: Record<string, unknown>;
|
|
11
|
-
paymentIntent: {
|
|
12
|
-
type: "hold";
|
|
13
|
-
};
|
|
14
|
-
idempotencyKey: string;
|
|
15
|
-
};
|
|
16
|
-
export declare function buildStorefrontCheckoutStartBody(input: {
|
|
17
|
-
bookingId: string;
|
|
18
|
-
draft: Draft;
|
|
19
|
-
acceptance: ContractAcceptanceEvent | null;
|
|
20
|
-
returnOrigin: string;
|
|
21
|
-
}): {
|
|
22
|
-
returnOrigin: string;
|
|
23
|
-
payerName?: string | undefined;
|
|
24
|
-
payerEmail?: string | undefined;
|
|
25
|
-
contractAcceptance?: {
|
|
26
|
-
templateId: string;
|
|
27
|
-
templateSlug: string;
|
|
28
|
-
acceptedTerms: true;
|
|
29
|
-
acceptedMarketing: boolean;
|
|
30
|
-
acceptedAt: string;
|
|
31
|
-
renderedHtml: string;
|
|
32
|
-
} | undefined;
|
|
33
|
-
bookingId: string;
|
|
34
|
-
paymentIntent: "hold" | "card" | "bank_transfer" | "inquiry";
|
|
35
|
-
};
|
|
36
|
-
export declare function buildStorefrontCommitParty(draft: Draft): Record<string, unknown>;
|