@voyantjs/storefront 0.24.1 → 0.24.3

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.
@@ -0,0 +1,12 @@
1
+ import type { Module } from "@voyantjs/core";
2
+ import type { HonoModule } from "@voyantjs/hono/module";
3
+ import { createStorefrontPublicRoutes } from "./routes-public.js";
4
+ export type { StorefrontPublicRoutes } from "./routes-public.js";
5
+ export { createStorefrontPublicRoutes } from "./routes-public.js";
6
+ export type { StorefrontOfferResolvers, StorefrontRequestContext, StorefrontServiceOptions, } from "./service.js";
7
+ export { createStorefrontService, resolveStorefrontSettings } from "./service.js";
8
+ export type { StorefrontDepartureListQuery, StorefrontFormField, StorefrontFormFieldInput, StorefrontPaymentMethod, StorefrontPaymentMethodCode, StorefrontPaymentMethodInput, StorefrontProductAvailabilitySummaryQuery, StorefrontPromotionalOffer, StorefrontSettings, StorefrontSettingsInput, } from "./validation.js";
9
+ export { storefrontDepartureItinerarySchema, storefrontDepartureListQuerySchema, storefrontDepartureListResponseSchema, storefrontDeparturePricePreviewInputSchema, storefrontDeparturePricePreviewSchema, storefrontDepartureSchema, storefrontFormFieldInputSchema, storefrontFormFieldOptionSchema, storefrontFormFieldSchema, storefrontFormFieldTypeSchema, storefrontPaymentMethodCodeSchema, storefrontPaymentMethodInputSchema, storefrontPaymentMethodSchema, storefrontProductAvailabilitySlotSchema, storefrontProductAvailabilityStateSchema, storefrontProductAvailabilitySummaryQuerySchema, storefrontProductAvailabilitySummaryResponseSchema, storefrontProductAvailabilitySummarySchema, storefrontProductExtensionsQuerySchema, storefrontProductExtensionsResponseSchema, storefrontPromotionalOfferListQuerySchema, storefrontPromotionalOfferListResponseSchema, storefrontPromotionalOfferResponseSchema, storefrontPromotionalOfferSchema, storefrontSettingsInputSchema, storefrontSettingsSchema, } from "./validation.js";
10
+ export declare const storefrontModule: Module;
11
+ export declare function createStorefrontHonoModule(options?: Parameters<typeof createStorefrontPublicRoutes>[0]): HonoModule;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAEvD,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAA;AAEjE,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAChE,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAA;AACjE,YAAY,EACV,wBAAwB,EACxB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAA;AACjF,YAAY,EACV,4BAA4B,EAC5B,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,2BAA2B,EAC3B,4BAA4B,EAC5B,yCAAyC,EACzC,0BAA0B,EAC1B,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,kCAAkC,EAClC,kCAAkC,EAClC,qCAAqC,EACrC,0CAA0C,EAC1C,qCAAqC,EACrC,yBAAyB,EACzB,8BAA8B,EAC9B,+BAA+B,EAC/B,yBAAyB,EACzB,6BAA6B,EAC7B,iCAAiC,EACjC,kCAAkC,EAClC,6BAA6B,EAC7B,uCAAuC,EACvC,wCAAwC,EACxC,+CAA+C,EAC/C,kDAAkD,EAClD,0CAA0C,EAC1C,sCAAsC,EACtC,yCAAyC,EACzC,yCAAyC,EACzC,4CAA4C,EAC5C,wCAAwC,EACxC,gCAAgC,EAChC,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,iBAAiB,CAAA;AAExB,eAAO,MAAM,gBAAgB,EAAE,MAE9B,CAAA;AAED,wBAAgB,0BAA0B,CACxC,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAC,CAAC,CAAC,GAC3D,UAAU,CAMZ"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import { createStorefrontPublicRoutes } from "./routes-public.js";
2
+ export { createStorefrontPublicRoutes } from "./routes-public.js";
3
+ export { createStorefrontService, resolveStorefrontSettings } from "./service.js";
4
+ export { storefrontDepartureItinerarySchema, storefrontDepartureListQuerySchema, storefrontDepartureListResponseSchema, storefrontDeparturePricePreviewInputSchema, storefrontDeparturePricePreviewSchema, storefrontDepartureSchema, storefrontFormFieldInputSchema, storefrontFormFieldOptionSchema, storefrontFormFieldSchema, storefrontFormFieldTypeSchema, storefrontPaymentMethodCodeSchema, storefrontPaymentMethodInputSchema, storefrontPaymentMethodSchema, storefrontProductAvailabilitySlotSchema, storefrontProductAvailabilityStateSchema, storefrontProductAvailabilitySummaryQuerySchema, storefrontProductAvailabilitySummaryResponseSchema, storefrontProductAvailabilitySummarySchema, storefrontProductExtensionsQuerySchema, storefrontProductExtensionsResponseSchema, storefrontPromotionalOfferListQuerySchema, storefrontPromotionalOfferListResponseSchema, storefrontPromotionalOfferResponseSchema, storefrontPromotionalOfferSchema, storefrontSettingsInputSchema, storefrontSettingsSchema, } from "./validation.js";
5
+ export const storefrontModule = {
6
+ name: "storefront",
7
+ };
8
+ export function createStorefrontHonoModule(options) {
9
+ return {
10
+ module: storefrontModule,
11
+ publicPath: "/",
12
+ publicRoutes: createStorefrontPublicRoutes(options),
13
+ };
14
+ }
@@ -0,0 +1,476 @@
1
+ import { type StorefrontServiceOptions } from "./service.js";
2
+ type Env = {
3
+ Variables: {
4
+ db: unknown;
5
+ };
6
+ };
7
+ export declare function createStorefrontPublicRoutes(options?: StorefrontServiceOptions): import("hono/hono-base").HonoBase<Env, {
8
+ "/settings": {
9
+ $get: {
10
+ input: {};
11
+ output: {
12
+ data: {
13
+ branding: {
14
+ logoUrl: string | null;
15
+ supportedLanguages: string[];
16
+ };
17
+ support: {
18
+ email: string | null;
19
+ phone: string | null;
20
+ };
21
+ legal: {
22
+ termsUrl: string | null;
23
+ privacyUrl: string | null;
24
+ defaultContractTemplateId: string | null;
25
+ };
26
+ forms: {
27
+ billing: {
28
+ fields: {
29
+ key: string;
30
+ label: string;
31
+ type: "date" | "select" | "email" | "text" | "country" | "tel" | "textarea" | "checkbox";
32
+ required: boolean;
33
+ placeholder: string | null;
34
+ description: string | null;
35
+ autocomplete: string | null;
36
+ options: {
37
+ value: string;
38
+ label: string;
39
+ }[];
40
+ }[];
41
+ };
42
+ travelers: {
43
+ fields: {
44
+ key: string;
45
+ label: string;
46
+ type: "date" | "select" | "email" | "text" | "country" | "tel" | "textarea" | "checkbox";
47
+ required: boolean;
48
+ placeholder: string | null;
49
+ description: string | null;
50
+ autocomplete: string | null;
51
+ options: {
52
+ value: string;
53
+ label: string;
54
+ }[];
55
+ }[];
56
+ };
57
+ };
58
+ payment: {
59
+ defaultMethod: "voucher" | "card" | "bank_transfer" | "cash" | "invoice" | null;
60
+ methods: {
61
+ code: "voucher" | "card" | "bank_transfer" | "cash" | "invoice";
62
+ label: string;
63
+ description: string | null;
64
+ enabled: boolean;
65
+ }[];
66
+ };
67
+ };
68
+ };
69
+ outputFormat: "json";
70
+ status: import("hono/utils/http-status").ContentfulStatusCode;
71
+ };
72
+ };
73
+ } & {
74
+ "/departures/:departureId": {
75
+ $get: {
76
+ input: {
77
+ param: {
78
+ departureId: string;
79
+ };
80
+ };
81
+ output: {
82
+ data: {
83
+ id: string;
84
+ productId: string;
85
+ itineraryId: string;
86
+ optionId: string | null;
87
+ dateLocal: string | null;
88
+ startAt: string | null;
89
+ endAt: string | null;
90
+ timezone: string;
91
+ startTime: {
92
+ id: string;
93
+ label: string | null;
94
+ startTimeLocal: string;
95
+ durationMinutes: number | null;
96
+ } | null;
97
+ meetingPoint: string | null;
98
+ capacity: number | null;
99
+ remaining: number | null;
100
+ departureStatus: "open" | "closed" | "sold_out" | "cancelled" | "on_request";
101
+ nights: number | null;
102
+ days: number | null;
103
+ ratePlans: {
104
+ id: string;
105
+ active: boolean;
106
+ name: string;
107
+ pricingModel: string;
108
+ basePrices: {
109
+ amount: number;
110
+ currencyCode: string;
111
+ }[];
112
+ roomPrices: {
113
+ amount: number;
114
+ currencyCode: string;
115
+ roomType: {
116
+ id: string;
117
+ name: string;
118
+ occupancy: {
119
+ adultsMin: number;
120
+ adultsMax: number;
121
+ childrenMax: number;
122
+ };
123
+ };
124
+ }[];
125
+ }[];
126
+ };
127
+ };
128
+ outputFormat: "json";
129
+ status: import("hono/utils/http-status").ContentfulStatusCode;
130
+ } | {
131
+ input: {
132
+ param: {
133
+ departureId: string;
134
+ };
135
+ };
136
+ output: {
137
+ error: string;
138
+ };
139
+ outputFormat: "json";
140
+ status: 404;
141
+ };
142
+ };
143
+ } & {
144
+ "/products/:productId/departures": {
145
+ $get: {
146
+ input: {
147
+ param: {
148
+ productId: string;
149
+ };
150
+ };
151
+ output: {
152
+ data: {
153
+ id: string;
154
+ productId: string;
155
+ itineraryId: string;
156
+ optionId: string | null;
157
+ dateLocal: string | null;
158
+ startAt: string | null;
159
+ endAt: string | null;
160
+ timezone: string;
161
+ startTime: {
162
+ id: string;
163
+ label: string | null;
164
+ startTimeLocal: string;
165
+ durationMinutes: number | null;
166
+ } | null;
167
+ meetingPoint: string | null;
168
+ capacity: number | null;
169
+ remaining: number | null;
170
+ departureStatus: "open" | "closed" | "sold_out" | "cancelled" | "on_request";
171
+ nights: number | null;
172
+ days: number | null;
173
+ ratePlans: {
174
+ id: string;
175
+ active: boolean;
176
+ name: string;
177
+ pricingModel: string;
178
+ basePrices: {
179
+ amount: number;
180
+ currencyCode: string;
181
+ }[];
182
+ roomPrices: {
183
+ amount: number;
184
+ currencyCode: string;
185
+ roomType: {
186
+ id: string;
187
+ name: string;
188
+ occupancy: {
189
+ adultsMin: number;
190
+ adultsMax: number;
191
+ childrenMax: number;
192
+ };
193
+ };
194
+ }[];
195
+ }[];
196
+ }[];
197
+ total: number;
198
+ limit: number;
199
+ offset: number;
200
+ };
201
+ outputFormat: "json";
202
+ status: import("hono/utils/http-status").ContentfulStatusCode;
203
+ };
204
+ };
205
+ } & {
206
+ "/departures/:departureId/price": {
207
+ $post: {
208
+ input: {
209
+ param: {
210
+ departureId: string;
211
+ };
212
+ };
213
+ output: {
214
+ data: {
215
+ departureId: string;
216
+ productId: string;
217
+ optionId: string | null;
218
+ currencyCode: string;
219
+ basePrice: number;
220
+ taxAmount: number;
221
+ total: number;
222
+ notes: string | null;
223
+ lineItems: {
224
+ name: string;
225
+ total: number;
226
+ quantity: number;
227
+ unitPrice: number;
228
+ }[];
229
+ };
230
+ };
231
+ outputFormat: "json";
232
+ status: import("hono/utils/http-status").ContentfulStatusCode;
233
+ } | {
234
+ input: {
235
+ param: {
236
+ departureId: string;
237
+ };
238
+ };
239
+ output: {
240
+ error: string;
241
+ };
242
+ outputFormat: "json";
243
+ status: 404;
244
+ };
245
+ };
246
+ } & {
247
+ "/products/:productId/extensions": {
248
+ $get: {
249
+ input: {
250
+ param: {
251
+ productId: string;
252
+ };
253
+ };
254
+ output: {
255
+ data: {
256
+ extensions: {
257
+ id: string;
258
+ name: string;
259
+ label: string;
260
+ required: boolean;
261
+ selectable: boolean;
262
+ hasOptions: boolean;
263
+ refProductId: string | null;
264
+ thumb: string | null;
265
+ pricePerPerson: number | null;
266
+ currencyCode: string;
267
+ pricingMode: string;
268
+ defaultQuantity: number | null;
269
+ minQuantity: number | null;
270
+ maxQuantity: number | null;
271
+ }[];
272
+ items: {
273
+ id: string;
274
+ name: string;
275
+ label: string;
276
+ required: boolean;
277
+ selectable: boolean;
278
+ hasOptions: boolean;
279
+ refProductId: string | null;
280
+ thumb: string | null;
281
+ pricePerPerson: number | null;
282
+ currencyCode: string;
283
+ pricingMode: string;
284
+ defaultQuantity: number | null;
285
+ minQuantity: number | null;
286
+ maxQuantity: number | null;
287
+ }[];
288
+ details: {
289
+ [x: string]: {
290
+ description: string | null;
291
+ media: {
292
+ url: string;
293
+ alt: string | null;
294
+ }[];
295
+ };
296
+ };
297
+ currencyCode: string;
298
+ };
299
+ };
300
+ outputFormat: "json";
301
+ status: import("hono/utils/http-status").ContentfulStatusCode;
302
+ };
303
+ };
304
+ } & {
305
+ "/products/:productId/availability": {
306
+ $get: {
307
+ input: {
308
+ param: {
309
+ productId: string;
310
+ };
311
+ };
312
+ output: {
313
+ data: {
314
+ productId: string;
315
+ availabilityState: "closed" | "sold_out" | "cancelled" | "past_cutoff" | "too_early" | "unavailable" | "on_request" | "available";
316
+ counts: {
317
+ total: number;
318
+ open: number;
319
+ closed: number;
320
+ soldOut: number;
321
+ cancelled: number;
322
+ onRequest: number;
323
+ pastCutoff: number;
324
+ tooEarly: number;
325
+ available: number;
326
+ };
327
+ departures: {
328
+ id: string;
329
+ productId: string;
330
+ optionId: string | null;
331
+ dateLocal: string | null;
332
+ startAt: string | null;
333
+ endAt: string | null;
334
+ timezone: string;
335
+ status: "open" | "closed" | "sold_out" | "cancelled" | "on_request";
336
+ availabilityState: "closed" | "sold_out" | "cancelled" | "past_cutoff" | "too_early" | "unavailable" | "on_request" | "available";
337
+ capacity: number | null;
338
+ remaining: number | null;
339
+ pastCutoff: boolean;
340
+ tooEarly: boolean;
341
+ }[];
342
+ total: number;
343
+ limit: number;
344
+ offset: number;
345
+ };
346
+ };
347
+ outputFormat: "json";
348
+ status: import("hono/utils/http-status").ContentfulStatusCode;
349
+ };
350
+ };
351
+ } & {
352
+ "/products/:productId/departures/:departureId/itinerary": {
353
+ $get: {
354
+ input: {
355
+ param: {
356
+ departureId: string;
357
+ } & {
358
+ productId: string;
359
+ };
360
+ };
361
+ output: {
362
+ data: {
363
+ id: string;
364
+ days: {
365
+ id: string;
366
+ title: string;
367
+ description: string | null;
368
+ thumbnail: {
369
+ url: string;
370
+ } | null;
371
+ segments: {
372
+ id: string;
373
+ title: string;
374
+ description: string | null;
375
+ }[];
376
+ }[];
377
+ };
378
+ };
379
+ outputFormat: "json";
380
+ status: import("hono/utils/http-status").ContentfulStatusCode;
381
+ } | {
382
+ input: {
383
+ param: {
384
+ departureId: string;
385
+ } & {
386
+ productId: string;
387
+ };
388
+ };
389
+ output: {
390
+ error: string;
391
+ };
392
+ outputFormat: "json";
393
+ status: 404;
394
+ };
395
+ };
396
+ } & {
397
+ "/products/:productId/offers": {
398
+ $get: {
399
+ input: {
400
+ param: {
401
+ productId: string;
402
+ };
403
+ };
404
+ output: {
405
+ data: {
406
+ id: string;
407
+ name: string;
408
+ slug: string | null;
409
+ description: string | null;
410
+ discountType: "percentage" | "fixed_amount";
411
+ discountValue: string;
412
+ currency: string | null;
413
+ applicableProductIds: string[];
414
+ applicableDepartureIds: string[];
415
+ validFrom: string | null;
416
+ validTo: string | null;
417
+ minTravelers: number | null;
418
+ imageMobileUrl: string | null;
419
+ imageDesktopUrl: string | null;
420
+ stackable: boolean;
421
+ createdAt: string;
422
+ updatedAt: string;
423
+ }[];
424
+ };
425
+ outputFormat: "json";
426
+ status: import("hono/utils/http-status").ContentfulStatusCode;
427
+ };
428
+ };
429
+ } & {
430
+ "/offers/:slug": {
431
+ $get: {
432
+ input: {
433
+ param: {
434
+ slug: string;
435
+ };
436
+ };
437
+ output: {
438
+ data: {
439
+ id: string;
440
+ name: string;
441
+ slug: string | null;
442
+ description: string | null;
443
+ discountType: "percentage" | "fixed_amount";
444
+ discountValue: string;
445
+ currency: string | null;
446
+ applicableProductIds: string[];
447
+ applicableDepartureIds: string[];
448
+ validFrom: string | null;
449
+ validTo: string | null;
450
+ minTravelers: number | null;
451
+ imageMobileUrl: string | null;
452
+ imageDesktopUrl: string | null;
453
+ stackable: boolean;
454
+ createdAt: string;
455
+ updatedAt: string;
456
+ };
457
+ };
458
+ outputFormat: "json";
459
+ status: import("hono/utils/http-status").ContentfulStatusCode;
460
+ } | {
461
+ input: {
462
+ param: {
463
+ slug: string;
464
+ };
465
+ };
466
+ output: {
467
+ error: string;
468
+ };
469
+ outputFormat: "json";
470
+ status: 404;
471
+ };
472
+ };
473
+ }, "/", "/offers/:slug">;
474
+ export type StorefrontPublicRoutes = ReturnType<typeof createStorefrontPublicRoutes>;
475
+ export {};
476
+ //# sourceMappingURL=routes-public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,cAAc,CAAA;AASrB,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,OAAO,CAAA;KACZ,CAAA;CACF,CAAA;AAED,wBAAgB,4BAA4B,CAAC,OAAO,CAAC,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAiG9E;AAED,MAAM,MAAM,sBAAsB,GAAG,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAA"}
@@ -0,0 +1,73 @@
1
+ import { parseJsonBody, parseQuery } from "@voyantjs/hono";
2
+ import { Hono } from "hono";
3
+ import { createStorefrontService, } from "./service.js";
4
+ import { storefrontDepartureListQuerySchema, storefrontDeparturePricePreviewInputSchema, storefrontProductAvailabilitySummaryQuerySchema, storefrontProductExtensionsQuerySchema, storefrontPromotionalOfferListQuerySchema, } from "./validation.js";
5
+ export function createStorefrontPublicRoutes(options) {
6
+ const storefrontService = createStorefrontService(options);
7
+ function getRequestContext(c) {
8
+ return {
9
+ db: c.get("db"),
10
+ env: c.env,
11
+ context: c,
12
+ };
13
+ }
14
+ return new Hono()
15
+ .get("/settings", async (c) => {
16
+ return c.json({ data: await storefrontService.resolveSettings(getRequestContext(c)) });
17
+ })
18
+ .get("/departures/:departureId", async (c) => {
19
+ const departure = await storefrontService.getDeparture(c.get("db"), c.req.param("departureId"));
20
+ return departure
21
+ ? c.json({ data: departure })
22
+ : c.json({ error: "Storefront departure not found" }, 404);
23
+ })
24
+ .get("/products/:productId/departures", async (c) => {
25
+ return c.json(await storefrontService.listProductDepartures(c.get("db"), c.req.param("productId"), await parseQuery(c, storefrontDepartureListQuerySchema)));
26
+ })
27
+ .post("/departures/:departureId/price", async (c) => {
28
+ const preview = await storefrontService.previewDeparturePrice(c.get("db"), c.req.param("departureId"), await parseJsonBody(c, storefrontDeparturePricePreviewInputSchema));
29
+ return preview
30
+ ? c.json({ data: preview })
31
+ : c.json({ error: "Storefront departure not found" }, 404);
32
+ })
33
+ .get("/products/:productId/extensions", async (c) => {
34
+ const query = await parseQuery(c, storefrontProductExtensionsQuerySchema);
35
+ return c.json({
36
+ data: await storefrontService.getProductExtensions(c.get("db"), c.req.param("productId"), query.optionId),
37
+ });
38
+ })
39
+ .get("/products/:productId/availability", async (c) => {
40
+ return c.json({
41
+ data: await storefrontService.getProductAvailabilitySummary(c.get("db"), c.req.param("productId"), await parseQuery(c, storefrontProductAvailabilitySummaryQuerySchema)),
42
+ });
43
+ })
44
+ .get("/products/:productId/departures/:departureId/itinerary", async (c) => {
45
+ const itinerary = await storefrontService.getDepartureItinerary(c.get("db"), {
46
+ departureId: c.req.param("departureId"),
47
+ productId: c.req.param("productId"),
48
+ });
49
+ return itinerary
50
+ ? c.json({ data: itinerary })
51
+ : c.json({ error: "Storefront itinerary not found" }, 404);
52
+ })
53
+ .get("/products/:productId/offers", async (c) => {
54
+ const query = await parseQuery(c, storefrontPromotionalOfferListQuerySchema);
55
+ return c.json({
56
+ data: await storefrontService.listApplicableOffers({
57
+ productId: c.req.param("productId"),
58
+ departureId: query.departureId,
59
+ locale: query.locale,
60
+ context: getRequestContext(c),
61
+ }),
62
+ });
63
+ })
64
+ .get("/offers/:slug", async (c) => {
65
+ const query = await parseQuery(c, storefrontPromotionalOfferListQuerySchema);
66
+ const offer = await storefrontService.getOfferBySlug({
67
+ slug: c.req.param("slug"),
68
+ locale: query.locale,
69
+ context: getRequestContext(c),
70
+ });
71
+ return offer ? c.json({ data: offer }) : c.json({ error: "Storefront offer not found" }, 404);
72
+ });
73
+ }