@voyant-travel/catalog 0.117.2
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/LICENSE +201 -0
- package/README.md +190 -0
- package/dist/adapter/booking-forwarding.d.ts +2 -0
- package/dist/adapter/booking-forwarding.d.ts.map +1 -0
- package/dist/adapter/booking-forwarding.js +1 -0
- package/dist/adapter/channel-push-contracts.d.ts +2 -0
- package/dist/adapter/channel-push-contracts.d.ts.map +1 -0
- package/dist/adapter/channel-push-contracts.js +1 -0
- package/dist/adapter/contract.d.ts +2 -0
- package/dist/adapter/contract.d.ts.map +1 -0
- package/dist/adapter/contract.js +1 -0
- package/dist/adapter/contract.test.d.ts +2 -0
- package/dist/adapter/contract.test.d.ts.map +1 -0
- package/dist/adapter/contract.test.js +390 -0
- package/dist/adapter/provider-contracts.d.ts +2 -0
- package/dist/adapter/provider-contracts.d.ts.map +1 -0
- package/dist/adapter/provider-contracts.js +1 -0
- package/dist/adapter/provider-contracts.test.d.ts +2 -0
- package/dist/adapter/provider-contracts.test.d.ts.map +1 -0
- package/dist/adapter/provider-contracts.test.js +206 -0
- package/dist/adapter/schemas.d.ts +2 -0
- package/dist/adapter/schemas.d.ts.map +1 -0
- package/dist/adapter/schemas.js +1 -0
- package/dist/adapter/schemas.test.d.ts +2 -0
- package/dist/adapter/schemas.test.d.ts.map +1 -0
- package/dist/adapter/schemas.test.js +344 -0
- package/dist/booking-engine/book.d.ts +124 -0
- package/dist/booking-engine/book.d.ts.map +1 -0
- package/dist/booking-engine/book.js +311 -0
- package/dist/booking-engine/cancel.d.ts +40 -0
- package/dist/booking-engine/cancel.d.ts.map +1 -0
- package/dist/booking-engine/cancel.js +56 -0
- package/dist/booking-engine/checkout-finalize.d.ts +146 -0
- package/dist/booking-engine/checkout-finalize.d.ts.map +1 -0
- package/dist/booking-engine/checkout-finalize.js +132 -0
- package/dist/booking-engine/contracts.d.ts +9 -0
- package/dist/booking-engine/contracts.d.ts.map +1 -0
- package/dist/booking-engine/contracts.js +8 -0
- package/dist/booking-engine/contracts.test.d.ts +2 -0
- package/dist/booking-engine/contracts.test.d.ts.map +1 -0
- package/dist/booking-engine/contracts.test.js +116 -0
- package/dist/booking-engine/draft-shape.d.ts +10 -0
- package/dist/booking-engine/draft-shape.d.ts.map +1 -0
- package/dist/booking-engine/draft-shape.js +9 -0
- package/dist/booking-engine/draft-shape.test.d.ts +2 -0
- package/dist/booking-engine/draft-shape.test.d.ts.map +1 -0
- package/dist/booking-engine/draft-shape.test.js +74 -0
- package/dist/booking-engine/drafts-schema.d.ts +302 -0
- package/dist/booking-engine/drafts-schema.d.ts.map +1 -0
- package/dist/booking-engine/drafts-schema.js +53 -0
- package/dist/booking-engine/drafts-service.d.ts +41 -0
- package/dist/booking-engine/drafts-service.d.ts.map +1 -0
- package/dist/booking-engine/drafts-service.js +108 -0
- package/dist/booking-engine/errors.d.ts +81 -0
- package/dist/booking-engine/errors.d.ts.map +1 -0
- package/dist/booking-engine/errors.js +113 -0
- package/dist/booking-engine/index.d.ts +36 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +34 -0
- package/dist/booking-engine/orders.d.ts +41 -0
- package/dist/booking-engine/orders.d.ts.map +1 -0
- package/dist/booking-engine/orders.js +49 -0
- package/dist/booking-engine/owned-handler.d.ts +166 -0
- package/dist/booking-engine/owned-handler.d.ts.map +1 -0
- package/dist/booking-engine/owned-handler.js +50 -0
- package/dist/booking-engine/owned-handler.test.d.ts +2 -0
- package/dist/booking-engine/owned-handler.test.d.ts.map +1 -0
- package/dist/booking-engine/owned-handler.test.js +63 -0
- package/dist/booking-engine/promotions-contract.d.ts +8 -0
- package/dist/booking-engine/promotions-contract.d.ts.map +1 -0
- package/dist/booking-engine/promotions-contract.js +7 -0
- package/dist/booking-engine/quote-enricher.test.d.ts +12 -0
- package/dist/booking-engine/quote-enricher.test.d.ts.map +1 -0
- package/dist/booking-engine/quote-enricher.test.js +138 -0
- package/dist/booking-engine/quote.d.ts +163 -0
- package/dist/booking-engine/quote.d.ts.map +1 -0
- package/dist/booking-engine/quote.js +259 -0
- package/dist/booking-engine/registry.d.ts +85 -0
- package/dist/booking-engine/registry.d.ts.map +1 -0
- package/dist/booking-engine/registry.js +118 -0
- package/dist/booking-engine/registry.test.d.ts +2 -0
- package/dist/booking-engine/registry.test.d.ts.map +1 -0
- package/dist/booking-engine/registry.test.js +132 -0
- package/dist/booking-engine/routes-contracts.d.ts +169 -0
- package/dist/booking-engine/routes-contracts.d.ts.map +1 -0
- package/dist/booking-engine/routes-contracts.js +63 -0
- package/dist/booking-engine/routes.d.ts +7 -0
- package/dist/booking-engine/routes.d.ts.map +1 -0
- package/dist/booking-engine/routes.js +443 -0
- package/dist/booking-engine/routes.test.d.ts +2 -0
- package/dist/booking-engine/routes.test.d.ts.map +1 -0
- package/dist/booking-engine/routes.test.js +304 -0
- package/dist/booking-engine/schema.d.ts +455 -0
- package/dist/booking-engine/schema.d.ts.map +1 -0
- package/dist/booking-engine/schema.js +75 -0
- package/dist/booking-engine/snapshot-content.d.ts +120 -0
- package/dist/booking-engine/snapshot-content.d.ts.map +1 -0
- package/dist/booking-engine/snapshot-content.js +110 -0
- package/dist/booking-engine/snapshot-content.test.d.ts +2 -0
- package/dist/booking-engine/snapshot-content.test.d.ts.map +1 -0
- package/dist/booking-engine/snapshot-content.test.js +213 -0
- package/dist/booking-engine/sync.d.ts +136 -0
- package/dist/booking-engine/sync.d.ts.map +1 -0
- package/dist/booking-engine/sync.js +177 -0
- package/dist/booking-engine/sync.test.d.ts +2 -0
- package/dist/booking-engine/sync.test.d.ts.map +1 -0
- package/dist/booking-engine/sync.test.js +377 -0
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +1 -0
- package/dist/contract.test.d.ts +2 -0
- package/dist/contract.test.d.ts.map +1 -0
- package/dist/contract.test.js +107 -0
- package/dist/drift/events.d.ts +2 -0
- package/dist/drift/events.d.ts.map +1 -0
- package/dist/drift/events.js +1 -0
- package/dist/drift/events.test.d.ts +2 -0
- package/dist/drift/events.test.d.ts.map +1 -0
- package/dist/drift/events.test.js +100 -0
- package/dist/embeddings/contract.d.ts +85 -0
- package/dist/embeddings/contract.d.ts.map +1 -0
- package/dist/embeddings/contract.js +42 -0
- package/dist/embeddings/contract.test.d.ts +2 -0
- package/dist/embeddings/contract.test.d.ts.map +1 -0
- package/dist/embeddings/contract.test.js +30 -0
- package/dist/embeddings/gemini.d.ts +110 -0
- package/dist/embeddings/gemini.d.ts.map +1 -0
- package/dist/embeddings/gemini.js +118 -0
- package/dist/embeddings/gemini.test.d.ts +2 -0
- package/dist/embeddings/gemini.test.d.ts.map +1 -0
- package/dist/embeddings/gemini.test.js +132 -0
- package/dist/embeddings/model-registry.d.ts +62 -0
- package/dist/embeddings/model-registry.d.ts.map +1 -0
- package/dist/embeddings/model-registry.js +78 -0
- package/dist/embeddings/model-registry.test.d.ts +2 -0
- package/dist/embeddings/model-registry.test.d.ts.map +1 -0
- package/dist/embeddings/model-registry.test.js +81 -0
- package/dist/embeddings/openai.d.ts +81 -0
- package/dist/embeddings/openai.d.ts.map +1 -0
- package/dist/embeddings/openai.js +123 -0
- package/dist/embeddings/openai.test.d.ts +2 -0
- package/dist/embeddings/openai.test.d.ts.map +1 -0
- package/dist/embeddings/openai.test.js +164 -0
- package/dist/events/taxonomy.d.ts +158 -0
- package/dist/events/taxonomy.d.ts.map +1 -0
- package/dist/events/taxonomy.js +99 -0
- package/dist/events/taxonomy.test.d.ts +2 -0
- package/dist/events/taxonomy.test.d.ts.map +1 -0
- package/dist/events/taxonomy.test.js +48 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/indexer/contract.d.ts +203 -0
- package/dist/indexer/contract.d.ts.map +1 -0
- package/dist/indexer/contract.js +16 -0
- package/dist/indexer/typesense-search-query.d.ts +31 -0
- package/dist/indexer/typesense-search-query.d.ts.map +1 -0
- package/dist/indexer/typesense-search-query.js +185 -0
- package/dist/indexer/typesense.d.ts +105 -0
- package/dist/indexer/typesense.d.ts.map +1 -0
- package/dist/indexer/typesense.js +394 -0
- package/dist/indexer/typesense.test.d.ts +2 -0
- package/dist/indexer/typesense.test.d.ts.map +1 -0
- package/dist/indexer/typesense.test.js +253 -0
- package/dist/overlay/resolver.d.ts +101 -0
- package/dist/overlay/resolver.d.ts.map +1 -0
- package/dist/overlay/resolver.js +167 -0
- package/dist/overlay/resolver.test.d.ts +2 -0
- package/dist/overlay/resolver.test.d.ts.map +1 -0
- package/dist/overlay/resolver.test.js +179 -0
- package/dist/overlay/schema.d.ts +266 -0
- package/dist/overlay/schema.d.ts.map +1 -0
- package/dist/overlay/schema.js +71 -0
- package/dist/provenance.d.ts +2 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +1 -0
- package/dist/schema-sourced-entries.d.ts +344 -0
- package/dist/schema-sourced-entries.d.ts.map +1 -0
- package/dist/schema-sourced-entries.js +75 -0
- package/dist/schema.d.ts +21 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +20 -0
- package/dist/search/federate.d.ts +58 -0
- package/dist/search/federate.d.ts.map +1 -0
- package/dist/search/federate.js +103 -0
- package/dist/search/federate.test.d.ts +2 -0
- package/dist/search/federate.test.d.ts.map +1 -0
- package/dist/search/federate.test.js +146 -0
- package/dist/search/rerank.d.ts +77 -0
- package/dist/search/rerank.d.ts.map +1 -0
- package/dist/search/rerank.js +68 -0
- package/dist/search/rerank.test.d.ts +2 -0
- package/dist/search/rerank.test.d.ts.map +1 -0
- package/dist/search/rerank.test.js +60 -0
- package/dist/search/routes.d.ts +144 -0
- package/dist/search/routes.d.ts.map +1 -0
- package/dist/search/routes.js +288 -0
- package/dist/search/routes.test.d.ts +2 -0
- package/dist/search/routes.test.d.ts.map +1 -0
- package/dist/search/routes.test.js +322 -0
- package/dist/search/semantic.d.ts +63 -0
- package/dist/search/semantic.d.ts.map +1 -0
- package/dist/search/semantic.js +75 -0
- package/dist/search/semantic.test.d.ts +2 -0
- package/dist/search/semantic.test.d.ts.map +1 -0
- package/dist/search/semantic.test.js +143 -0
- package/dist/services/build-indexer-document.test.d.ts +2 -0
- package/dist/services/build-indexer-document.test.d.ts.map +1 -0
- package/dist/services/build-indexer-document.test.js +102 -0
- package/dist/services/content-service.d.ts +125 -0
- package/dist/services/content-service.d.ts.map +1 -0
- package/dist/services/content-service.js +139 -0
- package/dist/services/content-service.test.d.ts +2 -0
- package/dist/services/content-service.test.d.ts.map +1 -0
- package/dist/services/content-service.test.js +322 -0
- package/dist/services/indexer-service.d.ts +109 -0
- package/dist/services/indexer-service.d.ts.map +1 -0
- package/dist/services/indexer-service.js +123 -0
- package/dist/services/indexer-service.test.d.ts +2 -0
- package/dist/services/indexer-service.test.d.ts.map +1 -0
- package/dist/services/indexer-service.test.js +176 -0
- package/dist/services/overlay-service.d.ts +108 -0
- package/dist/services/overlay-service.d.ts.map +1 -0
- package/dist/services/overlay-service.js +211 -0
- package/dist/services/overlay-service.test.d.ts +2 -0
- package/dist/services/overlay-service.test.d.ts.map +1 -0
- package/dist/services/overlay-service.test.js +79 -0
- package/dist/services/snapshot-builder.test.d.ts +2 -0
- package/dist/services/snapshot-builder.test.d.ts.map +1 -0
- package/dist/services/snapshot-builder.test.js +93 -0
- package/dist/services/snapshot-service.d.ts +78 -0
- package/dist/services/snapshot-service.d.ts.map +1 -0
- package/dist/services/snapshot-service.js +165 -0
- package/dist/services/sourced-entry-service.d.ts +142 -0
- package/dist/services/sourced-entry-service.d.ts.map +1 -0
- package/dist/services/sourced-entry-service.js +203 -0
- package/dist/services/sourced-entry-service.test.d.ts +10 -0
- package/dist/services/sourced-entry-service.test.d.ts.map +1 -0
- package/dist/services/sourced-entry-service.test.js +66 -0
- package/dist/snapshot/schema.d.ts +362 -0
- package/dist/snapshot/schema.d.ts.map +1 -0
- package/dist/snapshot/schema.js +102 -0
- package/package.json +210 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { handleApiError, parseJsonBody, RequestValidationError } from "@voyant-travel/hono";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { readSourcedEntry } from "../services/sourced-entry-service.js";
|
|
4
|
+
import { bookEntity } from "./book.js";
|
|
5
|
+
import { bookResponseV1, quoteResponseV1, } from "./contracts.js";
|
|
6
|
+
import { createBookingDraft, DEFAULT_DRAFT_TTL_MS, deleteBookingDraft, getBookingDraft, markDraftConsumed, updateBookingDraft, } from "./drafts-service.js";
|
|
7
|
+
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";
|
|
8
|
+
import { OWNED_SOURCE_KIND } from "./owned-handler.js";
|
|
9
|
+
import { quoteEntity } from "./quote.js";
|
|
10
|
+
import { bookBodySchema, draftBodySchema, holdPlaceBodySchema, holdReleaseBodySchema, quoteBodySchema, } from "./routes-contracts.js";
|
|
11
|
+
const DEFAULT_HOLD_TTL_MS = 30 * 60 * 1000;
|
|
12
|
+
export function createCatalogBookingRoutes(options) {
|
|
13
|
+
return new Hono()
|
|
14
|
+
.post("/quote", async (c) => handleQuote(c, options))
|
|
15
|
+
.post("/book", async (c) => handleBook(c, options))
|
|
16
|
+
.put("/drafts/:id", async (c) => handleDraftPut(c, options))
|
|
17
|
+
.get("/drafts/:id", async (c) => handleDraftGet(c, options))
|
|
18
|
+
.delete("/drafts/:id", async (c) => handleDraftDelete(c, options))
|
|
19
|
+
.post("/holds/place", async (c) => handleHoldPlace(c, options))
|
|
20
|
+
.post("/holds/release", async (c) => handleHoldRelease(c, options));
|
|
21
|
+
}
|
|
22
|
+
export function createCatalogBookingHonoModule(options) {
|
|
23
|
+
return {
|
|
24
|
+
module: { name: "catalog" },
|
|
25
|
+
adminRoutes: createCatalogBookingRoutes(options),
|
|
26
|
+
publicRoutes: createCatalogBookingRoutes(options),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function handleQuote(c, options) {
|
|
30
|
+
const body = await parseJsonBody(c, quoteBodySchema);
|
|
31
|
+
const db = options.resolveDb(c);
|
|
32
|
+
const provenance = body.sourceKind
|
|
33
|
+
? {
|
|
34
|
+
sourceKind: body.sourceKind,
|
|
35
|
+
sourceProvider: body.sourceProvider,
|
|
36
|
+
sourceConnectionId: body.sourceConnectionId,
|
|
37
|
+
sourceRef: body.sourceRef,
|
|
38
|
+
}
|
|
39
|
+
: await resolveEntityProvenance(c, options, db, body.entityModule, body.entityId);
|
|
40
|
+
const correlationId = resolveCorrelationId(c, options);
|
|
41
|
+
const adapterContext = resolveAdapterContext(c, options, {
|
|
42
|
+
db,
|
|
43
|
+
operation: "quote",
|
|
44
|
+
entityModule: body.entityModule,
|
|
45
|
+
entityId: body.entityId,
|
|
46
|
+
sourceKind: provenance.sourceKind,
|
|
47
|
+
sourceConnectionId: provenance.sourceConnectionId,
|
|
48
|
+
correlationId,
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
const result = await quoteEntity(db, {
|
|
52
|
+
registry: options.resolveSourceRegistry(c),
|
|
53
|
+
ownedHandlers: options.resolveOwnedHandlers?.(c),
|
|
54
|
+
contentEnricher: options.contentEnricher,
|
|
55
|
+
onEnricherError: options.onContentEnricherError,
|
|
56
|
+
evaluatePromotions: options.resolveEvaluatePromotions?.({ c, db }),
|
|
57
|
+
}, {
|
|
58
|
+
entityModule: body.entityModule,
|
|
59
|
+
entityId: body.entityId,
|
|
60
|
+
sourceKind: provenance.sourceKind,
|
|
61
|
+
sourceProvider: provenance.sourceProvider,
|
|
62
|
+
sourceConnectionId: provenance.sourceConnectionId,
|
|
63
|
+
sourceRef: provenance.sourceRef,
|
|
64
|
+
scope: {
|
|
65
|
+
locale: body.scope?.locale ?? "en-GB",
|
|
66
|
+
audience: body.scope?.audience ?? defaultAudienceForPath(c),
|
|
67
|
+
market: body.scope?.market ?? "default",
|
|
68
|
+
currency: body.scope?.currency,
|
|
69
|
+
},
|
|
70
|
+
parameters: engineParametersFromDraft(body.parameters, body.draft),
|
|
71
|
+
ttlMs: body.ttlMs,
|
|
72
|
+
adapterContext,
|
|
73
|
+
});
|
|
74
|
+
const transformed = (await options.transformQuoteResult?.({ c, db, request: body, provenance, result })) ?? result;
|
|
75
|
+
return c.json(serializeQuoteResult(transformed));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
return bookingEngineErrorResponse(c, err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function handleBook(c, options) {
|
|
82
|
+
const body = await parseJsonBody(c, bookBodySchema);
|
|
83
|
+
const db = options.resolveDb(c);
|
|
84
|
+
const correlationId = resolveCorrelationId(c, options);
|
|
85
|
+
let quoteId = body.quoteId;
|
|
86
|
+
let draftPayload;
|
|
87
|
+
if (!quoteId && body.draftId) {
|
|
88
|
+
const draft = await getBookingDraft(db, body.draftId);
|
|
89
|
+
if (!draft) {
|
|
90
|
+
return c.json({ error: "draft not found" }, 404);
|
|
91
|
+
}
|
|
92
|
+
if (!draft.current_quote_id) {
|
|
93
|
+
return c.json({ error: "draft has no current quote - call /quote first" }, 409);
|
|
94
|
+
}
|
|
95
|
+
quoteId = draft.current_quote_id;
|
|
96
|
+
draftPayload = draft.draft_payload;
|
|
97
|
+
}
|
|
98
|
+
if (!quoteId) {
|
|
99
|
+
return c.json({ error: "quoteId could not be resolved" }, 400);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const adapterContext = resolveAdapterContext(c, options, {
|
|
103
|
+
db,
|
|
104
|
+
operation: "book",
|
|
105
|
+
sourceKind: "engine",
|
|
106
|
+
correlationId,
|
|
107
|
+
});
|
|
108
|
+
const result = await bookEntity(db, {
|
|
109
|
+
registry: options.resolveSourceRegistry(c),
|
|
110
|
+
ownedHandlers: options.resolveOwnedHandlers?.(c),
|
|
111
|
+
captureSnapshotContent: options.captureSnapshotContent,
|
|
112
|
+
}, {
|
|
113
|
+
quoteId,
|
|
114
|
+
bookingId: body.bookingId,
|
|
115
|
+
party: body.party,
|
|
116
|
+
paymentIntent: body.paymentIntent,
|
|
117
|
+
parameters: engineParametersFromDraft(body.parameters, draftPayload ?? body.parameters?.draft),
|
|
118
|
+
idempotencyKey: body.idempotencyKey,
|
|
119
|
+
adapterContext,
|
|
120
|
+
contentScope: options.resolveContentScope?.({ c, db, body, draftPayload }),
|
|
121
|
+
});
|
|
122
|
+
if (body.draftId) {
|
|
123
|
+
try {
|
|
124
|
+
await markDraftConsumed(db, body.draftId, result.bookingId);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
options.onDraftConsumedError?.({
|
|
128
|
+
c,
|
|
129
|
+
db,
|
|
130
|
+
draftId: body.draftId,
|
|
131
|
+
bookingId: result.bookingId,
|
|
132
|
+
error,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
await options.onCommitted?.({ c, db, request: body, result });
|
|
137
|
+
const transformed = (await options.transformBookResult?.({ c, db, request: body, result })) ?? result;
|
|
138
|
+
return c.json(serializeBookResult(transformed));
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
return bookingEngineErrorResponse(c, err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function handleDraftPut(c, options) {
|
|
145
|
+
const id = c.req.param("id");
|
|
146
|
+
if (!id)
|
|
147
|
+
throw new RequestValidationError("id is required");
|
|
148
|
+
const body = await parseJsonBody(c, draftBodySchema);
|
|
149
|
+
const db = options.resolveDb(c);
|
|
150
|
+
const existing = await getBookingDraft(db, id);
|
|
151
|
+
if (existing) {
|
|
152
|
+
const updated = await updateBookingDraft(db, id, {
|
|
153
|
+
draftPayload: body.draftPayload,
|
|
154
|
+
currentStep: body.currentStep,
|
|
155
|
+
currentQuoteId: body.currentQuoteId,
|
|
156
|
+
refreshTtlMs: body.ttlMs ?? DEFAULT_DRAFT_TTL_MS,
|
|
157
|
+
});
|
|
158
|
+
return c.json(updated);
|
|
159
|
+
}
|
|
160
|
+
if (!body.entityModule || !body.entityId) {
|
|
161
|
+
throw new RequestValidationError("entityModule and entityId are required when creating a draft");
|
|
162
|
+
}
|
|
163
|
+
const provenance = body.sourceKind
|
|
164
|
+
? {
|
|
165
|
+
sourceKind: body.sourceKind,
|
|
166
|
+
sourceConnectionId: body.sourceConnectionId,
|
|
167
|
+
sourceRef: body.sourceRef,
|
|
168
|
+
}
|
|
169
|
+
: await resolveEntityProvenance(c, options, db, body.entityModule, body.entityId);
|
|
170
|
+
const created = await createBookingDraft(db, {
|
|
171
|
+
id,
|
|
172
|
+
entityModule: body.entityModule,
|
|
173
|
+
entityId: body.entityId,
|
|
174
|
+
sourceKind: provenance.sourceKind,
|
|
175
|
+
sourceConnectionId: provenance.sourceConnectionId,
|
|
176
|
+
sourceRef: provenance.sourceRef,
|
|
177
|
+
draftPayload: body.draftPayload,
|
|
178
|
+
currentStep: body.currentStep,
|
|
179
|
+
currentQuoteId: body.currentQuoteId,
|
|
180
|
+
createdBy: resolveActorId(c, options),
|
|
181
|
+
ttlMs: body.ttlMs,
|
|
182
|
+
});
|
|
183
|
+
return c.json(created, 201);
|
|
184
|
+
}
|
|
185
|
+
async function handleDraftGet(c, options) {
|
|
186
|
+
const id = c.req.param("id");
|
|
187
|
+
if (!id)
|
|
188
|
+
throw new RequestValidationError("id is required");
|
|
189
|
+
const row = await getBookingDraft(options.resolveDb(c), id);
|
|
190
|
+
if (!row)
|
|
191
|
+
return c.json({ error: "draft not found" }, 404);
|
|
192
|
+
return c.json(row);
|
|
193
|
+
}
|
|
194
|
+
async function handleDraftDelete(c, options) {
|
|
195
|
+
const id = c.req.param("id");
|
|
196
|
+
if (!id)
|
|
197
|
+
throw new RequestValidationError("id is required");
|
|
198
|
+
await deleteBookingDraft(options.resolveDb(c), id);
|
|
199
|
+
return c.body(null, 204);
|
|
200
|
+
}
|
|
201
|
+
async function handleHoldPlace(c, options) {
|
|
202
|
+
const body = await parseJsonBody(c, holdPlaceBodySchema);
|
|
203
|
+
const ownedHandlers = options.resolveOwnedHandlers?.(c);
|
|
204
|
+
const handler = ownedHandlers?.resolve(body.entityModule);
|
|
205
|
+
if (!handler?.placeHold) {
|
|
206
|
+
return c.json({ error: "no hold primitive registered for this vertical" }, 503);
|
|
207
|
+
}
|
|
208
|
+
const db = options.resolveDb(c);
|
|
209
|
+
const correlationId = resolveCorrelationId(c, options);
|
|
210
|
+
const ttlMs = body.ttlMs ??
|
|
211
|
+
(await (options.resolveHoldTtlMs?.({
|
|
212
|
+
c,
|
|
213
|
+
db,
|
|
214
|
+
entityModule: body.entityModule,
|
|
215
|
+
entityId: body.entityId,
|
|
216
|
+
}) ?? DEFAULT_HOLD_TTL_MS));
|
|
217
|
+
const adapterContext = resolveAdapterContext(c, options, {
|
|
218
|
+
db,
|
|
219
|
+
operation: "hold",
|
|
220
|
+
entityModule: body.entityModule,
|
|
221
|
+
entityId: body.entityId,
|
|
222
|
+
sourceKind: "engine",
|
|
223
|
+
correlationId,
|
|
224
|
+
});
|
|
225
|
+
try {
|
|
226
|
+
const result = await handler.placeHold({ db, adapterContext }, {
|
|
227
|
+
entityModule: body.entityModule,
|
|
228
|
+
entityId: body.entityId,
|
|
229
|
+
draftId: body.draftId,
|
|
230
|
+
ttlMs,
|
|
231
|
+
parameters: body.parameters,
|
|
232
|
+
});
|
|
233
|
+
return c.json({ holdToken: result.holdToken, expiresAt: result.expiresAt.toISOString() });
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
return bookingEngineErrorResponse(c, err);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function handleHoldRelease(c, options) {
|
|
240
|
+
const body = await parseJsonBody(c, holdReleaseBodySchema);
|
|
241
|
+
const ownedHandlers = options.resolveOwnedHandlers?.(c);
|
|
242
|
+
const handler = ownedHandlers?.resolve(body.entityModule);
|
|
243
|
+
if (!handler?.releaseHold) {
|
|
244
|
+
return c.body(null, 204);
|
|
245
|
+
}
|
|
246
|
+
const db = options.resolveDb(c);
|
|
247
|
+
const correlationId = resolveCorrelationId(c, options);
|
|
248
|
+
const adapterContext = resolveAdapterContext(c, options, {
|
|
249
|
+
db,
|
|
250
|
+
operation: "hold",
|
|
251
|
+
entityModule: body.entityModule,
|
|
252
|
+
sourceKind: "engine",
|
|
253
|
+
correlationId,
|
|
254
|
+
});
|
|
255
|
+
try {
|
|
256
|
+
await handler.releaseHold({ db, adapterContext }, body.holdToken);
|
|
257
|
+
return c.body(null, 204);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return bookingEngineErrorResponse(c, err);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function resolveEntityProvenance(c, options, db, entityModule, entityId) {
|
|
264
|
+
if (options.resolveEntityProvenance) {
|
|
265
|
+
return options.resolveEntityProvenance({ c, db, entityModule, entityId });
|
|
266
|
+
}
|
|
267
|
+
const row = await readSourcedEntry(db, entityModule, entityId);
|
|
268
|
+
if (!row)
|
|
269
|
+
return { sourceKind: OWNED_SOURCE_KIND };
|
|
270
|
+
return {
|
|
271
|
+
sourceKind: row.source_kind,
|
|
272
|
+
sourceProvider: row.source_provider ?? undefined,
|
|
273
|
+
sourceConnectionId: row.source_connection_id ?? undefined,
|
|
274
|
+
sourceRef: row.source_ref ?? undefined,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function resolveActorId(c, options) {
|
|
278
|
+
if (options.resolveActorId)
|
|
279
|
+
return options.resolveActorId(c);
|
|
280
|
+
const userId = c.var.userId;
|
|
281
|
+
return typeof userId === "string" ? userId : null;
|
|
282
|
+
}
|
|
283
|
+
function resolveCorrelationId(c, options) {
|
|
284
|
+
return options.resolveCorrelationId?.(c) ?? c.req.header("x-request-id") ?? cryptoRandom();
|
|
285
|
+
}
|
|
286
|
+
function resolveAdapterContext(c, options, input) {
|
|
287
|
+
return (options.resolveAdapterContext?.({ c, ...input }) ?? {
|
|
288
|
+
connection_id: input.sourceConnectionId ?? input.sourceKind,
|
|
289
|
+
correlation_id: input.correlationId,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function defaultAudienceForPath(c) {
|
|
293
|
+
return c.req.path.startsWith("/v1/public/") ? "customer" : "staff";
|
|
294
|
+
}
|
|
295
|
+
function engineParametersFromDraft(parameters, draftPayload) {
|
|
296
|
+
const draft = asRecord(draftPayload);
|
|
297
|
+
const configure = asRecord(draft?.configure);
|
|
298
|
+
const departureSlotId = stringValue(configure?.departureSlotId);
|
|
299
|
+
const paxCount = sumDraftPax(configure?.pax);
|
|
300
|
+
const next = {
|
|
301
|
+
...(parameters ?? {}),
|
|
302
|
+
...(draft ? { draft } : {}),
|
|
303
|
+
};
|
|
304
|
+
if (departureSlotId) {
|
|
305
|
+
if (next.departureSlotId == null)
|
|
306
|
+
next.departureSlotId = departureSlotId;
|
|
307
|
+
if (next.departure_id == null)
|
|
308
|
+
next.departure_id = departureSlotId;
|
|
309
|
+
if (next.slotId == null)
|
|
310
|
+
next.slotId = departureSlotId;
|
|
311
|
+
}
|
|
312
|
+
if (paxCount > 0 && next.paxCount == null) {
|
|
313
|
+
next.paxCount = paxCount;
|
|
314
|
+
}
|
|
315
|
+
for (const key of ["roomTypeId", "ratePlanId", "board"]) {
|
|
316
|
+
const value = stringValue(configure?.[key]);
|
|
317
|
+
if (value && next[key] == null)
|
|
318
|
+
next[key] = value;
|
|
319
|
+
}
|
|
320
|
+
// Lift `draft.promotionCode` to the top-level so `quoteEntity`'s
|
|
321
|
+
// promotion hook can read it without descending into the nested
|
|
322
|
+
// draft. Same lifting pattern as `paxCount` above. Per
|
|
323
|
+
// docs/architecture/promotions-architecture.md §7.0.
|
|
324
|
+
const promotionCode = stringValue(draft?.promotionCode);
|
|
325
|
+
if (promotionCode && next.promotionCode == null) {
|
|
326
|
+
next.promotionCode = promotionCode;
|
|
327
|
+
}
|
|
328
|
+
return next;
|
|
329
|
+
}
|
|
330
|
+
function asRecord(value) {
|
|
331
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
332
|
+
? value
|
|
333
|
+
: undefined;
|
|
334
|
+
}
|
|
335
|
+
function stringValue(value) {
|
|
336
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
337
|
+
}
|
|
338
|
+
function sumDraftPax(value) {
|
|
339
|
+
const pax = asRecord(value);
|
|
340
|
+
if (!pax)
|
|
341
|
+
return 0;
|
|
342
|
+
let total = 0;
|
|
343
|
+
for (const count of Object.values(pax)) {
|
|
344
|
+
if (typeof count === "number" && Number.isFinite(count) && count > 0) {
|
|
345
|
+
total += count;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return total;
|
|
349
|
+
}
|
|
350
|
+
function bookingEngineErrorResponse(c, err) {
|
|
351
|
+
if (err instanceof BookingEngineError) {
|
|
352
|
+
const status = statusForCode(err.code);
|
|
353
|
+
return c.json({ error: err.message, code: err.code, context: err.context }, status);
|
|
354
|
+
}
|
|
355
|
+
return handleApiError(err, c);
|
|
356
|
+
}
|
|
357
|
+
function statusForCode(code) {
|
|
358
|
+
switch (code) {
|
|
359
|
+
case NO_ADAPTER_REGISTERED:
|
|
360
|
+
case NO_HANDLER_REGISTERED:
|
|
361
|
+
return 503;
|
|
362
|
+
case QUOTE_NOT_FOUND:
|
|
363
|
+
case ORDER_NOT_FOUND:
|
|
364
|
+
return 404;
|
|
365
|
+
case QUOTE_EXPIRED:
|
|
366
|
+
case QUOTE_MISMATCH:
|
|
367
|
+
case ORDER_ALREADY_CANCELLED:
|
|
368
|
+
return 409;
|
|
369
|
+
case RESERVE_FAILED:
|
|
370
|
+
return 502;
|
|
371
|
+
default:
|
|
372
|
+
return 500;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function cryptoRandom() {
|
|
376
|
+
if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.randomUUID) {
|
|
377
|
+
return globalThis.crypto.randomUUID();
|
|
378
|
+
}
|
|
379
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
380
|
+
}
|
|
381
|
+
function serializeQuoteResult(result) {
|
|
382
|
+
return quoteResponseV1.parse({
|
|
383
|
+
...result,
|
|
384
|
+
quotedAt: result.quotedAt.toISOString(),
|
|
385
|
+
expiresAt: result.expiresAt.toISOString(),
|
|
386
|
+
pricing: toPricingBreakdownV1(result.pricing),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function serializeBookResult(result) {
|
|
390
|
+
return bookResponseV1.parse({
|
|
391
|
+
...result,
|
|
392
|
+
pricing: toPricingBreakdownV1(result.pricing),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function toPricingBreakdownV1(basis) {
|
|
396
|
+
if (!basis)
|
|
397
|
+
return undefined;
|
|
398
|
+
if (basis.breakdown) {
|
|
399
|
+
const breakdown = basis.breakdown;
|
|
400
|
+
if (breakdown.currency && Array.isArray(breakdown.lines) && Array.isArray(breakdown.taxes)) {
|
|
401
|
+
return breakdown;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const lines = [
|
|
405
|
+
{
|
|
406
|
+
kind: "base",
|
|
407
|
+
label: "Base",
|
|
408
|
+
quantity: 1,
|
|
409
|
+
unitAmount: basis.base_amount,
|
|
410
|
+
totalAmount: basis.base_amount,
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
if (basis.fees > 0) {
|
|
414
|
+
lines.push({ kind: "fee", label: "Fees", unitAmount: basis.fees, totalAmount: basis.fees });
|
|
415
|
+
}
|
|
416
|
+
if (basis.surcharges > 0) {
|
|
417
|
+
lines.push({
|
|
418
|
+
kind: "supplement",
|
|
419
|
+
label: "Surcharges",
|
|
420
|
+
unitAmount: basis.surcharges,
|
|
421
|
+
totalAmount: basis.surcharges,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const subtotal = basis.base_amount + basis.fees + basis.surcharges;
|
|
425
|
+
return {
|
|
426
|
+
currency: basis.currency,
|
|
427
|
+
lines,
|
|
428
|
+
taxes: basis.taxes > 0
|
|
429
|
+
? [
|
|
430
|
+
{
|
|
431
|
+
code: "tax",
|
|
432
|
+
label: "Tax",
|
|
433
|
+
rate: 0,
|
|
434
|
+
amount: basis.taxes,
|
|
435
|
+
base: basis.base_amount,
|
|
436
|
+
},
|
|
437
|
+
]
|
|
438
|
+
: [],
|
|
439
|
+
subtotal,
|
|
440
|
+
taxTotal: basis.taxes,
|
|
441
|
+
total: subtotal + basis.taxes,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.test.d.ts","sourceRoot":"","sources":["../../src/booking-engine/routes.test.ts"],"names":[],"mappings":""}
|