@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.
Files changed (126) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +231 -0
  3. package/dist/booking-intents.d.ts +42 -0
  4. package/dist/booking-intents.d.ts.map +1 -0
  5. package/dist/booking-intents.js +83 -0
  6. package/dist/customer-portal/index.d.ts +16 -0
  7. package/dist/customer-portal/index.d.ts.map +1 -0
  8. package/dist/customer-portal/index.js +23 -0
  9. package/dist/customer-portal/route-runtime.d.ts +16 -0
  10. package/dist/customer-portal/route-runtime.d.ts.map +1 -0
  11. package/dist/customer-portal/route-runtime.js +27 -0
  12. package/dist/customer-portal/routes-public.d.ts +1936 -0
  13. package/dist/customer-portal/routes-public.d.ts.map +1 -0
  14. package/dist/customer-portal/routes-public.js +165 -0
  15. package/dist/customer-portal/routes.d.ts +43 -0
  16. package/dist/customer-portal/routes.d.ts.map +1 -0
  17. package/dist/customer-portal/routes.js +17 -0
  18. package/dist/customer-portal/service-public-impl.d.ts +138 -0
  19. package/dist/customer-portal/service-public-impl.d.ts.map +1 -0
  20. package/dist/customer-portal/service-public-impl.js +1808 -0
  21. package/dist/customer-portal/service-public.d.ts +2 -0
  22. package/dist/customer-portal/service-public.d.ts.map +1 -0
  23. package/dist/customer-portal/service-public.js +1 -0
  24. package/dist/customer-portal/validation-public/bookings.d.ts +551 -0
  25. package/dist/customer-portal/validation-public/bookings.d.ts.map +1 -0
  26. package/dist/customer-portal/validation-public/bookings.js +132 -0
  27. package/dist/customer-portal/validation-public/common.d.ts +162 -0
  28. package/dist/customer-portal/validation-public/common.d.ts.map +1 -0
  29. package/dist/customer-portal/validation-public/common.js +139 -0
  30. package/dist/customer-portal/validation-public/profile.d.ts +749 -0
  31. package/dist/customer-portal/validation-public/profile.d.ts.map +1 -0
  32. package/dist/customer-portal/validation-public/profile.js +308 -0
  33. package/dist/customer-portal/validation-public.d.ts +3 -0
  34. package/dist/customer-portal/validation-public.d.ts.map +1 -0
  35. package/dist/customer-portal/validation-public.js +2 -0
  36. package/dist/guest-booking-guard.d.ts +24 -0
  37. package/dist/guest-booking-guard.d.ts.map +1 -0
  38. package/dist/guest-booking-guard.js +55 -0
  39. package/dist/index.d.ts +23 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +41 -0
  42. package/dist/product-extra-ref.d.ts +238 -0
  43. package/dist/product-extra-ref.d.ts.map +1 -0
  44. package/dist/product-extra-ref.js +22 -0
  45. package/dist/routes-admin.d.ts +220 -0
  46. package/dist/routes-admin.d.ts.map +1 -0
  47. package/dist/routes-admin.js +28 -0
  48. package/dist/routes-public.d.ts +1475 -0
  49. package/dist/routes-public.d.ts.map +1 -0
  50. package/dist/routes-public.js +362 -0
  51. package/dist/service-booking-session-bootstrap.d.ts +227 -0
  52. package/dist/service-booking-session-bootstrap.d.ts.map +1 -0
  53. package/dist/service-booking-session-bootstrap.js +287 -0
  54. package/dist/service-boundary-resource-sql.d.ts +18 -0
  55. package/dist/service-boundary-resource-sql.d.ts.map +1 -0
  56. package/dist/service-boundary-resource-sql.js +73 -0
  57. package/dist/service-boundary-sql.d.ts +103 -0
  58. package/dist/service-boundary-sql.d.ts.map +1 -0
  59. package/dist/service-boundary-sql.js +307 -0
  60. package/dist/service-departures-core.d.ts +41 -0
  61. package/dist/service-departures-core.d.ts.map +1 -0
  62. package/dist/service-departures-core.js +92 -0
  63. package/dist/service-departures-extensions.d.ts +46 -0
  64. package/dist/service-departures-extensions.d.ts.map +1 -0
  65. package/dist/service-departures-extensions.js +81 -0
  66. package/dist/service-departures-offers.d.ts +220 -0
  67. package/dist/service-departures-offers.d.ts.map +1 -0
  68. package/dist/service-departures-offers.js +177 -0
  69. package/dist/service-departures-price-preview.d.ts +306 -0
  70. package/dist/service-departures-price-preview.d.ts.map +1 -0
  71. package/dist/service-departures-price-preview.js +383 -0
  72. package/dist/service-departures-pricing-context.d.ts +115 -0
  73. package/dist/service-departures-pricing-context.d.ts.map +1 -0
  74. package/dist/service-departures-pricing-context.js +237 -0
  75. package/dist/service-departures-pricing.d.ts +5 -0
  76. package/dist/service-departures-pricing.d.ts.map +1 -0
  77. package/dist/service-departures-pricing.js +4 -0
  78. package/dist/service-departures.d.ts +192 -0
  79. package/dist/service-departures.d.ts.map +1 -0
  80. package/dist/service-departures.js +213 -0
  81. package/dist/service-intake.d.ts +130 -0
  82. package/dist/service-intake.d.ts.map +1 -0
  83. package/dist/service-intake.js +274 -0
  84. package/dist/service-transport-eligibility.d.ts +10 -0
  85. package/dist/service-transport-eligibility.d.ts.map +1 -0
  86. package/dist/service-transport-eligibility.js +198 -0
  87. package/dist/service.d.ts +1062 -0
  88. package/dist/service.d.ts.map +1 -0
  89. package/dist/service.js +332 -0
  90. package/dist/transport-eligibility.d.ts +4 -0
  91. package/dist/transport-eligibility.d.ts.map +1 -0
  92. package/dist/transport-eligibility.js +2 -0
  93. package/dist/validation/departures.d.ts +1669 -0
  94. package/dist/validation/departures.d.ts.map +1 -0
  95. package/dist/validation/departures.js +397 -0
  96. package/dist/validation/intake.d.ts +147 -0
  97. package/dist/validation/intake.d.ts.map +1 -0
  98. package/dist/validation/intake.js +69 -0
  99. package/dist/validation/offers.d.ts +340 -0
  100. package/dist/validation/offers.d.ts.map +1 -0
  101. package/dist/validation/offers.js +117 -0
  102. package/dist/validation-settings.d.ts +609 -0
  103. package/dist/validation-settings.d.ts.map +1 -0
  104. package/dist/validation-settings.js +235 -0
  105. package/dist/validation-transport-eligibility.d.ts +314 -0
  106. package/dist/validation-transport-eligibility.d.ts.map +1 -0
  107. package/dist/validation-transport-eligibility.js +97 -0
  108. package/dist/validation.d.ts +6 -0
  109. package/dist/validation.d.ts.map +1 -0
  110. package/dist/validation.js +4 -0
  111. package/dist/verification/index.d.ts +12 -0
  112. package/dist/verification/index.d.ts.map +1 -0
  113. package/dist/verification/index.js +18 -0
  114. package/dist/verification/routes-public.d.ts +121 -0
  115. package/dist/verification/routes-public.d.ts.map +1 -0
  116. package/dist/verification/routes-public.js +125 -0
  117. package/dist/verification/schema.d.ts +273 -0
  118. package/dist/verification/schema.d.ts.map +1 -0
  119. package/dist/verification/schema.js +50 -0
  120. package/dist/verification/service.d.ts +114 -0
  121. package/dist/verification/service.d.ts.map +1 -0
  122. package/dist/verification/service.js +283 -0
  123. package/dist/verification/validation.d.ts +98 -0
  124. package/dist/verification/validation.d.ts.map +1 -0
  125. package/dist/verification/validation.js +54 -0
  126. 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
+ }