@voyant-travel/bookings-react 0.151.5 → 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.
- package/README.md +6 -1
- package/dist/admin/booking-detail-host.d.ts +12 -12
- package/dist/admin/booking-detail-host.d.ts.map +1 -1
- package/dist/admin/booking-detail-host.js +12 -5
- package/dist/admin/index.d.ts +5 -26
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +11 -25
- package/dist/admin/pages/booking-detail-page.d.ts +3 -5
- package/dist/admin/pages/booking-detail-page.d.ts.map +1 -1
- package/dist/admin/pages/booking-detail-page.js +3 -5
- package/dist/admin/pages/bookings-index-page.d.ts +2 -12
- package/dist/admin/pages/bookings-index-page.d.ts.map +1 -1
- package/dist/admin/pages/bookings-index-page.js +4 -3
- package/dist/admin/slots.d.ts +7 -0
- package/dist/admin/slots.d.ts.map +1 -1
- package/dist/admin/slots.js +7 -0
- package/dist/hooks/use-public-booking-session-flow-mutation.d.ts +1 -1
- package/dist/hooks/use-public-booking-session.d.ts +1 -1
- package/dist/query-options.d.ts +4 -4
- package/dist/storefront/index.d.ts +4 -0
- package/dist/storefront/index.d.ts.map +1 -0
- package/dist/storefront/index.js +3 -0
- package/dist/storefront/resolve-contract-variables.d.ts +131 -0
- package/dist/storefront/resolve-contract-variables.d.ts.map +1 -0
- package/dist/storefront/resolve-contract-variables.js +492 -0
- package/dist/storefront/storefront-booking-errors.d.ts +13 -0
- package/dist/storefront/storefront-booking-errors.d.ts.map +1 -0
- package/dist/storefront/storefront-booking-errors.js +50 -0
- package/dist/storefront/storefront-booking-journey.d.ts +72 -0
- package/dist/storefront/storefront-booking-journey.d.ts.map +1 -0
- package/dist/storefront/storefront-booking-journey.js +380 -0
- package/dist/storefront/storefront-booking-page.d.ts +36 -0
- package/dist/storefront/storefront-booking-page.d.ts.map +1 -0
- package/dist/storefront/storefront-booking-page.js +213 -0
- 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"}
|