@voyantjs/products 0.20.0 → 0.21.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 (70) hide show
  1. package/dist/booking-engine/handler.d.ts +203 -0
  2. package/dist/booking-engine/handler.d.ts.map +1 -0
  3. package/dist/booking-engine/handler.js +330 -0
  4. package/dist/booking-engine/index.d.ts +8 -0
  5. package/dist/booking-engine/index.d.ts.map +1 -0
  6. package/dist/booking-engine/index.js +7 -0
  7. package/dist/catalog-policy.d.ts.map +1 -1
  8. package/dist/catalog-policy.js +15 -1
  9. package/dist/content-shape.d.ts +217 -0
  10. package/dist/content-shape.d.ts.map +1 -0
  11. package/dist/content-shape.js +159 -0
  12. package/dist/draft-shape.d.ts +43 -0
  13. package/dist/draft-shape.d.ts.map +1 -0
  14. package/dist/draft-shape.js +46 -0
  15. package/dist/events.d.ts +37 -0
  16. package/dist/events.d.ts.map +1 -0
  17. package/dist/events.js +32 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/routes-content.d.ts +74 -0
  22. package/dist/routes-content.d.ts.map +1 -0
  23. package/dist/routes-content.js +117 -0
  24. package/dist/routes.d.ts +40 -20
  25. package/dist/routes.d.ts.map +1 -1
  26. package/dist/routes.js +83 -13
  27. package/dist/schema-core.d.ts +240 -1
  28. package/dist/schema-core.d.ts.map +1 -1
  29. package/dist/schema-core.js +49 -0
  30. package/dist/schema-itinerary.d.ts +18 -1
  31. package/dist/schema-itinerary.d.ts.map +1 -1
  32. package/dist/schema-itinerary.js +1 -0
  33. package/dist/schema-settings.d.ts +1 -1
  34. package/dist/schema-sourced-content.d.ts +262 -0
  35. package/dist/schema-sourced-content.d.ts.map +1 -0
  36. package/dist/schema-sourced-content.js +69 -0
  37. package/dist/schema-taxonomy.d.ts +17 -0
  38. package/dist/schema-taxonomy.d.ts.map +1 -1
  39. package/dist/schema-taxonomy.js +13 -0
  40. package/dist/schema.d.ts +1 -0
  41. package/dist/schema.d.ts.map +1 -1
  42. package/dist/schema.js +1 -0
  43. package/dist/service-catalog-plane.d.ts.map +1 -1
  44. package/dist/service-catalog-plane.js +1 -0
  45. package/dist/service-content-owned.d.ts +68 -0
  46. package/dist/service-content-owned.d.ts.map +1 -0
  47. package/dist/service-content-owned.js +224 -0
  48. package/dist/service-content-synthesizer.d.ts +90 -0
  49. package/dist/service-content-synthesizer.d.ts.map +1 -0
  50. package/dist/service-content-synthesizer.js +171 -0
  51. package/dist/service-content.d.ts +106 -0
  52. package/dist/service-content.d.ts.map +1 -0
  53. package/dist/service-content.js +365 -0
  54. package/dist/service.d.ts +76 -22
  55. package/dist/service.d.ts.map +1 -1
  56. package/dist/service.js +4 -0
  57. package/dist/tasks/brochures.d.ts +1 -0
  58. package/dist/tasks/brochures.d.ts.map +1 -1
  59. package/dist/tasks/brochures.js +3 -0
  60. package/dist/validation-catalog.d.ts +4 -4
  61. package/dist/validation-config.d.ts +3 -3
  62. package/dist/validation-content.d.ts +34 -4
  63. package/dist/validation-content.d.ts.map +1 -1
  64. package/dist/validation-content.js +13 -0
  65. package/dist/validation-core.d.ts +53 -3
  66. package/dist/validation-core.d.ts.map +1 -1
  67. package/dist/validation-core.js +16 -0
  68. package/dist/validation-public.d.ts +9 -9
  69. package/dist/validation-shared.d.ts +4 -4
  70. package/package.json +12 -7
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Products content shape — the rich detail-page content shape returned
3
+ * by `getContent` and stored in `products_sourced_content.payload`.
4
+ *
5
+ * Schema versions are managed by this module: the constant
6
+ * `PRODUCTS_CONTENT_SCHEMA_VERSION` stamps every cache write; reads
7
+ * skip rows with an unrecognized version (treated as cache miss). Bump
8
+ * the version when the shape changes; old cache rows are then evicted
9
+ * by a single `DELETE WHERE content_schema_version != current`.
10
+ *
11
+ * Pure types + Zod + a vertical-specific `mergeOverlaysIntoProductContent`
12
+ * that wraps the catalog plane's content-shape-aware merger with this
13
+ * vertical's validator.
14
+ *
15
+ * See `docs/architecture/catalog-sourced-content.md` §3.2, §3.5.4, §3.6.
16
+ */
17
+ import { type ContentOverlay, type MergeOverlaysOptions } from "@voyantjs/catalog";
18
+ import { z } from "zod";
19
+ /**
20
+ * The current content-schema version. Stamped on every cache write.
21
+ * Bump when the `productContentSchema` shape changes incompatibly.
22
+ */
23
+ export declare const PRODUCTS_CONTENT_SCHEMA_VERSION = "products/v1";
24
+ /**
25
+ * Top-level product summary fields. Maps loosely to the owned `products`
26
+ * table — the read service synthesizes from indexed projection + overlay
27
+ * for thin adapters, or stores adapter-served data for rich ones.
28
+ */
29
+ export declare const productSummarySchema: z.ZodObject<{
30
+ id: z.ZodString;
31
+ name: z.ZodString;
32
+ status: z.ZodOptional<z.ZodString>;
33
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
34
+ highlights: z.ZodOptional<z.ZodArray<z.ZodString>>;
35
+ hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
36
+ duration_days: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
37
+ start_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
38
+ end_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
39
+ sell_currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
40
+ supplier: z.ZodOptional<z.ZodNullable<z.ZodString>>;
41
+ country: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
+ departure_city: z.ZodOptional<z.ZodNullable<z.ZodString>>;
43
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
44
+ }, z.core.$strip>;
45
+ export declare const productMediaItemSchema: z.ZodObject<{
46
+ url: z.ZodString;
47
+ type: z.ZodDefault<z.ZodEnum<{
48
+ image: "image";
49
+ video: "video";
50
+ document: "document";
51
+ }>>;
52
+ caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
53
+ alt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
54
+ }, z.core.$strip>;
55
+ export declare const productOptionUnitSchema: z.ZodObject<{
56
+ id: z.ZodString;
57
+ type: z.ZodString;
58
+ label: z.ZodOptional<z.ZodString>;
59
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
60
+ capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
61
+ }, z.core.$strip>;
62
+ export declare const productOptionSchema: z.ZodObject<{
63
+ id: z.ZodString;
64
+ name: z.ZodString;
65
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
+ units: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
67
+ id: z.ZodString;
68
+ type: z.ZodString;
69
+ label: z.ZodOptional<z.ZodString>;
70
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
71
+ capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
72
+ }, z.core.$strip>>>>;
73
+ inclusions: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
74
+ }, z.core.$strip>;
75
+ export declare const productDaySchema: z.ZodObject<{
76
+ day_number: z.ZodNumber;
77
+ title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
78
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
79
+ location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
80
+ services: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
81
+ }, z.core.$strip>;
82
+ export declare const productPolicySchema: z.ZodObject<{
83
+ kind: z.ZodEnum<{
84
+ supplier_notes: "supplier_notes";
85
+ cancellation: "cancellation";
86
+ payment: "payment";
87
+ requirements: "requirements";
88
+ }>;
89
+ body: z.ZodString;
90
+ rules: z.ZodOptional<z.ZodUnknown>;
91
+ }, z.core.$strip>;
92
+ /**
93
+ * A single bookable departure / time slot — the "when" surface of the
94
+ * product. ISO 8601 timestamps for `starts_at` / `ends_at` so locale
95
+ * formatting happens at render time, never in the cache.
96
+ *
97
+ * Owned products derive these from `availability_slots`; sourced
98
+ * adapters return them via `getContent`. Empty array = "always-on"
99
+ * product (e.g. an evergreen transfer service) or one whose schedule
100
+ * is on-request.
101
+ */
102
+ export declare const productDepartureSchema: z.ZodObject<{
103
+ id: z.ZodString;
104
+ starts_at: z.ZodString;
105
+ ends_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
106
+ status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
107
+ capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
108
+ remaining: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
109
+ lowest_price_cents: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
110
+ currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
111
+ note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
112
+ }, z.core.$strip>;
113
+ /**
114
+ * The product content payload. Cache writes validate against this
115
+ * schema; cache reads skip rows that don't validate (treated as cache
116
+ * miss to surface adapter integration bugs without corrupting reads).
117
+ */
118
+ export declare const productContentSchema: z.ZodObject<{
119
+ product: z.ZodObject<{
120
+ id: z.ZodString;
121
+ name: z.ZodString;
122
+ status: z.ZodOptional<z.ZodString>;
123
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
124
+ highlights: z.ZodOptional<z.ZodArray<z.ZodString>>;
125
+ hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
126
+ duration_days: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
127
+ start_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
128
+ end_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
129
+ sell_currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
130
+ supplier: z.ZodOptional<z.ZodNullable<z.ZodString>>;
131
+ country: z.ZodOptional<z.ZodNullable<z.ZodString>>;
132
+ departure_city: z.ZodOptional<z.ZodNullable<z.ZodString>>;
133
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
134
+ }, z.core.$strip>;
135
+ options: z.ZodDefault<z.ZodArray<z.ZodObject<{
136
+ id: z.ZodString;
137
+ name: z.ZodString;
138
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
139
+ units: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
140
+ id: z.ZodString;
141
+ type: z.ZodString;
142
+ label: z.ZodOptional<z.ZodString>;
143
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
144
+ capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
145
+ }, z.core.$strip>>>>;
146
+ inclusions: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
147
+ }, z.core.$strip>>>;
148
+ days: z.ZodDefault<z.ZodArray<z.ZodObject<{
149
+ day_number: z.ZodNumber;
150
+ title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
151
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
152
+ location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
153
+ services: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
154
+ }, z.core.$strip>>>;
155
+ media: z.ZodDefault<z.ZodArray<z.ZodObject<{
156
+ url: z.ZodString;
157
+ type: z.ZodDefault<z.ZodEnum<{
158
+ image: "image";
159
+ video: "video";
160
+ document: "document";
161
+ }>>;
162
+ caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
163
+ alt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
164
+ }, z.core.$strip>>>;
165
+ policies: z.ZodDefault<z.ZodArray<z.ZodObject<{
166
+ kind: z.ZodEnum<{
167
+ supplier_notes: "supplier_notes";
168
+ cancellation: "cancellation";
169
+ payment: "payment";
170
+ requirements: "requirements";
171
+ }>;
172
+ body: z.ZodString;
173
+ rules: z.ZodOptional<z.ZodUnknown>;
174
+ }, z.core.$strip>>>;
175
+ departures: z.ZodDefault<z.ZodArray<z.ZodObject<{
176
+ id: z.ZodString;
177
+ starts_at: z.ZodString;
178
+ ends_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
179
+ status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
180
+ capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
181
+ remaining: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
182
+ lowest_price_cents: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
183
+ currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
184
+ note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
185
+ }, z.core.$strip>>>;
186
+ }, z.core.$strip>;
187
+ export type ProductContent = z.infer<typeof productContentSchema>;
188
+ export type ProductSummary = z.infer<typeof productSummarySchema>;
189
+ export type ProductMediaItem = z.infer<typeof productMediaItemSchema>;
190
+ export type ProductOption = z.infer<typeof productOptionSchema>;
191
+ export type ProductDeparture = z.infer<typeof productDepartureSchema>;
192
+ export type ProductDay = z.infer<typeof productDaySchema>;
193
+ export type ProductPolicy = z.infer<typeof productPolicySchema>;
194
+ /**
195
+ * Validate a `ProductContent` payload. Returns the parsed result on
196
+ * success or a structured failure on rejection. Used by the cache write
197
+ * path and by `mergeOverlaysIntoProductContent` to gate overlay merges.
198
+ */
199
+ export declare function validateProductContent(payload: unknown): {
200
+ valid: true;
201
+ content: ProductContent;
202
+ } | {
203
+ valid: false;
204
+ reason: string;
205
+ };
206
+ /**
207
+ * Apply a list of editorial overlays to a product content payload via
208
+ * RFC 6901 JSON pointers. Validates the merged result against the
209
+ * vertical's Zod schema; overlays that produce an invalid payload are
210
+ * rolled back and reported via `onOverlayError`.
211
+ *
212
+ * Per sourced-content §3.5.4, this is the "content-shape-aware merger"
213
+ * — the catalog plane stays neutral about the content shape; the
214
+ * vertical plugs in its validator here.
215
+ */
216
+ export declare function mergeOverlaysIntoProductContent(payload: ProductContent, overlays: ReadonlyArray<ContentOverlay>, options?: Pick<MergeOverlaysOptions, "onOverlayError">): ProductContent;
217
+ //# sourceMappingURL=content-shape.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Products content shape — the rich detail-page content shape returned
3
+ * by `getContent` and stored in `products_sourced_content.payload`.
4
+ *
5
+ * Schema versions are managed by this module: the constant
6
+ * `PRODUCTS_CONTENT_SCHEMA_VERSION` stamps every cache write; reads
7
+ * skip rows with an unrecognized version (treated as cache miss). Bump
8
+ * the version when the shape changes; old cache rows are then evicted
9
+ * by a single `DELETE WHERE content_schema_version != current`.
10
+ *
11
+ * Pure types + Zod + a vertical-specific `mergeOverlaysIntoProductContent`
12
+ * that wraps the catalog plane's content-shape-aware merger with this
13
+ * vertical's validator.
14
+ *
15
+ * See `docs/architecture/catalog-sourced-content.md` §3.2, §3.5.4, §3.6.
16
+ */
17
+ import { mergeOverlaysIntoContent, } from "@voyantjs/catalog";
18
+ import { z } from "zod";
19
+ /**
20
+ * The current content-schema version. Stamped on every cache write.
21
+ * Bump when the `productContentSchema` shape changes incompatibly.
22
+ */
23
+ export const PRODUCTS_CONTENT_SCHEMA_VERSION = "products/v1";
24
+ /**
25
+ * Top-level product summary fields. Maps loosely to the owned `products`
26
+ * table — the read service synthesizes from indexed projection + overlay
27
+ * for thin adapters, or stores adapter-served data for rich ones.
28
+ */
29
+ export const productSummarySchema = z.object({
30
+ id: z.string(),
31
+ name: z.string(),
32
+ status: z.string().optional(),
33
+ description: z.string().nullable().optional(),
34
+ highlights: z.array(z.string()).optional(),
35
+ hero_image_url: z.string().nullable().optional(),
36
+ duration_days: z.number().int().nonnegative().nullable().optional(),
37
+ start_date: z.string().nullable().optional(),
38
+ end_date: z.string().nullable().optional(),
39
+ sell_currency: z.string().nullable().optional(),
40
+ supplier: z.string().nullable().optional(),
41
+ country: z.string().nullable().optional(),
42
+ departure_city: z.string().nullable().optional(),
43
+ tags: z.array(z.string()).optional(),
44
+ });
45
+ export const productMediaItemSchema = z.object({
46
+ url: z.string(),
47
+ type: z.enum(["image", "video", "document"]).default("image"),
48
+ caption: z.string().nullable().optional(),
49
+ alt: z.string().nullable().optional(),
50
+ });
51
+ export const productOptionUnitSchema = z.object({
52
+ id: z.string(),
53
+ type: z.string(),
54
+ label: z.string().optional(),
55
+ description: z.string().nullable().optional(),
56
+ capacity: z.number().int().nonnegative().nullable().optional(),
57
+ });
58
+ export const productOptionSchema = z.object({
59
+ id: z.string(),
60
+ name: z.string(),
61
+ description: z.string().nullable().optional(),
62
+ units: z.array(productOptionUnitSchema).optional().default([]),
63
+ inclusions: z.array(z.string()).optional().default([]),
64
+ });
65
+ export const productDaySchema = z.object({
66
+ day_number: z.number().int().positive(),
67
+ title: z.string().nullable().optional(),
68
+ description: z.string().nullable().optional(),
69
+ location: z.string().nullable().optional(),
70
+ services: z.array(z.string()).optional().default([]),
71
+ });
72
+ export const productPolicySchema = z.object({
73
+ kind: z.enum(["cancellation", "payment", "supplier_notes", "requirements"]),
74
+ body: z.string(),
75
+ /** Optional structured rules — vertical-specific. */
76
+ rules: z.unknown().optional(),
77
+ });
78
+ /**
79
+ * A single bookable departure / time slot — the "when" surface of the
80
+ * product. ISO 8601 timestamps for `starts_at` / `ends_at` so locale
81
+ * formatting happens at render time, never in the cache.
82
+ *
83
+ * Owned products derive these from `availability_slots`; sourced
84
+ * adapters return them via `getContent`. Empty array = "always-on"
85
+ * product (e.g. an evergreen transfer service) or one whose schedule
86
+ * is on-request.
87
+ */
88
+ export const productDepartureSchema = z.object({
89
+ id: z.string(),
90
+ starts_at: z.string(),
91
+ ends_at: z.string().nullable().optional(),
92
+ /** "open" | "limited" | "sold_out" | "closed" | "on_request" — display only. */
93
+ status: z.string().nullable().optional(),
94
+ /** Total capacity for the slot, when known. */
95
+ capacity: z.number().int().nonnegative().nullable().optional(),
96
+ /** Remaining capacity. Null = unknown / not surfaced; 0 = sold out. */
97
+ remaining: z.number().int().nonnegative().nullable().optional(),
98
+ /** Lowest pricing hint in cents — display only. Real price comes via liveResolve. */
99
+ lowest_price_cents: z.number().int().nonnegative().nullable().optional(),
100
+ currency: z.string().nullable().optional(),
101
+ /** Free-form note (weather caveat, sales window, etc). */
102
+ note: z.string().nullable().optional(),
103
+ });
104
+ /**
105
+ * The product content payload. Cache writes validate against this
106
+ * schema; cache reads skip rows that don't validate (treated as cache
107
+ * miss to surface adapter integration bugs without corrupting reads).
108
+ */
109
+ export const productContentSchema = z.object({
110
+ product: productSummarySchema,
111
+ options: z.array(productOptionSchema).default([]),
112
+ days: z.array(productDaySchema).default([]),
113
+ media: z.array(productMediaItemSchema).default([]),
114
+ policies: z.array(productPolicySchema).default([]),
115
+ departures: z.array(productDepartureSchema).default([]),
116
+ });
117
+ /**
118
+ * Validate a `ProductContent` payload. Returns the parsed result on
119
+ * success or a structured failure on rejection. Used by the cache write
120
+ * path and by `mergeOverlaysIntoProductContent` to gate overlay merges.
121
+ */
122
+ export function validateProductContent(payload) {
123
+ const result = productContentSchema.safeParse(payload);
124
+ if (result.success) {
125
+ return { valid: true, content: result.data };
126
+ }
127
+ // Take the first issue's message — that's enough signal for ops; full
128
+ // detail is available on `result.error.issues` if a caller cares.
129
+ const issue = result.error.issues[0];
130
+ return {
131
+ valid: false,
132
+ reason: issue ? `${issue.path.join(".")}: ${issue.message}` : "validation failed",
133
+ };
134
+ }
135
+ /**
136
+ * Apply a list of editorial overlays to a product content payload via
137
+ * RFC 6901 JSON pointers. Validates the merged result against the
138
+ * vertical's Zod schema; overlays that produce an invalid payload are
139
+ * rolled back and reported via `onOverlayError`.
140
+ *
141
+ * Per sourced-content §3.5.4, this is the "content-shape-aware merger"
142
+ * — the catalog plane stays neutral about the content shape; the
143
+ * vertical plugs in its validator here.
144
+ */
145
+ export function mergeOverlaysIntoProductContent(payload, overlays, options = {}) {
146
+ const merged = mergeOverlaysIntoContent(payload, overlays, {
147
+ validate(p) {
148
+ const result = validateProductContent(p);
149
+ if (result.valid) {
150
+ return { valid: true };
151
+ }
152
+ return { valid: false, reason: result.reason };
153
+ },
154
+ onOverlayError: options.onOverlayError,
155
+ });
156
+ // The validator gates merges, so a successful merge always parses —
157
+ // re-parse here to satisfy the return type without an unsafe cast.
158
+ return productContentSchema.parse(merged);
159
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Project a `ProductContent` payload into a `BookingDraftShape` so
3
+ * the journey wizard can render the correct sub-steps for a sourced
4
+ * product.
5
+ *
6
+ * Tour-style products typically need:
7
+ * - Configure: occupancy band selection (adult / child / infant)
8
+ * and optionally a date / departure picker if `days[]` carries
9
+ * scheduled departures.
10
+ * - Travelers: per-pax fields (first / last / email; passport when
11
+ * the supplier requires it — surfaced via overlay when known).
12
+ * - Add-ons: the product's own `options[]` projected as add-on
13
+ * offers.
14
+ *
15
+ * No accommodation sub-step today (multi-day tours w/ rooms route
16
+ * through hospitality, not products). Pricing flows through
17
+ * liveResolve at quote time, not the descriptor.
18
+ *
19
+ * See `docs/architecture/booking-journey-architecture.md` §3 + §F.
20
+ */
21
+ import { type BookingDraftShape, type PaxBandSpec } from "@voyantjs/catalog/booking-engine";
22
+ import type { ProductContent } from "./content-shape.js";
23
+ export interface BuildProductDraftShapeOptions {
24
+ /** Locale — used for option-label fallback. Defaults to `"en-GB"`. */
25
+ locale?: string;
26
+ /**
27
+ * Override the default pax bands. Use when the supplier mandates
28
+ * specific age cutoffs (rare for tour products; common for cruises
29
+ * and family-oriented packages).
30
+ */
31
+ paxBands?: ReadonlyArray<PaxBandSpec>;
32
+ /**
33
+ * Override the maximum total pax. Defaults to `paxBands` sum.
34
+ * Useful when supplier capacity is < combined band max (e.g. a
35
+ * 6-pax max party on a private tour).
36
+ */
37
+ paxBandsAllowedTotal?: {
38
+ min: number;
39
+ max: number;
40
+ };
41
+ }
42
+ export declare function buildProductDraftShape(content: ProductContent, options?: BuildProductDraftShapeOptions): BookingDraftShape;
43
+ //# sourceMappingURL=draft-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-shape.d.ts","sourceRoot":"","sources":["../src/draft-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAEL,KAAK,iBAAiB,EAKtB,KAAK,WAAW,EAEjB,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,MAAM,WAAW,6BAA6B;IAC5C,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IACrC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;CACpD;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,6BAAkC,GAC1C,iBAAiB,CA0BnB"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Project a `ProductContent` payload into a `BookingDraftShape` so
3
+ * the journey wizard can render the correct sub-steps for a sourced
4
+ * product.
5
+ *
6
+ * Tour-style products typically need:
7
+ * - Configure: occupancy band selection (adult / child / infant)
8
+ * and optionally a date / departure picker if `days[]` carries
9
+ * scheduled departures.
10
+ * - Travelers: per-pax fields (first / last / email; passport when
11
+ * the supplier requires it — surfaced via overlay when known).
12
+ * - Add-ons: the product's own `options[]` projected as add-on
13
+ * offers.
14
+ *
15
+ * No accommodation sub-step today (multi-day tours w/ rooms route
16
+ * through hospitality, not products). Pricing flows through
17
+ * liveResolve at quote time, not the descriptor.
18
+ *
19
+ * See `docs/architecture/booking-journey-architecture.md` §3 + §F.
20
+ */
21
+ import { DEFAULT_PAX_BANDS, defaultBookingFields, defaultDraftShapeFlags, defaultTravelerFields, paxBandsAllowedTotalFrom, } from "@voyantjs/catalog/booking-engine";
22
+ export function buildProductDraftShape(content, options = {}) {
23
+ const paxBands = options.paxBands ?? DEFAULT_PAX_BANDS;
24
+ const total = options.paxBandsAllowedTotal ?? paxBandsAllowedTotalFrom(paxBands);
25
+ // Project the product's own options into add-on offers. Each
26
+ // option becomes an extras-type add-on; verticals with grouped
27
+ // catalogs (cruise excursions) override.
28
+ const addonItems = content.options.map((opt) => ({
29
+ id: opt.id,
30
+ name: opt.name,
31
+ description: opt.description ?? null,
32
+ kind: "extras",
33
+ pricingMode: null,
34
+ }));
35
+ return {
36
+ ...defaultDraftShapeFlags(),
37
+ showsAddons: addonItems.length > 0,
38
+ paxBands,
39
+ paxBandsAllowedTotal: total,
40
+ travelerFields: defaultTravelerFields(),
41
+ bookingFields: defaultBookingFields(),
42
+ addons: addonItems.length > 0 ? { catalog: addonItems } : undefined,
43
+ paymentIntents: ["hold", "card"],
44
+ configureSubSteps: [{ kind: "occupancy", bands: paxBands }],
45
+ };
46
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Product domain events.
3
+ *
4
+ * Emitted by the products module on lifecycle and content changes.
5
+ * `product.content.changed` is the load-bearing channel-push signal —
6
+ * fired when ANY content axis (description, itinerary, media, options,
7
+ * days) changes.
8
+ *
9
+ * Per docs/architecture/channel-push-architecture.md §6.
10
+ */
11
+ import type { EventBus } from "@voyantjs/core";
12
+ /** Stable event identifier. */
13
+ export declare const PRODUCT_CONTENT_CHANGED_EVENT: "product.content.changed";
14
+ export interface ProductContentChangedEvent {
15
+ /** Product id whose content changed. */
16
+ id: string;
17
+ /**
18
+ * The content axis that changed, when known. Diagnostic only — channel
19
+ * push hashes the full current content at push time so this field is
20
+ * not load-bearing for correctness.
21
+ */
22
+ axis?: "product" | "option" | "day" | "media" | "feature" | "faq" | "location" | "destination" | "translation";
23
+ }
24
+ /**
25
+ * Helper for route handlers / services to fire `product.content.changed`
26
+ * with a stable shape. Fire-and-forget per the EventBus contract; the
27
+ * caller does not await this if it's already on a hot path.
28
+ *
29
+ * v1 wiring: every product service/route mutation that touches a
30
+ * content-affecting field should call this helper. The top-level PATCH
31
+ * route already calls it (alongside the legacy `product.updated`); child
32
+ * routes can be wired incrementally. Until full coverage exists, the
33
+ * channel-push reconciler (§13) catches missed events by hashing
34
+ * current content per (product, channel).
35
+ */
36
+ export declare function emitProductContentChanged(eventBus: EventBus | undefined, payload: ProductContentChangedEvent): Promise<void>;
37
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAE9C,+BAA+B;AAC/B,eAAO,MAAM,6BAA6B,EAAG,yBAAkC,CAAA;AAE/E,MAAM,WAAW,0BAA0B;IACzC,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,IAAI,CAAC,EACD,SAAS,GACT,QAAQ,GACR,KAAK,GACL,OAAO,GACP,SAAS,GACT,KAAK,GACL,UAAU,GACV,aAAa,GACb,aAAa,CAAA;CAClB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,QAAQ,GAAG,SAAS,EAC9B,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,CAMf"}
package/dist/events.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Product domain events.
3
+ *
4
+ * Emitted by the products module on lifecycle and content changes.
5
+ * `product.content.changed` is the load-bearing channel-push signal —
6
+ * fired when ANY content axis (description, itinerary, media, options,
7
+ * days) changes.
8
+ *
9
+ * Per docs/architecture/channel-push-architecture.md §6.
10
+ */
11
+ /** Stable event identifier. */
12
+ export const PRODUCT_CONTENT_CHANGED_EVENT = "product.content.changed";
13
+ /**
14
+ * Helper for route handlers / services to fire `product.content.changed`
15
+ * with a stable shape. Fire-and-forget per the EventBus contract; the
16
+ * caller does not await this if it's already on a hot path.
17
+ *
18
+ * v1 wiring: every product service/route mutation that touches a
19
+ * content-affecting field should call this helper. The top-level PATCH
20
+ * route already calls it (alongside the legacy `product.updated`); child
21
+ * routes can be wired incrementally. Until full coverage exists, the
22
+ * channel-push reconciler (§13) catches missed events by hashing
23
+ * current content per (product, channel).
24
+ */
25
+ export async function emitProductContentChanged(eventBus, payload) {
26
+ if (!eventBus)
27
+ return;
28
+ await eventBus.emit(PRODUCT_CONTENT_CHANGED_EVENT, payload, {
29
+ category: "domain",
30
+ source: "service",
31
+ });
32
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { LinkableDefinition, Module } from "@voyantjs/core";
2
2
  import type { HonoModule } from "@voyantjs/hono/module";
3
3
  export { productsBookingExtension } from "./booking-extension.js";
4
+ export { emitProductContentChanged, PRODUCT_CONTENT_CHANGED_EVENT, type ProductContentChangedEvent, } from "./events.js";
4
5
  export type { ProductRoutes } from "./routes.js";
5
6
  export type { PublicProductRoutes } from "./routes-public.js";
6
7
  export { publicProductRoutes } from "./routes-public.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAKvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,KAAK,0BAA0B,GAChC,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EACL,oCAAoC,EACpC,6CAA6C,EAC7C,oDAAoD,EACpD,oCAAoC,EACpC,KAAK,sCAAsC,EAC3C,+BAA+B,EAC/B,kCAAkC,EAClC,KAAK,sBAAsB,EAC3B,KAAK,iCAAiC,EACtC,6BAA6B,GAC9B,MAAM,kBAAkB,CAAA;AAEzB,eAAO,MAAM,eAAe,EAAE,kBAK7B,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,MAK5B,CAAA;AAED,eAAO,MAAM,kBAAkB,EAAE,UAKhC,CAAA;AAED,YAAY,EACV,aAAa,EACb,wBAAwB,EACxB,UAAU,EACV,2BAA2B,EAC3B,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,wBAAwB,EACxB,aAAa,EACb,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,2BAA2B,EAC3B,aAAa,EACb,uBAAuB,EACvB,qBAAqB,EACrB,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAC3B,UAAU,EACV,qBAAqB,EACrB,OAAO,EACP,wBAAwB,EACxB,iBAAiB,EACjB,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,WAAW,EACX,aAAa,EACb,wBAAwB,EACxB,UAAU,EACV,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,EACX,cAAc,EACd,wBAAwB,GACzB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,YAAY,EACZ,uBAAuB,EACvB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,WAAW,EACX,sBAAsB,EACtB,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,yBAAyB,EACzB,QAAQ,EACR,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,yBAAyB,GAC1B,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,iCAAiC,EACjC,oCAAoC,EACpC,6BAA6B,EAC7B,2BAA2B,EAC3B,iCAAiC,EACjC,sBAAsB,EACtB,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,EACzB,oCAAoC,EACpC,mBAAmB,EACnB,sBAAsB,EACtB,gCAAgC,EAChC,8BAA8B,EAC9B,uBAAuB,EACvB,oCAAoC,EACpC,mBAAmB,EACnB,yBAAyB,EACzB,oCAAoC,EACpC,uCAAuC,EACvC,gCAAgC,EAChC,8BAA8B,EAC9B,oCAAoC,EACpC,yBAAyB,EACzB,6BAA6B,EAC7B,sBAAsB,EACtB,8BAA8B,EAC9B,2BAA2B,EAC3B,4BAA4B,EAC5B,uCAAuC,EACvC,yBAAyB,EACzB,mCAAmC,EACnC,iCAAiC,EACjC,0BAA0B,EAC1B,uCAAuC,EACvC,yBAAyB,EACzB,mBAAmB,EACnB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,iCAAiC,EACjC,oCAAoC,EACpC,6BAA6B,EAC7B,2BAA2B,EAC3B,iCAAiC,EACjC,sBAAsB,EACtB,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACxB,yBAAyB,EACzB,oCAAoC,EACpC,mBAAmB,EACnB,sBAAsB,EACtB,gCAAgC,EAChC,8BAA8B,EAC9B,uBAAuB,EACvB,oCAAoC,EACpC,2BAA2B,GAC5B,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,qBAAqB,EACrB,8BAA8B,EAC9B,iCAAiC,EACjC,6BAA6B,EAC7B,8BAA8B,GAC/B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,oCAAoC,EACpC,uCAAuC,EACvC,2BAA2B,EAC3B,mCAAmC,EACnC,oCAAoC,GACrC,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,uCAAuC,EACvC,0CAA0C,EAC1C,8BAA8B,GAC/B,MAAM,wBAAwB,CAAA"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { productRoutes } from "./routes.js";
2
2
  import { publicProductRoutes } from "./routes-public.js";
3
3
  export { productsBookingExtension } from "./booking-extension.js";
4
+ export { emitProductContentChanged, PRODUCT_CONTENT_CHANGED_EVENT, } from "./events.js";
4
5
  export { publicProductRoutes } from "./routes-public.js";
5
6
  export { productsService } from "./service.js";
6
7
  export { catalogProductsService } from "./service-catalog.js";
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Product content routes — unified owned + sourced detail endpoint.
3
+ *
4
+ * GET /:id/content
5
+ *
6
+ * Returns the full `ProductContent` payload for ANY product:
7
+ * - **Sourced**: cache hit → cached row + overlay merge; cache miss
8
+ * with rich adapter → adapter fetch + write-through; cache miss
9
+ * with thin adapter → synthesizer fallback (sourced-content §3.3,
10
+ * §3.4, §3.6).
11
+ * - **Owned**: read from the products module's own tables and
12
+ * project to ProductContent. Overlay merge applies the same way.
13
+ * Marked `source: "owned"` in the response.
14
+ *
15
+ * 404 only when the entity doesn't exist (no sourced-entry row AND
16
+ * no owned product row). The catalog detail sheet calls this on
17
+ * click to enrich the indexed projection with itinerary, media,
18
+ * options, and policies.
19
+ *
20
+ * Templates mount this router under their preferred prefix; the
21
+ * factory takes a `resolveRegistry` callback so the catalog
22
+ * `SourceAdapterRegistry` stays template-owned (singleton lifetime,
23
+ * adapters carry HTTP clients).
24
+ *
25
+ * See `docs/architecture/catalog-sourced-content.md` §3.3.
26
+ */
27
+ import type { SourceAdapterRegistry } from "@voyantjs/catalog/booking-engine";
28
+ import type { AnyDrizzleDb } from "@voyantjs/db";
29
+ import type { Context } from "hono";
30
+ import { Hono } from "hono";
31
+ export interface ProductContentRoutesEnv {
32
+ Variables: {
33
+ db: AnyDrizzleDb;
34
+ };
35
+ }
36
+ export interface CreateProductContentRoutesOptions {
37
+ /**
38
+ * Resolve the catalog `SourceAdapterRegistry` for the current
39
+ * request. Templates typically return a process-local singleton
40
+ * built lazily from env (mirroring the booking-engine registry
41
+ * pattern).
42
+ */
43
+ resolveRegistry: (c: Context) => SourceAdapterRegistry;
44
+ /**
45
+ * Optional sink for overlay-merge diagnostics. When set, called
46
+ * once per overlay that fails to apply. Defaults to silent (the
47
+ * read still succeeds; the bad overlay is skipped).
48
+ */
49
+ onOverlayError?: (event: {
50
+ field_path: string;
51
+ reason: string;
52
+ }) => void;
53
+ /**
54
+ * Optional override for `acceptMachineTranslated`. Defaults to
55
+ * `true` — storefront-friendly. Operator surfaces typically set
56
+ * `false` so ops sees authored content before deciding to override.
57
+ */
58
+ defaultAcceptMachineTranslated?: boolean;
59
+ }
60
+ /**
61
+ * Build the product content router. Returns a Hono instance that
62
+ * exposes a single `GET /:id/content` route. Templates mount it under
63
+ * `/v1/admin/products` or `/v1/public/products` as appropriate.
64
+ */
65
+ export declare function createProductContentRoutes(options: CreateProductContentRoutesOptions): Hono<ProductContentRoutesEnv>;
66
+ /**
67
+ * Parse an `Accept-Language` header into an ordered list of BCP 47
68
+ * tags. Quality factors are honored — higher-q first; ties keep
69
+ * insertion order. Lifted out of the route handler so it's testable
70
+ * in isolation.
71
+ */
72
+ export declare function parseAcceptLanguage(header: string): string[];
73
+ export type ProductContentRoutes = ReturnType<typeof createProductContentRoutes>;
74
+ //# sourceMappingURL=routes-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-content.d.ts","sourceRoot":"","sources":["../src/routes-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE;QACT,EAAE,EAAE,YAAY,CAAA;KACjB,CAAA;CACF;AAED,MAAM,WAAW,iCAAiC;IAChD;;;;;OAKG;IACH,eAAe,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,qBAAqB,CAAA;IACtD;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACxE;;;;OAIG;IACH,8BAA8B,CAAC,EAAE,OAAO,CAAA;CACzC;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,IAAI,CAAC,uBAAuB,CAAC,CA+D/B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAqB5D;AAED,MAAM,MAAM,oBAAoB,GAAG,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAA"}