@voyantjs/extras 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,30 @@
1
+ /**
2
+ * Catalog plane field policy for `packages/extras`.
3
+ *
4
+ * Extras are booking add-ons (optional line items layered on a booked
5
+ * parent product) — **not independently sellable inventory**. Per
6
+ * architecture §3.3.1, extras are a partial-adoption vertical:
7
+ *
8
+ * - **Adopt:** provenance shape (§5.1), booking snapshot graph (§5.3),
9
+ * catalog event taxonomy for the cancellation/fulfillment lifecycle.
10
+ * - **Skip:** search index projection (§5.4), editorial overlay store
11
+ * (§5.2), embeddings / RAG (Phase 2). Extras are discovered through
12
+ * the parent's surface, not via standalone catalog browse.
13
+ *
14
+ * Every field below has `reindex: "none"` because extras don't appear in
15
+ * the search index. Snapshot mode is `"on-book"` (or `"never"` for
16
+ * volatile fields) because refunds and audits need to know exactly what
17
+ * extra a customer added.
18
+ *
19
+ * Scope of this file:
20
+ * - The `product_extras` table (extra catalog definitions).
21
+ *
22
+ * Out of scope:
23
+ * - `option_extra_configs` — option-level overrides; promoted child.
24
+ * - `booking_extras` — runtime line items on a booking; not catalog data.
25
+ */
26
+ import { type FieldPolicyInput } from "@voyantjs/catalog/contract";
27
+ declare const EXTRAS_FIELD_POLICY: FieldPolicyInput[];
28
+ export declare const extrasCatalogPolicy: import("@voyantjs/catalog").FieldPolicy[];
29
+ export { EXTRAS_FIELD_POLICY };
30
+ //# sourceMappingURL=catalog-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF,QAAA,MAAM,mBAAmB,EAAE,gBAAgB,EA0Q1C,CAAA;AAED,eAAO,MAAM,mBAAmB,2CAAyC,CAAA;AAEzE,OAAO,EAAE,mBAAmB,EAAE,CAAA"}
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Catalog plane field policy for `packages/extras`.
3
+ *
4
+ * Extras are booking add-ons (optional line items layered on a booked
5
+ * parent product) — **not independently sellable inventory**. Per
6
+ * architecture §3.3.1, extras are a partial-adoption vertical:
7
+ *
8
+ * - **Adopt:** provenance shape (§5.1), booking snapshot graph (§5.3),
9
+ * catalog event taxonomy for the cancellation/fulfillment lifecycle.
10
+ * - **Skip:** search index projection (§5.4), editorial overlay store
11
+ * (§5.2), embeddings / RAG (Phase 2). Extras are discovered through
12
+ * the parent's surface, not via standalone catalog browse.
13
+ *
14
+ * Every field below has `reindex: "none"` because extras don't appear in
15
+ * the search index. Snapshot mode is `"on-book"` (or `"never"` for
16
+ * volatile fields) because refunds and audits need to know exactly what
17
+ * extra a customer added.
18
+ *
19
+ * Scope of this file:
20
+ * - The `product_extras` table (extra catalog definitions).
21
+ *
22
+ * Out of scope:
23
+ * - `option_extra_configs` — option-level overrides; promoted child.
24
+ * - `booking_extras` — runtime line items on a booking; not catalog data.
25
+ */
26
+ import { defineFieldPolicy } from "@voyantjs/catalog/contract";
27
+ const EXTRAS_FIELD_POLICY = [
28
+ // ── Source pointer / provenance ─────────────────────────────────────────
29
+ {
30
+ path: "source.kind",
31
+ class: "managed",
32
+ merge: "source-only",
33
+ drift: "critical",
34
+ reindex: "none",
35
+ snapshot: "on-book",
36
+ query: "indexed-column",
37
+ localized: false,
38
+ visibility: ["staff"],
39
+ editRole: "none",
40
+ overrideFriction: "none",
41
+ sourceFreshness: "sync",
42
+ },
43
+ {
44
+ path: "source.ref",
45
+ class: "managed",
46
+ merge: "source-only",
47
+ drift: "critical",
48
+ reindex: "none",
49
+ snapshot: "on-book",
50
+ query: "indexed-column",
51
+ localized: false,
52
+ visibility: ["staff"],
53
+ editRole: "none",
54
+ overrideFriction: "none",
55
+ sourceFreshness: "sync",
56
+ },
57
+ {
58
+ path: "seller.operator_id",
59
+ class: "managed",
60
+ merge: "source-only",
61
+ drift: "critical",
62
+ reindex: "none",
63
+ snapshot: "on-book",
64
+ query: "indexed-column",
65
+ localized: false,
66
+ visibility: ["staff"],
67
+ editRole: "none",
68
+ overrideFriction: "none",
69
+ sourceFreshness: "static",
70
+ },
71
+ // ── Identity / lifecycle ────────────────────────────────────────────────
72
+ {
73
+ path: "id",
74
+ class: "managed",
75
+ merge: "source-only",
76
+ drift: "critical",
77
+ reindex: "none",
78
+ snapshot: "on-book",
79
+ query: "first-class-table",
80
+ localized: false,
81
+ visibility: ["staff", "customer", "partner"],
82
+ editRole: "none",
83
+ overrideFriction: "none",
84
+ sourceFreshness: "static",
85
+ },
86
+ {
87
+ path: "code",
88
+ class: "managed",
89
+ merge: "source-only",
90
+ drift: "high",
91
+ reindex: "none",
92
+ snapshot: "on-book",
93
+ query: "indexed-column",
94
+ localized: false,
95
+ visibility: ["staff"],
96
+ editRole: "none",
97
+ overrideFriction: "none",
98
+ sourceFreshness: "sync",
99
+ },
100
+ {
101
+ path: "createdAt",
102
+ class: "managed",
103
+ merge: "source-only",
104
+ drift: "none",
105
+ reindex: "none",
106
+ snapshot: "on-book",
107
+ query: "indexed-column",
108
+ localized: false,
109
+ visibility: ["staff"],
110
+ editRole: "none",
111
+ overrideFriction: "none",
112
+ sourceFreshness: "static",
113
+ },
114
+ {
115
+ path: "updatedAt",
116
+ class: "managed",
117
+ merge: "source-only",
118
+ drift: "none",
119
+ reindex: "none",
120
+ snapshot: "never",
121
+ query: "indexed-column",
122
+ localized: false,
123
+ visibility: ["staff"],
124
+ editRole: "none",
125
+ overrideFriction: "none",
126
+ sourceFreshness: "sync",
127
+ },
128
+ // ── Cross-module reference (the parent product the extra attaches to) ──
129
+ {
130
+ path: "productId",
131
+ class: "managed",
132
+ merge: "source-only",
133
+ drift: "critical",
134
+ reindex: "none",
135
+ snapshot: "on-book",
136
+ query: "indexed-column",
137
+ localized: false,
138
+ visibility: ["staff"],
139
+ editRole: "none",
140
+ overrideFriction: "none",
141
+ sourceFreshness: "sync",
142
+ },
143
+ // ── Snapshot-relevant managed fields ────────────────────────────────────
144
+ // Extras' name/description aren't merchandised standalone — they're
145
+ // shown inside the parent product's add-on UI. Marked as managed for
146
+ // simplicity; if a future case requires marketing overrides on extras'
147
+ // names, this can be promoted to merchandisable.
148
+ {
149
+ path: "name",
150
+ class: "managed",
151
+ merge: "source-only",
152
+ drift: "medium",
153
+ reindex: "none",
154
+ snapshot: "on-book",
155
+ query: "indexed-column",
156
+ localized: false,
157
+ visibility: ["staff", "customer", "partner"],
158
+ editRole: "none",
159
+ overrideFriction: "none",
160
+ sourceFreshness: "sync",
161
+ },
162
+ {
163
+ path: "description",
164
+ class: "managed",
165
+ merge: "source-only",
166
+ drift: "low",
167
+ reindex: "none",
168
+ snapshot: "on-book",
169
+ query: "blob-only",
170
+ localized: false,
171
+ visibility: ["staff", "customer", "partner"],
172
+ editRole: "none",
173
+ overrideFriction: "none",
174
+ sourceFreshness: "sync",
175
+ },
176
+ // ── Selection / pricing structure (snapshotted at book time) ───────────
177
+ {
178
+ path: "selectionType",
179
+ class: "structural",
180
+ merge: "source-only",
181
+ drift: "high",
182
+ reindex: "none",
183
+ snapshot: "on-book",
184
+ query: "indexed-column",
185
+ localized: false,
186
+ visibility: ["staff", "customer", "partner"],
187
+ editRole: "none",
188
+ overrideFriction: "none",
189
+ sourceFreshness: "sync",
190
+ },
191
+ {
192
+ path: "pricingMode",
193
+ class: "structural",
194
+ merge: "source-only",
195
+ drift: "high",
196
+ reindex: "none",
197
+ snapshot: "on-quote-and-book",
198
+ query: "indexed-column",
199
+ localized: false,
200
+ visibility: ["staff", "customer", "partner"],
201
+ editRole: "none",
202
+ overrideFriction: "none",
203
+ sourceFreshness: "sync",
204
+ },
205
+ {
206
+ path: "pricedPerPerson",
207
+ class: "structural",
208
+ merge: "source-only",
209
+ drift: "high",
210
+ reindex: "none",
211
+ snapshot: "on-quote-and-book",
212
+ query: "indexed-column",
213
+ localized: false,
214
+ visibility: ["staff", "customer", "partner"],
215
+ editRole: "none",
216
+ overrideFriction: "none",
217
+ sourceFreshness: "sync",
218
+ },
219
+ {
220
+ path: "minQuantity",
221
+ class: "structural",
222
+ merge: "source-only",
223
+ drift: "medium",
224
+ reindex: "none",
225
+ snapshot: "on-book",
226
+ query: "indexed-column",
227
+ localized: false,
228
+ visibility: ["staff", "customer", "partner"],
229
+ editRole: "none",
230
+ overrideFriction: "none",
231
+ sourceFreshness: "sync",
232
+ },
233
+ {
234
+ path: "maxQuantity",
235
+ class: "structural",
236
+ merge: "source-only",
237
+ drift: "medium",
238
+ reindex: "none",
239
+ snapshot: "on-book",
240
+ query: "indexed-column",
241
+ localized: false,
242
+ visibility: ["staff", "customer", "partner"],
243
+ editRole: "none",
244
+ overrideFriction: "none",
245
+ sourceFreshness: "sync",
246
+ },
247
+ {
248
+ path: "defaultQuantity",
249
+ class: "structural",
250
+ merge: "source-only",
251
+ drift: "low",
252
+ reindex: "none",
253
+ snapshot: "on-book",
254
+ query: "indexed-column",
255
+ localized: false,
256
+ visibility: ["staff", "customer", "partner"],
257
+ editRole: "none",
258
+ overrideFriction: "none",
259
+ sourceFreshness: "sync",
260
+ },
261
+ {
262
+ path: "active",
263
+ class: "structural",
264
+ merge: "source-only",
265
+ drift: "high",
266
+ reindex: "none",
267
+ snapshot: "on-book",
268
+ query: "indexed-column",
269
+ localized: false,
270
+ visibility: ["staff"],
271
+ editRole: "none",
272
+ overrideFriction: "none",
273
+ sourceFreshness: "sync",
274
+ },
275
+ {
276
+ path: "sortOrder",
277
+ class: "structural",
278
+ merge: "source-only",
279
+ drift: "none",
280
+ reindex: "none",
281
+ snapshot: "never",
282
+ query: "indexed-column",
283
+ localized: false,
284
+ visibility: ["staff"],
285
+ editRole: "ops",
286
+ overrideFriction: "none",
287
+ sourceFreshness: "sync",
288
+ },
289
+ ];
290
+ export const extrasCatalogPolicy = defineFieldPolicy(EXTRAS_FIELD_POLICY);
291
+ export { EXTRAS_FIELD_POLICY };
package/dist/routes.d.ts CHANGED
@@ -44,24 +44,24 @@ export declare const extrasRoutes: import("hono/hono-base").HonoBase<Env, {
44
44
  input: {};
45
45
  output: {
46
46
  data: {
47
- metadata: {
48
- [x: string]: import("hono/utils/types").JSONValue;
49
- } | null;
50
47
  id: string;
51
- name: string;
48
+ code: string | null;
52
49
  createdAt: string;
53
50
  updatedAt: string;
54
- description: string | null;
55
- code: string | null;
56
- active: boolean;
57
51
  productId: string;
52
+ name: string;
53
+ description: string | null;
58
54
  selectionType: "optional" | "required" | "default_selected" | "unavailable";
59
55
  pricingMode: "included" | "per_person" | "per_booking" | "quantity_based" | "on_request" | "free";
60
56
  pricedPerPerson: boolean;
61
57
  minQuantity: number | null;
62
58
  maxQuantity: number | null;
63
59
  defaultQuantity: number | null;
60
+ active: boolean;
64
61
  sortOrder: number;
62
+ metadata: {
63
+ [x: string]: import("hono/utils/types").JSONValue;
64
+ } | null;
65
65
  } | null;
66
66
  };
67
67
  outputFormat: "json";
@@ -223,21 +223,21 @@ export declare const extrasRoutes: import("hono/hono-base").HonoBase<Env, {
223
223
  input: {};
224
224
  output: {
225
225
  data: {
226
- metadata: {
227
- [x: string]: import("hono/utils/types").JSONValue;
228
- } | null;
229
226
  id: string;
230
227
  createdAt: string;
231
228
  updatedAt: string;
232
- notes: string | null;
233
- active: boolean;
234
229
  selectionType: "optional" | "required" | "default_selected" | "unavailable" | null;
235
230
  pricingMode: "included" | "per_person" | "per_booking" | "quantity_based" | "on_request" | "free" | null;
236
231
  pricedPerPerson: boolean | null;
237
232
  minQuantity: number | null;
238
233
  maxQuantity: number | null;
239
234
  defaultQuantity: number | null;
235
+ active: boolean;
240
236
  sortOrder: number;
237
+ metadata: {
238
+ [x: string]: import("hono/utils/types").JSONValue;
239
+ } | null;
240
+ notes: string | null;
241
241
  optionId: string;
242
242
  productExtraId: string;
243
243
  isDefault: boolean;
@@ -406,19 +406,19 @@ export declare const extrasRoutes: import("hono/hono-base").HonoBase<Env, {
406
406
  input: {};
407
407
  output: {
408
408
  data: {
409
- metadata: {
410
- [x: string]: import("hono/utils/types").JSONValue;
411
- } | null;
412
409
  id: string;
413
- name: string;
414
410
  createdAt: string;
415
411
  updatedAt: string;
416
- status: "draft" | "selected" | "confirmed" | "cancelled" | "fulfilled";
417
- notes: string | null;
412
+ name: string;
418
413
  description: string | null;
419
- bookingId: string;
420
414
  pricingMode: "included" | "per_person" | "per_booking" | "quantity_based" | "on_request" | "free";
421
415
  pricedPerPerson: boolean;
416
+ metadata: {
417
+ [x: string]: import("hono/utils/types").JSONValue;
418
+ } | null;
419
+ status: "draft" | "selected" | "confirmed" | "cancelled" | "fulfilled";
420
+ notes: string | null;
421
+ bookingId: string;
422
422
  productExtraId: string | null;
423
423
  optionExtraConfigId: string | null;
424
424
  quantity: number;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Catalog-plane integration for the extras service.
3
+ *
4
+ * Extras are a **partial-adoption vertical** per architecture §3.3.1: they
5
+ * participate in provenance + booking snapshot + catalog event taxonomy,
6
+ * but skip the search index and overlay store. The service-plane integration
7
+ * here reflects that — `getResolvedExtraById` returns a resolved view but
8
+ * the resolver always sees an empty overlay set (extras have no overlay
9
+ * rows; the catalog-policy file declares no merchandisable fields).
10
+ *
11
+ * The value of running through the catalog-plane resolver anyway is
12
+ * uniformity: extras' projection and visibility filtering use the same
13
+ * machinery as every other vertical, and snapshot capture at booking commit
14
+ * (the most important participation surface for extras) reuses
15
+ * `productExtraRowToProjection` to build the frozen payload.
16
+ *
17
+ * See `docs/architecture/catalog-architecture.md` §9.1 + §3.3.1.
18
+ */
19
+ import { type CaptureSnapshotInput, type DocumentBuilder, type DocumentEmitter, type IndexerDocument, type IndexerSlice, type PricingBasis, type Provenance, type ResolvedView, type ResolverScope } from "@voyantjs/catalog";
20
+ import type { AnyDrizzleDb } from "@voyantjs/db";
21
+ import { productExtras } from "./schema.js";
22
+ /**
23
+ * Maps a product-extra row to a field-keyed projection. Extras almost
24
+ * always inherit their provenance from the parent product they attach to;
25
+ * the caller passes the parent's source kind / ref through, defaulting to
26
+ * `owned` for operator-defined extras.
27
+ */
28
+ export declare function productExtraRowToProjection(row: typeof productExtras.$inferSelect, context: {
29
+ sellerOperatorId: string;
30
+ sourceKind?: string;
31
+ sourceRef?: string;
32
+ }): ReadonlyMap<string, unknown>;
33
+ export declare function productExtraProvenance(_row: typeof productExtras.$inferSelect, context: {
34
+ sellerOperatorId: string;
35
+ sourceKind?: string;
36
+ sourceRef?: string;
37
+ }): Provenance;
38
+ export interface ProductExtraCatalogContext {
39
+ sellerOperatorId: string;
40
+ scope: ResolverScope;
41
+ sourceKind?: string;
42
+ sourceRef?: string;
43
+ }
44
+ /**
45
+ * Catalog-aware extra fetch. The catalog-policy declares no merchandisable
46
+ * fields for extras (per §3.3.1 partial adoption), so the resolver acts as
47
+ * a pure visibility filter rather than an overlay-merge engine. Useful
48
+ * primarily for snapshot capture at booking time.
49
+ */
50
+ export declare function getResolvedExtraById(db: AnyDrizzleDb, id: string, context: ProductExtraCatalogContext): Promise<ResolvedView | null>;
51
+ export declare function listResolvedExtras(db: AnyDrizzleDb, rows: ReadonlyArray<typeof productExtras.$inferSelect>, context: ProductExtraCatalogContext): Promise<ResolvedView[]>;
52
+ /**
53
+ * Build a `CaptureSnapshotInput` for a product extra. Extras participate
54
+ * in the snapshot graph (per §3.3.1 partial adoption) so refunds can know
55
+ * exactly what add-on the customer purchased and what selectionType /
56
+ * pricingMode applied at booking time.
57
+ */
58
+ export declare function buildExtraSnapshotInput(db: AnyDrizzleDb, extraId: string, context: ProductExtraCatalogContext & {
59
+ pricingBasis?: PricingBasis;
60
+ }): Promise<Omit<CaptureSnapshotInput, "bookingId"> | null>;
61
+ /**
62
+ * Note: per architecture §3.3.1, extras opt out of search-index
63
+ * participation — the catalog-policy declares every field with
64
+ * `reindex: "none"`. The emitter is provided for completeness (a deployment
65
+ * may opt extras into the index for ops-side keyword search) but production
66
+ * deployments should not register an extras emitter with the IndexerService.
67
+ */
68
+ export declare function createExtraDocumentEmitter(context: {
69
+ sellerOperatorId: string;
70
+ sourceKind?: string;
71
+ sourceRef?: string;
72
+ }): DocumentEmitter<typeof productExtras.$inferSelect>;
73
+ export declare function createExtraDocumentBuilder(db: AnyDrizzleDb, context: {
74
+ sellerOperatorId: string;
75
+ sourceKind?: string;
76
+ sourceRef?: string;
77
+ }): DocumentBuilder;
78
+ export type { CaptureSnapshotInput, DocumentBuilder, DocumentEmitter, IndexerDocument, IndexerSlice, PricingBasis, Provenance, ResolvedView, ResolverScope, };
79
+ //# 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;;;;;;;;;;;;;;;;;GAiBG;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,EAEnB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAIhD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAU3C;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,OAAO,aAAa,CAAC,YAAY,EACtC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CA4B9B;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,OAAO,aAAa,CAAC,YAAY,EACvC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,UAAU,CAMZ;AAED,MAAM,WAAW,0BAA0B;IACzC,gBAAgB,EAAE,MAAM,CAAA;IACxB,KAAK,EAAE,aAAa,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAW9B;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,aAAa,CAAC,OAAO,aAAa,CAAC,YAAY,CAAC,EACtD,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,YAAY,EAAE,CAAC,CAazB;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,0BAA0B,GAAG;IAAE,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GACpE,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,CAUzD;AAMD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,eAAe,CAAC,OAAO,aAAa,CAAC,YAAY,CAAC,CAarD;AAED,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,eAAe,CAYjB;AAED,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,aAAa,GACd,CAAA"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Catalog-plane integration for the extras service.
3
+ *
4
+ * Extras are a **partial-adoption vertical** per architecture §3.3.1: they
5
+ * participate in provenance + booking snapshot + catalog event taxonomy,
6
+ * but skip the search index and overlay store. The service-plane integration
7
+ * here reflects that — `getResolvedExtraById` returns a resolved view but
8
+ * the resolver always sees an empty overlay set (extras have no overlay
9
+ * rows; the catalog-policy file declares no merchandisable fields).
10
+ *
11
+ * The value of running through the catalog-plane resolver anyway is
12
+ * uniformity: extras' projection and visibility filtering use the same
13
+ * machinery as every other vertical, and snapshot capture at booking commit
14
+ * (the most important participation surface for extras) reuses
15
+ * `productExtraRowToProjection` to build the frozen payload.
16
+ *
17
+ * See `docs/architecture/catalog-architecture.md` §9.1 + §3.3.1.
18
+ */
19
+ import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, resolveEntityView, } from "@voyantjs/catalog";
20
+ import { eq } from "drizzle-orm";
21
+ import { extrasCatalogPolicy } from "./catalog-policy.js";
22
+ import { productExtras } from "./schema.js";
23
+ let _registry;
24
+ function getExtrasRegistry() {
25
+ if (!_registry) {
26
+ _registry = createFieldPolicyRegistry(extrasCatalogPolicy);
27
+ }
28
+ return _registry;
29
+ }
30
+ /**
31
+ * Maps a product-extra row to a field-keyed projection. Extras almost
32
+ * always inherit their provenance from the parent product they attach to;
33
+ * the caller passes the parent's source kind / ref through, defaulting to
34
+ * `owned` for operator-defined extras.
35
+ */
36
+ export function productExtraRowToProjection(row, context) {
37
+ return new Map([
38
+ // Provenance
39
+ ["source.kind", context.sourceKind ?? "owned"],
40
+ ["source.ref", context.sourceRef],
41
+ ["seller.operator_id", context.sellerOperatorId],
42
+ // Identity + cross-module reference
43
+ ["id", row.id],
44
+ ["code", row.code],
45
+ ["productId", row.productId],
46
+ ["createdAt", row.createdAt],
47
+ ["updatedAt", row.updatedAt],
48
+ // Snapshot-relevant managed fields
49
+ ["name", row.name],
50
+ ["description", row.description],
51
+ // Selection / pricing structure
52
+ ["selectionType", row.selectionType],
53
+ ["pricingMode", row.pricingMode],
54
+ ["pricedPerPerson", row.pricedPerPerson],
55
+ ["minQuantity", row.minQuantity],
56
+ ["maxQuantity", row.maxQuantity],
57
+ ["defaultQuantity", row.defaultQuantity],
58
+ ["active", row.active],
59
+ ["sortOrder", row.sortOrder],
60
+ ]);
61
+ }
62
+ export function productExtraProvenance(_row, context) {
63
+ return {
64
+ source_kind: context.sourceKind ?? "owned",
65
+ source_freshness: context.sourceKind && context.sourceKind !== "owned" ? "sync" : "static",
66
+ source_ref: context.sourceRef,
67
+ };
68
+ }
69
+ /**
70
+ * Catalog-aware extra fetch. The catalog-policy declares no merchandisable
71
+ * fields for extras (per §3.3.1 partial adoption), so the resolver acts as
72
+ * a pure visibility filter rather than an overlay-merge engine. Useful
73
+ * primarily for snapshot capture at booking time.
74
+ */
75
+ export async function getResolvedExtraById(db, id, context) {
76
+ const rows = await db.select().from(productExtras).where(eq(productExtras.id, id)).limit(1);
77
+ const row = rows[0];
78
+ if (!row)
79
+ return null;
80
+ const projection = productExtraRowToProjection(row, {
81
+ sellerOperatorId: context.sellerOperatorId,
82
+ sourceKind: context.sourceKind,
83
+ sourceRef: context.sourceRef,
84
+ });
85
+ return resolveEntityView(db, getExtrasRegistry(), "extras", id, projection, context.scope);
86
+ }
87
+ export async function listResolvedExtras(db, rows, context) {
88
+ const registry = getExtrasRegistry();
89
+ const views = [];
90
+ for (const row of rows) {
91
+ const projection = productExtraRowToProjection(row, {
92
+ sellerOperatorId: context.sellerOperatorId,
93
+ sourceKind: context.sourceKind,
94
+ sourceRef: context.sourceRef,
95
+ });
96
+ const view = await resolveEntityView(db, registry, "extras", row.id, projection, context.scope);
97
+ views.push(view);
98
+ }
99
+ return views;
100
+ }
101
+ /**
102
+ * Build a `CaptureSnapshotInput` for a product extra. Extras participate
103
+ * in the snapshot graph (per §3.3.1 partial adoption) so refunds can know
104
+ * exactly what add-on the customer purchased and what selectionType /
105
+ * pricingMode applied at booking time.
106
+ */
107
+ export async function buildExtraSnapshotInput(db, extraId, context) {
108
+ const view = await getResolvedExtraById(db, extraId, context);
109
+ if (!view)
110
+ return null;
111
+ return buildSnapshotInputFromView(view, {
112
+ entityModule: "extras",
113
+ entityId: extraId,
114
+ sourceKind: context.sourceKind ?? "owned",
115
+ sourceRef: context.sourceRef,
116
+ pricingBasis: context.pricingBasis,
117
+ });
118
+ }
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ // Indexer document emission
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+ /**
123
+ * Note: per architecture §3.3.1, extras opt out of search-index
124
+ * participation — the catalog-policy declares every field with
125
+ * `reindex: "none"`. The emitter is provided for completeness (a deployment
126
+ * may opt extras into the index for ops-side keyword search) but production
127
+ * deployments should not register an extras emitter with the IndexerService.
128
+ */
129
+ export function createExtraDocumentEmitter(context) {
130
+ const registry = getExtrasRegistry();
131
+ return {
132
+ vertical: "extras",
133
+ emit(source, slice) {
134
+ const projection = productExtraRowToProjection(source, {
135
+ sellerOperatorId: context.sellerOperatorId,
136
+ sourceKind: context.sourceKind,
137
+ sourceRef: context.sourceRef,
138
+ });
139
+ return buildIndexerDocument(registry, projection, slice, source.id);
140
+ },
141
+ };
142
+ }
143
+ export function createExtraDocumentBuilder(db, context) {
144
+ const emitter = createExtraDocumentEmitter(context);
145
+ return async (entityId, slice) => {
146
+ const rows = await db
147
+ .select()
148
+ .from(productExtras)
149
+ .where(eq(productExtras.id, entityId))
150
+ .limit(1);
151
+ const row = rows[0];
152
+ if (!row)
153
+ return null;
154
+ return emitter.emit(row, slice);
155
+ };
156
+ }
package/dist/service.d.ts CHANGED
@@ -53,22 +53,22 @@ export declare const extrasService: {
53
53
  updatedAt: Date;
54
54
  } | null>;
55
55
  createProductExtra(db: PostgresJsDatabase, data: CreateProductExtraInput): Promise<{
56
- metadata: Record<string, unknown> | null;
57
56
  id: string;
58
- name: string;
57
+ code: string | null;
59
58
  createdAt: Date;
60
59
  updatedAt: Date;
61
- description: string | null;
62
- code: string | null;
63
- active: boolean;
64
60
  productId: string;
61
+ name: string;
62
+ description: string | null;
65
63
  selectionType: "optional" | "required" | "default_selected" | "unavailable";
66
64
  pricingMode: "included" | "per_person" | "per_booking" | "quantity_based" | "on_request" | "free";
67
65
  pricedPerPerson: boolean;
68
66
  minQuantity: number | null;
69
67
  maxQuantity: number | null;
70
68
  defaultQuantity: number | null;
69
+ active: boolean;
71
70
  sortOrder: number;
71
+ metadata: Record<string, unknown> | null;
72
72
  } | null>;
73
73
  updateProductExtra(db: PostgresJsDatabase, id: string, data: UpdateProductExtraInput): Promise<{
74
74
  id: string;
@@ -133,19 +133,19 @@ export declare const extrasService: {
133
133
  updatedAt: Date;
134
134
  } | null>;
135
135
  createOptionExtraConfig(db: PostgresJsDatabase, data: CreateOptionExtraConfigInput): Promise<{
136
- metadata: Record<string, unknown> | null;
137
136
  id: string;
138
137
  createdAt: Date;
139
138
  updatedAt: Date;
140
- notes: string | null;
141
- active: boolean;
142
139
  selectionType: "optional" | "required" | "default_selected" | "unavailable" | null;
143
140
  pricingMode: "included" | "per_person" | "per_booking" | "quantity_based" | "on_request" | "free" | null;
144
141
  pricedPerPerson: boolean | null;
145
142
  minQuantity: number | null;
146
143
  maxQuantity: number | null;
147
144
  defaultQuantity: number | null;
145
+ active: boolean;
148
146
  sortOrder: number;
147
+ metadata: Record<string, unknown> | null;
148
+ notes: string | null;
149
149
  optionId: string;
150
150
  productExtraId: string;
151
151
  isDefault: boolean;
@@ -221,17 +221,17 @@ export declare const extrasService: {
221
221
  updatedAt: Date;
222
222
  } | null>;
223
223
  createBookingExtra(db: PostgresJsDatabase, data: CreateBookingExtraInput): Promise<{
224
- metadata: Record<string, unknown> | null;
225
224
  id: string;
226
- name: string;
227
225
  createdAt: Date;
228
226
  updatedAt: Date;
229
- status: "draft" | "selected" | "confirmed" | "cancelled" | "fulfilled";
230
- notes: string | null;
227
+ name: string;
231
228
  description: string | null;
232
- bookingId: string;
233
229
  pricingMode: "included" | "per_person" | "per_booking" | "quantity_based" | "on_request" | "free";
234
230
  pricedPerPerson: boolean;
231
+ metadata: Record<string, unknown> | null;
232
+ status: "draft" | "selected" | "confirmed" | "cancelled" | "fulfilled";
233
+ notes: string | null;
234
+ bookingId: string;
235
235
  productExtraId: string | null;
236
236
  optionExtraConfigId: string | null;
237
237
  quantity: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/extras",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,14 +29,15 @@
29
29
  "drizzle-orm": "^0.45.2",
30
30
  "hono": "^4.12.10",
31
31
  "zod": "^4.3.6",
32
- "@voyantjs/core": "0.19.0",
33
- "@voyantjs/db": "0.19.0",
34
- "@voyantjs/hono": "0.19.0"
32
+ "@voyantjs/core": "0.20.0",
33
+ "@voyantjs/db": "0.20.0",
34
+ "@voyantjs/hono": "0.20.0",
35
+ "@voyantjs/catalog": "0.20.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "typescript": "^6.0.2",
38
- "@voyantjs/bookings": "0.19.0",
39
- "@voyantjs/products": "0.19.0",
39
+ "@voyantjs/bookings": "0.20.0",
40
+ "@voyantjs/products": "0.20.0",
40
41
  "@voyantjs/voyant-typescript-config": "0.1.0"
41
42
  },
42
43
  "files": [