@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.
- package/dist/catalog-policy.d.ts +30 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +291 -0
- package/dist/routes.d.ts +19 -19
- package/dist/service-catalog-plane.d.ts +79 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +156 -0
- package/dist/service.d.ts +13 -13
- package/package.json +7 -6
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog plane field policy for `packages/extras`.
|
|
3
|
+
*
|
|
4
|
+
* Extras are booking add-ons (optional line items layered on a booked
|
|
5
|
+
* parent product) — **not independently sellable inventory**. Per
|
|
6
|
+
* architecture §3.3.1, extras are a partial-adoption vertical:
|
|
7
|
+
*
|
|
8
|
+
* - **Adopt:** provenance shape (§5.1), booking snapshot graph (§5.3),
|
|
9
|
+
* catalog event taxonomy for the cancellation/fulfillment lifecycle.
|
|
10
|
+
* - **Skip:** search index projection (§5.4), editorial overlay store
|
|
11
|
+
* (§5.2), embeddings / RAG (Phase 2). Extras are discovered through
|
|
12
|
+
* the parent's surface, not via standalone catalog browse.
|
|
13
|
+
*
|
|
14
|
+
* Every field below has `reindex: "none"` because extras don't appear in
|
|
15
|
+
* the search index. Snapshot mode is `"on-book"` (or `"never"` for
|
|
16
|
+
* volatile fields) because refunds and audits need to know exactly what
|
|
17
|
+
* extra a customer added.
|
|
18
|
+
*
|
|
19
|
+
* Scope of this file:
|
|
20
|
+
* - The `product_extras` table (extra catalog definitions).
|
|
21
|
+
*
|
|
22
|
+
* Out of scope:
|
|
23
|
+
* - `option_extra_configs` — option-level overrides; promoted child.
|
|
24
|
+
* - `booking_extras` — runtime line items on a booking; not catalog data.
|
|
25
|
+
*/
|
|
26
|
+
import { type FieldPolicyInput } from "@voyantjs/catalog/contract";
|
|
27
|
+
declare const EXTRAS_FIELD_POLICY: FieldPolicyInput[];
|
|
28
|
+
export declare const extrasCatalogPolicy: import("@voyantjs/catalog").FieldPolicy[];
|
|
29
|
+
export { EXTRAS_FIELD_POLICY };
|
|
30
|
+
//# sourceMappingURL=catalog-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF,QAAA,MAAM,mBAAmB,EAAE,gBAAgB,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
33
|
-
"@voyantjs/db": "0.
|
|
34
|
-
"@voyantjs/hono": "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.
|
|
39
|
-
"@voyantjs/products": "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": [
|