@voyantjs/extras 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,CA6B9B;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,157 @@
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
+ ["supplierId", row.supplierId],
47
+ ["createdAt", row.createdAt],
48
+ ["updatedAt", row.updatedAt],
49
+ // Snapshot-relevant managed fields
50
+ ["name", row.name],
51
+ ["description", row.description],
52
+ // Selection / pricing structure
53
+ ["selectionType", row.selectionType],
54
+ ["pricingMode", row.pricingMode],
55
+ ["pricedPerPerson", row.pricedPerPerson],
56
+ ["minQuantity", row.minQuantity],
57
+ ["maxQuantity", row.maxQuantity],
58
+ ["defaultQuantity", row.defaultQuantity],
59
+ ["active", row.active],
60
+ ["sortOrder", row.sortOrder],
61
+ ]);
62
+ }
63
+ export function productExtraProvenance(_row, context) {
64
+ return {
65
+ source_kind: context.sourceKind ?? "owned",
66
+ source_freshness: context.sourceKind && context.sourceKind !== "owned" ? "sync" : "static",
67
+ source_ref: context.sourceRef,
68
+ };
69
+ }
70
+ /**
71
+ * Catalog-aware extra fetch. The catalog-policy declares no merchandisable
72
+ * fields for extras (per §3.3.1 partial adoption), so the resolver acts as
73
+ * a pure visibility filter rather than an overlay-merge engine. Useful
74
+ * primarily for snapshot capture at booking time.
75
+ */
76
+ export async function getResolvedExtraById(db, id, context) {
77
+ const rows = await db.select().from(productExtras).where(eq(productExtras.id, id)).limit(1);
78
+ const row = rows[0];
79
+ if (!row)
80
+ return null;
81
+ const projection = productExtraRowToProjection(row, {
82
+ sellerOperatorId: context.sellerOperatorId,
83
+ sourceKind: context.sourceKind,
84
+ sourceRef: context.sourceRef,
85
+ });
86
+ return resolveEntityView(db, getExtrasRegistry(), "extras", id, projection, context.scope);
87
+ }
88
+ export async function listResolvedExtras(db, rows, context) {
89
+ const registry = getExtrasRegistry();
90
+ const views = [];
91
+ for (const row of rows) {
92
+ const projection = productExtraRowToProjection(row, {
93
+ sellerOperatorId: context.sellerOperatorId,
94
+ sourceKind: context.sourceKind,
95
+ sourceRef: context.sourceRef,
96
+ });
97
+ const view = await resolveEntityView(db, registry, "extras", row.id, projection, context.scope);
98
+ views.push(view);
99
+ }
100
+ return views;
101
+ }
102
+ /**
103
+ * Build a `CaptureSnapshotInput` for a product extra. Extras participate
104
+ * in the snapshot graph (per §3.3.1 partial adoption) so refunds can know
105
+ * exactly what add-on the customer purchased and what selectionType /
106
+ * pricingMode applied at booking time.
107
+ */
108
+ export async function buildExtraSnapshotInput(db, extraId, context) {
109
+ const view = await getResolvedExtraById(db, extraId, context);
110
+ if (!view)
111
+ return null;
112
+ return buildSnapshotInputFromView(view, {
113
+ entityModule: "extras",
114
+ entityId: extraId,
115
+ sourceKind: context.sourceKind ?? "owned",
116
+ sourceRef: context.sourceRef,
117
+ pricingBasis: context.pricingBasis,
118
+ });
119
+ }
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+ // Indexer document emission
122
+ // ─────────────────────────────────────────────────────────────────────────────
123
+ /**
124
+ * Note: per architecture §3.3.1, extras opt out of search-index
125
+ * participation — the catalog-policy declares every field with
126
+ * `reindex: "none"`. The emitter is provided for completeness (a deployment
127
+ * may opt extras into the index for ops-side keyword search) but production
128
+ * deployments should not register an extras emitter with the IndexerService.
129
+ */
130
+ export function createExtraDocumentEmitter(context) {
131
+ const registry = getExtrasRegistry();
132
+ return {
133
+ vertical: "extras",
134
+ emit(source, slice) {
135
+ const projection = productExtraRowToProjection(source, {
136
+ sellerOperatorId: context.sellerOperatorId,
137
+ sourceKind: context.sourceKind,
138
+ sourceRef: context.sourceRef,
139
+ });
140
+ return buildIndexerDocument(registry, projection, slice, source.id);
141
+ },
142
+ };
143
+ }
144
+ export function createExtraDocumentBuilder(db, context) {
145
+ const emitter = createExtraDocumentEmitter(context);
146
+ return async (entityId, slice) => {
147
+ const rows = await db
148
+ .select()
149
+ .from(productExtras)
150
+ .where(eq(productExtras.id, entityId))
151
+ .limit(1);
152
+ const row = rows[0];
153
+ if (!row)
154
+ return null;
155
+ return emitter.emit(row, slice);
156
+ };
157
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Extras content synthesizer — fallback for thin adapters that declare
3
+ * `supportsContentFetch: false`.
4
+ *
5
+ * Produces the most complete `ExtraContent` blob we can legitimately
6
+ * synthesize from the durable sourced-entry projection + locale-aware
7
+ * overlays + plane-level provenance. Sub-options render as a typed
8
+ * empty state unless the projection genuinely carries them.
9
+ *
10
+ * Per §3.6: never invents plausible-but-unverified fields, never
11
+ * machine-translates, never mines snapshots, never caches its own
12
+ * output.
13
+ */
14
+ import { type ProvenanceReadResult } from "@voyantjs/catalog";
15
+ import type { AnyDrizzleDb } from "@voyantjs/db";
16
+ import { type ExtraContent } from "./content-shape.js";
17
+ export interface SynthesizeExtraContentOptions {
18
+ provenance: Extract<ProvenanceReadResult, {
19
+ kind: "sourced";
20
+ }>;
21
+ overlays?: ReadonlyArray<{
22
+ field_path: string;
23
+ value: unknown;
24
+ }>;
25
+ }
26
+ export interface SynthesizedExtraContent {
27
+ content: ExtraContent;
28
+ content_schema_version: string;
29
+ served_locale: string;
30
+ source_kind: string;
31
+ source_provider?: string;
32
+ }
33
+ export declare function synthesizeExtraContent(scope: {
34
+ locale: string;
35
+ }, options: SynthesizeExtraContentOptions): SynthesizedExtraContent;
36
+ export declare function synthesizeExtraContentFromDb(db: AnyDrizzleDb, scope: {
37
+ locale: string;
38
+ }, provenance: Extract<ProvenanceReadResult, {
39
+ kind: "sourced";
40
+ }>): Promise<SynthesizedExtraContent>;
41
+ //# sourceMappingURL=service-content-synthesizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-content-synthesizer.d.ts","sourceRoot":"","sources":["../src/service-content-synthesizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IAC9D,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CACjE;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,YAAY,CAAA;IACrB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,OAAO,EAAE,6BAA6B,GACrC,uBAAuB,CAiCzB;AAED,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAC7D,OAAO,CAAC,uBAAuB,CAAC,CAOlC"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Extras content synthesizer — fallback for thin adapters that declare
3
+ * `supportsContentFetch: false`.
4
+ *
5
+ * Produces the most complete `ExtraContent` blob we can legitimately
6
+ * synthesize from the durable sourced-entry projection + locale-aware
7
+ * overlays + plane-level provenance. Sub-options render as a typed
8
+ * empty state unless the projection genuinely carries them.
9
+ *
10
+ * Per §3.6: never invents plausible-but-unverified fields, never
11
+ * machine-translates, never mines snapshots, never caches its own
12
+ * output.
13
+ */
14
+ import { fetchOverlaysForEntity, mergeOverlaysIntoContent, } from "@voyantjs/catalog";
15
+ import { EXTRAS_CONTENT_SCHEMA_VERSION, extraContentSchema, } from "./content-shape.js";
16
+ export function synthesizeExtraContent(scope, options) {
17
+ const projection = options.provenance.projection;
18
+ const extra = pickExtraSummary(projection, options.provenance);
19
+ const media = pickMedia(projection);
20
+ const policies = pickPolicies(projection);
21
+ const baseContent = {
22
+ extra,
23
+ options: [],
24
+ media,
25
+ policies,
26
+ };
27
+ let merged = baseContent;
28
+ if (options.overlays && options.overlays.length > 0) {
29
+ const result = mergeOverlaysIntoContent(baseContent, options.overlays, {
30
+ validate(p) {
31
+ const r = extraContentSchema.safeParse(p);
32
+ return r.success
33
+ ? { valid: true }
34
+ : { valid: false, reason: r.error.issues[0]?.message ?? "invalid" };
35
+ },
36
+ });
37
+ merged = extraContentSchema.parse(result);
38
+ }
39
+ return {
40
+ content: merged,
41
+ content_schema_version: EXTRAS_CONTENT_SCHEMA_VERSION,
42
+ served_locale: scope.locale,
43
+ source_kind: options.provenance.provenance.source_kind,
44
+ source_provider: options.provenance.provenance.source_provider,
45
+ };
46
+ }
47
+ export async function synthesizeExtraContentFromDb(db, scope, provenance) {
48
+ const entityId = entityIdFromProvenance(provenance);
49
+ const overlays = await fetchOverlaysForEntity(db, "extras", entityId);
50
+ return synthesizeExtraContent(scope, {
51
+ provenance,
52
+ overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
53
+ });
54
+ }
55
+ function entityIdFromProvenance(provenance) {
56
+ const fromProjection = provenance.projection.id;
57
+ if (typeof fromProjection === "string" && fromProjection.length > 0) {
58
+ return fromProjection;
59
+ }
60
+ return provenance.entry_id;
61
+ }
62
+ function pickExtraSummary(projection, provenance) {
63
+ return {
64
+ id: stringOr(projection.id, "") || provenance.entry_id,
65
+ name: stringOr(projection.name, "") || stringOr(projection.title, "") || "Unnamed extra",
66
+ status: stringOr(projection.status, undefined),
67
+ description: stringOr(projection.description, null),
68
+ selection_type: stringOr(projection.selection_type, "optional"),
69
+ pricing_mode: stringOr(projection.pricing_mode, "per_booking"),
70
+ priced_per_person: typeof projection.priced_per_person === "boolean" ? projection.priced_per_person : undefined,
71
+ category: stringOr(projection.category, null),
72
+ hero_image_url: stringOr(projection.hero_image_url, null),
73
+ highlights: stringArrayOr(projection.highlights, []),
74
+ supplier: stringOr(projection.supplier, null) ??
75
+ stringOr(projection.supplier_name, null) ??
76
+ provenance.provenance.source_provider ??
77
+ null,
78
+ duration_minutes: numberOr(projection.duration_minutes, null),
79
+ requirements_summary: stringOr(projection.requirements_summary, null),
80
+ };
81
+ }
82
+ function pickMedia(projection) {
83
+ const heroUrl = stringOr(projection.hero_image_url, null);
84
+ const out = [];
85
+ if (heroUrl) {
86
+ out.push({ url: heroUrl, type: "image", caption: null, alt: null });
87
+ }
88
+ const additional = projection.media;
89
+ if (Array.isArray(additional)) {
90
+ for (const item of additional) {
91
+ if (item && typeof item === "object") {
92
+ const obj = item;
93
+ const url = stringOr(obj.url, null);
94
+ if (!url)
95
+ continue;
96
+ out.push({
97
+ url,
98
+ type: pickMediaType(obj.type),
99
+ caption: stringOr(obj.caption, null),
100
+ alt: stringOr(obj.alt, null),
101
+ });
102
+ }
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ function pickMediaType(value) {
108
+ if (value === "video")
109
+ return "video";
110
+ if (value === "document")
111
+ return "document";
112
+ return "image";
113
+ }
114
+ function pickPolicies(projection) {
115
+ const out = [];
116
+ const cancel = stringOr(projection.cancellation_policy, null);
117
+ if (cancel)
118
+ out.push({ kind: "cancellation", body: cancel });
119
+ const payment = stringOr(projection.payment_terms, null);
120
+ if (payment)
121
+ out.push({ kind: "payment", body: payment });
122
+ const requirements = stringOr(projection.requirements, null);
123
+ if (requirements)
124
+ out.push({ kind: "requirements", body: requirements });
125
+ return out;
126
+ }
127
+ function stringOr(value, fallback) {
128
+ return typeof value === "string" && value.length > 0 ? value : fallback;
129
+ }
130
+ function numberOr(value, fallback) {
131
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
132
+ }
133
+ function stringArrayOr(value, fallback) {
134
+ if (!Array.isArray(value))
135
+ return fallback;
136
+ const out = value.filter((v) => typeof v === "string");
137
+ return out.length > 0 ? out : fallback;
138
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Extras content service — `getExtraContent` with locale-resolved
3
+ * cache reads, SWR refresh, and synthesizer fallback.
4
+ *
5
+ * Mirrors `service-content.ts` in the products / cruises / hospitality
6
+ * / charters packages but extras-shaped. The extras content aggregate
7
+ * (§3.2 / §3.6) is `{ extra, options[], media[], policies[] }` — one
8
+ * payload returned by a single getContent. Pricing stays out (volatile-
9
+ * live, flows through `liveResolve`).
10
+ *
11
+ * Extras don't appear in the search index (per the vertical's catalog-
12
+ * policy.ts), but sourced extras still need rich content for the
13
+ * booking-flow's add-on selection UI. The cache layer covers exactly
14
+ * that surface.
15
+ */
16
+ import { type ContentLocaleResolution, type InvalidateOnDrift, type SourceAdapter, type SourceAdapterContext } from "@voyantjs/catalog";
17
+ import type { SourceAdapterRegistry } from "@voyantjs/catalog/booking-engine";
18
+ import type { AnyDrizzleDb } from "@voyantjs/db";
19
+ import { type ExtraContent } from "./content-shape.js";
20
+ export interface ExtraContentScope {
21
+ preferredLocales: ReadonlyArray<string>;
22
+ market?: string;
23
+ currency?: string;
24
+ acceptMachineTranslated?: boolean;
25
+ }
26
+ export interface GetExtraContentOptions {
27
+ registry: SourceAdapterRegistry;
28
+ buildAdapterContext?: (adapter: SourceAdapter) => SourceAdapterContext;
29
+ onOverlayError?: (event: {
30
+ field_path: string;
31
+ reason: string;
32
+ }) => void;
33
+ }
34
+ export interface ResolvedExtraContent {
35
+ content: ExtraContent;
36
+ resolution: ContentLocaleResolution<{
37
+ locale: string;
38
+ payload: ExtraContent;
39
+ }>;
40
+ source: "sourced-cache" | "sourced-fresh" | "synthesized";
41
+ served_stale: boolean;
42
+ synthesized: boolean;
43
+ machine_translated: boolean;
44
+ }
45
+ export declare function getExtraContent(db: AnyDrizzleDb, entityId: string, scope: ExtraContentScope, options: GetExtraContentOptions): Promise<ResolvedExtraContent | null>;
46
+ /** Drift event consumer for the extras content cache. Per §3.4.1. */
47
+ export declare const invalidateExtraContentOnDrift: InvalidateOnDrift;
48
+ //# sourceMappingURL=service-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-content.d.ts","sourceRoot":"","sources":["../src/service-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,KAAK,uBAAuB,EAK5B,KAAK,iBAAiB,EAKtB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAE1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGhD,OAAO,EAEL,KAAK,YAAY,EAIlB,MAAM,oBAAoB,CAAA;AAc3B,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,oBAAoB,CAAA;IACtE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzE;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,YAAY,CAAA;IACrB,UAAU,EAAE,uBAAuB,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC,CAAA;IAC9E,MAAM,EAAE,eAAe,GAAG,eAAe,GAAG,aAAa,CAAA;IACzD,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,iBAAiB,EACxB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA0FtC;AA2ND,qEAAqE;AACrE,eAAO,MAAM,6BAA6B,EAAE,iBAG3C,CAAA"}