@voyant-travel/trips 0.111.1 → 0.113.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/catalog-component.d.ts +63 -0
- package/dist/catalog-component.d.ts.map +1 -0
- package/dist/catalog-component.js +349 -0
- package/dist/checkout/billing.d.ts +9 -0
- package/dist/checkout/billing.d.ts.map +1 -0
- package/dist/checkout/billing.js +54 -0
- package/dist/checkout/index.d.ts +5 -0
- package/dist/checkout/index.d.ts.map +1 -0
- package/dist/checkout/index.js +3 -0
- package/dist/checkout/pricing.d.ts +9 -0
- package/dist/checkout/pricing.d.ts.map +1 -0
- package/dist/checkout/pricing.js +100 -0
- package/dist/checkout/start-checkout.d.ts +14 -0
- package/dist/checkout/start-checkout.d.ts.map +1 -0
- package/dist/checkout/start-checkout.js +74 -0
- package/dist/checkout/types.d.ts +82 -0
- package/dist/checkout/types.d.ts.map +1 -0
- package/dist/checkout/types.js +1 -0
- package/dist/flight-component.d.ts +66 -0
- package/dist/flight-component.d.ts.map +1 -0
- package/dist/flight-component.js +177 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/mcp-routes.d.ts +30 -0
- package/dist/mcp-routes.d.ts.map +1 -0
- package/dist/mcp-routes.js +48 -0
- package/package.json +26 -3
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type CancelEntityResult, type OwnedBookingHandlerRegistry, type QuoteEntityDeps, type QuoteEntityResult, type QuoteResponseV1, type SourceAdapterRegistry } from "@voyant-travel/catalog/booking-engine";
|
|
2
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
3
|
+
import type { CancelComponentInput, CancelComponentResult, CatalogComponentQuoteInput, ComponentCancellationPreview, ComponentCancellationPreviewInput, ComponentCheckoutInput, ComponentCheckoutResult, ReleaseReservedComponentInput, ReleaseReservedComponentResult, ReserveComponentInput, ReserveComponentResult } from "./service-types.js";
|
|
4
|
+
/** Per-request adapter context propagated to the catalog source adapters. */
|
|
5
|
+
export interface CatalogAdapterContext {
|
|
6
|
+
connection_id: string;
|
|
7
|
+
correlation_id: string;
|
|
8
|
+
}
|
|
9
|
+
/** Deployment-specific checkout hand-off for a reserved component. */
|
|
10
|
+
export type StartComponentCheckout = (input: ComponentCheckoutInput) => Promise<ComponentCheckoutResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Deployment-supplied, request-scoped readers + registries for the catalog
|
|
13
|
+
* component adapter. These cross the boundaries trips must not import
|
|
14
|
+
* statically (process-local registries, commerce promotions, the deployment's
|
|
15
|
+
* tax recompute + checkout wiring) and so are injected.
|
|
16
|
+
*/
|
|
17
|
+
export interface CatalogComponentAdapterOptions {
|
|
18
|
+
/** The per-request drizzle handle. */
|
|
19
|
+
db: AnyDrizzleDb;
|
|
20
|
+
/** Process-local source-adapter registry (deployment-assembled). */
|
|
21
|
+
registry: SourceAdapterRegistry;
|
|
22
|
+
/** Process-local owned-handler registry (deployment-assembled). */
|
|
23
|
+
ownedHandlers: OwnedBookingHandlerRegistry;
|
|
24
|
+
/**
|
|
25
|
+
* Promotion evaluator for the quote path. Injected because
|
|
26
|
+
* `createCatalogPromotionEvaluator` lives in commerce, which transitively
|
|
27
|
+
* (optionally) depends on quotes → trips.
|
|
28
|
+
*/
|
|
29
|
+
evaluatePromotions: QuoteEntityDeps["evaluatePromotions"];
|
|
30
|
+
/**
|
|
31
|
+
* Customer-facing tax recompute applied to a quote result. Injected because
|
|
32
|
+
* the deployment resolves its own tax settings (a deployment reader) and the
|
|
33
|
+
* transform is shared with the catalog-booking route module.
|
|
34
|
+
*/
|
|
35
|
+
transformQuoteResult: (result: QuoteEntityResult, entityModule: string, entityId: string, sourceKind: string) => Promise<QuoteEntityResult>;
|
|
36
|
+
/** Builds the per-request adapter context (correlation id, connection id). */
|
|
37
|
+
adapterContext: (connectionId: string | null | undefined) => CatalogAdapterContext;
|
|
38
|
+
/** Deployment-specific checkout hand-off (payment-provider wiring). */
|
|
39
|
+
startCheckout: StartComponentCheckout;
|
|
40
|
+
}
|
|
41
|
+
/** The catalog component orchestration surface produced by the factory. */
|
|
42
|
+
export interface CatalogComponentAdapter {
|
|
43
|
+
quote(input: CatalogComponentQuoteInput): Promise<QuoteResponseV1>;
|
|
44
|
+
reserve(input: ReserveComponentInput): Promise<ReserveComponentResult>;
|
|
45
|
+
release(input: ReleaseReservedComponentInput): Promise<ReleaseReservedComponentResult>;
|
|
46
|
+
previewCancellation(input: ComponentCancellationPreviewInput): Promise<ComponentCancellationPreview>;
|
|
47
|
+
cancel(input: CancelComponentInput): Promise<CancelComponentResult>;
|
|
48
|
+
startCheckout(input: ComponentCheckoutInput): Promise<ComponentCheckoutResult>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the catalog-backed trip-component orchestration bound to a request's
|
|
52
|
+
* db + deployment registries/readers.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createCatalogComponentAdapter(options: CatalogComponentAdapterOptions): CatalogComponentAdapter;
|
|
55
|
+
/**
|
|
56
|
+
* Pure catalog-component cancellation preview. Has no db / registry reads —
|
|
57
|
+
* supplier cancellation previews aren't available, so the cancellation result
|
|
58
|
+
* itself is authoritative. Exported standalone so deployments can preview
|
|
59
|
+
* without constructing a request-scoped adapter.
|
|
60
|
+
*/
|
|
61
|
+
export declare function previewCancellation(input: ComponentCancellationPreviewInput): Promise<ComponentCancellationPreview>;
|
|
62
|
+
export type { CancelEntityResult };
|
|
63
|
+
//# sourceMappingURL=catalog-component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-component.d.ts","sourceRoot":"","sources":["../src/catalog-component.ts"],"names":[],"mappings":"AAsCA,OAAO,EAKL,KAAK,kBAAkB,EAEvB,KAAK,2BAA2B,EAEhC,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAGpB,KAAK,qBAAqB,EAC3B,MAAM,uCAAuC,CAAA;AAE9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,KAAK,EACV,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,EAC1B,4BAA4B,EAC5B,iCAAiC,EACjC,sBAAsB,EACtB,uBAAuB,EACvB,6BAA6B,EAC7B,8BAA8B,EAC9B,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,oBAAoB,CAAA;AAE3B,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,sEAAsE;AACtE,MAAM,MAAM,sBAAsB,GAAG,CACnC,KAAK,EAAE,sBAAsB,KAC1B,OAAO,CAAC,uBAAuB,CAAC,CAAA;AAErC;;;;;GAKG;AACH,MAAM,WAAW,8BAA8B;IAC7C,sCAAsC;IACtC,EAAE,EAAE,YAAY,CAAA;IAChB,oEAAoE;IACpE,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,mEAAmE;IACnE,aAAa,EAAE,2BAA2B,CAAA;IAC1C;;;;OAIG;IACH,kBAAkB,EAAE,eAAe,CAAC,oBAAoB,CAAC,CAAA;IACzD;;;;OAIG;IACH,oBAAoB,EAAE,CACpB,MAAM,EAAE,iBAAiB,EACzB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC/B,8EAA8E;IAC9E,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,qBAAqB,CAAA;IAClF,uEAAuE;IACvE,aAAa,EAAE,sBAAsB,CAAA;CACtC;AAED,2EAA2E;AAC3E,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAClE,OAAO,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACtE,OAAO,CAAC,KAAK,EAAE,6BAA6B,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAAA;IACtF,mBAAmB,CACjB,KAAK,EAAE,iCAAiC,GACvC,OAAO,CAAC,4BAA4B,CAAC,CAAA;IACxC,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACnE,aAAa,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;CAC/E;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,8BAA8B,GACtC,uBAAuB,CAuLzB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,iCAAiC,GACvC,OAAO,CAAC,4BAA4B,CAAC,CA6BvC;AAkJD,YAAY,EAAE,kBAAkB,EAAE,CAAA"}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-backed trip-component orchestration — owned by `@voyant-travel/trips`.
|
|
3
|
+
*
|
|
4
|
+
* Trips owns the reserve/checkout flow for catalog-backed components, so the
|
|
5
|
+
* orchestration that turns a `TripComponent` into a catalog booking-engine
|
|
6
|
+
* quote / reservation / cancellation lives here rather than in any deployment:
|
|
7
|
+
* - offer validation (`quote`) + customer-facing tax recompute hand-off,
|
|
8
|
+
* - reserve-with-origin-tracking (stamp the booking's catalog reservation
|
|
9
|
+
* origin so the component → booking link survives),
|
|
10
|
+
* - hold release (compensation) + cancellation preview + cancel,
|
|
11
|
+
* - checkout hand-off.
|
|
12
|
+
*
|
|
13
|
+
* WHY SOME PIECES ARE INJECTED (not imported):
|
|
14
|
+
*
|
|
15
|
+
* Trips depends acyclically on `@voyant-travel/catalog` (booking-engine) and
|
|
16
|
+
* `@voyant-travel/bookings` (origin upsert), so those are imported directly.
|
|
17
|
+
* Three things stay deployment-supplied and are injected via `options`:
|
|
18
|
+
*
|
|
19
|
+
* 1. The `SourceAdapterRegistry` / `OwnedBookingHandlerRegistry` — these are
|
|
20
|
+
* process-local registries assembled from a deployment's installed source
|
|
21
|
+
* adapters + owned vertical handlers. They live in the deployment.
|
|
22
|
+
* 2. The promotion evaluator — `createCatalogPromotionEvaluator` lives in
|
|
23
|
+
* `@voyant-travel/commerce`, and `commerce → quotes → trips` would make a
|
|
24
|
+
* package cycle. So the evaluator is injected.
|
|
25
|
+
* 3. The customer-facing tax recompute (`transformQuoteResult`) — the operator
|
|
26
|
+
* resolves its own tax settings (a deployment reader) and the transform is
|
|
27
|
+
* shared with the catalog-booking route module, so it stays in the
|
|
28
|
+
* deployment and is injected.
|
|
29
|
+
* 4. The checkout starter (`startCatalogCheckout`) — deployment-specific
|
|
30
|
+
* payment-provider wiring.
|
|
31
|
+
*
|
|
32
|
+
* Behaviour is byte-for-byte equivalent to the operator's previous
|
|
33
|
+
* `trips-catalog-runtime.ts`.
|
|
34
|
+
*/
|
|
35
|
+
import { toCatalogReservationBookingOriginInput, upsertBookingOrigin, } from "@voyant-travel/bookings";
|
|
36
|
+
import { bookEntity, bookingDraftV1, cancelEntity, quoteEntity, quoteResponseV1, } from "@voyant-travel/catalog/booking-engine";
|
|
37
|
+
import { toBookingDraftV1 } from "./catalog-component-adapter.js";
|
|
38
|
+
/**
|
|
39
|
+
* Build the catalog-backed trip-component orchestration bound to a request's
|
|
40
|
+
* db + deployment registries/readers.
|
|
41
|
+
*/
|
|
42
|
+
export function createCatalogComponentAdapter(options) {
|
|
43
|
+
const { db, registry, ownedHandlers, evaluatePromotions, transformQuoteResult, adapterContext } = options;
|
|
44
|
+
async function quote(input) {
|
|
45
|
+
const component = input.component;
|
|
46
|
+
const entityModule = required(component.entityModule, "component.entityModule");
|
|
47
|
+
const entityId = required(component.entityId, "component.entityId");
|
|
48
|
+
const sourceKind = required(component.sourceKind, "component.sourceKind");
|
|
49
|
+
const result = await quoteEntity(db, {
|
|
50
|
+
registry,
|
|
51
|
+
ownedHandlers,
|
|
52
|
+
evaluatePromotions,
|
|
53
|
+
}, {
|
|
54
|
+
entityModule,
|
|
55
|
+
entityId,
|
|
56
|
+
sourceKind,
|
|
57
|
+
sourceConnectionId: component.sourceConnectionId ?? undefined,
|
|
58
|
+
sourceRef: component.sourceRef ?? undefined,
|
|
59
|
+
scope: {
|
|
60
|
+
locale: input.scope.locale ?? "en-GB",
|
|
61
|
+
audience: input.scope.audience ?? "staff",
|
|
62
|
+
market: input.scope.market ?? "default",
|
|
63
|
+
currency: input.scope.currency,
|
|
64
|
+
},
|
|
65
|
+
parameters: engineParametersFromBookingDraft(undefined, input.bookingDraft),
|
|
66
|
+
ttlMs: input.ttlMs,
|
|
67
|
+
adapterContext: adapterContext(component.sourceConnectionId ?? sourceKind),
|
|
68
|
+
});
|
|
69
|
+
const transformed = await transformQuoteResult(result, entityModule, entityId, sourceKind);
|
|
70
|
+
return serializeQuoteResult(transformed);
|
|
71
|
+
}
|
|
72
|
+
async function reserve(input) {
|
|
73
|
+
const component = input.component;
|
|
74
|
+
const quoteId = required(component.catalogQuoteId, "component.catalogQuoteId");
|
|
75
|
+
const bookingDraft = bookingDraftFromComponent(component);
|
|
76
|
+
// The trips can start underlying bookings in draft status. When the
|
|
77
|
+
// operator leaves that option unchecked, the resulting booking lands in
|
|
78
|
+
// `awaiting_payment`. The owned products handler reads this off
|
|
79
|
+
// `request.parameters.initialStatus` and forwards it to the bridge.
|
|
80
|
+
const createAsDraft = readBoolean(input.envelope.constraints?.createAsDraft);
|
|
81
|
+
const initialStatus = createAsDraft ? "draft" : "awaiting_payment";
|
|
82
|
+
const result = await bookEntity(db, {
|
|
83
|
+
registry,
|
|
84
|
+
ownedHandlers,
|
|
85
|
+
}, {
|
|
86
|
+
quoteId,
|
|
87
|
+
party: {
|
|
88
|
+
draft: bookingDraft,
|
|
89
|
+
travelerParty: input.envelope.travelerParty,
|
|
90
|
+
},
|
|
91
|
+
paymentIntent: { type: "hold" },
|
|
92
|
+
parameters: { ...engineParametersFromBookingDraft(undefined, bookingDraft), initialStatus },
|
|
93
|
+
idempotencyKey: componentReserveIdempotencyKey(component.id, quoteId),
|
|
94
|
+
adapterContext: adapterContext(component.sourceConnectionId ?? component.sourceKind),
|
|
95
|
+
});
|
|
96
|
+
if (result.status === "failed") {
|
|
97
|
+
throw new Error("component_reservation_failed");
|
|
98
|
+
}
|
|
99
|
+
const orderRef = result.orderRef || result.snapshotId;
|
|
100
|
+
if (result.bookingId) {
|
|
101
|
+
await upsertBookingOrigin(db, toCatalogReservationBookingOriginInput({
|
|
102
|
+
bookingId: result.bookingId,
|
|
103
|
+
tripEnvelopeId: input.envelope.id,
|
|
104
|
+
tripComponentId: component.id,
|
|
105
|
+
reservationPlanId: input.reservationPlanId,
|
|
106
|
+
catalogPriceResponseId: quoteId,
|
|
107
|
+
catalogSnapshotId: result.snapshotId,
|
|
108
|
+
providerSourceKind: component.sourceKind,
|
|
109
|
+
providerSourceConnectionId: component.sourceConnectionId,
|
|
110
|
+
providerSourceRef: component.sourceRef,
|
|
111
|
+
providerOrderRef: orderRef,
|
|
112
|
+
metadata: {
|
|
113
|
+
entityModule: component.entityModule,
|
|
114
|
+
entityId: component.entityId,
|
|
115
|
+
createAsDraft,
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
status: bookStatusToComponentStatus(result.status),
|
|
121
|
+
bookingId: result.bookingId,
|
|
122
|
+
orderId: orderRef,
|
|
123
|
+
providerRef: orderRef,
|
|
124
|
+
supplierRef: orderRef,
|
|
125
|
+
warnings: result.status === "held" ? undefined : [`booking_engine_status:${result.status}`],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function release(input) {
|
|
129
|
+
const component = input.component;
|
|
130
|
+
if (!component.bookingId || !component.entityModule || !component.entityId) {
|
|
131
|
+
return { released: false, reason: "missing_component_booking_ref" };
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const result = await cancelEntity(db, { registry }, {
|
|
135
|
+
bookingId: component.bookingId,
|
|
136
|
+
entityModule: component.entityModule,
|
|
137
|
+
entityId: component.entityId,
|
|
138
|
+
reason: "Trips compensation",
|
|
139
|
+
adapterContext: adapterContext(component.sourceConnectionId ?? component.sourceKind),
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
released: result.status === "cancelled",
|
|
143
|
+
reason: result.status === "refused" ? "cancel_refused" : undefined,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
released: false,
|
|
149
|
+
reason: error instanceof Error ? error.message : "release_failed",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function cancel(input) {
|
|
154
|
+
const component = input.component;
|
|
155
|
+
if (!component.bookingId || !component.entityModule || !component.entityId) {
|
|
156
|
+
return { status: "refused", reason: "missing_component_booking_ref" };
|
|
157
|
+
}
|
|
158
|
+
const result = await cancelEntity(db, { registry }, {
|
|
159
|
+
bookingId: component.bookingId,
|
|
160
|
+
entityModule: component.entityModule,
|
|
161
|
+
entityId: component.entityId,
|
|
162
|
+
reason: input.reason,
|
|
163
|
+
adapterContext: adapterContext(component.sourceConnectionId ?? component.sourceKind),
|
|
164
|
+
});
|
|
165
|
+
// Catalog adapters can return "pending" when an async cancel was submitted
|
|
166
|
+
// (email/partner-portal/batch) and the inventory hasn't been released yet.
|
|
167
|
+
// The trips's `CancelComponentResult` doesn't model that state;
|
|
168
|
+
// surface it as `refused` with a reason so the trip lands in remediation
|
|
169
|
+
// and the operator follows up out-of-band. `pending_channel` flows through
|
|
170
|
+
// the reason so the UI can show where the request went.
|
|
171
|
+
const status = result.status === "pending" ? "refused" : result.status;
|
|
172
|
+
const reason = result.status === "cancelled"
|
|
173
|
+
? undefined
|
|
174
|
+
: result.status === "pending"
|
|
175
|
+
? `cancel_pending${result.pendingChannel ? `:${result.pendingChannel}` : ""}`
|
|
176
|
+
: `cancel_${result.status}`;
|
|
177
|
+
return {
|
|
178
|
+
status,
|
|
179
|
+
refundAmountCents: result.refundAmount,
|
|
180
|
+
refundCurrency: result.refundCurrency,
|
|
181
|
+
reason,
|
|
182
|
+
snapshot: { snapshotId: result.snapshotId },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function startCheckout(input) {
|
|
186
|
+
return options.startCheckout(input);
|
|
187
|
+
}
|
|
188
|
+
return { quote, reserve, release, previewCancellation, cancel, startCheckout };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Pure catalog-component cancellation preview. Has no db / registry reads —
|
|
192
|
+
* supplier cancellation previews aren't available, so the cancellation result
|
|
193
|
+
* itself is authoritative. Exported standalone so deployments can preview
|
|
194
|
+
* without constructing a request-scoped adapter.
|
|
195
|
+
*/
|
|
196
|
+
export function previewCancellation(input) {
|
|
197
|
+
const component = input.component;
|
|
198
|
+
if (!component.bookingId || !component.entityModule || !component.entityId) {
|
|
199
|
+
return Promise.resolve({
|
|
200
|
+
componentId: component.id,
|
|
201
|
+
action: "staff_remediation",
|
|
202
|
+
currentStatus: component.status,
|
|
203
|
+
staffActionRequired: true,
|
|
204
|
+
reason: "missing_component_booking_ref",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return Promise.resolve({
|
|
208
|
+
componentId: component.id,
|
|
209
|
+
action: "cancel",
|
|
210
|
+
currentStatus: component.status,
|
|
211
|
+
staffActionRequired: false,
|
|
212
|
+
refundAmountCents: 0,
|
|
213
|
+
refundCurrency: component.componentCurrency ?? undefined,
|
|
214
|
+
penaltyAmountCents: 0,
|
|
215
|
+
policySummary: "Supplier cancellation preview is not available; cancellation result is authoritative.",
|
|
216
|
+
snapshot: {
|
|
217
|
+
bookingId: component.bookingId,
|
|
218
|
+
entityModule: component.entityModule,
|
|
219
|
+
entityId: component.entityId,
|
|
220
|
+
sourceKind: component.sourceKind,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// ── Pure helpers (vertical-agnostic) ────────────────────────────────────────
|
|
225
|
+
function bookingDraftFromComponent(component) {
|
|
226
|
+
const metadata = component.metadata;
|
|
227
|
+
const candidate = metadata.bookingDraftV1 ?? metadata.bookingDraft;
|
|
228
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
229
|
+
return bookingDraftV1.parse(candidate);
|
|
230
|
+
}
|
|
231
|
+
return toBookingDraftV1(component);
|
|
232
|
+
}
|
|
233
|
+
function serializeQuoteResult(result) {
|
|
234
|
+
return quoteResponseV1.parse({
|
|
235
|
+
...result,
|
|
236
|
+
quotedAt: result.quotedAt.toISOString(),
|
|
237
|
+
expiresAt: result.expiresAt.toISOString(),
|
|
238
|
+
pricing: toPricingBreakdownV1(result.pricing),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function toPricingBreakdownV1(basis) {
|
|
242
|
+
if (!basis)
|
|
243
|
+
return undefined;
|
|
244
|
+
if (basis.breakdown) {
|
|
245
|
+
const breakdown = basis.breakdown;
|
|
246
|
+
if (breakdown.currency && Array.isArray(breakdown.lines) && Array.isArray(breakdown.taxes)) {
|
|
247
|
+
return breakdown;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const lines = [
|
|
251
|
+
{
|
|
252
|
+
kind: "base",
|
|
253
|
+
label: "Base",
|
|
254
|
+
quantity: 1,
|
|
255
|
+
unitAmount: basis.base_amount,
|
|
256
|
+
totalAmount: basis.base_amount,
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
if (basis.fees > 0) {
|
|
260
|
+
lines.push({ kind: "fee", label: "Fees", unitAmount: basis.fees, totalAmount: basis.fees });
|
|
261
|
+
}
|
|
262
|
+
if (basis.surcharges > 0) {
|
|
263
|
+
lines.push({
|
|
264
|
+
kind: "supplement",
|
|
265
|
+
label: "Surcharges",
|
|
266
|
+
unitAmount: basis.surcharges,
|
|
267
|
+
totalAmount: basis.surcharges,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const subtotal = basis.base_amount + basis.fees + basis.surcharges;
|
|
271
|
+
return {
|
|
272
|
+
currency: basis.currency,
|
|
273
|
+
lines,
|
|
274
|
+
taxes: basis.taxes > 0
|
|
275
|
+
? [
|
|
276
|
+
{
|
|
277
|
+
code: "tax",
|
|
278
|
+
label: "Tax",
|
|
279
|
+
rate: 0,
|
|
280
|
+
amount: basis.taxes,
|
|
281
|
+
base: basis.base_amount,
|
|
282
|
+
},
|
|
283
|
+
]
|
|
284
|
+
: [],
|
|
285
|
+
subtotal,
|
|
286
|
+
taxTotal: basis.taxes,
|
|
287
|
+
total: subtotal + basis.taxes,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function bookStatusToComponentStatus(status) {
|
|
291
|
+
return status === "held" ? "held" : "booked";
|
|
292
|
+
}
|
|
293
|
+
function componentReserveIdempotencyKey(componentId, quoteId) {
|
|
294
|
+
return `trips:${componentId}:${quoteId}`.slice(0, 128);
|
|
295
|
+
}
|
|
296
|
+
function engineParametersFromBookingDraft(parameters, bookingDraftPayload) {
|
|
297
|
+
const bookingDraft = asRecord(bookingDraftPayload);
|
|
298
|
+
const configure = asRecord(bookingDraft?.configure);
|
|
299
|
+
const departureSlotId = stringValue(configure?.departureSlotId);
|
|
300
|
+
const paxCount = sumBookingDraftPax(configure?.pax);
|
|
301
|
+
const next = {
|
|
302
|
+
...(parameters ?? {}),
|
|
303
|
+
...(bookingDraft ? { draft: bookingDraft } : {}),
|
|
304
|
+
};
|
|
305
|
+
if (departureSlotId) {
|
|
306
|
+
if (next.departureSlotId == null)
|
|
307
|
+
next.departureSlotId = departureSlotId;
|
|
308
|
+
if (next.departure_id == null)
|
|
309
|
+
next.departure_id = departureSlotId;
|
|
310
|
+
if (next.slotId == null)
|
|
311
|
+
next.slotId = departureSlotId;
|
|
312
|
+
}
|
|
313
|
+
if (paxCount > 0 && next.paxCount == null) {
|
|
314
|
+
next.paxCount = paxCount;
|
|
315
|
+
}
|
|
316
|
+
const promotionCode = stringValue(bookingDraft?.promotionCode);
|
|
317
|
+
if (promotionCode && next.promotionCode == null) {
|
|
318
|
+
next.promotionCode = promotionCode;
|
|
319
|
+
}
|
|
320
|
+
return next;
|
|
321
|
+
}
|
|
322
|
+
function readBoolean(value) {
|
|
323
|
+
return value === true;
|
|
324
|
+
}
|
|
325
|
+
function asRecord(value) {
|
|
326
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
327
|
+
? value
|
|
328
|
+
: undefined;
|
|
329
|
+
}
|
|
330
|
+
function stringValue(value) {
|
|
331
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
332
|
+
}
|
|
333
|
+
function sumBookingDraftPax(value) {
|
|
334
|
+
const pax = asRecord(value);
|
|
335
|
+
if (!pax)
|
|
336
|
+
return 0;
|
|
337
|
+
let total = 0;
|
|
338
|
+
for (const count of Object.values(pax)) {
|
|
339
|
+
if (typeof count === "number" && Number.isFinite(count) && count > 0) {
|
|
340
|
+
total += count;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return total;
|
|
344
|
+
}
|
|
345
|
+
function required(value, label) {
|
|
346
|
+
if (!value)
|
|
347
|
+
throw new Error(`${label} is required`);
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SynthesizedTripBilling, TripBillingInfo } from "./types.js";
|
|
2
|
+
export declare function readTripBilling(travelerParty: Record<string, unknown>): TripBillingInfo;
|
|
3
|
+
export declare function formatTripBillingName(billing: TripBillingInfo): string | null;
|
|
4
|
+
export declare function synthesizeTripBilling(billing: TripBillingInfo): SynthesizedTripBilling;
|
|
5
|
+
export declare function splitTripBillingName(value: string): {
|
|
6
|
+
firstName: string;
|
|
7
|
+
lastName: string;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=billing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../../src/checkout/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEzE,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,CAcvF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAS7E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,GAAG,sBAAsB,CAatF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAM3F"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function readTripBilling(travelerParty) {
|
|
2
|
+
const billing = asRecord(travelerParty.billing);
|
|
3
|
+
const contact = asRecord(billing?.contact);
|
|
4
|
+
return {
|
|
5
|
+
buyerType: stringValue(billing?.buyerType),
|
|
6
|
+
personId: stringValue(billing?.personId),
|
|
7
|
+
organizationId: stringValue(billing?.organizationId),
|
|
8
|
+
contact: {
|
|
9
|
+
firstName: stringValue(contact?.firstName),
|
|
10
|
+
lastName: stringValue(contact?.lastName),
|
|
11
|
+
email: stringValue(contact?.email),
|
|
12
|
+
phone: stringValue(contact?.phone),
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function formatTripBillingName(billing) {
|
|
17
|
+
return [billing.contact?.firstName, billing.contact?.lastName]
|
|
18
|
+
.filter((part) => Boolean(part))
|
|
19
|
+
.join(" ")
|
|
20
|
+
.trim()
|
|
21
|
+
? [billing.contact?.firstName, billing.contact?.lastName]
|
|
22
|
+
.filter((part) => Boolean(part))
|
|
23
|
+
.join(" ")
|
|
24
|
+
: null;
|
|
25
|
+
}
|
|
26
|
+
export function synthesizeTripBilling(billing) {
|
|
27
|
+
const names = splitTripBillingName(formatTripBillingName(billing) ?? "Trip customer");
|
|
28
|
+
return {
|
|
29
|
+
email: billing.contact?.email ?? "",
|
|
30
|
+
phone: billing.contact?.phone ?? "0000000000",
|
|
31
|
+
firstName: names.firstName,
|
|
32
|
+
lastName: names.lastName,
|
|
33
|
+
city: "TBD",
|
|
34
|
+
country: 642,
|
|
35
|
+
state: "TBD",
|
|
36
|
+
postalCode: "00000",
|
|
37
|
+
details: "Pending — customer to confirm at payment.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function splitTripBillingName(value) {
|
|
41
|
+
const parts = value.trim().split(/\s+/).filter(Boolean);
|
|
42
|
+
return {
|
|
43
|
+
firstName: parts[0] ?? "Trip",
|
|
44
|
+
lastName: parts.slice(1).join(" ") || "Customer",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function asRecord(value) {
|
|
48
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
49
|
+
? value
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
function stringValue(value) {
|
|
53
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
54
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { formatTripBillingName, readTripBilling, splitTripBillingName, synthesizeTripBilling, } from "./billing.js";
|
|
2
|
+
export { buildTripPaymentSummary, checkoutPricingForTrip } from "./pricing.js";
|
|
3
|
+
export { startTripCheckout } from "./start-checkout.js";
|
|
4
|
+
export type { FxQuote, SynthesizedTripBilling, Trip, TripBillingInfo, TripCheckoutAllocation, TripCheckoutDeps, TripCheckoutInput, TripCheckoutResult, } from "./types.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checkout/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,YAAY,EACV,OAAO,EACP,sBAAsB,EACtB,IAAI,EACJ,eAAe,EACf,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Trip } from "../service.js";
|
|
2
|
+
import type { TripCheckoutAllocation, TripCheckoutDeps } from "./types.js";
|
|
3
|
+
export declare function checkoutPricingForTrip(quoteFx: TripCheckoutDeps["quoteFx"], trip: Trip, request: Record<string, unknown>): Promise<{
|
|
4
|
+
currency: string;
|
|
5
|
+
totalAmountCents: number;
|
|
6
|
+
allocations: TripCheckoutAllocation[];
|
|
7
|
+
}>;
|
|
8
|
+
export declare function buildTripPaymentSummary(trip: Trip, currency: string, allocations?: TripCheckoutAllocation[]): string;
|
|
9
|
+
//# sourceMappingURL=pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/checkout/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAE1E,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,EACpC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC;IACT,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,EAAE,MAAM,CAAA;IACxB,WAAW,EAAE,sBAAsB,EAAE,CAAA;CACtC,CAAC,CAsDD;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,sBAAsB,EAAE,GACrC,MAAM,CAwCR"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export async function checkoutPricingForTrip(quoteFx, trip, request) {
|
|
2
|
+
const active = trip.components.filter((component) => component.status !== "removed" && component.status !== "cancelled");
|
|
3
|
+
const collectionCurrency = stringValue(request.collectionCurrency) ?? trip.envelope.aggregateCurrency ?? "EUR";
|
|
4
|
+
const allocations = [];
|
|
5
|
+
for (const component of active) {
|
|
6
|
+
const sourceCurrency = component.componentCurrency ?? collectionCurrency;
|
|
7
|
+
const sourceAmountCents = component.componentTotalAmountCents ?? 0;
|
|
8
|
+
if (sourceAmountCents <= 0)
|
|
9
|
+
continue;
|
|
10
|
+
if (sourceCurrency === collectionCurrency) {
|
|
11
|
+
allocations.push({
|
|
12
|
+
componentId: component.id,
|
|
13
|
+
kind: component.kind,
|
|
14
|
+
bookingId: component.bookingId,
|
|
15
|
+
orderId: component.orderId,
|
|
16
|
+
sourceCurrency,
|
|
17
|
+
sourceAmountCents,
|
|
18
|
+
targetCurrency: collectionCurrency,
|
|
19
|
+
targetAmountCents: sourceAmountCents,
|
|
20
|
+
});
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const fx = await quoteFx(sourceCurrency, collectionCurrency);
|
|
24
|
+
allocations.push({
|
|
25
|
+
componentId: component.id,
|
|
26
|
+
kind: component.kind,
|
|
27
|
+
bookingId: component.bookingId,
|
|
28
|
+
orderId: component.orderId,
|
|
29
|
+
sourceCurrency,
|
|
30
|
+
sourceAmountCents,
|
|
31
|
+
targetCurrency: collectionCurrency,
|
|
32
|
+
targetAmountCents: convertCents(sourceAmountCents, fx.rate),
|
|
33
|
+
fx: {
|
|
34
|
+
rate: fx.rate,
|
|
35
|
+
provider: "voyant_data_fx",
|
|
36
|
+
quotedAt: fx.quotedAt,
|
|
37
|
+
validUntil: fx.validUntil,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
currency: collectionCurrency,
|
|
43
|
+
totalAmountCents: allocations.reduce((sum, allocation) => sum + allocation.targetAmountCents, 0),
|
|
44
|
+
allocations,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function buildTripPaymentSummary(trip, currency, allocations) {
|
|
48
|
+
const lines = ["Trip payment summary"];
|
|
49
|
+
const byComponentId = new Map(allocations?.map((allocation) => [allocation.componentId, allocation]));
|
|
50
|
+
for (const component of trip.components.filter((item) => item.status !== "removed" && item.status !== "cancelled")) {
|
|
51
|
+
const allocation = byComponentId.get(component.id);
|
|
52
|
+
if (allocation?.fx) {
|
|
53
|
+
lines.push(`${componentDisplayName(component)} — ${formatCents(allocation.sourceAmountCents, allocation.sourceCurrency)} -> ${formatCents(allocation.targetAmountCents, allocation.targetCurrency)}`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
lines.push(`${componentDisplayName(component)} — ${formatCents(allocation?.targetAmountCents ?? component.componentTotalAmountCents, allocation?.targetCurrency ?? component.componentCurrency ?? currency)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const total = allocations?.reduce((sum, allocation) => sum + allocation.targetAmountCents, 0);
|
|
60
|
+
lines.push(`Total payable — ${formatCents(total ?? trip.envelope.aggregateTotalAmountCents, currency)}`);
|
|
61
|
+
const fxAllocations = allocations?.filter((allocation) => allocation.fx) ?? [];
|
|
62
|
+
if (fxAllocations.length > 0) {
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push("FX rates");
|
|
65
|
+
for (const allocation of fxAllocations) {
|
|
66
|
+
lines.push(`${allocation.sourceCurrency}->${allocation.targetCurrency}: ${allocation.fx?.rate} quoted ${allocation.fx?.quotedAt}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
function componentDisplayName(component) {
|
|
72
|
+
const metadata = asRecord(component.metadata);
|
|
73
|
+
const catalogItem = asRecord(metadata?.catalogItem);
|
|
74
|
+
const flightDraft = asRecord(metadata?.flightDraft);
|
|
75
|
+
const origin = stringValue(flightDraft?.origin);
|
|
76
|
+
const destination = stringValue(flightDraft?.destination);
|
|
77
|
+
if (origin && destination)
|
|
78
|
+
return `${origin} -> ${destination}`;
|
|
79
|
+
return (stringValue(catalogItem?.name) ||
|
|
80
|
+
stringValue(component.title) ||
|
|
81
|
+
stringValue(component.description) ||
|
|
82
|
+
component.kind.replaceAll("_", " "));
|
|
83
|
+
}
|
|
84
|
+
function formatCents(amountCents, currency) {
|
|
85
|
+
return ((amountCents ?? 0) / 100).toLocaleString("en-GB", {
|
|
86
|
+
style: "currency",
|
|
87
|
+
currency,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function convertCents(amountCents, rate) {
|
|
91
|
+
return Math.round(amountCents * rate);
|
|
92
|
+
}
|
|
93
|
+
function asRecord(value) {
|
|
94
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
95
|
+
? value
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
|
98
|
+
function stringValue(value) {
|
|
99
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
100
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TripCheckoutInput, TripCheckoutResult } from "../service.js";
|
|
2
|
+
import type { TripCheckoutDeps } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Orchestrate a trip checkout: price the trip's components into the collection
|
|
5
|
+
* currency, create the finance payment session, optionally start the payment
|
|
6
|
+
* provider, and return the customer-facing checkout link / bank-transfer
|
|
7
|
+
* instructions.
|
|
8
|
+
*
|
|
9
|
+
* Finance (`createPaymentSession` + `buildPaymentLinkUrl`) is composed
|
|
10
|
+
* directly. The deployment supplies FX quoting, the checkout base URL, and the
|
|
11
|
+
* payment-provider start via `deps`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function startTripCheckout(deps: TripCheckoutDeps, input: TripCheckoutInput): Promise<TripCheckoutResult>;
|
|
14
|
+
//# sourceMappingURL=start-checkout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start-checkout.d.ts","sourceRoot":"","sources":["../../src/checkout/start-checkout.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAG1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAElD;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CA8D7B"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { buildPaymentLinkUrl, financeService } from "@voyant-travel/finance";
|
|
2
|
+
import { formatTripBillingName, readTripBilling, synthesizeTripBilling } from "./billing.js";
|
|
3
|
+
import { buildTripPaymentSummary, checkoutPricingForTrip } from "./pricing.js";
|
|
4
|
+
/**
|
|
5
|
+
* Orchestrate a trip checkout: price the trip's components into the collection
|
|
6
|
+
* currency, create the finance payment session, optionally start the payment
|
|
7
|
+
* provider, and return the customer-facing checkout link / bank-transfer
|
|
8
|
+
* instructions.
|
|
9
|
+
*
|
|
10
|
+
* Finance (`createPaymentSession` + `buildPaymentLinkUrl`) is composed
|
|
11
|
+
* directly. The deployment supplies FX quoting, the checkout base URL, and the
|
|
12
|
+
* payment-provider start via `deps`.
|
|
13
|
+
*/
|
|
14
|
+
export async function startTripCheckout(deps, input) {
|
|
15
|
+
const db = deps.db;
|
|
16
|
+
const pricing = await checkoutPricingForTrip(deps.quoteFx, input.trip, input.request);
|
|
17
|
+
if (pricing.totalAmountCents <= 0) {
|
|
18
|
+
throw new Error("trip_checkout_total_required");
|
|
19
|
+
}
|
|
20
|
+
const billing = readTripBilling(input.trip.envelope.travelerParty);
|
|
21
|
+
const payerName = formatTripBillingName(billing);
|
|
22
|
+
const payerEmail = billing.contact?.email ?? null;
|
|
23
|
+
if (!payerName || !payerEmail) {
|
|
24
|
+
throw new Error("trip_checkout_billing_required");
|
|
25
|
+
}
|
|
26
|
+
const paymentMethod = input.intent === "bank_transfer" ? "bank_transfer" : "credit_card";
|
|
27
|
+
const session = await financeService.createPaymentSession(db, {
|
|
28
|
+
targetType: "other",
|
|
29
|
+
targetId: input.trip.envelope.id,
|
|
30
|
+
idempotencyKey: `trip-checkout:${input.trip.envelope.id}:${pricing.currency}:${pricing.totalAmountCents}`,
|
|
31
|
+
clientReference: input.trip.envelope.id,
|
|
32
|
+
currency: pricing.currency,
|
|
33
|
+
amountCents: pricing.totalAmountCents,
|
|
34
|
+
status: "pending",
|
|
35
|
+
provider: input.intent === "bank_transfer" ? null : "netopia",
|
|
36
|
+
paymentMethod,
|
|
37
|
+
payerPersonId: billing.personId ?? null,
|
|
38
|
+
payerOrganizationId: billing.organizationId ?? null,
|
|
39
|
+
payerEmail,
|
|
40
|
+
payerName,
|
|
41
|
+
notes: buildTripPaymentSummary(input.trip, pricing.currency, pricing.allocations),
|
|
42
|
+
metadata: {
|
|
43
|
+
tripEnvelopeId: input.trip.envelope.id,
|
|
44
|
+
collectionCurrency: pricing.currency,
|
|
45
|
+
componentAllocations: pricing.allocations,
|
|
46
|
+
fxAllocations: pricing.allocations.filter((allocation) => allocation.fx),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
if (!session) {
|
|
50
|
+
throw new Error("trip_checkout_session_create_failed");
|
|
51
|
+
}
|
|
52
|
+
if (input.intent !== "bank_transfer") {
|
|
53
|
+
try {
|
|
54
|
+
if (deps.startProviderPayment) {
|
|
55
|
+
await deps.startProviderPayment({
|
|
56
|
+
paymentSessionId: session.id,
|
|
57
|
+
billing: synthesizeTripBilling(billing),
|
|
58
|
+
description: `Trip ${input.trip.envelope.id}`,
|
|
59
|
+
trip: input.trip,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.warn("[trips] netopia start failed for trip payment session:", error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
kind: input.intent === "bank_transfer" ? "bank_transfer_instructions" : "payment_session",
|
|
69
|
+
paymentSessionId: session.id,
|
|
70
|
+
checkoutUrl: buildPaymentLinkUrl(session.id, {
|
|
71
|
+
baseUrl: deps.resolveCheckoutBaseUrl(),
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Trip, TripCheckoutInput, TripCheckoutResult } from "../service.js";
|
|
2
|
+
/** A per-component currency allocation produced while pricing a trip checkout. */
|
|
3
|
+
export interface TripCheckoutAllocation {
|
|
4
|
+
componentId: string;
|
|
5
|
+
kind: string;
|
|
6
|
+
bookingId: string | null;
|
|
7
|
+
orderId: string | null;
|
|
8
|
+
sourceCurrency: string;
|
|
9
|
+
sourceAmountCents: number;
|
|
10
|
+
targetCurrency: string;
|
|
11
|
+
targetAmountCents: number;
|
|
12
|
+
fx?: {
|
|
13
|
+
rate: number;
|
|
14
|
+
provider: "voyant_data_fx";
|
|
15
|
+
quotedAt: string;
|
|
16
|
+
validUntil?: string | null;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** A resolved FX quote between two currencies. */
|
|
20
|
+
export interface FxQuote {
|
|
21
|
+
rate: number;
|
|
22
|
+
quotedAt: string;
|
|
23
|
+
validUntil?: string | null;
|
|
24
|
+
}
|
|
25
|
+
/** Billing details extracted from a trip's traveler party. */
|
|
26
|
+
export interface TripBillingInfo {
|
|
27
|
+
buyerType?: string | null;
|
|
28
|
+
personId?: string | null;
|
|
29
|
+
organizationId?: string | null;
|
|
30
|
+
contact?: {
|
|
31
|
+
firstName?: string | null;
|
|
32
|
+
lastName?: string | null;
|
|
33
|
+
email?: string | null;
|
|
34
|
+
phone?: string | null;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Synthesized billing payload handed to a payment provider. */
|
|
38
|
+
export interface SynthesizedTripBilling {
|
|
39
|
+
email: string;
|
|
40
|
+
phone: string;
|
|
41
|
+
firstName: string;
|
|
42
|
+
lastName: string;
|
|
43
|
+
city: string;
|
|
44
|
+
country: number;
|
|
45
|
+
state: string;
|
|
46
|
+
postalCode: string;
|
|
47
|
+
details: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Deployment-supplied dependencies for the trip-checkout orchestration. The
|
|
51
|
+
* trips package composes finance (`createPaymentSession` + payment-link URL)
|
|
52
|
+
* directly, but everything that is a deployment provider choice — FX rates,
|
|
53
|
+
* the public checkout base URL, and the payment-provider start (Netopia) —
|
|
54
|
+
* is injected here.
|
|
55
|
+
*/
|
|
56
|
+
export interface TripCheckoutDeps {
|
|
57
|
+
/** The finance-compatible drizzle db used for `createPaymentSession`. */
|
|
58
|
+
db: unknown;
|
|
59
|
+
/**
|
|
60
|
+
* Quote an FX rate from `sourceCurrency` to `targetCurrency`. Called only
|
|
61
|
+
* when a component's currency differs from the collection currency.
|
|
62
|
+
*/
|
|
63
|
+
quoteFx(sourceCurrency: string, targetCurrency: string): Promise<FxQuote>;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the customer-facing checkout base URL used to build the payment
|
|
66
|
+
* link. May return `null` (the helper falls back to a root-relative URL).
|
|
67
|
+
*/
|
|
68
|
+
resolveCheckoutBaseUrl(): string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Start the payment-provider session for a non-bank-transfer checkout. This
|
|
71
|
+
* is best-effort; the orchestration logs and continues on failure. Omit to
|
|
72
|
+
* skip provider start entirely.
|
|
73
|
+
*/
|
|
74
|
+
startProviderPayment?(args: {
|
|
75
|
+
paymentSessionId: string;
|
|
76
|
+
billing: SynthesizedTripBilling;
|
|
77
|
+
description: string;
|
|
78
|
+
trip: Trip;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
export type { Trip, TripCheckoutInput, TripCheckoutResult };
|
|
82
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/checkout/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAEhF,kFAAkF;AAClF,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,EAAE,CAAC,EAAE;QACH,IAAI,EAAE,MAAM,CAAA;QACZ,QAAQ,EAAE,gBAAgB,CAAA;QAC1B,QAAQ,EAAE,MAAM,CAAA;QAChB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B,CAAA;CACF;AAED,kDAAkD;AAClD,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,OAAO,CAAC,EAAE;QACR,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KACtB,CAAA;CACF;AAED,gEAAgE;AAChE,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,EAAE,EAAE,OAAO,CAAA;IACX;;;OAGG;IACH,OAAO,CAAC,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACzE;;;OAGG;IACH,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAAA;IACvC;;;;OAIG;IACH,oBAAoB,CAAC,CAAC,IAAI,EAAE;QAC1B,gBAAgB,EAAE,MAAM,CAAA;QACxB,OAAO,EAAE,sBAAsB,CAAA;QAC/B,WAAW,EAAE,MAAM,CAAA;QACnB,IAAI,EAAE,IAAI,CAAA;KACX,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClB;AAED,YAAY,EAAE,IAAI,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-catalog (flight) trip-component orchestration — owned by
|
|
3
|
+
* `@voyant-travel/trips`.
|
|
4
|
+
*
|
|
5
|
+
* Trips owns the reserve/checkout flow for flight placeholder components, so the
|
|
6
|
+
* orchestration that prices a selected flight offer before reserve (detecting
|
|
7
|
+
* price changes / expiry) and books the held flight order lives here:
|
|
8
|
+
* - flight preflight + price-change detection (`validateBeforeReserve`),
|
|
9
|
+
* - passenger-roster building (DOB / contact fallbacks) + billing mapping,
|
|
10
|
+
* - reserve (book the held flight order).
|
|
11
|
+
*
|
|
12
|
+
* WHY THE FLIGHT ADAPTER IS INJECTED (not imported):
|
|
13
|
+
*
|
|
14
|
+
* Trips reads flight *contract types* from `@voyant-travel/flights` (a leaf
|
|
15
|
+
* contract dependency, acyclic), but the concrete adapter
|
|
16
|
+
* (`createDemoFlightAdapter` from `@voyant-travel/plugin-flights-demo`) is
|
|
17
|
+
* deployment-specific provider wiring — which provider, which base URL — so it
|
|
18
|
+
* is injected via `options.adapter` rather than imported. The price/expiry
|
|
19
|
+
* detection, passenger mapping, and billing fallbacks are vertical-agnostic
|
|
20
|
+
* orchestration and live here.
|
|
21
|
+
*
|
|
22
|
+
* Behaviour is byte-for-byte equivalent to the operator's previous
|
|
23
|
+
* `trips-flight-runtime.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import type { FlightBookRequest, FlightOffer, FlightOrder } from "@voyant-travel/flights/contract/types";
|
|
26
|
+
import type { ReserveComponentInput, ReserveComponentPreflightResult, ReserveComponentResult } from "./service-types.js";
|
|
27
|
+
/** Per-request adapter context propagated to the flight adapter. */
|
|
28
|
+
export interface FlightAdapterContext {
|
|
29
|
+
connectionId: string;
|
|
30
|
+
correlationId?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The minimal flight-adapter surface the orchestration needs. Injected because
|
|
34
|
+
* the concrete adapter is deployment-specific provider wiring.
|
|
35
|
+
*/
|
|
36
|
+
export interface FlightComponentAdapter {
|
|
37
|
+
priceOffer(ctx: FlightAdapterContext, request: {
|
|
38
|
+
offerId: string;
|
|
39
|
+
offer?: FlightOffer;
|
|
40
|
+
}): Promise<{
|
|
41
|
+
offer: FlightOffer;
|
|
42
|
+
valid: boolean;
|
|
43
|
+
invalidReason?: string;
|
|
44
|
+
}>;
|
|
45
|
+
bookFlight(ctx: FlightAdapterContext, request: FlightBookRequest): Promise<{
|
|
46
|
+
order: FlightOrder;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
/** Deployment-supplied, request-scoped flight wiring. */
|
|
50
|
+
export interface FlightComponentAdapterOptions {
|
|
51
|
+
/** The deployment-specific flight adapter (provider + base URL). */
|
|
52
|
+
adapter: FlightComponentAdapter;
|
|
53
|
+
/** Per-request adapter context (connection id + correlation id). */
|
|
54
|
+
adapterContext: FlightAdapterContext;
|
|
55
|
+
}
|
|
56
|
+
/** The flight component orchestration surface produced by the factory. */
|
|
57
|
+
export interface FlightComponentAdapterApi {
|
|
58
|
+
validateBeforeReserve(input: ReserveComponentInput): Promise<ReserveComponentPreflightResult | null>;
|
|
59
|
+
reserve(input: ReserveComponentInput): Promise<ReserveComponentResult | null>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build the flight (non-catalog) trip-component orchestration bound to a
|
|
63
|
+
* request's flight adapter + adapter context.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createFlightComponentAdapter(options: FlightComponentAdapterOptions): FlightComponentAdapterApi;
|
|
66
|
+
//# sourceMappingURL=flight-component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flight-component.d.ts","sourceRoot":"","sources":["../src/flight-component.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAEV,iBAAiB,EACjB,WAAW,EACX,WAAW,EAGZ,MAAM,uCAAuC,CAAA;AAG9C,OAAO,KAAK,EACV,qBAAqB,EACrB,+BAA+B,EAC/B,sBAAsB,EACvB,MAAM,oBAAoB,CAAA;AAE3B,oEAAoE;AACpE,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,CACR,GAAG,EAAE,oBAAoB,EACzB,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,WAAW,CAAA;KAAE,GAChD,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1E,UAAU,CAAC,GAAG,EAAE,oBAAoB,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,CAAC,CAAA;CACnG;AAED,yDAAyD;AACzD,MAAM,WAAW,6BAA6B;IAC5C,oEAAoE;IACpE,OAAO,EAAE,sBAAsB,CAAA;IAC/B,oEAAoE;IACpE,cAAc,EAAE,oBAAoB,CAAA;CACrC;AAED,0EAA0E;AAC1E,MAAM,WAAW,yBAAyB;IACxC,qBAAqB,CACnB,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,+BAA+B,GAAG,IAAI,CAAC,CAAA;IAClD,OAAO,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAA;CAC9E;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,yBAAyB,CAyG3B"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { formatTripBillingName, readTripBilling, splitTripBillingName } from "./checkout/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build the flight (non-catalog) trip-component orchestration bound to a
|
|
4
|
+
* request's flight adapter + adapter context.
|
|
5
|
+
*/
|
|
6
|
+
export function createFlightComponentAdapter(options) {
|
|
7
|
+
const { adapter, adapterContext } = options;
|
|
8
|
+
async function validateBeforeReserve(input) {
|
|
9
|
+
if (input.component.kind !== "flight_placeholder")
|
|
10
|
+
return null;
|
|
11
|
+
const flightDraft = asRecord(input.component.metadata)?.flightDraft;
|
|
12
|
+
const draftRecord = asRecord(flightDraft);
|
|
13
|
+
const selectedOffer = draftRecord?.selectedOffer;
|
|
14
|
+
const offerId = stringValue(draftRecord?.offerId) ?? selectedOffer?.offerId;
|
|
15
|
+
if (!selectedOffer || !offerId) {
|
|
16
|
+
return { status: "unavailable", reason: "flight_offer_required" };
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const priced = await adapter.priceOffer(adapterContext, {
|
|
20
|
+
offerId,
|
|
21
|
+
offer: selectedOffer,
|
|
22
|
+
});
|
|
23
|
+
if (!priced.valid) {
|
|
24
|
+
return {
|
|
25
|
+
status: "unavailable",
|
|
26
|
+
reason: priced.invalidReason ?? "flight_offer_unavailable",
|
|
27
|
+
details: { offerId },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const previousPricing = asRecord(draftRecord?.pricing);
|
|
31
|
+
const ancillaryAmountCents = numberValue(previousPricing?.ancillaryAmountCents) ?? 0;
|
|
32
|
+
const currentTotalAmountCents = moneyToCents(priced.offer.totalPrice.amount) + ancillaryAmountCents;
|
|
33
|
+
const previousTotalAmountCents = input.component.componentTotalAmountCents ??
|
|
34
|
+
numberValue(previousPricing?.totalAmountCents) ??
|
|
35
|
+
0;
|
|
36
|
+
const currentCurrency = priced.offer.totalPrice.currency;
|
|
37
|
+
const previousCurrency = input.component.componentCurrency ??
|
|
38
|
+
stringValue(previousPricing?.currency) ??
|
|
39
|
+
currentCurrency;
|
|
40
|
+
if (currentCurrency !== previousCurrency ||
|
|
41
|
+
currentTotalAmountCents !== previousTotalAmountCents) {
|
|
42
|
+
return {
|
|
43
|
+
status: "price_changed",
|
|
44
|
+
reason: "flight_price_changed",
|
|
45
|
+
details: {
|
|
46
|
+
offerId,
|
|
47
|
+
previous: {
|
|
48
|
+
currency: previousCurrency,
|
|
49
|
+
totalAmountCents: previousTotalAmountCents,
|
|
50
|
+
},
|
|
51
|
+
current: {
|
|
52
|
+
currency: currentCurrency,
|
|
53
|
+
totalAmountCents: currentTotalAmountCents,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { status: "ok" };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
status: isExpiredOffer(selectedOffer) ? "expired" : "unavailable",
|
|
63
|
+
reason: error instanceof Error ? error.message : "flight_offer_unavailable",
|
|
64
|
+
details: { offerId },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function reserve(input) {
|
|
69
|
+
if (input.component.kind !== "flight_placeholder")
|
|
70
|
+
return null;
|
|
71
|
+
const flightDraft = asRecord(input.component.metadata)?.flightDraft;
|
|
72
|
+
const selectedOffer = asRecord(flightDraft)?.selectedOffer;
|
|
73
|
+
if (!selectedOffer?.offerId) {
|
|
74
|
+
throw new Error("flight_offer_required");
|
|
75
|
+
}
|
|
76
|
+
const request = {
|
|
77
|
+
offerId: selectedOffer.offerId,
|
|
78
|
+
offer: selectedOffer,
|
|
79
|
+
passengers: flightPassengersFromTravelerParty(input.envelope.travelerParty),
|
|
80
|
+
contact: flightContactFromTravelerParty(input.envelope.travelerParty),
|
|
81
|
+
paymentIntent: { type: "hold" },
|
|
82
|
+
ancillaries: asRecord(flightDraft)?.ancillaries,
|
|
83
|
+
};
|
|
84
|
+
const response = await adapter.bookFlight(adapterContext, request);
|
|
85
|
+
const order = response.order;
|
|
86
|
+
return {
|
|
87
|
+
status: order.status === "ticketed" ? "booked" : "held",
|
|
88
|
+
orderId: order.orderId,
|
|
89
|
+
providerRef: order.pnr ?? order.orderId,
|
|
90
|
+
supplierRef: order.orderId,
|
|
91
|
+
holdExpiresAt: order.paymentDeadline,
|
|
92
|
+
warnings: order.paymentDeadline ? undefined : ["flight_hold_deadline_missing"],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { validateBeforeReserve, reserve };
|
|
96
|
+
}
|
|
97
|
+
// ── Pure helpers (vertical-agnostic) ────────────────────────────────────────
|
|
98
|
+
function moneyToCents(amount) {
|
|
99
|
+
const parsed = Number.parseFloat(amount);
|
|
100
|
+
return Number.isFinite(parsed) ? Math.round(parsed * 100) : 0;
|
|
101
|
+
}
|
|
102
|
+
function isExpiredOffer(offer) {
|
|
103
|
+
const expiresAt = offer.expiresAt ? new Date(offer.expiresAt) : null;
|
|
104
|
+
return Boolean(expiresAt && expiresAt.getTime() <= Date.now());
|
|
105
|
+
}
|
|
106
|
+
function flightPassengersFromTravelerParty(travelerParty) {
|
|
107
|
+
const travelers = Array.isArray(travelerParty.travelers)
|
|
108
|
+
? travelerParty.travelers
|
|
109
|
+
: [];
|
|
110
|
+
const passengers = travelers.map((traveler, index) => flightPassengerFromTraveler(asRecord(traveler) ?? {}, index));
|
|
111
|
+
if (passengers.length > 0)
|
|
112
|
+
return passengers;
|
|
113
|
+
const billing = readTripBilling(travelerParty);
|
|
114
|
+
const names = splitTripBillingName(formatTripBillingName(billing) ?? "Lead traveler");
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
passengerId: "traveler_1",
|
|
118
|
+
type: "adult",
|
|
119
|
+
firstName: names.firstName,
|
|
120
|
+
lastName: names.lastName,
|
|
121
|
+
dateOfBirth: "1990-01-01",
|
|
122
|
+
...(billing.contact?.email ? { email: billing.contact.email } : {}),
|
|
123
|
+
...(billing.contact?.phone ? { phone: billing.contact.phone } : {}),
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
function flightPassengerFromTraveler(traveler, index) {
|
|
128
|
+
const type = passengerTypeFromRole(stringValue(traveler.role));
|
|
129
|
+
return {
|
|
130
|
+
passengerId: stringValue(traveler.localId) || stringValue(traveler.personId) || `traveler_${index + 1}`,
|
|
131
|
+
type,
|
|
132
|
+
firstName: stringValue(traveler.firstName) || fallbackFirstName(type),
|
|
133
|
+
lastName: stringValue(traveler.lastName) || `${index + 1}`,
|
|
134
|
+
dateOfBirth: stringValue(traveler.dateOfBirth) || fallbackDobForPassengerType(type),
|
|
135
|
+
...(stringValue(traveler.email) ? { email: stringValue(traveler.email) ?? undefined } : {}),
|
|
136
|
+
...(stringValue(traveler.phone) ? { phone: stringValue(traveler.phone) ?? undefined } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function passengerTypeFromRole(role) {
|
|
140
|
+
if (role === "child")
|
|
141
|
+
return "child";
|
|
142
|
+
if (role === "infant")
|
|
143
|
+
return "infant";
|
|
144
|
+
return "adult";
|
|
145
|
+
}
|
|
146
|
+
function fallbackFirstName(type) {
|
|
147
|
+
if (type === "child")
|
|
148
|
+
return "Child";
|
|
149
|
+
if (type === "infant")
|
|
150
|
+
return "Infant";
|
|
151
|
+
return "Adult";
|
|
152
|
+
}
|
|
153
|
+
function fallbackDobForPassengerType(type) {
|
|
154
|
+
if (type === "child")
|
|
155
|
+
return "2016-01-01";
|
|
156
|
+
if (type === "infant")
|
|
157
|
+
return "2025-01-01";
|
|
158
|
+
return "1990-01-01";
|
|
159
|
+
}
|
|
160
|
+
function flightContactFromTravelerParty(travelerParty) {
|
|
161
|
+
const billing = readTripBilling(travelerParty);
|
|
162
|
+
return {
|
|
163
|
+
...(billing.contact?.email ? { email: billing.contact.email } : {}),
|
|
164
|
+
...(billing.contact?.phone ? { phone: billing.contact.phone } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function asRecord(value) {
|
|
168
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
169
|
+
? value
|
|
170
|
+
: undefined;
|
|
171
|
+
}
|
|
172
|
+
function stringValue(value) {
|
|
173
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
174
|
+
}
|
|
175
|
+
function numberValue(value) {
|
|
176
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
177
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Module } from "@voyant-travel/core";
|
|
2
2
|
import type { HonoModule } from "@voyant-travel/hono/module";
|
|
3
3
|
import { type TripsRoutesOptions } from "./routes.js";
|
|
4
|
+
export { type CatalogAdapterContext, type CatalogComponentAdapter, type CatalogComponentAdapterOptions, createCatalogComponentAdapter, previewCancellation, type StartComponentCheckout, } from "./catalog-component.js";
|
|
4
5
|
export { type CatalogComponentBookingDraftOverrides, isCatalogBackedTripComponent, toBookingDraftV1, } from "./catalog-component-adapter.js";
|
|
5
6
|
export { CRUISE_EXTENSION_METADATA_KIND, type CruiseExtensionExtra, type CruiseExtensionLifecycle, type CruiseExtensionLinkCommand, type CruiseExtensionLinkInput, type CruiseExtensionPlacement, type CruiseExtensionRepresentation, type CruiseExtensionSelection, type CruiseExtensionTargetKind, createCruiseExtensionComponent, createCruiseExtensionExtra, createCruiseExtensionLinkCommand, cruiseExtensionLinkKey, groupCruiseExtensionLinksByProduct, representCruiseExtensionSelection, } from "./cruise-extension.js";
|
|
7
|
+
export { createFlightComponentAdapter, type FlightAdapterContext, type FlightComponentAdapter, type FlightComponentAdapterApi, type FlightComponentAdapterOptions, } from "./flight-component.js";
|
|
6
8
|
export type { TripsRoutes, TripsRoutesOptions } from "./routes.js";
|
|
7
9
|
export { createTripsRoutes } from "./routes.js";
|
|
8
10
|
export declare const tripsModule: Module;
|
|
@@ -14,6 +16,7 @@ export declare function createTripsHonoModule(options?: TripsHonoModuleOptions):
|
|
|
14
16
|
export type { McpToolContent, McpToolContext, McpToolDefinition, McpToolErrorCode, McpToolHandler, McpToolResult, } from "./mcp-contract.js";
|
|
15
17
|
export { McpToolError } from "./mcp-contract.js";
|
|
16
18
|
export { type CreateMcpToolRegistryOptions, createMcpToolRegistry, enforceAudienceAuthorization, type McpToolListEntry, type McpToolRegistry, requireService, } from "./mcp-registry.js";
|
|
19
|
+
export { createTripMcpRoutes, type TripMcpRoutesOptions } from "./mcp-routes.js";
|
|
17
20
|
export { type CreateTripArgs, createTripTool, type PriceTripArgs, priceTripTool, type ReserveTripArgs, type ReviseTripArgs, reserveTripTool, reviseTripTool, type TripsMcpServices, tripsMcpTools, } from "./mcp-tools.js";
|
|
18
21
|
export { tripsRoutes } from "./routes.js";
|
|
19
22
|
export type { NewTripComponent, NewTripComponentEvent, NewTripEnvelope, NewTripReservationPlan, NewTripSnapshot, TripComponent, TripComponentEvent, TripComponentPricingSnapshot, TripComponentTaxLineSnapshot, TripEnvelope, TripEnvelopePricingSnapshot, TripReservationPlan, TripReservationPlanCompensationSnapshot, TripReservationPlanComponentSnapshot, TripReservationPlanFailureSnapshot, TripSnapshot, TripSnapshotProposal, TripSnapshotProposalLine, } from "./schema.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createTripsRoutes, tripsRoutes } from "./routes.js";
|
|
2
|
+
export { createCatalogComponentAdapter, previewCancellation, } from "./catalog-component.js";
|
|
2
3
|
export { isCatalogBackedTripComponent, toBookingDraftV1, } from "./catalog-component-adapter.js";
|
|
3
4
|
export { CRUISE_EXTENSION_METADATA_KIND, createCruiseExtensionComponent, createCruiseExtensionExtra, createCruiseExtensionLinkCommand, cruiseExtensionLinkKey, groupCruiseExtensionLinksByProduct, representCruiseExtensionSelection, } from "./cruise-extension.js";
|
|
5
|
+
export { createFlightComponentAdapter, } from "./flight-component.js";
|
|
4
6
|
export { createTripsRoutes } from "./routes.js";
|
|
5
7
|
export const tripsModule = {
|
|
6
8
|
name: "trips",
|
|
@@ -23,6 +25,7 @@ export function createTripsHonoModule(options = {}) {
|
|
|
23
25
|
}
|
|
24
26
|
export { McpToolError } from "./mcp-contract.js";
|
|
25
27
|
export { createMcpToolRegistry, enforceAudienceAuthorization, requireService, } from "./mcp-registry.js";
|
|
28
|
+
export { createTripMcpRoutes } from "./mcp-routes.js";
|
|
26
29
|
export { createTripTool, priceTripTool, reserveTripTool, reviseTripTool, tripsMcpTools, } from "./mcp-tools.js";
|
|
27
30
|
export { tripsRoutes } from "./routes.js";
|
|
28
31
|
export { tripComponentEvents, tripComponentEventTypeEnum, tripComponentKindEnum, tripComponentStatusEnum, tripComponents, tripEnvelopeStatusEnum, tripEnvelopes, tripReservationPlanStatusEnum, tripReservationPlans, tripSnapshots, } from "./schema.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trips agent tool surface (transport: HTTP).
|
|
3
|
+
*
|
|
4
|
+
* A generic tool-dispatch route owned by the trips package. It registers the
|
|
5
|
+
* trips command tools (create / revise / price / reserve) and dispatches a
|
|
6
|
+
* `POST /tools/:tool` call against them.
|
|
7
|
+
*
|
|
8
|
+
* The deployment supplies the per-request `McpToolContext` and the
|
|
9
|
+
* `TripsMcpServices` (which bind the trips service to the deployment's db +
|
|
10
|
+
* dependency wiring) via `options`. Mount the returned router at
|
|
11
|
+
* `/v1/admin/mcp`.
|
|
12
|
+
*/
|
|
13
|
+
import { type Context, Hono } from "hono";
|
|
14
|
+
import type { McpToolContext } from "./mcp-contract.js";
|
|
15
|
+
import { type TripsMcpServices } from "./mcp-tools.js";
|
|
16
|
+
export interface TripMcpRoutesOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Build the per-request MCP tool context (actor / tenant / default scope).
|
|
19
|
+
* Derived from the request's auth + the deployment's defaults.
|
|
20
|
+
*/
|
|
21
|
+
buildContext(c: Context): McpToolContext;
|
|
22
|
+
/**
|
|
23
|
+
* Build the trips MCP services for this request — binds the trips service to
|
|
24
|
+
* the deployment's db + dependency wiring.
|
|
25
|
+
*/
|
|
26
|
+
buildTripsServices(c: Context): TripsMcpServices;
|
|
27
|
+
}
|
|
28
|
+
/** Build the trips MCP admin routes (relative paths; mount at `/v1/admin/mcp`). */
|
|
29
|
+
export declare function createTripMcpRoutes(options: TripMcpRoutesOptions): Hono;
|
|
30
|
+
//# sourceMappingURL=mcp-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-routes.d.ts","sourceRoot":"","sources":["../src/mcp-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,KAAK,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAEvD,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAA;AAEvB,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,cAAc,CAAA;IACxC;;;OAGG;IACH,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAA;CACjD;AASD,mFAAmF;AACnF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CA0BvE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trips agent tool surface (transport: HTTP).
|
|
3
|
+
*
|
|
4
|
+
* A generic tool-dispatch route owned by the trips package. It registers the
|
|
5
|
+
* trips command tools (create / revise / price / reserve) and dispatches a
|
|
6
|
+
* `POST /tools/:tool` call against them.
|
|
7
|
+
*
|
|
8
|
+
* The deployment supplies the per-request `McpToolContext` and the
|
|
9
|
+
* `TripsMcpServices` (which bind the trips service to the deployment's db +
|
|
10
|
+
* dependency wiring) via `options`. Mount the returned router at
|
|
11
|
+
* `/v1/admin/mcp`.
|
|
12
|
+
*/
|
|
13
|
+
import { Hono } from "hono";
|
|
14
|
+
import { createMcpToolRegistry } from "./mcp-registry.js";
|
|
15
|
+
import { createTripTool, priceTripTool, reserveTripTool, reviseTripTool, } from "./mcp-tools.js";
|
|
16
|
+
function registerAdminTools(registry) {
|
|
17
|
+
registry.register(createTripTool);
|
|
18
|
+
registry.register(reviseTripTool);
|
|
19
|
+
registry.register(priceTripTool);
|
|
20
|
+
registry.register(reserveTripTool);
|
|
21
|
+
}
|
|
22
|
+
/** Build the trips MCP admin routes (relative paths; mount at `/v1/admin/mcp`). */
|
|
23
|
+
export function createTripMcpRoutes(options) {
|
|
24
|
+
async function handle(c) {
|
|
25
|
+
const tool = c.req.param("tool");
|
|
26
|
+
if (!tool)
|
|
27
|
+
return c.json({ error: "Missing tool name" }, 400);
|
|
28
|
+
let body;
|
|
29
|
+
try {
|
|
30
|
+
body = await c.req.json();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
body = {};
|
|
34
|
+
}
|
|
35
|
+
const registry = createMcpToolRegistry({
|
|
36
|
+
context: {
|
|
37
|
+
...options.buildContext(c),
|
|
38
|
+
trips: options.buildTripsServices(c),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
registerAdminTools(registry);
|
|
42
|
+
const result = await registry.dispatchTool(tool, body);
|
|
43
|
+
return c.json(result);
|
|
44
|
+
}
|
|
45
|
+
const routes = new Hono();
|
|
46
|
+
routes.post("/tools/:tool", handle);
|
|
47
|
+
return routes;
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/trips",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.113.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,6 +29,26 @@
|
|
|
29
29
|
"import": "./dist/mcp-tools.js",
|
|
30
30
|
"default": "./dist/mcp-tools.js"
|
|
31
31
|
},
|
|
32
|
+
"./mcp": {
|
|
33
|
+
"types": "./dist/mcp-routes.d.ts",
|
|
34
|
+
"import": "./dist/mcp-routes.js",
|
|
35
|
+
"default": "./dist/mcp-routes.js"
|
|
36
|
+
},
|
|
37
|
+
"./checkout": {
|
|
38
|
+
"types": "./dist/checkout/index.d.ts",
|
|
39
|
+
"import": "./dist/checkout/index.js",
|
|
40
|
+
"default": "./dist/checkout/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./catalog-component": {
|
|
43
|
+
"types": "./dist/catalog-component.d.ts",
|
|
44
|
+
"import": "./dist/catalog-component.js",
|
|
45
|
+
"default": "./dist/catalog-component.js"
|
|
46
|
+
},
|
|
47
|
+
"./flight-component": {
|
|
48
|
+
"types": "./dist/flight-component.d.ts",
|
|
49
|
+
"import": "./dist/flight-component.js",
|
|
50
|
+
"default": "./dist/flight-component.js"
|
|
51
|
+
},
|
|
32
52
|
"./cruise-extension": {
|
|
33
53
|
"types": "./dist/cruise-extension.d.ts",
|
|
34
54
|
"import": "./dist/cruise-extension.js",
|
|
@@ -51,9 +71,12 @@
|
|
|
51
71
|
"hono": "^4.12.10",
|
|
52
72
|
"zod": "^4.3.6",
|
|
53
73
|
"@voyant-travel/core": "^0.109.0",
|
|
54
|
-
"@voyant-travel/
|
|
74
|
+
"@voyant-travel/bookings": "^0.122.0",
|
|
75
|
+
"@voyant-travel/catalog": "^0.120.0",
|
|
55
76
|
"@voyant-travel/db": "^0.108.1",
|
|
56
|
-
"@voyant-travel/
|
|
77
|
+
"@voyant-travel/finance": "^0.122.0",
|
|
78
|
+
"@voyant-travel/flights": "^0.122.0",
|
|
79
|
+
"@voyant-travel/hono": "^0.111.0"
|
|
57
80
|
},
|
|
58
81
|
"devDependencies": {
|
|
59
82
|
"typescript": "^6.0.2",
|