@voyant-travel/bookings-react 0.151.5 → 0.153.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,131 @@
1
+ /**
2
+ * Default mapping from a `BookingDraftV1` (+ live quote pricing +
3
+ * resolved entity summary + operator / acceptance context) to the
4
+ * variable bag the operator's contract templates render against.
5
+ *
6
+ * The variable surface is designed around Voyant's domain model:
7
+ *
8
+ * - sell vs cost money split (`sellAmountCents`, `sellCurrency`,
9
+ * `costAmountCents`, `costCurrency`, `baseCurrency`)
10
+ * - vertical-aware schedule blocks — `departureSlot` for products,
11
+ * `sailing` for cruises, `stay` for accommodations
12
+ * - pax-bands as a record (Voyant doesn't lock you into adult /
13
+ * child / infant — descriptors can declare any band code)
14
+ * - sourced vs owned booking arms (`booking.source.kind`,
15
+ * `booking.source.supplier`)
16
+ * - CRM customer + lead traveler split, kept distinct because the
17
+ * buyer + the lead passenger are often different people
18
+ *
19
+ * Naming is camelCase by default. A handful of snake_case aliases
20
+ * are emitted for keys the seeded `customer-sales-agreement`
21
+ * template references (`contract.date`, `booking.startDate`,
22
+ * `travelers[].participantType` …) so authored templates render
23
+ * out-of-the-box.
24
+ *
25
+ * Notes on what is + isn't filled at preview time:
26
+ *
27
+ * - The booking row has not been created yet — `booking.bookingId`,
28
+ * `booking.bookingNumber`, the persisted status, and
29
+ * `contract.contractNumber` / `contract.signedAt` are empty. The
30
+ * server-side auto-generate-contract subscriber re-renders the
31
+ * same template on `booking.confirmed` with the persisted values
32
+ * filled in.
33
+ * - `acceptance.ipAddress` / `userAgent` are captured server-side
34
+ * at `/checkout/start` from request headers. Empty during preview.
35
+ * - `operator.*` reads from the `operatorInfo` block injected by the
36
+ * storefront wrapper (fetched from `/v1/public/operator-profile`).
37
+ * Anything not configured renders as the empty string.
38
+ * - Vertical-specific blocks (`sailing`, `stay`, `departureSlot`)
39
+ * are populated only when `entitySummary` carries enough context.
40
+ */
41
+ import type { BookingDraftV1, PricingBreakdownV1 } from "@voyant-travel/catalog/booking-engine";
42
+ import type { ComputedScheduleEntry, PaymentPolicySource } from "@voyant-travel/finance/payment-policy";
43
+ import type { BookingEntitySummary } from "../journey/index.js";
44
+ export interface OperatorInfoVariables {
45
+ /** Trading name shown to customers. */
46
+ name?: string;
47
+ /** Legal company name when different from `name`. */
48
+ legalName?: string;
49
+ /** Tax / VAT id. */
50
+ vatId?: string;
51
+ /** Trade register number (RO: J-number; UK: company number; …). */
52
+ registrationNumber?: string;
53
+ /** Postal address — single string or markdown for the contract block. */
54
+ address?: string;
55
+ phone?: string;
56
+ email?: string;
57
+ website?: string;
58
+ iban?: string;
59
+ bank?: string;
60
+ /** License number — tour-operator license, hotel rating registry,
61
+ * cruise flag-state number, depending on what the operator is. */
62
+ license?: string;
63
+ /** Issuing authority for the license. */
64
+ licenseAuthority?: string;
65
+ /** Human whose name appears on the operator-side signature line. */
66
+ signatoryName?: string;
67
+ /** Their role / title (e.g. "Managing Director"). */
68
+ signatoryRole?: string;
69
+ }
70
+ interface AcceptanceContextVariables {
71
+ ipAddress?: string;
72
+ userAgent?: string;
73
+ acceptedAt?: string;
74
+ marketingConsent?: boolean;
75
+ templateSlug?: string;
76
+ templateId?: string;
77
+ }
78
+ /**
79
+ * Source provenance for the booked entity, resolved from the catalog
80
+ * plane (the public content endpoint returns it as `provenance` +
81
+ * `product.supplier`). Threaded into the contract preview so the
82
+ * `booking.source` block reflects the real sourced/owned arm instead
83
+ * of defaulting to `owned` with a blank supplier (voyant#2619).
84
+ *
85
+ * When absent (or `kind` is empty / `"owned"`), the contract renders
86
+ * the owned arm — blank connection/ref/supplier — exactly as before.
87
+ */
88
+ export interface ContractSourceContext {
89
+ /** Provenance kind — `"owned"` for owned inventory, otherwise the
90
+ * upstream source kind (e.g. `marketplace:demo`). */
91
+ kind?: string;
92
+ /** Source connection that produced the row (sourced only). */
93
+ connectionId?: string;
94
+ /** Stable upstream object id (sourced only). */
95
+ ref?: string;
96
+ /** Supplier disclosed to the customer in the contract. */
97
+ supplier?: {
98
+ id?: string;
99
+ name?: string;
100
+ };
101
+ }
102
+ export interface ResolveContractVariablesContext {
103
+ entityModule: string;
104
+ entityId: string;
105
+ entitySummary?: BookingEntitySummary;
106
+ pricing?: PricingBreakdownV1 | null;
107
+ /** Operator profile — fetched from `/v1/public/operator-profile` by
108
+ * the storefront wrapper. Anything missing renders as empty. */
109
+ operatorInfo?: OperatorInfoVariables;
110
+ /** Acceptance fingerprint — populated only on server-side renders
111
+ * (post-confirm contract auto-generation). At preview time the
112
+ * storefront leaves this undefined and the variables render
113
+ * empty. */
114
+ acceptance?: AcceptanceContextVariables;
115
+ /** Pre-computed schedule from `computePaymentSchedule()`. The
116
+ * storefront wrapper computes this in real time as the customer
117
+ * picks their date so the contract preview shows live deposit
118
+ * / balance numbers. */
119
+ paymentSchedule?: ComputedScheduleEntry[] | null;
120
+ /** Which layer of the cascade the active policy came from. Used
121
+ * for traceability in contract templates. */
122
+ paymentPolicySource?: PaymentPolicySource;
123
+ /** Resolved source provenance for the booked entity. Populated by
124
+ * the storefront wrapper from the public content endpoint's
125
+ * `provenance` + supplier. When omitted the booking renders as the
126
+ * owned arm (voyant#2619). */
127
+ source?: ContractSourceContext;
128
+ }
129
+ export declare function resolveContractVariables(draft: BookingDraftV1, ctx: ResolveContractVariablesContext): Record<string, unknown>;
130
+ export {};
131
+ //# sourceMappingURL=resolve-contract-variables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-contract-variables.d.ts","sourceRoot":"","sources":["../../src/storefront/resolve-contract-variables.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAA;AAC/F,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,uCAAuC,CAAA;AAE9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAE/D,MAAM,WAAW,qBAAqB;IACpC,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;uEACmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yCAAyC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,UAAU,0BAA0B;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;0DACsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,QAAQ,CAAC,EAAE;QACT,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED,MAAM,WAAW,+BAA+B;IAC9C,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,oBAAoB,CAAA;IACpC,OAAO,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAA;IACnC;qEACiE;IACjE,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC;;;iBAGa;IACb,UAAU,CAAC,EAAE,0BAA0B,CAAA;IACvC;;;6BAGyB;IACzB,eAAe,CAAC,EAAE,qBAAqB,EAAE,GAAG,IAAI,CAAA;IAChD;kDAC8C;IAC9C,mBAAmB,CAAC,EAAE,mBAAmB,CAAA;IACzC;;;mCAG+B;IAC/B,MAAM,CAAC,EAAE,qBAAqB,CAAA;CAC/B;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,+BAA+B,GACnC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqVzB"}
@@ -0,0 +1,492 @@
1
+ // agent-quality: file-size exception -- owner: bookings-react; contract variables stay centralized and behavior-tested.
2
+ /**
3
+ * Default mapping from a `BookingDraftV1` (+ live quote pricing +
4
+ * resolved entity summary + operator / acceptance context) to the
5
+ * variable bag the operator's contract templates render against.
6
+ *
7
+ * The variable surface is designed around Voyant's domain model:
8
+ *
9
+ * - sell vs cost money split (`sellAmountCents`, `sellCurrency`,
10
+ * `costAmountCents`, `costCurrency`, `baseCurrency`)
11
+ * - vertical-aware schedule blocks — `departureSlot` for products,
12
+ * `sailing` for cruises, `stay` for accommodations
13
+ * - pax-bands as a record (Voyant doesn't lock you into adult /
14
+ * child / infant — descriptors can declare any band code)
15
+ * - sourced vs owned booking arms (`booking.source.kind`,
16
+ * `booking.source.supplier`)
17
+ * - CRM customer + lead traveler split, kept distinct because the
18
+ * buyer + the lead passenger are often different people
19
+ *
20
+ * Naming is camelCase by default. A handful of snake_case aliases
21
+ * are emitted for keys the seeded `customer-sales-agreement`
22
+ * template references (`contract.date`, `booking.startDate`,
23
+ * `travelers[].participantType` …) so authored templates render
24
+ * out-of-the-box.
25
+ *
26
+ * Notes on what is + isn't filled at preview time:
27
+ *
28
+ * - The booking row has not been created yet — `booking.bookingId`,
29
+ * `booking.bookingNumber`, the persisted status, and
30
+ * `contract.contractNumber` / `contract.signedAt` are empty. The
31
+ * server-side auto-generate-contract subscriber re-renders the
32
+ * same template on `booking.confirmed` with the persisted values
33
+ * filled in.
34
+ * - `acceptance.ipAddress` / `userAgent` are captured server-side
35
+ * at `/checkout/start` from request headers. Empty during preview.
36
+ * - `operator.*` reads from the `operatorInfo` block injected by the
37
+ * storefront wrapper (fetched from `/v1/public/operator-profile`).
38
+ * Anything not configured renders as the empty string.
39
+ * - Vertical-specific blocks (`sailing`, `stay`, `departureSlot`)
40
+ * are populated only when `entitySummary` carries enough context.
41
+ */
42
+ export function resolveContractVariables(draft, ctx) {
43
+ const billing = draft.billing;
44
+ const contact = billing.contact;
45
+ const address = billing.address;
46
+ const summary = ctx.entitySummary;
47
+ const pricing = ctx.pricing ?? null;
48
+ const operatorInfo = ctx.operatorInfo ?? {};
49
+ const acceptance = ctx.acceptance ?? {};
50
+ const travelers = draft.travelers.map((t, i) => {
51
+ const fullName = [t.firstName, t.lastName].filter(Boolean).join(" ").trim();
52
+ const docs = t.documents ?? {};
53
+ return {
54
+ index: i + 1,
55
+ band: t.band,
56
+ // The seeded template branches on `participantType != "traveler"`.
57
+ // We map adult → traveler, everything else passes through.
58
+ participantType: t.band === "adult" ? "traveler" : t.band,
59
+ isLead: i === 0,
60
+ firstName: t.firstName,
61
+ lastName: t.lastName,
62
+ fullName,
63
+ email: t.email ?? "",
64
+ phone: t.phone ?? "",
65
+ dateOfBirth: t.dateOfBirth ?? "",
66
+ document: {
67
+ type: stringFromDoc(docs, "documentType"),
68
+ number: stringFromDoc(docs, "documentNumber"),
69
+ country: stringFromDoc(docs, "documentCountry"),
70
+ issuingAuthority: stringFromDoc(docs, "issuingAuthority"),
71
+ issueDate: stringFromDoc(docs, "issueDate"),
72
+ expiryDate: stringFromDoc(docs, "documentExpiry"),
73
+ },
74
+ };
75
+ });
76
+ const paxBands = draft.configure?.pax ?? {};
77
+ const paxTotal = Object.values(paxBands).reduce((acc, count) => acc + (count ?? 0), 0);
78
+ // Prefer raw ISO from entitySummary (set by the detail page from
79
+ // departures[].starts_at / sailings[].start_date / search.checkIn)
80
+ // over the slot id we keep in the draft.
81
+ const startDate = summary?.startDate ??
82
+ draft.configure?.dateRange?.checkIn ??
83
+ draft.configure?.departureDate ??
84
+ "";
85
+ const endDate = summary?.endDate ?? draft.configure?.dateRange?.checkOut ?? draft.configure?.departureDate ?? "";
86
+ const durationNights = computeNights(startDate, endDate);
87
+ const customerFullName = [contact.firstName, contact.lastName].filter(Boolean).join(" ").trim();
88
+ const leadTraveler = contact.firstName || contact.lastName
89
+ ? {
90
+ firstName: contact.firstName,
91
+ lastName: contact.lastName,
92
+ fullName: customerFullName,
93
+ email: contact.email ?? "",
94
+ phone: contact.phone ?? "",
95
+ }
96
+ : null;
97
+ // Quote line items map to the contract's `items[]` collection.
98
+ const items = (pricing?.lines ?? []).map((line, idx) => ({
99
+ index: idx + 1,
100
+ kind: line.kind,
101
+ description: line.label ?? line.kind ?? "",
102
+ quantity: line.quantity ?? 1,
103
+ unitAmountCents: line.unitAmount,
104
+ totalAmountCents: line.totalAmount,
105
+ taxIncluded: line.taxIncluded ?? false,
106
+ currency: pricing?.currency ?? "",
107
+ }));
108
+ const addons = items.filter((i) => i.kind === "addon" || i.kind === "supplement");
109
+ const totalCents = pricing?.total ?? 0;
110
+ const subtotalCents = pricing?.subtotal ?? 0;
111
+ const taxTotalCents = pricing?.taxTotal ?? 0;
112
+ const sellCurrency = pricing?.currency ?? "";
113
+ const accommodationRooms = (draft.accommodation?.rooms ?? []).map((r) => ({
114
+ optionUnitId: r.optionUnitId,
115
+ quantity: r.quantity,
116
+ ratePlanId: r.ratePlanId ?? "",
117
+ }));
118
+ const vertical = summary?.vertical ?? ctx.entityModule;
119
+ const vehiclesAndStay = buildVerticalBlocks({
120
+ vertical,
121
+ summary,
122
+ draft,
123
+ accommodationRooms,
124
+ durationNights,
125
+ startDate,
126
+ endDate,
127
+ });
128
+ const today = new Date();
129
+ const todayIso = today.toISOString().slice(0, 10);
130
+ const todayIsoDateTime = today.toISOString();
131
+ const todayTime = today.toISOString().slice(11, 19);
132
+ // Map the precomputed schedule into the deposit / balance / full
133
+ // shapes contract templates read. `"full"` collapses to the
134
+ // balance-row slot (it's the single payment due in that scenario).
135
+ const schedule = ctx.paymentSchedule ?? [];
136
+ const depositRow = schedule.find((r) => r.scheduleType === "deposit");
137
+ const balanceRow = schedule.find((r) => r.scheduleType === "balance") ??
138
+ schedule.find((r) => r.scheduleType === "full");
139
+ return {
140
+ // ───────── Top-level clocks ─────────
141
+ today: todayIso,
142
+ currentDate: todayIso,
143
+ currentDateTime: todayIsoDateTime,
144
+ currentTime: todayTime,
145
+ // ───────── Contract metadata ─────────
146
+ contract: {
147
+ contractNumber: "",
148
+ number: "",
149
+ contractDate: todayIso,
150
+ // snake-cased alias used by the seeded customer-sales-agreement
151
+ // template (`{{ contract.date | format_date: "long" }}`)
152
+ date: todayIso,
153
+ signedAt: acceptance.acceptedAt ?? "",
154
+ isManual: false,
155
+ series: "",
156
+ channel: "storefront",
157
+ source: "self_service",
158
+ },
159
+ // ───────── Booking ─────────
160
+ booking: {
161
+ // Identity (filled in once the booking row exists; empty at preview)
162
+ bookingId: "",
163
+ bookingNumber: "",
164
+ // alias used by templates that read `{{ booking.number }}`
165
+ number: "",
166
+ status: "draft",
167
+ // Entity context
168
+ entityModule: ctx.entityModule,
169
+ entityId: ctx.entityId,
170
+ vertical,
171
+ productName: summary?.name ?? ctx.entityId,
172
+ productSubtitle: summary?.subtitle ?? "",
173
+ destination: summary?.destination ?? summary?.locationLabel ?? "",
174
+ whenLabel: summary?.whenLabel ?? "",
175
+ locationLabel: summary?.locationLabel ?? "",
176
+ // Pax — bands map keeps Voyant's flexible band model intact
177
+ pax: paxTotal,
178
+ paxTotal,
179
+ paxBands,
180
+ paxAdult: paxBands.adult ?? 0,
181
+ paxChild: paxBands.child ?? 0,
182
+ paxInfant: paxBands.infant ?? 0,
183
+ // Configure (raw inputs for verticals that need slot ids)
184
+ departureSlotId: draft.configure?.departureSlotId ?? "",
185
+ cabinCategoryId: draft.configure?.cabinCategoryId ?? "",
186
+ cabinNumberId: draft.configure?.cabinNumberId ?? "",
187
+ airArrangement: draft.configure?.airArrangement ?? "",
188
+ // Trip dates (vertical-agnostic) — primary names + the
189
+ // snake_case aliases the seeded template references
190
+ travelDates: {
191
+ start: startDate,
192
+ end: endDate,
193
+ durationNights,
194
+ },
195
+ startDate,
196
+ endDate,
197
+ checkIn: draft.configure?.dateRange?.checkIn ?? "",
198
+ checkOut: draft.configure?.dateRange?.checkOut ?? "",
199
+ departureDate: draft.configure?.departureDate ?? "",
200
+ // Money (Voyant's sell vs cost split)
201
+ sellCurrency,
202
+ sellAmountCents: totalCents,
203
+ sellSubtotalCents: subtotalCents,
204
+ sellTaxAmountCents: taxTotalCents,
205
+ sellDiscountAmountCents: 0,
206
+ // Cost-side fields are operator-private and only emitted by
207
+ // server-side renders that pass them in via context. Empty
208
+ // during the customer-facing preview by design.
209
+ costCurrency: "",
210
+ costAmountCents: 0,
211
+ baseCurrency: "",
212
+ // Common alias shapes used by templates we ship with the
213
+ // operator starter
214
+ currency: sellCurrency,
215
+ totalAmountCents: totalCents,
216
+ subtotalAmountCents: subtotalCents,
217
+ taxAmountCents: taxTotalCents,
218
+ discountAmountCents: 0,
219
+ // Settlement state — empty at preview, post-confirm renders
220
+ // can fill these from the booking + payments table
221
+ paidAmountCents: 0,
222
+ balanceDueCents: totalCents,
223
+ // Payment-schedule-derived deposit / balance. The storefront
224
+ // wrapper runs `computePaymentSchedule()` against the
225
+ // operator's policy + the draft's pricing/dates and passes
226
+ // the result via `paymentSchedule`, so the contract preview
227
+ // shows the same numbers the post-confirm schedule will
228
+ // generate (modulo per-layer overrides applied later).
229
+ depositAmountCents: depositRow?.amountCents ?? 0,
230
+ depositDueDate: depositRow?.dueDate ?? "",
231
+ balanceAmountCents: balanceRow?.amountCents ?? 0,
232
+ balanceDueDate: balanceRow?.dueDate ?? "",
233
+ paymentPolicy: {
234
+ source: ctx.paymentPolicySource ?? "operator_default",
235
+ },
236
+ // Best-effort accommodation summary. Server-side resolver
237
+ // overrides with the persisted booking_items rows.
238
+ roomsSummary: buildRoomsSummary(draft),
239
+ // Source provenance — resolved from the catalog plane via the
240
+ // public content endpoint's `provenance` + supplier and threaded
241
+ // in by the storefront wrapper. Falls back to the owned arm
242
+ // (blank connection/ref/supplier) when unresolved (voyant#2619).
243
+ source: buildBookingSource(ctx.source),
244
+ // Notes
245
+ internalNotes: draft.internalNotes ?? "",
246
+ customerNotes: draft.customerNotes ?? "",
247
+ },
248
+ // ───────── Customer / Buyer ─────────
249
+ customer: {
250
+ type: billing.buyerType,
251
+ firstName: contact.firstName,
252
+ lastName: contact.lastName,
253
+ fullName: customerFullName,
254
+ email: contact.email ?? "",
255
+ phone: contact.phone ?? "",
256
+ dateOfBirth: travelers[0]?.dateOfBirth ?? "",
257
+ companyName: billing.company?.name ?? "",
258
+ vatId: billing.company?.vatId ?? "",
259
+ registrationNumber: billing.company?.registrationNumber ?? "",
260
+ address: {
261
+ line1: address.line1 ?? "",
262
+ line2: address.line2 ?? "",
263
+ city: address.city ?? "",
264
+ postal: address.postal ?? "",
265
+ country: address.country ?? "",
266
+ },
267
+ // Lead traveler's identity document, used as the buyer's
268
+ // identity doc for B2C flows
269
+ document: travelers[0]?.document ?? {
270
+ type: "",
271
+ number: "",
272
+ country: "",
273
+ issuingAuthority: "",
274
+ issueDate: "",
275
+ expiryDate: "",
276
+ },
277
+ },
278
+ // ───────── Lead traveler ─────────
279
+ leadTraveler,
280
+ // ───────── Travelers ─────────
281
+ travelers,
282
+ // `passengers` is a common alias used by templates ported from
283
+ // other systems
284
+ passengers: travelers,
285
+ // ───────── Pricing line items ─────────
286
+ items,
287
+ addons,
288
+ // ───────── Product (subject of the booking) ─────────
289
+ product: {
290
+ title: summary?.name ?? "",
291
+ subtitle: summary?.subtitle ?? "",
292
+ destination: summary?.destination ?? summary?.locationLabel ?? "",
293
+ module: ctx.entityModule,
294
+ id: ctx.entityId,
295
+ vertical,
296
+ heroImageUrl: summary?.heroImageUrl ?? "",
297
+ },
298
+ // ───────── Vertical-specific schedule blocks ─────────
299
+ ...vehiclesAndStay,
300
+ // ───────── Payment ─────────
301
+ payment: {
302
+ intent: draft.payment.intent,
303
+ method: paymentMethodLabel(draft.payment.intent),
304
+ amountCents: totalCents,
305
+ currency: sellCurrency,
306
+ schedule: schedule.map((row, idx) => ({
307
+ index: idx + 1,
308
+ type: row.scheduleType,
309
+ amountCents: row.amountCents,
310
+ currency: row.currency,
311
+ dueDate: row.dueDate,
312
+ status: "pending",
313
+ })),
314
+ capturedAt: "",
315
+ // Alias used by templates that prefer `payment.created_at`-style naming.
316
+ createdAt: "",
317
+ },
318
+ // ───────── Operator ─────────
319
+ operator: {
320
+ name: operatorInfo.name ?? "",
321
+ legalName: operatorInfo.legalName ?? operatorInfo.name ?? "",
322
+ vatId: operatorInfo.vatId ?? "",
323
+ registrationNumber: operatorInfo.registrationNumber ?? "",
324
+ address: operatorInfo.address ?? "",
325
+ phone: operatorInfo.phone ?? "",
326
+ email: operatorInfo.email ?? "",
327
+ website: operatorInfo.website ?? "",
328
+ iban: operatorInfo.iban ?? "",
329
+ bank: operatorInfo.bank ?? "",
330
+ license: operatorInfo.license ?? "",
331
+ licenseAuthority: operatorInfo.licenseAuthority ?? "",
332
+ signatoryName: operatorInfo.signatoryName ?? "",
333
+ signatoryRole: operatorInfo.signatoryRole ?? "",
334
+ },
335
+ // ───────── Acceptance fingerprint ─────────
336
+ acceptance: {
337
+ ipAddress: acceptance.ipAddress ?? "",
338
+ userAgent: acceptance.userAgent ?? "",
339
+ acceptedAt: acceptance.acceptedAt ?? "",
340
+ marketingConsent: acceptance.marketingConsent ?? false,
341
+ templateSlug: acceptance.templateSlug ?? "",
342
+ templateId: acceptance.templateId ?? "",
343
+ },
344
+ };
345
+ }
346
+ /**
347
+ * Map resolved provenance into the contract's `booking.source` block.
348
+ *
349
+ * A missing context, or an explicit `"owned"` / empty `kind`, renders
350
+ * the owned arm with blank connection/ref/supplier — preserving the
351
+ * pre-fix behavior for genuinely owned inventory. Sourced inventory
352
+ * carries its real `kind`, `connectionId`, `ref`, and supplier so the
353
+ * customer contract can disclose the correct supplier/operator split
354
+ * (voyant#2619).
355
+ */
356
+ function buildBookingSource(source) {
357
+ const kind = source?.kind?.trim() ? source.kind : "owned";
358
+ const isOwned = kind === "owned";
359
+ return {
360
+ kind,
361
+ connectionId: source?.connectionId ?? "",
362
+ ref: source?.ref ?? "",
363
+ supplier: {
364
+ // Owned inventory has no upstream supplier — keep it blank so the
365
+ // owned arm is unchanged from before the fix.
366
+ id: isOwned ? "" : (source?.supplier?.id ?? ""),
367
+ name: isOwned ? "" : (source?.supplier?.name ?? ""),
368
+ },
369
+ };
370
+ }
371
+ function stringFromDoc(documents, key) {
372
+ const value = documents?.[key];
373
+ return typeof value === "string" ? value : "";
374
+ }
375
+ // These labels are emitted into contract PDFs as template variable values,
376
+ // not rendered in the operator UI. Contract localization is driven by the
377
+ // contract's `language` field on render and lives in the template body —
378
+ // not the operator's UI locale — so these stay in English at this layer.
379
+ function paymentMethodLabel(intent) {
380
+ switch (intent) {
381
+ case "card":
382
+ // i18n-literal-ok
383
+ return "Card payment";
384
+ case "bank_transfer":
385
+ // i18n-literal-ok
386
+ return "Bank transfer";
387
+ case "inquiry":
388
+ // i18n-literal-ok
389
+ return "Inquiry / Hold";
390
+ case "hold":
391
+ // i18n-literal-ok
392
+ return "Hold";
393
+ case "ticket_on_credit":
394
+ // i18n-literal-ok
395
+ return "Ticket on credit";
396
+ default:
397
+ return "";
398
+ }
399
+ }
400
+ /**
401
+ * Build a "1× DBL (BB)"-style summary from the draft's accommodation
402
+ * rooms. Server-side renders override this with a richer summary
403
+ * derived from the persisted booking_items.
404
+ */
405
+ function buildRoomsSummary(draft) {
406
+ const rooms = draft.accommodation?.rooms ?? [];
407
+ if (rooms.length === 0)
408
+ return "";
409
+ return rooms
410
+ .map((r) => `${r.quantity}× ${r.optionUnitId}${r.ratePlanId ? ` (${r.ratePlanId})` : ""}`)
411
+ .join(", ");
412
+ }
413
+ function computeNights(startDate, endDate) {
414
+ if (!startDate || !endDate)
415
+ return 0;
416
+ try {
417
+ const start = new Date(startDate);
418
+ const end = new Date(endDate);
419
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()))
420
+ return 0;
421
+ const ms = end.getTime() - start.getTime();
422
+ return Math.max(0, Math.round(ms / (1000 * 60 * 60 * 24)));
423
+ }
424
+ catch {
425
+ return 0;
426
+ }
427
+ }
428
+ /**
429
+ * Build vertical-specific top-level blocks. Verticals that don't
430
+ * apply emit empty objects so `{{ sailing.ship }}` renders blank
431
+ * rather than throwing in a strict templating engine.
432
+ */
433
+ function buildVerticalBlocks(ctx) {
434
+ const { vertical, summary, draft, accommodationRooms, durationNights, startDate, endDate } = ctx;
435
+ const blocks = {
436
+ departureSlot: {
437
+ slotId: draft.configure?.departureSlotId ?? "",
438
+ startAt: startDate,
439
+ endAt: endDate,
440
+ durationDays: durationNights,
441
+ },
442
+ sailing: {
443
+ sailingId: "",
444
+ ship: "",
445
+ embarkationPort: "",
446
+ disembarkationPort: "",
447
+ airArrangement: draft.configure?.airArrangement ?? "",
448
+ startDate: "",
449
+ endDate: "",
450
+ },
451
+ stay: {
452
+ checkIn: draft.configure?.dateRange?.checkIn ?? "",
453
+ checkOut: draft.configure?.dateRange?.checkOut ?? "",
454
+ nights: durationNights,
455
+ rooms: accommodationRooms,
456
+ },
457
+ };
458
+ if (vertical === "products") {
459
+ blocks.departureSlot = {
460
+ slotId: draft.configure?.departureSlotId ?? "",
461
+ startAt: startDate,
462
+ endAt: endDate,
463
+ durationDays: durationNights,
464
+ departureCity: summary?.locationLabel ?? "",
465
+ };
466
+ }
467
+ else if (vertical === "cruises") {
468
+ const route = summary?.locationLabel ?? "";
469
+ const [embarkation, disembarkation] = route.split("→").map((s) => s.trim());
470
+ blocks.sailing = {
471
+ sailingId: draft.configure?.departureSlotId ?? "",
472
+ ship: summary?.subtitle ?? "",
473
+ embarkationPort: embarkation ?? "",
474
+ disembarkationPort: disembarkation ?? embarkation ?? "",
475
+ airArrangement: draft.configure?.airArrangement ?? "",
476
+ startDate,
477
+ endDate,
478
+ cabinCategoryId: draft.configure?.cabinCategoryId ?? "",
479
+ cabinNumberId: draft.configure?.cabinNumberId ?? "",
480
+ };
481
+ }
482
+ else if (vertical === "accommodations") {
483
+ blocks.stay = {
484
+ checkIn: draft.configure?.dateRange?.checkIn ?? "",
485
+ checkOut: draft.configure?.dateRange?.checkOut ?? "",
486
+ nights: durationNights,
487
+ rooms: accommodationRooms,
488
+ destination: summary?.locationLabel ?? "",
489
+ };
490
+ }
491
+ return blocks;
492
+ }
@@ -0,0 +1,13 @@
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;
13
+ //# sourceMappingURL=storefront-booking-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storefront-booking-errors.d.ts","sourceRoot":"","sources":["../../src/storefront/storefront-booking-errors.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,OAAO,CAAA;SAAE,CAAA;KAAE,CAAA;CACrD;AAMD,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,uBAAuB,EAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,MAAM,EAChB,wBAAwB,EAAE,MAAM,GAC/B,MAAM,CAOR"}