@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.
- package/LICENSE +201 -0
- package/README.md +145 -0
- package/dist/accepted-quote-version-reservation-golden-flow.test.d.ts +2 -0
- package/dist/accepted-quote-version-reservation-golden-flow.test.d.ts.map +1 -0
- package/dist/accepted-quote-version-reservation-golden-flow.test.js +398 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/interface.d.ts +18 -0
- package/dist/interface.d.ts.map +1 -0
- package/dist/interface.js +246 -0
- package/dist/interface.test.d.ts +2 -0
- package/dist/interface.test.d.ts.map +1 -0
- package/dist/interface.test.js +357 -0
- package/dist/markets/index.d.ts +11 -0
- package/dist/markets/index.d.ts.map +1 -0
- package/dist/markets/index.js +12 -0
- package/dist/markets/routes.d.ts +1182 -0
- package/dist/markets/routes.d.ts.map +1 -0
- package/dist/markets/routes.js +209 -0
- package/dist/markets/schema.d.ts +1527 -0
- package/dist/markets/schema.d.ts.map +1 -0
- package/dist/markets/schema.js +240 -0
- package/dist/markets/service-core.d.ts +253 -0
- package/dist/markets/service-core.d.ts.map +1 -0
- package/dist/markets/service-core.js +242 -0
- package/dist/markets/service-rules.d.ts +191 -0
- package/dist/markets/service-rules.d.ts.map +1 -0
- package/dist/markets/service-rules.js +155 -0
- package/dist/markets/service-shared.d.ts +36 -0
- package/dist/markets/service-shared.d.ts.map +1 -0
- package/dist/markets/service-shared.js +7 -0
- package/dist/markets/service.d.ts +43 -0
- package/dist/markets/service.d.ts.map +1 -0
- package/dist/markets/service.js +42 -0
- package/dist/markets/validation.d.ts +451 -0
- package/dist/markets/validation.d.ts.map +1 -0
- package/dist/markets/validation.js +160 -0
- package/dist/pricing/events.d.ts +53 -0
- package/dist/pricing/events.d.ts.map +1 -0
- package/dist/pricing/events.js +28 -0
- package/dist/pricing/index.d.ts +15 -0
- package/dist/pricing/index.d.ts.map +1 -0
- package/dist/pricing/index.js +18 -0
- package/dist/pricing/routes-core.d.ts +981 -0
- package/dist/pricing/routes-core.d.ts.map +1 -0
- package/dist/pricing/routes-core.js +102 -0
- package/dist/pricing/routes-public.d.ts +136 -0
- package/dist/pricing/routes-public.d.ts.map +1 -0
- package/dist/pricing/routes-public.js +14 -0
- package/dist/pricing/routes-rules.d.ts +1339 -0
- package/dist/pricing/routes-rules.d.ts.map +1 -0
- package/dist/pricing/routes-rules.js +138 -0
- package/dist/pricing/routes-shared.d.ts +14 -0
- package/dist/pricing/routes-shared.d.ts.map +1 -0
- package/dist/pricing/routes-shared.js +3 -0
- package/dist/pricing/routes.d.ts +7 -0
- package/dist/pricing/routes.d.ts.map +1 -0
- package/dist/pricing/routes.js +6 -0
- package/dist/pricing/schema-catalogs.d.ts +467 -0
- package/dist/pricing/schema-catalogs.d.ts.map +1 -0
- package/dist/pricing/schema-catalogs.js +47 -0
- package/dist/pricing/schema-categories.d.ts +497 -0
- package/dist/pricing/schema-categories.d.ts.map +1 -0
- package/dist/pricing/schema-categories.js +54 -0
- package/dist/pricing/schema-departure-overrides.d.ts +228 -0
- package/dist/pricing/schema-departure-overrides.d.ts.map +1 -0
- package/dist/pricing/schema-departure-overrides.js +36 -0
- package/dist/pricing/schema-option-rules.d.ts +1770 -0
- package/dist/pricing/schema-option-rules.d.ts.map +1 -0
- package/dist/pricing/schema-option-rules.js +181 -0
- package/dist/pricing/schema-policies.d.ts +395 -0
- package/dist/pricing/schema-policies.d.ts.map +1 -0
- package/dist/pricing/schema-policies.js +41 -0
- package/dist/pricing/schema-relations.d.ts +59 -0
- package/dist/pricing/schema-relations.d.ts.map +1 -0
- package/dist/pricing/schema-relations.js +111 -0
- package/dist/pricing/schema-shared.d.ts +11 -0
- package/dist/pricing/schema-shared.d.ts.map +1 -0
- package/dist/pricing/schema-shared.js +67 -0
- package/dist/pricing/schema.d.ts +8 -0
- package/dist/pricing/schema.d.ts.map +1 -0
- package/dist/pricing/schema.js +7 -0
- package/dist/pricing/service-catalog-plane-pricing.d.ts +95 -0
- package/dist/pricing/service-catalog-plane-pricing.d.ts.map +1 -0
- package/dist/pricing/service-catalog-plane-pricing.js +382 -0
- package/dist/pricing/service-catalogs.d.ts +139 -0
- package/dist/pricing/service-catalogs.d.ts.map +1 -0
- package/dist/pricing/service-catalogs.js +89 -0
- package/dist/pricing/service-categories.d.ts +147 -0
- package/dist/pricing/service-categories.d.ts.map +1 -0
- package/dist/pricing/service-categories.js +105 -0
- package/dist/pricing/service-departure-overrides.d.ts +67 -0
- package/dist/pricing/service-departure-overrides.d.ts.map +1 -0
- package/dist/pricing/service-departure-overrides.js +54 -0
- package/dist/pricing/service-option-rules.d.ts +321 -0
- package/dist/pricing/service-option-rules.d.ts.map +1 -0
- package/dist/pricing/service-option-rules.js +340 -0
- package/dist/pricing/service-policies.d.ts +123 -0
- package/dist/pricing/service-policies.d.ts.map +1 -0
- package/dist/pricing/service-policies.js +95 -0
- package/dist/pricing/service-public.d.ts +89 -0
- package/dist/pricing/service-public.d.ts.map +1 -0
- package/dist/pricing/service-public.js +473 -0
- package/dist/pricing/service-rule-resolver.d.ts +67 -0
- package/dist/pricing/service-rule-resolver.d.ts.map +1 -0
- package/dist/pricing/service-rule-resolver.js +204 -0
- package/dist/pricing/service-shared.d.ts +53 -0
- package/dist/pricing/service-shared.d.ts.map +1 -0
- package/dist/pricing/service-shared.js +4 -0
- package/dist/pricing/service-transfer-rules.d.ts +211 -0
- package/dist/pricing/service-transfer-rules.d.ts.map +1 -0
- package/dist/pricing/service-transfer-rules.js +139 -0
- package/dist/pricing/service.d.ts +79 -0
- package/dist/pricing/service.d.ts.map +1 -0
- package/dist/pricing/service.js +78 -0
- package/dist/pricing/validation-public.d.ts +412 -0
- package/dist/pricing/validation-public.d.ts.map +1 -0
- package/dist/pricing/validation-public.js +111 -0
- package/dist/pricing/validation-shared.d.ts +71 -0
- package/dist/pricing/validation-shared.d.ts.map +1 -0
- package/dist/pricing/validation-shared.js +63 -0
- package/dist/pricing/validation.d.ts +987 -0
- package/dist/pricing/validation.d.ts.map +1 -0
- package/dist/pricing/validation.js +307 -0
- package/dist/promotions/events.d.ts +38 -0
- package/dist/promotions/events.d.ts.map +1 -0
- package/dist/promotions/events.js +25 -0
- package/dist/promotions/index.d.ts +12 -0
- package/dist/promotions/index.d.ts.map +1 -0
- package/dist/promotions/index.js +17 -0
- package/dist/promotions/routes-shared.d.ts +14 -0
- package/dist/promotions/routes-shared.d.ts.map +1 -0
- package/dist/promotions/routes-shared.js +3 -0
- package/dist/promotions/routes.d.ts +395 -0
- package/dist/promotions/routes.d.ts.map +1 -0
- package/dist/promotions/routes.js +55 -0
- package/dist/promotions/schema.d.ts +675 -0
- package/dist/promotions/schema.d.ts.map +1 -0
- package/dist/promotions/schema.js +126 -0
- package/dist/promotions/service-booking-confirmed.d.ts +77 -0
- package/dist/promotions/service-booking-confirmed.d.ts.map +1 -0
- package/dist/promotions/service-booking-confirmed.js +134 -0
- package/dist/promotions/service-boundary-scheduler.d.ts +85 -0
- package/dist/promotions/service-boundary-scheduler.d.ts.map +1 -0
- package/dist/promotions/service-boundary-scheduler.js +141 -0
- package/dist/promotions/service-catalog-evaluator.d.ts +22 -0
- package/dist/promotions/service-catalog-evaluator.d.ts.map +1 -0
- package/dist/promotions/service-catalog-evaluator.js +33 -0
- package/dist/promotions/service-catalog-plane-promotions.d.ts +73 -0
- package/dist/promotions/service-catalog-plane-promotions.d.ts.map +1 -0
- package/dist/promotions/service-catalog-plane-promotions.js +118 -0
- package/dist/promotions/service-evaluator.d.ts +134 -0
- package/dist/promotions/service-evaluator.d.ts.map +1 -0
- package/dist/promotions/service-evaluator.js +302 -0
- package/dist/promotions/service-storefront.d.ts +147 -0
- package/dist/promotions/service-storefront.d.ts.map +1 -0
- package/dist/promotions/service-storefront.js +326 -0
- package/dist/promotions/service.d.ts +143 -0
- package/dist/promotions/service.d.ts.map +1 -0
- package/dist/promotions/service.js +359 -0
- package/dist/promotions/validation.d.ts +195 -0
- package/dist/promotions/validation.d.ts.map +1 -0
- package/dist/promotions/validation.js +167 -0
- package/dist/promotions/workflow-bulk-reindex.d.ts +36 -0
- package/dist/promotions/workflow-bulk-reindex.d.ts.map +1 -0
- package/dist/promotions/workflow-bulk-reindex.js +53 -0
- package/dist/promotions/workflow-runtime.d.ts +17 -0
- package/dist/promotions/workflow-runtime.d.ts.map +1 -0
- package/dist/promotions/workflow-runtime.js +9 -0
- package/dist/runtime.d.ts +18 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +27 -0
- package/dist/runtime.test.d.ts +2 -0
- package/dist/runtime.test.d.ts.map +1 -0
- package/dist/runtime.test.js +25 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +4 -0
- package/dist/sellability/index.d.ts +13 -0
- package/dist/sellability/index.d.ts.map +1 -0
- package/dist/sellability/index.js +17 -0
- package/dist/sellability/routes.d.ts +2332 -0
- package/dist/sellability/routes.d.ts.map +1 -0
- package/dist/sellability/routes.js +166 -0
- package/dist/sellability/schema.d.ts +1716 -0
- package/dist/sellability/schema.d.ts.map +1 -0
- package/dist/sellability/schema.js +278 -0
- package/dist/sellability/service-records.d.ts +316 -0
- package/dist/sellability/service-records.d.ts.map +1 -0
- package/dist/sellability/service-records.js +253 -0
- package/dist/sellability/service-resolve.d.ts +72 -0
- package/dist/sellability/service-resolve.d.ts.map +1 -0
- package/dist/sellability/service-resolve.js +580 -0
- package/dist/sellability/service-shared.d.ts +124 -0
- package/dist/sellability/service-shared.d.ts.map +1 -0
- package/dist/sellability/service-shared.js +96 -0
- package/dist/sellability/service-snapshots.d.ts +191 -0
- package/dist/sellability/service-snapshots.d.ts.map +1 -0
- package/dist/sellability/service-snapshots.js +153 -0
- package/dist/sellability/service.d.ts +1038 -0
- package/dist/sellability/service.d.ts.map +1 -0
- package/dist/sellability/service.js +17 -0
- package/dist/sellability/validation.d.ts +477 -0
- package/dist/sellability/validation.d.ts.map +1 -0
- package/dist/sellability/validation.js +192 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- 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"}
|