@voyant-travel/inventory 0.1.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/LICENSE +201 -0
- package/dist/action-ledger-drift.d.ts +29 -0
- package/dist/action-ledger-drift.d.ts.map +1 -0
- package/dist/action-ledger-drift.js +338 -0
- package/dist/action-ledger.d.ts +104 -0
- package/dist/action-ledger.d.ts.map +1 -0
- package/dist/action-ledger.js +100 -0
- package/dist/authoring/builder.d.ts +37 -0
- package/dist/authoring/builder.d.ts.map +1 -0
- package/dist/authoring/builder.js +248 -0
- package/dist/authoring/clone-content.d.ts +38 -0
- package/dist/authoring/clone-content.d.ts.map +1 -0
- package/dist/authoring/clone-content.js +367 -0
- package/dist/authoring/clone-pricing.d.ts +9 -0
- package/dist/authoring/clone-pricing.d.ts.map +1 -0
- package/dist/authoring/clone-pricing.js +242 -0
- package/dist/authoring/clone.d.ts +45 -0
- package/dist/authoring/clone.d.ts.map +1 -0
- package/dist/authoring/clone.js +142 -0
- package/dist/authoring/errors.d.ts +21 -0
- package/dist/authoring/errors.d.ts.map +1 -0
- package/dist/authoring/errors.js +13 -0
- package/dist/authoring/extension.d.ts +248 -0
- package/dist/authoring/extension.d.ts.map +1 -0
- package/dist/authoring/extension.js +116 -0
- package/dist/authoring/index.d.ts +12 -0
- package/dist/authoring/index.d.ts.map +1 -0
- package/dist/authoring/index.js +11 -0
- package/dist/authoring/schema.d.ts +85 -0
- package/dist/authoring/schema.d.ts.map +1 -0
- package/dist/authoring/schema.js +16 -0
- package/dist/authoring/service.d.ts +28 -0
- package/dist/authoring/service.d.ts.map +1 -0
- package/dist/authoring/service.js +66 -0
- package/dist/authoring/spec.d.ts +524 -0
- package/dist/authoring/spec.d.ts.map +1 -0
- package/dist/authoring/spec.js +167 -0
- package/dist/authoring/validate.d.ts +17 -0
- package/dist/authoring/validate.d.ts.map +1 -0
- package/dist/authoring/validate.js +83 -0
- package/dist/authoring.d.ts +2 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +1 -0
- package/dist/booking-engine/handler-support.d.ts +91 -0
- package/dist/booking-engine/handler-support.d.ts.map +1 -0
- package/dist/booking-engine/handler-support.js +355 -0
- package/dist/booking-engine/handler.d.ts +404 -0
- package/dist/booking-engine/handler.d.ts.map +1 -0
- package/dist/booking-engine/handler.js +398 -0
- package/dist/booking-engine/index.d.ts +8 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +7 -0
- package/dist/booking-engine.d.ts +2 -0
- package/dist/booking-engine.d.ts.map +1 -0
- package/dist/booking-engine.js +1 -0
- package/dist/booking-extension.d.ts +278 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +161 -0
- package/dist/catalog-policy-departures.d.ts +52 -0
- package/dist/catalog-policy-departures.d.ts.map +1 -0
- package/dist/catalog-policy-departures.js +169 -0
- package/dist/catalog-policy-destinations.d.ts +43 -0
- package/dist/catalog-policy-destinations.d.ts.map +1 -0
- package/dist/catalog-policy-destinations.js +165 -0
- package/dist/catalog-policy-pricing.d.ts +55 -0
- package/dist/catalog-policy-pricing.d.ts.map +1 -0
- package/dist/catalog-policy-pricing.js +109 -0
- package/dist/catalog-policy-promotions.d.ts +52 -0
- package/dist/catalog-policy-promotions.d.ts.map +1 -0
- package/dist/catalog-policy-promotions.js +270 -0
- package/dist/catalog-policy-taxonomy.d.ts +51 -0
- package/dist/catalog-policy-taxonomy.d.ts.map +1 -0
- package/dist/catalog-policy-taxonomy.js +191 -0
- package/dist/catalog-policy.d.ts +33 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +733 -0
- package/dist/content-shape.d.ts +15 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +28 -0
- package/dist/draft-shape.d.ts +43 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +48 -0
- package/dist/events.d.ts +37 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +32 -0
- package/dist/extras/catalog-policy.d.ts +30 -0
- package/dist/extras/catalog-policy.d.ts.map +1 -0
- package/dist/extras/catalog-policy.js +319 -0
- package/dist/extras/content-shape.d.ts +5 -0
- package/dist/extras/content-shape.d.ts.map +1 -0
- package/dist/extras/content-shape.js +13 -0
- package/dist/extras/draft-shape.d.ts +34 -0
- package/dist/extras/draft-shape.d.ts.map +1 -0
- package/dist/extras/draft-shape.js +69 -0
- package/dist/extras/routes.d.ts +380 -0
- package/dist/extras/routes.d.ts.map +1 -0
- package/dist/extras/routes.js +59 -0
- package/dist/extras/schema-sourced-content.d.ts +254 -0
- package/dist/extras/schema-sourced-content.d.ts.map +1 -0
- package/dist/extras/schema-sourced-content.js +45 -0
- package/dist/extras/schema.d.ts +628 -0
- package/dist/extras/schema.d.ts.map +1 -0
- package/dist/extras/schema.js +87 -0
- package/dist/extras/service-catalog-plane.d.ts +77 -0
- package/dist/extras/service-catalog-plane.d.ts.map +1 -0
- package/dist/extras/service-catalog-plane.js +219 -0
- package/dist/extras/service-content-synthesizer.d.ts +41 -0
- package/dist/extras/service-content-synthesizer.d.ts.map +1 -0
- package/dist/extras/service-content-synthesizer.js +138 -0
- package/dist/extras/service-content.d.ts +48 -0
- package/dist/extras/service-content.d.ts.map +1 -0
- package/dist/extras/service-content.js +253 -0
- package/dist/extras/service.d.ts +185 -0
- package/dist/extras/service.d.ts.map +1 -0
- package/dist/extras/service.js +96 -0
- package/dist/extras/validation.d.ts +437 -0
- package/dist/extras/validation.d.ts.map +1 -0
- package/dist/extras/validation.js +149 -0
- package/dist/extras.d.ts +267 -0
- package/dist/extras.d.ts.map +1 -0
- package/dist/extras.js +19 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/interface.d.ts +5869 -0
- package/dist/interface.d.ts.map +1 -0
- package/dist/interface.js +54 -0
- package/dist/public-routes.d.ts +2 -0
- package/dist/public-routes.d.ts.map +1 -0
- package/dist/public-routes.js +1 -0
- package/dist/public-validation.d.ts +2 -0
- package/dist/public-validation.d.ts.map +1 -0
- package/dist/public-validation.js +1 -0
- package/dist/read-model.d.ts +25 -0
- package/dist/read-model.d.ts.map +1 -0
- package/dist/read-model.js +99 -0
- package/dist/route-env.d.ts +22 -0
- package/dist/route-env.d.ts.map +1 -0
- package/dist/route-env.js +1 -0
- package/dist/routes-associations.d.ts +164 -0
- package/dist/routes-associations.d.ts.map +1 -0
- package/dist/routes-associations.js +100 -0
- package/dist/routes-catalog.d.ts +436 -0
- package/dist/routes-catalog.d.ts.map +1 -0
- package/dist/routes-catalog.js +104 -0
- package/dist/routes-configuration.d.ts +773 -0
- package/dist/routes-configuration.d.ts.map +1 -0
- package/dist/routes-configuration.js +364 -0
- package/dist/routes-content.d.ts +74 -0
- package/dist/routes-content.d.ts.map +1 -0
- package/dist/routes-content.js +117 -0
- package/dist/routes-core.d.ts +331 -0
- package/dist/routes-core.d.ts.map +1 -0
- package/dist/routes-core.js +95 -0
- package/dist/routes-itinerary.d.ts +759 -0
- package/dist/routes-itinerary.d.ts.map +1 -0
- package/dist/routes-itinerary.js +387 -0
- package/dist/routes-maintenance.d.ts +32 -0
- package/dist/routes-maintenance.d.ts.map +1 -0
- package/dist/routes-maintenance.js +14 -0
- package/dist/routes-media.d.ts +634 -0
- package/dist/routes-media.d.ts.map +1 -0
- package/dist/routes-media.js +245 -0
- package/dist/routes-merchandising.d.ts +1120 -0
- package/dist/routes-merchandising.d.ts.map +1 -0
- package/dist/routes-merchandising.js +377 -0
- package/dist/routes-options.d.ts +363 -0
- package/dist/routes-options.d.ts.map +1 -0
- package/dist/routes-options.js +173 -0
- package/dist/routes-public.d.ts +776 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +119 -0
- package/dist/routes-translations.d.ts +489 -0
- package/dist/routes-translations.d.ts.map +1 -0
- package/dist/routes-translations.js +258 -0
- package/dist/routes.d.ts +5097 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +64 -0
- package/dist/schema-core.d.ts +1238 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +157 -0
- package/dist/schema-itinerary.d.ts +1169 -0
- package/dist/schema-itinerary.d.ts.map +1 -0
- package/dist/schema-itinerary.js +130 -0
- package/dist/schema-relations.d.ts +117 -0
- package/dist/schema-relations.d.ts.map +1 -0
- package/dist/schema-relations.js +192 -0
- package/dist/schema-settings.d.ts +1800 -0
- package/dist/schema-settings.d.ts.map +1 -0
- package/dist/schema-settings.js +220 -0
- package/dist/schema-shared.d.ts +15 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +91 -0
- package/dist/schema-sourced-content.d.ts +262 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +69 -0
- package/dist/schema-taxonomy.d.ts +1363 -0
- package/dist/schema-taxonomy.d.ts.map +1 -0
- package/dist/schema-taxonomy.js +203 -0
- package/dist/schema.d.ts +10 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +9 -0
- package/dist/service-aggregates.d.ts +29 -0
- package/dist/service-aggregates.d.ts.map +1 -0
- package/dist/service-aggregates.js +56 -0
- package/dist/service-catalog-plane-destinations.d.ts +30 -0
- package/dist/service-catalog-plane-destinations.d.ts.map +1 -0
- package/dist/service-catalog-plane-destinations.js +143 -0
- package/dist/service-catalog-plane-taxonomy.d.ts +73 -0
- package/dist/service-catalog-plane-taxonomy.d.ts.map +1 -0
- package/dist/service-catalog-plane-taxonomy.js +242 -0
- package/dist/service-catalog-plane.d.ts +179 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +431 -0
- package/dist/service-catalog.d.ts +251 -0
- package/dist/service-catalog.d.ts.map +1 -0
- package/dist/service-catalog.js +517 -0
- package/dist/service-configuration.d.ts +261 -0
- package/dist/service-configuration.d.ts.map +1 -0
- package/dist/service-configuration.js +343 -0
- package/dist/service-content-owned.d.ts +68 -0
- package/dist/service-content-owned.d.ts.map +1 -0
- package/dist/service-content-owned.js +329 -0
- package/dist/service-content-synthesizer.d.ts +90 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +178 -0
- package/dist/service-content.d.ts +106 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +388 -0
- package/dist/service-core.d.ts +194 -0
- package/dist/service-core.d.ts.map +1 -0
- package/dist/service-core.js +213 -0
- package/dist/service-delivery-formats.d.ts +58 -0
- package/dist/service-delivery-formats.d.ts.map +1 -0
- package/dist/service-delivery-formats.js +107 -0
- package/dist/service-destinations.d.ts +223 -0
- package/dist/service-destinations.d.ts.map +1 -0
- package/dist/service-destinations.js +310 -0
- package/dist/service-itinerary-history.d.ts +457 -0
- package/dist/service-itinerary-history.d.ts.map +1 -0
- package/dist/service-itinerary-history.js +135 -0
- package/dist/service-itinerary.d.ts +1149 -0
- package/dist/service-itinerary.d.ts.map +1 -0
- package/dist/service-itinerary.js +419 -0
- package/dist/service-media.d.ts +272 -0
- package/dist/service-media.d.ts.map +1 -0
- package/dist/service-media.js +320 -0
- package/dist/service-merchandising.d.ts +184 -0
- package/dist/service-merchandising.d.ts.map +1 -0
- package/dist/service-merchandising.js +181 -0
- package/dist/service-option-translations.d.ts +268 -0
- package/dist/service-option-translations.d.ts.map +1 -0
- package/dist/service-option-translations.js +300 -0
- package/dist/service-options.d.ts +181 -0
- package/dist/service-options.d.ts.map +1 -0
- package/dist/service-options.js +179 -0
- package/dist/service-product-destinations.d.ts +37 -0
- package/dist/service-product-destinations.d.ts.map +1 -0
- package/dist/service-product-destinations.js +94 -0
- package/dist/service-public.d.ts +664 -0
- package/dist/service-public.d.ts.map +1 -0
- package/dist/service-public.js +374 -0
- package/dist/service-taxonomy.d.ts +197 -0
- package/dist/service-taxonomy.d.ts.map +1 -0
- package/dist/service-taxonomy.js +221 -0
- package/dist/service.d.ts +3929 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +28 -0
- package/dist/tasks/brochure-printers.d.ts +31 -0
- package/dist/tasks/brochure-printers.d.ts.map +1 -0
- package/dist/tasks/brochure-printers.js +149 -0
- package/dist/tasks/brochure-templates.d.ts +36 -0
- package/dist/tasks/brochure-templates.d.ts.map +1 -0
- package/dist/tasks/brochure-templates.js +110 -0
- package/dist/tasks/brochures.d.ts +43 -0
- package/dist/tasks/brochures.d.ts.map +1 -0
- package/dist/tasks/brochures.js +72 -0
- package/dist/tasks/generate-pdf.d.ts +8 -0
- package/dist/tasks/generate-pdf.d.ts.map +1 -0
- package/dist/tasks/generate-pdf.js +106 -0
- package/dist/tasks/index.d.ts +5 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/pdf-text.d.ts +2 -0
- package/dist/tasks/pdf-text.d.ts.map +1 -0
- package/dist/tasks/pdf-text.js +40 -0
- package/dist/tasks.d.ts +2 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +1 -0
- package/dist/validation-catalog.d.ts +2 -0
- package/dist/validation-catalog.d.ts.map +1 -0
- package/dist/validation-catalog.js +3 -0
- package/dist/validation-config.d.ts +2 -0
- package/dist/validation-config.d.ts.map +1 -0
- package/dist/validation-config.js +3 -0
- package/dist/validation-content.d.ts +2 -0
- package/dist/validation-content.d.ts.map +1 -0
- package/dist/validation-content.js +3 -0
- package/dist/validation-core.d.ts +2 -0
- package/dist/validation-core.d.ts.map +1 -0
- package/dist/validation-core.js +3 -0
- package/dist/validation-public.d.ts +2 -0
- package/dist/validation-public.d.ts.map +1 -0
- package/dist/validation-public.js +3 -0
- package/dist/validation-shared.d.ts +2 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +3 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +3 -0
- package/package.json +204 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owned-arm booking handler for the `products` vertical.
|
|
3
|
+
* agent-quality: file-size exception -- Product booking handler keeps quote, commit, cancel, and status behavior together until booking-engine handlers are split by operation.
|
|
4
|
+
*
|
|
5
|
+
* Per `docs/architecture/booking-journey-architecture.md` §6 +
|
|
6
|
+
* §10 Phase A. Composes:
|
|
7
|
+
*
|
|
8
|
+
* - The products vertical's existing pricing primitives
|
|
9
|
+
* (`products.sellAmountCents` / `sellCurrency`) for pricing
|
|
10
|
+
* basis. Per-pax / per-band pricing layered in Phase C+ via
|
|
11
|
+
* `product_pax_pricing_tiers`.
|
|
12
|
+
* - `getProductContent` + `buildProductDraftShape` for the journey
|
|
13
|
+
* wizard's step descriptor.
|
|
14
|
+
* - An injected `createBooking` function for the commit path
|
|
15
|
+
* — keeps the Inventory-owned Product handler from depending on
|
|
16
|
+
* `@voyant-travel/finance` (no workspace cycle).
|
|
17
|
+
*
|
|
18
|
+
* Phase A scope (deliberately narrow):
|
|
19
|
+
* - Price = product.sellAmountCents × pax_count, no taxes / addons /
|
|
20
|
+
* accommodation / vouchers.
|
|
21
|
+
* - Commit goes through the bridge into `bookingsCreate`'s input
|
|
22
|
+
* shape — products-only, no extras / accommodations / cruises / encrypted
|
|
23
|
+
* travel details / snapshot graph.
|
|
24
|
+
*
|
|
25
|
+
* Phase C+ extensions land on this same handler without re-architecting
|
|
26
|
+
* the dispatch.
|
|
27
|
+
*/
|
|
28
|
+
import { DEFAULT_PAX_BANDS, defaultBookingFields, defaultDraftShapeFlags, defaultTravelerFields, paxBandsAllowedTotalFrom, } from "@voyant-travel/catalog/booking-engine";
|
|
29
|
+
import { applyAddonSelections, bookingExtraLinesFromAddonSelections, bookingItemLinesFromOptionSelections, defaultBookingNumber, extractBillingParty, extractInternalNotes, extractPartyTravelers, extractTaxLines, loadProduct, normalizeOptionSelections, priceOptionSelections, priceQuote, readInitialStatus, resolveSellAmountCentsOverride, sumPax, } from "./handler-support.js";
|
|
30
|
+
export function buildOwnedProductDraftShape(options = {}) {
|
|
31
|
+
// Use the product's configured traveler types when supplied; otherwise
|
|
32
|
+
// the generic adult/child/infant defaults.
|
|
33
|
+
const paxBands = options.paxBands && options.paxBands.length > 0 ? options.paxBands : DEFAULT_PAX_BANDS;
|
|
34
|
+
const fields = options.travelerFields ?? defaultTravelerFields();
|
|
35
|
+
const addons = options.addonCatalog ?? [];
|
|
36
|
+
const variants = options.productOptions ?? [];
|
|
37
|
+
const flags = defaultDraftShapeFlags();
|
|
38
|
+
// Room/vehicle-style products sell inventory units (rooms) the operator
|
|
39
|
+
// must pick a quantity of; person-only products price by pax band alone.
|
|
40
|
+
const hasInventoryUnits = variants.some((variant) => variant.units?.some((unit) => unit.unitType === "room" || unit.unitType === "vehicle"));
|
|
41
|
+
return {
|
|
42
|
+
...flags,
|
|
43
|
+
showsAddons: addons.length > 0,
|
|
44
|
+
paxBands,
|
|
45
|
+
paxBandsAllowedTotal: paxBandsAllowedTotalFrom(paxBands),
|
|
46
|
+
...(options.paxBandDependencies && options.paxBandDependencies.length > 0
|
|
47
|
+
? { paxBandDependencies: options.paxBandDependencies }
|
|
48
|
+
: {}),
|
|
49
|
+
travelerFields: fields,
|
|
50
|
+
bookingFields: defaultBookingFields(),
|
|
51
|
+
paymentIntents: ["hold", "card"],
|
|
52
|
+
configureSubSteps: [
|
|
53
|
+
...(variants.length > 0 ? [{ kind: "product-option", options: variants }] : []),
|
|
54
|
+
// Owned products are scheduled — the operator picks a real departure.
|
|
55
|
+
// The journey renders an injected slot picker for this kind, falling
|
|
56
|
+
// back to a free date when the product has no scheduled departures.
|
|
57
|
+
{ kind: "departure", required: true },
|
|
58
|
+
// Inventory products: the operator picks room/unit quantities for the
|
|
59
|
+
// chosen option + departure. The journey renders an injected units
|
|
60
|
+
// picker that writes `configure.optionSelections`.
|
|
61
|
+
...(hasInventoryUnits ? [{ kind: "option-units" }] : []),
|
|
62
|
+
{ kind: "occupancy", bands: paxBands },
|
|
63
|
+
],
|
|
64
|
+
addons: addons.length > 0 ? { catalog: addons } : undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Run an optional enrichment loader so a single failure (a missing
|
|
69
|
+
* migration, a flaky query) never rejects the quote. The journey
|
|
70
|
+
* descriptor — steps, pax bands, options, extras, units — must always
|
|
71
|
+
* render; a broken enrichment source degrades to `undefined`, not a
|
|
72
|
+
* collapsed booking shape. Logs the cause for diagnosis.
|
|
73
|
+
*/
|
|
74
|
+
async function safeLoad(label, promise) {
|
|
75
|
+
if (!promise)
|
|
76
|
+
return undefined;
|
|
77
|
+
try {
|
|
78
|
+
return await promise;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.warn(`[products/booking-engine] ${label} failed; continuing without it`, error);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function createProductsBookingHandler(options) {
|
|
86
|
+
const generateNumber = options.generateBookingNumber ?? defaultBookingNumber;
|
|
87
|
+
return {
|
|
88
|
+
entityModule: "products",
|
|
89
|
+
async computeQuote(ctx, request) {
|
|
90
|
+
const product = await loadProduct(ctx.db, request.entityId);
|
|
91
|
+
if (!product) {
|
|
92
|
+
return { available: false, invalidReason: "product_not_found" };
|
|
93
|
+
}
|
|
94
|
+
if (product.status !== "active" && product.status !== "draft") {
|
|
95
|
+
return {
|
|
96
|
+
available: false,
|
|
97
|
+
invalidReason: `product_status_${product.status}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const draft = (request.draft ?? {});
|
|
101
|
+
const optionId = draft.configure?.variantId;
|
|
102
|
+
const optionSelections = normalizeOptionSelections(draft.configure?.optionSelections);
|
|
103
|
+
const slotId = draft.configure?.departureSlotId;
|
|
104
|
+
// Concurrent enrichment + slot-date lookup. The slot date is
|
|
105
|
+
// needed before we can call loadResolvedOptionPrice, so it
|
|
106
|
+
// joins this batch.
|
|
107
|
+
const [travelerFields, addonCatalog, productOptionCatalog, paxBands, paxBandDependencies, taxRate, slotDate,] = await Promise.all([
|
|
108
|
+
safeLoad("loadTravelerFields", options.loadTravelerFields?.(ctx, request.entityId)),
|
|
109
|
+
safeLoad("loadAddonCatalog", options.loadAddonCatalog?.(ctx, request.entityId)),
|
|
110
|
+
safeLoad("loadProductOptions", options.loadProductOptions?.(ctx, request.entityId)),
|
|
111
|
+
safeLoad("loadPaxBands", options.loadPaxBands?.(ctx, request.entityId)),
|
|
112
|
+
safeLoad("loadPaxBandDependencies", options.loadPaxBandDependencies?.(ctx, request.entityId)),
|
|
113
|
+
safeLoad("loadTaxRate", options.loadTaxRate?.(ctx, {
|
|
114
|
+
productId: request.entityId,
|
|
115
|
+
buyerCountry: draft.billing?.address?.country,
|
|
116
|
+
buyerType: draft.billing?.buyerType,
|
|
117
|
+
})),
|
|
118
|
+
slotId && options.loadSlotDate
|
|
119
|
+
? safeLoad("loadSlotDate", options.loadSlotDate(ctx, slotId)).then((date) => date ?? draft.configure?.departureDate ?? null)
|
|
120
|
+
: Promise.resolve(draft.configure?.departureDate ?? null),
|
|
121
|
+
]);
|
|
122
|
+
// The journey descriptor never depends on pricing — build it
|
|
123
|
+
// unconditionally so the wizard always renders the right steps,
|
|
124
|
+
// bands, options, extras and units. Pricing is best-effort: a
|
|
125
|
+
// failure here returns the shape with no price rather than 500ing
|
|
126
|
+
// the quote (which would collapse the shape to the bare default).
|
|
127
|
+
const shape = buildOwnedProductDraftShape({
|
|
128
|
+
travelerFields,
|
|
129
|
+
addonCatalog,
|
|
130
|
+
productOptions: productOptionCatalog,
|
|
131
|
+
paxBands,
|
|
132
|
+
paxBandDependencies,
|
|
133
|
+
});
|
|
134
|
+
let available = false;
|
|
135
|
+
let pricing;
|
|
136
|
+
try {
|
|
137
|
+
const resolvedPrice = optionSelections.length === 0 && optionId && slotDate && options.loadResolvedOptionPrice
|
|
138
|
+
? await options.loadResolvedOptionPrice(ctx, {
|
|
139
|
+
productId: request.entityId,
|
|
140
|
+
optionId,
|
|
141
|
+
date: slotDate,
|
|
142
|
+
})
|
|
143
|
+
: null;
|
|
144
|
+
const paxCount = sumPax(draft.configure?.pax);
|
|
145
|
+
// Per-pax pricing fallback: when no pax is supplied yet, quote a
|
|
146
|
+
// single-occupant baseline so the wizard can render a starter
|
|
147
|
+
// total before the user picks counts.
|
|
148
|
+
const effectivePax = paxCount > 0 ? paxCount : 1;
|
|
149
|
+
const priced = optionSelections.length > 0
|
|
150
|
+
? await priceOptionSelections({
|
|
151
|
+
ctx,
|
|
152
|
+
options,
|
|
153
|
+
product,
|
|
154
|
+
productOptions: productOptionCatalog ?? [],
|
|
155
|
+
selections: optionSelections,
|
|
156
|
+
slotDate,
|
|
157
|
+
})
|
|
158
|
+
: priceQuote({
|
|
159
|
+
product,
|
|
160
|
+
resolvedPrice,
|
|
161
|
+
pax: draft.configure?.pax,
|
|
162
|
+
effectivePax,
|
|
163
|
+
});
|
|
164
|
+
const pricedWithAddons = applyAddonSelections({
|
|
165
|
+
priced,
|
|
166
|
+
addons: draft.addons,
|
|
167
|
+
addonCatalog: addonCatalog ?? [],
|
|
168
|
+
effectivePax,
|
|
169
|
+
});
|
|
170
|
+
// Tax computation. The base is taxable; addons/accommodation
|
|
171
|
+
// get the same rate in this MVP cut. Per-line override (the
|
|
172
|
+
// `applies_to` axis on tax_classes.lines) lands in a follow-up
|
|
173
|
+
// when the catalog actually carries mixed treatments.
|
|
174
|
+
const taxIsInclusive = taxRate?.priceMode === "inclusive";
|
|
175
|
+
const grossCents = pricedWithAddons.totalCents;
|
|
176
|
+
const taxCents = taxRate && taxRate.rate > 0
|
|
177
|
+
? taxIsInclusive
|
|
178
|
+
? Math.round(grossCents - grossCents / (1 + taxRate.rate))
|
|
179
|
+
: Math.round(grossCents * taxRate.rate)
|
|
180
|
+
: 0;
|
|
181
|
+
const netCents = taxIsInclusive ? grossCents - taxCents : grossCents;
|
|
182
|
+
const payableCents = taxIsInclusive ? grossCents : netCents + taxCents;
|
|
183
|
+
available = grossCents > 0;
|
|
184
|
+
pricing = available
|
|
185
|
+
? {
|
|
186
|
+
base_amount: netCents,
|
|
187
|
+
taxes: taxCents,
|
|
188
|
+
fees: 0,
|
|
189
|
+
surcharges: 0,
|
|
190
|
+
currency: product.sellCurrency,
|
|
191
|
+
breakdown: {
|
|
192
|
+
// `currency` is required for the API serializer to use this
|
|
193
|
+
// itemized breakdown instead of synthesizing a single "Base"
|
|
194
|
+
// line from base_amount.
|
|
195
|
+
currency: product.sellCurrency,
|
|
196
|
+
lines: pricedWithAddons.lines.map((line) => ({
|
|
197
|
+
...line,
|
|
198
|
+
taxIncluded: taxIsInclusive,
|
|
199
|
+
})),
|
|
200
|
+
taxes: taxRate && taxCents > 0
|
|
201
|
+
? [
|
|
202
|
+
{
|
|
203
|
+
code: taxRate.code,
|
|
204
|
+
label: taxRate.label,
|
|
205
|
+
rate: taxRate.rate,
|
|
206
|
+
amount: taxCents,
|
|
207
|
+
base: netCents,
|
|
208
|
+
includedInPrice: taxIsInclusive,
|
|
209
|
+
scope: taxIsInclusive ? "included" : "excluded",
|
|
210
|
+
},
|
|
211
|
+
]
|
|
212
|
+
: [],
|
|
213
|
+
subtotal: netCents,
|
|
214
|
+
taxTotal: taxCents,
|
|
215
|
+
total: payableCents,
|
|
216
|
+
paxCount: effectivePax,
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
: undefined;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.warn("[products/booking-engine] pricing failed; returning shape without a price", error);
|
|
223
|
+
available = false;
|
|
224
|
+
pricing = undefined;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
available,
|
|
228
|
+
invalidReason: available ? undefined : "no_sell_amount_configured",
|
|
229
|
+
pricing,
|
|
230
|
+
shape,
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
/**
|
|
234
|
+
* Place a soft hold on the row's chosen slot. When the
|
|
235
|
+
* `holds` bridge is wired, decrements
|
|
236
|
+
* `availability_slots.remainingPax` against the slot for the
|
|
237
|
+
* pax count; concurrent placeHold attempts are serialized via
|
|
238
|
+
* a row-level lock inside the bridge. When omitted, returns a
|
|
239
|
+
* stamping token without touching inventory.
|
|
240
|
+
*
|
|
241
|
+
* The slot id and pax count are pulled off
|
|
242
|
+
* `request.parameters.slotId` / `request.parameters.paxCount`
|
|
243
|
+
* — the journey wizard threads these from the draft's
|
|
244
|
+
* Configure step (`departureSlotId` + summed `pax`).
|
|
245
|
+
*/
|
|
246
|
+
async placeHold(_ctx, request) {
|
|
247
|
+
const token = request.draftId ?? defaultBookingNumber();
|
|
248
|
+
const expiresAt = new Date(Date.now() + request.ttlMs);
|
|
249
|
+
if (!options.holds) {
|
|
250
|
+
return { holdToken: token, expiresAt };
|
|
251
|
+
}
|
|
252
|
+
const params = (request.parameters ?? {});
|
|
253
|
+
const slotId = params.slotId;
|
|
254
|
+
const paxCount = params.paxCount ?? 1;
|
|
255
|
+
if (!slotId || !request.draftId) {
|
|
256
|
+
// No slot chosen yet → no inventory to lock. Return a
|
|
257
|
+
// stamping token so the journey can still call extend /
|
|
258
|
+
// release.
|
|
259
|
+
return { holdToken: token, expiresAt };
|
|
260
|
+
}
|
|
261
|
+
const result = await options.holds.place({
|
|
262
|
+
draftId: request.draftId,
|
|
263
|
+
productId: params.productId ?? request.entityId,
|
|
264
|
+
slotId,
|
|
265
|
+
paxCount,
|
|
266
|
+
ttlMs: request.ttlMs,
|
|
267
|
+
holdToken: token,
|
|
268
|
+
});
|
|
269
|
+
if (result.status === "ok") {
|
|
270
|
+
return { holdToken: result.holdToken, expiresAt: result.expiresAt };
|
|
271
|
+
}
|
|
272
|
+
// Capacity / lookup failures fall back to a stamping token
|
|
273
|
+
// — the journey commit will revalidate via the engine's
|
|
274
|
+
// re-quote and reject if capacity has dried up.
|
|
275
|
+
return { holdToken: token, expiresAt };
|
|
276
|
+
},
|
|
277
|
+
async extendHold(_ctx, holdToken, request) {
|
|
278
|
+
const ttlMs = request?.ttlMs ?? 30 * 60 * 1000;
|
|
279
|
+
if (options.holds) {
|
|
280
|
+
const result = await options.holds.extend({ holdToken, ttlMs });
|
|
281
|
+
if (result.status === "ok") {
|
|
282
|
+
return { holdToken, expiresAt: result.expiresAt };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { holdToken, expiresAt: new Date(Date.now() + ttlMs) };
|
|
286
|
+
},
|
|
287
|
+
async releaseHold(_ctx, holdToken) {
|
|
288
|
+
if (options.holds) {
|
|
289
|
+
await options.holds.release(holdToken);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
async commit(ctx, request) {
|
|
293
|
+
const draft = (request.draft ?? {});
|
|
294
|
+
// Defensive product load — the bridge will fail with
|
|
295
|
+
// `product_not_found` anyway, but a clean early-return keeps the
|
|
296
|
+
// commit path's error envelope predictable.
|
|
297
|
+
const product = await loadProduct(ctx.db, request.entityId);
|
|
298
|
+
if (!product) {
|
|
299
|
+
return {
|
|
300
|
+
status: "failed",
|
|
301
|
+
orderRef: "",
|
|
302
|
+
upstreamPayload: { reason: "product_not_found" },
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const partyBilling = extractBillingParty(request.party);
|
|
306
|
+
const partyTravelers = extractPartyTravelers(request.party);
|
|
307
|
+
const travelers = (draft.travelers ?? []).map((t, index) => ({
|
|
308
|
+
firstName: t.firstName,
|
|
309
|
+
lastName: t.lastName,
|
|
310
|
+
email: t.email,
|
|
311
|
+
phone: t.phone,
|
|
312
|
+
personId: partyTravelers[index]?.personId ?? null,
|
|
313
|
+
participantType: "traveler",
|
|
314
|
+
travelerCategory: t.band === "child" || t.band === "infant"
|
|
315
|
+
? t.band
|
|
316
|
+
: "adult",
|
|
317
|
+
}));
|
|
318
|
+
// Promotion-discounted quotes: thread the discounted customer-
|
|
319
|
+
// facing amount into the booking's seed sellAmountCents so
|
|
320
|
+
// checkout / payment see the quoted amount, not the product list
|
|
321
|
+
// price. Inclusive-tax quotes rewrite `base_amount` to net
|
|
322
|
+
// subtotal during tax recompute, so derive the override from the
|
|
323
|
+
// gross breakdown total when an included tax line is present.
|
|
324
|
+
const sellAmountCentsOverride = resolveSellAmountCentsOverride(request.pricing);
|
|
325
|
+
const optionSelections = normalizeOptionSelections(draft.configure?.optionSelections);
|
|
326
|
+
const selectedOptionIds = [
|
|
327
|
+
...new Set(optionSelections.map((selection) => selection.optionId)),
|
|
328
|
+
];
|
|
329
|
+
const primaryOptionId = selectedOptionIds.length === 1
|
|
330
|
+
? selectedOptionIds[0]
|
|
331
|
+
: optionSelections.length === 0
|
|
332
|
+
? (draft.configure?.variantId ?? null)
|
|
333
|
+
: null;
|
|
334
|
+
const bridge = await options.createBooking({
|
|
335
|
+
productId: product.id,
|
|
336
|
+
optionId: primaryOptionId,
|
|
337
|
+
// Link the departure so the booking item carries availability_slot_id
|
|
338
|
+
// (powers the duplicate-departure check + slot-level reporting).
|
|
339
|
+
slotId: draft.configure?.departureSlotId ?? null,
|
|
340
|
+
bookingNumber: generateNumber(),
|
|
341
|
+
personId: partyBilling.personId,
|
|
342
|
+
organizationId: partyBilling.organizationId,
|
|
343
|
+
contactFirstName: partyBilling.contactFirstName,
|
|
344
|
+
contactLastName: partyBilling.contactLastName,
|
|
345
|
+
contactEmail: partyBilling.contactEmail,
|
|
346
|
+
contactPhone: partyBilling.contactPhone,
|
|
347
|
+
internalNotes: extractInternalNotes(request.party),
|
|
348
|
+
travelers: travelers.length > 0 ? travelers : undefined,
|
|
349
|
+
paymentSchedules: draft.paymentSchedules,
|
|
350
|
+
documentGeneration: draft.documentGeneration
|
|
351
|
+
? {
|
|
352
|
+
contractDocument: draft.documentGeneration.contractDocument === true,
|
|
353
|
+
invoiceDocument: draft.documentGeneration.invoiceDocument === true,
|
|
354
|
+
invoiceType: draft.documentGeneration.invoiceType === "proforma" ? "proforma" : "invoice",
|
|
355
|
+
}
|
|
356
|
+
: undefined,
|
|
357
|
+
suppressNotifications: draft.suppressNotifications,
|
|
358
|
+
sellAmountCentsOverride,
|
|
359
|
+
// Manual operator override: `confirmedSellAmountCents` wins over the
|
|
360
|
+
// quote/promotion price; the quote total is the `catalog` baseline so
|
|
361
|
+
// booking-create's override audit + required-reason check fire correctly.
|
|
362
|
+
...(draft.priceOverride
|
|
363
|
+
? {
|
|
364
|
+
catalogSellAmountCents: sellAmountCentsOverride ?? null,
|
|
365
|
+
confirmedSellAmountCents: draft.priceOverride.amountCents,
|
|
366
|
+
priceOverrideReason: draft.priceOverride.reason.trim() || null,
|
|
367
|
+
}
|
|
368
|
+
: {}),
|
|
369
|
+
// Operator-applied gift / refund-credit voucher. booking-create
|
|
370
|
+
// redeems it atomically and re-checks status / expiry / balance.
|
|
371
|
+
voucherRedemption: draft.voucherRedemption,
|
|
372
|
+
taxLines: extractTaxLines(request.pricing),
|
|
373
|
+
itemLines: bookingItemLinesFromOptionSelections(optionSelections),
|
|
374
|
+
extraLines: bookingExtraLinesFromAddonSelections({
|
|
375
|
+
addons: draft.addons,
|
|
376
|
+
addonCatalog: await options.loadAddonCatalog?.(ctx, product.id),
|
|
377
|
+
currency: product.sellCurrency,
|
|
378
|
+
quantityMultiplier: Math.max(1, travelers.length || 1),
|
|
379
|
+
}),
|
|
380
|
+
initialStatus: readInitialStatus(request.parameters),
|
|
381
|
+
});
|
|
382
|
+
if (bridge.status !== "ok" || !bridge.bookingId) {
|
|
383
|
+
return {
|
|
384
|
+
status: "failed",
|
|
385
|
+
orderRef: "",
|
|
386
|
+
upstreamPayload: { bridge },
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
status: "held",
|
|
391
|
+
bookingId: bridge.bookingId,
|
|
392
|
+
orderRef: bridge.bookingNumber ?? bridge.bookingId,
|
|
393
|
+
pricing: request.pricing,
|
|
394
|
+
upstreamPayload: { bridgeBookingId: bridge.bookingId },
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@voyant-travel/inventory/booking-engine` — owned-arm booking handler
|
|
3
|
+
* for the Product vertical.
|
|
4
|
+
*
|
|
5
|
+
* Per `docs/architecture/booking-journey-architecture.md` §6.
|
|
6
|
+
*/
|
|
7
|
+
export { type AvailabilityHoldBridge, type BookingCreateBridge, type BookingCreateBridgeInput, type BookingCreateBridgeResult, buildOwnedProductDraftShape, type CreateProductsBookingHandlerOptions, createProductsBookingHandler, } from "./handler.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/booking-engine/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,2BAA2B,EAC3B,KAAK,mCAAmC,EACxC,4BAA4B,GAC7B,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@voyant-travel/inventory/booking-engine` — owned-arm booking handler
|
|
3
|
+
* for the Product vertical.
|
|
4
|
+
*
|
|
5
|
+
* Per `docs/architecture/booking-journey-architecture.md` §6.
|
|
6
|
+
*/
|
|
7
|
+
export { buildOwnedProductDraftShape, createProductsBookingHandler, } from "./handler.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-engine.d.ts","sourceRoot":"","sources":["../src/booking-engine.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./booking-engine/index.js";
|