@voyantjs/products 0.55.0 → 0.55.1
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.
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Phase C+ extensions land on this same handler without re-architecting
|
|
25
25
|
* the dispatch.
|
|
26
26
|
*/
|
|
27
|
-
import { type AddonOffer, type BookingDraftShape, type OwnedBookingHandler, type OwnedHandlerContext, type TravelerFieldRequirement } from "@voyantjs/catalog/booking-engine";
|
|
27
|
+
import { type AddonOffer, type BookingDraftShape, type OwnedBookingHandler, type OwnedHandlerContext, type ProductVariantOption, type TravelerFieldRequirement } from "@voyantjs/catalog/booking-engine";
|
|
28
28
|
/**
|
|
29
29
|
* Subset of `bookingsCreate`'s input the bridge builds.
|
|
30
30
|
* Mirrors the schema in `service-booking-create.ts` — kept
|
|
@@ -37,6 +37,10 @@ export interface BookingCreateBridgeInput {
|
|
|
37
37
|
bookingNumber: string;
|
|
38
38
|
personId?: string | null;
|
|
39
39
|
organizationId?: string | null;
|
|
40
|
+
contactFirstName?: string | null;
|
|
41
|
+
contactLastName?: string | null;
|
|
42
|
+
contactEmail?: string | null;
|
|
43
|
+
contactPhone?: string | null;
|
|
40
44
|
internalNotes?: string | null;
|
|
41
45
|
/**
|
|
42
46
|
* Override the seed sellAmountCents the booking lands at. The owned
|
|
@@ -46,6 +50,15 @@ export interface BookingCreateBridgeInput {
|
|
|
46
50
|
* docs/architecture/promotions-architecture.md §7.1.
|
|
47
51
|
*/
|
|
48
52
|
sellAmountCentsOverride?: number | null;
|
|
53
|
+
/**
|
|
54
|
+
* Status the booking lands in. When omitted, `@voyantjs/finance`'s
|
|
55
|
+
* `createBooking` defaults to `"draft"`. Callers that compose bookings
|
|
56
|
+
* via the catalog booking engine (e.g. trip composer reserve, journey
|
|
57
|
+
* commit) typically want `"awaiting_payment"` so the booking shows in
|
|
58
|
+
* the operator's queue with a payable balance. The handler forwards
|
|
59
|
+
* whatever value is passed via `commit`'s `request.parameters.initialStatus`.
|
|
60
|
+
*/
|
|
61
|
+
initialStatus?: "draft" | "on_hold" | "awaiting_payment" | "confirmed" | "in_progress" | "completed" | "cancelled" | "expired";
|
|
49
62
|
travelers?: Array<{
|
|
50
63
|
firstName: string;
|
|
51
64
|
lastName: string;
|
|
@@ -64,6 +77,10 @@ export interface BookingCreateBridgeInput {
|
|
|
64
77
|
amountCents: number;
|
|
65
78
|
notes?: string | null;
|
|
66
79
|
}>;
|
|
80
|
+
documentGeneration?: {
|
|
81
|
+
contractDocument: boolean;
|
|
82
|
+
invoiceDocument: boolean;
|
|
83
|
+
};
|
|
67
84
|
taxLines?: Array<{
|
|
68
85
|
code?: string | null;
|
|
69
86
|
name: string;
|
|
@@ -76,6 +93,26 @@ export interface BookingCreateBridgeInput {
|
|
|
76
93
|
remittanceParty?: string | null;
|
|
77
94
|
sortOrder?: number;
|
|
78
95
|
}>;
|
|
96
|
+
itemLines?: Array<{
|
|
97
|
+
optionId?: string | null;
|
|
98
|
+
optionUnitId: string;
|
|
99
|
+
quantity: number;
|
|
100
|
+
title?: string | null;
|
|
101
|
+
description?: string | null;
|
|
102
|
+
unitSellAmountCents?: number | null;
|
|
103
|
+
totalSellAmountCents?: number | null;
|
|
104
|
+
}>;
|
|
105
|
+
extraLines?: Array<{
|
|
106
|
+
productExtraId: string;
|
|
107
|
+
name: string;
|
|
108
|
+
description?: string | null;
|
|
109
|
+
pricingMode?: string | null;
|
|
110
|
+
pricedPerPerson?: boolean | null;
|
|
111
|
+
quantity: number;
|
|
112
|
+
sellCurrency: string;
|
|
113
|
+
unitSellAmountCents?: number | null;
|
|
114
|
+
totalSellAmountCents?: number | null;
|
|
115
|
+
}>;
|
|
79
116
|
}
|
|
80
117
|
export interface BookingCreateBridgeResult {
|
|
81
118
|
status: "ok" | "product_not_found" | string;
|
|
@@ -102,6 +139,12 @@ export interface BuildOwnedProductDraftShapeOptions {
|
|
|
102
139
|
* `showsAddons` is false.
|
|
103
140
|
*/
|
|
104
141
|
addonCatalog?: ReadonlyArray<AddonOffer>;
|
|
142
|
+
/**
|
|
143
|
+
* Product options / variants. These select `draft.configure.variantId`
|
|
144
|
+
* and are distinct from extras: one option changes the underlying
|
|
145
|
+
* booking configuration, while extras add optional line items.
|
|
146
|
+
*/
|
|
147
|
+
productOptions?: ReadonlyArray<ProductVariantOption>;
|
|
105
148
|
}
|
|
106
149
|
export declare function buildOwnedProductDraftShape(options?: BuildOwnedProductDraftShapeOptions): BookingDraftShape;
|
|
107
150
|
/** A per-unit price within a resolved option price rule, returned by
|
|
@@ -152,6 +195,12 @@ export interface OwnedProductsShapeLoaders {
|
|
|
152
195
|
* the products package free of an @voyantjs/extras dependency.
|
|
153
196
|
*/
|
|
154
197
|
loadAddonCatalog?: (ctx: OwnedHandlerContext, productId: string) => Promise<ReadonlyArray<AddonOffer>>;
|
|
198
|
+
/**
|
|
199
|
+
* Resolve product options / variants from the owning products module.
|
|
200
|
+
* Optional for tests and deployments that do not expose option
|
|
201
|
+
* variants in the booking flow.
|
|
202
|
+
*/
|
|
203
|
+
loadProductOptions?: (ctx: OwnedHandlerContext, productId: string) => Promise<ReadonlyArray<ProductVariantOption>>;
|
|
155
204
|
/**
|
|
156
205
|
* Resolve the tax rate for a given (product, buyer country) pair.
|
|
157
206
|
* Templates wire this to a function that reads
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/booking-engine/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EACL,KAAK,UAAU,EACf,KAAK,iBAAiB,EAStB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/booking-engine/handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -31,6 +31,7 @@ export function buildOwnedProductDraftShape(options = {}) {
|
|
|
31
31
|
const paxBands = DEFAULT_PAX_BANDS;
|
|
32
32
|
const fields = options.travelerFields ?? defaultTravelerFields();
|
|
33
33
|
const addons = options.addonCatalog ?? [];
|
|
34
|
+
const variants = options.productOptions ?? [];
|
|
34
35
|
const flags = defaultDraftShapeFlags();
|
|
35
36
|
return {
|
|
36
37
|
...flags,
|
|
@@ -40,7 +41,10 @@ export function buildOwnedProductDraftShape(options = {}) {
|
|
|
40
41
|
travelerFields: fields,
|
|
41
42
|
bookingFields: defaultBookingFields(),
|
|
42
43
|
paymentIntents: ["hold", "card"],
|
|
43
|
-
configureSubSteps: [
|
|
44
|
+
configureSubSteps: [
|
|
45
|
+
...(variants.length > 0 ? [{ kind: "product-option", options: variants }] : []),
|
|
46
|
+
{ kind: "occupancy", bands: paxBands },
|
|
47
|
+
],
|
|
44
48
|
addons: addons.length > 0 ? { catalog: addons } : undefined,
|
|
45
49
|
};
|
|
46
50
|
}
|
|
@@ -61,13 +65,15 @@ export function createProductsBookingHandler(options) {
|
|
|
61
65
|
}
|
|
62
66
|
const draft = (request.draft ?? {});
|
|
63
67
|
const optionId = draft.configure?.variantId;
|
|
68
|
+
const optionSelections = normalizeOptionSelections(draft.configure?.optionSelections);
|
|
64
69
|
const slotId = draft.configure?.departureSlotId;
|
|
65
70
|
// Concurrent enrichment + slot-date lookup. The slot date is
|
|
66
71
|
// needed before we can call loadResolvedOptionPrice, so it
|
|
67
72
|
// joins this batch.
|
|
68
|
-
const [travelerFields, addonCatalog, taxRate, slotDate] = await Promise.all([
|
|
73
|
+
const [travelerFields, addonCatalog, productOptionCatalog, taxRate, slotDate] = await Promise.all([
|
|
69
74
|
options.loadTravelerFields?.(ctx, request.entityId) ?? Promise.resolve(undefined),
|
|
70
75
|
options.loadAddonCatalog?.(ctx, request.entityId) ?? Promise.resolve(undefined),
|
|
76
|
+
options.loadProductOptions?.(ctx, request.entityId) ?? Promise.resolve(undefined),
|
|
71
77
|
options.loadTaxRate?.(ctx, {
|
|
72
78
|
productId: request.entityId,
|
|
73
79
|
buyerCountry: draft.billing?.address?.country,
|
|
@@ -77,7 +83,7 @@ export function createProductsBookingHandler(options) {
|
|
|
77
83
|
? options.loadSlotDate(ctx, slotId)
|
|
78
84
|
: Promise.resolve(draft.configure?.departureDate ?? null),
|
|
79
85
|
]);
|
|
80
|
-
const resolvedPrice = optionId && slotDate && options.loadResolvedOptionPrice
|
|
86
|
+
const resolvedPrice = optionSelections.length === 0 && optionId && slotDate && options.loadResolvedOptionPrice
|
|
81
87
|
? await options.loadResolvedOptionPrice(ctx, {
|
|
82
88
|
productId: request.entityId,
|
|
83
89
|
optionId,
|
|
@@ -89,10 +95,25 @@ export function createProductsBookingHandler(options) {
|
|
|
89
95
|
// single-occupant baseline so the wizard can render a starter
|
|
90
96
|
// total before the user picks counts.
|
|
91
97
|
const effectivePax = paxCount > 0 ? paxCount : 1;
|
|
92
|
-
const priced =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
const priced = optionSelections.length > 0
|
|
99
|
+
? await priceOptionSelections({
|
|
100
|
+
ctx,
|
|
101
|
+
options,
|
|
102
|
+
product,
|
|
103
|
+
productOptions: productOptionCatalog ?? [],
|
|
104
|
+
selections: optionSelections,
|
|
105
|
+
slotDate,
|
|
106
|
+
})
|
|
107
|
+
: priceQuote({
|
|
108
|
+
product,
|
|
109
|
+
resolvedPrice,
|
|
110
|
+
pax: draft.configure?.pax,
|
|
111
|
+
effectivePax,
|
|
112
|
+
});
|
|
113
|
+
const pricedWithAddons = applyAddonSelections({
|
|
114
|
+
priced,
|
|
115
|
+
addons: draft.addons,
|
|
116
|
+
addonCatalog: addonCatalog ?? [],
|
|
96
117
|
effectivePax,
|
|
97
118
|
});
|
|
98
119
|
// Tax computation. The base is taxable; addons/accommodation
|
|
@@ -100,7 +121,7 @@ export function createProductsBookingHandler(options) {
|
|
|
100
121
|
// `applies_to` axis on tax_classes.lines) lands in a follow-up
|
|
101
122
|
// when the catalog actually carries mixed treatments.
|
|
102
123
|
const taxIsInclusive = taxRate?.priceMode === "inclusive";
|
|
103
|
-
const grossCents =
|
|
124
|
+
const grossCents = pricedWithAddons.totalCents;
|
|
104
125
|
const taxCents = taxRate && taxRate.rate > 0
|
|
105
126
|
? taxIsInclusive
|
|
106
127
|
? Math.round(grossCents - grossCents / (1 + taxRate.rate))
|
|
@@ -117,7 +138,7 @@ export function createProductsBookingHandler(options) {
|
|
|
117
138
|
surcharges: 0,
|
|
118
139
|
currency: product.sellCurrency,
|
|
119
140
|
breakdown: {
|
|
120
|
-
lines:
|
|
141
|
+
lines: pricedWithAddons.lines.map((line) => ({
|
|
121
142
|
...line,
|
|
122
143
|
taxIncluded: taxIsInclusive,
|
|
123
144
|
})),
|
|
@@ -148,6 +169,7 @@ export function createProductsBookingHandler(options) {
|
|
|
148
169
|
shape: buildOwnedProductDraftShape({
|
|
149
170
|
travelerFields,
|
|
150
171
|
addonCatalog,
|
|
172
|
+
productOptions: productOptionCatalog,
|
|
151
173
|
}),
|
|
152
174
|
};
|
|
153
175
|
},
|
|
@@ -223,11 +245,14 @@ export function createProductsBookingHandler(options) {
|
|
|
223
245
|
upstreamPayload: { reason: "product_not_found" },
|
|
224
246
|
};
|
|
225
247
|
}
|
|
226
|
-
const
|
|
248
|
+
const partyBilling = extractBillingParty(request.party);
|
|
249
|
+
const partyTravelers = extractPartyTravelers(request.party);
|
|
250
|
+
const travelers = (draft.travelers ?? []).map((t, index) => ({
|
|
227
251
|
firstName: t.firstName,
|
|
228
252
|
lastName: t.lastName,
|
|
229
253
|
email: t.email,
|
|
230
254
|
phone: t.phone,
|
|
255
|
+
personId: partyTravelers[index]?.personId ?? null,
|
|
231
256
|
participantType: "traveler",
|
|
232
257
|
travelerCategory: t.band === "child" || t.band === "infant"
|
|
233
258
|
? t.band
|
|
@@ -240,15 +265,44 @@ export function createProductsBookingHandler(options) {
|
|
|
240
265
|
// subtotal during tax recompute, so derive the override from the
|
|
241
266
|
// gross breakdown total when an included tax line is present.
|
|
242
267
|
const sellAmountCentsOverride = resolveSellAmountCentsOverride(request.pricing);
|
|
268
|
+
const optionSelections = normalizeOptionSelections(draft.configure?.optionSelections);
|
|
269
|
+
const selectedOptionIds = [
|
|
270
|
+
...new Set(optionSelections.map((selection) => selection.optionId)),
|
|
271
|
+
];
|
|
272
|
+
const primaryOptionId = selectedOptionIds.length === 1
|
|
273
|
+
? selectedOptionIds[0]
|
|
274
|
+
: optionSelections.length === 0
|
|
275
|
+
? (draft.configure?.variantId ?? null)
|
|
276
|
+
: null;
|
|
243
277
|
const bridge = await options.createBooking({
|
|
244
278
|
productId: product.id,
|
|
279
|
+
optionId: primaryOptionId,
|
|
245
280
|
bookingNumber: generateNumber(),
|
|
246
|
-
personId:
|
|
247
|
-
organizationId:
|
|
281
|
+
personId: partyBilling.personId,
|
|
282
|
+
organizationId: partyBilling.organizationId,
|
|
283
|
+
contactFirstName: partyBilling.contactFirstName,
|
|
284
|
+
contactLastName: partyBilling.contactLastName,
|
|
285
|
+
contactEmail: partyBilling.contactEmail,
|
|
286
|
+
contactPhone: partyBilling.contactPhone,
|
|
248
287
|
internalNotes: extractInternalNotes(request.party),
|
|
249
288
|
travelers: travelers.length > 0 ? travelers : undefined,
|
|
289
|
+
paymentSchedules: draft.paymentSchedules,
|
|
290
|
+
documentGeneration: draft.documentGeneration
|
|
291
|
+
? {
|
|
292
|
+
contractDocument: draft.documentGeneration.contractDocument === true,
|
|
293
|
+
invoiceDocument: draft.documentGeneration.invoiceDocument === true,
|
|
294
|
+
}
|
|
295
|
+
: undefined,
|
|
250
296
|
sellAmountCentsOverride,
|
|
251
297
|
taxLines: extractTaxLines(request.pricing),
|
|
298
|
+
itemLines: bookingItemLinesFromOptionSelections(optionSelections),
|
|
299
|
+
extraLines: bookingExtraLinesFromAddonSelections({
|
|
300
|
+
addons: draft.addons,
|
|
301
|
+
addonCatalog: await options.loadAddonCatalog?.(ctx, product.id),
|
|
302
|
+
currency: product.sellCurrency,
|
|
303
|
+
quantityMultiplier: Math.max(1, travelers.length || 1),
|
|
304
|
+
}),
|
|
305
|
+
initialStatus: readInitialStatus(request.parameters),
|
|
252
306
|
});
|
|
253
307
|
if (bridge.status !== "ok" || !bridge.bookingId) {
|
|
254
308
|
return {
|
|
@@ -288,6 +342,135 @@ function sumPax(pax) {
|
|
|
288
342
|
}
|
|
289
343
|
return total;
|
|
290
344
|
}
|
|
345
|
+
function normalizeOptionSelections(selections) {
|
|
346
|
+
if (!Array.isArray(selections))
|
|
347
|
+
return [];
|
|
348
|
+
return selections.flatMap((selection) => {
|
|
349
|
+
if (!selection ||
|
|
350
|
+
typeof selection !== "object" ||
|
|
351
|
+
typeof selection.optionId !== "string" ||
|
|
352
|
+
selection.optionId.length === 0) {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
const quantity = typeof selection.quantity === "number" && Number.isFinite(selection.quantity)
|
|
356
|
+
? Math.floor(selection.quantity)
|
|
357
|
+
: 0;
|
|
358
|
+
if (quantity <= 0)
|
|
359
|
+
return [];
|
|
360
|
+
return [
|
|
361
|
+
{
|
|
362
|
+
optionId: selection.optionId,
|
|
363
|
+
...(typeof selection.optionUnitId === "string" && selection.optionUnitId.length > 0
|
|
364
|
+
? { optionUnitId: selection.optionUnitId }
|
|
365
|
+
: {}),
|
|
366
|
+
quantity,
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async function priceOptionSelections(input) {
|
|
372
|
+
const lines = [];
|
|
373
|
+
let totalCents = 0;
|
|
374
|
+
const optionsById = new Map(input.productOptions.map((option) => [option.id, option]));
|
|
375
|
+
for (const selection of input.selections) {
|
|
376
|
+
const resolvedPrice = input.slotDate && input.options.loadResolvedOptionPrice
|
|
377
|
+
? await input.options.loadResolvedOptionPrice(input.ctx, {
|
|
378
|
+
productId: input.product.id,
|
|
379
|
+
optionId: selection.optionId,
|
|
380
|
+
date: input.slotDate,
|
|
381
|
+
})
|
|
382
|
+
: null;
|
|
383
|
+
const unitPrice = selection.optionUnitId && resolvedPrice?.unitPrices
|
|
384
|
+
? resolvedPrice.unitPrices.find((unit) => unit.unitId === selection.optionUnitId)
|
|
385
|
+
?.sellAmountCents
|
|
386
|
+
: null;
|
|
387
|
+
const unitAmount = unitPrice ?? resolvedPrice?.baseSellAmountCents ?? input.product.sellAmountCents ?? 0;
|
|
388
|
+
if (unitAmount <= 0)
|
|
389
|
+
continue;
|
|
390
|
+
const totalAmount = unitAmount * selection.quantity;
|
|
391
|
+
totalCents += totalAmount;
|
|
392
|
+
lines.push({
|
|
393
|
+
kind: "base",
|
|
394
|
+
label: optionsById.get(selection.optionId)?.name ?? input.product.name,
|
|
395
|
+
quantity: selection.quantity,
|
|
396
|
+
unitAmount,
|
|
397
|
+
totalAmount,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return { totalCents, lines };
|
|
401
|
+
}
|
|
402
|
+
function bookingItemLinesFromOptionSelections(selections) {
|
|
403
|
+
const lines = selections.flatMap((selection) => selection.optionUnitId
|
|
404
|
+
? [
|
|
405
|
+
{
|
|
406
|
+
optionId: selection.optionId,
|
|
407
|
+
optionUnitId: selection.optionUnitId,
|
|
408
|
+
quantity: selection.quantity,
|
|
409
|
+
},
|
|
410
|
+
]
|
|
411
|
+
: []);
|
|
412
|
+
return lines.length > 0 ? lines : undefined;
|
|
413
|
+
}
|
|
414
|
+
function applyAddonSelections(input) {
|
|
415
|
+
const extraLines = bookingExtraLinesFromAddonSelections({
|
|
416
|
+
addons: input.addons,
|
|
417
|
+
addonCatalog: input.addonCatalog,
|
|
418
|
+
currency: "EUR",
|
|
419
|
+
});
|
|
420
|
+
if (!extraLines?.length)
|
|
421
|
+
return input.priced;
|
|
422
|
+
const lines = [...input.priced.lines];
|
|
423
|
+
let totalCents = input.priced.totalCents;
|
|
424
|
+
for (const extra of extraLines) {
|
|
425
|
+
const unitAmount = extra.unitSellAmountCents ?? 0;
|
|
426
|
+
const quantity = extra.pricingMode === "per_person" || extra.pricedPerPerson
|
|
427
|
+
? Math.max(1, input.effectivePax) * extra.quantity
|
|
428
|
+
: extra.quantity;
|
|
429
|
+
const totalAmount = unitAmount * quantity;
|
|
430
|
+
if (totalAmount <= 0)
|
|
431
|
+
continue;
|
|
432
|
+
totalCents += totalAmount;
|
|
433
|
+
lines.push({
|
|
434
|
+
kind: "addon",
|
|
435
|
+
label: extra.name,
|
|
436
|
+
quantity,
|
|
437
|
+
unitAmount,
|
|
438
|
+
totalAmount,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return { totalCents, lines };
|
|
442
|
+
}
|
|
443
|
+
function bookingExtraLinesFromAddonSelections(input) {
|
|
444
|
+
if (!Array.isArray(input.addons) || input.addons.length === 0)
|
|
445
|
+
return undefined;
|
|
446
|
+
const catalogById = new Map((input.addonCatalog ?? []).map((offer) => [offer.id, offer]));
|
|
447
|
+
const lines = input.addons.flatMap((selection) => {
|
|
448
|
+
const offer = catalogById.get(selection.extraId);
|
|
449
|
+
const quantity = typeof selection.quantity === "number" && Number.isFinite(selection.quantity)
|
|
450
|
+
? Math.floor(selection.quantity)
|
|
451
|
+
: 0;
|
|
452
|
+
if (!offer || quantity <= 0)
|
|
453
|
+
return [];
|
|
454
|
+
const unitSellAmountCents = offer.unitAmountCents ?? null;
|
|
455
|
+
const chargedQuantity = offer.pricingMode === "per_person" || offer.pricedPerPerson
|
|
456
|
+
? quantity * Math.max(1, input.quantityMultiplier ?? 1)
|
|
457
|
+
: quantity;
|
|
458
|
+
return [
|
|
459
|
+
{
|
|
460
|
+
productExtraId: offer.id,
|
|
461
|
+
name: offer.name,
|
|
462
|
+
description: offer.description ?? null,
|
|
463
|
+
pricingMode: offer.pricingMode ?? null,
|
|
464
|
+
pricedPerPerson: offer.pricedPerPerson ?? null,
|
|
465
|
+
quantity,
|
|
466
|
+
sellCurrency: offer.currency ?? input.currency,
|
|
467
|
+
unitSellAmountCents,
|
|
468
|
+
totalSellAmountCents: unitSellAmountCents == null ? null : unitSellAmountCents * chargedQuantity,
|
|
469
|
+
},
|
|
470
|
+
];
|
|
471
|
+
});
|
|
472
|
+
return lines.length > 0 ? lines : undefined;
|
|
473
|
+
}
|
|
291
474
|
/**
|
|
292
475
|
* Three-way price computation:
|
|
293
476
|
*
|
|
@@ -362,17 +545,21 @@ function priceQuote(input) {
|
|
|
362
545
|
],
|
|
363
546
|
};
|
|
364
547
|
}
|
|
365
|
-
function
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
548
|
+
function readInitialStatus(parameters) {
|
|
549
|
+
const allowed = [
|
|
550
|
+
"draft",
|
|
551
|
+
"on_hold",
|
|
552
|
+
"awaiting_payment",
|
|
553
|
+
"confirmed",
|
|
554
|
+
"in_progress",
|
|
555
|
+
"completed",
|
|
556
|
+
"cancelled",
|
|
557
|
+
"expired",
|
|
558
|
+
];
|
|
559
|
+
const raw = parameters?.initialStatus;
|
|
560
|
+
return typeof raw === "string" && allowed.includes(raw)
|
|
561
|
+
? raw
|
|
562
|
+
: undefined;
|
|
376
563
|
}
|
|
377
564
|
function extractInternalNotes(party) {
|
|
378
565
|
if (!party)
|
|
@@ -380,6 +567,36 @@ function extractInternalNotes(party) {
|
|
|
380
567
|
const v = party.internalNotes;
|
|
381
568
|
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
382
569
|
}
|
|
570
|
+
function extractBillingParty(party) {
|
|
571
|
+
const directBilling = asRecord(party?.billing);
|
|
572
|
+
const travelerParty = asRecord(party?.travelerParty);
|
|
573
|
+
const envelopeBilling = asRecord(travelerParty?.billing);
|
|
574
|
+
const billing = envelopeBilling ?? directBilling;
|
|
575
|
+
const contact = asRecord(billing?.contact);
|
|
576
|
+
return {
|
|
577
|
+
personId: stringValue(party?.personId) ?? stringValue(billing?.personId),
|
|
578
|
+
organizationId: stringValue(party?.organizationId) ?? stringValue(billing?.organizationId),
|
|
579
|
+
contactFirstName: stringValue(contact?.firstName),
|
|
580
|
+
contactLastName: stringValue(contact?.lastName),
|
|
581
|
+
contactEmail: stringValue(contact?.email),
|
|
582
|
+
contactPhone: stringValue(contact?.phone),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function extractPartyTravelers(party) {
|
|
586
|
+
const travelerParty = asRecord(party?.travelerParty);
|
|
587
|
+
const travelers = Array.isArray(travelerParty?.travelers) ? travelerParty.travelers : [];
|
|
588
|
+
return travelers.map((traveler) => ({
|
|
589
|
+
personId: stringValue(asRecord(traveler)?.personId),
|
|
590
|
+
}));
|
|
591
|
+
}
|
|
592
|
+
function asRecord(value) {
|
|
593
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
594
|
+
? value
|
|
595
|
+
: null;
|
|
596
|
+
}
|
|
597
|
+
function stringValue(value) {
|
|
598
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
599
|
+
}
|
|
383
600
|
function extractTaxLines(pricing) {
|
|
384
601
|
const breakdown = pricing?.breakdown;
|
|
385
602
|
if (!breakdown || typeof breakdown !== "object" || Array.isArray(breakdown))
|
package/dist/draft-shape.d.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* scheduled departures.
|
|
10
10
|
* - Travelers: per-pax fields (first / last / email; passport when
|
|
11
11
|
* the supplier requires it — surfaced via overlay when known).
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
12
|
+
* - Product options: the product's own `options[]` projected as a
|
|
13
|
+
* configure sub-step, setting `draft.configure.variantId`.
|
|
14
14
|
*
|
|
15
15
|
* No accommodation sub-step today (multi-day tours w/ rooms route
|
|
16
16
|
* through accommodations, not products). Pricing flows through
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"draft-shape.d.ts","sourceRoot":"","sources":["../src/draft-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"draft-shape.d.ts","sourceRoot":"","sources":["../src/draft-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EACL,KAAK,iBAAiB,EAKtB,KAAK,WAAW,EAEjB,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,MAAM,WAAW,6BAA6B;IAC5C,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IACrC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CACpD;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,6BAAkC,GAC1C,iBAAiB,CAwBnB"}
|
package/dist/draft-shape.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* scheduled departures.
|
|
10
10
|
* - Travelers: per-pax fields (first / last / email; passport when
|
|
11
11
|
* the supplier requires it — surfaced via overlay when known).
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
12
|
+
* - Product options: the product's own `options[]` projected as a
|
|
13
|
+
* configure sub-step, setting `draft.configure.variantId`.
|
|
14
14
|
*
|
|
15
15
|
* No accommodation sub-step today (multi-day tours w/ rooms route
|
|
16
16
|
* through accommodations, not products). Pricing flows through
|
|
@@ -22,25 +22,23 @@ import { DEFAULT_PAX_BANDS, defaultBookingFields, defaultDraftShapeFlags, defaul
|
|
|
22
22
|
export function buildProductDraftShape(content, options = {}) {
|
|
23
23
|
const paxBands = options.paxBands ?? DEFAULT_PAX_BANDS;
|
|
24
24
|
const total = options.paxBandsAllowedTotal ?? paxBandsAllowedTotalFrom(paxBands);
|
|
25
|
-
|
|
26
|
-
// option becomes an extras-type add-on; verticals with grouped
|
|
27
|
-
// catalogs (cruise excursions) override.
|
|
28
|
-
const addonItems = content.options.map((opt) => ({
|
|
25
|
+
const productOptions = content.options.map((opt) => ({
|
|
29
26
|
id: opt.id,
|
|
30
27
|
name: opt.name,
|
|
31
28
|
description: opt.description ?? null,
|
|
32
|
-
kind: "extras",
|
|
33
|
-
pricingMode: null,
|
|
34
29
|
}));
|
|
35
30
|
return {
|
|
36
31
|
...defaultDraftShapeFlags(),
|
|
37
|
-
showsAddons: addonItems.length > 0,
|
|
38
32
|
paxBands,
|
|
39
33
|
paxBandsAllowedTotal: total,
|
|
40
34
|
travelerFields: defaultTravelerFields(),
|
|
41
35
|
bookingFields: defaultBookingFields(),
|
|
42
|
-
addons: addonItems.length > 0 ? { catalog: addonItems } : undefined,
|
|
43
36
|
paymentIntents: ["hold", "card"],
|
|
44
|
-
configureSubSteps: [
|
|
37
|
+
configureSubSteps: [
|
|
38
|
+
...(productOptions.length > 0
|
|
39
|
+
? [{ kind: "product-option", options: productOptions }]
|
|
40
|
+
: []),
|
|
41
|
+
{ kind: "occupancy", bands: paxBands },
|
|
42
|
+
],
|
|
45
43
|
};
|
|
46
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/products",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -132,13 +132,13 @@
|
|
|
132
132
|
"pdf-lib": "^1.17.1",
|
|
133
133
|
"sanitize-html": "^2.17.4",
|
|
134
134
|
"zod": "^4.3.6",
|
|
135
|
-
"@voyantjs/action-ledger": "0.55.
|
|
136
|
-
"@voyantjs/core": "0.55.
|
|
137
|
-
"@voyantjs/db": "0.55.
|
|
138
|
-
"@voyantjs/hono": "0.55.
|
|
139
|
-
"@voyantjs/utils": "0.55.
|
|
140
|
-
"@voyantjs/catalog": "0.55.
|
|
141
|
-
"@voyantjs/storage": "0.55.
|
|
135
|
+
"@voyantjs/action-ledger": "0.55.1",
|
|
136
|
+
"@voyantjs/core": "0.55.1",
|
|
137
|
+
"@voyantjs/db": "0.55.1",
|
|
138
|
+
"@voyantjs/hono": "0.55.1",
|
|
139
|
+
"@voyantjs/utils": "0.55.1",
|
|
140
|
+
"@voyantjs/catalog": "0.55.1",
|
|
141
|
+
"@voyantjs/storage": "0.55.1"
|
|
142
142
|
},
|
|
143
143
|
"devDependencies": {
|
|
144
144
|
"@types/sanitize-html": "^2.16.1",
|