@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.
Files changed (311) hide show
  1. package/LICENSE +201 -0
  2. package/dist/action-ledger-drift.d.ts +29 -0
  3. package/dist/action-ledger-drift.d.ts.map +1 -0
  4. package/dist/action-ledger-drift.js +338 -0
  5. package/dist/action-ledger.d.ts +104 -0
  6. package/dist/action-ledger.d.ts.map +1 -0
  7. package/dist/action-ledger.js +100 -0
  8. package/dist/authoring/builder.d.ts +37 -0
  9. package/dist/authoring/builder.d.ts.map +1 -0
  10. package/dist/authoring/builder.js +248 -0
  11. package/dist/authoring/clone-content.d.ts +38 -0
  12. package/dist/authoring/clone-content.d.ts.map +1 -0
  13. package/dist/authoring/clone-content.js +367 -0
  14. package/dist/authoring/clone-pricing.d.ts +9 -0
  15. package/dist/authoring/clone-pricing.d.ts.map +1 -0
  16. package/dist/authoring/clone-pricing.js +242 -0
  17. package/dist/authoring/clone.d.ts +45 -0
  18. package/dist/authoring/clone.d.ts.map +1 -0
  19. package/dist/authoring/clone.js +142 -0
  20. package/dist/authoring/errors.d.ts +21 -0
  21. package/dist/authoring/errors.d.ts.map +1 -0
  22. package/dist/authoring/errors.js +13 -0
  23. package/dist/authoring/extension.d.ts +248 -0
  24. package/dist/authoring/extension.d.ts.map +1 -0
  25. package/dist/authoring/extension.js +116 -0
  26. package/dist/authoring/index.d.ts +12 -0
  27. package/dist/authoring/index.d.ts.map +1 -0
  28. package/dist/authoring/index.js +11 -0
  29. package/dist/authoring/schema.d.ts +85 -0
  30. package/dist/authoring/schema.d.ts.map +1 -0
  31. package/dist/authoring/schema.js +16 -0
  32. package/dist/authoring/service.d.ts +28 -0
  33. package/dist/authoring/service.d.ts.map +1 -0
  34. package/dist/authoring/service.js +66 -0
  35. package/dist/authoring/spec.d.ts +524 -0
  36. package/dist/authoring/spec.d.ts.map +1 -0
  37. package/dist/authoring/spec.js +167 -0
  38. package/dist/authoring/validate.d.ts +17 -0
  39. package/dist/authoring/validate.d.ts.map +1 -0
  40. package/dist/authoring/validate.js +83 -0
  41. package/dist/authoring.d.ts +2 -0
  42. package/dist/authoring.d.ts.map +1 -0
  43. package/dist/authoring.js +1 -0
  44. package/dist/booking-engine/handler-support.d.ts +91 -0
  45. package/dist/booking-engine/handler-support.d.ts.map +1 -0
  46. package/dist/booking-engine/handler-support.js +355 -0
  47. package/dist/booking-engine/handler.d.ts +404 -0
  48. package/dist/booking-engine/handler.d.ts.map +1 -0
  49. package/dist/booking-engine/handler.js +398 -0
  50. package/dist/booking-engine/index.d.ts +8 -0
  51. package/dist/booking-engine/index.d.ts.map +1 -0
  52. package/dist/booking-engine/index.js +7 -0
  53. package/dist/booking-engine.d.ts +2 -0
  54. package/dist/booking-engine.d.ts.map +1 -0
  55. package/dist/booking-engine.js +1 -0
  56. package/dist/booking-extension.d.ts +278 -0
  57. package/dist/booking-extension.d.ts.map +1 -0
  58. package/dist/booking-extension.js +161 -0
  59. package/dist/catalog-policy-departures.d.ts +52 -0
  60. package/dist/catalog-policy-departures.d.ts.map +1 -0
  61. package/dist/catalog-policy-departures.js +169 -0
  62. package/dist/catalog-policy-destinations.d.ts +43 -0
  63. package/dist/catalog-policy-destinations.d.ts.map +1 -0
  64. package/dist/catalog-policy-destinations.js +165 -0
  65. package/dist/catalog-policy-pricing.d.ts +55 -0
  66. package/dist/catalog-policy-pricing.d.ts.map +1 -0
  67. package/dist/catalog-policy-pricing.js +109 -0
  68. package/dist/catalog-policy-promotions.d.ts +52 -0
  69. package/dist/catalog-policy-promotions.d.ts.map +1 -0
  70. package/dist/catalog-policy-promotions.js +270 -0
  71. package/dist/catalog-policy-taxonomy.d.ts +51 -0
  72. package/dist/catalog-policy-taxonomy.d.ts.map +1 -0
  73. package/dist/catalog-policy-taxonomy.js +191 -0
  74. package/dist/catalog-policy.d.ts +33 -0
  75. package/dist/catalog-policy.d.ts.map +1 -0
  76. package/dist/catalog-policy.js +733 -0
  77. package/dist/content-shape.d.ts +15 -0
  78. package/dist/content-shape.d.ts.map +1 -0
  79. package/dist/content-shape.js +28 -0
  80. package/dist/draft-shape.d.ts +43 -0
  81. package/dist/draft-shape.d.ts.map +1 -0
  82. package/dist/draft-shape.js +48 -0
  83. package/dist/events.d.ts +37 -0
  84. package/dist/events.d.ts.map +1 -0
  85. package/dist/events.js +32 -0
  86. package/dist/extras/catalog-policy.d.ts +30 -0
  87. package/dist/extras/catalog-policy.d.ts.map +1 -0
  88. package/dist/extras/catalog-policy.js +319 -0
  89. package/dist/extras/content-shape.d.ts +5 -0
  90. package/dist/extras/content-shape.d.ts.map +1 -0
  91. package/dist/extras/content-shape.js +13 -0
  92. package/dist/extras/draft-shape.d.ts +34 -0
  93. package/dist/extras/draft-shape.d.ts.map +1 -0
  94. package/dist/extras/draft-shape.js +69 -0
  95. package/dist/extras/routes.d.ts +380 -0
  96. package/dist/extras/routes.d.ts.map +1 -0
  97. package/dist/extras/routes.js +59 -0
  98. package/dist/extras/schema-sourced-content.d.ts +254 -0
  99. package/dist/extras/schema-sourced-content.d.ts.map +1 -0
  100. package/dist/extras/schema-sourced-content.js +45 -0
  101. package/dist/extras/schema.d.ts +628 -0
  102. package/dist/extras/schema.d.ts.map +1 -0
  103. package/dist/extras/schema.js +87 -0
  104. package/dist/extras/service-catalog-plane.d.ts +77 -0
  105. package/dist/extras/service-catalog-plane.d.ts.map +1 -0
  106. package/dist/extras/service-catalog-plane.js +219 -0
  107. package/dist/extras/service-content-synthesizer.d.ts +41 -0
  108. package/dist/extras/service-content-synthesizer.d.ts.map +1 -0
  109. package/dist/extras/service-content-synthesizer.js +138 -0
  110. package/dist/extras/service-content.d.ts +48 -0
  111. package/dist/extras/service-content.d.ts.map +1 -0
  112. package/dist/extras/service-content.js +253 -0
  113. package/dist/extras/service.d.ts +185 -0
  114. package/dist/extras/service.d.ts.map +1 -0
  115. package/dist/extras/service.js +96 -0
  116. package/dist/extras/validation.d.ts +437 -0
  117. package/dist/extras/validation.d.ts.map +1 -0
  118. package/dist/extras/validation.js +149 -0
  119. package/dist/extras.d.ts +267 -0
  120. package/dist/extras.d.ts.map +1 -0
  121. package/dist/extras.js +19 -0
  122. package/dist/index.d.ts +22 -0
  123. package/dist/index.d.ts.map +1 -0
  124. package/dist/index.js +32 -0
  125. package/dist/interface.d.ts +5869 -0
  126. package/dist/interface.d.ts.map +1 -0
  127. package/dist/interface.js +54 -0
  128. package/dist/public-routes.d.ts +2 -0
  129. package/dist/public-routes.d.ts.map +1 -0
  130. package/dist/public-routes.js +1 -0
  131. package/dist/public-validation.d.ts +2 -0
  132. package/dist/public-validation.d.ts.map +1 -0
  133. package/dist/public-validation.js +1 -0
  134. package/dist/read-model.d.ts +25 -0
  135. package/dist/read-model.d.ts.map +1 -0
  136. package/dist/read-model.js +99 -0
  137. package/dist/route-env.d.ts +22 -0
  138. package/dist/route-env.d.ts.map +1 -0
  139. package/dist/route-env.js +1 -0
  140. package/dist/routes-associations.d.ts +164 -0
  141. package/dist/routes-associations.d.ts.map +1 -0
  142. package/dist/routes-associations.js +100 -0
  143. package/dist/routes-catalog.d.ts +436 -0
  144. package/dist/routes-catalog.d.ts.map +1 -0
  145. package/dist/routes-catalog.js +104 -0
  146. package/dist/routes-configuration.d.ts +773 -0
  147. package/dist/routes-configuration.d.ts.map +1 -0
  148. package/dist/routes-configuration.js +364 -0
  149. package/dist/routes-content.d.ts +74 -0
  150. package/dist/routes-content.d.ts.map +1 -0
  151. package/dist/routes-content.js +117 -0
  152. package/dist/routes-core.d.ts +331 -0
  153. package/dist/routes-core.d.ts.map +1 -0
  154. package/dist/routes-core.js +95 -0
  155. package/dist/routes-itinerary.d.ts +759 -0
  156. package/dist/routes-itinerary.d.ts.map +1 -0
  157. package/dist/routes-itinerary.js +387 -0
  158. package/dist/routes-maintenance.d.ts +32 -0
  159. package/dist/routes-maintenance.d.ts.map +1 -0
  160. package/dist/routes-maintenance.js +14 -0
  161. package/dist/routes-media.d.ts +634 -0
  162. package/dist/routes-media.d.ts.map +1 -0
  163. package/dist/routes-media.js +245 -0
  164. package/dist/routes-merchandising.d.ts +1120 -0
  165. package/dist/routes-merchandising.d.ts.map +1 -0
  166. package/dist/routes-merchandising.js +377 -0
  167. package/dist/routes-options.d.ts +363 -0
  168. package/dist/routes-options.d.ts.map +1 -0
  169. package/dist/routes-options.js +173 -0
  170. package/dist/routes-public.d.ts +776 -0
  171. package/dist/routes-public.d.ts.map +1 -0
  172. package/dist/routes-public.js +119 -0
  173. package/dist/routes-translations.d.ts +489 -0
  174. package/dist/routes-translations.d.ts.map +1 -0
  175. package/dist/routes-translations.js +258 -0
  176. package/dist/routes.d.ts +5097 -0
  177. package/dist/routes.d.ts.map +1 -0
  178. package/dist/routes.js +64 -0
  179. package/dist/schema-core.d.ts +1238 -0
  180. package/dist/schema-core.d.ts.map +1 -0
  181. package/dist/schema-core.js +157 -0
  182. package/dist/schema-itinerary.d.ts +1169 -0
  183. package/dist/schema-itinerary.d.ts.map +1 -0
  184. package/dist/schema-itinerary.js +130 -0
  185. package/dist/schema-relations.d.ts +117 -0
  186. package/dist/schema-relations.d.ts.map +1 -0
  187. package/dist/schema-relations.js +192 -0
  188. package/dist/schema-settings.d.ts +1800 -0
  189. package/dist/schema-settings.d.ts.map +1 -0
  190. package/dist/schema-settings.js +220 -0
  191. package/dist/schema-shared.d.ts +15 -0
  192. package/dist/schema-shared.d.ts.map +1 -0
  193. package/dist/schema-shared.js +91 -0
  194. package/dist/schema-sourced-content.d.ts +262 -0
  195. package/dist/schema-sourced-content.d.ts.map +1 -0
  196. package/dist/schema-sourced-content.js +69 -0
  197. package/dist/schema-taxonomy.d.ts +1363 -0
  198. package/dist/schema-taxonomy.d.ts.map +1 -0
  199. package/dist/schema-taxonomy.js +203 -0
  200. package/dist/schema.d.ts +10 -0
  201. package/dist/schema.d.ts.map +1 -0
  202. package/dist/schema.js +9 -0
  203. package/dist/service-aggregates.d.ts +29 -0
  204. package/dist/service-aggregates.d.ts.map +1 -0
  205. package/dist/service-aggregates.js +56 -0
  206. package/dist/service-catalog-plane-destinations.d.ts +30 -0
  207. package/dist/service-catalog-plane-destinations.d.ts.map +1 -0
  208. package/dist/service-catalog-plane-destinations.js +143 -0
  209. package/dist/service-catalog-plane-taxonomy.d.ts +73 -0
  210. package/dist/service-catalog-plane-taxonomy.d.ts.map +1 -0
  211. package/dist/service-catalog-plane-taxonomy.js +242 -0
  212. package/dist/service-catalog-plane.d.ts +179 -0
  213. package/dist/service-catalog-plane.d.ts.map +1 -0
  214. package/dist/service-catalog-plane.js +431 -0
  215. package/dist/service-catalog.d.ts +251 -0
  216. package/dist/service-catalog.d.ts.map +1 -0
  217. package/dist/service-catalog.js +517 -0
  218. package/dist/service-configuration.d.ts +261 -0
  219. package/dist/service-configuration.d.ts.map +1 -0
  220. package/dist/service-configuration.js +343 -0
  221. package/dist/service-content-owned.d.ts +68 -0
  222. package/dist/service-content-owned.d.ts.map +1 -0
  223. package/dist/service-content-owned.js +329 -0
  224. package/dist/service-content-synthesizer.d.ts +90 -0
  225. package/dist/service-content-synthesizer.d.ts.map +1 -0
  226. package/dist/service-content-synthesizer.js +178 -0
  227. package/dist/service-content.d.ts +106 -0
  228. package/dist/service-content.d.ts.map +1 -0
  229. package/dist/service-content.js +388 -0
  230. package/dist/service-core.d.ts +194 -0
  231. package/dist/service-core.d.ts.map +1 -0
  232. package/dist/service-core.js +213 -0
  233. package/dist/service-delivery-formats.d.ts +58 -0
  234. package/dist/service-delivery-formats.d.ts.map +1 -0
  235. package/dist/service-delivery-formats.js +107 -0
  236. package/dist/service-destinations.d.ts +223 -0
  237. package/dist/service-destinations.d.ts.map +1 -0
  238. package/dist/service-destinations.js +310 -0
  239. package/dist/service-itinerary-history.d.ts +457 -0
  240. package/dist/service-itinerary-history.d.ts.map +1 -0
  241. package/dist/service-itinerary-history.js +135 -0
  242. package/dist/service-itinerary.d.ts +1149 -0
  243. package/dist/service-itinerary.d.ts.map +1 -0
  244. package/dist/service-itinerary.js +419 -0
  245. package/dist/service-media.d.ts +272 -0
  246. package/dist/service-media.d.ts.map +1 -0
  247. package/dist/service-media.js +320 -0
  248. package/dist/service-merchandising.d.ts +184 -0
  249. package/dist/service-merchandising.d.ts.map +1 -0
  250. package/dist/service-merchandising.js +181 -0
  251. package/dist/service-option-translations.d.ts +268 -0
  252. package/dist/service-option-translations.d.ts.map +1 -0
  253. package/dist/service-option-translations.js +300 -0
  254. package/dist/service-options.d.ts +181 -0
  255. package/dist/service-options.d.ts.map +1 -0
  256. package/dist/service-options.js +179 -0
  257. package/dist/service-product-destinations.d.ts +37 -0
  258. package/dist/service-product-destinations.d.ts.map +1 -0
  259. package/dist/service-product-destinations.js +94 -0
  260. package/dist/service-public.d.ts +664 -0
  261. package/dist/service-public.d.ts.map +1 -0
  262. package/dist/service-public.js +374 -0
  263. package/dist/service-taxonomy.d.ts +197 -0
  264. package/dist/service-taxonomy.d.ts.map +1 -0
  265. package/dist/service-taxonomy.js +221 -0
  266. package/dist/service.d.ts +3929 -0
  267. package/dist/service.d.ts.map +1 -0
  268. package/dist/service.js +28 -0
  269. package/dist/tasks/brochure-printers.d.ts +31 -0
  270. package/dist/tasks/brochure-printers.d.ts.map +1 -0
  271. package/dist/tasks/brochure-printers.js +149 -0
  272. package/dist/tasks/brochure-templates.d.ts +36 -0
  273. package/dist/tasks/brochure-templates.d.ts.map +1 -0
  274. package/dist/tasks/brochure-templates.js +110 -0
  275. package/dist/tasks/brochures.d.ts +43 -0
  276. package/dist/tasks/brochures.d.ts.map +1 -0
  277. package/dist/tasks/brochures.js +72 -0
  278. package/dist/tasks/generate-pdf.d.ts +8 -0
  279. package/dist/tasks/generate-pdf.d.ts.map +1 -0
  280. package/dist/tasks/generate-pdf.js +106 -0
  281. package/dist/tasks/index.d.ts +5 -0
  282. package/dist/tasks/index.d.ts.map +1 -0
  283. package/dist/tasks/index.js +4 -0
  284. package/dist/tasks/pdf-text.d.ts +2 -0
  285. package/dist/tasks/pdf-text.d.ts.map +1 -0
  286. package/dist/tasks/pdf-text.js +40 -0
  287. package/dist/tasks.d.ts +2 -0
  288. package/dist/tasks.d.ts.map +1 -0
  289. package/dist/tasks.js +1 -0
  290. package/dist/validation-catalog.d.ts +2 -0
  291. package/dist/validation-catalog.d.ts.map +1 -0
  292. package/dist/validation-catalog.js +3 -0
  293. package/dist/validation-config.d.ts +2 -0
  294. package/dist/validation-config.d.ts.map +1 -0
  295. package/dist/validation-config.js +3 -0
  296. package/dist/validation-content.d.ts +2 -0
  297. package/dist/validation-content.d.ts.map +1 -0
  298. package/dist/validation-content.js +3 -0
  299. package/dist/validation-core.d.ts +2 -0
  300. package/dist/validation-core.d.ts.map +1 -0
  301. package/dist/validation-core.js +3 -0
  302. package/dist/validation-public.d.ts +2 -0
  303. package/dist/validation-public.d.ts.map +1 -0
  304. package/dist/validation-public.js +3 -0
  305. package/dist/validation-shared.d.ts +2 -0
  306. package/dist/validation-shared.d.ts.map +1 -0
  307. package/dist/validation-shared.js +3 -0
  308. package/dist/validation.d.ts +2 -0
  309. package/dist/validation.d.ts.map +1 -0
  310. package/dist/validation.js +3 -0
  311. 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,2 @@
1
+ export * from "./booking-engine/index.js";
2
+ //# sourceMappingURL=booking-engine.d.ts.map
@@ -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";