@voyant-travel/catalog 0.118.0 → 0.119.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.
@@ -24,6 +24,7 @@ export { type AccommodationSubStep, type AddonGroup, type AddonOffer, type Booki
24
24
  export { bookingDraftsTable, type InsertBookingDraft, type SelectBookingDraft, } from "./drafts-schema.js";
25
25
  export { createBookingDraft, DEFAULT_DRAFT_TTL_MS, deleteBookingDraft, findExpiredDrafts, getBookingDraft, markDraftConsumed, type UpdateDraftPatch, type UpsertDraftInput, updateBookingDraft, } from "./drafts-service.js";
26
26
  export { BookingEngineError, type BookingEngineErrorCode, NO_ADAPTER_REGISTERED, NO_HANDLER_REGISTERED, NoAdapterRegisteredError, NoOwnedHandlerRegisteredError, ORDER_ALREADY_CANCELLED, ORDER_NOT_FOUND, QUOTE_EXPIRED, QUOTE_MISMATCH, QUOTE_NOT_FOUND, QuoteExpiredError, QuoteMismatchError, RESERVE_FAILED, ReserveFailedError, SNAPSHOT_CONTENT_UNAVAILABLE, SnapshotContentUnavailableError, } from "./errors.js";
27
+ export { type CatalogBookingRouteModuleOptions, type CatalogOwnedProductSummary, type CatalogProductContentReadContext, type CatalogProductContentScope, type CatalogResolvedDeparture, type CatalogResolvedProductContent, createCatalogBookingOrdersRoutes, mountCatalogBookingRoutes, type SlotRow, } from "./operator-routes.js";
27
28
  export { getOrderById, type ListOrdersQuery, type ListOrdersResult, listOrders, } from "./orders.js";
28
29
  export { type CommitOwnedRequest, type CommitOwnedResult, type ComputeQuoteRequest, type ComputeQuoteResult, createOwnedBookingHandlerRegistry, type HoldRequest, type HoldResult, OWNED_SOURCE_KIND, type OwnedBookingHandler, type OwnedBookingHandlerRegistry, type OwnedHandlerContext, type OwnedQuoteScope, } from "./owned-handler.js";
29
30
  export type { AppliedOffer, CodeStatus, PromotionEvaluationInput, PromotionEvaluationOutput, } from "./promotions-contract.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/booking-engine/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/booking-engine/index.ts"],"names":[],"mappings":"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"}
@@ -24,6 +24,7 @@ export { DEFAULT_PAX_BANDS, DEFAULT_PAX_TOTAL, defaultBookingFields, defaultDraf
24
24
  export { bookingDraftsTable, } from "./drafts-schema.js";
25
25
  export { createBookingDraft, DEFAULT_DRAFT_TTL_MS, deleteBookingDraft, findExpiredDrafts, getBookingDraft, markDraftConsumed, updateBookingDraft, } from "./drafts-service.js";
26
26
  export { BookingEngineError, NO_ADAPTER_REGISTERED, NO_HANDLER_REGISTERED, NoAdapterRegisteredError, NoOwnedHandlerRegisteredError, ORDER_ALREADY_CANCELLED, ORDER_NOT_FOUND, QUOTE_EXPIRED, QUOTE_MISMATCH, QUOTE_NOT_FOUND, QuoteExpiredError, QuoteMismatchError, RESERVE_FAILED, ReserveFailedError, SNAPSHOT_CONTENT_UNAVAILABLE, SnapshotContentUnavailableError, } from "./errors.js";
27
+ export { createCatalogBookingOrdersRoutes, mountCatalogBookingRoutes, } from "./operator-routes.js";
27
28
  export { getOrderById, listOrders, } from "./orders.js";
28
29
  export { createOwnedBookingHandlerRegistry, OWNED_SOURCE_KIND, } from "./owned-handler.js";
29
30
  export { DEFAULT_QUOTE_TTL_MS, quoteEntity, } from "./quote.js";
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Catalog booking-engine route module — the full admin + public booking
3
+ * surface, owned by `@voyant-travel/catalog`.
4
+ *
5
+ * A deployment composes this and supplies two structural options:
6
+ * - `booking` — the `CatalogBookingRoutesOptions` (db / registries /
7
+ * hold-ttl / promotions / tax hooks) it already builds today, and
8
+ * - `resolveRegistry(c)` — pulls the process-local `SourceAdapterRegistry`
9
+ * off the request context (cancel needs it to dispatch to adapters).
10
+ *
11
+ * The module mounts the shared lifecycle from `./routes.js` on **two**
12
+ * surfaces and adds the admin-only order-management endpoints:
13
+ *
14
+ * POST /v1/{admin,public}/catalog/quote → quoteEntity
15
+ * POST /v1/{admin,public}/catalog/book → bookEntity
16
+ * PUT /v1/{admin,public}/catalog/drafts/:id → upsert booking draft
17
+ * GET /v1/{admin,public}/catalog/drafts/:id → read booking draft
18
+ * DELETE /v1/{admin,public}/catalog/drafts/:id → delete booking draft
19
+ * POST /v1/{admin,public}/catalog/holds/place → place hold
20
+ * POST /v1/{admin,public}/catalog/holds/release → release hold
21
+ * GET /v1/admin/catalog/orders → listOrders
22
+ * GET /v1/admin/catalog/orders/:id → getOrderById
23
+ * POST /v1/admin/catalog/orders/:id/cancel → cancelEntity
24
+ * GET /v1/{admin,public}/catalog/slots → availability slots
25
+ * GET /v1/admin/bookings/:id/catalog-snapshot → frozen catalog snapshot
26
+ *
27
+ * Auth posture comes from the deployment's `createApp` middleware chain —
28
+ * `/v1/admin/...` requires staff, `/v1/public/...` accepts the configured
29
+ * public actors. Per booking-journey-architecture §10 Phase B.
30
+ *
31
+ * The slots + catalog-snapshot handlers read across module boundaries
32
+ * (`@voyant-travel/inventory` / `@voyant-travel/operations`), both of which
33
+ * already depend on `@voyant-travel/catalog`. Statically importing them here
34
+ * would create an import cycle, so the cross-package reads are supplied by the
35
+ * deployment as INJECTED option functions — the package never imports those
36
+ * modules, it only calls the readers the deployment hands it.
37
+ */
38
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
39
+ import { type Context, Hono } from "hono";
40
+ import type { SourceAdapterRegistry } from "./registry.js";
41
+ import { type CatalogBookingRoutesOptions } from "./routes.js";
42
+ /**
43
+ * A single resolved departure/slot as projected by `getProductContent`.
44
+ * Structural mirror of `@voyant-travel/inventory`'s `ProductDeparture` so the
45
+ * package needn't import inventory — only the fields the slots handler maps.
46
+ */
47
+ export interface CatalogResolvedDeparture {
48
+ id: string;
49
+ starts_at: string;
50
+ ends_at?: string | null;
51
+ status?: string | null;
52
+ capacity?: number | null;
53
+ remaining?: number | null;
54
+ }
55
+ /**
56
+ * Structural result of the injected `getProductContent` reader. Mirrors
57
+ * `@voyant-travel/inventory/service-content`'s `ResolvedProductContent`, but
58
+ * narrowed to the only field the slots handler reads (`content.departures`).
59
+ */
60
+ export interface CatalogResolvedProductContent {
61
+ content: {
62
+ departures?: ReadonlyArray<CatalogResolvedDeparture>;
63
+ };
64
+ }
65
+ /** Locale scope passed to the injected `getProductContent` reader. */
66
+ export interface CatalogProductContentScope {
67
+ preferredLocales: ReadonlyArray<string>;
68
+ }
69
+ /** Adapter/runtime context passed to the injected `getProductContent` reader. */
70
+ export interface CatalogProductContentReadContext {
71
+ registry: SourceAdapterRegistry;
72
+ forceFresh?: boolean;
73
+ }
74
+ /**
75
+ * Owned-product summary returned by the injected `getOwnedProductById` reader.
76
+ * Structural mirror of the inventory `productsService.getProductById` result,
77
+ * narrowed to the two fields the snapshot fallback reads.
78
+ */
79
+ export interface CatalogOwnedProductSummary {
80
+ name: string | null;
81
+ description: string | null;
82
+ }
83
+ /**
84
+ * Deployment-supplied options for the catalog booking-engine route module.
85
+ * Structural only — no deployment imports, no platform bindings. The three
86
+ * cross-package readers (`getProductContent`, `listAvailabilitySlots`,
87
+ * `getOwnedProductById`) are INJECTED so the package can host the slots +
88
+ * snapshot handlers without statically importing `@voyant-travel/inventory`
89
+ * or `@voyant-travel/operations` (both of which depend on catalog).
90
+ */
91
+ export interface CatalogBookingRouteModuleOptions {
92
+ /**
93
+ * The booking-engine lifecycle options (db, source/owned registries,
94
+ * hold-ttl, promotions, tax transforms). The deployment already builds
95
+ * these for `createCatalogBookingRoutes`.
96
+ */
97
+ booking: CatalogBookingRoutesOptions;
98
+ /**
99
+ * Resolve the process-local source-adapter registry for a request. Used by
100
+ * the order-cancel handler to dispatch to the registered adapter.
101
+ */
102
+ resolveRegistry(c: Context): SourceAdapterRegistry;
103
+ /**
104
+ * Read the resolved product content for a sourced product (slots path).
105
+ * Modelled on `@voyant-travel/inventory/service-content`'s
106
+ * `getProductContent`; structural so catalog doesn't import inventory.
107
+ */
108
+ getProductContent(db: AnyDrizzleDb, productId: string, scope: CatalogProductContentScope, ctx: CatalogProductContentReadContext): Promise<CatalogResolvedProductContent | null>;
109
+ /**
110
+ * Read the owned `availability_slots` rows for a product (owned slots path).
111
+ * The deployment owns the drizzle query against
112
+ * `@voyant-travel/operations`; this returns the already-mapped rows.
113
+ */
114
+ listAvailabilitySlots(db: AnyDrizzleDb, productId: string, todayIso: string): Promise<SlotRow[]>;
115
+ /**
116
+ * Read an owned product by id for the snapshot fallback. Structural mirror
117
+ * of inventory `productsService.getProductById`; returns `{ name,
118
+ * description } | null`.
119
+ */
120
+ getOwnedProductById(db: AnyDrizzleDb, productId: string): Promise<CatalogOwnedProductSummary | null>;
121
+ }
122
+ /**
123
+ * The slot row shape returned by both the sourced and owned slots paths.
124
+ * Date-bearing fields accept `Date | string` because the owned path forwards
125
+ * raw drizzle timestamp columns (serialized to ISO strings by `c.json`),
126
+ * while the sourced path projects ISO strings directly.
127
+ */
128
+ export interface SlotRow {
129
+ id: string;
130
+ dateLocal: string;
131
+ startsAt: string | Date;
132
+ endsAt: string | Date | null;
133
+ timezone: string;
134
+ status: string;
135
+ unlimited: boolean;
136
+ remainingPax: number | null;
137
+ initialPax: number | null;
138
+ nights: number | null;
139
+ days: number | null;
140
+ }
141
+ /**
142
+ * Admin-only order-management routes (relative paths; mount at
143
+ * `/v1/admin/catalog`). Surfaces snapshot rows cross-vertically and routes
144
+ * cancels back through the registered source adapter.
145
+ */
146
+ export declare function createCatalogBookingOrdersRoutes(options: CatalogBookingRouteModuleOptions): Hono;
147
+ /**
148
+ * Mount the full catalog booking-engine surface (both surfaces + admin
149
+ * orders) onto an absolute-path Hono app. Mirrors the operator's previous
150
+ * `mountCatalogBookingRoutes`, minus the cross-package snapshot/slots
151
+ * handlers that have to stay in the deployment (cycle).
152
+ */
153
+ export declare function mountCatalogBookingRoutes(hono: Hono, options: CatalogBookingRouteModuleOptions): void;
154
+ //# sourceMappingURL=operator-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operator-routes.d.ts","sourceRoot":"","sources":["../../src/booking-engine/operator-routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Catalog booking-engine route module — the full admin + public booking
3
+ * surface, owned by `@voyant-travel/catalog`.
4
+ *
5
+ * A deployment composes this and supplies two structural options:
6
+ * - `booking` — the `CatalogBookingRoutesOptions` (db / registries /
7
+ * hold-ttl / promotions / tax hooks) it already builds today, and
8
+ * - `resolveRegistry(c)` — pulls the process-local `SourceAdapterRegistry`
9
+ * off the request context (cancel needs it to dispatch to adapters).
10
+ *
11
+ * The module mounts the shared lifecycle from `./routes.js` on **two**
12
+ * surfaces and adds the admin-only order-management endpoints:
13
+ *
14
+ * POST /v1/{admin,public}/catalog/quote → quoteEntity
15
+ * POST /v1/{admin,public}/catalog/book → bookEntity
16
+ * PUT /v1/{admin,public}/catalog/drafts/:id → upsert booking draft
17
+ * GET /v1/{admin,public}/catalog/drafts/:id → read booking draft
18
+ * DELETE /v1/{admin,public}/catalog/drafts/:id → delete booking draft
19
+ * POST /v1/{admin,public}/catalog/holds/place → place hold
20
+ * POST /v1/{admin,public}/catalog/holds/release → release hold
21
+ * GET /v1/admin/catalog/orders → listOrders
22
+ * GET /v1/admin/catalog/orders/:id → getOrderById
23
+ * POST /v1/admin/catalog/orders/:id/cancel → cancelEntity
24
+ * GET /v1/{admin,public}/catalog/slots → availability slots
25
+ * GET /v1/admin/bookings/:id/catalog-snapshot → frozen catalog snapshot
26
+ *
27
+ * Auth posture comes from the deployment's `createApp` middleware chain —
28
+ * `/v1/admin/...` requires staff, `/v1/public/...` accepts the configured
29
+ * public actors. Per booking-journey-architecture §10 Phase B.
30
+ *
31
+ * The slots + catalog-snapshot handlers read across module boundaries
32
+ * (`@voyant-travel/inventory` / `@voyant-travel/operations`), both of which
33
+ * already depend on `@voyant-travel/catalog`. Statically importing them here
34
+ * would create an import cycle, so the cross-package reads are supplied by the
35
+ * deployment as INJECTED option functions — the package never imports those
36
+ * modules, it only calls the readers the deployment hands it.
37
+ */
38
+ import { and, eq } from "drizzle-orm";
39
+ import { Hono } from "hono";
40
+ import { bookingCatalogSnapshotTable, catalogSourcedEntriesTable } from "../schema.js";
41
+ import { readSourcedEntry } from "../services/sourced-entry-service.js";
42
+ import { cancelEntity } from "./cancel.js";
43
+ import { BookingEngineError, NO_ADAPTER_REGISTERED, NO_HANDLER_REGISTERED, ORDER_ALREADY_CANCELLED, ORDER_NOT_FOUND, QUOTE_EXPIRED, QUOTE_MISMATCH, QUOTE_NOT_FOUND, RESERVE_FAILED, } from "./errors.js";
44
+ import { getOrderById, listOrders } from "./orders.js";
45
+ import { createCatalogBookingRoutes } from "./routes.js";
46
+ function getDb(options, c) {
47
+ return options.booking.resolveDb(c);
48
+ }
49
+ /**
50
+ * Admin-only order-management routes (relative paths; mount at
51
+ * `/v1/admin/catalog`). Surfaces snapshot rows cross-vertically and routes
52
+ * cancels back through the registered source adapter.
53
+ */
54
+ export function createCatalogBookingOrdersRoutes(options) {
55
+ const hono = new Hono();
56
+ hono.get("/orders", async (c) => handleListOrders(c, options));
57
+ hono.get("/orders/:id", async (c) => handleGetOrder(c, options));
58
+ hono.post("/orders/:id/cancel", async (c) => handleCancel(c, options));
59
+ return hono;
60
+ }
61
+ /**
62
+ * Mount the full catalog booking-engine surface (both surfaces + admin
63
+ * orders) onto an absolute-path Hono app. Mirrors the operator's previous
64
+ * `mountCatalogBookingRoutes`, minus the cross-package snapshot/slots
65
+ * handlers that have to stay in the deployment (cycle).
66
+ */
67
+ export function mountCatalogBookingRoutes(hono, options) {
68
+ for (const prefix of ["/v1/admin/catalog", "/v1/public/catalog"]) {
69
+ hono.route(prefix, createCatalogBookingRoutes(options.booking));
70
+ }
71
+ // Admin-only — order management (list / get / cancel).
72
+ hono.route("/v1/admin/catalog", createCatalogBookingOrdersRoutes(options));
73
+ // List available departures / slots for a product. Drives the
74
+ // storefront's departure-select on the product detail page —
75
+ // customers pick from real available options, not a free-form
76
+ // calendar (per booking-journey-architecture §10).
77
+ for (const prefix of ["/v1/admin/catalog", "/v1/public/catalog"]) {
78
+ hono.get(`${prefix}/slots`, async (c) => handleListSlots(c, options));
79
+ }
80
+ // Admin-only — read the catalog snapshot tied to a booking.
81
+ // Backs the BookingCatalogSourceCard on the booking detail page;
82
+ // surfaces the frozen entity reference + pricing + (optionally) the
83
+ // captured content payload so operators can see exactly what the
84
+ // customer was quoted at booking time.
85
+ hono.get("/v1/admin/bookings/:id/catalog-snapshot", async (c) => handleGetBookingSnapshot(c, options));
86
+ }
87
+ // ─────────────────────────────────────────────────────────────────
88
+ // Handlers
89
+ // ─────────────────────────────────────────────────────────────────
90
+ async function handleListOrders(c, options) {
91
+ const db = getDb(options, c);
92
+ const url = new URL(c.req.url);
93
+ const bookingId = url.searchParams.get("bookingId") ?? undefined;
94
+ const entityModule = url.searchParams.get("entityModule") ?? undefined;
95
+ const sourceKindsParam = url.searchParams.get("sourceKinds");
96
+ const sourceKinds = sourceKindsParam ? sourceKindsParam.split(",") : undefined;
97
+ const limit = Number.parseInt(url.searchParams.get("limit") ?? "50", 10);
98
+ const offset = Number.parseInt(url.searchParams.get("offset") ?? "0", 10);
99
+ const result = await listOrders(db, {
100
+ bookingId,
101
+ entityModule,
102
+ sourceKinds,
103
+ limit: Number.isFinite(limit) ? limit : 50,
104
+ offset: Number.isFinite(offset) ? offset : 0,
105
+ });
106
+ return c.json({ rows: result.rows });
107
+ }
108
+ async function handleGetOrder(c, options) {
109
+ const db = getDb(options, c);
110
+ const id = c.req.param("id");
111
+ if (!id)
112
+ return c.json({ error: "id is required" }, 400);
113
+ const row = await getOrderById(db, id);
114
+ if (!row)
115
+ return c.json({ error: "order not found" }, 404);
116
+ return c.json(row);
117
+ }
118
+ async function handleCancel(c, options) {
119
+ let body;
120
+ try {
121
+ body = await c.req.json();
122
+ }
123
+ catch {
124
+ body = {};
125
+ }
126
+ if (!body.bookingId || !body.entityModule || !body.entityId) {
127
+ return c.json({ error: "bookingId, entityModule, and entityId are required in the body" }, 400);
128
+ }
129
+ const db = getDb(options, c);
130
+ const registry = options.resolveRegistry(c);
131
+ const correlationId = c.req.header("x-request-id") ?? cryptoRandom();
132
+ try {
133
+ const result = await cancelEntity(db, { registry }, {
134
+ bookingId: body.bookingId,
135
+ entityModule: body.entityModule,
136
+ entityId: body.entityId,
137
+ reason: body.reason,
138
+ adapterContext: { connection_id: "engine", correlation_id: correlationId },
139
+ });
140
+ return c.json(result);
141
+ }
142
+ catch (err) {
143
+ return errorResponse(c, err);
144
+ }
145
+ }
146
+ async function handleListSlots(c, options) {
147
+ const url = new URL(c.req.url);
148
+ const entityModule = url.searchParams.get("entityModule");
149
+ const entityId = url.searchParams.get("entityId");
150
+ if (!entityModule || !entityId) {
151
+ return c.json({ error: "entityModule and entityId are required" }, 400);
152
+ }
153
+ // Cruises + accommodations have vertical-specific scheduling
154
+ // (sailings, rate plans) surfaced by the detail page directly off
155
+ // their content payloads. This endpoint only serves products.
156
+ if (entityModule !== "products") {
157
+ return c.json({ rows: [] });
158
+ }
159
+ const db = getDb(options, c);
160
+ // Sourced products carry their schedule in the sourced-content
161
+ // payload — the upstream's `getContent` is the source of truth, not
162
+ // any owned `availability_slots` row. Owned products keep using the
163
+ // owned table since `buildOwnedProductContent` doesn't project
164
+ // availability_slots into ProductContent.departures.
165
+ const sourcedEntry = await readSourcedEntry(db, "products", entityId);
166
+ if (sourcedEntry) {
167
+ const registry = options.resolveRegistry(c);
168
+ const acceptHeader = c.req.header("accept-language") ?? "";
169
+ const preferredLocales = acceptHeader
170
+ .split(",")
171
+ .map((s) => s.split(";")[0]?.trim())
172
+ .filter((s) => Boolean(s));
173
+ const resolved = await options.getProductContent(db, entityId, { preferredLocales: preferredLocales.length > 0 ? preferredLocales : ["en-GB"] }, { registry, forceFresh: true });
174
+ const today = new Date().toISOString().slice(0, 10);
175
+ const rows = (resolved?.content.departures ?? [])
176
+ .filter((d) => {
177
+ if (d.status === "sold_out" || d.status === "closed")
178
+ return false;
179
+ return d.starts_at.slice(0, 10) >= today;
180
+ })
181
+ .slice(0, 60)
182
+ .map((d) => ({
183
+ id: d.id,
184
+ dateLocal: d.starts_at.slice(0, 10),
185
+ startsAt: d.starts_at,
186
+ endsAt: d.ends_at ?? null,
187
+ timezone: "UTC",
188
+ status: d.status ?? "open",
189
+ unlimited: d.capacity == null && d.remaining == null,
190
+ remainingPax: d.remaining ?? null,
191
+ initialPax: d.capacity ?? null,
192
+ nights: null,
193
+ days: null,
194
+ }));
195
+ return c.json({ rows });
196
+ }
197
+ const today = new Date().toISOString().slice(0, 10);
198
+ const rows = await options.listAvailabilitySlots(db, entityId, today);
199
+ return c.json({ rows });
200
+ }
201
+ /**
202
+ * GET /v1/admin/bookings/:id/catalog-snapshot
203
+ *
204
+ * Returns the `booking_catalog_snapshot` row for this booking — the
205
+ * frozen view of what the customer actually purchased: which entity
206
+ * (product / cruise / accommodations), which source (owned / Bokun / Mews),
207
+ * the quoted pricing breakdown, and the captured content payload.
208
+ *
209
+ * The response is **enriched server-side** with operator-friendly
210
+ * resolved fields so the admin UI doesn't have to chase ids:
211
+ * - `resolved.entity.title` — human title from the sourced
212
+ * projection (`name`/`title`) or the owned product's `name`.
213
+ * - `resolved.entity.description` — short description when present.
214
+ * - `resolved.entity.supplierName` — supplier label when present.
215
+ * - `resolved.source.label` — friendly source name.
216
+ *
217
+ * Used by the booking detail page's "Catalog source" card so
218
+ * operators see "Demo · Reykjavík Northern Lights Hunt" instead of
219
+ * `cdmi_01kqp28138f69btmp1n15yjj7r`. Returns 404 when no snapshot
220
+ * exists (legacy bookings).
221
+ */
222
+ async function handleGetBookingSnapshot(c, options) {
223
+ const bookingId = c.req.param("id");
224
+ if (!bookingId)
225
+ return c.json({ error: "id is required" }, 400);
226
+ const db = getDb(options, c);
227
+ const [snapshot] = await db
228
+ .select()
229
+ .from(bookingCatalogSnapshotTable)
230
+ .where(eq(bookingCatalogSnapshotTable.booking_id, bookingId))
231
+ .limit(1);
232
+ if (!snapshot) {
233
+ return c.json({ error: "snapshot_not_found" }, 404);
234
+ }
235
+ const resolved = await resolveSnapshotForAdmin(db, options, {
236
+ entity_module: snapshot.entity_module,
237
+ entity_id: snapshot.entity_id,
238
+ source_kind: snapshot.source_kind,
239
+ source_provider: snapshot.source_provider,
240
+ frozen_payload: (snapshot.frozen_payload ?? {}),
241
+ });
242
+ return c.json({ data: { ...snapshot, resolved } });
243
+ }
244
+ /**
245
+ * Resolve admin-friendly labels for a booking_catalog_snapshot row.
246
+ * Tries the sourced-entry projection first (covers demo, Bokun, etc.),
247
+ * falls back to owned products. Returns null fields rather than
248
+ * throwing when sources are missing — the admin UI treats nulls as
249
+ * "fall back to id".
250
+ */
251
+ async function resolveSnapshotForAdmin(db, options, snapshot) {
252
+ const entity = {
253
+ title: null,
254
+ description: null,
255
+ supplierName: null,
256
+ imageUrl: null,
257
+ };
258
+ // Attempt 1: sourced_entries projection. Covers demo + every
259
+ // upstream provider that registers via the sourced-entry write path.
260
+ try {
261
+ const [sourced] = await db
262
+ .select({ projection: catalogSourcedEntriesTable.projection })
263
+ .from(catalogSourcedEntriesTable)
264
+ .where(and(eq(catalogSourcedEntriesTable.entity_module, snapshot.entity_module), eq(catalogSourcedEntriesTable.entity_id, snapshot.entity_id)))
265
+ .limit(1);
266
+ if (sourced?.projection) {
267
+ const p = sourced.projection;
268
+ entity.title = pickString(p.name, p.title);
269
+ entity.description = pickString(p.description, p.summary);
270
+ entity.supplierName = pickString(p.supplierId, p.supplier_name, p.supplierName);
271
+ entity.imageUrl = pickString(p.heroImageUrl, p.image_url, p.imageUrl);
272
+ }
273
+ }
274
+ catch {
275
+ // ignore, fall through
276
+ }
277
+ // Attempt 2: owned products row.
278
+ if (!entity.title && snapshot.entity_module === "products") {
279
+ try {
280
+ const product = await options.getOwnedProductById(db, snapshot.entity_id);
281
+ if (product) {
282
+ entity.title = product.name;
283
+ entity.description = product.description;
284
+ }
285
+ }
286
+ catch {
287
+ // ignore
288
+ }
289
+ }
290
+ // Attempt 3: pull from the snapshot's frozen upstream payload as
291
+ // last resort (sourced quotes capture the upstream object inline).
292
+ if (!entity.title) {
293
+ const upstream = snapshot.frozen_payload?.quote
294
+ ?.upstream_payload;
295
+ if (upstream) {
296
+ entity.title = pickString(upstream.name, upstream.title);
297
+ entity.description = pickString(upstream.description, upstream.summary);
298
+ }
299
+ }
300
+ const source = {
301
+ label: friendlySourceLabel(snapshot.source_kind),
302
+ providerLabel: snapshot.source_provider,
303
+ };
304
+ return { entity, source };
305
+ }
306
+ function pickString(...candidates) {
307
+ for (const c of candidates) {
308
+ if (typeof c === "string" && c.trim().length > 0)
309
+ return c;
310
+ }
311
+ return null;
312
+ }
313
+ /**
314
+ * Map raw `source_kind` strings to the labels operators recognise.
315
+ * "demo" → "Demo Catalog", "owned" → "Owned (this operator)", etc.
316
+ * Anything we don't recognise comes back title-cased.
317
+ */
318
+ function friendlySourceLabel(sourceKind) {
319
+ const map = {
320
+ demo: "Demo Catalog",
321
+ owned: "Owned (this operator)",
322
+ bokun: "Bókun",
323
+ mews: "Mews",
324
+ fareharbor: "FareHarbor",
325
+ rezdy: "Rezdy",
326
+ };
327
+ return map[sourceKind] ?? sourceKind.replace(/^./, (c) => c.toUpperCase());
328
+ }
329
+ // ─────────────────────────────────────────────────────────────────
330
+ // Helpers
331
+ // ─────────────────────────────────────────────────────────────────
332
+ function errorResponse(c, err) {
333
+ if (err instanceof BookingEngineError) {
334
+ const status = statusForCode(err.code);
335
+ return c.json({ error: err.message, code: err.code, context: err.context }, status);
336
+ }
337
+ const message = err instanceof Error ? err.message : String(err);
338
+ return c.json({ error: message }, 500);
339
+ }
340
+ function statusForCode(code) {
341
+ switch (code) {
342
+ case NO_ADAPTER_REGISTERED:
343
+ case NO_HANDLER_REGISTERED:
344
+ return 503;
345
+ case QUOTE_NOT_FOUND:
346
+ case ORDER_NOT_FOUND:
347
+ return 404;
348
+ case QUOTE_EXPIRED:
349
+ case QUOTE_MISMATCH:
350
+ case ORDER_ALREADY_CANCELLED:
351
+ return 409;
352
+ case RESERVE_FAILED:
353
+ return 502;
354
+ default:
355
+ return 500;
356
+ }
357
+ }
358
+ function cryptoRandom() {
359
+ if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.randomUUID) {
360
+ return globalThis.crypto.randomUUID();
361
+ }
362
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
363
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=operator-routes.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operator-routes.test.d.ts","sourceRoot":"","sources":["../../src/booking-engine/operator-routes.test.ts"],"names":[],"mappings":""}