@voyantjs/products 0.19.0 → 0.20.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 +33 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +407 -0
- package/dist/routes.d.ts +7 -6
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +5 -3
- package/dist/service-catalog-plane.d.ts +129 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +211 -0
- package/dist/service.d.ts +6 -6
- package/dist/tasks/brochures.d.ts +1 -1
- package/dist/tasks/brochures.d.ts.map +1 -1
- package/package.json +7 -6
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog plane field policy for `packages/products`.
|
|
3
|
+
*
|
|
4
|
+
* Declares every product field's governance under the
|
|
5
|
+
* `@voyantjs/catalog` 12-attribute contract. Phase B shake-out
|
|
6
|
+
* adoption — see `docs/architecture/catalog-architecture.md` §9.1.
|
|
7
|
+
*
|
|
8
|
+
* Scope of this file:
|
|
9
|
+
* - The root `products` table (from `schema-core.ts`).
|
|
10
|
+
* - Provenance + identity fields the catalog plane needs to track.
|
|
11
|
+
*
|
|
12
|
+
* Out of scope (deferred to follow-up adoption passes):
|
|
13
|
+
* - `productOptions`, `optionUnits`, `productDays`, `productNotes`,
|
|
14
|
+
* `productVersions` — promoted child entities (per composition rule §6.2);
|
|
15
|
+
* each gets its own micro-registry when wired in.
|
|
16
|
+
* - The split of `tags` into `marketing_tags` + `facet_tags` per the
|
|
17
|
+
* human-readable / machine-evaluable rule (§7.1). Today's schema has a
|
|
18
|
+
* single `tags` column; declared here as merchandisable with a TODO.
|
|
19
|
+
*/
|
|
20
|
+
import { type FieldPolicyInput } from "@voyantjs/catalog/contract";
|
|
21
|
+
/**
|
|
22
|
+
* Field-policy declarations for `products`. Pass through `defineFieldPolicy`
|
|
23
|
+
* to apply inheritance and produce the runtime registry.
|
|
24
|
+
*/
|
|
25
|
+
declare const PRODUCT_FIELD_POLICY: FieldPolicyInput[];
|
|
26
|
+
/**
|
|
27
|
+
* Resolved field-policy registry for products. Verticals adopt the catalog
|
|
28
|
+
* plane by exporting this; templates wire it into the indexer, overlay
|
|
29
|
+
* resolver, and snapshot capture pipeline.
|
|
30
|
+
*/
|
|
31
|
+
export declare const productCatalogPolicy: import("@voyantjs/catalog").FieldPolicy[];
|
|
32
|
+
export { PRODUCT_FIELD_POLICY };
|
|
33
|
+
//# 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;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,EA4X3C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,2CAA0C,CAAA;AAE3E,OAAO,EAAE,oBAAoB,EAAE,CAAA"}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog plane field policy for `packages/products`.
|
|
3
|
+
*
|
|
4
|
+
* Declares every product field's governance under the
|
|
5
|
+
* `@voyantjs/catalog` 12-attribute contract. Phase B shake-out
|
|
6
|
+
* adoption — see `docs/architecture/catalog-architecture.md` §9.1.
|
|
7
|
+
*
|
|
8
|
+
* Scope of this file:
|
|
9
|
+
* - The root `products` table (from `schema-core.ts`).
|
|
10
|
+
* - Provenance + identity fields the catalog plane needs to track.
|
|
11
|
+
*
|
|
12
|
+
* Out of scope (deferred to follow-up adoption passes):
|
|
13
|
+
* - `productOptions`, `optionUnits`, `productDays`, `productNotes`,
|
|
14
|
+
* `productVersions` — promoted child entities (per composition rule §6.2);
|
|
15
|
+
* each gets its own micro-registry when wired in.
|
|
16
|
+
* - The split of `tags` into `marketing_tags` + `facet_tags` per the
|
|
17
|
+
* human-readable / machine-evaluable rule (§7.1). Today's schema has a
|
|
18
|
+
* single `tags` column; declared here as merchandisable with a TODO.
|
|
19
|
+
*/
|
|
20
|
+
import { defineFieldPolicy } from "@voyantjs/catalog/contract";
|
|
21
|
+
/**
|
|
22
|
+
* Field-policy declarations for `products`. Pass through `defineFieldPolicy`
|
|
23
|
+
* to apply inheritance and produce the runtime registry.
|
|
24
|
+
*/
|
|
25
|
+
const PRODUCT_FIELD_POLICY = [
|
|
26
|
+
// ── Source pointer / provenance ─────────────────────────────────────────
|
|
27
|
+
// These are not columns on the `products` table; they live on the parallel
|
|
28
|
+
// catalog Provenance row. Declared here so the indexer / overlay resolver
|
|
29
|
+
// know how to treat them.
|
|
30
|
+
{
|
|
31
|
+
path: "source.kind",
|
|
32
|
+
class: "managed",
|
|
33
|
+
merge: "source-only",
|
|
34
|
+
drift: "critical",
|
|
35
|
+
reindex: "entry",
|
|
36
|
+
snapshot: "on-book",
|
|
37
|
+
query: "indexed-column",
|
|
38
|
+
localized: false,
|
|
39
|
+
visibility: ["staff"],
|
|
40
|
+
editRole: "none",
|
|
41
|
+
overrideFriction: "none",
|
|
42
|
+
sourceFreshness: "sync",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
path: "source.ref",
|
|
46
|
+
class: "managed",
|
|
47
|
+
merge: "source-only",
|
|
48
|
+
drift: "critical",
|
|
49
|
+
reindex: "none",
|
|
50
|
+
snapshot: "on-book",
|
|
51
|
+
query: "indexed-column",
|
|
52
|
+
localized: false,
|
|
53
|
+
visibility: ["staff"],
|
|
54
|
+
editRole: "none",
|
|
55
|
+
overrideFriction: "none",
|
|
56
|
+
sourceFreshness: "sync",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: "seller.operator_id",
|
|
60
|
+
class: "managed",
|
|
61
|
+
merge: "source-only",
|
|
62
|
+
drift: "critical",
|
|
63
|
+
reindex: "none",
|
|
64
|
+
snapshot: "on-book",
|
|
65
|
+
query: "indexed-column",
|
|
66
|
+
localized: false,
|
|
67
|
+
visibility: ["staff"],
|
|
68
|
+
editRole: "none",
|
|
69
|
+
overrideFriction: "none",
|
|
70
|
+
sourceFreshness: "static",
|
|
71
|
+
},
|
|
72
|
+
// ── Identity / lifecycle ────────────────────────────────────────────────
|
|
73
|
+
{
|
|
74
|
+
path: "id",
|
|
75
|
+
class: "managed",
|
|
76
|
+
merge: "source-only",
|
|
77
|
+
drift: "critical",
|
|
78
|
+
reindex: "none",
|
|
79
|
+
snapshot: "on-book",
|
|
80
|
+
query: "first-class-table",
|
|
81
|
+
localized: false,
|
|
82
|
+
visibility: ["staff", "customer", "partner"],
|
|
83
|
+
editRole: "none",
|
|
84
|
+
overrideFriction: "none",
|
|
85
|
+
sourceFreshness: "static",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: "createdAt",
|
|
89
|
+
class: "managed",
|
|
90
|
+
merge: "source-only",
|
|
91
|
+
drift: "none",
|
|
92
|
+
reindex: "none",
|
|
93
|
+
snapshot: "on-book",
|
|
94
|
+
query: "indexed-column",
|
|
95
|
+
localized: false,
|
|
96
|
+
visibility: ["staff"],
|
|
97
|
+
editRole: "none",
|
|
98
|
+
overrideFriction: "none",
|
|
99
|
+
sourceFreshness: "static",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
path: "updatedAt",
|
|
103
|
+
class: "managed",
|
|
104
|
+
merge: "source-only",
|
|
105
|
+
drift: "none",
|
|
106
|
+
reindex: "none",
|
|
107
|
+
snapshot: "never",
|
|
108
|
+
query: "indexed-column",
|
|
109
|
+
localized: false,
|
|
110
|
+
visibility: ["staff"],
|
|
111
|
+
editRole: "none",
|
|
112
|
+
overrideFriction: "none",
|
|
113
|
+
sourceFreshness: "sync",
|
|
114
|
+
},
|
|
115
|
+
// ── Merchandisable / marketing ──────────────────────────────────────────
|
|
116
|
+
// Note: `name` maps to "title" in catalog vocabulary. The existing schema
|
|
117
|
+
// column stays `name` for backwards compatibility; the field-policy path
|
|
118
|
+
// uses the schema column name so the indexer can map it directly.
|
|
119
|
+
{
|
|
120
|
+
path: "name",
|
|
121
|
+
class: "merchandisable",
|
|
122
|
+
merge: "replace",
|
|
123
|
+
drift: "medium",
|
|
124
|
+
reindex: "entry-locale",
|
|
125
|
+
snapshot: "on-book",
|
|
126
|
+
query: "indexed-column",
|
|
127
|
+
localized: true,
|
|
128
|
+
visibility: ["staff", "customer", "partner"],
|
|
129
|
+
editRole: "marketing",
|
|
130
|
+
overrideFriction: "none",
|
|
131
|
+
sourceFreshness: "sync",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
path: "description",
|
|
135
|
+
class: "merchandisable",
|
|
136
|
+
merge: "replace",
|
|
137
|
+
drift: "low",
|
|
138
|
+
reindex: "entry-locale",
|
|
139
|
+
snapshot: "on-book",
|
|
140
|
+
query: "blob-only",
|
|
141
|
+
localized: true,
|
|
142
|
+
visibility: ["staff", "customer", "partner"],
|
|
143
|
+
editRole: "marketing",
|
|
144
|
+
overrideFriction: "none",
|
|
145
|
+
sourceFreshness: "sync",
|
|
146
|
+
},
|
|
147
|
+
// TODO(catalog): split into marketing_tags + facet_tags per architecture
|
|
148
|
+
// §7.1 (human-readable + machine-evaluable rule). Today's schema has one
|
|
149
|
+
// `tags` column; declared here as merchandisable + additive-set so
|
|
150
|
+
// marketing can extend the source set without trampling it. Until the
|
|
151
|
+
// split lands, search-facet leakage is possible (marketing edits affect
|
|
152
|
+
// search facets); flag for follow-up.
|
|
153
|
+
{
|
|
154
|
+
path: "tags[]",
|
|
155
|
+
class: "merchandisable",
|
|
156
|
+
merge: "additive-set",
|
|
157
|
+
drift: "low",
|
|
158
|
+
reindex: "entry",
|
|
159
|
+
snapshot: "on-book",
|
|
160
|
+
query: "indexed-column",
|
|
161
|
+
localized: false,
|
|
162
|
+
visibility: ["staff", "customer", "partner"],
|
|
163
|
+
editRole: "marketing",
|
|
164
|
+
overrideFriction: "none",
|
|
165
|
+
sourceFreshness: "sync",
|
|
166
|
+
},
|
|
167
|
+
// ── Structural / facet-affecting ───────────────────────────────────────
|
|
168
|
+
{
|
|
169
|
+
path: "status",
|
|
170
|
+
class: "structural",
|
|
171
|
+
merge: "source-only",
|
|
172
|
+
drift: "high",
|
|
173
|
+
reindex: "facet-affecting",
|
|
174
|
+
snapshot: "on-book",
|
|
175
|
+
query: "indexed-column",
|
|
176
|
+
localized: false,
|
|
177
|
+
visibility: ["staff"],
|
|
178
|
+
editRole: "none",
|
|
179
|
+
overrideFriction: "none",
|
|
180
|
+
sourceFreshness: "sync",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
path: "bookingMode",
|
|
184
|
+
class: "structural",
|
|
185
|
+
merge: "source-only",
|
|
186
|
+
drift: "high",
|
|
187
|
+
reindex: "facet-affecting",
|
|
188
|
+
snapshot: "on-book",
|
|
189
|
+
query: "indexed-column",
|
|
190
|
+
localized: false,
|
|
191
|
+
visibility: ["staff", "customer", "partner"],
|
|
192
|
+
editRole: "none",
|
|
193
|
+
overrideFriction: "none",
|
|
194
|
+
sourceFreshness: "sync",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
path: "capacityMode",
|
|
198
|
+
class: "structural",
|
|
199
|
+
merge: "source-only",
|
|
200
|
+
drift: "medium",
|
|
201
|
+
reindex: "entry",
|
|
202
|
+
snapshot: "on-book",
|
|
203
|
+
query: "indexed-column",
|
|
204
|
+
localized: false,
|
|
205
|
+
visibility: ["staff"],
|
|
206
|
+
editRole: "none",
|
|
207
|
+
overrideFriction: "none",
|
|
208
|
+
sourceFreshness: "sync",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
// The `visibility` *column* on the products table — distinct from the
|
|
212
|
+
// catalog plane's audience-visibility axis.
|
|
213
|
+
path: "visibility",
|
|
214
|
+
class: "structural",
|
|
215
|
+
merge: "source-only",
|
|
216
|
+
drift: "high",
|
|
217
|
+
reindex: "facet-affecting",
|
|
218
|
+
snapshot: "on-book",
|
|
219
|
+
query: "indexed-column",
|
|
220
|
+
localized: false,
|
|
221
|
+
visibility: ["staff"],
|
|
222
|
+
editRole: "none",
|
|
223
|
+
overrideFriction: "none",
|
|
224
|
+
sourceFreshness: "sync",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
path: "activated",
|
|
228
|
+
class: "structural",
|
|
229
|
+
merge: "source-only",
|
|
230
|
+
drift: "high",
|
|
231
|
+
reindex: "facet-affecting",
|
|
232
|
+
snapshot: "on-book",
|
|
233
|
+
query: "indexed-column",
|
|
234
|
+
localized: false,
|
|
235
|
+
visibility: ["staff"],
|
|
236
|
+
editRole: "none",
|
|
237
|
+
overrideFriction: "none",
|
|
238
|
+
sourceFreshness: "sync",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
path: "productTypeId",
|
|
242
|
+
class: "structural",
|
|
243
|
+
merge: "source-only",
|
|
244
|
+
drift: "medium",
|
|
245
|
+
reindex: "facet-affecting",
|
|
246
|
+
snapshot: "on-book",
|
|
247
|
+
query: "indexed-column",
|
|
248
|
+
localized: false,
|
|
249
|
+
visibility: ["staff", "customer", "partner"],
|
|
250
|
+
editRole: "none",
|
|
251
|
+
overrideFriction: "none",
|
|
252
|
+
sourceFreshness: "sync",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
path: "facilityId",
|
|
256
|
+
class: "structural",
|
|
257
|
+
merge: "source-only",
|
|
258
|
+
drift: "medium",
|
|
259
|
+
reindex: "entry",
|
|
260
|
+
snapshot: "on-book",
|
|
261
|
+
query: "indexed-column",
|
|
262
|
+
localized: false,
|
|
263
|
+
visibility: ["staff"],
|
|
264
|
+
editRole: "none",
|
|
265
|
+
overrideFriction: "none",
|
|
266
|
+
sourceFreshness: "sync",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
path: "pax",
|
|
270
|
+
class: "structural",
|
|
271
|
+
merge: "source-only",
|
|
272
|
+
drift: "medium",
|
|
273
|
+
reindex: "entry",
|
|
274
|
+
snapshot: "on-book",
|
|
275
|
+
query: "indexed-column",
|
|
276
|
+
localized: false,
|
|
277
|
+
visibility: ["staff", "customer", "partner"],
|
|
278
|
+
editRole: "none",
|
|
279
|
+
overrideFriction: "none",
|
|
280
|
+
sourceFreshness: "sync",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
path: "startDate",
|
|
284
|
+
class: "structural",
|
|
285
|
+
merge: "source-only",
|
|
286
|
+
drift: "medium",
|
|
287
|
+
reindex: "facet-affecting",
|
|
288
|
+
snapshot: "on-book",
|
|
289
|
+
query: "indexed-column",
|
|
290
|
+
localized: false,
|
|
291
|
+
visibility: ["staff", "customer", "partner"],
|
|
292
|
+
editRole: "none",
|
|
293
|
+
overrideFriction: "none",
|
|
294
|
+
sourceFreshness: "sync",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
path: "endDate",
|
|
298
|
+
class: "structural",
|
|
299
|
+
merge: "source-only",
|
|
300
|
+
drift: "medium",
|
|
301
|
+
reindex: "facet-affecting",
|
|
302
|
+
snapshot: "on-book",
|
|
303
|
+
query: "indexed-column",
|
|
304
|
+
localized: false,
|
|
305
|
+
visibility: ["staff", "customer", "partner"],
|
|
306
|
+
editRole: "none",
|
|
307
|
+
overrideFriction: "none",
|
|
308
|
+
sourceFreshness: "sync",
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
path: "timezone",
|
|
312
|
+
class: "managed",
|
|
313
|
+
merge: "source-only",
|
|
314
|
+
drift: "low",
|
|
315
|
+
reindex: "none",
|
|
316
|
+
snapshot: "on-book",
|
|
317
|
+
query: "blob-only",
|
|
318
|
+
localized: false,
|
|
319
|
+
visibility: ["staff", "customer", "partner"],
|
|
320
|
+
editRole: "none",
|
|
321
|
+
overrideFriction: "none",
|
|
322
|
+
sourceFreshness: "sync",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
path: "reservationTimeoutMinutes",
|
|
326
|
+
class: "structural",
|
|
327
|
+
merge: "source-only",
|
|
328
|
+
drift: "low",
|
|
329
|
+
reindex: "none",
|
|
330
|
+
snapshot: "never",
|
|
331
|
+
query: "blob-only",
|
|
332
|
+
localized: false,
|
|
333
|
+
visibility: ["staff"],
|
|
334
|
+
editRole: "none",
|
|
335
|
+
overrideFriction: "none",
|
|
336
|
+
sourceFreshness: "sync",
|
|
337
|
+
},
|
|
338
|
+
// ── Pricing (configured defaults — not the live quote) ──────────────────
|
|
339
|
+
// These are the operator's configured prices on the product. The live
|
|
340
|
+
// quote engine resolves volatile-live `quote_price` separately at quote
|
|
341
|
+
// time and is captured at booking commit (snapshot mode handled by the
|
|
342
|
+
// pricing module's own field policy when it adopts).
|
|
343
|
+
{
|
|
344
|
+
path: "sellAmountCents",
|
|
345
|
+
class: "structural",
|
|
346
|
+
merge: "source-only",
|
|
347
|
+
drift: "high",
|
|
348
|
+
reindex: "entry",
|
|
349
|
+
snapshot: "on-quote-and-book",
|
|
350
|
+
query: "indexed-column",
|
|
351
|
+
localized: false,
|
|
352
|
+
visibility: ["staff", "customer", "partner"],
|
|
353
|
+
editRole: "none",
|
|
354
|
+
overrideFriction: "none",
|
|
355
|
+
sourceFreshness: "sync",
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
path: "sellCurrency",
|
|
359
|
+
class: "managed",
|
|
360
|
+
merge: "source-only",
|
|
361
|
+
drift: "high",
|
|
362
|
+
reindex: "entry",
|
|
363
|
+
snapshot: "on-quote-and-book",
|
|
364
|
+
query: "indexed-column",
|
|
365
|
+
localized: false,
|
|
366
|
+
visibility: ["staff", "customer", "partner"],
|
|
367
|
+
editRole: "none",
|
|
368
|
+
overrideFriction: "none",
|
|
369
|
+
sourceFreshness: "sync",
|
|
370
|
+
},
|
|
371
|
+
// ── Internal / staff-only ──────────────────────────────────────────────
|
|
372
|
+
{
|
|
373
|
+
path: "costAmountCents",
|
|
374
|
+
class: "managed",
|
|
375
|
+
merge: "source-only",
|
|
376
|
+
drift: "medium",
|
|
377
|
+
reindex: "none",
|
|
378
|
+
snapshot: "never",
|
|
379
|
+
query: "blob-only",
|
|
380
|
+
localized: false,
|
|
381
|
+
visibility: ["staff"],
|
|
382
|
+
editRole: "none",
|
|
383
|
+
overrideFriction: "none",
|
|
384
|
+
sourceFreshness: "sync",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
path: "marginPercent",
|
|
388
|
+
class: "managed",
|
|
389
|
+
merge: "source-only",
|
|
390
|
+
drift: "medium",
|
|
391
|
+
reindex: "none",
|
|
392
|
+
snapshot: "never",
|
|
393
|
+
query: "blob-only",
|
|
394
|
+
localized: false,
|
|
395
|
+
visibility: ["staff"],
|
|
396
|
+
editRole: "none",
|
|
397
|
+
overrideFriction: "none",
|
|
398
|
+
sourceFreshness: "sync",
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
/**
|
|
402
|
+
* Resolved field-policy registry for products. Verticals adopt the catalog
|
|
403
|
+
* plane by exporting this; templates wire it into the indexer, overlay
|
|
404
|
+
* resolver, and snapshot capture pipeline.
|
|
405
|
+
*/
|
|
406
|
+
export const productCatalogPolicy = defineFieldPolicy(PRODUCT_FIELD_POLICY);
|
|
407
|
+
export { PRODUCT_FIELD_POLICY };
|
package/dist/routes.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ type Env = {
|
|
|
3
3
|
Variables: {
|
|
4
4
|
db: PostgresJsDatabase;
|
|
5
5
|
userId?: string;
|
|
6
|
+
eventBus?: import("@voyantjs/core").EventBus;
|
|
6
7
|
};
|
|
7
8
|
};
|
|
8
9
|
export declare const productRoutes: import("hono/hono-base").HonoBase<Env, {
|
|
@@ -80,18 +81,18 @@ export declare const productRoutes: import("hono/hono-base").HonoBase<Env, {
|
|
|
80
81
|
endDate: string | null;
|
|
81
82
|
timezone: string | null;
|
|
82
83
|
description: string | null;
|
|
84
|
+
visibility: "public" | "private" | "hidden";
|
|
83
85
|
bookingMode: "date" | "other" | "date_time" | "open" | "stay" | "transfer" | "itinerary";
|
|
84
86
|
capacityMode: "free_sale" | "limited" | "on_request";
|
|
85
|
-
visibility: "public" | "private" | "hidden";
|
|
86
87
|
activated: boolean;
|
|
88
|
+
productTypeId: string | null;
|
|
89
|
+
facilityId: string | null;
|
|
90
|
+
pax: number | null;
|
|
87
91
|
reservationTimeoutMinutes: number | null;
|
|
88
|
-
sellCurrency: string;
|
|
89
92
|
sellAmountCents: number | null;
|
|
93
|
+
sellCurrency: string;
|
|
90
94
|
costAmountCents: number | null;
|
|
91
95
|
marginPercent: number | null;
|
|
92
|
-
facilityId: string | null;
|
|
93
|
-
pax: number | null;
|
|
94
|
-
productTypeId: string | null;
|
|
95
96
|
tags: string[] | null;
|
|
96
97
|
};
|
|
97
98
|
};
|
|
@@ -4120,8 +4121,8 @@ export declare const productRoutes: import("hono/hono-base").HonoBase<Env, {
|
|
|
4120
4121
|
createdAt: string;
|
|
4121
4122
|
notes: string | null;
|
|
4122
4123
|
productId: string;
|
|
4123
|
-
versionNumber: number;
|
|
4124
4124
|
snapshot: import("hono/utils/types").JSONValue;
|
|
4125
|
+
versionNumber: number;
|
|
4125
4126
|
authorId: string;
|
|
4126
4127
|
};
|
|
4127
4128
|
};
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAkFjE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAkFjE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,CAAC,EAAE,OAAO,gBAAgB,EAAE,QAAQ,CAAA;KAC7C,CAAA;CACF,CAAA;AAMD,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAkiDtB,CAAA;AAEJ,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAA"}
|
package/dist/routes.js
CHANGED
|
@@ -19,9 +19,9 @@ export const productRoutes = new Hono()
|
|
|
19
19
|
})
|
|
20
20
|
// POST / — Create product
|
|
21
21
|
.post("/", async (c) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}, 201);
|
|
22
|
+
const row = await productsService.createProduct(c.get("db"), await parseJsonBody(c, insertProductSchema));
|
|
23
|
+
await c.get("eventBus")?.emit("product.created", { id: row.id });
|
|
24
|
+
return c.json({ data: row }, 201);
|
|
25
25
|
})
|
|
26
26
|
// ==========================================================================
|
|
27
27
|
// Product operating configuration
|
|
@@ -729,6 +729,7 @@ export const productRoutes = new Hono()
|
|
|
729
729
|
if (!row) {
|
|
730
730
|
return c.json({ error: "Product not found" }, 404);
|
|
731
731
|
}
|
|
732
|
+
await c.get("eventBus")?.emit("product.updated", { id: row.id });
|
|
732
733
|
return c.json({ data: row });
|
|
733
734
|
})
|
|
734
735
|
// DELETE /:id — Delete product
|
|
@@ -737,6 +738,7 @@ export const productRoutes = new Hono()
|
|
|
737
738
|
if (!row) {
|
|
738
739
|
return c.json({ error: "Product not found" }, 404);
|
|
739
740
|
}
|
|
741
|
+
await c.get("eventBus")?.emit("product.deleted", { id: row.id });
|
|
740
742
|
return c.json({ success: true }, 200);
|
|
741
743
|
})
|
|
742
744
|
// ==========================================================================
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-plane integration for the products service.
|
|
3
|
+
*
|
|
4
|
+
* Adds catalog-aware service methods alongside the existing `productsService`
|
|
5
|
+
* surface in `service.ts`. Routes opt in: the original `getProductById` /
|
|
6
|
+
* `listProducts` continue to return raw DB rows; the methods here return
|
|
7
|
+
* resolved CatalogEntry views with overlays + visibility filtering applied.
|
|
8
|
+
*
|
|
9
|
+
* Existing service code is untouched. Migration is per-route, gradual.
|
|
10
|
+
*
|
|
11
|
+
* Naming note: this file is `service-catalog-plane.ts` (not `service-catalog.ts`)
|
|
12
|
+
* because the existing `service-catalog.ts` handles the products module's own
|
|
13
|
+
* catalog management (categories, tags, types). The "catalog plane" is the
|
|
14
|
+
* cross-vertical projection / overlay / snapshot infrastructure from
|
|
15
|
+
* `@voyantjs/catalog`.
|
|
16
|
+
*
|
|
17
|
+
* See `docs/architecture/catalog-architecture.md` §9.1 for the integration
|
|
18
|
+
* pattern this file establishes (replicated for cruises, hospitality, etc.
|
|
19
|
+
* in their own service-catalog-plane.ts files).
|
|
20
|
+
*/
|
|
21
|
+
import { type CaptureSnapshotInput, type DocumentBuilder, type DocumentEmitter, type IndexerDocument, type IndexerSlice, type PricingBasis, type Provenance, type ResolvedView, type ResolverScope, type Visibility } from "@voyantjs/catalog";
|
|
22
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
23
|
+
import { products } from "./schema-core.js";
|
|
24
|
+
/**
|
|
25
|
+
* Maps a product row to a field-keyed projection consumable by the catalog
|
|
26
|
+
* resolver. Field paths match the policy registry declarations in
|
|
27
|
+
* `catalog-policy.ts`.
|
|
28
|
+
*
|
|
29
|
+
* Provenance fields (`source.kind`, `source.ref`, `seller.operator_id`) are
|
|
30
|
+
* synthesized: today's products module models operator-owned inventory
|
|
31
|
+
* exclusively, so `source.kind = "owned"` and `source.ref = undefined`.
|
|
32
|
+
* When sourced products land (e.g. via Voyant Connect), this helper picks
|
|
33
|
+
* up the provenance from a parallel provenance row instead.
|
|
34
|
+
*/
|
|
35
|
+
export declare function productRowToProjection(row: typeof products.$inferSelect, context: {
|
|
36
|
+
sellerOperatorId: string;
|
|
37
|
+
}): ReadonlyMap<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Returns the Provenance tuple for a product row. Owned products synthesize
|
|
40
|
+
* a `source.kind: "owned"` provenance with `static` freshness; sourced
|
|
41
|
+
* products (Voyant Connect / GDS / direct API) carry their actual source
|
|
42
|
+
* connection identity. Phase 1 ships only the owned form.
|
|
43
|
+
*/
|
|
44
|
+
export declare function productProvenance(_row: typeof products.$inferSelect, _context: {
|
|
45
|
+
sellerOperatorId: string;
|
|
46
|
+
}): Provenance;
|
|
47
|
+
/** Service-context the catalog-aware methods need. Templates wire this in. */
|
|
48
|
+
export interface ProductCatalogContext {
|
|
49
|
+
/** The deployment's operator/tenant identifier — synthesized into provenance. */
|
|
50
|
+
sellerOperatorId: string;
|
|
51
|
+
/** Variant scope for the request. */
|
|
52
|
+
scope: ResolverScope;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Catalog-aware product fetch. Returns the resolved view (source projection
|
|
56
|
+
* + active overlays + visibility filtering) instead of the raw DB row.
|
|
57
|
+
*
|
|
58
|
+
* The original `productsService.getProductById` continues to return raw
|
|
59
|
+
* rows — routes that haven't migrated to the catalog plane keep working.
|
|
60
|
+
*
|
|
61
|
+
* Returns `null` if no product with `id` exists.
|
|
62
|
+
*/
|
|
63
|
+
export declare function getResolvedProductById(db: AnyDrizzleDb, id: string, context: ProductCatalogContext): Promise<ResolvedView | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Catalog-aware product list. Returns resolved views per row.
|
|
66
|
+
*
|
|
67
|
+
* Caller fetches the rows (typically via the existing `productsService.listProducts`
|
|
68
|
+
* with whatever filtering / pagination / sort the route applies) and passes
|
|
69
|
+
* them in. This keeps query construction in the existing service layer and
|
|
70
|
+
* adds the catalog overlay step on top.
|
|
71
|
+
*
|
|
72
|
+
* For Phase B v1, this is a naive per-row resolver that issues one overlay
|
|
73
|
+
* fetch per product. Real list paths (storefront browse, admin search)
|
|
74
|
+
* should go through the search index instead — `IndexerService.search` is
|
|
75
|
+
* already wired for that purpose. Use this method for small admin-facing
|
|
76
|
+
* lists or detail-page composition where the index isn't on the read path.
|
|
77
|
+
*
|
|
78
|
+
* Production-grade batched overlay fetch is a TODO; the catalog plane
|
|
79
|
+
* supports it conceptually but `fetchOverlaysForEntity` is currently
|
|
80
|
+
* one-entity-at-a-time. A future `fetchOverlaysForEntities(db, [(module, id), ...])`
|
|
81
|
+
* lands with the indexer hot-path optimization.
|
|
82
|
+
*/
|
|
83
|
+
export declare function listResolvedProducts(db: AnyDrizzleDb, rows: ReadonlyArray<typeof products.$inferSelect>, context: ProductCatalogContext): Promise<ResolvedView[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Build a `CaptureSnapshotInput` for a product to feed into the catalog
|
|
86
|
+
* plane's `captureSnapshot` / `captureSnapshotGraph` helpers at booking
|
|
87
|
+
* commit time. Fetches the product, resolves its view (overlays applied,
|
|
88
|
+
* visibility filter for the supplied scope), and returns the snapshot
|
|
89
|
+
* input shape.
|
|
90
|
+
*
|
|
91
|
+
* Returns `null` if the product doesn't exist.
|
|
92
|
+
*
|
|
93
|
+
* Composition: a single-product booking calls this once and passes the
|
|
94
|
+
* result to `captureSnapshot`. A composite booking (e.g. a tour-package
|
|
95
|
+
* booking with referenced hospitality + excursions) calls this and the
|
|
96
|
+
* other verticals' equivalents, collects the inputs, and passes them all
|
|
97
|
+
* to `captureSnapshotGraph` in one transaction.
|
|
98
|
+
*/
|
|
99
|
+
export declare function buildProductSnapshotInput(db: AnyDrizzleDb, productId: string, context: ProductCatalogContext & {
|
|
100
|
+
pricingBasis?: PricingBasis;
|
|
101
|
+
}): Promise<Omit<CaptureSnapshotInput, "bookingId"> | null>;
|
|
102
|
+
/**
|
|
103
|
+
* Construct a sync `DocumentEmitter` for products. The emitter takes a
|
|
104
|
+
* pre-fetched product row + a slice and returns the indexer document
|
|
105
|
+
* (filtered by visibility, with blob-only fields skipped).
|
|
106
|
+
*
|
|
107
|
+
* Bulk-reindex pipelines that already have rows in hand call this directly.
|
|
108
|
+
* Live reindex paths use `createProductDocumentBuilder` below, which fetches
|
|
109
|
+
* the row before emitting.
|
|
110
|
+
*/
|
|
111
|
+
export declare function createProductDocumentEmitter(context: {
|
|
112
|
+
sellerOperatorId: string;
|
|
113
|
+
}): DocumentEmitter<typeof products.$inferSelect>;
|
|
114
|
+
/**
|
|
115
|
+
* Async `DocumentBuilder` for products — fetches the row by id, then emits.
|
|
116
|
+
* Plug this into `IndexerService.reindexEntity` for live reindex events.
|
|
117
|
+
*
|
|
118
|
+
* Returns `null` if the product no longer exists (e.g. it was deleted
|
|
119
|
+
* between the reindex enqueue and the worker picking it up). Callers can
|
|
120
|
+
* treat `null` as a delete signal.
|
|
121
|
+
*/
|
|
122
|
+
export declare function createProductDocumentBuilder(db: AnyDrizzleDb, context: {
|
|
123
|
+
sellerOperatorId: string;
|
|
124
|
+
}): DocumentBuilder;
|
|
125
|
+
/**
|
|
126
|
+
* Re-exports for routes that only import from this file.
|
|
127
|
+
*/
|
|
128
|
+
export type { CaptureSnapshotInput, DocumentBuilder, DocumentEmitter, IndexerDocument, IndexerSlice, PricingBasis, Provenance, ResolvedView, ResolverScope, Visibility, };
|
|
129
|
+
//# sourceMappingURL=service-catalog-plane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,eAAe,EACpB,KAAK,eAAe,EAEpB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAElB,KAAK,UAAU,EAChB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAIhD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAc3C;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,QAAQ,CAAC,YAAY,EACjC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACpC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAuC9B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,QAAQ,CAAC,YAAY,EAClC,QAAQ,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACrC,UAAU,CAKZ;AAED,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,gBAAgB,EAAE,MAAM,CAAA;IACxB,qCAAqC;IACrC,KAAK,EAAE,aAAa,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAS9B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,aAAa,CAAC,OAAO,QAAQ,CAAC,YAAY,CAAC,EACjD,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,EAAE,CAAC,CAkBzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,qBAAqB,GAAG;IAAE,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GAC/D,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,CASzD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,gBAAgB,EAAE,MAAM,CAAA;CACzB,GAAG,eAAe,CAAC,OAAO,QAAQ,CAAC,YAAY,CAAC,CAWhD;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACpC,eAAe,CAQjB;AAED;;GAEG;AACH,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,aAAa,EACb,UAAU,GACX,CAAA"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-plane integration for the products service.
|
|
3
|
+
*
|
|
4
|
+
* Adds catalog-aware service methods alongside the existing `productsService`
|
|
5
|
+
* surface in `service.ts`. Routes opt in: the original `getProductById` /
|
|
6
|
+
* `listProducts` continue to return raw DB rows; the methods here return
|
|
7
|
+
* resolved CatalogEntry views with overlays + visibility filtering applied.
|
|
8
|
+
*
|
|
9
|
+
* Existing service code is untouched. Migration is per-route, gradual.
|
|
10
|
+
*
|
|
11
|
+
* Naming note: this file is `service-catalog-plane.ts` (not `service-catalog.ts`)
|
|
12
|
+
* because the existing `service-catalog.ts` handles the products module's own
|
|
13
|
+
* catalog management (categories, tags, types). The "catalog plane" is the
|
|
14
|
+
* cross-vertical projection / overlay / snapshot infrastructure from
|
|
15
|
+
* `@voyantjs/catalog`.
|
|
16
|
+
*
|
|
17
|
+
* See `docs/architecture/catalog-architecture.md` §9.1 for the integration
|
|
18
|
+
* pattern this file establishes (replicated for cruises, hospitality, etc.
|
|
19
|
+
* in their own service-catalog-plane.ts files).
|
|
20
|
+
*/
|
|
21
|
+
import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, resolveEntityView, } from "@voyantjs/catalog";
|
|
22
|
+
import { eq } from "drizzle-orm";
|
|
23
|
+
import { productCatalogPolicy } from "./catalog-policy.js";
|
|
24
|
+
import { products } from "./schema-core.js";
|
|
25
|
+
/**
|
|
26
|
+
* Lazy-initialized registry. Built once per process; the field-policy file
|
|
27
|
+
* is static so this is safe to memoize.
|
|
28
|
+
*/
|
|
29
|
+
let _registry;
|
|
30
|
+
function getProductsRegistry() {
|
|
31
|
+
if (!_registry) {
|
|
32
|
+
_registry = createFieldPolicyRegistry(productCatalogPolicy);
|
|
33
|
+
}
|
|
34
|
+
return _registry;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Maps a product row to a field-keyed projection consumable by the catalog
|
|
38
|
+
* resolver. Field paths match the policy registry declarations in
|
|
39
|
+
* `catalog-policy.ts`.
|
|
40
|
+
*
|
|
41
|
+
* Provenance fields (`source.kind`, `source.ref`, `seller.operator_id`) are
|
|
42
|
+
* synthesized: today's products module models operator-owned inventory
|
|
43
|
+
* exclusively, so `source.kind = "owned"` and `source.ref = undefined`.
|
|
44
|
+
* When sourced products land (e.g. via Voyant Connect), this helper picks
|
|
45
|
+
* up the provenance from a parallel provenance row instead.
|
|
46
|
+
*/
|
|
47
|
+
export function productRowToProjection(row, context) {
|
|
48
|
+
const projection = new Map([
|
|
49
|
+
// Provenance — synthesized for owned products.
|
|
50
|
+
["source.kind", "owned"],
|
|
51
|
+
["seller.operator_id", context.sellerOperatorId],
|
|
52
|
+
// Identity
|
|
53
|
+
["id", row.id],
|
|
54
|
+
["createdAt", row.createdAt],
|
|
55
|
+
["updatedAt", row.updatedAt],
|
|
56
|
+
// Merchandisable
|
|
57
|
+
["name", row.name],
|
|
58
|
+
["description", row.description],
|
|
59
|
+
["tags[]", row.tags],
|
|
60
|
+
// Structural
|
|
61
|
+
["status", row.status],
|
|
62
|
+
["bookingMode", row.bookingMode],
|
|
63
|
+
["capacityMode", row.capacityMode],
|
|
64
|
+
["visibility", row.visibility],
|
|
65
|
+
["activated", row.activated],
|
|
66
|
+
["productTypeId", row.productTypeId],
|
|
67
|
+
["facilityId", row.facilityId],
|
|
68
|
+
["pax", row.pax],
|
|
69
|
+
["startDate", row.startDate],
|
|
70
|
+
["endDate", row.endDate],
|
|
71
|
+
["timezone", row.timezone],
|
|
72
|
+
["reservationTimeoutMinutes", row.reservationTimeoutMinutes],
|
|
73
|
+
// Pricing (configured defaults — quote-time prices come from pricing module)
|
|
74
|
+
["sellAmountCents", row.sellAmountCents],
|
|
75
|
+
["sellCurrency", row.sellCurrency],
|
|
76
|
+
// Internal / staff-only
|
|
77
|
+
["costAmountCents", row.costAmountCents],
|
|
78
|
+
["marginPercent", row.marginPercent],
|
|
79
|
+
]);
|
|
80
|
+
return projection;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Returns the Provenance tuple for a product row. Owned products synthesize
|
|
84
|
+
* a `source.kind: "owned"` provenance with `static` freshness; sourced
|
|
85
|
+
* products (Voyant Connect / GDS / direct API) carry their actual source
|
|
86
|
+
* connection identity. Phase 1 ships only the owned form.
|
|
87
|
+
*/
|
|
88
|
+
export function productProvenance(_row, _context) {
|
|
89
|
+
return {
|
|
90
|
+
source_kind: "owned",
|
|
91
|
+
source_freshness: "static",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Catalog-aware product fetch. Returns the resolved view (source projection
|
|
96
|
+
* + active overlays + visibility filtering) instead of the raw DB row.
|
|
97
|
+
*
|
|
98
|
+
* The original `productsService.getProductById` continues to return raw
|
|
99
|
+
* rows — routes that haven't migrated to the catalog plane keep working.
|
|
100
|
+
*
|
|
101
|
+
* Returns `null` if no product with `id` exists.
|
|
102
|
+
*/
|
|
103
|
+
export async function getResolvedProductById(db, id, context) {
|
|
104
|
+
const rows = await db.select().from(products).where(eq(products.id, id)).limit(1);
|
|
105
|
+
const row = rows[0];
|
|
106
|
+
if (!row)
|
|
107
|
+
return null;
|
|
108
|
+
const projection = productRowToProjection(row, {
|
|
109
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
110
|
+
});
|
|
111
|
+
return resolveEntityView(db, getProductsRegistry(), "products", id, projection, context.scope);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Catalog-aware product list. Returns resolved views per row.
|
|
115
|
+
*
|
|
116
|
+
* Caller fetches the rows (typically via the existing `productsService.listProducts`
|
|
117
|
+
* with whatever filtering / pagination / sort the route applies) and passes
|
|
118
|
+
* them in. This keeps query construction in the existing service layer and
|
|
119
|
+
* adds the catalog overlay step on top.
|
|
120
|
+
*
|
|
121
|
+
* For Phase B v1, this is a naive per-row resolver that issues one overlay
|
|
122
|
+
* fetch per product. Real list paths (storefront browse, admin search)
|
|
123
|
+
* should go through the search index instead — `IndexerService.search` is
|
|
124
|
+
* already wired for that purpose. Use this method for small admin-facing
|
|
125
|
+
* lists or detail-page composition where the index isn't on the read path.
|
|
126
|
+
*
|
|
127
|
+
* Production-grade batched overlay fetch is a TODO; the catalog plane
|
|
128
|
+
* supports it conceptually but `fetchOverlaysForEntity` is currently
|
|
129
|
+
* one-entity-at-a-time. A future `fetchOverlaysForEntities(db, [(module, id), ...])`
|
|
130
|
+
* lands with the indexer hot-path optimization.
|
|
131
|
+
*/
|
|
132
|
+
export async function listResolvedProducts(db, rows, context) {
|
|
133
|
+
const registry = getProductsRegistry();
|
|
134
|
+
const views = [];
|
|
135
|
+
for (const row of rows) {
|
|
136
|
+
const projection = productRowToProjection(row, {
|
|
137
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
138
|
+
});
|
|
139
|
+
const view = await resolveEntityView(db, registry, "products", row.id, projection, context.scope);
|
|
140
|
+
views.push(view);
|
|
141
|
+
}
|
|
142
|
+
return views;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build a `CaptureSnapshotInput` for a product to feed into the catalog
|
|
146
|
+
* plane's `captureSnapshot` / `captureSnapshotGraph` helpers at booking
|
|
147
|
+
* commit time. Fetches the product, resolves its view (overlays applied,
|
|
148
|
+
* visibility filter for the supplied scope), and returns the snapshot
|
|
149
|
+
* input shape.
|
|
150
|
+
*
|
|
151
|
+
* Returns `null` if the product doesn't exist.
|
|
152
|
+
*
|
|
153
|
+
* Composition: a single-product booking calls this once and passes the
|
|
154
|
+
* result to `captureSnapshot`. A composite booking (e.g. a tour-package
|
|
155
|
+
* booking with referenced hospitality + excursions) calls this and the
|
|
156
|
+
* other verticals' equivalents, collects the inputs, and passes them all
|
|
157
|
+
* to `captureSnapshotGraph` in one transaction.
|
|
158
|
+
*/
|
|
159
|
+
export async function buildProductSnapshotInput(db, productId, context) {
|
|
160
|
+
const view = await getResolvedProductById(db, productId, context);
|
|
161
|
+
if (!view)
|
|
162
|
+
return null;
|
|
163
|
+
return buildSnapshotInputFromView(view, {
|
|
164
|
+
entityModule: "products",
|
|
165
|
+
entityId: productId,
|
|
166
|
+
sourceKind: "owned",
|
|
167
|
+
pricingBasis: context.pricingBasis,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
// Indexer document emission
|
|
172
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
173
|
+
/**
|
|
174
|
+
* Construct a sync `DocumentEmitter` for products. The emitter takes a
|
|
175
|
+
* pre-fetched product row + a slice and returns the indexer document
|
|
176
|
+
* (filtered by visibility, with blob-only fields skipped).
|
|
177
|
+
*
|
|
178
|
+
* Bulk-reindex pipelines that already have rows in hand call this directly.
|
|
179
|
+
* Live reindex paths use `createProductDocumentBuilder` below, which fetches
|
|
180
|
+
* the row before emitting.
|
|
181
|
+
*/
|
|
182
|
+
export function createProductDocumentEmitter(context) {
|
|
183
|
+
const registry = getProductsRegistry();
|
|
184
|
+
return {
|
|
185
|
+
vertical: "products",
|
|
186
|
+
emit(source, slice) {
|
|
187
|
+
const projection = productRowToProjection(source, {
|
|
188
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
189
|
+
});
|
|
190
|
+
return buildIndexerDocument(registry, projection, slice, source.id);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Async `DocumentBuilder` for products — fetches the row by id, then emits.
|
|
196
|
+
* Plug this into `IndexerService.reindexEntity` for live reindex events.
|
|
197
|
+
*
|
|
198
|
+
* Returns `null` if the product no longer exists (e.g. it was deleted
|
|
199
|
+
* between the reindex enqueue and the worker picking it up). Callers can
|
|
200
|
+
* treat `null` as a delete signal.
|
|
201
|
+
*/
|
|
202
|
+
export function createProductDocumentBuilder(db, context) {
|
|
203
|
+
const emitter = createProductDocumentEmitter(context);
|
|
204
|
+
return async (entityId, slice) => {
|
|
205
|
+
const rows = await db.select().from(products).where(eq(products.id, entityId)).limit(1);
|
|
206
|
+
const row = rows[0];
|
|
207
|
+
if (!row)
|
|
208
|
+
return null;
|
|
209
|
+
return emitter.emit(row, slice);
|
|
210
|
+
};
|
|
211
|
+
}
|
package/dist/service.d.ts
CHANGED
|
@@ -138,18 +138,18 @@ export declare const productsService: {
|
|
|
138
138
|
endDate: string | null;
|
|
139
139
|
timezone: string | null;
|
|
140
140
|
description: string | null;
|
|
141
|
+
visibility: "public" | "private" | "hidden";
|
|
141
142
|
bookingMode: "date" | "other" | "date_time" | "open" | "stay" | "transfer" | "itinerary";
|
|
142
143
|
capacityMode: "free_sale" | "limited" | "on_request";
|
|
143
|
-
visibility: "public" | "private" | "hidden";
|
|
144
144
|
activated: boolean;
|
|
145
|
+
productTypeId: string | null;
|
|
146
|
+
facilityId: string | null;
|
|
147
|
+
pax: number | null;
|
|
145
148
|
reservationTimeoutMinutes: number | null;
|
|
146
|
-
sellCurrency: string;
|
|
147
149
|
sellAmountCents: number | null;
|
|
150
|
+
sellCurrency: string;
|
|
148
151
|
costAmountCents: number | null;
|
|
149
152
|
marginPercent: number | null;
|
|
150
|
-
facilityId: string | null;
|
|
151
|
-
pax: number | null;
|
|
152
|
-
productTypeId: string | null;
|
|
153
153
|
tags: string[] | null;
|
|
154
154
|
}>;
|
|
155
155
|
updateProduct(db: PostgresJsDatabase, id: string, data: UpdateProductInput): Promise<{
|
|
@@ -2356,8 +2356,8 @@ export declare const productsService: {
|
|
|
2356
2356
|
createdAt: Date;
|
|
2357
2357
|
notes: string | null;
|
|
2358
2358
|
productId: string;
|
|
2359
|
-
versionNumber: number;
|
|
2360
2359
|
snapshot: unknown;
|
|
2360
|
+
versionNumber: number;
|
|
2361
2361
|
authorId: string;
|
|
2362
2362
|
} | null | undefined>;
|
|
2363
2363
|
listNotes(db: PostgresJsDatabase, productId: string): Omit<import("drizzle-orm/pg-core").PgSelectBase<"product_notes", {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageProvider } from "@voyantjs/
|
|
1
|
+
import type { StorageProvider } from "@voyantjs/storage";
|
|
2
2
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
3
|
import { type ProductBrochurePrinter } from "./brochure-printers.js";
|
|
4
4
|
import { type ProductBrochureTemplateDefinition } from "./brochure-templates.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brochures.d.ts","sourceRoot":"","sources":["../../src/tasks/brochures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"brochures.d.ts","sourceRoot":"","sources":["../../src/tasks/brochures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAGL,KAAK,iCAAiC,EAEvC,MAAM,yBAAyB,CAAA;AAGhC,MAAM,WAAW,sCAAsC;IACrD,OAAO,EAAE,eAAe,CAAA;IACxB,QAAQ,CAAC,EAAE,iCAAiC,CAAA;IAC5C,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC,CAAA;IACpF,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,wBAAsB,+BAA+B,CACnD,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,sCAAsC;;;;;;;;;;;;;;;;;;;;;;;;;GA2EhD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/products",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -50,11 +50,12 @@
|
|
|
50
50
|
"hono": "^4.12.10",
|
|
51
51
|
"pdf-lib": "^1.17.1",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
-
"@voyantjs/core": "0.
|
|
54
|
-
"@voyantjs/db": "0.
|
|
55
|
-
"@voyantjs/hono": "0.
|
|
56
|
-
"@voyantjs/utils": "0.
|
|
57
|
-
"@voyantjs/
|
|
53
|
+
"@voyantjs/core": "0.20.0",
|
|
54
|
+
"@voyantjs/db": "0.20.0",
|
|
55
|
+
"@voyantjs/hono": "0.20.0",
|
|
56
|
+
"@voyantjs/utils": "0.20.0",
|
|
57
|
+
"@voyantjs/catalog": "0.20.0",
|
|
58
|
+
"@voyantjs/storage": "0.20.0"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"typescript": "^6.0.2",
|