@voyant-travel/bookings-react 0.151.4 → 0.152.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.
Files changed (35) hide show
  1. package/README.md +6 -1
  2. package/dist/admin/booking-detail-host.d.ts +12 -12
  3. package/dist/admin/booking-detail-host.d.ts.map +1 -1
  4. package/dist/admin/booking-detail-host.js +12 -5
  5. package/dist/admin/index.d.ts +5 -26
  6. package/dist/admin/index.d.ts.map +1 -1
  7. package/dist/admin/index.js +11 -25
  8. package/dist/admin/pages/booking-detail-page.d.ts +3 -5
  9. package/dist/admin/pages/booking-detail-page.d.ts.map +1 -1
  10. package/dist/admin/pages/booking-detail-page.js +3 -5
  11. package/dist/admin/pages/bookings-index-page.d.ts +2 -12
  12. package/dist/admin/pages/bookings-index-page.d.ts.map +1 -1
  13. package/dist/admin/pages/bookings-index-page.js +4 -3
  14. package/dist/admin/slots.d.ts +7 -0
  15. package/dist/admin/slots.d.ts.map +1 -1
  16. package/dist/admin/slots.js +7 -0
  17. package/dist/hooks/use-public-booking-session-flow-mutation.d.ts +1 -1
  18. package/dist/hooks/use-public-booking-session.d.ts +1 -1
  19. package/dist/query-options.d.ts +4 -4
  20. package/dist/storefront/index.d.ts +4 -0
  21. package/dist/storefront/index.d.ts.map +1 -0
  22. package/dist/storefront/index.js +3 -0
  23. package/dist/storefront/resolve-contract-variables.d.ts +131 -0
  24. package/dist/storefront/resolve-contract-variables.d.ts.map +1 -0
  25. package/dist/storefront/resolve-contract-variables.js +492 -0
  26. package/dist/storefront/storefront-booking-errors.d.ts +13 -0
  27. package/dist/storefront/storefront-booking-errors.d.ts.map +1 -0
  28. package/dist/storefront/storefront-booking-errors.js +50 -0
  29. package/dist/storefront/storefront-booking-journey.d.ts +72 -0
  30. package/dist/storefront/storefront-booking-journey.d.ts.map +1 -0
  31. package/dist/storefront/storefront-booking-journey.js +380 -0
  32. package/dist/storefront/storefront-booking-page.d.ts +36 -0
  33. package/dist/storefront/storefront-booking-page.d.ts.map +1 -0
  34. package/dist/storefront/storefront-booking-page.js +213 -0
  35. package/package.json +59 -24
@@ -0,0 +1,50 @@
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
+ }
@@ -0,0 +1,72 @@
1
+ import { type BookingEntitySummary, type BookingJourneyCheckoutContext, type ContractAcceptanceEvent, type Draft } from "../journey/index.js";
2
+ import { type ContractSourceContext } from "./resolve-contract-variables.js";
3
+ export interface StorefrontBookingJourneyMessages {
4
+ checkoutFailed: string;
5
+ requestReference: string;
6
+ reserveFailed: string;
7
+ }
8
+ export interface StorefrontBookingJourneyScope {
9
+ marketId?: string;
10
+ locale?: string;
11
+ currency?: string;
12
+ }
13
+ export type StorefrontCheckoutConfirmationKind = "bank_transfer" | "card_pending" | "hold" | "inquiry";
14
+ export interface StorefrontBookingJourneyProps {
15
+ entityModule: string;
16
+ entityId: string;
17
+ /**
18
+ * Source provenance — optional on the storefront. When absent,
19
+ * the public engine route resolves it from
20
+ * `(entityModule, entityId)` via the catalog plane's
21
+ * sourced-entry lookup. Admin surfaces still pass it explicitly
22
+ * via the packaged `<BookingJourneyHost />`
23
+ * (`@voyant-travel/bookings-react/admin/booking-journey-host`).
24
+ */
25
+ sourceKind?: string;
26
+ sourceConnectionId?: string;
27
+ sourceRef?: string;
28
+ draftId: string;
29
+ /** Pre-locked configure inputs (departure / sailing / cabin /
30
+ * date-range / pax) collected on the detail page. */
31
+ initialConfigure: Record<string, unknown>;
32
+ /** Pre-locked accommodation slice (room/rate for accommodations). */
33
+ initialAccommodation?: Record<string, unknown>;
34
+ /** Optional summary of the entity being booked — surfaces in the
35
+ * side panel so the customer keeps context while filling out the
36
+ * journey. */
37
+ entitySummary?: BookingEntitySummary;
38
+ /**
39
+ * Resolved source provenance for the booked entity — kind /
40
+ * connection / ref + supplier. The `/book` route reads it off the
41
+ * public content endpoint's `provenance` and passes it here so the
42
+ * contract preview's `booking.source` block reflects sourced
43
+ * inventory instead of defaulting to owned/blank (voyant#2619).
44
+ */
45
+ entitySource?: ContractSourceContext;
46
+ /**
47
+ * Per-product override for the contract template. When unset (the
48
+ * default), the storefront resolves the active customer-scope
49
+ * template via /v1/public/legal/contracts/templates/default —
50
+ * whichever one the operator has marked as the customer default
51
+ * in the legal admin. Set this when a specific product/cruise/
52
+ * hotel needs a non-default contract.
53
+ */
54
+ contractTemplateSlug?: string;
55
+ /** Optional marketing-opt-in label — when set, an extra checkbox
56
+ * is rendered in the contract dialog. */
57
+ contractMarketingLabel?: string;
58
+ /** Fired after the user accepts the contract (or, when no
59
+ * template is configured, when they click Confirm on Review).
60
+ * The route handles the actual checkout-start dispatch + redirect
61
+ * to Netopia / bank-transfer instructions. */
62
+ onContractAccepted?: (acceptance: ContractAcceptanceEvent | null, context: BookingJourneyCheckoutContext) => void | Promise<void>;
63
+ messages?: StorefrontBookingJourneyMessages;
64
+ scope?: StorefrontBookingJourneyScope;
65
+ onNavigateToShop: () => void;
66
+ onNavigateToConfirmation: (bookingId: string, kind?: StorefrontCheckoutConfirmationKind) => void;
67
+ onRedirectToPayment?: (url: string) => void;
68
+ className?: string;
69
+ }
70
+ 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;
71
+ export declare function buildStorefrontCommitParty(draft: Draft): Record<string, unknown>;
72
+ //# sourceMappingURL=storefront-booking-journey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-booking-journey.d.ts","sourceRoot":"","sources":["../../src/storefront/storefront-booking-journey.tsx"],"names":[],"mappings":"AAuBA,OAAO,EACL,KAAK,oBAAoB,EAEzB,KAAK,6BAA6B,EAElC,KAAK,uBAAuB,EAC5B,KAAK,KAAK,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,KAAK,qBAAqB,EAG3B,MAAM,iCAAiC,CAAA;AAmBxC,MAAM,WAAW,gCAAgC;IAC/C,cAAc,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,MAAM,CAAA;IACxB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,kCAAkC,GAC1C,eAAe,GACf,cAAc,GACd,MAAM,GACN,SAAS,CAAA;AAQb,MAAM,WAAW,6BAA6B;IAC5C,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf;0DACsD;IACtD,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACzC,qEAAqE;IACrE,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9C;;mBAEe;IACf,aAAa,CAAC,EAAE,oBAAoB,CAAA;IACpC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;8CAC0C;IAC1C,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B;;;mDAG+C;IAC/C,kBAAkB,CAAC,EAAE,CACnB,UAAU,EAAE,uBAAuB,GAAG,IAAI,EAC1C,OAAO,EAAE,6BAA6B,KACnC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,QAAQ,CAAC,EAAE,gCAAgC,CAAA;IAC3C,KAAK,CAAC,EAAE,6BAA6B,CAAA;IACrC,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,wBAAwB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,kCAAkC,KAAK,IAAI,CAAA;IAChG,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,wBAAwB,CAAC,EACvC,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,kBAAkB,EAClB,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,kBAAkB,EAClB,QAA0B,EAC1B,KAAU,EACV,gBAAgB,EAChB,wBAAwB,EACxB,mBAA0D,EAC1D,SAAS,GACV,EAAE,6BAA6B,GAAG,KAAK,CAAC,YAAY,CAkRpD;AASD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgChF"}
@@ -0,0 +1,380 @@
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
+ /**
21
+ * Marker for checkout failures we've already turned into a localized,
22
+ * customer-facing message. Native errors (a `fetch` network drop, a
23
+ * `Response.json()` parse of an HTML 502) are NOT this type, so the outer catch
24
+ * can wrap them in the generic message instead of showing raw browser/parser
25
+ * text to the shopper (voyant#2638).
26
+ */
27
+ class CheckoutError extends Error {
28
+ }
29
+ const defaultMessages = {
30
+ checkoutFailed: "We couldn't complete your booking. Please review your selection or try again.",
31
+ requestReference: "Reference: {requestId}",
32
+ reserveFailed: "This selection is no longer available. Adjust it and try again.",
33
+ };
34
+ 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, }) {
35
+ const { baseUrl, fetcher } = useVoyantReactContext();
36
+ // Carry the shopper's selected market/currency/locale (voyant#2643) into the
37
+ // journey's live quote so checkout prices in the same scope as browse/detail,
38
+ // not the default. The `(storefront)` layout provides the scope; unselected
39
+ // fields stay undefined and the quote falls back to the surface default.
40
+ // Resolve the contract template the journey will preview. The
41
+ // per-product override wins when set; otherwise we fetch
42
+ // whatever the operator marked as the active customer-scope
43
+ // template in `legal/contract_templates`. A 404 means no template
44
+ // has been seeded — the journey skips the preview dialog and
45
+ // commits without a contract.
46
+ const resolvedSlug = useResolvedContractSlug(contractTemplateSlug, baseUrl, fetcher);
47
+ const operatorProfile = usePublicOperatorProfile(baseUrl, fetcher);
48
+ const resolvedPolicy = useResolvedPaymentPolicy(entityModule, entityId, baseUrl, fetcher);
49
+ // Storefront-specific slot wiring. NO CRM picker — customers fill
50
+ // an inline contact form, which is the BookingJourney's default
51
+ // when `renderLeadContactPicker` is absent. Operators who later
52
+ // sign in could swap to the CRM picker mid-journey via an
53
+ // upgrade-path hook (Phase E follow-up).
54
+ const slots = {
55
+ onCommitted(result) {
56
+ onNavigateToConfirmation(result.bookingId);
57
+ },
58
+ onCancelled() {
59
+ onNavigateToShop();
60
+ },
61
+ };
62
+ // Default checkout-start handler — when the caller doesn't supply
63
+ // its own `onContractAccepted`, we run the standard storefront
64
+ // checkout flow:
65
+ //
66
+ // 1. POST /v1/public/catalog/book with the draft id → bookingId
67
+ // 2. POST /v1/public/catalog/checkout/start with the bookingId,
68
+ // the payment intent, and the captured acceptance.
69
+ // 3. Route the customer based on the response: card → 302 to
70
+ // Netopia; bank_transfer → instructions page; inquiry →
71
+ // thanks page.
72
+ //
73
+ const defaultCheckoutHandler = async (acceptance, context) => {
74
+ try {
75
+ const intent = checkoutIntentFromDraft(context.draft.payment.intent);
76
+ const contact = context.draft.billing?.contact;
77
+ const payerName = [contact?.firstName, contact?.lastName].filter(Boolean).join(" ").trim();
78
+ const idempotencyKey = `bj-${draftId}-${acceptance?.acceptedAt ?? "noaccept"}`;
79
+ // Step 1 — book the entity. Send the live scoped quote id explicitly
80
+ // (the server prefers `quoteId` over resolving the draft's stored
81
+ // `currentQuoteId`), so a market/currency change made mid-journey books
82
+ // the price the shopper is actually looking at rather than a stale one
83
+ // (voyant#2643). Falls back to draft resolution when no live quote yet.
84
+ const bookRes = await fetcher(apiPath(baseUrl, "/v1/public/catalog/book"), {
85
+ method: "POST",
86
+ credentials: "include",
87
+ headers: { "content-type": "application/json" },
88
+ body: JSON.stringify({
89
+ draftId,
90
+ quoteId: context.quoteId,
91
+ party: buildStorefrontCommitParty(context.draft),
92
+ paymentIntent: { type: "hold" },
93
+ idempotencyKey,
94
+ }),
95
+ });
96
+ if (!bookRes.ok) {
97
+ const errBody = (await bookRes.json().catch(() => ({})));
98
+ console.error("[storefront] /book failed", errBody);
99
+ // Reserve failures (e.g. 502 RESERVE_FAILED with reason
100
+ // "rates_missing") must reach the customer — throw so the journey
101
+ // surfaces a visible error instead of silently dropping back to
102
+ // Review (voyant#2638). A missing-rate / availability reason gets the
103
+ // "adjust your selection" copy; everything else the generic message.
104
+ // The engine error serializer nests the upstream payload under
105
+ // `context.upstreamPayload` (ReserveFailedError), not at top level.
106
+ const reason = typeof errBody.context?.upstreamPayload?.reason === "string"
107
+ ? errBody.context.upstreamPayload.reason
108
+ : undefined;
109
+ throw new CheckoutError(reason === "rates_missing"
110
+ ? messages.reserveFailed
111
+ : buildStorefrontBookFailureMessage(errBody, bookRes.headers.get("x-request-id"), messages.checkoutFailed, messages.requestReference));
112
+ }
113
+ const bookJson = (await bookRes.json());
114
+ const bookingId = bookJson.bookingId;
115
+ if (!bookingId) {
116
+ console.error("[storefront] /book returned no bookingId", bookJson);
117
+ throw new CheckoutError(messages.checkoutFailed);
118
+ }
119
+ // Step 2 — start checkout with the payment method selected in
120
+ // the journey's Payment step. Card goes to the PSP; bank
121
+ // transfer returns IBAN/reference instructions; inquiry skips
122
+ // inventory/payment.
123
+ const startRes = await fetcher(apiPath(baseUrl, "/v1/public/catalog/checkout/start"), {
124
+ method: "POST",
125
+ credentials: "include",
126
+ headers: { "content-type": "application/json" },
127
+ body: JSON.stringify({
128
+ bookingId,
129
+ paymentIntent: intent,
130
+ ...(acceptance
131
+ ? {
132
+ contractAcceptance: {
133
+ templateId: acceptance.templateId,
134
+ templateSlug: acceptance.templateSlug,
135
+ acceptedTerms: true,
136
+ acceptedMarketing: acceptance.acceptedMarketing,
137
+ acceptedAt: acceptance.acceptedAt,
138
+ renderedHtml: acceptance.renderedHtml,
139
+ },
140
+ }
141
+ : {}),
142
+ ...(contact?.email ? { payerEmail: contact.email } : {}),
143
+ ...(payerName ? { payerName } : {}),
144
+ returnOrigin: window.location.origin,
145
+ }),
146
+ });
147
+ const json = (await startRes.json());
148
+ if ("error" in json) {
149
+ console.error("[storefront] /checkout/start error", json);
150
+ throw new CheckoutError(messages.checkoutFailed);
151
+ }
152
+ switch (json.kind) {
153
+ case "card_redirect":
154
+ if (json.redirectUrl) {
155
+ onRedirectToPayment(json.redirectUrl);
156
+ }
157
+ else {
158
+ onNavigateToConfirmation(json.bookingId, "card_pending");
159
+ }
160
+ break;
161
+ case "bank_transfer_instructions":
162
+ // Hand the instructions to the confirmation page so the
163
+ // customer leaves with the IBAN + reference visible. Stored
164
+ // in sessionStorage to keep the URL clean — refreshing the
165
+ // page replays the same instructions.
166
+ if (typeof sessionStorage !== "undefined") {
167
+ sessionStorage.setItem(`voyant.checkout.${json.bookingId}`, JSON.stringify(json));
168
+ }
169
+ onNavigateToConfirmation(json.bookingId, "bank_transfer");
170
+ break;
171
+ case "inquiry_received":
172
+ onNavigateToConfirmation(json.bookingId, "inquiry");
173
+ break;
174
+ case "hold_placed":
175
+ onNavigateToConfirmation(json.bookingId, "hold");
176
+ break;
177
+ }
178
+ }
179
+ catch (err) {
180
+ console.error("[storefront] checkout flow failed", err);
181
+ // Re-throw so <BookingJourney /> can render a visible checkout error
182
+ // (voyant#2638). Preserve our own localized CheckoutError messages; wrap
183
+ // anything else (native fetch/network error, JSON parse of an HTML 502)
184
+ // in the generic message so raw browser/parser text never reaches the UI.
185
+ throw err instanceof CheckoutError ? err : new Error(messages.checkoutFailed);
186
+ }
187
+ };
188
+ 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
189
+ ? {
190
+ templateSlug: resolvedSlug,
191
+ previewUrl: apiPath(baseUrl, `/v1/public/legal/contracts/templates/by-slug/${encodeURIComponent(resolvedSlug)}/preview`),
192
+ acceptLanguage: typeof navigator !== "undefined" ? navigator.language : undefined,
193
+ resolveVariables: ({ draft, pricing }) => {
194
+ // Use the server-resolved cascade when the public
195
+ // /v1/public/payment-policy/resolve endpoint has
196
+ // come back; while the request is in flight, fall
197
+ // back to the operator default so the customer sees
198
+ // a sensible preview rather than an empty schedule.
199
+ const policy = resolvedPolicy?.policy ??
200
+ operatorProfile?.customerPaymentPolicy ??
201
+ noDepositPolicy;
202
+ const source = resolvedPolicy?.source ?? "operator_default";
203
+ const schedule = pricing
204
+ ? computePaymentSchedule({
205
+ totalCents: pricing.total,
206
+ currency: pricing.currency,
207
+ departureDate: entitySummary?.startDate ?? null,
208
+ }, policy)
209
+ : [];
210
+ return resolveContractVariables(draft, {
211
+ entityModule,
212
+ entityId,
213
+ entitySummary,
214
+ pricing,
215
+ operatorInfo: operatorProfile,
216
+ paymentSchedule: schedule,
217
+ paymentPolicySource: source,
218
+ source: entitySource,
219
+ });
220
+ },
221
+ ...(contractMarketingLabel ? { marketingLabel: contractMarketingLabel } : {}),
222
+ }
223
+ : undefined, onContractAccepted: onContractAccepted ?? defaultCheckoutHandler, paymentCapabilities: {
224
+ // Storefront defaults — covers the three customer-facing
225
+ // payment paths a typical tour operator wants. The deployment
226
+ // can override per-product (e.g. high-ticket cruises that
227
+ // should always go through inquiry first).
228
+ //
229
+ // - acceptsCard: real-time card via the configured PSP
230
+ // (renderPaymentProviderStep supplies the widget — when
231
+ // absent we fall back to a "we'll email a link" message).
232
+ // - acceptsBankTransfer: lock inventory, send bank details
233
+ // out of band, reconcile on receipt of funds.
234
+ // - acceptsInquiry: lead-only path. No inventory hold, no
235
+ // charge — operator follows up manually. Useful for
236
+ // custom itineraries / availability-on-request products.
237
+ acceptsCard: true,
238
+ acceptsBankTransfer: true,
239
+ acceptsHold: false,
240
+ acceptsTicketOnCredit: false,
241
+ acceptsInquiry: true,
242
+ }, className: className, ...slots }));
243
+ }
244
+ function checkoutIntentFromDraft(intent) {
245
+ if (intent === "card" || intent === "bank_transfer" || intent === "inquiry")
246
+ return intent;
247
+ return "hold";
248
+ }
249
+ export function buildStorefrontCommitParty(draft) {
250
+ const contact = draft.billing.contact;
251
+ const personId = draft.billing.buyerType === "B2C" ? contact.personId : undefined;
252
+ const organizationId = draft.billing.buyerType === "B2B" ? draft.billing.organizationId : undefined;
253
+ return {
254
+ personId,
255
+ organizationId,
256
+ billing: {
257
+ personId,
258
+ organizationId,
259
+ contact: {
260
+ firstName: contact.firstName,
261
+ lastName: contact.lastName,
262
+ email: contact.email,
263
+ phone: contact.phone,
264
+ },
265
+ },
266
+ travelerParty: {
267
+ travelers: draft.travelers.map((traveler) => ({
268
+ personId: traveler.personId,
269
+ firstName: traveler.firstName,
270
+ lastName: traveler.lastName,
271
+ email: traveler.email,
272
+ phone: traveler.phone,
273
+ dateOfBirth: traveler.dateOfBirth,
274
+ band: traveler.band,
275
+ documents: traveler.documents,
276
+ isPrimary: traveler.isPrimary,
277
+ })),
278
+ },
279
+ };
280
+ }
281
+ /**
282
+ * Resolve which contract template to preview at the Review step.
283
+ *
284
+ * Order:
285
+ * 1. The per-product `contractTemplateSlug` override the caller
286
+ * passed in (skips the network call).
287
+ * 2. The active customer-scope template returned by
288
+ * `GET /v1/public/legal/contracts/templates/default?scope=customer`.
289
+ *
290
+ * Returns `undefined` while the request is in flight or when the
291
+ * deployment hasn't seeded a customer template — the journey then
292
+ * skips the dialog and routes Confirm straight to the
293
+ * checkout-start handler.
294
+ */
295
+ function useResolvedContractSlug(override, baseUrl, fetcher) {
296
+ const language = typeof navigator !== "undefined" ? navigator.language?.split("-")[0] : undefined;
297
+ const { data } = useQuery({
298
+ queryKey: ["public-legal-default-template", "customer", language ?? "en"],
299
+ queryFn: async () => {
300
+ const params = new URLSearchParams({ scope: "customer" });
301
+ if (language)
302
+ params.set("language", language);
303
+ const res = await fetcher(apiPath(baseUrl, `/v1/public/legal/contracts/templates/default?${params.toString()}`), { credentials: "include" });
304
+ if (!res.ok)
305
+ return null;
306
+ const json = (await res.json());
307
+ return json.data?.slug ?? null;
308
+ },
309
+ enabled: !override,
310
+ staleTime: 5 * 60 * 1000,
311
+ });
312
+ if (override)
313
+ return override;
314
+ return data ?? undefined;
315
+ }
316
+ /**
317
+ * Fetch the operator profile (name / legal name / address / license /
318
+ * default customer payment policy) from the public settings endpoint.
319
+ * The result is cached for 5 minutes — operator
320
+ * details rarely change, and stale-while-revalidate is fine for the
321
+ * contract preview UI.
322
+ *
323
+ * Returns `undefined` while the request is in flight or when the
324
+ * operator hasn't filled in Settings -> Operator profile yet — the contract
325
+ * preview then renders the operator block with `-` placeholders (the
326
+ * template renderer's missing-value substitution kicks in).
327
+ */
328
+ function usePublicOperatorProfile(baseUrl, fetcher) {
329
+ const { data } = useQuery({
330
+ queryKey: ["public-operator-profile"],
331
+ queryFn: async () => {
332
+ const res = await fetcher(apiPath(baseUrl, "/v1/public/operator-profile"), {
333
+ credentials: "include",
334
+ });
335
+ if (!res.ok)
336
+ return null;
337
+ const json = (await res.json());
338
+ return json.data ?? null;
339
+ },
340
+ staleTime: 5 * 60 * 1000,
341
+ });
342
+ return data ?? undefined;
343
+ }
344
+ /**
345
+ * Server-side cascade resolution for the storefront preview.
346
+ *
347
+ * Calls `POST /v1/public/payment-policy/resolve` with the entity
348
+ * coordinates and returns the resolved policy + which cascade layer
349
+ * supplied it (operator_default | supplier | category | listing).
350
+ *
351
+ * Resolves at entity granularity only — the journey's later
352
+ * sailing / cabin / rate-plan selections refine the cascade
353
+ * server-side at booking-confirmed time. Storefront preview shows
354
+ * the entity-level result, which is correct for the common case
355
+ * (single sailing per cruise, single rate plan picked at booking
356
+ * time, etc.) and gracefully degrades to a less-specific layer when
357
+ * the journey hasn't picked yet.
358
+ */
359
+ function useResolvedPaymentPolicy(entityModule, entityId, baseUrl, fetcher) {
360
+ const { data } = useQuery({
361
+ queryKey: ["public-payment-policy", entityModule, entityId],
362
+ queryFn: async () => {
363
+ const res = await fetcher(apiPath(baseUrl, "/v1/public/payment-policy/resolve"), {
364
+ method: "POST",
365
+ credentials: "include",
366
+ headers: { "content-type": "application/json" },
367
+ body: JSON.stringify({ entityModule, entityId }),
368
+ });
369
+ if (!res.ok)
370
+ return null;
371
+ const json = (await res.json());
372
+ return json.data ?? null;
373
+ },
374
+ staleTime: 5 * 60 * 1000,
375
+ });
376
+ return data ?? undefined;
377
+ }
378
+ function apiPath(baseUrl, path) {
379
+ return `${baseUrl.replace(/\/$/, "")}${path}`;
380
+ }
@@ -0,0 +1,36 @@
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;
36
+ //# sourceMappingURL=storefront-booking-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-booking-page.d.ts","sourceRoot":"","sources":["../../src/storefront/storefront-booking-page.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,OAAO,EAEL,KAAK,gCAAgC,EACrC,KAAK,6BAA6B,EACnC,MAAM,iCAAiC,CAAA;AAExC,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;iBAcxC,CAAA;AAEF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAA;AAEnF,MAAM,WAAW,6BAA8B,SAAQ,gCAAgC;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAChD,6BAA6B,EAC3B,WAAW,GACX,wBAAwB,GACxB,sBAAsB,GACtB,oBAAoB,GACpB,qBAAqB,CACxB,CAAA;AAED,MAAM,WAAW,0BAA0B;IACzC,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,uBAAuB,CAAA;IAC/B,QAAQ,EAAE,6BAA6B,CAAA;IACvC,UAAU,CAAC,EAAE,+BAA+B,CAAA;CAC7C;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,EACpC,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,UAAU,GACX,EAAE,0BAA0B,GAAG,KAAK,CAAC,YAAY,CAgCjD"}