@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.
@@ -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
  };
@@ -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;KAChB,CAAA;CACF,CAAA;AAMD,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAmiDtB,CAAA;AAEJ,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,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
- return c.json({
23
- data: await productsService.createProduct(c.get("db"), await parseJsonBody(c, insertProductSchema)),
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/voyant-storage";
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,0BAA0B,CAAA;AAC/D,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"}
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.19.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.19.0",
54
- "@voyantjs/db": "0.19.0",
55
- "@voyantjs/hono": "0.19.0",
56
- "@voyantjs/utils": "0.19.0",
57
- "@voyantjs/voyant-storage": "0.19.0"
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",