@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,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bookEntity` — the second step in the booking engine lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Validates the supplied quote, calls `adapter.reserve`, and writes a
|
|
5
|
+
* `booking_catalog_snapshot` row with the frozen view, source pointer,
|
|
6
|
+
* and pricing breakdown. The snapshot is the audit record; the source
|
|
7
|
+
* pointer is what subsequent `cancelEntity` / status calls dispatch
|
|
8
|
+
* against.
|
|
9
|
+
*
|
|
10
|
+
* `bookingId` is plain text (no FK), so the engine accepts an existing
|
|
11
|
+
* id from the caller (when a `bookings` row already exists) or generates
|
|
12
|
+
* one on demand. This keeps the engine decoupled from `packages/bookings`
|
|
13
|
+
* — the engine and the bookings module evolve independently and are
|
|
14
|
+
* joined at read time via the shared `booking_id` text column.
|
|
15
|
+
*/
|
|
16
|
+
import { newId } from "@voyant-travel/db/lib/typeid";
|
|
17
|
+
import { eq } from "drizzle-orm";
|
|
18
|
+
import { captureSnapshot } from "../services/snapshot-service.js";
|
|
19
|
+
import { bookingCatalogSnapshotTable, } from "../snapshot/schema.js";
|
|
20
|
+
import { BookingEngineError, QUOTE_NOT_FOUND, QuoteExpiredError, QuoteMismatchError, ReserveFailedError, } from "./errors.js";
|
|
21
|
+
import { OWNED_SOURCE_KIND } from "./owned-handler.js";
|
|
22
|
+
import { catalogQuotesTable } from "./schema.js";
|
|
23
|
+
function paymentIntentToAdapterRecord(intent) {
|
|
24
|
+
return { ...intent };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Book the row referenced by `quoteId`. End-to-end: validate quote,
|
|
28
|
+
* dispatch `adapter.reserve`, capture snapshot, mark quote consumed.
|
|
29
|
+
*
|
|
30
|
+
* Throws:
|
|
31
|
+
* - `BookingEngineError(QUOTE_NOT_FOUND)` if the quote doesn't exist.
|
|
32
|
+
* - `QuoteExpiredError` if the quote's `expires_at` has passed.
|
|
33
|
+
* - `QuoteMismatchError` if a future caller tries to book a different
|
|
34
|
+
* entity than the quote was issued for (defensive — the engine
|
|
35
|
+
* re-reads quote.entity_*).
|
|
36
|
+
* - `NoAdapterRegisteredError` if the registry has no adapter for
|
|
37
|
+
* the quote's `source_kind`.
|
|
38
|
+
* - `ReserveFailedError` when the adapter returns `status: "failed"`.
|
|
39
|
+
*/
|
|
40
|
+
export async function bookEntity(db, deps, request) {
|
|
41
|
+
// Idempotency check — when the caller supplies a key, look for a
|
|
42
|
+
// prior snapshot with the same key and short-circuit. Snapshot rows
|
|
43
|
+
// outlive quotes (audit), so the lookup works even if the quote
|
|
44
|
+
// has been consumed or expired.
|
|
45
|
+
if (request.idempotencyKey) {
|
|
46
|
+
const prior = await loadSnapshotByIdempotencyKey(db, request.idempotencyKey);
|
|
47
|
+
if (prior) {
|
|
48
|
+
return {
|
|
49
|
+
bookingId: prior.booking_id,
|
|
50
|
+
orderRef: prior.source_ref ?? prior.id,
|
|
51
|
+
// Status comes from the prior commit's payload; default to
|
|
52
|
+
// "held" since we don't persist the original status column.
|
|
53
|
+
// Callers that need the precise status should re-read the
|
|
54
|
+
// booking row.
|
|
55
|
+
status: "held",
|
|
56
|
+
snapshotId: prior.id,
|
|
57
|
+
pricing: snapshotToPricing(prior),
|
|
58
|
+
upstreamPayload: undefined,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const quote = await loadQuote(db, request.quoteId);
|
|
63
|
+
assertQuoteUsable(quote);
|
|
64
|
+
const paymentIntent = request.paymentIntent ?? { type: "hold" };
|
|
65
|
+
const bookingId = request.bookingId ?? newId("bookings");
|
|
66
|
+
const isOwned = quote.source_kind === OWNED_SOURCE_KIND && deps.ownedHandlers != null;
|
|
67
|
+
// Owned arm: dispatch to handler.commit; skip snapshot-content
|
|
68
|
+
// capture (owned content lives in the operator's own DB, not in a
|
|
69
|
+
// remote upstream).
|
|
70
|
+
if (isOwned) {
|
|
71
|
+
if (!deps.ownedHandlers)
|
|
72
|
+
throw new Error("unreachable: ownedHandlers checked above");
|
|
73
|
+
const handler = deps.ownedHandlers.resolveOrThrow(quote.entity_module);
|
|
74
|
+
const quotePricing = readPricingFromQuote(quote);
|
|
75
|
+
const commitResult = await handler.commit({ db, adapterContext: request.adapterContext }, {
|
|
76
|
+
entityModule: quote.entity_module,
|
|
77
|
+
entityId: quote.entity_id,
|
|
78
|
+
bookingId,
|
|
79
|
+
party: request.party,
|
|
80
|
+
parameters: request.parameters,
|
|
81
|
+
pricing: quotePricing,
|
|
82
|
+
draft: request.parameters?.draft,
|
|
83
|
+
});
|
|
84
|
+
if (commitResult.status === "failed") {
|
|
85
|
+
throw new ReserveFailedError(commitResult.upstreamPayload, quote.source_kind, quote.entity_id);
|
|
86
|
+
}
|
|
87
|
+
// The handler may mint its own booking row id rather than use the provided
|
|
88
|
+
// one — adopt it so the response + snapshot + quote marker reference the
|
|
89
|
+
// real booking (otherwise the detail page 404s on a non-existent id).
|
|
90
|
+
const ownedBookingId = commitResult.bookingId ?? bookingId;
|
|
91
|
+
const finalPricing = commitResult.pricing ?? quotePricing;
|
|
92
|
+
const ownedFrozenPayload = {
|
|
93
|
+
quote: serializeQuote(quote),
|
|
94
|
+
commit: commitResult.upstreamPayload ?? null,
|
|
95
|
+
paymentIntent,
|
|
96
|
+
};
|
|
97
|
+
const ownedSnapshot = await captureSnapshot(db, {
|
|
98
|
+
bookingId: ownedBookingId,
|
|
99
|
+
entityModule: quote.entity_module,
|
|
100
|
+
entityId: quote.entity_id,
|
|
101
|
+
sourceKind: quote.source_kind,
|
|
102
|
+
sourceProvider: quote.source_provider ?? undefined,
|
|
103
|
+
sourceConnectionId: quote.source_connection_id ?? undefined,
|
|
104
|
+
sourceRef: commitResult.orderRef || quote.source_ref || undefined,
|
|
105
|
+
frozenPayload: ownedFrozenPayload,
|
|
106
|
+
pricingBasis: finalPricing,
|
|
107
|
+
idempotencyKey: request.idempotencyKey,
|
|
108
|
+
});
|
|
109
|
+
await markQuoteConsumed(db, quote.id, ownedBookingId);
|
|
110
|
+
return {
|
|
111
|
+
bookingId: ownedBookingId,
|
|
112
|
+
orderRef: commitResult.orderRef || ownedSnapshot.id,
|
|
113
|
+
status: commitResult.status,
|
|
114
|
+
snapshotId: ownedSnapshot.id,
|
|
115
|
+
pricing: finalPricing,
|
|
116
|
+
upstreamPayload: commitResult.upstreamPayload,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Sourced arm — preserves the existing dispatch path verbatim.
|
|
120
|
+
const adapter = quote.source_connection_id
|
|
121
|
+
? (deps.registry.resolveByConnection(quote.source_connection_id) ??
|
|
122
|
+
deps.registry.resolveOrThrow(quote.source_kind))
|
|
123
|
+
: deps.registry.resolveOrThrow(quote.source_kind);
|
|
124
|
+
if (!adapter.reserve) {
|
|
125
|
+
throw new BookingEngineError("RESERVE_FAILED", `adapter "${adapter.kind}" does not implement reserve()`, { sourceKind: quote.source_kind });
|
|
126
|
+
}
|
|
127
|
+
const reserveRequest = {
|
|
128
|
+
entity_module: quote.entity_module,
|
|
129
|
+
entity_id: quote.entity_id,
|
|
130
|
+
parameters: request.parameters ?? {},
|
|
131
|
+
party: request.party,
|
|
132
|
+
payment_intent: paymentIntentToAdapterRecord(paymentIntent),
|
|
133
|
+
scope: request.adapterScope,
|
|
134
|
+
idempotency_key: request.idempotencyKey,
|
|
135
|
+
};
|
|
136
|
+
const reserveResult = await adapter.reserve(request.adapterContext, reserveRequest);
|
|
137
|
+
if (reserveResult.status === "failed") {
|
|
138
|
+
throw new ReserveFailedError(reserveResult.upstream_payload, quote.source_kind, quote.entity_id);
|
|
139
|
+
}
|
|
140
|
+
const pricing = readPricingFromQuote(quote);
|
|
141
|
+
// Snapshot content capture per sourced-content §5.1 — refresh from
|
|
142
|
+
// the adapter, fall back to cache, throw if neither produces content.
|
|
143
|
+
// Skipped entirely when the deps callback isn't wired (legacy
|
|
144
|
+
// behavior).
|
|
145
|
+
let contentCapture = null;
|
|
146
|
+
if (deps.captureSnapshotContent && request.contentScope) {
|
|
147
|
+
contentCapture = await deps.captureSnapshotContent({
|
|
148
|
+
db,
|
|
149
|
+
entity_module: quote.entity_module,
|
|
150
|
+
entity_id: quote.entity_id,
|
|
151
|
+
source_kind: quote.source_kind,
|
|
152
|
+
source_connection_id: quote.source_connection_id ?? undefined,
|
|
153
|
+
source_ref: reserveResult.upstream_ref || quote.source_ref || undefined,
|
|
154
|
+
locale: request.contentScope.locale,
|
|
155
|
+
market: request.contentScope.market,
|
|
156
|
+
currency: request.contentScope.currency,
|
|
157
|
+
adapterContext: request.adapterContext,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const frozenPayload = {
|
|
161
|
+
quote: serializeQuote(quote),
|
|
162
|
+
reserve: reserveResult.upstream_payload ?? null,
|
|
163
|
+
paymentIntent,
|
|
164
|
+
};
|
|
165
|
+
if (contentCapture) {
|
|
166
|
+
// The content_capture envelope is alongside `content` so audit can
|
|
167
|
+
// later distinguish a fresh capture from a cache fallback (per
|
|
168
|
+
// §5.1). Both fields are nested under frozen_payload as opaque
|
|
169
|
+
// JSONB — no schema migration required.
|
|
170
|
+
frozenPayload.content = contentCapture.content;
|
|
171
|
+
frozenPayload.content_capture = {
|
|
172
|
+
source: contentCapture.source,
|
|
173
|
+
fetched_at: contentCapture.fetched_at,
|
|
174
|
+
fallback_reason: contentCapture.fallback_reason,
|
|
175
|
+
content_etag: contentCapture.content_etag,
|
|
176
|
+
content_schema_version: contentCapture.content_schema_version,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const snapshotInput = {
|
|
180
|
+
bookingId,
|
|
181
|
+
entityModule: quote.entity_module,
|
|
182
|
+
entityId: quote.entity_id,
|
|
183
|
+
sourceKind: quote.source_kind,
|
|
184
|
+
sourceProvider: quote.source_provider ?? undefined,
|
|
185
|
+
sourceConnectionId: quote.source_connection_id ?? undefined,
|
|
186
|
+
sourceRef: reserveResult.upstream_ref || quote.source_ref || undefined,
|
|
187
|
+
frozenPayload,
|
|
188
|
+
pricingBasis: pricing,
|
|
189
|
+
idempotencyKey: request.idempotencyKey,
|
|
190
|
+
};
|
|
191
|
+
const snapshot = await captureSnapshot(db, snapshotInput);
|
|
192
|
+
await markQuoteConsumed(db, quote.id, bookingId);
|
|
193
|
+
return {
|
|
194
|
+
bookingId,
|
|
195
|
+
orderRef: reserveResult.upstream_ref || snapshot.id,
|
|
196
|
+
status: reserveResult.status,
|
|
197
|
+
snapshotId: snapshot.id,
|
|
198
|
+
pricing,
|
|
199
|
+
upstreamPayload: reserveResult.upstream_payload,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
async function loadSnapshotByIdempotencyKey(db, idempotencyKey) {
|
|
204
|
+
const rows = (await db
|
|
205
|
+
.select()
|
|
206
|
+
.from(bookingCatalogSnapshotTable)
|
|
207
|
+
.where(eq(bookingCatalogSnapshotTable.idempotency_key, idempotencyKey))
|
|
208
|
+
.limit(1));
|
|
209
|
+
return rows[0];
|
|
210
|
+
}
|
|
211
|
+
function snapshotToPricing(snapshot) {
|
|
212
|
+
if (snapshot.pricing_base_amount == null || !snapshot.pricing_currency)
|
|
213
|
+
return undefined;
|
|
214
|
+
const base = typeof snapshot.pricing_base_amount === "string"
|
|
215
|
+
? Number.parseFloat(snapshot.pricing_base_amount)
|
|
216
|
+
: Number(snapshot.pricing_base_amount);
|
|
217
|
+
if (!Number.isFinite(base))
|
|
218
|
+
return undefined;
|
|
219
|
+
return {
|
|
220
|
+
base_amount: base,
|
|
221
|
+
taxes: numericOrZero(snapshot.pricing_taxes),
|
|
222
|
+
fees: numericOrZero(snapshot.pricing_fees),
|
|
223
|
+
surcharges: numericOrZero(snapshot.pricing_surcharges),
|
|
224
|
+
currency: snapshot.pricing_currency,
|
|
225
|
+
breakdown: snapshot.pricing_breakdown ?? undefined,
|
|
226
|
+
appliedOffers: snapshot.pricing_applied_offers ?? undefined,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function loadQuote(db, quoteId) {
|
|
230
|
+
const rows = (await db
|
|
231
|
+
.select()
|
|
232
|
+
.from(catalogQuotesTable)
|
|
233
|
+
.where(eq(catalogQuotesTable.id, quoteId))
|
|
234
|
+
.limit(1));
|
|
235
|
+
const quote = rows[0];
|
|
236
|
+
if (!quote) {
|
|
237
|
+
throw new BookingEngineError(QUOTE_NOT_FOUND, `quote ${quoteId} not found`, { quoteId });
|
|
238
|
+
}
|
|
239
|
+
return quote;
|
|
240
|
+
}
|
|
241
|
+
function assertQuoteUsable(quote) {
|
|
242
|
+
if (quote.consumed_at) {
|
|
243
|
+
throw new QuoteMismatchError(quote.id, { entityModule: quote.entity_module, entityId: quote.entity_id }, { entityModule: quote.entity_module, entityId: quote.entity_id });
|
|
244
|
+
}
|
|
245
|
+
const expiresAt = new Date(quote.expires_at);
|
|
246
|
+
if (Number.isNaN(expiresAt.getTime())) {
|
|
247
|
+
throw new BookingEngineError(QUOTE_NOT_FOUND, `quote ${quote.id} has invalid expires_at`, {
|
|
248
|
+
quoteId: quote.id,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (expiresAt.getTime() <= Date.now()) {
|
|
252
|
+
throw new QuoteExpiredError(quote.id, expiresAt);
|
|
253
|
+
}
|
|
254
|
+
if (!quote.available) {
|
|
255
|
+
throw new ReserveFailedError({ invalidReason: quote.invalid_reason }, quote.source_kind, quote.entity_id);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function markQuoteConsumed(db, quoteId, bookingId) {
|
|
259
|
+
await db
|
|
260
|
+
.update(catalogQuotesTable)
|
|
261
|
+
.set({
|
|
262
|
+
consumed_at: new Date(),
|
|
263
|
+
consumed_booking_id: bookingId,
|
|
264
|
+
})
|
|
265
|
+
.where(eq(catalogQuotesTable.id, quoteId));
|
|
266
|
+
}
|
|
267
|
+
function readPricingFromQuote(quote) {
|
|
268
|
+
const baseRaw = quote.pricing_base_amount;
|
|
269
|
+
if (baseRaw == null)
|
|
270
|
+
return undefined;
|
|
271
|
+
const base = typeof baseRaw === "string" ? Number.parseFloat(baseRaw) : Number(baseRaw);
|
|
272
|
+
if (!Number.isFinite(base))
|
|
273
|
+
return undefined;
|
|
274
|
+
const currency = quote.pricing_currency;
|
|
275
|
+
if (!currency)
|
|
276
|
+
return undefined;
|
|
277
|
+
return {
|
|
278
|
+
base_amount: base,
|
|
279
|
+
taxes: numericOrZero(quote.pricing_taxes),
|
|
280
|
+
fees: numericOrZero(quote.pricing_fees),
|
|
281
|
+
surcharges: numericOrZero(quote.pricing_surcharges),
|
|
282
|
+
currency,
|
|
283
|
+
breakdown: quote.pricing_breakdown ?? undefined,
|
|
284
|
+
appliedOffers: quote.pricing_applied_offers ?? undefined,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function numericOrZero(v) {
|
|
288
|
+
if (v == null)
|
|
289
|
+
return 0;
|
|
290
|
+
const n = typeof v === "string" ? Number.parseFloat(v) : Number(v);
|
|
291
|
+
return Number.isFinite(n) ? n : 0;
|
|
292
|
+
}
|
|
293
|
+
function serializeQuote(quote) {
|
|
294
|
+
return {
|
|
295
|
+
id: quote.id,
|
|
296
|
+
entity_module: quote.entity_module,
|
|
297
|
+
entity_id: quote.entity_id,
|
|
298
|
+
source_kind: quote.source_kind,
|
|
299
|
+
source_provider: quote.source_provider,
|
|
300
|
+
source_ref: quote.source_ref,
|
|
301
|
+
locale: quote.locale,
|
|
302
|
+
audience: quote.audience,
|
|
303
|
+
market: quote.market,
|
|
304
|
+
currency: quote.currency,
|
|
305
|
+
pricing_base_amount: quote.pricing_base_amount,
|
|
306
|
+
pricing_currency: quote.pricing_currency,
|
|
307
|
+
upstream_payload: quote.upstream_payload,
|
|
308
|
+
created_at: quote.created_at,
|
|
309
|
+
expires_at: quote.expires_at,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cancelEntity` — third step in the booking engine lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Looks up the snapshot row, dispatches `adapter.cancel`, returns the
|
|
5
|
+
* adapter's refund result. The snapshot stays — it's the audit record —
|
|
6
|
+
* and the booking row's lifecycle (set status to cancelled, etc.) is the
|
|
7
|
+
* caller's responsibility, kept outside the engine to preserve the
|
|
8
|
+
* decoupling from `packages/bookings`.
|
|
9
|
+
*/
|
|
10
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
11
|
+
import type { CancelResult, SourceAdapterContext, SourceAdapterRequestScope } from "../adapter/contract.js";
|
|
12
|
+
import type { SourceAdapterRegistry } from "./registry.js";
|
|
13
|
+
export interface CancelEntityRequest {
|
|
14
|
+
bookingId: string;
|
|
15
|
+
entityModule: string;
|
|
16
|
+
entityId: string;
|
|
17
|
+
reason?: string;
|
|
18
|
+
scope?: SourceAdapterRequestScope;
|
|
19
|
+
idempotencyKey?: string;
|
|
20
|
+
adapterContext: SourceAdapterContext;
|
|
21
|
+
}
|
|
22
|
+
export interface CancelEntityResult {
|
|
23
|
+
status: CancelResult["status"];
|
|
24
|
+
refundAmount?: number;
|
|
25
|
+
refundCurrency?: string;
|
|
26
|
+
pendingChannel?: string;
|
|
27
|
+
/** The snapshot row's id — exposed for callers that want to log the cancel against it. */
|
|
28
|
+
snapshotId: string;
|
|
29
|
+
}
|
|
30
|
+
export interface CancelEntityDeps {
|
|
31
|
+
registry: SourceAdapterRegistry;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Cancel the given (booking, entity) pair. Throws `ORDER_NOT_FOUND` when
|
|
35
|
+
* no snapshot row matches. Otherwise dispatches to the registered
|
|
36
|
+
* adapter; if the adapter doesn't implement `cancel`, returns the
|
|
37
|
+
* adapter's refusal verbatim so the caller can surface it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function cancelEntity(db: AnyDrizzleDb, deps: CancelEntityDeps, request: CancelEntityRequest): Promise<CancelEntityResult>;
|
|
40
|
+
//# sourceMappingURL=cancel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancel.d.ts","sourceRoot":"","sources":["../../src/booking-engine/cancel.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EACpB,yBAAyB,EAC1B,MAAM,wBAAwB,CAAA;AAO/B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,yBAAyB,CAAA;IACjC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,EAAE,oBAAoB,CAAA;CACrC;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAA;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;CAChC;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CA2B7B"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cancelEntity` — third step in the booking engine lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Looks up the snapshot row, dispatches `adapter.cancel`, returns the
|
|
5
|
+
* adapter's refund result. The snapshot stays — it's the audit record —
|
|
6
|
+
* and the booking row's lifecycle (set status to cancelled, etc.) is the
|
|
7
|
+
* caller's responsibility, kept outside the engine to preserve the
|
|
8
|
+
* decoupling from `packages/bookings`.
|
|
9
|
+
*/
|
|
10
|
+
import { and, eq } from "drizzle-orm";
|
|
11
|
+
import { bookingCatalogSnapshotTable, } from "../snapshot/schema.js";
|
|
12
|
+
import { BookingEngineError, ORDER_NOT_FOUND } from "./errors.js";
|
|
13
|
+
/**
|
|
14
|
+
* Cancel the given (booking, entity) pair. Throws `ORDER_NOT_FOUND` when
|
|
15
|
+
* no snapshot row matches. Otherwise dispatches to the registered
|
|
16
|
+
* adapter; if the adapter doesn't implement `cancel`, returns the
|
|
17
|
+
* adapter's refusal verbatim so the caller can surface it.
|
|
18
|
+
*/
|
|
19
|
+
export async function cancelEntity(db, deps, request) {
|
|
20
|
+
const snapshot = await loadSnapshot(db, request.bookingId, request.entityModule, request.entityId);
|
|
21
|
+
const adapter = snapshot.source_connection_id
|
|
22
|
+
? (deps.registry.resolveByConnection(snapshot.source_connection_id) ??
|
|
23
|
+
deps.registry.resolveOrThrow(snapshot.source_kind))
|
|
24
|
+
: deps.registry.resolveOrThrow(snapshot.source_kind);
|
|
25
|
+
if (!adapter.cancel) {
|
|
26
|
+
return {
|
|
27
|
+
status: "refused",
|
|
28
|
+
snapshotId: snapshot.id,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const result = await adapter.cancel(request.adapterContext, {
|
|
32
|
+
upstream_ref: snapshot.source_ref ?? snapshot.id,
|
|
33
|
+
reason: request.reason,
|
|
34
|
+
scope: request.scope,
|
|
35
|
+
idempotency_key: request.idempotencyKey,
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
status: result.status,
|
|
39
|
+
refundAmount: result.refund_amount,
|
|
40
|
+
refundCurrency: result.refund_currency,
|
|
41
|
+
pendingChannel: result.pending_channel,
|
|
42
|
+
snapshotId: snapshot.id,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function loadSnapshot(db, bookingId, entityModule, entityId) {
|
|
46
|
+
const rows = (await db
|
|
47
|
+
.select()
|
|
48
|
+
.from(bookingCatalogSnapshotTable)
|
|
49
|
+
.where(and(eq(bookingCatalogSnapshotTable.booking_id, bookingId), eq(bookingCatalogSnapshotTable.entity_module, entityModule), eq(bookingCatalogSnapshotTable.entity_id, entityId)))
|
|
50
|
+
.limit(1));
|
|
51
|
+
const snapshot = rows[0];
|
|
52
|
+
if (!snapshot) {
|
|
53
|
+
throw new BookingEngineError(ORDER_NOT_FOUND, `no snapshot row for booking=${bookingId} entity=${entityModule}:${entityId}`, { bookingId, entityModule, entityId });
|
|
54
|
+
}
|
|
55
|
+
return snapshot;
|
|
56
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `checkoutFinalize` workflow — runs on `payment.completed` for
|
|
3
|
+
* bookings created through the storefront's checkout-start path.
|
|
4
|
+
*
|
|
5
|
+
* Steps:
|
|
6
|
+
* 1. transition_to_confirmed — flip the booking from
|
|
7
|
+
* `awaiting_payment` to `confirmed`, stamping `paidAt`. This
|
|
8
|
+
* emits `booking.confirmed` which fans out to:
|
|
9
|
+
* - legal's auto-generate-contract subscriber (if wired)
|
|
10
|
+
* - finance's auto-generate-invoice subscriber (Phase 5)
|
|
11
|
+
* 2. issue_invoice — explicit fallback when finance auto-generation
|
|
12
|
+
* isn't wired. Idempotent — checks if an invoice already exists.
|
|
13
|
+
*
|
|
14
|
+
* Compensation: if `issue_invoice` fails after the booking is
|
|
15
|
+
* already confirmed, we don't roll back to `awaiting_payment` — the
|
|
16
|
+
* payment was real and the booking is real. Instead the workflow
|
|
17
|
+
* leaves the booking in `confirmed` and rethrows so the workflow
|
|
18
|
+
* runs view surfaces the failed step for ops.
|
|
19
|
+
*
|
|
20
|
+
* This is a thin adapter on top of `createWorkflow` from
|
|
21
|
+
* `@voyant-travel/core/workflows` — no durability, no event-await; the
|
|
22
|
+
* caller subscribes to `payment.completed` and runs the workflow
|
|
23
|
+
* inline. Phase 6 of the storefront-checkout-flow plan elaborates
|
|
24
|
+
* with workflow-runs surfacing.
|
|
25
|
+
*/
|
|
26
|
+
import type { EventBus } from "@voyant-travel/core";
|
|
27
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
28
|
+
export interface CheckoutFinalizeInput {
|
|
29
|
+
bookingId: string;
|
|
30
|
+
/** Optional payment metadata for audit logging. */
|
|
31
|
+
paymentSessionId?: string;
|
|
32
|
+
paymentIntent?: "card" | "bank_transfer" | "hold" | "ticket_on_credit";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Optional step-lifecycle hooks the caller can wire to the
|
|
36
|
+
* `@voyant-travel/workflow-runs` recorder (or any other observability
|
|
37
|
+
* sink). Catalog stays neutral — it just emits the events.
|
|
38
|
+
*/
|
|
39
|
+
export interface CheckoutFinalizeStepRecorder {
|
|
40
|
+
startStep(name: string): Promise<void> | void;
|
|
41
|
+
completeStep(name: string, output?: Record<string, unknown> | null): Promise<void> | void;
|
|
42
|
+
failStep(name: string, error: unknown): Promise<void> | void;
|
|
43
|
+
}
|
|
44
|
+
export interface CheckoutFinalizeDeps {
|
|
45
|
+
db: PostgresJsDatabase;
|
|
46
|
+
eventBus?: EventBus;
|
|
47
|
+
/** Optional observability sink — see CheckoutFinalizeStepRecorder. */
|
|
48
|
+
recorder?: CheckoutFinalizeStepRecorder;
|
|
49
|
+
/**
|
|
50
|
+
* Confirms the booking — flips it from `awaiting_payment`/`on_hold`
|
|
51
|
+
* to `confirmed`. Implementations should emit `booking.confirmed`
|
|
52
|
+
* once the transaction commits so downstream subscribers fan out.
|
|
53
|
+
*/
|
|
54
|
+
confirmBooking: (bookingId: string) => Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Issues the final invoice for the booking. When `convertedFromInvoiceId`
|
|
57
|
+
* is supplied (proforma → invoice path), implementations should
|
|
58
|
+
* preserve the linkage. Returning `null` is treated as "skipped"
|
|
59
|
+
* (e.g. invoice already issued) and not an error.
|
|
60
|
+
*/
|
|
61
|
+
issueInvoice: (input: {
|
|
62
|
+
bookingId: string;
|
|
63
|
+
convertedFromInvoiceId?: string | null;
|
|
64
|
+
}) => Promise<{
|
|
65
|
+
invoiceId: string;
|
|
66
|
+
} | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Look up an existing proforma for this booking so we can pass
|
|
69
|
+
* its id into `issueInvoice` (for the conversion linkage). Return
|
|
70
|
+
* `null` if there isn't one — the booking went through card or
|
|
71
|
+
* inquiry rather than bank-transfer.
|
|
72
|
+
*/
|
|
73
|
+
findProformaForBooking?: (bookingId: string) => Promise<{
|
|
74
|
+
invoiceId: string;
|
|
75
|
+
} | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Generate (or fetch existing) the contract PDF for the booking.
|
|
78
|
+
* The implementation is expected to be **idempotent** — if a
|
|
79
|
+
* contract document already exists for this booking, return its
|
|
80
|
+
* id without re-rendering. This keeps the explicit workflow step
|
|
81
|
+
* compatible with the legal package's `booking.confirmed`
|
|
82
|
+
* subscriber, which races with the step in storefront flows.
|
|
83
|
+
*
|
|
84
|
+
* Returning `null` is treated as "no contract template wired" and
|
|
85
|
+
* skipped silently — the operator may not have configured one,
|
|
86
|
+
* which is a deployment choice rather than a workflow failure.
|
|
87
|
+
*
|
|
88
|
+
* Optional: when omitted, the workflow skips this step entirely
|
|
89
|
+
* (operators that don't want explicit-step recording leave it
|
|
90
|
+
* unset and rely on the subscriber).
|
|
91
|
+
*/
|
|
92
|
+
generateContractPdf?: (input: {
|
|
93
|
+
bookingId: string;
|
|
94
|
+
}) => Promise<{
|
|
95
|
+
contractId: string;
|
|
96
|
+
attachmentId: string;
|
|
97
|
+
} | null>;
|
|
98
|
+
/**
|
|
99
|
+
* Reconcile paid `payment_sessions` for the booking against the
|
|
100
|
+
* just-issued invoice: update each paid session's `invoice_id`
|
|
101
|
+
* pointer and write a `payments` row so the invoice flips to paid.
|
|
102
|
+
*
|
|
103
|
+
* The session was created at storefront-checkout time with
|
|
104
|
+
* `target_type: "booking"` and `invoice_id: NULL` because the
|
|
105
|
+
* invoice didn't exist yet. Without this back-link, the invoice
|
|
106
|
+
* permanently reads as unpaid even though the customer's money is
|
|
107
|
+
* sitting in the paid session.
|
|
108
|
+
*
|
|
109
|
+
* Idempotency: implementations should skip sessions that already
|
|
110
|
+
* have an `invoice_id` set or already have a `payment_id`. Returns
|
|
111
|
+
* the count of newly-linked sessions for observability.
|
|
112
|
+
*/
|
|
113
|
+
linkPaymentToInvoice?: (input: {
|
|
114
|
+
bookingId: string;
|
|
115
|
+
invoiceId: string;
|
|
116
|
+
/** Hint from the workflow input — when set, prefer linking this session. */
|
|
117
|
+
paymentSessionId?: string;
|
|
118
|
+
}) => Promise<{
|
|
119
|
+
paymentId: string | null;
|
|
120
|
+
sessionsLinked: number;
|
|
121
|
+
}>;
|
|
122
|
+
}
|
|
123
|
+
export declare const checkoutFinalizeWorkflow: import("@voyant-travel/core").WorkflowDefinition;
|
|
124
|
+
export interface RunCheckoutFinalizeOptions {
|
|
125
|
+
/**
|
|
126
|
+
* For resume runs — name of the step to resume from. Steps before
|
|
127
|
+
* this one are skipped and their outputs hydrated from
|
|
128
|
+
* {@link RunCheckoutFinalizeOptions.seedResults}.
|
|
129
|
+
*/
|
|
130
|
+
skipUntil?: string;
|
|
131
|
+
/** Step outputs from the parent run, keyed by step name. */
|
|
132
|
+
seedResults?: Record<string, unknown>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Run the workflow with deps seeded. Wraps `checkoutFinalizeWorkflow.run`
|
|
136
|
+
* with the dependency-injection plumbing — the workflow primitive
|
|
137
|
+
* doesn't carry a "deps" concept on its own, so we pass them through
|
|
138
|
+
* `ctx.results` keyed under `__deps`.
|
|
139
|
+
*
|
|
140
|
+
* Resume support: when `skipUntil` is set, the seeded `__deps` step
|
|
141
|
+
* is added to `seedResults` automatically so the resumed step still
|
|
142
|
+
* sees `ctx.results.__deps`. The caller doesn't need to know about
|
|
143
|
+
* the deps-injection mechanism.
|
|
144
|
+
*/
|
|
145
|
+
export declare function runCheckoutFinalize(input: CheckoutFinalizeInput, deps: CheckoutFinalizeDeps, options?: RunCheckoutFinalizeOptions): Promise<void>;
|
|
146
|
+
//# sourceMappingURL=checkout-finalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkout-finalize.d.ts","sourceRoot":"","sources":["../../src/booking-engine/checkout-finalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,MAAM,GAAG,kBAAkB,CAAA;CACvE;AAED;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7C,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACzF,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC7D;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,kBAAkB,CAAA;IACtB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,4BAA4B,CAAA;IACvC;;;;OAIG;IACH,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD;;;;;OAKG;IACH,YAAY,EAAE,CAAC,KAAK,EAAE;QACpB,SAAS,EAAE,MAAM,CAAA;QACjB,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KACvC,KAAK,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAC3C;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACrF;;;;;;;;;;;;;;;OAeG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE;QAC5B,SAAS,EAAE,MAAM,CAAA;KAClB,KAAK,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClE;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE;QAC7B,SAAS,EAAE,MAAM,CAAA;QACjB,SAAS,EAAE,MAAM,CAAA;QACjB,4EAA4E;QAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,KAAK,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACpE;AAyBD,eAAO,MAAM,wBAAwB,kDAgEnC,CAAA;AAEF,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACtC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,qBAAqB,EAC5B,IAAI,EAAE,oBAAoB,EAC1B,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAuBf"}
|