@voyant-travel/commerce 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 (210) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +145 -0
  3. package/dist/accepted-quote-version-reservation-golden-flow.test.d.ts +2 -0
  4. package/dist/accepted-quote-version-reservation-golden-flow.test.d.ts.map +1 -0
  5. package/dist/accepted-quote-version-reservation-golden-flow.test.js +398 -0
  6. package/dist/index.d.ts +15 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +14 -0
  9. package/dist/interface.d.ts +18 -0
  10. package/dist/interface.d.ts.map +1 -0
  11. package/dist/interface.js +246 -0
  12. package/dist/interface.test.d.ts +2 -0
  13. package/dist/interface.test.d.ts.map +1 -0
  14. package/dist/interface.test.js +357 -0
  15. package/dist/markets/index.d.ts +11 -0
  16. package/dist/markets/index.d.ts.map +1 -0
  17. package/dist/markets/index.js +12 -0
  18. package/dist/markets/routes.d.ts +1182 -0
  19. package/dist/markets/routes.d.ts.map +1 -0
  20. package/dist/markets/routes.js +209 -0
  21. package/dist/markets/schema.d.ts +1527 -0
  22. package/dist/markets/schema.d.ts.map +1 -0
  23. package/dist/markets/schema.js +240 -0
  24. package/dist/markets/service-core.d.ts +253 -0
  25. package/dist/markets/service-core.d.ts.map +1 -0
  26. package/dist/markets/service-core.js +242 -0
  27. package/dist/markets/service-rules.d.ts +191 -0
  28. package/dist/markets/service-rules.d.ts.map +1 -0
  29. package/dist/markets/service-rules.js +155 -0
  30. package/dist/markets/service-shared.d.ts +36 -0
  31. package/dist/markets/service-shared.d.ts.map +1 -0
  32. package/dist/markets/service-shared.js +7 -0
  33. package/dist/markets/service.d.ts +43 -0
  34. package/dist/markets/service.d.ts.map +1 -0
  35. package/dist/markets/service.js +42 -0
  36. package/dist/markets/validation.d.ts +451 -0
  37. package/dist/markets/validation.d.ts.map +1 -0
  38. package/dist/markets/validation.js +160 -0
  39. package/dist/pricing/events.d.ts +53 -0
  40. package/dist/pricing/events.d.ts.map +1 -0
  41. package/dist/pricing/events.js +28 -0
  42. package/dist/pricing/index.d.ts +15 -0
  43. package/dist/pricing/index.d.ts.map +1 -0
  44. package/dist/pricing/index.js +18 -0
  45. package/dist/pricing/routes-core.d.ts +981 -0
  46. package/dist/pricing/routes-core.d.ts.map +1 -0
  47. package/dist/pricing/routes-core.js +102 -0
  48. package/dist/pricing/routes-public.d.ts +136 -0
  49. package/dist/pricing/routes-public.d.ts.map +1 -0
  50. package/dist/pricing/routes-public.js +14 -0
  51. package/dist/pricing/routes-rules.d.ts +1339 -0
  52. package/dist/pricing/routes-rules.d.ts.map +1 -0
  53. package/dist/pricing/routes-rules.js +138 -0
  54. package/dist/pricing/routes-shared.d.ts +14 -0
  55. package/dist/pricing/routes-shared.d.ts.map +1 -0
  56. package/dist/pricing/routes-shared.js +3 -0
  57. package/dist/pricing/routes.d.ts +7 -0
  58. package/dist/pricing/routes.d.ts.map +1 -0
  59. package/dist/pricing/routes.js +6 -0
  60. package/dist/pricing/schema-catalogs.d.ts +467 -0
  61. package/dist/pricing/schema-catalogs.d.ts.map +1 -0
  62. package/dist/pricing/schema-catalogs.js +47 -0
  63. package/dist/pricing/schema-categories.d.ts +497 -0
  64. package/dist/pricing/schema-categories.d.ts.map +1 -0
  65. package/dist/pricing/schema-categories.js +54 -0
  66. package/dist/pricing/schema-departure-overrides.d.ts +228 -0
  67. package/dist/pricing/schema-departure-overrides.d.ts.map +1 -0
  68. package/dist/pricing/schema-departure-overrides.js +36 -0
  69. package/dist/pricing/schema-option-rules.d.ts +1770 -0
  70. package/dist/pricing/schema-option-rules.d.ts.map +1 -0
  71. package/dist/pricing/schema-option-rules.js +181 -0
  72. package/dist/pricing/schema-policies.d.ts +395 -0
  73. package/dist/pricing/schema-policies.d.ts.map +1 -0
  74. package/dist/pricing/schema-policies.js +41 -0
  75. package/dist/pricing/schema-relations.d.ts +59 -0
  76. package/dist/pricing/schema-relations.d.ts.map +1 -0
  77. package/dist/pricing/schema-relations.js +111 -0
  78. package/dist/pricing/schema-shared.d.ts +11 -0
  79. package/dist/pricing/schema-shared.d.ts.map +1 -0
  80. package/dist/pricing/schema-shared.js +67 -0
  81. package/dist/pricing/schema.d.ts +8 -0
  82. package/dist/pricing/schema.d.ts.map +1 -0
  83. package/dist/pricing/schema.js +7 -0
  84. package/dist/pricing/service-catalog-plane-pricing.d.ts +95 -0
  85. package/dist/pricing/service-catalog-plane-pricing.d.ts.map +1 -0
  86. package/dist/pricing/service-catalog-plane-pricing.js +382 -0
  87. package/dist/pricing/service-catalogs.d.ts +139 -0
  88. package/dist/pricing/service-catalogs.d.ts.map +1 -0
  89. package/dist/pricing/service-catalogs.js +89 -0
  90. package/dist/pricing/service-categories.d.ts +147 -0
  91. package/dist/pricing/service-categories.d.ts.map +1 -0
  92. package/dist/pricing/service-categories.js +105 -0
  93. package/dist/pricing/service-departure-overrides.d.ts +67 -0
  94. package/dist/pricing/service-departure-overrides.d.ts.map +1 -0
  95. package/dist/pricing/service-departure-overrides.js +54 -0
  96. package/dist/pricing/service-option-rules.d.ts +321 -0
  97. package/dist/pricing/service-option-rules.d.ts.map +1 -0
  98. package/dist/pricing/service-option-rules.js +340 -0
  99. package/dist/pricing/service-policies.d.ts +123 -0
  100. package/dist/pricing/service-policies.d.ts.map +1 -0
  101. package/dist/pricing/service-policies.js +95 -0
  102. package/dist/pricing/service-public.d.ts +89 -0
  103. package/dist/pricing/service-public.d.ts.map +1 -0
  104. package/dist/pricing/service-public.js +473 -0
  105. package/dist/pricing/service-rule-resolver.d.ts +67 -0
  106. package/dist/pricing/service-rule-resolver.d.ts.map +1 -0
  107. package/dist/pricing/service-rule-resolver.js +204 -0
  108. package/dist/pricing/service-shared.d.ts +53 -0
  109. package/dist/pricing/service-shared.d.ts.map +1 -0
  110. package/dist/pricing/service-shared.js +4 -0
  111. package/dist/pricing/service-transfer-rules.d.ts +211 -0
  112. package/dist/pricing/service-transfer-rules.d.ts.map +1 -0
  113. package/dist/pricing/service-transfer-rules.js +139 -0
  114. package/dist/pricing/service.d.ts +79 -0
  115. package/dist/pricing/service.d.ts.map +1 -0
  116. package/dist/pricing/service.js +78 -0
  117. package/dist/pricing/validation-public.d.ts +412 -0
  118. package/dist/pricing/validation-public.d.ts.map +1 -0
  119. package/dist/pricing/validation-public.js +111 -0
  120. package/dist/pricing/validation-shared.d.ts +71 -0
  121. package/dist/pricing/validation-shared.d.ts.map +1 -0
  122. package/dist/pricing/validation-shared.js +63 -0
  123. package/dist/pricing/validation.d.ts +987 -0
  124. package/dist/pricing/validation.d.ts.map +1 -0
  125. package/dist/pricing/validation.js +307 -0
  126. package/dist/promotions/events.d.ts +38 -0
  127. package/dist/promotions/events.d.ts.map +1 -0
  128. package/dist/promotions/events.js +25 -0
  129. package/dist/promotions/index.d.ts +12 -0
  130. package/dist/promotions/index.d.ts.map +1 -0
  131. package/dist/promotions/index.js +17 -0
  132. package/dist/promotions/routes-shared.d.ts +14 -0
  133. package/dist/promotions/routes-shared.d.ts.map +1 -0
  134. package/dist/promotions/routes-shared.js +3 -0
  135. package/dist/promotions/routes.d.ts +395 -0
  136. package/dist/promotions/routes.d.ts.map +1 -0
  137. package/dist/promotions/routes.js +55 -0
  138. package/dist/promotions/schema.d.ts +675 -0
  139. package/dist/promotions/schema.d.ts.map +1 -0
  140. package/dist/promotions/schema.js +126 -0
  141. package/dist/promotions/service-booking-confirmed.d.ts +77 -0
  142. package/dist/promotions/service-booking-confirmed.d.ts.map +1 -0
  143. package/dist/promotions/service-booking-confirmed.js +134 -0
  144. package/dist/promotions/service-boundary-scheduler.d.ts +85 -0
  145. package/dist/promotions/service-boundary-scheduler.d.ts.map +1 -0
  146. package/dist/promotions/service-boundary-scheduler.js +141 -0
  147. package/dist/promotions/service-catalog-evaluator.d.ts +22 -0
  148. package/dist/promotions/service-catalog-evaluator.d.ts.map +1 -0
  149. package/dist/promotions/service-catalog-evaluator.js +33 -0
  150. package/dist/promotions/service-catalog-plane-promotions.d.ts +73 -0
  151. package/dist/promotions/service-catalog-plane-promotions.d.ts.map +1 -0
  152. package/dist/promotions/service-catalog-plane-promotions.js +118 -0
  153. package/dist/promotions/service-evaluator.d.ts +134 -0
  154. package/dist/promotions/service-evaluator.d.ts.map +1 -0
  155. package/dist/promotions/service-evaluator.js +302 -0
  156. package/dist/promotions/service-storefront.d.ts +147 -0
  157. package/dist/promotions/service-storefront.d.ts.map +1 -0
  158. package/dist/promotions/service-storefront.js +326 -0
  159. package/dist/promotions/service.d.ts +143 -0
  160. package/dist/promotions/service.d.ts.map +1 -0
  161. package/dist/promotions/service.js +359 -0
  162. package/dist/promotions/validation.d.ts +195 -0
  163. package/dist/promotions/validation.d.ts.map +1 -0
  164. package/dist/promotions/validation.js +167 -0
  165. package/dist/promotions/workflow-bulk-reindex.d.ts +36 -0
  166. package/dist/promotions/workflow-bulk-reindex.d.ts.map +1 -0
  167. package/dist/promotions/workflow-bulk-reindex.js +53 -0
  168. package/dist/promotions/workflow-runtime.d.ts +17 -0
  169. package/dist/promotions/workflow-runtime.d.ts.map +1 -0
  170. package/dist/promotions/workflow-runtime.js +9 -0
  171. package/dist/runtime.d.ts +18 -0
  172. package/dist/runtime.d.ts.map +1 -0
  173. package/dist/runtime.js +27 -0
  174. package/dist/runtime.test.d.ts +2 -0
  175. package/dist/runtime.test.d.ts.map +1 -0
  176. package/dist/runtime.test.js +25 -0
  177. package/dist/schema.d.ts +5 -0
  178. package/dist/schema.d.ts.map +1 -0
  179. package/dist/schema.js +4 -0
  180. package/dist/sellability/index.d.ts +13 -0
  181. package/dist/sellability/index.d.ts.map +1 -0
  182. package/dist/sellability/index.js +17 -0
  183. package/dist/sellability/routes.d.ts +2332 -0
  184. package/dist/sellability/routes.d.ts.map +1 -0
  185. package/dist/sellability/routes.js +166 -0
  186. package/dist/sellability/schema.d.ts +1716 -0
  187. package/dist/sellability/schema.d.ts.map +1 -0
  188. package/dist/sellability/schema.js +278 -0
  189. package/dist/sellability/service-records.d.ts +316 -0
  190. package/dist/sellability/service-records.d.ts.map +1 -0
  191. package/dist/sellability/service-records.js +253 -0
  192. package/dist/sellability/service-resolve.d.ts +72 -0
  193. package/dist/sellability/service-resolve.d.ts.map +1 -0
  194. package/dist/sellability/service-resolve.js +580 -0
  195. package/dist/sellability/service-shared.d.ts +124 -0
  196. package/dist/sellability/service-shared.d.ts.map +1 -0
  197. package/dist/sellability/service-shared.js +96 -0
  198. package/dist/sellability/service-snapshots.d.ts +191 -0
  199. package/dist/sellability/service-snapshots.d.ts.map +1 -0
  200. package/dist/sellability/service-snapshots.js +153 -0
  201. package/dist/sellability/service.d.ts +1038 -0
  202. package/dist/sellability/service.d.ts.map +1 -0
  203. package/dist/sellability/service.js +17 -0
  204. package/dist/sellability/validation.d.ts +477 -0
  205. package/dist/sellability/validation.d.ts.map +1 -0
  206. package/dist/sellability/validation.js +192 -0
  207. package/dist/types.d.ts +239 -0
  208. package/dist/types.d.ts.map +1 -0
  209. package/dist/types.js +1 -0
  210. package/package.json +62 -0
@@ -0,0 +1,246 @@
1
+ export class CommercialDecisionError extends Error {
2
+ code;
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "CommercialDecisionError";
7
+ }
8
+ }
9
+ export function createCommerceAdapterRegistry(adapters = []) {
10
+ const registrations = new Map();
11
+ function registerPriceAvailabilityAdapter(adapter) {
12
+ if (registrations.has(adapter.id)) {
13
+ throw new CommercialDecisionError(`Commercial adapter "${adapter.id}" is already registered.`, "duplicate_adapter");
14
+ }
15
+ registrations.set(adapter.id, adapter);
16
+ }
17
+ async function resolveAdapter(input) {
18
+ if (input.item.adapterHint) {
19
+ const hinted = registrations.get(input.item.adapterHint);
20
+ if (!hinted) {
21
+ return null;
22
+ }
23
+ return (await hinted.supports(input)) ? hinted : null;
24
+ }
25
+ const matches = [];
26
+ for (const adapter of registrations.values()) {
27
+ if (await adapter.supports(input)) {
28
+ matches.push(adapter);
29
+ }
30
+ }
31
+ if (matches.length > 1) {
32
+ throw new CommercialDecisionError(`More than one commercial adapter supports item "${input.item.id}".`, "adapter_ambiguous");
33
+ }
34
+ return matches[0] ?? null;
35
+ }
36
+ for (const adapter of adapters) {
37
+ registerPriceAvailabilityAdapter(adapter);
38
+ }
39
+ return {
40
+ listAdapters: () => Array.from(registrations.values()),
41
+ registerPriceAvailabilityAdapter,
42
+ resolveAdapter,
43
+ };
44
+ }
45
+ export async function evaluateCommercialDecision(input, context = {}) {
46
+ const registry = context.registry ?? createCommerceAdapterRegistry();
47
+ const evaluatedAt = input.requestedAt ?? context.now?.toISOString() ?? new Date().toISOString();
48
+ let adapter;
49
+ try {
50
+ adapter = await registry.resolveAdapter(input);
51
+ }
52
+ catch (error) {
53
+ if (error instanceof CommercialDecisionError && error.code === "adapter_ambiguous") {
54
+ return buildDecision(input, evaluatedAt, {
55
+ status: "error",
56
+ reason: {
57
+ code: "adapter_ambiguous",
58
+ message: error.message,
59
+ },
60
+ traces: [
61
+ commerceTrace("adapter_ambiguous", "error", {
62
+ message: error.message,
63
+ }),
64
+ ],
65
+ });
66
+ }
67
+ throw error;
68
+ }
69
+ if (!adapter) {
70
+ return buildDecision(input, evaluatedAt, {
71
+ status: "unbuyable",
72
+ reason: {
73
+ code: "unsupported_item",
74
+ message: "No price-availability adapter supports this item.",
75
+ },
76
+ traces: [
77
+ commerceTrace("adapter_not_found", "blocked", {
78
+ message: "No price-availability adapter supports this item.",
79
+ refs: { itemId: input.item.id },
80
+ }),
81
+ ],
82
+ });
83
+ }
84
+ const adapterHandle = {
85
+ adapterId: adapter.id,
86
+ adapterKind: adapter.kind,
87
+ };
88
+ let result;
89
+ try {
90
+ result = await adapter.evaluate(input, context);
91
+ }
92
+ catch (error) {
93
+ const message = error instanceof Error ? error.message : "Adapter execution failed.";
94
+ return buildDecision(input, evaluatedAt, {
95
+ status: "error",
96
+ reason: {
97
+ code: "adapter_failed",
98
+ message,
99
+ },
100
+ handles: [adapterHandle],
101
+ traces: [
102
+ commerceTrace("adapter_selected", "applied", {
103
+ refs: { adapterId: adapter.id },
104
+ }),
105
+ commerceTrace("adapter_failed", "error", {
106
+ message,
107
+ refs: { adapterId: adapter.id },
108
+ }),
109
+ ],
110
+ });
111
+ }
112
+ if (result.status === "available" && !result.pricing) {
113
+ return buildDecision(input, evaluatedAt, {
114
+ status: "error",
115
+ reason: {
116
+ code: "adapter_invalid_result",
117
+ message: "A buyable commercial decision requires pricing facts.",
118
+ },
119
+ handles: [adapterHandle, ...toAdapterHandles(adapter, result.handles)],
120
+ traces: [
121
+ commerceTrace("adapter_selected", "applied", {
122
+ refs: { adapterId: adapter.id },
123
+ }),
124
+ ...(result.traces ?? []),
125
+ commerceTrace("adapter_invalid_result", "error", {
126
+ message: "A buyable commercial decision requires pricing facts.",
127
+ refs: { adapterId: adapter.id },
128
+ }),
129
+ ],
130
+ });
131
+ }
132
+ const status = result.status === "available" ? "buyable" : "unbuyable";
133
+ return buildDecision(input, evaluatedAt, {
134
+ status,
135
+ validFrom: result.validFrom,
136
+ validUntil: result.validUntil,
137
+ reason: result.reason ??
138
+ (status === "buyable"
139
+ ? { code: "buyable", message: "Adapter returned an available price." }
140
+ : { code: "unavailable", message: "Adapter returned unavailable." }),
141
+ pricing: result.pricing,
142
+ fx: result.fx ?? result.pricing?.fx,
143
+ promotions: result.promotions,
144
+ availability: result.availability,
145
+ sellability: result.sellability,
146
+ market: result.market ?? input.market,
147
+ channel: result.channel ?? input.channel,
148
+ handles: [adapterHandle, ...toAdapterHandles(adapter, result.handles)],
149
+ traces: [
150
+ commerceTrace("adapter_selected", "applied", {
151
+ refs: { adapterId: adapter.id },
152
+ }),
153
+ ...(result.traces ?? []),
154
+ ],
155
+ });
156
+ }
157
+ export function createCommercialDecisionEvaluator(options = {}) {
158
+ const registry = createCommerceAdapterRegistry(options.adapters);
159
+ return {
160
+ registerPriceAvailabilityAdapter: registry.registerPriceAvailabilityAdapter,
161
+ evaluateCommercialDecision(input, context = {}) {
162
+ return evaluateCommercialDecision(input, { ...context, registry });
163
+ },
164
+ };
165
+ }
166
+ export async function recordCommercialSnapshot(decision, target, repository) {
167
+ return repository.recordCommercialSnapshot({
168
+ decision,
169
+ target,
170
+ idempotencyKey: target.idempotencyKey ?? decision.idempotencyKey,
171
+ });
172
+ }
173
+ function buildDecision(input, evaluatedAt, fields) {
174
+ return {
175
+ decisionId: `commercial_decision_${fingerprint(input.idempotencyKey
176
+ ? { idempotencyKey: input.idempotencyKey }
177
+ : {
178
+ evaluatedAt,
179
+ input,
180
+ reason: fields.reason,
181
+ status: fields.status,
182
+ })}`,
183
+ status: fields.status,
184
+ buyable: fields.status === "buyable",
185
+ input,
186
+ evaluatedAt,
187
+ idempotencyKey: input.idempotencyKey,
188
+ validFrom: fields.validFrom,
189
+ validUntil: fields.validUntil,
190
+ reason: fields.reason,
191
+ pricing: fields.pricing,
192
+ fx: fields.fx,
193
+ promotions: fields.promotions ?? {
194
+ requestedCodes: input.promotionCodes,
195
+ applied: [],
196
+ rejected: [],
197
+ },
198
+ availability: fields.availability,
199
+ sellability: fields.sellability,
200
+ market: fields.market ?? input.market,
201
+ channel: fields.channel ?? input.channel,
202
+ traces: fields.traces ?? [],
203
+ handles: fields.handles ?? [],
204
+ };
205
+ }
206
+ function commerceTrace(code, outcome, options = {}) {
207
+ return {
208
+ id: `trace_${fingerprint({ code, outcome, ...options })}`,
209
+ source: "commerce",
210
+ outcome,
211
+ code,
212
+ message: options.message,
213
+ refs: options.refs,
214
+ facts: options.facts,
215
+ };
216
+ }
217
+ function toAdapterHandles(adapter, handles) {
218
+ return (handles ?? []).map((handle) => ({
219
+ ...handle,
220
+ adapterId: adapter.id,
221
+ adapterKind: adapter.kind,
222
+ }));
223
+ }
224
+ function fingerprint(value) {
225
+ const text = stableStringify(value);
226
+ let hash = 0x811c9dc5;
227
+ for (let i = 0; i < text.length; i += 1) {
228
+ hash ^= text.charCodeAt(i);
229
+ hash = Math.imul(hash, 0x01000193);
230
+ }
231
+ return (hash >>> 0).toString(36);
232
+ }
233
+ function stableStringify(value) {
234
+ if (Array.isArray(value)) {
235
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
236
+ }
237
+ if (value && typeof value === "object") {
238
+ const record = value;
239
+ return `{${Object.keys(record)
240
+ .filter((key) => record[key] !== undefined)
241
+ .sort()
242
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
243
+ .join(",")}}`;
244
+ }
245
+ return JSON.stringify(value);
246
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=interface.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interface.test.d.ts","sourceRoot":"","sources":["../src/interface.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,357 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createCommercialDecisionEvaluator, recordCommercialSnapshot, } from "./index.js";
3
+ const operatedInput = {
4
+ item: {
5
+ kind: "catalog-item",
6
+ id: "catalog_item_operated_tour",
7
+ source: "operated",
8
+ vertical: "tour",
9
+ },
10
+ date: "2026-09-18",
11
+ party: {
12
+ pax: 2,
13
+ adults: 2,
14
+ },
15
+ currency: "EUR",
16
+ buyer: {
17
+ actorType: "customer",
18
+ segmentIds: ["retail"],
19
+ },
20
+ channel: {
21
+ id: "channel_direct",
22
+ kind: "direct",
23
+ },
24
+ market: {
25
+ id: "market_ro",
26
+ code: "RO",
27
+ currency: "EUR",
28
+ },
29
+ promotionCodes: ["EARLY10"],
30
+ requestedAt: "2026-06-13T09:00:00.000Z",
31
+ idempotencyKey: "quote-version:quote_v1:item:catalog_item_operated_tour",
32
+ };
33
+ const operatedInventoryAdapter = {
34
+ id: "operated-inventory",
35
+ kind: "operated-inventory",
36
+ supports(input) {
37
+ return input.item.kind === "catalog-item" && input.item.source === "operated";
38
+ },
39
+ evaluate(input) {
40
+ return {
41
+ status: "available",
42
+ validUntil: "2026-06-13T09:15:00.000Z",
43
+ pricing: {
44
+ currency: input.currency,
45
+ subtotal: {
46
+ amountMinor: 24000,
47
+ currency: input.currency,
48
+ },
49
+ discountTotal: {
50
+ amountMinor: 2400,
51
+ currency: input.currency,
52
+ },
53
+ total: {
54
+ amountMinor: 21600,
55
+ currency: input.currency,
56
+ },
57
+ components: [
58
+ {
59
+ kind: "base",
60
+ label: "Adult",
61
+ quantity: 2,
62
+ amount: {
63
+ amountMinor: 24000,
64
+ currency: input.currency,
65
+ },
66
+ ruleId: "price_rule_adult",
67
+ },
68
+ {
69
+ kind: "discount",
70
+ label: "Early booking",
71
+ amount: {
72
+ amountMinor: -2400,
73
+ currency: input.currency,
74
+ },
75
+ ruleId: "promotion_early10",
76
+ },
77
+ ],
78
+ priceRuleIds: ["price_rule_adult"],
79
+ },
80
+ promotions: {
81
+ requestedCodes: input.promotionCodes,
82
+ applied: [
83
+ {
84
+ id: "promotion_early10",
85
+ code: "EARLY10",
86
+ status: "applied",
87
+ discount: {
88
+ amountMinor: 2400,
89
+ currency: input.currency,
90
+ },
91
+ ruleId: "promotion_early10",
92
+ },
93
+ ],
94
+ rejected: [],
95
+ totalDiscount: {
96
+ amountMinor: 2400,
97
+ currency: input.currency,
98
+ },
99
+ },
100
+ availability: {
101
+ status: "limited",
102
+ capacityRemaining: 6,
103
+ allocationRef: "slot_2026_09_18",
104
+ },
105
+ sellability: {
106
+ status: "allowed",
107
+ policyIds: ["sellability_policy_public"],
108
+ },
109
+ traces: [
110
+ {
111
+ id: "trace_market_rule_ro",
112
+ source: "markets",
113
+ outcome: "applied",
114
+ code: "market_rule_applied",
115
+ ruleId: "market_rule_ro",
116
+ },
117
+ {
118
+ id: "trace_price_rule_adult",
119
+ source: "pricing",
120
+ outcome: "applied",
121
+ code: "price_rule_applied",
122
+ ruleId: "price_rule_adult",
123
+ },
124
+ {
125
+ id: "trace_promotion_early10",
126
+ source: "promotions",
127
+ outcome: "applied",
128
+ code: "promotion_applied",
129
+ ruleId: "promotion_early10",
130
+ },
131
+ ],
132
+ handles: [
133
+ {
134
+ providerId: "inventory",
135
+ handle: "slot_2026_09_18",
136
+ },
137
+ ],
138
+ };
139
+ },
140
+ };
141
+ const sourcedAdapter = {
142
+ id: "cruise-source",
143
+ kind: "source",
144
+ supports(input) {
145
+ return input.item.kind === "vertical-item" && input.item.vertical === "cruise";
146
+ },
147
+ evaluate(input) {
148
+ return {
149
+ status: "available",
150
+ validUntil: "2026-06-13T09:05:00.000Z",
151
+ pricing: {
152
+ currency: input.currency,
153
+ subtotal: {
154
+ amountMinor: 180000,
155
+ currency: input.currency,
156
+ },
157
+ taxTotal: {
158
+ amountMinor: 12000,
159
+ currency: input.currency,
160
+ },
161
+ total: {
162
+ amountMinor: 192000,
163
+ currency: input.currency,
164
+ },
165
+ components: [
166
+ {
167
+ kind: "base",
168
+ label: "Cabin fare",
169
+ amount: {
170
+ amountMinor: 180000,
171
+ currency: input.currency,
172
+ },
173
+ ruleId: "fare_offer_abc",
174
+ },
175
+ {
176
+ kind: "tax",
177
+ label: "Port taxes",
178
+ amount: {
179
+ amountMinor: 12000,
180
+ currency: input.currency,
181
+ },
182
+ },
183
+ ],
184
+ fx: {
185
+ sourceCurrency: "USD",
186
+ requestedCurrency: "EUR",
187
+ rate: "0.920000",
188
+ rateSetId: "fx_2026_06_13",
189
+ provider: {
190
+ providerId: "treasury",
191
+ },
192
+ quotedAt: "2026-06-13T09:00:00.000Z",
193
+ },
194
+ },
195
+ fx: {
196
+ sourceCurrency: "USD",
197
+ requestedCurrency: "EUR",
198
+ rate: "0.920000",
199
+ rateSetId: "fx_2026_06_13",
200
+ provider: {
201
+ providerId: "treasury",
202
+ },
203
+ quotedAt: "2026-06-13T09:00:00.000Z",
204
+ },
205
+ promotions: {
206
+ requestedCodes: input.promotionCodes,
207
+ applied: [],
208
+ rejected: [
209
+ {
210
+ code: "EARLY10",
211
+ status: "rejected",
212
+ reason: "not_source_eligible",
213
+ ruleId: "promotion_early10",
214
+ },
215
+ ],
216
+ },
217
+ availability: {
218
+ status: "available",
219
+ capacityRemaining: 3,
220
+ },
221
+ sellability: {
222
+ status: "allowed",
223
+ policyIds: ["source_policy_public"],
224
+ },
225
+ traces: [
226
+ {
227
+ id: "trace_live_offer",
228
+ source: "adapter",
229
+ outcome: "applied",
230
+ code: "live_offer_resolved",
231
+ refs: {
232
+ offerRef: "offer_abc",
233
+ },
234
+ },
235
+ {
236
+ id: "trace_fx",
237
+ source: "fx",
238
+ outcome: "applied",
239
+ code: "fx_rate_applied",
240
+ ruleId: "fx_2026_06_13",
241
+ },
242
+ ],
243
+ handles: [
244
+ {
245
+ providerId: "cruise-provider",
246
+ sourceId: "source_cruise",
247
+ externalRef: "offer_abc",
248
+ handle: "live_quote_123",
249
+ },
250
+ ],
251
+ };
252
+ },
253
+ };
254
+ describe("commercial decision Interface", () => {
255
+ it("evaluates an operated item through the operated inventory adapter", async () => {
256
+ const evaluator = createCommercialDecisionEvaluator({
257
+ adapters: [operatedInventoryAdapter],
258
+ });
259
+ const decision = await evaluator.evaluateCommercialDecision(operatedInput);
260
+ expect(decision.status).toBe("buyable");
261
+ expect(decision.buyable).toBe(true);
262
+ expect(decision.pricing?.total).toEqual({
263
+ amountMinor: 21600,
264
+ currency: "EUR",
265
+ });
266
+ expect(decision.promotions.applied).toHaveLength(1);
267
+ expect(decision.availability?.capacityRemaining).toBe(6);
268
+ expect(decision.sellability?.policyIds).toEqual(["sellability_policy_public"]);
269
+ expect(decision.traces.map((trace) => trace.code)).toEqual([
270
+ "adapter_selected",
271
+ "market_rule_applied",
272
+ "price_rule_applied",
273
+ "promotion_applied",
274
+ ]);
275
+ expect(decision.handles).toEqual([
276
+ {
277
+ adapterId: "operated-inventory",
278
+ adapterKind: "operated-inventory",
279
+ },
280
+ {
281
+ adapterId: "operated-inventory",
282
+ adapterKind: "operated-inventory",
283
+ providerId: "inventory",
284
+ handle: "slot_2026_09_18",
285
+ },
286
+ ]);
287
+ });
288
+ it("evaluates a sourced vertical item through a source adapter and records snapshots separately", async () => {
289
+ const evaluator = createCommercialDecisionEvaluator({
290
+ adapters: [operatedInventoryAdapter],
291
+ });
292
+ evaluator.registerPriceAvailabilityAdapter(sourcedAdapter);
293
+ const input = {
294
+ ...operatedInput,
295
+ item: {
296
+ kind: "vertical-item",
297
+ id: "cruise_sailing_2026_11_02",
298
+ vertical: "cruise",
299
+ source: "sourced",
300
+ sourceRef: {
301
+ sourceId: "source_cruise",
302
+ externalRef: "sailing_2026_11_02",
303
+ },
304
+ },
305
+ idempotencyKey: "quote-version:quote_v1:item:cruise_sailing_2026_11_02",
306
+ };
307
+ const writes = [];
308
+ const repository = {
309
+ async recordCommercialSnapshot(write) {
310
+ writes.push(write);
311
+ return {
312
+ snapshotId: `commercial_snapshot_${writes.length}`,
313
+ decisionId: write.decision.decisionId,
314
+ target: write.target,
315
+ idempotencyKey: write.idempotencyKey,
316
+ recordedAt: "2026-06-13T09:01:00.000Z",
317
+ };
318
+ },
319
+ };
320
+ const decision = await evaluator.evaluateCommercialDecision(input);
321
+ expect(writes).toHaveLength(0);
322
+ expect(decision.status).toBe("buyable");
323
+ expect(decision.pricing?.total.amountMinor).toBe(192000);
324
+ expect(decision.fx).toMatchObject({
325
+ sourceCurrency: "USD",
326
+ requestedCurrency: "EUR",
327
+ rateSetId: "fx_2026_06_13",
328
+ });
329
+ expect(decision.promotions.rejected).toEqual([
330
+ {
331
+ code: "EARLY10",
332
+ status: "rejected",
333
+ reason: "not_source_eligible",
334
+ ruleId: "promotion_early10",
335
+ },
336
+ ]);
337
+ expect(decision.traces.map((trace) => trace.code)).toContain("live_offer_resolved");
338
+ expect(decision.handles).toContainEqual({
339
+ adapterId: "cruise-source",
340
+ adapterKind: "source",
341
+ providerId: "cruise-provider",
342
+ sourceId: "source_cruise",
343
+ externalRef: "offer_abc",
344
+ handle: "live_quote_123",
345
+ });
346
+ const snapshot = await recordCommercialSnapshot(decision, {
347
+ kind: "quote-version",
348
+ id: "quote_version_1",
349
+ }, repository);
350
+ expect(writes).toHaveLength(1);
351
+ expect(snapshot).toMatchObject({
352
+ snapshotId: "commercial_snapshot_1",
353
+ decisionId: decision.decisionId,
354
+ idempotencyKey: "quote-version:quote_v1:item:cruise_sailing_2026_11_02",
355
+ });
356
+ });
357
+ });
@@ -0,0 +1,11 @@
1
+ import type { Module } from "@voyant-travel/core";
2
+ import type { HonoModule } from "@voyant-travel/hono/module";
3
+ import { marketsService } from "./service.js";
4
+ export type { MarketsRoutes } from "./routes.js";
5
+ export declare const marketsModule: Module;
6
+ export declare const marketsHonoModule: HonoModule;
7
+ export type { ExchangeRate, FxRateSet, Market, MarketChannelRule, MarketCurrency, MarketLocale, MarketPriceCatalog, MarketProductRule, NewExchangeRate, NewFxRateSet, NewMarket, NewMarketChannelRule, NewMarketCurrency, NewMarketLocale, NewMarketPriceCatalog, NewMarketProductRule, } from "./schema.js";
8
+ export { exchangeRates, fxRateSets, marketChannelRules, marketCurrencies, marketLocales, marketPriceCatalogs, marketProductRules, markets, } from "./schema.js";
9
+ export { exchangeRateListQuerySchema, fxRateSetListQuerySchema, insertExchangeRateSchema, insertFxRateSetSchema, insertMarketChannelRuleSchema, insertMarketCurrencySchema, insertMarketLocaleSchema, insertMarketPriceCatalogSchema, insertMarketProductRuleSchema, insertMarketSchema, marketChannelRuleListQuerySchema, marketCurrencyListQuerySchema, marketListQuerySchema, marketLocaleListQuerySchema, marketPriceCatalogListQuerySchema, marketProductRuleListQuerySchema, updateExchangeRateSchema, updateFxRateSetSchema, updateMarketChannelRuleSchema, updateMarketCurrencySchema, updateMarketLocaleSchema, updateMarketPriceCatalogSchema, updateMarketProductRuleSchema, updateMarketSchema, } from "./validation.js";
10
+ export { marketsService };
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/markets/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAG5D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE7C,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD,eAAO,MAAM,aAAa,EAAE,MAE3B,CAAA;AAED,eAAO,MAAM,iBAAiB,EAAE,UAG/B,CAAA;AAED,YAAY,EACV,YAAY,EACZ,SAAS,EACT,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,SAAS,EACT,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,aAAa,EACb,UAAU,EACV,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,OAAO,GACR,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,2BAA2B,EAC3B,wBAAwB,EACxB,wBAAwB,EACxB,qBAAqB,EACrB,6BAA6B,EAC7B,0BAA0B,EAC1B,wBAAwB,EACxB,8BAA8B,EAC9B,6BAA6B,EAC7B,kBAAkB,EAClB,gCAAgC,EAChC,6BAA6B,EAC7B,qBAAqB,EACrB,2BAA2B,EAC3B,iCAAiC,EACjC,gCAAgC,EAChC,wBAAwB,EACxB,qBAAqB,EACrB,6BAA6B,EAC7B,0BAA0B,EAC1B,wBAAwB,EACxB,8BAA8B,EAC9B,6BAA6B,EAC7B,kBAAkB,GACnB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,12 @@
1
+ import { marketsRoutes } from "./routes.js";
2
+ import { marketsService } from "./service.js";
3
+ export const marketsModule = {
4
+ name: "markets",
5
+ };
6
+ export const marketsHonoModule = {
7
+ module: marketsModule,
8
+ routes: marketsRoutes,
9
+ };
10
+ export { exchangeRates, fxRateSets, marketChannelRules, marketCurrencies, marketLocales, marketPriceCatalogs, marketProductRules, markets, } from "./schema.js";
11
+ export { exchangeRateListQuerySchema, fxRateSetListQuerySchema, insertExchangeRateSchema, insertFxRateSetSchema, insertMarketChannelRuleSchema, insertMarketCurrencySchema, insertMarketLocaleSchema, insertMarketPriceCatalogSchema, insertMarketProductRuleSchema, insertMarketSchema, marketChannelRuleListQuerySchema, marketCurrencyListQuerySchema, marketListQuerySchema, marketLocaleListQuerySchema, marketPriceCatalogListQuerySchema, marketProductRuleListQuerySchema, updateExchangeRateSchema, updateFxRateSetSchema, updateMarketChannelRuleSchema, updateMarketCurrencySchema, updateMarketLocaleSchema, updateMarketPriceCatalogSchema, updateMarketProductRuleSchema, updateMarketSchema, } from "./validation.js";
12
+ export { marketsService };