@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.
- package/dist/booking-engine/index.d.ts +1 -0
- package/dist/booking-engine/index.d.ts.map +1 -1
- package/dist/booking-engine/index.js +1 -0
- package/dist/booking-engine/operator-routes.d.ts +154 -0
- package/dist/booking-engine/operator-routes.d.ts.map +1 -0
- package/dist/booking-engine/operator-routes.js +363 -0
- package/dist/booking-engine/operator-routes.test.d.ts +2 -0
- package/dist/booking-engine/operator-routes.test.d.ts.map +1 -0
- package/dist/booking-engine/operator-routes.test.js +316 -0
- package/dist/embeddings/gemini.test.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/offers/operator-routes.d.ts +111 -0
- package/dist/offers/operator-routes.d.ts.map +1 -0
- package/dist/offers/operator-routes.js +721 -0
- package/dist/offers/operator-routes.test.d.ts +2 -0
- package/dist/offers/operator-routes.test.d.ts.map +1 -0
- package/dist/offers/operator-routes.test.js +144 -0
- package/package.json +8 -3
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,UAAU,GACX,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,EACjC,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,mBAAmB,EACnB,cAAc,EACd,yBAAyB,EACzB,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,qBAAqB,EAC1B,iBAAiB,EACjB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,KAAK,eAAe,EACpB,eAAe,EACf,0BAA0B,GAC3B,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,wBAAwB,EACxB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,wBAAwB,GAC9B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,kBAAkB,GACnB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,qBAAqB,EACrB,qBAAqB,EACrB,wBAAwB,EACxB,6BAA6B,EAC7B,uBAAuB,EACvB,eAAe,EACf,aAAa,EACb,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,4BAA4B,EAC5B,+BAA+B,GAChC,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,gCAAgC,EACrC,KAAK,0BAA0B,EAC/B,KAAK,gCAAgC,EACrC,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,6BAA6B,EAClC,gCAAgC,EAChC,yBAAyB,EACzB,KAAK,OAAO,GACb,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,iCAAiC,EACjC,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,iBAAiB,EACjB,KAAK,mBAAmB,EACxB,KAAK,2BAA2B,EAChC,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EACV,YAAY,EACZ,UAAU,EACV,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,oBAAoB,EACpB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,WAAW,GACZ,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,2BAA2B,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,KAAK,iCAAiC,EACtC,KAAK,sBAAsB,EAC3B,KAAK,gCAAgC,EACrC,KAAK,4BAA4B,EACjC,KAAK,+BAA+B,EACpC,KAAK,uBAAuB,EAC5B,KAAK,gCAAgC,EACrC,KAAK,2BAA2B,EAChC,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,6BAA6B,EAClC,KAAK,uBAAuB,EAC5B,KAAK,iCAAiC,EACtC,KAAK,2BAA2B,EAChC,8BAA8B,EAC9B,0BAA0B,GAC3B,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,sBAAsB,EAC3B,8BAA8B,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,GAC7B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,WAAW,GACZ,MAAM,WAAW,CAAA"}
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"operator-routes.test.d.ts","sourceRoot":"","sources":["../../src/booking-engine/operator-routes.test.ts"],"names":[],"mappings":""}
|