@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.
- package/dist/catalog-policy.d.ts +30 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +305 -0
- package/dist/content-shape.d.ts +118 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +98 -0
- package/dist/draft-shape.d.ts +34 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +69 -0
- package/dist/routes.d.ts +35 -31
- package/dist/routes.d.ts.map +1 -1
- package/dist/schema-sourced-content.d.ts +254 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +45 -0
- package/dist/schema.d.ts +21 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +6 -0
- package/dist/service-catalog-plane.d.ts +79 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +157 -0
- package/dist/service-content-synthesizer.d.ts +41 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +138 -0
- package/dist/service-content.d.ts +48 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +253 -0
- package/dist/service.d.ts +29 -25
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +2 -0
- package/dist/validation.d.ts +28 -24
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +2 -0
- package/package.json +7 -6
|
@@ -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
|
+
}
|