@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,473 @@
1
+ // agent-quality: file-size exception -- owner: pricing; existing service module stays co-located until a dedicated split preserves behavior and tests.
2
+ import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
3
+ import { optionPriceRules, optionUnitPriceRules, optionUnitTiers, priceCatalogs, priceSchedules, } from "./schema.js";
4
+ import { loadDeparturePriceOverrides, pickRulesForDate, } from "./service-rule-resolver.js";
5
+ function normalizeDate(value) {
6
+ if (!value) {
7
+ return null;
8
+ }
9
+ return value instanceof Date ? value.toISOString() : value;
10
+ }
11
+ function normalizeDateOnly(value) {
12
+ if (!value) {
13
+ return null;
14
+ }
15
+ return value instanceof Date ? value.toISOString().slice(0, 10) : value.slice(0, 10);
16
+ }
17
+ async function executeRows(db, query) {
18
+ // biome-ignore lint/suspicious/noExplicitAny: #1141 keeps cross-package SQL boundary reads driver-agnostic.
19
+ const result = await db.execute(query);
20
+ return Array.isArray(result) ? result : (result?.rows ?? []);
21
+ }
22
+ function sqlList(values) {
23
+ return sql.join(values.map((value) => sql `${value}`), sql `, `);
24
+ }
25
+ function andSql(conditions) {
26
+ return sql.join(conditions, sql ` AND `);
27
+ }
28
+ function readCount(row) {
29
+ const value = row?.count;
30
+ if (typeof value === "number")
31
+ return Number.isFinite(value) ? value : 0;
32
+ if (typeof value === "string") {
33
+ const parsed = Number(value);
34
+ return Number.isFinite(parsed) ? parsed : 0;
35
+ }
36
+ return 0;
37
+ }
38
+ async function ensurePublicProduct(db, productId) {
39
+ const rows = await executeRows(db,
40
+ // agent-quality: raw-sql reviewed -- owner: pricing; cross-module Product visibility check with parameter-bound id.
41
+ sql `
42
+ SELECT id, booking_mode, capacity_mode, sell_currency
43
+ FROM products
44
+ WHERE id = ${productId}
45
+ AND status::text = 'active'
46
+ AND activated = true
47
+ AND visibility::text = 'public'
48
+ LIMIT 1
49
+ `);
50
+ const product = rows[0];
51
+ return product
52
+ ? {
53
+ id: product.id,
54
+ bookingMode: product.booking_mode,
55
+ capacityMode: product.capacity_mode,
56
+ sellCurrency: product.sell_currency,
57
+ }
58
+ : null;
59
+ }
60
+ async function resolvePublicCatalog(db, input) {
61
+ if (input.catalogId) {
62
+ const [catalog] = await db
63
+ .select({
64
+ id: priceCatalogs.id,
65
+ code: priceCatalogs.code,
66
+ name: priceCatalogs.name,
67
+ currencyCode: priceCatalogs.currencyCode,
68
+ })
69
+ .from(priceCatalogs)
70
+ .where(and(eq(priceCatalogs.id, input.catalogId), eq(priceCatalogs.catalogType, "public"), eq(priceCatalogs.active, true)))
71
+ .limit(1);
72
+ return catalog ?? null;
73
+ }
74
+ const [catalog] = await db
75
+ .select({
76
+ id: priceCatalogs.id,
77
+ code: priceCatalogs.code,
78
+ name: priceCatalogs.name,
79
+ currencyCode: priceCatalogs.currencyCode,
80
+ })
81
+ .from(priceCatalogs)
82
+ .where(and(eq(priceCatalogs.catalogType, "public"), eq(priceCatalogs.active, true)))
83
+ .orderBy(desc(priceCatalogs.isDefault), asc(priceCatalogs.name))
84
+ .limit(1);
85
+ return catalog ?? null;
86
+ }
87
+ async function resolveQueryDate(db, query) {
88
+ if (query.date)
89
+ return query.date;
90
+ if (!query.departureId)
91
+ return null;
92
+ const rows = await executeRows(db,
93
+ // agent-quality: raw-sql reviewed -- owner: pricing; cross-module Availability date lookup by parameter-bound slot id.
94
+ sql `SELECT date_local FROM availability_slots WHERE id = ${query.departureId} LIMIT 1`);
95
+ const slot = rows[0];
96
+ return normalizeDateOnly(slot?.date_local);
97
+ }
98
+ async function loadPublicProductOptions(db, productId, optionId) {
99
+ const conditions = [sql `product_id = ${productId}`, sql `status::text = 'active'`];
100
+ if (optionId) {
101
+ conditions.push(sql `id = ${optionId}`);
102
+ }
103
+ return executeRows(db,
104
+ // agent-quality: raw-sql reviewed -- owner: pricing; cross-module Product option read with parameter-bound filters.
105
+ sql `
106
+ SELECT id, name, description, status::text AS status, is_default
107
+ FROM product_options
108
+ WHERE ${andSql(conditions)}
109
+ ORDER BY is_default DESC, sort_order ASC, name ASC
110
+ `);
111
+ }
112
+ async function loadPublicOptionUnits(db, optionIds) {
113
+ if (optionIds.length === 0)
114
+ return [];
115
+ return executeRows(db,
116
+ // agent-quality: raw-sql reviewed -- owner: pricing; cross-module Product option-unit read with parameter-bound option ids.
117
+ sql `
118
+ SELECT id, option_id, name, unit_type::text AS unit_type, sort_order
119
+ FROM option_units
120
+ WHERE option_id IN (${sqlList(optionIds)})
121
+ AND is_hidden = false
122
+ ORDER BY sort_order ASC, name ASC
123
+ `);
124
+ }
125
+ async function loadPublicStartTimeAdjustments(db, ruleIds) {
126
+ if (ruleIds.length === 0)
127
+ return [];
128
+ return executeRows(db,
129
+ // agent-quality: raw-sql reviewed -- owner: pricing; joins local pricing rules to cross-module Availability start-time labels by parameter-bound rule ids.
130
+ sql `
131
+ SELECT
132
+ rule.id,
133
+ rule.option_price_rule_id,
134
+ rule.start_time_id,
135
+ start_time.label,
136
+ start_time.start_time_local,
137
+ start_time.duration_minutes,
138
+ rule.rule_mode::text AS rule_mode,
139
+ rule.adjustment_type::text AS adjustment_type,
140
+ rule.sell_adjustment_cents,
141
+ rule.adjustment_basis_points
142
+ FROM option_start_time_rules rule
143
+ INNER JOIN availability_start_times start_time
144
+ ON start_time.id = rule.start_time_id
145
+ WHERE rule.option_price_rule_id IN (${sqlList(ruleIds)})
146
+ AND rule.active = true
147
+ AND start_time.active = true
148
+ ORDER BY start_time.sort_order ASC, start_time.start_time_local ASC
149
+ `);
150
+ }
151
+ async function narrowRulesByDate(db, rules, isoDate) {
152
+ if (rules.length === 0)
153
+ return rules;
154
+ const scheduleIds = Array.from(new Set(rules.map((r) => r.priceScheduleId).filter((id) => id !== null)));
155
+ const schedules = scheduleIds.length > 0
156
+ ? await db
157
+ .select({
158
+ id: priceSchedules.id,
159
+ active: priceSchedules.active,
160
+ priority: priceSchedules.priority,
161
+ recurrenceRule: priceSchedules.recurrenceRule,
162
+ validFrom: priceSchedules.validFrom,
163
+ validTo: priceSchedules.validTo,
164
+ weekdays: priceSchedules.weekdays,
165
+ timezone: priceSchedules.timezone,
166
+ })
167
+ .from(priceSchedules)
168
+ .where(inArray(priceSchedules.id, scheduleIds))
169
+ : [];
170
+ const scheduleMap = new Map(schedules.map((s) => [
171
+ s.id,
172
+ {
173
+ id: s.id,
174
+ active: s.active,
175
+ priority: s.priority,
176
+ recurrenceRule: s.recurrenceRule,
177
+ validFrom: s.validFrom,
178
+ validTo: s.validTo,
179
+ weekdays: s.weekdays ?? null,
180
+ timezone: s.timezone,
181
+ },
182
+ ]));
183
+ const rulesByOption = new Map();
184
+ for (const r of rules) {
185
+ const existing = rulesByOption.get(r.optionId) ?? [];
186
+ existing.push(r);
187
+ rulesByOption.set(r.optionId, existing);
188
+ }
189
+ const winners = [];
190
+ for (const [, candidateRules] of rulesByOption) {
191
+ const picked = pickRulesForDate(candidateRules, scheduleMap, isoDate);
192
+ const winnerId = picked[0]?.id;
193
+ if (!winnerId)
194
+ continue;
195
+ const winner = candidateRules.find((r) => r.id === winnerId);
196
+ if (winner)
197
+ winners.push(winner);
198
+ }
199
+ return winners;
200
+ }
201
+ export const publicPricingService = {
202
+ async getProductPricingSnapshot(db, productId, query) {
203
+ const product = await ensurePublicProduct(db, productId);
204
+ if (!product) {
205
+ return null;
206
+ }
207
+ const catalog = await resolvePublicCatalog(db, query);
208
+ if (!catalog) {
209
+ return null;
210
+ }
211
+ const options = await loadPublicProductOptions(db, productId, query.optionId);
212
+ if (options.length === 0) {
213
+ return {
214
+ productId,
215
+ catalog: {
216
+ ...catalog,
217
+ currencyCode: catalog.currencyCode ?? product.sellCurrency,
218
+ },
219
+ options: [],
220
+ };
221
+ }
222
+ const optionIds = options.map((option) => option.id);
223
+ const resolvedDate = await resolveQueryDate(db, query);
224
+ const overridesByUnit = query.departureId
225
+ ? await loadDeparturePriceOverrides(db, {
226
+ departureId: query.departureId,
227
+ catalogId: catalog.id,
228
+ })
229
+ : new Map();
230
+ const [units, allRules] = await Promise.all([
231
+ loadPublicOptionUnits(db, optionIds),
232
+ db
233
+ .select({
234
+ id: optionPriceRules.id,
235
+ optionId: optionPriceRules.optionId,
236
+ name: optionPriceRules.name,
237
+ description: optionPriceRules.description,
238
+ pricingMode: optionPriceRules.pricingMode,
239
+ baseSellAmountCents: optionPriceRules.baseSellAmountCents,
240
+ minPerBooking: optionPriceRules.minPerBooking,
241
+ maxPerBooking: optionPriceRules.maxPerBooking,
242
+ isDefault: optionPriceRules.isDefault,
243
+ cancellationPolicyId: optionPriceRules.cancellationPolicyId,
244
+ priceScheduleId: optionPriceRules.priceScheduleId,
245
+ })
246
+ .from(optionPriceRules)
247
+ .where(and(eq(optionPriceRules.productId, productId), inArray(optionPriceRules.optionId, optionIds), eq(optionPriceRules.priceCatalogId, catalog.id), eq(optionPriceRules.active, true)))
248
+ .orderBy(desc(optionPriceRules.isDefault), asc(optionPriceRules.name)),
249
+ ]);
250
+ const rules = resolvedDate ? await narrowRulesByDate(db, allRules, resolvedDate) : allRules;
251
+ const ruleIds = rules.map((rule) => rule.id);
252
+ const [unitPrices, startTimeAdjustments] = await Promise.all([
253
+ ruleIds.length > 0
254
+ ? db
255
+ .select({
256
+ id: optionUnitPriceRules.id,
257
+ optionPriceRuleId: optionUnitPriceRules.optionPriceRuleId,
258
+ unitId: optionUnitPriceRules.unitId,
259
+ pricingMode: optionUnitPriceRules.pricingMode,
260
+ sellAmountCents: optionUnitPriceRules.sellAmountCents,
261
+ minQuantity: optionUnitPriceRules.minQuantity,
262
+ maxQuantity: optionUnitPriceRules.maxQuantity,
263
+ pricingCategoryId: optionUnitPriceRules.pricingCategoryId,
264
+ sortOrder: optionUnitPriceRules.sortOrder,
265
+ })
266
+ .from(optionUnitPriceRules)
267
+ .where(and(inArray(optionUnitPriceRules.optionPriceRuleId, ruleIds), eq(optionUnitPriceRules.active, true)))
268
+ .orderBy(asc(optionUnitPriceRules.sortOrder), asc(optionUnitPriceRules.createdAt))
269
+ : Promise.resolve([]),
270
+ loadPublicStartTimeAdjustments(db, ruleIds),
271
+ ]);
272
+ const unitPriceIds = unitPrices.map((unitPrice) => unitPrice.id);
273
+ const tiers = unitPriceIds.length > 0
274
+ ? await db
275
+ .select({
276
+ id: optionUnitTiers.id,
277
+ optionUnitPriceRuleId: optionUnitTiers.optionUnitPriceRuleId,
278
+ minQuantity: optionUnitTiers.minQuantity,
279
+ maxQuantity: optionUnitTiers.maxQuantity,
280
+ sellAmountCents: optionUnitTiers.sellAmountCents,
281
+ sortOrder: optionUnitTiers.sortOrder,
282
+ })
283
+ .from(optionUnitTiers)
284
+ .where(and(inArray(optionUnitTiers.optionUnitPriceRuleId, unitPriceIds), eq(optionUnitTiers.active, true)))
285
+ .orderBy(asc(optionUnitTiers.sortOrder), asc(optionUnitTiers.minQuantity))
286
+ : [];
287
+ const unitById = new Map(units.map((unit) => [
288
+ unit.id,
289
+ {
290
+ id: unit.id,
291
+ unitId: unit.id,
292
+ unitName: unit.name,
293
+ unitType: unit.unit_type,
294
+ sortOrder: unit.sort_order,
295
+ },
296
+ ]));
297
+ const tiersByUnitPriceRule = new Map();
298
+ for (const tier of tiers) {
299
+ const existing = tiersByUnitPriceRule.get(tier.optionUnitPriceRuleId) ?? [];
300
+ existing.push(tier);
301
+ tiersByUnitPriceRule.set(tier.optionUnitPriceRuleId, existing);
302
+ }
303
+ const unitPricesByRule = new Map();
304
+ for (const unitPrice of unitPrices) {
305
+ const existing = unitPricesByRule.get(unitPrice.optionPriceRuleId) ?? [];
306
+ existing.push(unitPrice);
307
+ unitPricesByRule.set(unitPrice.optionPriceRuleId, existing);
308
+ }
309
+ const startTimeAdjustmentsByRule = new Map();
310
+ for (const adjustment of startTimeAdjustments) {
311
+ const existing = startTimeAdjustmentsByRule.get(adjustment.option_price_rule_id) ?? [];
312
+ existing.push(adjustment);
313
+ startTimeAdjustmentsByRule.set(adjustment.option_price_rule_id, existing);
314
+ }
315
+ const rulesByOption = new Map();
316
+ for (const rule of rules) {
317
+ const existing = rulesByOption.get(rule.optionId) ?? [];
318
+ existing.push(rule);
319
+ rulesByOption.set(rule.optionId, existing);
320
+ }
321
+ return {
322
+ productId,
323
+ catalog: {
324
+ ...catalog,
325
+ currencyCode: catalog.currencyCode ?? product.sellCurrency,
326
+ },
327
+ options: options.map((option) => ({
328
+ id: option.id,
329
+ name: option.name,
330
+ description: option.description ?? null,
331
+ status: option.status,
332
+ isDefault: option.is_default,
333
+ bookingMode: product.bookingMode,
334
+ capacityMode: product.capacityMode,
335
+ pricingRules: (rulesByOption.get(option.id) ?? []).map((rule) => ({
336
+ id: rule.id,
337
+ name: rule.name,
338
+ description: rule.description ?? null,
339
+ pricingMode: rule.pricingMode,
340
+ baseSellAmountCents: rule.baseSellAmountCents ?? null,
341
+ minPerBooking: rule.minPerBooking ?? null,
342
+ maxPerBooking: rule.maxPerBooking ?? null,
343
+ isDefault: rule.isDefault,
344
+ cancellationPolicyId: rule.cancellationPolicyId ?? null,
345
+ unitPrices: (unitPricesByRule.get(rule.id) ?? [])
346
+ .map((unitPrice) => {
347
+ const unit = unitById.get(unitPrice.unitId);
348
+ if (!unit) {
349
+ return null;
350
+ }
351
+ const override = overridesByUnit.get(unit.unitId);
352
+ return {
353
+ id: unitPrice.id,
354
+ unitId: unit.unitId,
355
+ unitName: unit.unitName,
356
+ unitType: unit.unitType,
357
+ pricingMode: unitPrice.pricingMode,
358
+ sellAmountCents: override
359
+ ? override.sellAmountCents
360
+ : (unitPrice.sellAmountCents ?? null),
361
+ minQuantity: unitPrice.minQuantity ?? null,
362
+ maxQuantity: unitPrice.maxQuantity ?? null,
363
+ pricingCategoryId: unitPrice.pricingCategoryId ?? null,
364
+ sortOrder: unitPrice.sortOrder,
365
+ tiers: (tiersByUnitPriceRule.get(unitPrice.id) ?? []).map((tier) => ({
366
+ id: tier.id,
367
+ minQuantity: tier.minQuantity,
368
+ maxQuantity: tier.maxQuantity ?? null,
369
+ sellAmountCents: tier.sellAmountCents ?? null,
370
+ sortOrder: tier.sortOrder,
371
+ })),
372
+ };
373
+ })
374
+ .filter((value) => value !== null),
375
+ startTimeAdjustments: (startTimeAdjustmentsByRule.get(rule.id) ?? []).map((adjustment) => ({
376
+ id: adjustment.id,
377
+ startTimeId: adjustment.start_time_id,
378
+ label: adjustment.label ?? null,
379
+ startTimeLocal: adjustment.start_time_local,
380
+ ruleMode: adjustment.rule_mode,
381
+ adjustmentType: adjustment.adjustment_type ?? null,
382
+ sellAdjustmentCents: adjustment.sell_adjustment_cents ?? null,
383
+ adjustmentBasisPoints: adjustment.adjustment_basis_points ?? null,
384
+ })),
385
+ })),
386
+ })),
387
+ };
388
+ },
389
+ async getAvailabilitySnapshot(db, productId, query) {
390
+ const product = await ensurePublicProduct(db, productId);
391
+ if (!product) {
392
+ return null;
393
+ }
394
+ const conditions = [sql `slot.product_id = ${productId}`, sql `slot.status::text <> 'cancelled'`];
395
+ if (query.optionId) {
396
+ conditions.push(sql `slot.option_id = ${query.optionId}`);
397
+ }
398
+ if (query.dateFrom) {
399
+ conditions.push(sql `slot.date_local >= ${query.dateFrom}`);
400
+ }
401
+ if (query.dateTo) {
402
+ conditions.push(sql `slot.date_local <= ${query.dateTo}`);
403
+ }
404
+ if (query.status) {
405
+ conditions.push(sql `slot.status::text = ${query.status}`);
406
+ }
407
+ else {
408
+ conditions.push(sql `slot.status::text IN ('open', 'sold_out')`);
409
+ }
410
+ const where = andSql(conditions);
411
+ const [rows, countResult] = await Promise.all([
412
+ executeRows(db,
413
+ // agent-quality: raw-sql reviewed -- owner: pricing; cross-module Availability public snapshot read with parameter-bound filters.
414
+ sql `
415
+ SELECT
416
+ slot.id,
417
+ slot.option_id,
418
+ slot.date_local,
419
+ slot.starts_at,
420
+ slot.ends_at,
421
+ slot.timezone,
422
+ slot.status::text AS status,
423
+ slot.unlimited,
424
+ slot.remaining_pax,
425
+ slot.remaining_resources,
426
+ slot.past_cutoff,
427
+ slot.too_early,
428
+ start_time.id AS start_time_id,
429
+ start_time.label AS start_time_label,
430
+ start_time.start_time_local,
431
+ start_time.duration_minutes
432
+ FROM availability_slots slot
433
+ LEFT JOIN availability_start_times start_time
434
+ ON start_time.id = slot.start_time_id
435
+ WHERE ${where}
436
+ ORDER BY slot.starts_at ASC
437
+ LIMIT ${query.limit}
438
+ OFFSET ${query.offset}
439
+ `),
440
+ executeRows(db,
441
+ // agent-quality: raw-sql reviewed -- owner: pricing; count query uses the same parameter-bound Availability filters as the page query.
442
+ sql `SELECT count(*)::int AS count FROM availability_slots slot WHERE ${where}`),
443
+ ]);
444
+ return {
445
+ productId,
446
+ slots: rows.map((row) => ({
447
+ id: row.id,
448
+ optionId: row.option_id ?? null,
449
+ dateLocal: normalizeDateOnly(row.date_local),
450
+ startsAt: normalizeDate(row.starts_at),
451
+ endsAt: normalizeDate(row.ends_at),
452
+ timezone: row.timezone,
453
+ status: row.status,
454
+ unlimited: row.unlimited,
455
+ remainingPax: row.remaining_pax ?? null,
456
+ remainingResources: row.remaining_resources ?? null,
457
+ pastCutoff: row.past_cutoff,
458
+ tooEarly: row.too_early,
459
+ startTime: row.start_time_id
460
+ ? {
461
+ id: row.start_time_id,
462
+ label: row.start_time_label ?? null,
463
+ startTimeLocal: row.start_time_local ?? "",
464
+ durationMinutes: row.duration_minutes ?? null,
465
+ }
466
+ : null,
467
+ })),
468
+ total: readCount(countResult[0]),
469
+ limit: query.limit,
470
+ offset: query.offset,
471
+ };
472
+ },
473
+ };
@@ -0,0 +1,67 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ export interface ResolverRuleInput {
3
+ id: string;
4
+ name: string;
5
+ isDefault: boolean;
6
+ priceScheduleId: string | null;
7
+ }
8
+ export interface ResolverScheduleInput {
9
+ id: string;
10
+ active: boolean;
11
+ priority: number;
12
+ recurrenceRule: string;
13
+ validFrom: string | null;
14
+ validTo: string | null;
15
+ weekdays: string[] | null;
16
+ timezone: string | null;
17
+ }
18
+ /**
19
+ * Pick the option price rule that applies to a given date, given a set of
20
+ * candidate rules and their schedules.
21
+ *
22
+ * - Rules with a matching schedule beat rules without one.
23
+ * - Among scheduled matches, highest `priority` wins. Ties: `isDefault` first,
24
+ * then alphabetic by `name`.
25
+ * - When no schedule matches, a rule with `isDefault=true` and no schedule acts
26
+ * as the fallback. Without one, returns an empty array.
27
+ */
28
+ export declare function pickRulesForDate(rules: ResolverRuleInput[], schedules: Map<string, ResolverScheduleInput>, isoDate: string): ResolverRuleInput[];
29
+ export interface ResolveOptionPriceRulesParams {
30
+ productId: string;
31
+ optionIds: string[];
32
+ catalogId: string;
33
+ date: string;
34
+ }
35
+ /**
36
+ * DB-backed wrapper around `pickRulesForDate`. Fetches active rules for the
37
+ * product/option/catalog plus their schedules, then picks the winning rule
38
+ * per option for the given date.
39
+ *
40
+ * Returns a Map keyed by optionId. Options whose rules don't match the date
41
+ * (and have no default) are absent from the map.
42
+ */
43
+ export declare function resolveOptionPriceRulesForDate(db: PostgresJsDatabase, params: ResolveOptionPriceRulesParams): Promise<Map<string, ResolverRuleInput>>;
44
+ /**
45
+ * Per-departure price override applied to a specific unit, keyed by unitId.
46
+ * Resolved AFTER rule selection: the rule's per-unit price gets replaced with
47
+ * the override's amount for any matching unit. Units without an override fall
48
+ * through to the rule's normal price.
49
+ */
50
+ export interface UnitPriceOverride {
51
+ id: string;
52
+ unitId: string;
53
+ sellAmountCents: number;
54
+ costAmountCents: number | null;
55
+ }
56
+ /**
57
+ * Fetch active per-unit price overrides for a given departure + catalog.
58
+ *
59
+ * Returns a Map keyed by `optionUnitId` so callers can apply overrides while
60
+ * iterating per-unit prices in a snapshot. Inactive overrides are excluded at
61
+ * query time.
62
+ */
63
+ export declare function loadDeparturePriceOverrides(db: PostgresJsDatabase, params: {
64
+ departureId: string;
65
+ catalogId: string;
66
+ }): Promise<Map<string, UnitPriceOverride>>;
67
+ //# sourceMappingURL=service-rule-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-rule-resolver.d.ts","sourceRoot":"","sources":["../../src/pricing/service-rule-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAmBjE,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,OAAO,CAAA;IAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAoED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,iBAAiB,EAAE,EAC1B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,EAC7C,OAAO,EAAE,MAAM,GACd,iBAAiB,EAAE,CAgCrB;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;GAOG;AACH,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CA8EzC;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACjD,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CA4BzC"}