@voyantjs/extras 0.19.0 → 0.21.0

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,30 @@
1
+ /**
2
+ * Catalog plane field policy for `packages/extras`.
3
+ *
4
+ * Extras are booking add-ons (optional line items layered on a booked
5
+ * parent product) — **not independently sellable inventory**. Per
6
+ * architecture §3.3.1, extras are a partial-adoption vertical:
7
+ *
8
+ * - **Adopt:** provenance shape (§5.1), booking snapshot graph (§5.3),
9
+ * catalog event taxonomy for the cancellation/fulfillment lifecycle.
10
+ * - **Skip:** search index projection (§5.4), editorial overlay store
11
+ * (§5.2), embeddings / RAG (Phase 2). Extras are discovered through
12
+ * the parent's surface, not via standalone catalog browse.
13
+ *
14
+ * Every field below has `reindex: "none"` because extras don't appear in
15
+ * the search index. Snapshot mode is `"on-book"` (or `"never"` for
16
+ * volatile fields) because refunds and audits need to know exactly what
17
+ * extra a customer added.
18
+ *
19
+ * Scope of this file:
20
+ * - The `product_extras` table (extra catalog definitions).
21
+ *
22
+ * Out of scope:
23
+ * - `option_extra_configs` — option-level overrides; promoted child.
24
+ * - `booking_extras` — runtime line items on a booking; not catalog data.
25
+ */
26
+ import { type FieldPolicyInput } from "@voyantjs/catalog/contract";
27
+ declare const EXTRAS_FIELD_POLICY: FieldPolicyInput[];
28
+ export declare const extrasCatalogPolicy: import("@voyantjs/catalog").FieldPolicy[];
29
+ export { EXTRAS_FIELD_POLICY };
30
+ //# sourceMappingURL=catalog-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF,QAAA,MAAM,mBAAmB,EAAE,gBAAgB,EAwR1C,CAAA;AAED,eAAO,MAAM,mBAAmB,2CAAyC,CAAA;AAEzE,OAAO,EAAE,mBAAmB,EAAE,CAAA"}
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Catalog plane field policy for `packages/extras`.
3
+ *
4
+ * Extras are booking add-ons (optional line items layered on a booked
5
+ * parent product) — **not independently sellable inventory**. Per
6
+ * architecture §3.3.1, extras are a partial-adoption vertical:
7
+ *
8
+ * - **Adopt:** provenance shape (§5.1), booking snapshot graph (§5.3),
9
+ * catalog event taxonomy for the cancellation/fulfillment lifecycle.
10
+ * - **Skip:** search index projection (§5.4), editorial overlay store
11
+ * (§5.2), embeddings / RAG (Phase 2). Extras are discovered through
12
+ * the parent's surface, not via standalone catalog browse.
13
+ *
14
+ * Every field below has `reindex: "none"` because extras don't appear in
15
+ * the search index. Snapshot mode is `"on-book"` (or `"never"` for
16
+ * volatile fields) because refunds and audits need to know exactly what
17
+ * extra a customer added.
18
+ *
19
+ * Scope of this file:
20
+ * - The `product_extras` table (extra catalog definitions).
21
+ *
22
+ * Out of scope:
23
+ * - `option_extra_configs` — option-level overrides; promoted child.
24
+ * - `booking_extras` — runtime line items on a booking; not catalog data.
25
+ */
26
+ import { defineFieldPolicy } from "@voyantjs/catalog/contract";
27
+ const EXTRAS_FIELD_POLICY = [
28
+ // ── Source pointer / provenance ─────────────────────────────────────────
29
+ {
30
+ path: "source.kind",
31
+ class: "managed",
32
+ merge: "source-only",
33
+ drift: "critical",
34
+ reindex: "facet-affecting",
35
+ snapshot: "on-book",
36
+ query: "indexed-column",
37
+ localized: false,
38
+ visibility: ["staff"],
39
+ editRole: "none",
40
+ overrideFriction: "none",
41
+ sourceFreshness: "sync",
42
+ },
43
+ {
44
+ path: "source.ref",
45
+ class: "managed",
46
+ merge: "source-only",
47
+ drift: "critical",
48
+ reindex: "none",
49
+ snapshot: "on-book",
50
+ query: "indexed-column",
51
+ localized: false,
52
+ visibility: ["staff"],
53
+ editRole: "none",
54
+ overrideFriction: "none",
55
+ sourceFreshness: "sync",
56
+ },
57
+ {
58
+ path: "seller.operator_id",
59
+ class: "managed",
60
+ merge: "source-only",
61
+ drift: "critical",
62
+ reindex: "none",
63
+ snapshot: "on-book",
64
+ query: "indexed-column",
65
+ localized: false,
66
+ visibility: ["staff"],
67
+ editRole: "none",
68
+ overrideFriction: "none",
69
+ sourceFreshness: "static",
70
+ },
71
+ // ── Identity / lifecycle ────────────────────────────────────────────────
72
+ {
73
+ path: "id",
74
+ class: "managed",
75
+ merge: "source-only",
76
+ drift: "critical",
77
+ reindex: "none",
78
+ snapshot: "on-book",
79
+ query: "first-class-table",
80
+ localized: false,
81
+ visibility: ["staff", "customer", "partner"],
82
+ editRole: "none",
83
+ overrideFriction: "none",
84
+ sourceFreshness: "static",
85
+ },
86
+ {
87
+ path: "code",
88
+ class: "managed",
89
+ merge: "source-only",
90
+ drift: "high",
91
+ reindex: "none",
92
+ snapshot: "on-book",
93
+ query: "indexed-column",
94
+ localized: false,
95
+ visibility: ["staff"],
96
+ editRole: "none",
97
+ overrideFriction: "none",
98
+ sourceFreshness: "sync",
99
+ },
100
+ {
101
+ path: "createdAt",
102
+ class: "managed",
103
+ merge: "source-only",
104
+ drift: "none",
105
+ reindex: "none",
106
+ snapshot: "on-book",
107
+ query: "indexed-column",
108
+ localized: false,
109
+ visibility: ["staff"],
110
+ editRole: "none",
111
+ overrideFriction: "none",
112
+ sourceFreshness: "static",
113
+ },
114
+ {
115
+ path: "updatedAt",
116
+ class: "managed",
117
+ merge: "source-only",
118
+ drift: "none",
119
+ reindex: "none",
120
+ snapshot: "never",
121
+ query: "indexed-column",
122
+ localized: false,
123
+ visibility: ["staff"],
124
+ editRole: "none",
125
+ overrideFriction: "none",
126
+ sourceFreshness: "sync",
127
+ },
128
+ // ── Cross-module reference (the parent product the extra attaches to) ──
129
+ {
130
+ path: "productId",
131
+ class: "managed",
132
+ merge: "source-only",
133
+ drift: "critical",
134
+ reindex: "none",
135
+ snapshot: "on-book",
136
+ query: "indexed-column",
137
+ localized: false,
138
+ visibility: ["staff"],
139
+ editRole: "none",
140
+ overrideFriction: "none",
141
+ sourceFreshness: "sync",
142
+ },
143
+ {
144
+ path: "supplierId",
145
+ class: "structural",
146
+ merge: "source-only",
147
+ drift: "high",
148
+ reindex: "none",
149
+ snapshot: "on-book",
150
+ query: "indexed-column",
151
+ localized: false,
152
+ visibility: ["staff"],
153
+ editRole: "none",
154
+ overrideFriction: "none",
155
+ sourceFreshness: "sync",
156
+ },
157
+ // ── Snapshot-relevant managed fields ────────────────────────────────────
158
+ // Extras' name/description aren't merchandised standalone — they're
159
+ // shown inside the parent product's add-on UI. Marked as managed for
160
+ // simplicity; if a future case requires marketing overrides on extras'
161
+ // names, this can be promoted to merchandisable.
162
+ {
163
+ path: "name",
164
+ class: "managed",
165
+ merge: "source-only",
166
+ drift: "medium",
167
+ reindex: "none",
168
+ snapshot: "on-book",
169
+ query: "indexed-column",
170
+ localized: false,
171
+ visibility: ["staff", "customer", "partner"],
172
+ editRole: "none",
173
+ overrideFriction: "none",
174
+ sourceFreshness: "sync",
175
+ },
176
+ {
177
+ path: "description",
178
+ class: "managed",
179
+ merge: "source-only",
180
+ drift: "low",
181
+ reindex: "none",
182
+ snapshot: "on-book",
183
+ query: "blob-only",
184
+ localized: false,
185
+ visibility: ["staff", "customer", "partner"],
186
+ editRole: "none",
187
+ overrideFriction: "none",
188
+ sourceFreshness: "sync",
189
+ },
190
+ // ── Selection / pricing structure (snapshotted at book time) ───────────
191
+ {
192
+ path: "selectionType",
193
+ class: "structural",
194
+ merge: "source-only",
195
+ drift: "high",
196
+ reindex: "none",
197
+ snapshot: "on-book",
198
+ query: "indexed-column",
199
+ localized: false,
200
+ visibility: ["staff", "customer", "partner"],
201
+ editRole: "none",
202
+ overrideFriction: "none",
203
+ sourceFreshness: "sync",
204
+ },
205
+ {
206
+ path: "pricingMode",
207
+ class: "structural",
208
+ merge: "source-only",
209
+ drift: "high",
210
+ reindex: "none",
211
+ snapshot: "on-quote-and-book",
212
+ query: "indexed-column",
213
+ localized: false,
214
+ visibility: ["staff", "customer", "partner"],
215
+ editRole: "none",
216
+ overrideFriction: "none",
217
+ sourceFreshness: "sync",
218
+ },
219
+ {
220
+ path: "pricedPerPerson",
221
+ class: "structural",
222
+ merge: "source-only",
223
+ drift: "high",
224
+ reindex: "none",
225
+ snapshot: "on-quote-and-book",
226
+ query: "indexed-column",
227
+ localized: false,
228
+ visibility: ["staff", "customer", "partner"],
229
+ editRole: "none",
230
+ overrideFriction: "none",
231
+ sourceFreshness: "sync",
232
+ },
233
+ {
234
+ path: "minQuantity",
235
+ class: "structural",
236
+ merge: "source-only",
237
+ drift: "medium",
238
+ reindex: "none",
239
+ snapshot: "on-book",
240
+ query: "indexed-column",
241
+ localized: false,
242
+ visibility: ["staff", "customer", "partner"],
243
+ editRole: "none",
244
+ overrideFriction: "none",
245
+ sourceFreshness: "sync",
246
+ },
247
+ {
248
+ path: "maxQuantity",
249
+ class: "structural",
250
+ merge: "source-only",
251
+ drift: "medium",
252
+ reindex: "none",
253
+ snapshot: "on-book",
254
+ query: "indexed-column",
255
+ localized: false,
256
+ visibility: ["staff", "customer", "partner"],
257
+ editRole: "none",
258
+ overrideFriction: "none",
259
+ sourceFreshness: "sync",
260
+ },
261
+ {
262
+ path: "defaultQuantity",
263
+ class: "structural",
264
+ merge: "source-only",
265
+ drift: "low",
266
+ reindex: "none",
267
+ snapshot: "on-book",
268
+ query: "indexed-column",
269
+ localized: false,
270
+ visibility: ["staff", "customer", "partner"],
271
+ editRole: "none",
272
+ overrideFriction: "none",
273
+ sourceFreshness: "sync",
274
+ },
275
+ {
276
+ path: "active",
277
+ class: "structural",
278
+ merge: "source-only",
279
+ drift: "high",
280
+ reindex: "none",
281
+ snapshot: "on-book",
282
+ query: "indexed-column",
283
+ localized: false,
284
+ visibility: ["staff"],
285
+ editRole: "none",
286
+ overrideFriction: "none",
287
+ sourceFreshness: "sync",
288
+ },
289
+ {
290
+ path: "sortOrder",
291
+ class: "structural",
292
+ merge: "source-only",
293
+ drift: "none",
294
+ reindex: "none",
295
+ snapshot: "never",
296
+ query: "indexed-column",
297
+ localized: false,
298
+ visibility: ["staff"],
299
+ editRole: "ops",
300
+ overrideFriction: "none",
301
+ sourceFreshness: "sync",
302
+ },
303
+ ];
304
+ export const extrasCatalogPolicy = defineFieldPolicy(EXTRAS_FIELD_POLICY);
305
+ export { EXTRAS_FIELD_POLICY };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Extras content shape — the rich detail-page content shape returned
3
+ * by `getContent` for sourced extras (excursions, transfers, add-on
4
+ * services).
5
+ *
6
+ * The extras content aggregate is `{ extra, options[], media[],
7
+ * policies[] }` — one payload returned by a single `getContent`.
8
+ * Pricing stays out (volatile-live, flows through `liveResolve`).
9
+ *
10
+ * Extras are simpler than the other verticals because they're add-ons,
11
+ * not standalone products. There's no day-by-day itinerary, no
12
+ * room-type / cabin-category map, no ship spec — just an extra
13
+ * description, optional sub-options (e.g. "half-day vs full-day"),
14
+ * media, and the operational/cancellation policies.
15
+ *
16
+ * See `docs/architecture/catalog-sourced-content.md` §3.2, §3.5.4, §3.6.
17
+ */
18
+ import { type ContentOverlay, type MergeOverlaysOptions } from "@voyantjs/catalog";
19
+ import { z } from "zod";
20
+ export declare const EXTRAS_CONTENT_SCHEMA_VERSION = "extras/v1";
21
+ export declare const extraSummarySchema: z.ZodObject<{
22
+ id: z.ZodString;
23
+ name: z.ZodString;
24
+ status: z.ZodOptional<z.ZodString>;
25
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
26
+ selection_type: z.ZodOptional<z.ZodString>;
27
+ pricing_mode: z.ZodOptional<z.ZodString>;
28
+ priced_per_person: z.ZodOptional<z.ZodBoolean>;
29
+ category: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
31
+ highlights: z.ZodOptional<z.ZodArray<z.ZodString>>;
32
+ supplier: z.ZodOptional<z.ZodNullable<z.ZodString>>;
33
+ duration_minutes: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
34
+ requirements_summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
35
+ }, z.core.$strip>;
36
+ export declare const extraOptionSchema: z.ZodObject<{
37
+ id: z.ZodString;
38
+ name: z.ZodString;
39
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
40
+ default_selected: z.ZodOptional<z.ZodBoolean>;
41
+ }, z.core.$strip>;
42
+ export declare const extraMediaItemSchema: z.ZodObject<{
43
+ url: z.ZodString;
44
+ type: z.ZodDefault<z.ZodEnum<{
45
+ image: "image";
46
+ video: "video";
47
+ document: "document";
48
+ }>>;
49
+ caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
50
+ alt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
51
+ }, z.core.$strip>;
52
+ export declare const extraPolicySchema: z.ZodObject<{
53
+ kind: z.ZodEnum<{
54
+ supplier_notes: "supplier_notes";
55
+ cancellation: "cancellation";
56
+ payment: "payment";
57
+ requirements: "requirements";
58
+ }>;
59
+ body: z.ZodString;
60
+ rules: z.ZodOptional<z.ZodUnknown>;
61
+ }, z.core.$strip>;
62
+ export declare const extraContentSchema: z.ZodObject<{
63
+ extra: z.ZodObject<{
64
+ id: z.ZodString;
65
+ name: z.ZodString;
66
+ status: z.ZodOptional<z.ZodString>;
67
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
68
+ selection_type: z.ZodOptional<z.ZodString>;
69
+ pricing_mode: z.ZodOptional<z.ZodString>;
70
+ priced_per_person: z.ZodOptional<z.ZodBoolean>;
71
+ category: z.ZodOptional<z.ZodNullable<z.ZodString>>;
72
+ hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
73
+ highlights: z.ZodOptional<z.ZodArray<z.ZodString>>;
74
+ supplier: z.ZodOptional<z.ZodNullable<z.ZodString>>;
75
+ duration_minutes: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
76
+ requirements_summary: z.ZodOptional<z.ZodNullable<z.ZodString>>;
77
+ }, z.core.$strip>;
78
+ options: z.ZodDefault<z.ZodArray<z.ZodObject<{
79
+ id: z.ZodString;
80
+ name: z.ZodString;
81
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
82
+ default_selected: z.ZodOptional<z.ZodBoolean>;
83
+ }, z.core.$strip>>>;
84
+ media: z.ZodDefault<z.ZodArray<z.ZodObject<{
85
+ url: z.ZodString;
86
+ type: z.ZodDefault<z.ZodEnum<{
87
+ image: "image";
88
+ video: "video";
89
+ document: "document";
90
+ }>>;
91
+ caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
92
+ alt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
93
+ }, z.core.$strip>>>;
94
+ policies: z.ZodDefault<z.ZodArray<z.ZodObject<{
95
+ kind: z.ZodEnum<{
96
+ supplier_notes: "supplier_notes";
97
+ cancellation: "cancellation";
98
+ payment: "payment";
99
+ requirements: "requirements";
100
+ }>;
101
+ body: z.ZodString;
102
+ rules: z.ZodOptional<z.ZodUnknown>;
103
+ }, z.core.$strip>>>;
104
+ }, z.core.$strip>;
105
+ export type ExtraContent = z.infer<typeof extraContentSchema>;
106
+ export type ExtraSummary = z.infer<typeof extraSummarySchema>;
107
+ export type ExtraOption = z.infer<typeof extraOptionSchema>;
108
+ export type ExtraMediaItem = z.infer<typeof extraMediaItemSchema>;
109
+ export type ExtraPolicy = z.infer<typeof extraPolicySchema>;
110
+ export declare function validateExtraContent(payload: unknown): {
111
+ valid: true;
112
+ content: ExtraContent;
113
+ } | {
114
+ valid: false;
115
+ reason: string;
116
+ };
117
+ export declare function mergeOverlaysIntoExtraContent(payload: ExtraContent, overlays: ReadonlyArray<ContentOverlay>, options?: Pick<MergeOverlaysOptions, "onOverlayError">): ExtraContent;
118
+ //# 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;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAE1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,6BAA6B,cAAc,CAAA;AAExD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;iBAgC7B,CAAA;AAEF,eAAO,MAAM,iBAAiB;;;;;iBAM5B,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;;iBAK/B,CAAA;AAEF,eAAO,MAAM,iBAAiB;;;;;;;;;iBAI5B,CAAA;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAC7D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAC7D,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAC3D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAE3D,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,GACf;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAU3E;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,aAAa,CAAC,cAAc,CAAC,EACvC,OAAO,GAAE,IAAI,CAAC,oBAAoB,EAAE,gBAAgB,CAAM,GACzD,YAAY,CASd"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Extras content shape — the rich detail-page content shape returned
3
+ * by `getContent` for sourced extras (excursions, transfers, add-on
4
+ * services).
5
+ *
6
+ * The extras content aggregate is `{ extra, options[], media[],
7
+ * policies[] }` — one payload returned by a single `getContent`.
8
+ * Pricing stays out (volatile-live, flows through `liveResolve`).
9
+ *
10
+ * Extras are simpler than the other verticals because they're add-ons,
11
+ * not standalone products. There's no day-by-day itinerary, no
12
+ * room-type / cabin-category map, no ship spec — just an extra
13
+ * description, optional sub-options (e.g. "half-day vs full-day"),
14
+ * media, and the operational/cancellation policies.
15
+ *
16
+ * See `docs/architecture/catalog-sourced-content.md` §3.2, §3.5.4, §3.6.
17
+ */
18
+ import { mergeOverlaysIntoContent, } from "@voyantjs/catalog";
19
+ import { z } from "zod";
20
+ export const EXTRAS_CONTENT_SCHEMA_VERSION = "extras/v1";
21
+ export const extraSummarySchema = z.object({
22
+ id: z.string(),
23
+ name: z.string(),
24
+ status: z.string().optional(),
25
+ description: z.string().nullable().optional(),
26
+ /**
27
+ * Selection type — mirrors the owned `extra_selection_type` enum:
28
+ * "optional" | "required" | "default_selected" | "unavailable".
29
+ * Sourced adapters set what they support; thin synthesis defaults to
30
+ * "optional".
31
+ */
32
+ selection_type: z.string().optional(),
33
+ /**
34
+ * Pricing mode — mirrors the owned `extra_pricing_mode` enum:
35
+ * "included" | "per_person" | "per_booking" | "quantity_based" |
36
+ * "on_request" | "free". Captures the structural pricing model the
37
+ * upstream advertises; actual prices come through `liveResolve`.
38
+ */
39
+ pricing_mode: z.string().optional(),
40
+ /** Hint — true when the extra is priced per traveler, not per booking. */
41
+ priced_per_person: z.boolean().optional(),
42
+ /** Service category (e.g. "transfer", "excursion", "insurance", "spa"). */
43
+ category: z.string().nullable().optional(),
44
+ /** Hero media URL. */
45
+ hero_image_url: z.string().nullable().optional(),
46
+ highlights: z.array(z.string()).optional(),
47
+ /** Free-form supplier hint surfaced to ops (not customer-facing). */
48
+ supplier: z.string().nullable().optional(),
49
+ /** Estimated duration in minutes for time-bound extras (excursions). */
50
+ duration_minutes: z.number().int().nonnegative().nullable().optional(),
51
+ /** Constraints / requirements summary surfaced on the booking flow. */
52
+ requirements_summary: z.string().nullable().optional(),
53
+ });
54
+ export const extraOptionSchema = z.object({
55
+ id: z.string(),
56
+ name: z.string(),
57
+ description: z.string().nullable().optional(),
58
+ /** Whether this option auto-selects when the extra is selected. */
59
+ default_selected: z.boolean().optional(),
60
+ });
61
+ export const extraMediaItemSchema = z.object({
62
+ url: z.string(),
63
+ type: z.enum(["image", "video", "document"]).default("image"),
64
+ caption: z.string().nullable().optional(),
65
+ alt: z.string().nullable().optional(),
66
+ });
67
+ export const extraPolicySchema = z.object({
68
+ kind: z.enum(["cancellation", "payment", "supplier_notes", "requirements"]),
69
+ body: z.string(),
70
+ rules: z.unknown().optional(),
71
+ });
72
+ export const extraContentSchema = z.object({
73
+ extra: extraSummarySchema,
74
+ options: z.array(extraOptionSchema).default([]),
75
+ media: z.array(extraMediaItemSchema).default([]),
76
+ policies: z.array(extraPolicySchema).default([]),
77
+ });
78
+ export function validateExtraContent(payload) {
79
+ const result = extraContentSchema.safeParse(payload);
80
+ if (result.success) {
81
+ return { valid: true, content: result.data };
82
+ }
83
+ const issue = result.error.issues[0];
84
+ return {
85
+ valid: false,
86
+ reason: issue ? `${issue.path.join(".")}: ${issue.message}` : "validation failed",
87
+ };
88
+ }
89
+ export function mergeOverlaysIntoExtraContent(payload, overlays, options = {}) {
90
+ const merged = mergeOverlaysIntoContent(payload, overlays, {
91
+ validate(p) {
92
+ const r = validateExtraContent(p);
93
+ return r.valid ? { valid: true } : { valid: false, reason: r.reason };
94
+ },
95
+ onOverlayError: options.onOverlayError,
96
+ });
97
+ return extraContentSchema.parse(merged);
98
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Project an `ExtraContent` payload into a `BookingDraftShape`.
3
+ *
4
+ * Extras are booking add-ons (excursions, transfers, insurance) —
5
+ * they're never standalone bookable on their own; they always layer
6
+ * onto a parent product. So the descriptor here is **degenerate**:
7
+ *
8
+ * - `showsConfigure: false` (no configure step — extras are picked
9
+ * during the parent product's add-on step).
10
+ * - `showsTravelers: false` (extras don't have their own pax flow).
11
+ * - `showsPayment: false` (extras roll up into the parent's pricing).
12
+ * - `addons.catalog`: the extra itself + its sub-options projected
13
+ * as a small catalog. Useful for journey contexts that want to
14
+ * render an add-on detail view (e.g. ops admin reviewing a
15
+ * supplier's extras).
16
+ *
17
+ * In practice templates rarely call this directly — the parent
18
+ * product's `BookingDraftShape` aggregates extras via the journey's
19
+ * cross-product composer. This function is the building block.
20
+ */
21
+ import { type BookingDraftShape } from "@voyantjs/catalog/booking-engine";
22
+ import type { ExtraContent } from "./content-shape.js";
23
+ export interface BuildExtraDraftShapeOptions {
24
+ locale?: string;
25
+ /**
26
+ * When true, returns the full descriptor surface (configure +
27
+ * travelers + payment all visible). Useful for tests or admin
28
+ * surfaces that render extras standalone. Defaults to false (the
29
+ * normal "extras-as-addon" mode).
30
+ */
31
+ standalone?: boolean;
32
+ }
33
+ export declare function buildExtraDraftShape(content: ExtraContent, options?: BuildExtraDraftShapeOptions): BookingDraftShape;
34
+ //# 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,EAMvB,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAMtD,MAAM,WAAW,2BAA2B;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE,2BAAgC,GACxC,iBAAiB,CAsCnB"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Project an `ExtraContent` payload into a `BookingDraftShape`.
3
+ *
4
+ * Extras are booking add-ons (excursions, transfers, insurance) —
5
+ * they're never standalone bookable on their own; they always layer
6
+ * onto a parent product. So the descriptor here is **degenerate**:
7
+ *
8
+ * - `showsConfigure: false` (no configure step — extras are picked
9
+ * during the parent product's add-on step).
10
+ * - `showsTravelers: false` (extras don't have their own pax flow).
11
+ * - `showsPayment: false` (extras roll up into the parent's pricing).
12
+ * - `addons.catalog`: the extra itself + its sub-options projected
13
+ * as a small catalog. Useful for journey contexts that want to
14
+ * render an add-on detail view (e.g. ops admin reviewing a
15
+ * supplier's extras).
16
+ *
17
+ * In practice templates rarely call this directly — the parent
18
+ * product's `BookingDraftShape` aggregates extras via the journey's
19
+ * cross-product composer. This function is the building block.
20
+ */
21
+ import { defaultBookingFields, defaultDraftShapeFlags, defaultTravelerFields, paxBandsAllowedTotalFrom, } from "@voyantjs/catalog/booking-engine";
22
+ const DEGENERATE_PAX_BANDS = [
23
+ { code: "adult", label: "Adult", minCount: 0, maxCount: 8 },
24
+ ];
25
+ export function buildExtraDraftShape(content, options = {}) {
26
+ const flags = defaultDraftShapeFlags();
27
+ const standalone = options.standalone ?? false;
28
+ // Project the extra + its options as add-on offers. The extra
29
+ // itself becomes the lead item; sub-options follow.
30
+ const items = [
31
+ {
32
+ id: content.extra.id,
33
+ name: content.extra.name,
34
+ description: content.extra.description ?? null,
35
+ kind: kindForExtraCategory(content.extra.category ?? null),
36
+ pricingMode: content.extra.pricing_mode ?? null,
37
+ },
38
+ ...content.options.map((opt) => ({
39
+ id: opt.id,
40
+ name: opt.name,
41
+ description: opt.description ?? null,
42
+ kind: "extras",
43
+ pricingMode: null,
44
+ })),
45
+ ];
46
+ return {
47
+ ...flags,
48
+ showsConfigure: standalone,
49
+ showsTravelers: standalone,
50
+ showsPayment: standalone,
51
+ showsAddons: true,
52
+ paxBands: DEGENERATE_PAX_BANDS,
53
+ paxBandsAllowedTotal: paxBandsAllowedTotalFrom(DEGENERATE_PAX_BANDS),
54
+ travelerFields: standalone ? defaultTravelerFields() : [],
55
+ bookingFields: standalone ? defaultBookingFields() : [],
56
+ addons: { catalog: items },
57
+ paymentIntents: standalone ? ["hold", "card"] : [],
58
+ };
59
+ }
60
+ function kindForExtraCategory(category) {
61
+ if (!category)
62
+ return "extras";
63
+ const lower = category.toLowerCase();
64
+ if (lower.includes("excursion") || lower.includes("tour"))
65
+ return "excursions";
66
+ if (lower.includes("insurance"))
67
+ return "insurance";
68
+ return "extras";
69
+ }