@voyant-travel/storefront 0.120.1
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 +231 -0
- package/dist/booking-intents.d.ts +42 -0
- package/dist/booking-intents.d.ts.map +1 -0
- package/dist/booking-intents.js +83 -0
- package/dist/customer-portal/index.d.ts +16 -0
- package/dist/customer-portal/index.d.ts.map +1 -0
- package/dist/customer-portal/index.js +23 -0
- package/dist/customer-portal/route-runtime.d.ts +16 -0
- package/dist/customer-portal/route-runtime.d.ts.map +1 -0
- package/dist/customer-portal/route-runtime.js +27 -0
- package/dist/customer-portal/routes-public.d.ts +1936 -0
- package/dist/customer-portal/routes-public.d.ts.map +1 -0
- package/dist/customer-portal/routes-public.js +165 -0
- package/dist/customer-portal/routes.d.ts +43 -0
- package/dist/customer-portal/routes.d.ts.map +1 -0
- package/dist/customer-portal/routes.js +17 -0
- package/dist/customer-portal/service-public-impl.d.ts +138 -0
- package/dist/customer-portal/service-public-impl.d.ts.map +1 -0
- package/dist/customer-portal/service-public-impl.js +1808 -0
- package/dist/customer-portal/service-public.d.ts +2 -0
- package/dist/customer-portal/service-public.d.ts.map +1 -0
- package/dist/customer-portal/service-public.js +1 -0
- package/dist/customer-portal/validation-public/bookings.d.ts +551 -0
- package/dist/customer-portal/validation-public/bookings.d.ts.map +1 -0
- package/dist/customer-portal/validation-public/bookings.js +132 -0
- package/dist/customer-portal/validation-public/common.d.ts +162 -0
- package/dist/customer-portal/validation-public/common.d.ts.map +1 -0
- package/dist/customer-portal/validation-public/common.js +139 -0
- package/dist/customer-portal/validation-public/profile.d.ts +749 -0
- package/dist/customer-portal/validation-public/profile.d.ts.map +1 -0
- package/dist/customer-portal/validation-public/profile.js +308 -0
- package/dist/customer-portal/validation-public.d.ts +3 -0
- package/dist/customer-portal/validation-public.d.ts.map +1 -0
- package/dist/customer-portal/validation-public.js +2 -0
- package/dist/guest-booking-guard.d.ts +24 -0
- package/dist/guest-booking-guard.d.ts.map +1 -0
- package/dist/guest-booking-guard.js +55 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/product-extra-ref.d.ts +238 -0
- package/dist/product-extra-ref.d.ts.map +1 -0
- package/dist/product-extra-ref.js +22 -0
- package/dist/routes-admin.d.ts +220 -0
- package/dist/routes-admin.d.ts.map +1 -0
- package/dist/routes-admin.js +28 -0
- package/dist/routes-public.d.ts +1475 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +362 -0
- package/dist/service-booking-session-bootstrap.d.ts +227 -0
- package/dist/service-booking-session-bootstrap.d.ts.map +1 -0
- package/dist/service-booking-session-bootstrap.js +287 -0
- package/dist/service-boundary-resource-sql.d.ts +18 -0
- package/dist/service-boundary-resource-sql.d.ts.map +1 -0
- package/dist/service-boundary-resource-sql.js +73 -0
- package/dist/service-boundary-sql.d.ts +103 -0
- package/dist/service-boundary-sql.d.ts.map +1 -0
- package/dist/service-boundary-sql.js +307 -0
- package/dist/service-departures-core.d.ts +41 -0
- package/dist/service-departures-core.d.ts.map +1 -0
- package/dist/service-departures-core.js +92 -0
- package/dist/service-departures-extensions.d.ts +46 -0
- package/dist/service-departures-extensions.d.ts.map +1 -0
- package/dist/service-departures-extensions.js +81 -0
- package/dist/service-departures-offers.d.ts +220 -0
- package/dist/service-departures-offers.d.ts.map +1 -0
- package/dist/service-departures-offers.js +177 -0
- package/dist/service-departures-price-preview.d.ts +306 -0
- package/dist/service-departures-price-preview.d.ts.map +1 -0
- package/dist/service-departures-price-preview.js +383 -0
- package/dist/service-departures-pricing-context.d.ts +115 -0
- package/dist/service-departures-pricing-context.d.ts.map +1 -0
- package/dist/service-departures-pricing-context.js +237 -0
- package/dist/service-departures-pricing.d.ts +5 -0
- package/dist/service-departures-pricing.d.ts.map +1 -0
- package/dist/service-departures-pricing.js +4 -0
- package/dist/service-departures.d.ts +192 -0
- package/dist/service-departures.d.ts.map +1 -0
- package/dist/service-departures.js +213 -0
- package/dist/service-intake.d.ts +130 -0
- package/dist/service-intake.d.ts.map +1 -0
- package/dist/service-intake.js +274 -0
- package/dist/service-transport-eligibility.d.ts +10 -0
- package/dist/service-transport-eligibility.d.ts.map +1 -0
- package/dist/service-transport-eligibility.js +198 -0
- package/dist/service.d.ts +1062 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +332 -0
- package/dist/transport-eligibility.d.ts +4 -0
- package/dist/transport-eligibility.d.ts.map +1 -0
- package/dist/transport-eligibility.js +2 -0
- package/dist/validation/departures.d.ts +1669 -0
- package/dist/validation/departures.d.ts.map +1 -0
- package/dist/validation/departures.js +397 -0
- package/dist/validation/intake.d.ts +147 -0
- package/dist/validation/intake.d.ts.map +1 -0
- package/dist/validation/intake.js +69 -0
- package/dist/validation/offers.d.ts +340 -0
- package/dist/validation/offers.d.ts.map +1 -0
- package/dist/validation/offers.js +117 -0
- package/dist/validation-settings.d.ts +609 -0
- package/dist/validation-settings.d.ts.map +1 -0
- package/dist/validation-settings.js +235 -0
- package/dist/validation-transport-eligibility.d.ts +314 -0
- package/dist/validation-transport-eligibility.d.ts.map +1 -0
- package/dist/validation-transport-eligibility.js +97 -0
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +4 -0
- package/dist/verification/index.d.ts +12 -0
- package/dist/verification/index.d.ts.map +1 -0
- package/dist/verification/index.js +18 -0
- package/dist/verification/routes-public.d.ts +121 -0
- package/dist/verification/routes-public.d.ts.map +1 -0
- package/dist/verification/routes-public.js +125 -0
- package/dist/verification/schema.d.ts +273 -0
- package/dist/verification/schema.d.ts.map +1 -0
- package/dist/verification/schema.js +50 -0
- package/dist/verification/service.d.ts +114 -0
- package/dist/verification/service.d.ts.map +1 -0
- package/dist/verification/service.js +283 -0
- package/dist/verification/validation.d.ts +98 -0
- package/dist/verification/validation.d.ts.map +1 -0
- package/dist/verification/validation.js +54 -0
- package/package.json +148 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
export { getStorefrontSlotResourceAvailability, getStorefrontSlotsResourceAvailability, } from "./service-boundary-resource-sql.js";
|
|
3
|
+
function isRowsResult(value) {
|
|
4
|
+
return (typeof value === "object" && value !== null && Array.isArray(value.rows));
|
|
5
|
+
}
|
|
6
|
+
async function executeBoundaryRows(db, query) {
|
|
7
|
+
const result = await db.execute(query);
|
|
8
|
+
return (Array.isArray(result) ? result : isRowsResult(result) ? result.rows : []);
|
|
9
|
+
}
|
|
10
|
+
function sqlList(values) {
|
|
11
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; callers pass only parameter-bound scalar ids into this SQL fragment.
|
|
12
|
+
return sql.join(values.map((value) => sql `${value}`), sql `, `);
|
|
13
|
+
}
|
|
14
|
+
function sqlAnd(conditions) {
|
|
15
|
+
return sql.join([...conditions], sql ` AND `);
|
|
16
|
+
}
|
|
17
|
+
function toNumber(value) {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return null;
|
|
20
|
+
return typeof value === "number" ? value : Number(value);
|
|
21
|
+
}
|
|
22
|
+
function mapSlot(row) {
|
|
23
|
+
return {
|
|
24
|
+
id: row.id,
|
|
25
|
+
productId: row.product_id,
|
|
26
|
+
itineraryId: row.itinerary_id,
|
|
27
|
+
optionId: row.option_id,
|
|
28
|
+
startTimeId: row.start_time_id,
|
|
29
|
+
dateLocal: row.date_local,
|
|
30
|
+
startsAt: row.starts_at,
|
|
31
|
+
endsAt: row.ends_at,
|
|
32
|
+
timezone: row.timezone,
|
|
33
|
+
status: row.status,
|
|
34
|
+
unlimited: row.unlimited,
|
|
35
|
+
initialPax: toNumber(row.initial_pax),
|
|
36
|
+
remainingPax: toNumber(row.remaining_pax),
|
|
37
|
+
remainingResources: toNumber(row.remaining_resources),
|
|
38
|
+
pastCutoff: row.past_cutoff,
|
|
39
|
+
tooEarly: row.too_early,
|
|
40
|
+
nights: toNumber(row.nights),
|
|
41
|
+
days: toNumber(row.days),
|
|
42
|
+
startTimeLabel: row.start_time_label,
|
|
43
|
+
startTimeLocal: row.start_time_local,
|
|
44
|
+
durationMinutes: toNumber(row.duration_minutes),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function buildSlotFilters(filters) {
|
|
48
|
+
const conditions = [
|
|
49
|
+
sql `p.status = 'active'`,
|
|
50
|
+
sql `p.activated = true`,
|
|
51
|
+
sql `p.visibility = 'public'`,
|
|
52
|
+
];
|
|
53
|
+
if (filters.productId) {
|
|
54
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; product id is parameter-bound in the public slot filter.
|
|
55
|
+
conditions.push(sql `s.product_id = ${filters.productId}`);
|
|
56
|
+
}
|
|
57
|
+
if (filters.slotId) {
|
|
58
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; slot id is parameter-bound in the public slot filter.
|
|
59
|
+
conditions.push(sql `s.id = ${filters.slotId}`);
|
|
60
|
+
}
|
|
61
|
+
if (filters.optionId) {
|
|
62
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; option id is parameter-bound in the public slot filter.
|
|
63
|
+
conditions.push(sql `s.option_id = ${filters.optionId}`);
|
|
64
|
+
}
|
|
65
|
+
if (filters.status) {
|
|
66
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; slot status is validated by Storefront before binding.
|
|
67
|
+
conditions.push(sql `s.status = ${filters.status}`);
|
|
68
|
+
}
|
|
69
|
+
else if (!filters.includeCancelled) {
|
|
70
|
+
conditions.push(sql `s.status <> 'cancelled'`);
|
|
71
|
+
}
|
|
72
|
+
if (filters.dateFrom) {
|
|
73
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; dateFrom is schema-validated and parameter-bound.
|
|
74
|
+
conditions.push(sql `s.date_local >= ${filters.dateFrom}`);
|
|
75
|
+
}
|
|
76
|
+
if (filters.dateTo) {
|
|
77
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; dateTo is schema-validated and parameter-bound.
|
|
78
|
+
conditions.push(sql `s.date_local <= ${filters.dateTo}`);
|
|
79
|
+
}
|
|
80
|
+
return conditions;
|
|
81
|
+
}
|
|
82
|
+
export async function listStorefrontSlots(db, filters = {}) {
|
|
83
|
+
const rows = await executeBoundaryRows(db,
|
|
84
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; public slot reads are filtered by parameter-bound user inputs and public Product flags.
|
|
85
|
+
sql `
|
|
86
|
+
SELECT
|
|
87
|
+
s.id,
|
|
88
|
+
s.product_id,
|
|
89
|
+
s.itinerary_id,
|
|
90
|
+
s.option_id,
|
|
91
|
+
s.start_time_id,
|
|
92
|
+
s.date_local,
|
|
93
|
+
s.starts_at,
|
|
94
|
+
s.ends_at,
|
|
95
|
+
s.timezone,
|
|
96
|
+
s.status,
|
|
97
|
+
s.unlimited,
|
|
98
|
+
s.initial_pax,
|
|
99
|
+
s.remaining_pax,
|
|
100
|
+
s.remaining_resources,
|
|
101
|
+
s.past_cutoff,
|
|
102
|
+
s.too_early,
|
|
103
|
+
s.nights,
|
|
104
|
+
s.days,
|
|
105
|
+
st.label AS start_time_label,
|
|
106
|
+
st.start_time_local,
|
|
107
|
+
st.duration_minutes
|
|
108
|
+
FROM availability_slots s
|
|
109
|
+
JOIN products p ON p.id = s.product_id
|
|
110
|
+
LEFT JOIN availability_start_times st ON st.id = s.start_time_id
|
|
111
|
+
WHERE ${sqlAnd(buildSlotFilters(filters))}
|
|
112
|
+
ORDER BY s.starts_at ASC
|
|
113
|
+
LIMIT ${filters.limit ?? 100}
|
|
114
|
+
OFFSET ${filters.offset ?? 0}
|
|
115
|
+
`);
|
|
116
|
+
return rows.map(mapSlot);
|
|
117
|
+
}
|
|
118
|
+
export async function countStorefrontSlots(db, filters = {}) {
|
|
119
|
+
const rows = await executeBoundaryRows(db,
|
|
120
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; count uses the same public slot predicates as listStorefrontSlots.
|
|
121
|
+
sql `
|
|
122
|
+
SELECT COUNT(*)::int AS value
|
|
123
|
+
FROM availability_slots s
|
|
124
|
+
JOIN products p ON p.id = s.product_id
|
|
125
|
+
WHERE ${sqlAnd(buildSlotFilters(filters))}
|
|
126
|
+
`);
|
|
127
|
+
return Number(rows[0]?.value ?? 0);
|
|
128
|
+
}
|
|
129
|
+
export async function loadStorefrontAvailabilitySlot(db, slotId) {
|
|
130
|
+
const rows = await executeBoundaryRows(db,
|
|
131
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; bootstrap validates a specific parameter-bound Availability slot id.
|
|
132
|
+
sql `
|
|
133
|
+
SELECT
|
|
134
|
+
s.id,
|
|
135
|
+
s.product_id,
|
|
136
|
+
s.itinerary_id,
|
|
137
|
+
s.option_id,
|
|
138
|
+
s.start_time_id,
|
|
139
|
+
s.date_local,
|
|
140
|
+
s.starts_at,
|
|
141
|
+
s.ends_at,
|
|
142
|
+
s.timezone,
|
|
143
|
+
s.status,
|
|
144
|
+
s.unlimited,
|
|
145
|
+
s.initial_pax,
|
|
146
|
+
s.remaining_pax,
|
|
147
|
+
s.remaining_resources,
|
|
148
|
+
s.past_cutoff,
|
|
149
|
+
s.too_early,
|
|
150
|
+
s.nights,
|
|
151
|
+
s.days,
|
|
152
|
+
st.label AS start_time_label,
|
|
153
|
+
st.start_time_local,
|
|
154
|
+
st.duration_minutes
|
|
155
|
+
FROM availability_slots s
|
|
156
|
+
LEFT JOIN availability_start_times st ON st.id = s.start_time_id
|
|
157
|
+
WHERE s.id = ${slotId}
|
|
158
|
+
LIMIT 1
|
|
159
|
+
`);
|
|
160
|
+
return rows[0] ? mapSlot(rows[0]) : null;
|
|
161
|
+
}
|
|
162
|
+
export async function listMeetingPointsByProductIds(db, productIds) {
|
|
163
|
+
const uniqueIds = [...new Set(productIds)].filter(Boolean);
|
|
164
|
+
if (uniqueIds.length === 0)
|
|
165
|
+
return new Map();
|
|
166
|
+
const rows = await executeBoundaryRows(db,
|
|
167
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; Product location ids are parameter-bound and read-only for public meeting points.
|
|
168
|
+
sql `
|
|
169
|
+
SELECT product_id, title
|
|
170
|
+
FROM product_locations
|
|
171
|
+
WHERE product_id IN (${sqlList(uniqueIds)})
|
|
172
|
+
ORDER BY location_type ASC, sort_order ASC, created_at ASC
|
|
173
|
+
`);
|
|
174
|
+
const byProduct = new Map();
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
if (!byProduct.has(row.product_id)) {
|
|
177
|
+
byProduct.set(row.product_id, row.title);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return byProduct;
|
|
181
|
+
}
|
|
182
|
+
export async function listDefaultItineraryIdsByProductIds(db, productIds) {
|
|
183
|
+
const uniqueIds = [...new Set(productIds)].filter(Boolean);
|
|
184
|
+
if (uniqueIds.length === 0)
|
|
185
|
+
return new Map();
|
|
186
|
+
const rows = await executeBoundaryRows(db,
|
|
187
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; Product itinerary ids are parameter-bound and read-only.
|
|
188
|
+
sql `
|
|
189
|
+
SELECT product_id, id AS itinerary_id
|
|
190
|
+
FROM product_itineraries
|
|
191
|
+
WHERE product_id IN (${sqlList(uniqueIds)})
|
|
192
|
+
AND is_default = true
|
|
193
|
+
ORDER BY sort_order ASC, created_at ASC
|
|
194
|
+
`);
|
|
195
|
+
return new Map(rows.map((row) => [row.product_id, row.itinerary_id]));
|
|
196
|
+
}
|
|
197
|
+
export async function loadProductPricingFacts(db, productId) {
|
|
198
|
+
const rows = await executeBoundaryRows(db,
|
|
199
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; Product pricing facts are read-only and product id is parameter-bound.
|
|
200
|
+
sql `
|
|
201
|
+
SELECT id, sell_currency, sell_amount_cents, capacity_mode
|
|
202
|
+
FROM products
|
|
203
|
+
WHERE id = ${productId}
|
|
204
|
+
LIMIT 1
|
|
205
|
+
`);
|
|
206
|
+
const row = rows[0];
|
|
207
|
+
return row
|
|
208
|
+
? {
|
|
209
|
+
id: row.id,
|
|
210
|
+
sellCurrency: row.sell_currency,
|
|
211
|
+
sellAmountCents: toNumber(row.sell_amount_cents),
|
|
212
|
+
capacityMode: row.capacity_mode,
|
|
213
|
+
}
|
|
214
|
+
: null;
|
|
215
|
+
}
|
|
216
|
+
export async function loadProductOptionFacts(db, input) {
|
|
217
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; optional Product option id is parameter-bound when present.
|
|
218
|
+
const optionFilter = input.optionId ? sql `AND id = ${input.optionId}` : sql ``;
|
|
219
|
+
const rows = await executeBoundaryRows(db,
|
|
220
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; Product option facts are read-only and filters are parameter-bound.
|
|
221
|
+
sql `
|
|
222
|
+
SELECT id, name, description
|
|
223
|
+
FROM product_options
|
|
224
|
+
WHERE product_id = ${input.productId}
|
|
225
|
+
AND status = 'active'
|
|
226
|
+
${optionFilter}
|
|
227
|
+
ORDER BY is_default DESC, sort_order ASC, name ASC
|
|
228
|
+
LIMIT 1
|
|
229
|
+
`);
|
|
230
|
+
return rows[0] ?? null;
|
|
231
|
+
}
|
|
232
|
+
export async function listOptionUnitFacts(db, optionId) {
|
|
233
|
+
const rows = await executeBoundaryRows(db,
|
|
234
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; option unit facts are read-only and option id is parameter-bound.
|
|
235
|
+
sql `
|
|
236
|
+
SELECT
|
|
237
|
+
id,
|
|
238
|
+
name,
|
|
239
|
+
unit_type,
|
|
240
|
+
min_age,
|
|
241
|
+
max_age,
|
|
242
|
+
occupancy_min,
|
|
243
|
+
occupancy_max,
|
|
244
|
+
is_required
|
|
245
|
+
FROM option_units
|
|
246
|
+
WHERE option_id = ${optionId}
|
|
247
|
+
AND is_hidden = false
|
|
248
|
+
ORDER BY sort_order ASC, name ASC
|
|
249
|
+
`);
|
|
250
|
+
return rows.map((row) => ({
|
|
251
|
+
id: row.id,
|
|
252
|
+
name: row.name,
|
|
253
|
+
unitType: row.unit_type,
|
|
254
|
+
minAge: toNumber(row.min_age),
|
|
255
|
+
maxAge: toNumber(row.max_age),
|
|
256
|
+
occupancyMin: toNumber(row.occupancy_min),
|
|
257
|
+
occupancyMax: toNumber(row.occupancy_max),
|
|
258
|
+
isRequired: row.is_required,
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
export async function listItineraryDays(db, itineraryId) {
|
|
262
|
+
return executeBoundaryRows(db,
|
|
263
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; itinerary id is parameter-bound and rows are read-only.
|
|
264
|
+
sql `
|
|
265
|
+
SELECT id, day_number AS "dayNumber", title, description
|
|
266
|
+
FROM product_days
|
|
267
|
+
WHERE itinerary_id = ${itineraryId}
|
|
268
|
+
ORDER BY day_number ASC
|
|
269
|
+
`);
|
|
270
|
+
}
|
|
271
|
+
export async function listItineraryDayServices(db, dayIds) {
|
|
272
|
+
const uniqueIds = [...new Set(dayIds)].filter(Boolean);
|
|
273
|
+
if (uniqueIds.length === 0)
|
|
274
|
+
return [];
|
|
275
|
+
return executeBoundaryRows(db,
|
|
276
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; day ids are parameter-bound and service rows are read-only.
|
|
277
|
+
sql `
|
|
278
|
+
SELECT
|
|
279
|
+
id,
|
|
280
|
+
day_id AS "dayId",
|
|
281
|
+
name,
|
|
282
|
+
description,
|
|
283
|
+
sort_order AS "sortOrder"
|
|
284
|
+
FROM product_day_services
|
|
285
|
+
WHERE day_id IN (${sqlList(uniqueIds)})
|
|
286
|
+
ORDER BY sort_order ASC, created_at ASC
|
|
287
|
+
`);
|
|
288
|
+
}
|
|
289
|
+
export async function listItineraryDayMedia(db, input) {
|
|
290
|
+
const uniqueDayIds = [...new Set(input.dayIds)].filter(Boolean);
|
|
291
|
+
if (uniqueDayIds.length === 0)
|
|
292
|
+
return [];
|
|
293
|
+
return executeBoundaryRows(db,
|
|
294
|
+
// agent-quality: raw-sql reviewed -- owner: storefront; product/day ids are parameter-bound and media rows are read-only.
|
|
295
|
+
sql `
|
|
296
|
+
SELECT
|
|
297
|
+
id,
|
|
298
|
+
day_id AS "dayId",
|
|
299
|
+
url,
|
|
300
|
+
is_cover AS "isCover",
|
|
301
|
+
sort_order AS "sortOrder"
|
|
302
|
+
FROM product_media
|
|
303
|
+
WHERE product_id = ${input.productId}
|
|
304
|
+
AND day_id IN (${sqlList(uniqueDayIds)})
|
|
305
|
+
ORDER BY is_cover DESC, sort_order ASC, created_at ASC
|
|
306
|
+
`);
|
|
307
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { countStorefrontSlots, listDefaultItineraryIdsByProductIds, listMeetingPointsByProductIds, listStorefrontSlots, type StorefrontSlotResourceAvailability, type StorefrontSlotRow, type StorefrontSlotStatus } from "./service-boundary-sql.js";
|
|
2
|
+
export type SlotResourceAvailability = StorefrontSlotResourceAvailability;
|
|
3
|
+
export type SlotRow = StorefrontSlotRow;
|
|
4
|
+
export type SlotStatus = StorefrontSlotStatus;
|
|
5
|
+
export { countStorefrontSlots as countSlots, listDefaultItineraryIdsByProductIds, listMeetingPointsByProductIds, listStorefrontSlots as listSlots, };
|
|
6
|
+
export declare function normalizeIso(value: Date | string | null | undefined): string | null;
|
|
7
|
+
export declare function normalizeLocalDate(value: Date | string | null | undefined): string | null;
|
|
8
|
+
export declare function buildResourceManifest(resources: SlotResourceAvailability[]): {
|
|
9
|
+
kinds: {
|
|
10
|
+
capacity: number;
|
|
11
|
+
assigned: number;
|
|
12
|
+
available: number;
|
|
13
|
+
kind: string;
|
|
14
|
+
}[];
|
|
15
|
+
resources: {
|
|
16
|
+
id: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
label: string | null;
|
|
19
|
+
refType: string | null;
|
|
20
|
+
refId: string | null;
|
|
21
|
+
capacity: number;
|
|
22
|
+
assigned: number;
|
|
23
|
+
available: number;
|
|
24
|
+
parentId: string | null;
|
|
25
|
+
flags: Record<string, unknown>;
|
|
26
|
+
}[];
|
|
27
|
+
} | null;
|
|
28
|
+
export type StorefrontProductAvailabilityState = "available" | "sold_out" | "closed" | "cancelled" | "on_request" | "past_cutoff" | "too_early" | "unavailable";
|
|
29
|
+
export declare function todayLocalDate(): string;
|
|
30
|
+
export declare function buildAvailabilityState(args: {
|
|
31
|
+
status: "open" | "closed" | "sold_out" | "cancelled" | "on_request";
|
|
32
|
+
remaining: number | null;
|
|
33
|
+
capacity: number | null;
|
|
34
|
+
pastCutoff: boolean;
|
|
35
|
+
tooEarly: boolean;
|
|
36
|
+
}): StorefrontProductAvailabilityState;
|
|
37
|
+
export declare function summarizeProductAvailability(departures: Array<{
|
|
38
|
+
availabilityState: StorefrontProductAvailabilityState;
|
|
39
|
+
status: "open" | "closed" | "sold_out" | "cancelled" | "on_request";
|
|
40
|
+
}>): StorefrontProductAvailabilityState;
|
|
41
|
+
//# sourceMappingURL=service-departures-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-departures-core.d.ts","sourceRoot":"","sources":["../src/service-departures-core.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,mCAAmC,EACnC,6BAA6B,EAC7B,mBAAmB,EACnB,KAAK,kCAAkC,EACvC,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAA;AAElC,MAAM,MAAM,wBAAwB,GAAG,kCAAkC,CAAA;AACzE,MAAM,MAAM,OAAO,GAAG,iBAAiB,CAAA;AACvC,MAAM,MAAM,UAAU,GAAG,oBAAoB,CAAA;AAC7C,OAAO,EACL,oBAAoB,IAAI,UAAU,EAClC,mCAAmC,EACnC,6BAA6B,EAC7B,mBAAmB,IAAI,SAAS,GACjC,CAAA;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,iBAWnE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,iBAUzE;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,wBAAwB,EAAE;;kBAE9B,MAAM;kBAAY,MAAM;mBAAa,MAAM;;;;;;;;;;;;;;;SAuBvF;AAED,MAAM,MAAM,kCAAkC,GAC1C,WAAW,GACX,UAAU,GACV,QAAQ,GACR,WAAW,GACX,YAAY,GACZ,aAAa,GACb,WAAW,GACX,aAAa,CAAA;AAEjB,wBAAgB,cAAc,WAE7B;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,YAAY,CAAA;IACnE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,OAAO,CAAA;CAClB,GAAG,kCAAkC,CAUrC;AAED,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,KAAK,CAAC;IAChB,iBAAiB,EAAE,kCAAkC,CAAA;IACrD,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,YAAY,CAAA;CACpE,CAAC,GACD,kCAAkC,CAwBpC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { countStorefrontSlots, listDefaultItineraryIdsByProductIds, listMeetingPointsByProductIds, listStorefrontSlots, } from "./service-boundary-sql.js";
|
|
2
|
+
export { countStorefrontSlots as countSlots, listDefaultItineraryIdsByProductIds, listMeetingPointsByProductIds, listStorefrontSlots as listSlots, };
|
|
3
|
+
export function normalizeIso(value) {
|
|
4
|
+
if (!value) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (value instanceof Date) {
|
|
8
|
+
return value.toISOString();
|
|
9
|
+
}
|
|
10
|
+
const parsed = new Date(value);
|
|
11
|
+
return Number.isNaN(parsed.getTime()) ? String(value) : parsed.toISOString();
|
|
12
|
+
}
|
|
13
|
+
export function normalizeLocalDate(value) {
|
|
14
|
+
if (!value) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (value instanceof Date) {
|
|
18
|
+
return value.toISOString().slice(0, 10);
|
|
19
|
+
}
|
|
20
|
+
return String(value).slice(0, 10);
|
|
21
|
+
}
|
|
22
|
+
export function buildResourceManifest(resources) {
|
|
23
|
+
if (resources.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
const totals = new Map();
|
|
26
|
+
for (const resource of resources) {
|
|
27
|
+
const bucket = totals.get(resource.kind) ?? { capacity: 0, assigned: 0, available: 0 };
|
|
28
|
+
bucket.capacity += resource.capacity;
|
|
29
|
+
bucket.assigned += resource.assigned;
|
|
30
|
+
bucket.available += resource.available;
|
|
31
|
+
totals.set(resource.kind, bucket);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
kinds: [...totals.entries()].map(([kind, totals]) => ({ kind, ...totals })),
|
|
35
|
+
resources: resources.map((resource) => ({
|
|
36
|
+
id: resource.id,
|
|
37
|
+
kind: resource.kind,
|
|
38
|
+
label: resource.label,
|
|
39
|
+
refType: resource.refType,
|
|
40
|
+
refId: resource.refId,
|
|
41
|
+
capacity: resource.capacity,
|
|
42
|
+
assigned: resource.assigned,
|
|
43
|
+
available: resource.available,
|
|
44
|
+
parentId: resource.parentId,
|
|
45
|
+
flags: resource.flags,
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function todayLocalDate() {
|
|
50
|
+
return new Date().toISOString().slice(0, 10);
|
|
51
|
+
}
|
|
52
|
+
export function buildAvailabilityState(args) {
|
|
53
|
+
if (args.status === "cancelled")
|
|
54
|
+
return "cancelled";
|
|
55
|
+
if (args.status === "closed")
|
|
56
|
+
return "closed";
|
|
57
|
+
if (args.status === "sold_out")
|
|
58
|
+
return "sold_out";
|
|
59
|
+
if (args.status === "on_request")
|
|
60
|
+
return "on_request";
|
|
61
|
+
if (args.pastCutoff)
|
|
62
|
+
return "past_cutoff";
|
|
63
|
+
if (args.tooEarly)
|
|
64
|
+
return "too_early";
|
|
65
|
+
if (args.capacity != null && args.remaining === 0)
|
|
66
|
+
return "sold_out";
|
|
67
|
+
return "available";
|
|
68
|
+
}
|
|
69
|
+
export function summarizeProductAvailability(departures) {
|
|
70
|
+
if (departures.some((departure) => departure.availabilityState === "available")) {
|
|
71
|
+
return "available";
|
|
72
|
+
}
|
|
73
|
+
if (departures.some((departure) => departure.availabilityState === "on_request")) {
|
|
74
|
+
return "on_request";
|
|
75
|
+
}
|
|
76
|
+
if (departures.some((departure) => departure.availabilityState === "too_early")) {
|
|
77
|
+
return "too_early";
|
|
78
|
+
}
|
|
79
|
+
if (departures.some((departure) => departure.availabilityState === "past_cutoff")) {
|
|
80
|
+
return "past_cutoff";
|
|
81
|
+
}
|
|
82
|
+
if (departures.some((departure) => departure.availabilityState === "sold_out")) {
|
|
83
|
+
return "sold_out";
|
|
84
|
+
}
|
|
85
|
+
if (departures.some((departure) => departure.availabilityState === "closed")) {
|
|
86
|
+
return "closed";
|
|
87
|
+
}
|
|
88
|
+
if (departures.some((departure) => departure.availabilityState === "cancelled")) {
|
|
89
|
+
return "cancelled";
|
|
90
|
+
}
|
|
91
|
+
return "unavailable";
|
|
92
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
export declare function getStorefrontProductExtensions(db: PostgresJsDatabase, productId: string, optionId?: string): Promise<{
|
|
3
|
+
extensions: {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
required: boolean;
|
|
8
|
+
selectable: boolean;
|
|
9
|
+
hasOptions: boolean;
|
|
10
|
+
refProductId: string | null;
|
|
11
|
+
thumb: string | null;
|
|
12
|
+
pricePerPerson: number | null;
|
|
13
|
+
currencyCode: string;
|
|
14
|
+
pricingMode: string;
|
|
15
|
+
defaultQuantity: number | null;
|
|
16
|
+
minQuantity: number | null;
|
|
17
|
+
maxQuantity: number | null;
|
|
18
|
+
}[];
|
|
19
|
+
items: {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
label: string;
|
|
23
|
+
required: boolean;
|
|
24
|
+
selectable: boolean;
|
|
25
|
+
hasOptions: boolean;
|
|
26
|
+
refProductId: string | null;
|
|
27
|
+
thumb: string | null;
|
|
28
|
+
pricePerPerson: number | null;
|
|
29
|
+
currencyCode: string;
|
|
30
|
+
pricingMode: string;
|
|
31
|
+
defaultQuantity: number | null;
|
|
32
|
+
minQuantity: number | null;
|
|
33
|
+
maxQuantity: number | null;
|
|
34
|
+
}[];
|
|
35
|
+
details: {
|
|
36
|
+
[k: string]: {
|
|
37
|
+
description: string | null;
|
|
38
|
+
media: {
|
|
39
|
+
url: string;
|
|
40
|
+
alt: string | null;
|
|
41
|
+
}[];
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
currencyCode: string;
|
|
45
|
+
}>;
|
|
46
|
+
//# sourceMappingURL=service-departures-extensions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-departures-extensions.d.ts","sourceRoot":"","sources":["../src/service-departures-extensions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AASjE,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGlB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { and, asc, eq } from "drizzle-orm";
|
|
2
|
+
import { productExtrasRef } from "./product-extra-ref.js";
|
|
3
|
+
import { centsToAmount, getPreferredCurrency, resolvePricingContext, } from "./service-departures-pricing-context.js";
|
|
4
|
+
export async function getStorefrontProductExtensions(db, productId, optionId) {
|
|
5
|
+
const context = await resolvePricingContext(db, productId, optionId);
|
|
6
|
+
const extras = await db
|
|
7
|
+
.select({
|
|
8
|
+
id: productExtrasRef.id,
|
|
9
|
+
name: productExtrasRef.name,
|
|
10
|
+
description: productExtrasRef.description,
|
|
11
|
+
selectionType: productExtrasRef.selectionType,
|
|
12
|
+
pricingMode: productExtrasRef.pricingMode,
|
|
13
|
+
pricedPerPerson: productExtrasRef.pricedPerPerson,
|
|
14
|
+
defaultQuantity: productExtrasRef.defaultQuantity,
|
|
15
|
+
minQuantity: productExtrasRef.minQuantity,
|
|
16
|
+
maxQuantity: productExtrasRef.maxQuantity,
|
|
17
|
+
metadata: productExtrasRef.metadata,
|
|
18
|
+
})
|
|
19
|
+
.from(productExtrasRef)
|
|
20
|
+
.where(and(eq(productExtrasRef.productId, productId), eq(productExtrasRef.active, true)))
|
|
21
|
+
.orderBy(asc(productExtrasRef.sortOrder), asc(productExtrasRef.name));
|
|
22
|
+
const priceRuleByExtraId = new Map(context.extraRules
|
|
23
|
+
.filter((rule) => rule.productExtraId)
|
|
24
|
+
.map((rule) => [rule.productExtraId, rule]));
|
|
25
|
+
const extensions = extras.map((extra) => {
|
|
26
|
+
const metadata = (extra.metadata ?? {});
|
|
27
|
+
const rule = priceRuleByExtraId.get(extra.id);
|
|
28
|
+
const pricingMode = rule?.pricingMode ?? (extra.pricedPerPerson ? "per_person" : extra.pricingMode);
|
|
29
|
+
const amount = centsToAmount(rule?.sellAmountCents);
|
|
30
|
+
return {
|
|
31
|
+
id: extra.id,
|
|
32
|
+
name: extra.name,
|
|
33
|
+
label: extra.name,
|
|
34
|
+
required: extra.selectionType === "required",
|
|
35
|
+
selectable: extra.selectionType !== "unavailable",
|
|
36
|
+
hasOptions: false,
|
|
37
|
+
refProductId: typeof metadata.refProductId === "string"
|
|
38
|
+
? metadata.refProductId
|
|
39
|
+
: typeof metadata.productId === "string"
|
|
40
|
+
? metadata.productId
|
|
41
|
+
: null,
|
|
42
|
+
thumb: typeof metadata.thumbUrl === "string" ? metadata.thumbUrl : null,
|
|
43
|
+
pricePerPerson: pricingMode === "per_person" || extra.pricedPerPerson ? (amount ?? null) : null,
|
|
44
|
+
currencyCode: getPreferredCurrency(context),
|
|
45
|
+
pricingMode,
|
|
46
|
+
defaultQuantity: extra.defaultQuantity ?? null,
|
|
47
|
+
minQuantity: extra.minQuantity ?? null,
|
|
48
|
+
maxQuantity: extra.maxQuantity ?? null,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
const details = Object.fromEntries(extras.map((extra) => {
|
|
52
|
+
const metadata = (extra.metadata ?? {});
|
|
53
|
+
const media = Array.isArray(metadata.media)
|
|
54
|
+
? metadata.media
|
|
55
|
+
.map((entry) => entry && typeof entry === "object"
|
|
56
|
+
? {
|
|
57
|
+
url: typeof entry.url === "string"
|
|
58
|
+
? String(entry.url)
|
|
59
|
+
: "",
|
|
60
|
+
alt: typeof entry.alt === "string"
|
|
61
|
+
? String(entry.alt)
|
|
62
|
+
: null,
|
|
63
|
+
}
|
|
64
|
+
: null)
|
|
65
|
+
.filter((value) => Boolean(value?.url))
|
|
66
|
+
: [];
|
|
67
|
+
return [
|
|
68
|
+
extra.id,
|
|
69
|
+
{
|
|
70
|
+
description: extra.description ?? null,
|
|
71
|
+
media,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
}));
|
|
75
|
+
return {
|
|
76
|
+
extensions,
|
|
77
|
+
items: extensions,
|
|
78
|
+
details,
|
|
79
|
+
currencyCode: getPreferredCurrency(context),
|
|
80
|
+
};
|
|
81
|
+
}
|