@voyantjs/products 0.20.0 → 0.21.1
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/booking-engine/handler.d.ts +203 -0
- package/dist/booking-engine/handler.d.ts.map +1 -0
- package/dist/booking-engine/handler.js +330 -0
- package/dist/booking-engine/index.d.ts +8 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +7 -0
- package/dist/catalog-policy.d.ts.map +1 -1
- package/dist/catalog-policy.js +15 -1
- package/dist/content-shape.d.ts +217 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +159 -0
- package/dist/draft-shape.d.ts +43 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +46 -0
- package/dist/events.d.ts +37 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/routes-content.d.ts +74 -0
- package/dist/routes-content.d.ts.map +1 -0
- package/dist/routes-content.js +117 -0
- package/dist/routes.d.ts +40 -20
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +83 -13
- package/dist/schema-core.d.ts +240 -1
- package/dist/schema-core.d.ts.map +1 -1
- package/dist/schema-core.js +49 -0
- package/dist/schema-itinerary.d.ts +18 -1
- package/dist/schema-itinerary.d.ts.map +1 -1
- package/dist/schema-itinerary.js +1 -0
- package/dist/schema-settings.d.ts +1 -1
- package/dist/schema-sourced-content.d.ts +262 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +69 -0
- package/dist/schema-taxonomy.d.ts +17 -0
- package/dist/schema-taxonomy.d.ts.map +1 -1
- package/dist/schema-taxonomy.js +13 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -0
- package/dist/service-catalog-plane.d.ts.map +1 -1
- package/dist/service-catalog-plane.js +1 -0
- package/dist/service-content-owned.d.ts +68 -0
- package/dist/service-content-owned.d.ts.map +1 -0
- package/dist/service-content-owned.js +224 -0
- package/dist/service-content-synthesizer.d.ts +90 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +171 -0
- package/dist/service-content.d.ts +106 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +365 -0
- package/dist/service.d.ts +76 -22
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +4 -0
- package/dist/tasks/brochures.d.ts +1 -0
- package/dist/tasks/brochures.d.ts.map +1 -1
- package/dist/tasks/brochures.js +3 -0
- package/dist/validation-catalog.d.ts +4 -4
- package/dist/validation-config.d.ts +3 -3
- package/dist/validation-content.d.ts +34 -4
- package/dist/validation-content.d.ts.map +1 -1
- package/dist/validation-content.js +13 -0
- package/dist/validation-core.d.ts +53 -3
- package/dist/validation-core.d.ts.map +1 -1
- package/dist/validation-core.js +16 -0
- package/dist/validation-public.d.ts +9 -9
- package/dist/validation-shared.d.ts +4 -4
- package/package.json +12 -7
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product content service — `getProductContent` with owned-vs-sourced
|
|
3
|
+
* dispatch, locale-resolved cache reads, SWR refresh, and synthesizer
|
|
4
|
+
* fallback.
|
|
5
|
+
*
|
|
6
|
+
* One entry point per vertical; the catalog plane stays neutral about
|
|
7
|
+
* per-vertical content shapes. Detail routes (operator and storefront)
|
|
8
|
+
* call `getProductContent(db, entityId, scope, options)` and get back a
|
|
9
|
+
* fully-resolved `ContentLocaleResolution<ProductContent>` regardless
|
|
10
|
+
* of whether the row is owned or sourced.
|
|
11
|
+
*
|
|
12
|
+
* Owned rows (entities in the products table without a sourced-entry
|
|
13
|
+
* row) read from the products tables directly — out of scope for this
|
|
14
|
+
* file in v1; callers that need owned reads compose them around this
|
|
15
|
+
* service. Phase D ships sourced + synthesizer; owned dispatch
|
|
16
|
+
* narrows when first sourced template adopts.
|
|
17
|
+
*
|
|
18
|
+
* See `docs/architecture/catalog-sourced-content.md` §3.3, §3.4, §3.6.
|
|
19
|
+
*/
|
|
20
|
+
import { createInvalidateOnDrift, fetchOverlaysForEntity, isStale, pickBestCachedLocale, readSourcedEntry, withContentRefreshLock, } from "@voyantjs/catalog";
|
|
21
|
+
import { and, eq } from "drizzle-orm";
|
|
22
|
+
import { mergeOverlaysIntoProductContent, PRODUCTS_CONTENT_SCHEMA_VERSION, productContentSchema, validateProductContent, } from "./content-shape.js";
|
|
23
|
+
import { PRODUCTS_CONTENT_MARKET_ANY, productsSourcedContentTable, } from "./schema-sourced-content.js";
|
|
24
|
+
import { buildOwnedProductContent } from "./service-content-owned.js";
|
|
25
|
+
import { synthesizeProductContent, } from "./service-content-synthesizer.js";
|
|
26
|
+
/** Default TTL when the adapter doesn't pin `fresh_until`. */
|
|
27
|
+
const PRODUCTS_DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24h, per §3.4
|
|
28
|
+
/**
|
|
29
|
+
* Read the rich product content for one entity, resolving locale
|
|
30
|
+
* preference, applying overlays, and refreshing in the background when
|
|
31
|
+
* stale. Returns `null` only when the entity is unknown (no
|
|
32
|
+
* sourced-entry row, no owned row).
|
|
33
|
+
*/
|
|
34
|
+
export async function getProductContent(db, entityId, scope, options) {
|
|
35
|
+
const sourcedEntry = await readSourcedEntry(db, "products", entityId);
|
|
36
|
+
if (!sourcedEntry) {
|
|
37
|
+
// Owned-product path. Read from the products module's own tables
|
|
38
|
+
// and project to ProductContent — locale resolution against
|
|
39
|
+
// product_translations + product_option_translations uses the
|
|
40
|
+
// same pickBestCachedLocale scoring the sourced cache reads use.
|
|
41
|
+
// Overlay merge applies the same way it does for sourced rows.
|
|
42
|
+
const owned = await buildOwnedProductContent(db, entityId, {
|
|
43
|
+
preferredLocales: scope.preferredLocales,
|
|
44
|
+
});
|
|
45
|
+
if (!owned)
|
|
46
|
+
return null;
|
|
47
|
+
const overlays = await fetchOverlaysForEntity(db, "products", entityId);
|
|
48
|
+
const merged = mergeOverlaysIntoProductContent(owned.content, overlays.map((o) => ({ field_path: o.field_path, value: o.value })), {
|
|
49
|
+
onOverlayError: options.onOverlayError
|
|
50
|
+
? (e) => options.onOverlayError({
|
|
51
|
+
field_path: e.overlay.field_path,
|
|
52
|
+
reason: e.reason,
|
|
53
|
+
})
|
|
54
|
+
: undefined,
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
content: merged,
|
|
58
|
+
resolution: {
|
|
59
|
+
candidate: { locale: owned.servedLocale, payload: merged },
|
|
60
|
+
served_locale: owned.servedLocale,
|
|
61
|
+
match_kind: owned.matchKind,
|
|
62
|
+
},
|
|
63
|
+
source: "owned",
|
|
64
|
+
served_stale: false,
|
|
65
|
+
synthesized: false,
|
|
66
|
+
machine_translated: false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Wrap the entry as a ProvenanceReadResult so the synthesizer can
|
|
70
|
+
// consume it without re-reading.
|
|
71
|
+
const provenance = {
|
|
72
|
+
kind: "sourced",
|
|
73
|
+
provenance: {
|
|
74
|
+
source_kind: sourcedEntry.source_kind,
|
|
75
|
+
source_provider: sourcedEntry.source_provider ?? undefined,
|
|
76
|
+
source_connection_id: sourcedEntry.source_connection_id ?? undefined,
|
|
77
|
+
source_ref: sourcedEntry.source_ref ?? undefined,
|
|
78
|
+
source_freshness: sourcedEntry.source_freshness,
|
|
79
|
+
last_sourced_at: sourcedEntry.last_sourced_at ?? undefined,
|
|
80
|
+
},
|
|
81
|
+
entry_id: sourcedEntry.id,
|
|
82
|
+
status: sourcedEntry.status,
|
|
83
|
+
projection: sourcedEntry.projection,
|
|
84
|
+
projection_etag: sourcedEntry.projection_etag,
|
|
85
|
+
projection_seen_at: sourcedEntry.projection_seen_at,
|
|
86
|
+
first_seen_at: sourcedEntry.first_seen_at,
|
|
87
|
+
last_seen_at: sourcedEntry.last_seen_at,
|
|
88
|
+
};
|
|
89
|
+
const adapter = sourcedEntry.source_connection_id
|
|
90
|
+
? (options.registry.resolveByConnection(sourcedEntry.source_connection_id) ??
|
|
91
|
+
options.registry.byKind(sourcedEntry.source_kind)[0]?.adapter)
|
|
92
|
+
: options.registry.byKind(sourcedEntry.source_kind)[0]?.adapter;
|
|
93
|
+
const adapterCtx = options.buildAdapterContext?.(adapter) ?? {
|
|
94
|
+
connection_id: sourcedEntry.source_connection_id ?? sourcedEntry.source_kind,
|
|
95
|
+
};
|
|
96
|
+
const market = scope.market ?? PRODUCTS_CONTENT_MARKET_ANY;
|
|
97
|
+
const acceptMT = scope.acceptMachineTranslated ?? true;
|
|
98
|
+
if (options.forceFresh && adapter?.getContent) {
|
|
99
|
+
const fresh = await fetchFreshContent(db, adapter, adapterCtx, {
|
|
100
|
+
entity_module: "products",
|
|
101
|
+
entity_id: entityId,
|
|
102
|
+
locale: scope.preferredLocales[0] ?? "en-GB",
|
|
103
|
+
market,
|
|
104
|
+
currency: scope.currency,
|
|
105
|
+
}, options);
|
|
106
|
+
if (fresh) {
|
|
107
|
+
return finalizeFresh(db, entityId, fresh, scope, options);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 1. Look up cached candidates across all locales for this entity.
|
|
111
|
+
const cachedRows = await fetchCacheCandidates(db, entityId, market);
|
|
112
|
+
const eligibleRows = acceptMT ? cachedRows : cachedRows.filter((r) => !r.machine_translated);
|
|
113
|
+
const best = pickBestCachedLocale(eligibleRows.map((row) => ({ ...row, locale: row.locale })), scope.preferredLocales);
|
|
114
|
+
const shouldRefreshLegacyAvailability = best
|
|
115
|
+
? hasLegacyDepartureAvailabilityGap(best.candidate)
|
|
116
|
+
: false;
|
|
117
|
+
if (best && !isStale(best.candidate) && !shouldRefreshLegacyAvailability) {
|
|
118
|
+
return finalizeFromCache(db, entityId, best, "sourced-cache", false, options);
|
|
119
|
+
}
|
|
120
|
+
if (best && (isStale(best.candidate) || shouldRefreshLegacyAvailability)) {
|
|
121
|
+
// SWR for ordinary stale reads. Legacy demo content without
|
|
122
|
+
// departure capacity is refreshed synchronously so operator
|
|
123
|
+
// availability surfaces do not show effectively-unlimited slots.
|
|
124
|
+
if (adapter?.getContent) {
|
|
125
|
+
const refreshRequest = {
|
|
126
|
+
entity_module: "products",
|
|
127
|
+
entity_id: entityId,
|
|
128
|
+
locale: scope.preferredLocales[0] ?? best.candidate.locale,
|
|
129
|
+
market,
|
|
130
|
+
currency: scope.currency,
|
|
131
|
+
};
|
|
132
|
+
if (shouldRefreshLegacyAvailability) {
|
|
133
|
+
const fresh = await fetchFreshContent(db, adapter, adapterCtx, refreshRequest, options);
|
|
134
|
+
if (fresh)
|
|
135
|
+
return finalizeFresh(db, entityId, fresh, scope, options);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
void scheduleRefresh(db, adapter, adapterCtx, refreshRequest);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return finalizeFromCache(db, entityId, best, "sourced-cache", true, options);
|
|
142
|
+
}
|
|
143
|
+
// No cache row at all — must produce content somehow.
|
|
144
|
+
if (!adapter?.getContent) {
|
|
145
|
+
// Thin adapter or no adapter registered — synthesize from
|
|
146
|
+
// projection + overlay + plane metadata (§3.6).
|
|
147
|
+
const overlays = await fetchOverlaysForEntity(db, "products", entityId);
|
|
148
|
+
const synthesized = synthesizeProductContent({ locale: scope.preferredLocales[0] ?? "en-GB" }, {
|
|
149
|
+
provenance,
|
|
150
|
+
overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
|
|
151
|
+
});
|
|
152
|
+
return wrapSynthesized(synthesized, scope, false);
|
|
153
|
+
}
|
|
154
|
+
// Cache miss with a rich adapter — block on the adapter, dedupe
|
|
155
|
+
// across workers via advisory lock, and write through to the cache.
|
|
156
|
+
const fresh = await fetchFreshContent(db, adapter, adapterCtx, {
|
|
157
|
+
entity_module: "products",
|
|
158
|
+
entity_id: entityId,
|
|
159
|
+
locale: scope.preferredLocales[0] ?? "en-GB",
|
|
160
|
+
market,
|
|
161
|
+
currency: scope.currency,
|
|
162
|
+
}, options);
|
|
163
|
+
if (!fresh) {
|
|
164
|
+
// The adapter call could not get the lock AND there's no cached
|
|
165
|
+
// row — fall back to synthesizer rather than blocking forever.
|
|
166
|
+
const overlays = await fetchOverlaysForEntity(db, "products", entityId);
|
|
167
|
+
const synthesized = synthesizeProductContent({ locale: scope.preferredLocales[0] ?? "en-GB" }, {
|
|
168
|
+
provenance,
|
|
169
|
+
overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
|
|
170
|
+
});
|
|
171
|
+
return wrapSynthesized(synthesized, scope, false);
|
|
172
|
+
}
|
|
173
|
+
return finalizeFresh(db, entityId, fresh, scope, options);
|
|
174
|
+
}
|
|
175
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
176
|
+
// Cache candidates
|
|
177
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
async function fetchCacheCandidates(db, entityId, market) {
|
|
179
|
+
const rows = await db
|
|
180
|
+
.select()
|
|
181
|
+
.from(productsSourcedContentTable)
|
|
182
|
+
.where(and(eq(productsSourcedContentTable.entity_id, entityId), eq(productsSourcedContentTable.market, market), eq(productsSourcedContentTable.content_schema_version, PRODUCTS_CONTENT_SCHEMA_VERSION)));
|
|
183
|
+
return rows;
|
|
184
|
+
}
|
|
185
|
+
function hasLegacyDepartureAvailabilityGap(row) {
|
|
186
|
+
const validation = validateProductContent(row.payload);
|
|
187
|
+
if (!validation.valid)
|
|
188
|
+
return false;
|
|
189
|
+
return validation.content.departures.some((departure) => departure.capacity == null && departure.remaining == null);
|
|
190
|
+
}
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
// Fresh fetch + write-through
|
|
193
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
194
|
+
async function fetchFreshContent(db, adapter, ctx, request, _options) {
|
|
195
|
+
const result = await withContentRefreshLock(db, {
|
|
196
|
+
entityModule: request.entity_module,
|
|
197
|
+
entityId: request.entity_id,
|
|
198
|
+
locale: request.locale,
|
|
199
|
+
market: request.market,
|
|
200
|
+
}, async () => {
|
|
201
|
+
const got = await adapter.getContent(ctx, request);
|
|
202
|
+
const validation = validateProductContent(got.content);
|
|
203
|
+
if (!validation.valid) {
|
|
204
|
+
// Surface adapter integration bugs, but don't write to cache.
|
|
205
|
+
throw new Error(`products getContent for ${request.entity_id} failed validation: ${validation.reason}`);
|
|
206
|
+
}
|
|
207
|
+
await writeCacheRow(db, request, got);
|
|
208
|
+
return got;
|
|
209
|
+
});
|
|
210
|
+
return result ?? null;
|
|
211
|
+
}
|
|
212
|
+
function scheduleRefresh(db, adapter, ctx, request) {
|
|
213
|
+
// Fire-and-forget. Errors are swallowed — a failed refresh just
|
|
214
|
+
// leaves the stale row in place; the next read tries again.
|
|
215
|
+
void withContentRefreshLock(db, {
|
|
216
|
+
entityModule: request.entity_module,
|
|
217
|
+
entityId: request.entity_id,
|
|
218
|
+
locale: request.locale,
|
|
219
|
+
market: request.market,
|
|
220
|
+
}, async () => {
|
|
221
|
+
const got = await adapter.getContent(ctx, request);
|
|
222
|
+
const validation = validateProductContent(got.content);
|
|
223
|
+
if (!validation.valid)
|
|
224
|
+
return;
|
|
225
|
+
await writeCacheRow(db, request, got);
|
|
226
|
+
}).catch(() => {
|
|
227
|
+
// intentionally swallow — see comment above
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function writeCacheRow(db, request, result) {
|
|
231
|
+
const market = request.market ?? PRODUCTS_CONTENT_MARKET_ANY;
|
|
232
|
+
const now = new Date();
|
|
233
|
+
// Date-like fields may arrive as strings when the adapter is an HTTP
|
|
234
|
+
// client (JSON.parse doesn't deserialize ISO timestamps to Date).
|
|
235
|
+
// Coerce at the cache-write boundary so the drizzle timestamp column
|
|
236
|
+
// gets a real Date — `value.toISOString is not a function` otherwise.
|
|
237
|
+
const sourceUpdatedAt = toDateOrNull(result.source_updated_at);
|
|
238
|
+
const freshUntil = toDateOrNull(result.fresh_until) ?? new Date(now.getTime() + PRODUCTS_DEFAULT_TTL_MS);
|
|
239
|
+
await db
|
|
240
|
+
.insert(productsSourcedContentTable)
|
|
241
|
+
.values({
|
|
242
|
+
entity_id: request.entity_id,
|
|
243
|
+
locale: request.locale,
|
|
244
|
+
market,
|
|
245
|
+
payload: result.content,
|
|
246
|
+
content_schema_version: result.content_schema_version,
|
|
247
|
+
returned_locale: result.returned_locale,
|
|
248
|
+
machine_translated: result.machine_translated ?? false,
|
|
249
|
+
source_updated_at: sourceUpdatedAt,
|
|
250
|
+
fetched_at: now,
|
|
251
|
+
fresh_until: freshUntil,
|
|
252
|
+
etag: result.etag ?? null,
|
|
253
|
+
fetch_status: "ok",
|
|
254
|
+
fetch_error: null,
|
|
255
|
+
})
|
|
256
|
+
.onConflictDoUpdate({
|
|
257
|
+
target: [
|
|
258
|
+
productsSourcedContentTable.entity_id,
|
|
259
|
+
productsSourcedContentTable.locale,
|
|
260
|
+
productsSourcedContentTable.market,
|
|
261
|
+
],
|
|
262
|
+
set: {
|
|
263
|
+
payload: result.content,
|
|
264
|
+
content_schema_version: result.content_schema_version,
|
|
265
|
+
returned_locale: result.returned_locale,
|
|
266
|
+
machine_translated: result.machine_translated ?? false,
|
|
267
|
+
source_updated_at: sourceUpdatedAt,
|
|
268
|
+
fetched_at: now,
|
|
269
|
+
fresh_until: freshUntil,
|
|
270
|
+
etag: result.etag ?? null,
|
|
271
|
+
fetch_status: "ok",
|
|
272
|
+
fetch_error: null,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function toDateOrNull(value) {
|
|
277
|
+
if (!value)
|
|
278
|
+
return null;
|
|
279
|
+
if (value instanceof Date)
|
|
280
|
+
return value;
|
|
281
|
+
const parsed = new Date(value);
|
|
282
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
283
|
+
}
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
285
|
+
// Finalizers — overlay merge + return shape
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
287
|
+
async function finalizeFromCache(db, entityId, best, source, servedStale, options) {
|
|
288
|
+
const cachedPayload = best.candidate.payload;
|
|
289
|
+
const validation = validateProductContent(cachedPayload);
|
|
290
|
+
if (!validation.valid) {
|
|
291
|
+
// Schema-version-mismatch case is filtered upstream; if we hit
|
|
292
|
+
// here, the cache row is corrupt for some other reason. Treat as
|
|
293
|
+
// cache miss → caller's next layer (synthesizer) handles.
|
|
294
|
+
throw new Error(`products cache row for ${entityId} (${best.candidate.locale}) failed validation: ${validation.reason}`);
|
|
295
|
+
}
|
|
296
|
+
const cachedContent = validation.content;
|
|
297
|
+
const overlays = await fetchOverlaysForEntity(db, "products", entityId);
|
|
298
|
+
const merged = mergeOverlaysIntoProductContent(cachedContent, overlays.map((o) => ({ field_path: o.field_path, value: o.value })), {
|
|
299
|
+
onOverlayError: options.onOverlayError
|
|
300
|
+
? (e) => options.onOverlayError({
|
|
301
|
+
field_path: e.overlay.field_path,
|
|
302
|
+
reason: e.reason,
|
|
303
|
+
})
|
|
304
|
+
: undefined,
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
content: merged,
|
|
308
|
+
resolution: {
|
|
309
|
+
candidate: { locale: best.candidate.locale, payload: merged },
|
|
310
|
+
served_locale: best.candidate.returned_locale,
|
|
311
|
+
match_kind: best.match_kind,
|
|
312
|
+
},
|
|
313
|
+
source,
|
|
314
|
+
served_stale: servedStale,
|
|
315
|
+
synthesized: false,
|
|
316
|
+
machine_translated: best.candidate.machine_translated,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async function finalizeFresh(db, entityId, fresh, scope, options) {
|
|
320
|
+
const cachedContent = productContentSchema.parse(fresh.content);
|
|
321
|
+
const overlays = await fetchOverlaysForEntity(db, "products", entityId);
|
|
322
|
+
const merged = mergeOverlaysIntoProductContent(cachedContent, overlays.map((o) => ({ field_path: o.field_path, value: o.value })), {
|
|
323
|
+
onOverlayError: options.onOverlayError
|
|
324
|
+
? (e) => options.onOverlayError({
|
|
325
|
+
field_path: e.overlay.field_path,
|
|
326
|
+
reason: e.reason,
|
|
327
|
+
})
|
|
328
|
+
: undefined,
|
|
329
|
+
});
|
|
330
|
+
return {
|
|
331
|
+
content: merged,
|
|
332
|
+
resolution: {
|
|
333
|
+
candidate: { locale: scope.preferredLocales[0] ?? fresh.returned_locale, payload: merged },
|
|
334
|
+
served_locale: fresh.returned_locale,
|
|
335
|
+
match_kind: scope.preferredLocales[0] === fresh.returned_locale ? "exact" : "language_match",
|
|
336
|
+
},
|
|
337
|
+
source: "sourced-fresh",
|
|
338
|
+
served_stale: false,
|
|
339
|
+
synthesized: false,
|
|
340
|
+
machine_translated: fresh.machine_translated ?? false,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function wrapSynthesized(synthesized, scope, servedStale) {
|
|
344
|
+
return {
|
|
345
|
+
content: synthesized.content,
|
|
346
|
+
resolution: {
|
|
347
|
+
candidate: { locale: synthesized.served_locale, payload: synthesized.content },
|
|
348
|
+
served_locale: synthesized.served_locale,
|
|
349
|
+
match_kind: scope.preferredLocales[0] === synthesized.served_locale ? "exact" : "any",
|
|
350
|
+
},
|
|
351
|
+
source: "synthesized",
|
|
352
|
+
served_stale: servedStale,
|
|
353
|
+
synthesized: true,
|
|
354
|
+
machine_translated: false,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Drift event consumer — sets `fresh_until = now()` on every cache row
|
|
359
|
+
* matching the event's (entity_module, entity_id [, locale [, market]])
|
|
360
|
+
* scope. The next read serves stale + schedules a SWR refresh.
|
|
361
|
+
*
|
|
362
|
+
* Templates subscribe this to the catalog plane's drift-event bus.
|
|
363
|
+
* Per sourced-content §3.4.1.
|
|
364
|
+
*/
|
|
365
|
+
export const invalidateProductContentOnDrift = createInvalidateOnDrift(productsSourcedContentTable, { entityModule: "products" });
|
package/dist/service.d.ts
CHANGED
|
@@ -92,10 +92,13 @@ export declare const productsService: {
|
|
|
92
92
|
costAmountCents: number | null;
|
|
93
93
|
marginPercent: number | null;
|
|
94
94
|
facilityId: string | null;
|
|
95
|
+
supplierId: string | null;
|
|
95
96
|
startDate: string | null;
|
|
96
97
|
endDate: string | null;
|
|
97
98
|
pax: number | null;
|
|
98
99
|
productTypeId: string | null;
|
|
100
|
+
taxClassId: string | null;
|
|
101
|
+
customerPaymentPolicy: unknown;
|
|
99
102
|
tags: string[] | null;
|
|
100
103
|
createdAt: Date;
|
|
101
104
|
updatedAt: Date;
|
|
@@ -120,10 +123,13 @@ export declare const productsService: {
|
|
|
120
123
|
costAmountCents: number | null;
|
|
121
124
|
marginPercent: number | null;
|
|
122
125
|
facilityId: string | null;
|
|
126
|
+
supplierId: string | null;
|
|
123
127
|
startDate: string | null;
|
|
124
128
|
endDate: string | null;
|
|
125
129
|
pax: number | null;
|
|
126
130
|
productTypeId: string | null;
|
|
131
|
+
taxClassId: string | null;
|
|
132
|
+
customerPaymentPolicy: unknown;
|
|
127
133
|
tags: string[] | null;
|
|
128
134
|
createdAt: Date;
|
|
129
135
|
updatedAt: Date;
|
|
@@ -144,6 +150,7 @@ export declare const productsService: {
|
|
|
144
150
|
activated: boolean;
|
|
145
151
|
productTypeId: string | null;
|
|
146
152
|
facilityId: string | null;
|
|
153
|
+
supplierId: string | null;
|
|
147
154
|
pax: number | null;
|
|
148
155
|
reservationTimeoutMinutes: number | null;
|
|
149
156
|
sellAmountCents: number | null;
|
|
@@ -151,6 +158,8 @@ export declare const productsService: {
|
|
|
151
158
|
costAmountCents: number | null;
|
|
152
159
|
marginPercent: number | null;
|
|
153
160
|
tags: string[] | null;
|
|
161
|
+
taxClassId: string | null;
|
|
162
|
+
customerPaymentPolicy: unknown;
|
|
154
163
|
}>;
|
|
155
164
|
updateProduct(db: PostgresJsDatabase, id: string, data: UpdateProductInput): Promise<{
|
|
156
165
|
id: string;
|
|
@@ -168,10 +177,13 @@ export declare const productsService: {
|
|
|
168
177
|
costAmountCents: number | null;
|
|
169
178
|
marginPercent: number | null;
|
|
170
179
|
facilityId: string | null;
|
|
180
|
+
supplierId: string | null;
|
|
171
181
|
startDate: string | null;
|
|
172
182
|
endDate: string | null;
|
|
173
183
|
pax: number | null;
|
|
174
184
|
productTypeId: string | null;
|
|
185
|
+
taxClassId: string | null;
|
|
186
|
+
customerPaymentPolicy: unknown;
|
|
175
187
|
tags: string[] | null;
|
|
176
188
|
createdAt: Date;
|
|
177
189
|
updatedAt: Date;
|
|
@@ -339,7 +351,7 @@ export declare const productsService: {
|
|
|
339
351
|
data: {
|
|
340
352
|
id: string;
|
|
341
353
|
productId: string;
|
|
342
|
-
capability: "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "
|
|
354
|
+
capability: "accommodation" | "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "transport";
|
|
343
355
|
enabled: boolean;
|
|
344
356
|
notes: string | null;
|
|
345
357
|
createdAt: Date;
|
|
@@ -352,7 +364,7 @@ export declare const productsService: {
|
|
|
352
364
|
getCapabilityById(db: PostgresJsDatabase, id: string): Promise<{
|
|
353
365
|
id: string;
|
|
354
366
|
productId: string;
|
|
355
|
-
capability: "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "
|
|
367
|
+
capability: "accommodation" | "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "transport";
|
|
356
368
|
enabled: boolean;
|
|
357
369
|
notes: string | null;
|
|
358
370
|
createdAt: Date;
|
|
@@ -365,12 +377,12 @@ export declare const productsService: {
|
|
|
365
377
|
updatedAt: Date;
|
|
366
378
|
notes: string | null;
|
|
367
379
|
productId: string;
|
|
368
|
-
capability: "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "
|
|
380
|
+
capability: "accommodation" | "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "transport";
|
|
369
381
|
} | null>;
|
|
370
382
|
updateCapability(db: PostgresJsDatabase, id: string, data: UpdateProductCapabilityInput): Promise<{
|
|
371
383
|
id: string;
|
|
372
384
|
productId: string;
|
|
373
|
-
capability: "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "
|
|
385
|
+
capability: "accommodation" | "on_request" | "private" | "instant_confirmation" | "pickup_available" | "dropoff_available" | "guided" | "shared" | "digital_ticket" | "voucher_required" | "external_inventory" | "multi_day" | "transport";
|
|
374
386
|
enabled: boolean;
|
|
375
387
|
notes: string | null;
|
|
376
388
|
createdAt: Date;
|
|
@@ -556,11 +568,11 @@ export declare const productsService: {
|
|
|
556
568
|
updatedAt: Date;
|
|
557
569
|
productId: string;
|
|
558
570
|
title: string;
|
|
559
|
-
sortOrder: number;
|
|
560
|
-
locationType: "start" | "other" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
561
|
-
address: string | null;
|
|
562
571
|
city: string | null;
|
|
572
|
+
address: string | null;
|
|
573
|
+
sortOrder: number;
|
|
563
574
|
countryCode: string | null;
|
|
575
|
+
locationType: "start" | "other" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
|
|
564
576
|
latitude: number | null;
|
|
565
577
|
longitude: number | null;
|
|
566
578
|
googlePlaceId: string | null;
|
|
@@ -806,7 +818,7 @@ export declare const productsService: {
|
|
|
806
818
|
name: string;
|
|
807
819
|
code: string | null;
|
|
808
820
|
description: string | null;
|
|
809
|
-
unitType: "service" | "other" | "
|
|
821
|
+
unitType: "service" | "other" | "group" | "person" | "room" | "vehicle";
|
|
810
822
|
minQuantity: number | null;
|
|
811
823
|
maxQuantity: number | null;
|
|
812
824
|
minAge: number | null;
|
|
@@ -829,7 +841,7 @@ export declare const productsService: {
|
|
|
829
841
|
name: string;
|
|
830
842
|
code: string | null;
|
|
831
843
|
description: string | null;
|
|
832
|
-
unitType: "service" | "other" | "
|
|
844
|
+
unitType: "service" | "other" | "group" | "person" | "room" | "vehicle";
|
|
833
845
|
minQuantity: number | null;
|
|
834
846
|
maxQuantity: number | null;
|
|
835
847
|
minAge: number | null;
|
|
@@ -850,12 +862,12 @@ export declare const productsService: {
|
|
|
850
862
|
description: string | null;
|
|
851
863
|
code: string | null;
|
|
852
864
|
optionId: string;
|
|
865
|
+
maxAge: number | null;
|
|
866
|
+
minAge: number | null;
|
|
853
867
|
sortOrder: number;
|
|
854
|
-
unitType: "service" | "other" | "
|
|
868
|
+
unitType: "service" | "other" | "group" | "person" | "room" | "vehicle";
|
|
855
869
|
minQuantity: number | null;
|
|
856
870
|
maxQuantity: number | null;
|
|
857
|
-
minAge: number | null;
|
|
858
|
-
maxAge: number | null;
|
|
859
871
|
occupancyMin: number | null;
|
|
860
872
|
occupancyMax: number | null;
|
|
861
873
|
isRequired: boolean;
|
|
@@ -867,7 +879,7 @@ export declare const productsService: {
|
|
|
867
879
|
name: string;
|
|
868
880
|
code: string | null;
|
|
869
881
|
description: string | null;
|
|
870
|
-
unitType: "service" | "other" | "
|
|
882
|
+
unitType: "service" | "other" | "group" | "person" | "room" | "vehicle";
|
|
871
883
|
minQuantity: number | null;
|
|
872
884
|
maxQuantity: number | null;
|
|
873
885
|
minAge: number | null;
|
|
@@ -1620,9 +1632,9 @@ export declare const productsService: {
|
|
|
1620
1632
|
updatedAt: Date;
|
|
1621
1633
|
description: string | null;
|
|
1622
1634
|
title: string | null;
|
|
1635
|
+
location: string | null;
|
|
1623
1636
|
itineraryId: string;
|
|
1624
1637
|
dayNumber: number;
|
|
1625
|
-
location: string | null;
|
|
1626
1638
|
} | null | undefined>;
|
|
1627
1639
|
createItineraryDay(db: PostgresJsDatabase, productId: string, itineraryId: string, data: CreateDayInput): Promise<{
|
|
1628
1640
|
id: string;
|
|
@@ -1630,9 +1642,9 @@ export declare const productsService: {
|
|
|
1630
1642
|
updatedAt: Date;
|
|
1631
1643
|
description: string | null;
|
|
1632
1644
|
title: string | null;
|
|
1645
|
+
location: string | null;
|
|
1633
1646
|
itineraryId: string;
|
|
1634
1647
|
dayNumber: number;
|
|
1635
|
-
location: string | null;
|
|
1636
1648
|
} | null | undefined>;
|
|
1637
1649
|
updateDay(db: PostgresJsDatabase, dayId: string, data: UpdateDayInput): Promise<{
|
|
1638
1650
|
id: string;
|
|
@@ -1704,7 +1716,7 @@ export declare const productsService: {
|
|
|
1704
1716
|
tableName: "product_day_services";
|
|
1705
1717
|
dataType: "string";
|
|
1706
1718
|
columnType: "PgEnumColumn";
|
|
1707
|
-
data: "other" | "
|
|
1719
|
+
data: "other" | "accommodation" | "transfer" | "experience" | "guide" | "meal";
|
|
1708
1720
|
driverParam: string;
|
|
1709
1721
|
notNull: true;
|
|
1710
1722
|
hasDefault: false;
|
|
@@ -1750,6 +1762,23 @@ export declare const productsService: {
|
|
|
1750
1762
|
identity: undefined;
|
|
1751
1763
|
generated: undefined;
|
|
1752
1764
|
}, {}, {}>;
|
|
1765
|
+
countryCode: import("drizzle-orm/pg-core").PgColumn<{
|
|
1766
|
+
name: "country_code";
|
|
1767
|
+
tableName: "product_day_services";
|
|
1768
|
+
dataType: "string";
|
|
1769
|
+
columnType: "PgText";
|
|
1770
|
+
data: string;
|
|
1771
|
+
driverParam: string;
|
|
1772
|
+
notNull: false;
|
|
1773
|
+
hasDefault: false;
|
|
1774
|
+
isPrimaryKey: false;
|
|
1775
|
+
isAutoincrement: false;
|
|
1776
|
+
hasRuntimeDefault: false;
|
|
1777
|
+
enumValues: [string, ...string[]];
|
|
1778
|
+
baseColumn: never;
|
|
1779
|
+
identity: undefined;
|
|
1780
|
+
generated: undefined;
|
|
1781
|
+
}, {}, {}>;
|
|
1753
1782
|
costCurrency: import("drizzle-orm/pg-core").PgColumn<{
|
|
1754
1783
|
name: "cost_currency";
|
|
1755
1784
|
tableName: "product_day_services";
|
|
@@ -1856,9 +1885,10 @@ export declare const productsService: {
|
|
|
1856
1885
|
id: string;
|
|
1857
1886
|
dayId: string;
|
|
1858
1887
|
supplierServiceId: string | null;
|
|
1859
|
-
serviceType: "other" | "
|
|
1888
|
+
serviceType: "other" | "accommodation" | "transfer" | "experience" | "guide" | "meal";
|
|
1860
1889
|
name: string;
|
|
1861
1890
|
description: string | null;
|
|
1891
|
+
countryCode: string | null;
|
|
1862
1892
|
costCurrency: string;
|
|
1863
1893
|
costAmountCents: number;
|
|
1864
1894
|
quantity: number;
|
|
@@ -1922,7 +1952,7 @@ export declare const productsService: {
|
|
|
1922
1952
|
tableName: "product_day_services";
|
|
1923
1953
|
dataType: "string";
|
|
1924
1954
|
columnType: "PgEnumColumn";
|
|
1925
|
-
data: "other" | "
|
|
1955
|
+
data: "other" | "accommodation" | "transfer" | "experience" | "guide" | "meal";
|
|
1926
1956
|
driverParam: string;
|
|
1927
1957
|
notNull: true;
|
|
1928
1958
|
hasDefault: false;
|
|
@@ -1968,6 +1998,23 @@ export declare const productsService: {
|
|
|
1968
1998
|
identity: undefined;
|
|
1969
1999
|
generated: undefined;
|
|
1970
2000
|
}, {}, {}>;
|
|
2001
|
+
countryCode: import("drizzle-orm/pg-core").PgColumn<{
|
|
2002
|
+
name: "country_code";
|
|
2003
|
+
tableName: "product_day_services";
|
|
2004
|
+
dataType: "string";
|
|
2005
|
+
columnType: "PgText";
|
|
2006
|
+
data: string;
|
|
2007
|
+
driverParam: string;
|
|
2008
|
+
notNull: false;
|
|
2009
|
+
hasDefault: false;
|
|
2010
|
+
isPrimaryKey: false;
|
|
2011
|
+
isAutoincrement: false;
|
|
2012
|
+
hasRuntimeDefault: false;
|
|
2013
|
+
enumValues: [string, ...string[]];
|
|
2014
|
+
baseColumn: never;
|
|
2015
|
+
identity: undefined;
|
|
2016
|
+
generated: undefined;
|
|
2017
|
+
}, {}, {}>;
|
|
1971
2018
|
costCurrency: import("drizzle-orm/pg-core").PgColumn<{
|
|
1972
2019
|
name: "cost_currency";
|
|
1973
2020
|
tableName: "product_day_services";
|
|
@@ -2079,19 +2126,21 @@ export declare const productsService: {
|
|
|
2079
2126
|
description: string | null;
|
|
2080
2127
|
supplierServiceId: string | null;
|
|
2081
2128
|
costAmountCents: number;
|
|
2129
|
+
quantity: number;
|
|
2082
2130
|
sortOrder: number | null;
|
|
2083
2131
|
dayId: string;
|
|
2084
|
-
serviceType: "other" | "
|
|
2132
|
+
serviceType: "other" | "accommodation" | "transfer" | "experience" | "guide" | "meal";
|
|
2133
|
+
countryCode: string | null;
|
|
2085
2134
|
costCurrency: string;
|
|
2086
|
-
quantity: number;
|
|
2087
2135
|
} | null | undefined>;
|
|
2088
2136
|
updateDayService(db: PostgresJsDatabase, productId: string, serviceId: string, data: UpdateDayServiceInput): Promise<{
|
|
2089
2137
|
id: string;
|
|
2090
2138
|
dayId: string;
|
|
2091
2139
|
supplierServiceId: string | null;
|
|
2092
|
-
serviceType: "other" | "
|
|
2140
|
+
serviceType: "other" | "accommodation" | "transfer" | "experience" | "guide" | "meal";
|
|
2093
2141
|
name: string;
|
|
2094
2142
|
description: string | null;
|
|
2143
|
+
countryCode: string | null;
|
|
2095
2144
|
costCurrency: string;
|
|
2096
2145
|
costAmountCents: number;
|
|
2097
2146
|
quantity: number;
|
|
@@ -2543,8 +2592,8 @@ export declare const productsService: {
|
|
|
2543
2592
|
id: string;
|
|
2544
2593
|
createdAt: Date;
|
|
2545
2594
|
productId: string;
|
|
2546
|
-
authorId: string;
|
|
2547
2595
|
content: string;
|
|
2596
|
+
authorId: string;
|
|
2548
2597
|
} | null | undefined>;
|
|
2549
2598
|
recalculate(db: PostgresJsDatabase, productId: string): Promise<{
|
|
2550
2599
|
costAmountCents: number;
|
|
@@ -2611,6 +2660,7 @@ export declare const productsService: {
|
|
|
2611
2660
|
description: string | null;
|
|
2612
2661
|
sortOrder: number;
|
|
2613
2662
|
active: boolean;
|
|
2663
|
+
customerPaymentPolicy: unknown;
|
|
2614
2664
|
metadata: Record<string, unknown> | null;
|
|
2615
2665
|
createdAt: Date;
|
|
2616
2666
|
updatedAt: Date;
|
|
@@ -2627,6 +2677,7 @@ export declare const productsService: {
|
|
|
2627
2677
|
description: string | null;
|
|
2628
2678
|
sortOrder: number;
|
|
2629
2679
|
active: boolean;
|
|
2680
|
+
customerPaymentPolicy: unknown;
|
|
2630
2681
|
metadata: Record<string, unknown> | null;
|
|
2631
2682
|
createdAt: Date;
|
|
2632
2683
|
updatedAt: Date;
|
|
@@ -2640,6 +2691,7 @@ export declare const productsService: {
|
|
|
2640
2691
|
slug: string;
|
|
2641
2692
|
description: string | null;
|
|
2642
2693
|
active: boolean;
|
|
2694
|
+
customerPaymentPolicy: unknown;
|
|
2643
2695
|
sortOrder: number;
|
|
2644
2696
|
parentId: string | null;
|
|
2645
2697
|
} | undefined>;
|
|
@@ -2651,6 +2703,7 @@ export declare const productsService: {
|
|
|
2651
2703
|
description: string | null;
|
|
2652
2704
|
sortOrder: number;
|
|
2653
2705
|
active: boolean;
|
|
2706
|
+
customerPaymentPolicy: unknown;
|
|
2654
2707
|
metadata: Record<string, unknown> | null;
|
|
2655
2708
|
createdAt: Date;
|
|
2656
2709
|
updatedAt: Date;
|
|
@@ -2708,6 +2761,7 @@ export declare const productsService: {
|
|
|
2708
2761
|
description: string | null;
|
|
2709
2762
|
sortOrder: number;
|
|
2710
2763
|
active: boolean;
|
|
2764
|
+
customerPaymentPolicy: unknown;
|
|
2711
2765
|
metadata: Record<string, unknown> | null;
|
|
2712
2766
|
createdAt: Date;
|
|
2713
2767
|
updatedAt: Date;
|