@voyantjs/products 0.52.1 → 0.52.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.
Files changed (73) hide show
  1. package/dist/action-ledger-drift.d.ts +29 -0
  2. package/dist/action-ledger-drift.d.ts.map +1 -0
  3. package/dist/action-ledger-drift.js +335 -0
  4. package/dist/action-ledger.d.ts +104 -0
  5. package/dist/action-ledger.d.ts.map +1 -0
  6. package/dist/action-ledger.js +100 -0
  7. package/dist/booking-extension.d.ts +3 -3
  8. package/dist/catalog-policy.d.ts.map +1 -1
  9. package/dist/catalog-policy.js +18 -0
  10. package/dist/content-shape.d.ts +2 -0
  11. package/dist/content-shape.d.ts.map +1 -1
  12. package/dist/content-shape.js +2 -0
  13. package/dist/events.d.ts +1 -1
  14. package/dist/events.d.ts.map +1 -1
  15. package/dist/route-env.d.ts +22 -0
  16. package/dist/route-env.d.ts.map +1 -0
  17. package/dist/route-env.js +1 -0
  18. package/dist/routes-associations.d.ts +164 -0
  19. package/dist/routes-associations.d.ts.map +1 -0
  20. package/dist/routes-associations.js +100 -0
  21. package/dist/routes-catalog.d.ts +436 -0
  22. package/dist/routes-catalog.d.ts.map +1 -0
  23. package/dist/routes-catalog.js +104 -0
  24. package/dist/routes-configuration.d.ts +773 -0
  25. package/dist/routes-configuration.d.ts.map +1 -0
  26. package/dist/routes-configuration.js +364 -0
  27. package/dist/routes-core.d.ts +302 -0
  28. package/dist/routes-core.d.ts.map +1 -0
  29. package/dist/routes-core.js +79 -0
  30. package/dist/routes-itinerary.d.ts +614 -0
  31. package/dist/routes-itinerary.d.ts.map +1 -0
  32. package/dist/routes-itinerary.js +309 -0
  33. package/dist/routes-maintenance.d.ts +32 -0
  34. package/dist/routes-maintenance.d.ts.map +1 -0
  35. package/dist/routes-maintenance.js +14 -0
  36. package/dist/routes-media.d.ts +634 -0
  37. package/dist/routes-media.d.ts.map +1 -0
  38. package/dist/routes-media.js +245 -0
  39. package/dist/routes-merchandising.d.ts +1108 -0
  40. package/dist/routes-merchandising.d.ts.map +1 -0
  41. package/dist/routes-merchandising.js +376 -0
  42. package/dist/routes-options.d.ts +363 -0
  43. package/dist/routes-options.d.ts.map +1 -0
  44. package/dist/routes-options.js +173 -0
  45. package/dist/routes-public.d.ts +4 -4
  46. package/dist/routes-translations.d.ts +477 -0
  47. package/dist/routes-translations.d.ts.map +1 -0
  48. package/dist/routes-translations.js +258 -0
  49. package/dist/routes.d.ts +417 -355
  50. package/dist/routes.d.ts.map +1 -1
  51. package/dist/routes.js +21 -1133
  52. package/dist/schema-core.d.ts +3 -3
  53. package/dist/schema-itinerary.d.ts +1 -1
  54. package/dist/schema-settings.d.ts +4 -4
  55. package/dist/service-catalog-plane.d.ts.map +1 -1
  56. package/dist/service-catalog-plane.js +48 -1
  57. package/dist/service-catalog.d.ts +2 -2
  58. package/dist/service-content-owned.d.ts.map +1 -1
  59. package/dist/service-content-owned.js +98 -4
  60. package/dist/service-public.d.ts +4 -4
  61. package/dist/service.d.ts +225 -97
  62. package/dist/service.d.ts.map +1 -1
  63. package/dist/service.js +91 -0
  64. package/dist/tasks/brochures.d.ts +1 -1
  65. package/dist/validation-catalog.d.ts +10 -10
  66. package/dist/validation-config.d.ts +17 -17
  67. package/dist/validation-content.d.ts +26 -26
  68. package/dist/validation-core.d.ts +46 -46
  69. package/dist/validation-core.d.ts.map +1 -1
  70. package/dist/validation-core.js +17 -1
  71. package/dist/validation-public.d.ts +25 -25
  72. package/dist/validation-shared.d.ts +11 -11
  73. package/package.json +13 -7
@@ -0,0 +1,29 @@
1
+ import type { AnyDrizzleDb } from "@voyantjs/db";
2
+ import { type SQL } from "drizzle-orm";
3
+ export type ProductActionLedgerDriftCheck = "product" | "product_option" | "option_unit" | "product_itinerary" | "product_day" | "product_day_service" | "product_media" | "product_capability" | "product_delivery_format";
4
+ export interface CheckProductActionLedgerDriftInput {
5
+ createdAtFrom?: Date | string | null;
6
+ sampleLimit?: number | null;
7
+ }
8
+ export interface ProductActionLedgerDriftRow {
9
+ check: ProductActionLedgerDriftCheck;
10
+ missingCount: number;
11
+ sampleIds: string[];
12
+ }
13
+ export interface CheckProductActionLedgerDriftResult {
14
+ ok: boolean;
15
+ rows: ProductActionLedgerDriftRow[];
16
+ }
17
+ interface ProductActionLedgerDriftQueryRow extends Record<string, unknown> {
18
+ check: ProductActionLedgerDriftCheck;
19
+ missing_count: number | string;
20
+ sample_ids: string[] | null;
21
+ }
22
+ export declare function buildProductActionLedgerDriftQueries(input?: CheckProductActionLedgerDriftInput): Record<ProductActionLedgerDriftCheck, SQL<ProductActionLedgerDriftQueryRow>>;
23
+ export declare function checkProductActionLedgerDrift(db: AnyDrizzleDb, input?: CheckProductActionLedgerDriftInput): Promise<CheckProductActionLedgerDriftResult>;
24
+ declare function normalizeRow(row: ProductActionLedgerDriftQueryRow): ProductActionLedgerDriftRow;
25
+ export declare const __test__: {
26
+ normalizeRow: typeof normalizeRow;
27
+ };
28
+ export {};
29
+ //# sourceMappingURL=action-ledger-drift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-ledger-drift.d.ts","sourceRoot":"","sources":["../src/action-ledger-drift.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,KAAK,GAAG,EAAwB,MAAM,aAAa,CAAA;AA6B5D,MAAM,MAAM,6BAA6B,GACrC,SAAS,GACT,gBAAgB,GAChB,aAAa,GACb,mBAAmB,GACnB,aAAa,GACb,qBAAqB,GACrB,eAAe,GACf,oBAAoB,GACpB,yBAAyB,CAAA;AAE7B,MAAM,WAAW,kCAAkC;IACjD,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACpC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,6BAA6B,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,mCAAmC;IAClD,EAAE,EAAE,OAAO,CAAA;IACX,IAAI,EAAE,2BAA2B,EAAE,CAAA;CACpC;AAED,UAAU,gCAAiC,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACxE,KAAK,EAAE,6BAA6B,CAAA;IACpC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAC5B;AAED,wBAAgB,oCAAoC,CAClD,KAAK,GAAE,kCAAuC,GAC7C,MAAM,CAAC,6BAA6B,EAAE,GAAG,CAAC,gCAAgC,CAAC,CAAC,CA0Q9E;AAED,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,YAAY,EAChB,KAAK,GAAE,kCAAuC,GAC7C,OAAO,CAAC,mCAAmC,CAAC,CAqB9C;AAyBD,iBAAS,YAAY,CAAC,GAAG,EAAE,gCAAgC,GAAG,2BAA2B,CAMxF;AAED,eAAO,MAAM,QAAQ;;CAEpB,CAAA"}
@@ -0,0 +1,335 @@
1
+ import { actionLedgerEntries } from "@voyantjs/action-ledger/schema";
2
+ import { sql } from "drizzle-orm";
3
+ import { optionUnits, productOptions, products } from "./schema-core.js";
4
+ import { productDayServices, productDays, productItineraries, productMedia, } from "./schema-itinerary.js";
5
+ import { productCapabilities, productDeliveryFormats } from "./schema-settings.js";
6
+ const DEFAULT_SAMPLE_LIMIT = 20;
7
+ const MAX_SAMPLE_LIMIT = 100;
8
+ const PRODUCT_CREATE_ACTION_NAME = "product.create";
9
+ const PRODUCT_OPTION_CREATE_ACTION_NAME = "product.option.create";
10
+ const OPTION_UNIT_CREATE_ACTION_NAME = "product.option_unit.create";
11
+ const PRODUCT_ITINERARY_CREATE_ACTION_NAMES = [
12
+ "product.itinerary.create",
13
+ "product.itinerary.duplicate",
14
+ ];
15
+ const PRODUCT_DAY_CREATE_ACTION_NAME = "product.day.create";
16
+ const PRODUCT_DAY_SERVICE_CREATE_ACTION_NAME = "product.day_service.create";
17
+ const PRODUCT_MEDIA_CREATE_ACTION_NAME = "product.media.create";
18
+ const PRODUCT_DAY_MEDIA_CREATE_ACTION_NAME = "product.day_media.create";
19
+ const PRODUCT_BROCHURE_CREATE_ACTION_NAME = "product.brochure.create";
20
+ const PRODUCT_CAPABILITY_CREATE_ACTION_NAME = "product.capability.create";
21
+ const PRODUCT_DELIVERY_FORMAT_CREATE_ACTION_NAME = "product.delivery_format.create";
22
+ export function buildProductActionLedgerDriftQueries(input = {}) {
23
+ const sampleLimit = normalizeSampleLimit(input.sampleLimit);
24
+ return {
25
+ product: sql `
26
+ SELECT
27
+ 'product' AS check,
28
+ count(*)::int AS missing_count,
29
+ coalesce(
30
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
31
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
32
+ ARRAY[]::text[]
33
+ ) AS sample_ids
34
+ FROM (
35
+ SELECT
36
+ ${products.id} AS candidate_id,
37
+ ${products.createdAt} AS created_at,
38
+ row_number() OVER (ORDER BY ${products.createdAt} DESC, ${products.id} DESC) AS sample_ordinal
39
+ FROM ${products}
40
+ WHERE 1 = 1
41
+ ${buildCreatedAtCondition(products.createdAt, input.createdAtFrom)}
42
+ AND NOT EXISTS (
43
+ SELECT 1
44
+ FROM ${actionLedgerEntries}
45
+ WHERE ${actionLedgerEntries.actionName} = ${PRODUCT_CREATE_ACTION_NAME}
46
+ AND ${actionLedgerEntries.targetType} = ${"product"}
47
+ AND ${actionLedgerEntries.targetId} = ${products.id}
48
+ )
49
+ ) missing
50
+ `,
51
+ product_option: sql `
52
+ SELECT
53
+ 'product_option' AS check,
54
+ count(*)::int AS missing_count,
55
+ coalesce(
56
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
57
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
58
+ ARRAY[]::text[]
59
+ ) AS sample_ids
60
+ FROM (
61
+ SELECT
62
+ ${productOptions.id} AS candidate_id,
63
+ ${productOptions.createdAt} AS created_at,
64
+ row_number() OVER (
65
+ ORDER BY ${productOptions.createdAt} DESC, ${productOptions.id} DESC
66
+ ) AS sample_ordinal
67
+ FROM ${productOptions}
68
+ WHERE 1 = 1
69
+ ${buildCreatedAtCondition(productOptions.createdAt, input.createdAtFrom)}
70
+ AND NOT EXISTS (
71
+ SELECT 1
72
+ FROM ${actionLedgerEntries}
73
+ WHERE ${actionLedgerEntries.actionName} = ${PRODUCT_OPTION_CREATE_ACTION_NAME}
74
+ AND ${actionLedgerEntries.targetType} = ${"product"}
75
+ AND ${actionLedgerEntries.targetId} = ${productOptions.productId}
76
+ )
77
+ ) missing
78
+ `,
79
+ option_unit: sql `
80
+ SELECT
81
+ 'option_unit' AS check,
82
+ count(*)::int AS missing_count,
83
+ coalesce(
84
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
85
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
86
+ ARRAY[]::text[]
87
+ ) AS sample_ids
88
+ FROM (
89
+ SELECT
90
+ ${optionUnits.id} AS candidate_id,
91
+ ${optionUnits.createdAt} AS created_at,
92
+ row_number() OVER (
93
+ ORDER BY ${optionUnits.createdAt} DESC, ${optionUnits.id} DESC
94
+ ) AS sample_ordinal
95
+ FROM ${optionUnits}
96
+ INNER JOIN ${productOptions} ON ${productOptions.id} = ${optionUnits.optionId}
97
+ WHERE 1 = 1
98
+ ${buildCreatedAtCondition(optionUnits.createdAt, input.createdAtFrom)}
99
+ AND NOT EXISTS (
100
+ SELECT 1
101
+ FROM ${actionLedgerEntries}
102
+ WHERE ${actionLedgerEntries.actionName} = ${OPTION_UNIT_CREATE_ACTION_NAME}
103
+ AND ${actionLedgerEntries.targetType} = ${"product"}
104
+ AND ${actionLedgerEntries.targetId} = ${productOptions.productId}
105
+ )
106
+ ) missing
107
+ `,
108
+ product_itinerary: sql `
109
+ SELECT
110
+ 'product_itinerary' AS check,
111
+ count(*)::int AS missing_count,
112
+ coalesce(
113
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
114
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
115
+ ARRAY[]::text[]
116
+ ) AS sample_ids
117
+ FROM (
118
+ SELECT
119
+ ${productItineraries.id} AS candidate_id,
120
+ ${productItineraries.createdAt} AS created_at,
121
+ row_number() OVER (
122
+ ORDER BY ${productItineraries.createdAt} DESC, ${productItineraries.id} DESC
123
+ ) AS sample_ordinal
124
+ FROM ${productItineraries}
125
+ WHERE 1 = 1
126
+ ${buildCreatedAtCondition(productItineraries.createdAt, input.createdAtFrom)}
127
+ AND NOT EXISTS (
128
+ SELECT 1
129
+ FROM ${actionLedgerEntries}
130
+ WHERE ${actionLedgerEntries.actionName} IN (${sql.join(PRODUCT_ITINERARY_CREATE_ACTION_NAMES.map((actionName) => sql `${actionName}`), sql `, `)})
131
+ AND ${actionLedgerEntries.targetType} = ${"product"}
132
+ AND ${actionLedgerEntries.targetId} = ${productItineraries.productId}
133
+ )
134
+ ) missing
135
+ `,
136
+ product_day: sql `
137
+ SELECT
138
+ 'product_day' AS check,
139
+ count(*)::int AS missing_count,
140
+ coalesce(
141
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
142
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
143
+ ARRAY[]::text[]
144
+ ) AS sample_ids
145
+ FROM (
146
+ SELECT
147
+ ${productDays.id} AS candidate_id,
148
+ ${productDays.createdAt} AS created_at,
149
+ row_number() OVER (
150
+ ORDER BY ${productDays.createdAt} DESC, ${productDays.id} DESC
151
+ ) AS sample_ordinal
152
+ FROM ${productDays}
153
+ INNER JOIN ${productItineraries} ON ${productItineraries.id} = ${productDays.itineraryId}
154
+ WHERE 1 = 1
155
+ ${buildCreatedAtCondition(productDays.createdAt, input.createdAtFrom)}
156
+ AND NOT EXISTS (
157
+ SELECT 1
158
+ FROM ${actionLedgerEntries}
159
+ WHERE ${actionLedgerEntries.actionName} = ${PRODUCT_DAY_CREATE_ACTION_NAME}
160
+ AND ${actionLedgerEntries.targetType} = ${"product"}
161
+ AND ${actionLedgerEntries.targetId} = ${productItineraries.productId}
162
+ )
163
+ ) missing
164
+ `,
165
+ product_day_service: sql `
166
+ SELECT
167
+ 'product_day_service' AS check,
168
+ count(*)::int AS missing_count,
169
+ coalesce(
170
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
171
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
172
+ ARRAY[]::text[]
173
+ ) AS sample_ids
174
+ FROM (
175
+ SELECT
176
+ ${productDayServices.id} AS candidate_id,
177
+ ${productDayServices.createdAt} AS created_at,
178
+ row_number() OVER (
179
+ ORDER BY ${productDayServices.createdAt} DESC, ${productDayServices.id} DESC
180
+ ) AS sample_ordinal
181
+ FROM ${productDayServices}
182
+ INNER JOIN ${productDays} ON ${productDays.id} = ${productDayServices.dayId}
183
+ INNER JOIN ${productItineraries} ON ${productItineraries.id} = ${productDays.itineraryId}
184
+ WHERE 1 = 1
185
+ ${buildCreatedAtCondition(productDayServices.createdAt, input.createdAtFrom)}
186
+ AND NOT EXISTS (
187
+ SELECT 1
188
+ FROM ${actionLedgerEntries}
189
+ WHERE ${actionLedgerEntries.actionName} = ${PRODUCT_DAY_SERVICE_CREATE_ACTION_NAME}
190
+ AND ${actionLedgerEntries.targetType} = ${"product"}
191
+ AND ${actionLedgerEntries.targetId} = ${productItineraries.productId}
192
+ )
193
+ ) missing
194
+ `,
195
+ product_media: sql `
196
+ SELECT
197
+ 'product_media' AS check,
198
+ count(*)::int AS missing_count,
199
+ coalesce(
200
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
201
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
202
+ ARRAY[]::text[]
203
+ ) AS sample_ids
204
+ FROM (
205
+ SELECT
206
+ ${productMedia.id} AS candidate_id,
207
+ ${productMedia.createdAt} AS created_at,
208
+ row_number() OVER (
209
+ ORDER BY ${productMedia.createdAt} DESC, ${productMedia.id} DESC
210
+ ) AS sample_ordinal
211
+ FROM ${productMedia}
212
+ WHERE 1 = 1
213
+ ${buildCreatedAtCondition(productMedia.createdAt, input.createdAtFrom)}
214
+ AND NOT EXISTS (
215
+ SELECT 1
216
+ FROM ${actionLedgerEntries}
217
+ WHERE ${actionLedgerEntries.actionName} = CASE
218
+ WHEN ${productMedia.isBrochure} = true THEN ${PRODUCT_BROCHURE_CREATE_ACTION_NAME}
219
+ WHEN ${productMedia.dayId} IS NOT NULL THEN ${PRODUCT_DAY_MEDIA_CREATE_ACTION_NAME}
220
+ ELSE ${PRODUCT_MEDIA_CREATE_ACTION_NAME}
221
+ END
222
+ AND ${actionLedgerEntries.targetType} = ${"product"}
223
+ AND ${actionLedgerEntries.targetId} = ${productMedia.productId}
224
+ )
225
+ ) missing
226
+ `,
227
+ product_capability: sql `
228
+ SELECT
229
+ 'product_capability' AS check,
230
+ count(*)::int AS missing_count,
231
+ coalesce(
232
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
233
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
234
+ ARRAY[]::text[]
235
+ ) AS sample_ids
236
+ FROM (
237
+ SELECT
238
+ ${productCapabilities.id} AS candidate_id,
239
+ ${productCapabilities.createdAt} AS created_at,
240
+ row_number() OVER (
241
+ ORDER BY ${productCapabilities.createdAt} DESC, ${productCapabilities.id} DESC
242
+ ) AS sample_ordinal
243
+ FROM ${productCapabilities}
244
+ WHERE 1 = 1
245
+ ${buildCreatedAtCondition(productCapabilities.createdAt, input.createdAtFrom)}
246
+ AND NOT EXISTS (
247
+ SELECT 1
248
+ FROM ${actionLedgerEntries}
249
+ WHERE ${actionLedgerEntries.actionName} = ${PRODUCT_CAPABILITY_CREATE_ACTION_NAME}
250
+ AND ${actionLedgerEntries.targetType} = ${"product"}
251
+ AND ${actionLedgerEntries.targetId} = ${productCapabilities.productId}
252
+ )
253
+ ) missing
254
+ `,
255
+ product_delivery_format: sql `
256
+ SELECT
257
+ 'product_delivery_format' AS check,
258
+ count(*)::int AS missing_count,
259
+ coalesce(
260
+ array_agg(candidate_id ORDER BY created_at DESC, candidate_id DESC)
261
+ FILTER (WHERE sample_ordinal <= ${sampleLimit}),
262
+ ARRAY[]::text[]
263
+ ) AS sample_ids
264
+ FROM (
265
+ SELECT
266
+ ${productDeliveryFormats.id} AS candidate_id,
267
+ ${productDeliveryFormats.createdAt} AS created_at,
268
+ row_number() OVER (
269
+ ORDER BY ${productDeliveryFormats.createdAt} DESC, ${productDeliveryFormats.id} DESC
270
+ ) AS sample_ordinal
271
+ FROM ${productDeliveryFormats}
272
+ WHERE 1 = 1
273
+ ${buildCreatedAtCondition(productDeliveryFormats.createdAt, input.createdAtFrom)}
274
+ AND NOT EXISTS (
275
+ SELECT 1
276
+ FROM ${actionLedgerEntries}
277
+ WHERE ${actionLedgerEntries.actionName} = ${PRODUCT_DELIVERY_FORMAT_CREATE_ACTION_NAME}
278
+ AND ${actionLedgerEntries.targetType} = ${"product"}
279
+ AND ${actionLedgerEntries.targetId} = ${productDeliveryFormats.productId}
280
+ )
281
+ ) missing
282
+ `,
283
+ };
284
+ }
285
+ export async function checkProductActionLedgerDrift(db, input = {}) {
286
+ const queries = buildProductActionLedgerDriftQueries(input);
287
+ const results = await Promise.all([
288
+ db.execute(queries.product),
289
+ db.execute(queries.product_option),
290
+ db.execute(queries.option_unit),
291
+ db.execute(queries.product_itinerary),
292
+ db.execute(queries.product_day),
293
+ db.execute(queries.product_day_service),
294
+ db.execute(queries.product_media),
295
+ db.execute(queries.product_capability),
296
+ db.execute(queries.product_delivery_format),
297
+ ]);
298
+ const rows = results
299
+ .flatMap((result) => extractRows(result))
300
+ .map((row) => normalizeRow(row));
301
+ return {
302
+ ok: rows.every((row) => row.missingCount === 0),
303
+ rows,
304
+ };
305
+ }
306
+ function normalizeSampleLimit(limit) {
307
+ if (!limit)
308
+ return DEFAULT_SAMPLE_LIMIT;
309
+ return Math.min(Math.max(Math.trunc(limit), 1), MAX_SAMPLE_LIMIT);
310
+ }
311
+ function buildCreatedAtCondition(column, value) {
312
+ if (!value)
313
+ return sql ``;
314
+ const date = value instanceof Date ? value : new Date(value);
315
+ if (Number.isNaN(date.getTime())) {
316
+ throw new Error("createdAtFrom must be a valid date");
317
+ }
318
+ return sql `AND ${column} >= ${date}`;
319
+ }
320
+ function extractRows(result) {
321
+ if (Array.isArray(result))
322
+ return result;
323
+ const maybeRows = result.rows;
324
+ return Array.isArray(maybeRows) ? maybeRows : [];
325
+ }
326
+ function normalizeRow(row) {
327
+ return {
328
+ check: row.check,
329
+ missingCount: Number(row.missing_count),
330
+ sampleIds: row.sample_ids ?? [],
331
+ };
332
+ }
333
+ export const __test__ = {
334
+ normalizeRow,
335
+ };
@@ -0,0 +1,104 @@
1
+ import { type ActionLedgerRequestContextValues } from "@voyantjs/action-ledger/request-context";
2
+ import { type ActionLedgerTargetTimelinePage } from "@voyantjs/action-ledger/timeline";
3
+ import type { Context } from "hono";
4
+ import type { Env } from "./route-env.js";
5
+ import type { Product } from "./schema.js";
6
+ export declare const productActionLedgerQuerySchema: import("zod").ZodPipe<import("zod").ZodObject<{
7
+ cursorOccurredAt: import("zod").ZodOptional<import("zod").ZodString>;
8
+ cursorId: import("zod").ZodOptional<import("zod").ZodString>;
9
+ limit: import("zod").ZodOptional<import("zod").ZodCoercedNumber<unknown>>;
10
+ }, import("zod/v4/core").$strip>, import("zod").ZodTransform<{
11
+ cursor: {
12
+ occurredAt: string;
13
+ id: string;
14
+ } | undefined;
15
+ limit?: number | undefined;
16
+ }, {
17
+ cursorOccurredAt?: string | undefined;
18
+ cursorId?: string | undefined;
19
+ limit?: number | undefined;
20
+ }>>;
21
+ export type ProductActionLedgerListResponse = ActionLedgerTargetTimelinePage;
22
+ export type ProductLedgerMutationAction = "create" | "update" | "delete" | "duplicate";
23
+ export declare function getProductActionLedgerRequestContext(c: Context<Env>): ActionLedgerRequestContextValues;
24
+ export declare function changedProductFields(input: Partial<Product>, before: Product | null, after: Product | null): string[];
25
+ export declare function changedMutationFields(input: object, before: Record<string, unknown> | null, after: Record<string, unknown> | null): string[];
26
+ export declare function productMutationSummary(action: ProductLedgerMutationAction, fields: string[], subject?: string): string;
27
+ export declare function appendProductMutationLedgerEntry(c: Context<Env>, input: {
28
+ action: ProductLedgerMutationAction;
29
+ productId: string;
30
+ changedFields: string[];
31
+ subject?: string;
32
+ actionName?: string;
33
+ routeOrToolName?: string;
34
+ summary?: string;
35
+ }): Promise<import("@voyantjs/action-ledger/service").AppendActionLedgerEntryResult>;
36
+ export declare function listProductActionLedger(c: Context<Env>): Promise<(Response & import("hono").TypedResponse<{
37
+ error: string;
38
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
39
+ data: {
40
+ id: string;
41
+ actionName: string;
42
+ actionVersion: string;
43
+ actionKind: "execute" | "reverse" | "update" | "delete" | "read" | "create" | "approve" | "reject" | "compensate" | "duplicate";
44
+ status: "requested" | "awaiting_approval" | "approved" | "denied" | "succeeded" | "failed" | "reversed" | "compensated" | "expired" | "cancelled" | "superseded";
45
+ evaluatedRisk: "low" | "medium" | "high" | "critical";
46
+ actorType: string | null;
47
+ principalType: "user" | "api_key" | "agent" | "workflow" | "system";
48
+ principalId: string;
49
+ principalSubtype: string | null;
50
+ sessionId: string | null;
51
+ apiTokenId: string | null;
52
+ internalRequest: boolean;
53
+ delegatedByPrincipalType: "user" | "api_key" | "agent" | "workflow" | "system" | null;
54
+ delegatedByPrincipalId: string | null;
55
+ delegationId: string | null;
56
+ callerType: string | null;
57
+ organizationId: string | null;
58
+ routeOrToolName: string | null;
59
+ workflowRunId: string | null;
60
+ workflowStepId: string | null;
61
+ correlationId: string | null;
62
+ causationActionId: string | null;
63
+ idempotencyScope: string | null;
64
+ idempotencyKey: string | null;
65
+ idempotencyFingerprint: string | null;
66
+ targetType: string;
67
+ targetId: string;
68
+ capabilityId: string | null;
69
+ capabilityVersion: string | null;
70
+ authorizationSource: string | null;
71
+ approvalId: string | null;
72
+ amendsActionId: string | null;
73
+ occurredAt: string;
74
+ createdAt: string;
75
+ mutationSummary: string | null;
76
+ }[];
77
+ pageInfo: {
78
+ nextCursor: {
79
+ occurredAt: string;
80
+ id: string;
81
+ } | null;
82
+ };
83
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
84
+ export declare const __test__: {
85
+ changedMutationFields: typeof changedMutationFields;
86
+ changedProductFields: typeof changedProductFields;
87
+ productMutationSummary: typeof productMutationSummary;
88
+ productActionLedgerQuerySchema: import("zod").ZodPipe<import("zod").ZodObject<{
89
+ cursorOccurredAt: import("zod").ZodOptional<import("zod").ZodString>;
90
+ cursorId: import("zod").ZodOptional<import("zod").ZodString>;
91
+ limit: import("zod").ZodOptional<import("zod").ZodCoercedNumber<unknown>>;
92
+ }, import("zod/v4/core").$strip>, import("zod").ZodTransform<{
93
+ cursor: {
94
+ occurredAt: string;
95
+ id: string;
96
+ } | undefined;
97
+ limit?: number | undefined;
98
+ }, {
99
+ cursorOccurredAt?: string | undefined;
100
+ cursorId?: string | undefined;
101
+ limit?: number | undefined;
102
+ }>>;
103
+ };
104
+ //# sourceMappingURL=action-ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-ledger.d.ts","sourceRoot":"","sources":["../src/action-ledger.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gCAAgC,EAEtC,MAAM,yCAAyC,CAAA;AAEhD,OAAO,EACL,KAAK,8BAA8B,EAGpC,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAG1C,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;GAAwC,CAAA;AAEnF,MAAM,MAAM,+BAA+B,GAAG,8BAA8B,CAAA;AAC5E,MAAM,MAAM,2BAA2B,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAA;AAEtF,wBAAgB,oCAAoC,CAClD,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GACd,gCAAgC,CAgBlC;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,EACvB,MAAM,EAAE,OAAO,GAAG,IAAI,EACtB,KAAK,EAAE,OAAO,GAAG,IAAI,GACpB,MAAM,EAAE,CAEV;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACtC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACpC,MAAM,EAAE,CAKV;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,2BAA2B,EACnC,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,SAAY,UASpB;AAED,wBAAsB,gCAAgC,CACpD,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,EACf,KAAK,EAAE;IACL,MAAM,EAAE,2BAA2B,CAAA;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,oFAgBF;AAED,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAmC5D;AAcD,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;CAKpB,CAAA"}
@@ -0,0 +1,100 @@
1
+ import { appendActionLedgerMutation, } from "@voyantjs/action-ledger/request-context";
2
+ import { actionLedgerService } from "@voyantjs/action-ledger/service";
3
+ import { actionLedgerTargetTimelineQuerySchema, buildActionLedgerTargetTimelinePage, } from "@voyantjs/action-ledger/timeline";
4
+ import { parseQuery } from "@voyantjs/hono";
5
+ import { productsService } from "./service.js";
6
+ export const productActionLedgerQuerySchema = actionLedgerTargetTimelineQuerySchema;
7
+ export function getProductActionLedgerRequestContext(c) {
8
+ return {
9
+ userId: c.get("userId") ?? null,
10
+ agentId: c.get("agentId") ?? null,
11
+ workflowPrincipalId: c.get("workflowPrincipalId") ?? null,
12
+ principalSubtype: c.get("principalSubtype") ?? null,
13
+ sessionId: c.get("sessionId") ?? null,
14
+ apiTokenId: c.get("apiTokenId") ?? c.get("apiKeyId") ?? null,
15
+ callerType: c.get("callerType") ?? null,
16
+ actor: c.get("actor") ?? null,
17
+ isInternalRequest: c.get("isInternalRequest") ?? false,
18
+ organizationId: c.get("organizationId") ?? null,
19
+ workflowRunId: c.get("workflowRunId") ?? null,
20
+ workflowStepId: c.get("workflowStepId") ?? null,
21
+ correlationId: c.req.header("x-correlation-id") ?? c.req.header("x-request-id") ?? null,
22
+ };
23
+ }
24
+ export function changedProductFields(input, before, after) {
25
+ return changedMutationFields(input, before, after);
26
+ }
27
+ export function changedMutationFields(input, before, after) {
28
+ const fields = Object.keys(input).filter((field) => !ignoredMutationFields.has(field));
29
+ if (!before || !after)
30
+ return fields.sort();
31
+ return fields.filter((field) => !productValuesEqual(before[field], after[field])).sort();
32
+ }
33
+ export function productMutationSummary(action, fields, subject = "product") {
34
+ const formattedSubject = subject.trim() || "product";
35
+ if (action === "delete")
36
+ return `Deleted ${formattedSubject}`;
37
+ if (action === "duplicate")
38
+ return `Duplicated ${formattedSubject}`;
39
+ if (fields.length === 0)
40
+ return action === "create" ? `Created ${formattedSubject}` : `Updated ${formattedSubject}`;
41
+ const verb = action === "create" ? "Created" : "Updated";
42
+ return `${verb} ${formattedSubject} fields: ${fields.join(", ")}`;
43
+ }
44
+ export async function appendProductMutationLedgerEntry(c, input) {
45
+ return appendActionLedgerMutation(c.get("db"), {
46
+ context: getProductActionLedgerRequestContext(c),
47
+ actionName: input.actionName ?? `product.${input.action}`,
48
+ actionKind: input.action === "duplicate" ? "create" : input.action,
49
+ evaluatedRisk: "medium",
50
+ targetType: "product",
51
+ targetId: input.productId,
52
+ routeOrToolName: input.routeOrToolName ?? `products.${input.action}`,
53
+ mutationDetail: {
54
+ summary: input.summary ?? productMutationSummary(input.action, input.changedFields, input.subject),
55
+ reversalKind: "none",
56
+ },
57
+ });
58
+ }
59
+ export async function listProductActionLedger(c) {
60
+ const productId = c.req.param("id");
61
+ if (!productId)
62
+ return c.json({ error: "Product not found" }, 404);
63
+ const product = await productsService.getProductById(c.get("db"), productId);
64
+ if (!product)
65
+ return c.json({ error: "Product not found" }, 404);
66
+ const query = parseQuery(c, productActionLedgerQuerySchema);
67
+ const limit = query.limit ?? 50;
68
+ const result = await actionLedgerService.listEntries(c.get("db"), {
69
+ targetType: "product",
70
+ targetId: product.id,
71
+ cursor: query.cursor,
72
+ limit: limit + 1,
73
+ });
74
+ const page = buildActionLedgerTargetTimelinePage({
75
+ entries: result.entries,
76
+ limit,
77
+ });
78
+ const details = await Promise.all(page.data.map((entry) => actionLedgerService.getEntry(c.get("db"), entry.id)));
79
+ const summariesByActionId = new Map(details.flatMap((detail) => detail ? [[detail.entry.id, detail.mutationDetail?.summary ?? null]] : []));
80
+ return c.json(buildActionLedgerTargetTimelinePage({
81
+ entries: result.entries,
82
+ limit,
83
+ mutationSummariesByActionId: summariesByActionId,
84
+ }));
85
+ }
86
+ function productValuesEqual(left, right) {
87
+ if (left instanceof Date || right instanceof Date) {
88
+ const leftTime = left instanceof Date ? left.getTime() : new Date(String(left)).getTime();
89
+ const rightTime = right instanceof Date ? right.getTime() : new Date(String(right)).getTime();
90
+ return leftTime === rightTime;
91
+ }
92
+ return JSON.stringify(left) === JSON.stringify(right);
93
+ }
94
+ const ignoredMutationFields = new Set(["updatedAt", "createdAt"]);
95
+ export const __test__ = {
96
+ changedMutationFields,
97
+ changedProductFields,
98
+ productMutationSummary,
99
+ productActionLedgerQuerySchema,
100
+ };
@@ -244,9 +244,9 @@ export declare const bookingProductExtensionService: {
244
244
  upsertBookingDetails(db: PostgresJsDatabase, bookingId: string, data: z.infer<typeof bookingProductDetailSchema>): Promise<{
245
245
  createdAt: Date;
246
246
  updatedAt: Date;
247
- bookingId: string;
248
247
  productId: string | null;
249
248
  optionId: string | null;
249
+ bookingId: string;
250
250
  } | null>;
251
251
  removeBookingDetails(db: PostgresJsDatabase, bookingId: string): Promise<{
252
252
  bookingId: string;
@@ -265,9 +265,9 @@ export declare const bookingProductExtensionService: {
265
265
  updatedAt: Date;
266
266
  productId: string | null;
267
267
  optionId: string | null;
268
- bookingItemId: string;
269
- unitId: string | null;
270
268
  supplierServiceId: string | null;
269
+ unitId: string | null;
270
+ bookingItemId: string;
271
271
  } | null>;
272
272
  removeItemDetails(db: PostgresJsDatabase, bookingItemId: string): Promise<{
273
273
  bookingItemId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,EAshB3C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,2CAA0C,CAAA;AAE3E,OAAO,EAAE,oBAAoB,EAAE,CAAA"}
1
+ {"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,EAwiB3C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,2CAA0C,CAAA;AAE3E,OAAO,EAAE,oBAAoB,EAAE,CAAA"}
@@ -377,6 +377,24 @@ const PRODUCT_FIELD_POLICY = [
377
377
  overrideFriction: "none",
378
378
  sourceFreshness: "sync",
379
379
  },
380
+ {
381
+ path: "availableDeparturesCount",
382
+ class: "merchandisable",
383
+ merge: "source-only",
384
+ // High-drift because the count moves whenever availability_slots are
385
+ // added/removed or close-outs change; reindex-on-entry keeps it warm
386
+ // enough for the operator catalog table without being load-bearing
387
+ // for downstream booking flows.
388
+ drift: "high",
389
+ reindex: "facet-affecting",
390
+ snapshot: "never",
391
+ query: "indexed-column",
392
+ localized: false,
393
+ visibility: ["staff", "customer", "partner"],
394
+ editRole: "none",
395
+ overrideFriction: "none",
396
+ sourceFreshness: "sync",
397
+ },
380
398
  {
381
399
  path: "durationDays",
382
400
  class: "structural",
@@ -77,6 +77,7 @@ export declare const productDaySchema: z.ZodObject<{
77
77
  title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
78
78
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
79
79
  location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
80
+ hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
80
81
  services: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
81
82
  }, z.core.$strip>;
82
83
  export declare const productPolicySchema: z.ZodObject<{
@@ -150,6 +151,7 @@ export declare const productContentSchema: z.ZodObject<{
150
151
  title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
151
152
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
152
153
  location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
154
+ hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
153
155
  services: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
154
156
  }, z.core.$strip>>>;
155
157
  media: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"content-shape.d.ts","sourceRoot":"","sources":["../src/content-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAE1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;GAGG;AACH,eAAO,MAAM,+BAA+B,gBAAgB,CAAA;AAE5D;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;iBAe/B,CAAA;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;iBAKjC,CAAA;AAEF,eAAO,MAAM,uBAAuB;;;;;;iBAMlC,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAM9B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;iBAM3B,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;iBAK9B,CAAA;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;iBAejC,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO/B,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACrE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACrE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACzD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAE/D;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,GACf;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAY7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,aAAa,CAAC,cAAc,CAAC,EACvC,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,gBAAgB,CAAM,GACzD,cAAc,CAchB"}
1
+ {"version":3,"file":"content-shape.d.ts","sourceRoot":"","sources":["../src/content-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAE1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;GAGG;AACH,eAAO,MAAM,+BAA+B,gBAAgB,CAAA;AAE5D;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;iBAe/B,CAAA;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;iBAKjC,CAAA;AAEF,eAAO,MAAM,uBAAuB;;;;;;iBAMlC,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAM9B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;iBAQ3B,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;iBAK9B,CAAA;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;iBAejC,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO/B,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACrE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACrE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACzD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAE/D;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,GACf;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAY7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,aAAa,CAAC,cAAc,CAAC,EACvC,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,gBAAgB,CAAM,GACzD,cAAc,CAchB"}
@@ -67,6 +67,8 @@ export const productDaySchema = z.object({
67
67
  title: z.string().nullable().optional(),
68
68
  description: z.string().nullable().optional(),
69
69
  location: z.string().nullable().optional(),
70
+ /** Day-level hero image (catalog detail sheet thumbnail). */
71
+ hero_image_url: z.string().nullable().optional(),
70
72
  services: z.array(z.string()).optional().default([]),
71
73
  });
72
74
  export const productPolicySchema = z.object({