@voyant-travel/flights 0.137.0 → 0.137.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.
@@ -1,6 +1,6 @@
1
1
  // agent-quality: file-size exception -- owner: flights; existing coverage file stays co-located until a dedicated split preserves behavior and tests.
2
2
  import { describe, expect, it } from "vitest";
3
- import { ancillaryRequestSchema, ancillaryResponseSchema, checkInRequestSchema, checkInResponseSchema, flightAdapterCapabilitiesSchema, flightAdapterContextSchema, flightBookRequestSchema, flightBookResponseSchema, flightCancelResponseSchema, flightGetOrderResponseSchema, flightModifyRequestSchema, flightModifyResponseSchema, flightOrdersListQuerySchema, flightOrdersListResponseSchema, flightPriceRequestSchema, flightPriceResponseSchema, flightRefundRequestSchema, flightRefundResponseSchema, flightSearchRequestSchema, flightSearchResponseSchema, flightVoidResponseSchema, moneySchema, seatMapRequestSchema, seatMapResponseSchema, seatSelectionRequestSchema, seatSelectionResponseSchema, ssrRequestSchema, ssrResponseSchema, } from "./schemas.js";
3
+ import { ancillaryRequestSchema, ancillaryResponseSchema, checkInRequestSchema, checkInResponseSchema, flightAdapterCapabilitiesSchema, flightAdapterContextSchema, flightBookRequestSchema, flightBookResponseSchema, flightCancelResponseSchema, flightGetOrderResponseSchema, flightModifyRequestSchema, flightModifyResponseSchema, flightOrdersListQuerySchema, flightOrdersListResponseSchema, flightPriceRequestSchema, flightPriceResponseSchema, flightRefundRequestSchema, flightRefundResponseSchema, flightSearchRequestSchema, flightSearchResponseSchema, flightVoidResponseSchema, moneySchema, paymentIntentSchema, seatMapRequestSchema, seatMapResponseSchema, seatSelectionRequestSchema, seatSelectionResponseSchema, ssrRequestSchema, ssrResponseSchema, } from "./schemas.js";
4
4
  const typeChecks = [
5
5
  true,
6
6
  true,
@@ -164,6 +164,7 @@ const roundTripCases = [
164
164
  ["flightSearchResponseSchema", flightSearchResponseSchema, { offers: [offer] }],
165
165
  ["flightPriceRequestSchema", flightPriceRequestSchema, { offerId: "offer_1", offer }],
166
166
  ["flightPriceResponseSchema", flightPriceResponseSchema, { offer, valid: true }],
167
+ ["paymentIntentSchema", paymentIntentSchema, { type: "bank_transfer" }],
167
168
  [
168
169
  "flightBookRequestSchema",
169
170
  flightBookRequestSchema,
package/dist/hono.d.ts CHANGED
@@ -2,21 +2,24 @@ import type { HonoModule } from "@voyant-travel/hono";
2
2
  import { type Context, Hono } from "hono";
3
3
  import type { FlightConnectorAdapter } from "./contract/adapter.js";
4
4
  import type { FlightOrder } from "./contract/types.js";
5
- /** A resolved payment session for a flight hold order. */
5
+ /** A resolved payment session for a flight order. */
6
6
  export interface FlightOrderPaymentSummary {
7
7
  sessionId: string;
8
8
  status: string;
9
9
  }
10
10
  /**
11
- * Deployment-supplied payment integration for flight hold orders. The flights
11
+ * Deployment-supplied payment integration for flight orders. The flights
12
12
  * module stays payment-provider agnostic; the deployment wires its finance /
13
13
  * payment provider here.
14
14
  */
15
15
  export interface FlightPaymentIntegration {
16
- /** Ensure (idempotently) a payment session exists for a hold order. */
16
+ /** Ensure (idempotently) a payment session exists for an order. */
17
17
  ensureOrderSession(c: Context, order: FlightOrder, contact?: {
18
18
  email?: string;
19
19
  phone?: string;
20
+ }, options?: {
21
+ paymentMethod?: "bank_transfer" | "credit_card";
22
+ startCardPayment?: boolean;
20
23
  }): Promise<FlightOrderPaymentSummary | null>;
21
24
  /** Bulk-resolve the most relevant payment session per order id (no N+1). */
22
25
  fetchOrderSessions(c: Context, orderIds: string[]): Promise<Map<string, FlightOrderPaymentSummary>>;
@@ -1 +1 @@
1
- {"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../src/hono.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAErD,OAAO,EAAE,KAAK,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEzC,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAGV,WAAW,EAIZ,MAAM,qBAAqB,CAAA;AAO5B,0DAA0D;AAC1D,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,uEAAuE;IACvE,kBAAkB,CAChB,CAAC,EAAE,OAAO,EACV,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3C,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAA;IAC5C,4EAA4E;IAC5E,kBAAkB,CAChB,CAAC,EAAE,OAAO,EACV,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAAA;CACnD;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAA;IAClD,yDAAyD;IACzD,OAAO,CAAC,EAAE,wBAAwB,CAAA;CACnC;AAED,MAAM,MAAM,wBAAwB,GAAG,mBAAmB,CAAA;AA8B1D,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAsO1E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,UAAU,CAKrF"}
1
+ {"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../src/hono.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAErD,OAAO,EAAE,KAAK,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEzC,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAGV,WAAW,EAKZ,MAAM,qBAAqB,CAAA;AAO5B,qDAAqD;AACrD,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,kBAAkB,CAChB,CAAC,EAAE,OAAO,EACV,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAC5C,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,eAAe,GAAG,aAAa,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAE,GACxF,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAA;IAC5C,4EAA4E;IAC5E,kBAAkB,CAChB,CAAC,EAAE,OAAO,EACV,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAAA;CACnD;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAA;IAClD,yDAAyD;IACzD,OAAO,CAAC,EAAE,wBAAwB,CAAA;CACnC;AAED,MAAM,MAAM,wBAAwB,GAAG,mBAAmB,CAAA;AAoD1D,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CA+O1E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,UAAU,CAKrF"}
package/dist/hono.js CHANGED
@@ -24,6 +24,25 @@ function attachPaymentSession(order, summary) {
24
24
  },
25
25
  };
26
26
  }
27
+ function statusForAdapterErrorMessage(message) {
28
+ return /(?:not found|_not_found\b)/i.test(message) ? 404 : 500;
29
+ }
30
+ function paymentSessionOptionsForIntent(intent) {
31
+ if (!intent || intent.type === "hold")
32
+ return { startCardPayment: true };
33
+ if (intent.type === "bank_transfer") {
34
+ return { paymentMethod: "bank_transfer", startCardPayment: false };
35
+ }
36
+ return null;
37
+ }
38
+ function adapterBookingRequestForIntent(body) {
39
+ if (body.paymentIntent?.type !== "bank_transfer")
40
+ return body;
41
+ return {
42
+ ...body,
43
+ paymentIntent: { type: "hold" },
44
+ };
45
+ }
27
46
  /** Build the flight admin routes (relative paths; mount at `/v1/admin/flights`). */
28
47
  export function createFlightAdminRoutes(options) {
29
48
  const { resolveAdapter, payment } = options;
@@ -96,7 +115,7 @@ export function createFlightAdminRoutes(options) {
96
115
  }
97
116
  catch (err) {
98
117
  const message = err instanceof Error ? err.message : String(err);
99
- return c.json({ error: message }, /not found/i.test(message) ? 404 : 500);
118
+ return c.json({ error: message }, statusForAdapterErrorMessage(message));
100
119
  }
101
120
  });
102
121
  // ── Re-price ────────────────────────────────────────────────────────────
@@ -132,12 +151,12 @@ export function createFlightAdminRoutes(options) {
132
151
  if (!body.passengers?.length)
133
152
  return c.json({ error: "passengers is required" }, 400);
134
153
  try {
135
- const response = await resolveAdapter(c).bookFlight(buildContext(c), body);
136
- // Hold is the default intent; eagerly create the payment session so the
137
- // order page can show the shareable link immediately.
138
- const isHold = !body.paymentIntent || body.paymentIntent.type === "hold";
139
- if (isHold && response.order && payment) {
140
- const summary = await payment.ensureOrderSession(c, response.order, body.contact);
154
+ const response = await resolveAdapter(c).bookFlight(buildContext(c), adapterBookingRequestForIntent(body));
155
+ // Hold is the default intent. Bank transfer also needs a finance session
156
+ // so the order can expose the reference/instructions through checkout.
157
+ const paymentSessionOptions = paymentSessionOptionsForIntent(body.paymentIntent);
158
+ if (paymentSessionOptions && response.order && payment) {
159
+ const summary = await payment.ensureOrderSession(c, response.order, body.contact, paymentSessionOptions);
141
160
  response.order = attachPaymentSession(response.order, summary);
142
161
  }
143
162
  return c.json(response);
@@ -191,14 +210,15 @@ export function createFlightAdminRoutes(options) {
191
210
  try {
192
211
  const response = await resolveAdapter(c).getOrder(buildContext(c), orderId);
193
212
  if (response.order && payment) {
194
- const summary = await payment.ensureOrderSession(c, response.order, response.order.contact);
213
+ const sessionByOrderId = await payment.fetchOrderSessions(c, [response.order.orderId]);
214
+ const summary = sessionByOrderId.get(response.order.orderId) ?? null;
195
215
  response.order = attachPaymentSession(response.order, summary);
196
216
  }
197
217
  return c.json(response);
198
218
  }
199
219
  catch (err) {
200
220
  const message = err instanceof Error ? err.message : String(err);
201
- return c.json({ error: message }, /not found/i.test(message) ? 404 : 500);
221
+ return c.json({ error: message }, statusForAdapterErrorMessage(message));
202
222
  }
203
223
  });
204
224
  // ── Cancel order ────────────────────────────────────────────────────────
@@ -219,7 +239,7 @@ export function createFlightAdminRoutes(options) {
219
239
  }
220
240
  catch (err) {
221
241
  const message = err instanceof Error ? err.message : String(err);
222
- return c.json({ error: message }, /not found/i.test(message) ? 404 : 500);
242
+ return c.json({ error: message }, statusForAdapterErrorMessage(message));
223
243
  }
224
244
  });
225
245
  // ── Reference: airports (with substring search) ───────────────────────────
package/dist/hono.test.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import { describe, expect, it, vi } from "vitest";
3
- import { createFlightAdminRoutes, createFlightsHonoModule } from "./hono.js";
3
+ import { createFlightAdminRoutes, createFlightsHonoModule, } from "./hono.js";
4
4
  function stubAdapter(over = {}) {
5
5
  return {
6
6
  capabilities: { provider: "stub", declared: [] },
@@ -12,8 +12,8 @@ function stubAdapter(over = {}) {
12
12
  ...over,
13
13
  };
14
14
  }
15
- function mount(adapter) {
16
- return new Hono().route("/v1/admin/flights", createFlightAdminRoutes({ resolveAdapter: () => adapter }));
15
+ function mount(adapter, payment) {
16
+ return new Hono().route("/v1/admin/flights", createFlightAdminRoutes({ resolveAdapter: () => adapter, payment }));
17
17
  }
18
18
  describe("flights hono module", () => {
19
19
  it("createFlightsHonoModule exposes adminRoutes on module 'flights'", () => {
@@ -50,4 +50,244 @@ describe("flights hono module", () => {
50
50
  });
51
51
  expect(res.status).toBe(501);
52
52
  });
53
+ it("creates and attaches a bank-transfer payment session during booking", async () => {
54
+ const adapter = stubAdapter();
55
+ const payment = {
56
+ ensureOrderSession: vi.fn(async () => ({ sessionId: "ps_bank", status: "pending" })),
57
+ fetchOrderSessions: vi.fn(async () => new Map()),
58
+ };
59
+ const app = mount(adapter, payment);
60
+ const res = await app.request("/v1/admin/flights/book", {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({
64
+ offerId: "off_1",
65
+ passengers: [{ passengerId: "p1" }],
66
+ contact: { email: "payer@example.com" },
67
+ paymentIntent: { type: "bank_transfer" },
68
+ }),
69
+ });
70
+ expect(res.status).toBe(200);
71
+ await expect(res.json()).resolves.toMatchObject({
72
+ order: {
73
+ providerData: {
74
+ paymentSessionId: "ps_bank",
75
+ paymentStatus: "pending",
76
+ },
77
+ },
78
+ });
79
+ expect(payment.ensureOrderSession).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ orderId: "ord_1" }), { email: "payer@example.com" }, { paymentMethod: "bank_transfer", startCardPayment: false });
80
+ expect(adapter.bookFlight).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ paymentIntent: { type: "hold" } }));
81
+ });
82
+ it("preserves hold payment-session creation during booking", async () => {
83
+ const adapter = stubAdapter();
84
+ const payment = {
85
+ ensureOrderSession: vi.fn(async () => ({ sessionId: "ps_hold", status: "pending" })),
86
+ fetchOrderSessions: vi.fn(async () => new Map()),
87
+ };
88
+ const app = mount(adapter, payment);
89
+ const res = await app.request("/v1/admin/flights/book", {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify({
93
+ offerId: "off_1",
94
+ passengers: [{ passengerId: "p1" }],
95
+ paymentIntent: { type: "hold" },
96
+ }),
97
+ });
98
+ expect(res.status).toBe(200);
99
+ expect(payment.ensureOrderSession).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ orderId: "ord_1" }), undefined, { startCardPayment: true });
100
+ });
101
+ it("does not create a payment session for card booking intent", async () => {
102
+ const adapter = stubAdapter();
103
+ const payment = {
104
+ ensureOrderSession: vi.fn(async () => ({ sessionId: "ps_card", status: "pending" })),
105
+ fetchOrderSessions: vi.fn(async () => new Map()),
106
+ };
107
+ const app = mount(adapter, payment);
108
+ const res = await app.request("/v1/admin/flights/book", {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({
112
+ offerId: "off_1",
113
+ passengers: [{ passengerId: "p1" }],
114
+ paymentIntent: { type: "card", token: "tok_1" },
115
+ }),
116
+ });
117
+ expect(res.status).toBe(200);
118
+ expect(payment.ensureOrderSession).not.toHaveBeenCalled();
119
+ expect(adapter.bookFlight).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ paymentIntent: { type: "card", token: "tok_1" } }));
120
+ });
121
+ it("does not create a delayed payment session when reading a card-ticketed order", async () => {
122
+ const adapter = stubAdapter({
123
+ bookFlight: vi.fn(async () => ({
124
+ order: {
125
+ orderId: "ord_card",
126
+ status: "ticketed",
127
+ passengers: [{ passengerId: "p1" }],
128
+ },
129
+ })),
130
+ getOrder: vi.fn(async () => ({
131
+ order: {
132
+ orderId: "ord_card",
133
+ status: "ticketed",
134
+ passengers: [{ passengerId: "p1" }],
135
+ },
136
+ })),
137
+ });
138
+ const payment = {
139
+ ensureOrderSession: vi.fn(async () => ({ sessionId: "ps_card", status: "pending" })),
140
+ fetchOrderSessions: vi.fn(async () => new Map()),
141
+ };
142
+ const app = mount(adapter, payment);
143
+ const bookRes = await app.request("/v1/admin/flights/book", {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({
147
+ offerId: "off_1",
148
+ passengers: [{ passengerId: "p1" }],
149
+ paymentIntent: { type: "card", token: "tok_1" },
150
+ }),
151
+ });
152
+ const readRes = await app.request("/v1/admin/flights/orders/ord_card");
153
+ expect(bookRes.status).toBe(200);
154
+ expect(readRes.status).toBe(200);
155
+ await expect(readRes.json()).resolves.toMatchObject({
156
+ order: {
157
+ orderId: "ord_card",
158
+ status: "ticketed",
159
+ },
160
+ });
161
+ expect(payment.ensureOrderSession).not.toHaveBeenCalled();
162
+ expect(payment.fetchOrderSessions).toHaveBeenCalledWith(expect.anything(), ["ord_card"]);
163
+ });
164
+ it("GET /orders/:orderId attaches an existing payment session without ensuring one", async () => {
165
+ const adapter = stubAdapter({
166
+ getOrder: vi.fn(async () => ({
167
+ order: {
168
+ orderId: "ord_1",
169
+ passengers: [],
170
+ providerData: { source: "stub" },
171
+ },
172
+ })),
173
+ });
174
+ const payment = {
175
+ ensureOrderSession: vi.fn(async () => {
176
+ throw new Error("GET must not create or start payment sessions");
177
+ }),
178
+ fetchOrderSessions: vi.fn(async () => new Map([["ord_1", { sessionId: "ps_existing", status: "pending" }]])),
179
+ };
180
+ const app = mount(adapter, payment);
181
+ const res = await app.request("/v1/admin/flights/orders/ord_1");
182
+ expect(res.status).toBe(200);
183
+ await expect(res.json()).resolves.toMatchObject({
184
+ order: {
185
+ providerData: {
186
+ source: "stub",
187
+ paymentSessionId: "ps_existing",
188
+ paymentStatus: "pending",
189
+ },
190
+ },
191
+ });
192
+ expect(payment.fetchOrderSessions).toHaveBeenCalledWith(expect.anything(), ["ord_1"]);
193
+ expect(payment.ensureOrderSession).not.toHaveBeenCalled();
194
+ });
195
+ it("does not create payment sessions when listing orders", async () => {
196
+ const adapter = stubAdapter({
197
+ listOrders: vi.fn(async () => ({
198
+ orders: [{ orderId: "ord_card", status: "ticketed", passengers: [] }],
199
+ pagination: { total: 1, hasMore: false },
200
+ })),
201
+ });
202
+ const payment = {
203
+ ensureOrderSession: vi.fn(async () => ({ sessionId: "ps_unexpected", status: "pending" })),
204
+ fetchOrderSessions: vi.fn(async () => new Map()),
205
+ };
206
+ const app = mount(adapter, payment);
207
+ const res = await app.request("/v1/admin/flights/orders");
208
+ expect(res.status).toBe(200);
209
+ await expect(res.json()).resolves.toMatchObject({
210
+ orders: [{ orderId: "ord_card", status: "ticketed" }],
211
+ });
212
+ expect(payment.fetchOrderSessions).toHaveBeenCalledWith(expect.anything(), ["ord_card"]);
213
+ expect(payment.ensureOrderSession).not.toHaveBeenCalled();
214
+ });
215
+ it("GET /orders attaches existing payment sessions without ensuring any", async () => {
216
+ const adapter = stubAdapter({
217
+ listOrders: vi.fn(async () => ({
218
+ orders: [
219
+ { orderId: "ord_1", passengers: [] },
220
+ { orderId: "ord_2", passengers: [] },
221
+ ],
222
+ pagination: { total: 2, hasMore: false },
223
+ })),
224
+ });
225
+ const payment = {
226
+ ensureOrderSession: vi.fn(async () => {
227
+ throw new Error("GET must not create or start payment sessions");
228
+ }),
229
+ fetchOrderSessions: vi.fn(async () => new Map([
230
+ ["ord_1", { sessionId: "ps_1", status: "pending" }],
231
+ ["ord_2", { sessionId: "ps_2", status: "paid" }],
232
+ ])),
233
+ };
234
+ const app = mount(adapter, payment);
235
+ const res = await app.request("/v1/admin/flights/orders");
236
+ expect(res.status).toBe(200);
237
+ await expect(res.json()).resolves.toMatchObject({
238
+ orders: [
239
+ { providerData: { paymentSessionId: "ps_1", paymentStatus: "pending" } },
240
+ { providerData: { paymentSessionId: "ps_2", paymentStatus: "paid" } },
241
+ ],
242
+ });
243
+ expect(payment.fetchOrderSessions).toHaveBeenCalledWith(expect.anything(), ["ord_1", "ord_2"]);
244
+ expect(payment.ensureOrderSession).not.toHaveBeenCalled();
245
+ });
246
+ it("attaches an existing payment session when reading an order", async () => {
247
+ const adapter = stubAdapter({
248
+ getOrder: vi.fn(async () => ({
249
+ order: { orderId: "ord_hold", status: "confirmed", passengers: [] },
250
+ })),
251
+ });
252
+ const payment = {
253
+ ensureOrderSession: vi.fn(async () => ({ sessionId: "ps_unexpected", status: "pending" })),
254
+ fetchOrderSessions: vi.fn(async () => new Map([["ord_hold", { sessionId: "ps_existing", status: "paid" }]])),
255
+ };
256
+ const app = mount(adapter, payment);
257
+ const res = await app.request("/v1/admin/flights/orders/ord_hold");
258
+ expect(res.status).toBe(200);
259
+ await expect(res.json()).resolves.toMatchObject({
260
+ order: {
261
+ providerData: {
262
+ paymentSessionId: "ps_existing",
263
+ paymentStatus: "paid",
264
+ },
265
+ },
266
+ });
267
+ expect(payment.ensureOrderSession).not.toHaveBeenCalled();
268
+ });
269
+ it("returns 404 when reading a missing order", async () => {
270
+ const adapter = stubAdapter({
271
+ getOrder: vi.fn(async () => {
272
+ throw new Error("order_not_found");
273
+ }),
274
+ });
275
+ const app = mount(adapter);
276
+ const res = await app.request("/v1/admin/flights/orders/no_such_order");
277
+ expect(res.status).toBe(404);
278
+ await expect(res.json()).resolves.toEqual({ error: "order_not_found" });
279
+ });
280
+ it("returns 404 when cancelling a missing order", async () => {
281
+ const adapter = stubAdapter({
282
+ cancelOrder: vi.fn(async () => {
283
+ throw new Error("order_not_found");
284
+ }),
285
+ });
286
+ const app = mount(adapter);
287
+ const res = await app.request("/v1/admin/flights/orders/no_such_order/cancel", {
288
+ method: "POST",
289
+ });
290
+ expect(res.status).toBe(404);
291
+ await expect(res.json()).resolves.toEqual({ error: "order_not_found" });
292
+ });
53
293
  });
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { createFlightAdminRoutes, createFlightsHonoModule, type FlightOrderPayme
5
5
  export { FLIGHTS_ENTITY_MODULE, mergedFlightOffersToCandidates, mergedFlightOfferToCandidate, } from "./orchestration/availability-bridge.js";
6
6
  export { type ConnectionResult, type ConnectionSearchStatus, type FanOutFlightSearchOptions, type FanOutFlightSearchResult, fanOutFlightSearch, type MergedFlightOffer, } from "./orchestration/fan-out.js";
7
7
  export { itineraryFingerprint } from "./orchestration/fingerprint.js";
8
- export { buildFlightSummary, createFlightOrderPaymentIntegration, type FlightCardBilling, type FlightOrderPaymentIntegrationDeps, formatDay, type OrderPaymentSessionsLike, parseAmountToCents, synthesizeBilling, } from "./payment-integration.js";
8
+ export { buildFlightSummary, createFlightOrderPaymentIntegration, type FlightCardBilling, type FlightOrderPaymentIntegrationDeps, type FlightOrderPaymentSessionOptions, formatDay, type OrderPaymentSessionsLike, parseAmountToCents, synthesizeBilling, } from "./payment-integration.js";
9
9
  export { type Aircraft, type Airline, type Airport, dedupeCodes, type ReferenceDataCapabilities, type ReferenceDataProvider, } from "./reference/contract.js";
10
10
  export { createLocalPostgresReferenceProvider, type LocalPostgresReferenceProviderOptions, referenceAircraft, referenceAirlines, referenceAirports, } from "./reference/local-postgres.js";
11
11
  export { createStaticBundleReferenceProvider, type StaticBundleProviderOptions, type StaticBundleReferenceData, } from "./reference/static-bundle.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,aAAa,EAClB,wBAAwB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,iCAAiC,EACjC,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,iBAAiB,GAClB,MAAM,uBAAuB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AAGnC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,qBAAqB,EACrB,8BAA8B,EAC9B,4BAA4B,GAC7B,MAAM,wCAAwC,CAAA;AAC/C,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,kBAAkB,EAClB,KAAK,iBAAiB,GACvB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAA;AAGrE,OAAO,EACL,kBAAkB,EAClB,mCAAmC,EACnC,KAAK,iBAAiB,EACtB,KAAK,iCAAiC,EACtC,SAAS,EACT,KAAK,wBAAwB,EAC7B,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,WAAW,EACX,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,GAC3B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,oCAAoC,EACpC,KAAK,qCAAqC,EAC1C,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACL,mCAAmC,EACnC,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,GAC/B,MAAM,8BAA8B,CAAA;AAErC,OAAO,EACL,KAAK,+BAA+B,EACpC,wBAAwB,GACzB,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,aAAa,EAClB,wBAAwB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,iCAAiC,EACjC,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,iBAAiB,GAClB,MAAM,uBAAuB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AAGnC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,qBAAqB,EACrB,8BAA8B,EAC9B,4BAA4B,GAC7B,MAAM,wCAAwC,CAAA;AAC/C,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,kBAAkB,EAClB,KAAK,iBAAiB,GACvB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAA;AAGrE,OAAO,EACL,kBAAkB,EAClB,mCAAmC,EACnC,KAAK,iBAAiB,EACtB,KAAK,iCAAiC,EACtC,KAAK,gCAAgC,EACrC,SAAS,EACT,KAAK,wBAAwB,EAC7B,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,WAAW,EACX,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,GAC3B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,oCAAoC,EACpC,KAAK,qCAAqC,EAC1C,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACL,mCAAmC,EACnC,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,GAC/B,MAAM,8BAA8B,CAAA;AAErC,OAAO,EACL,KAAK,+BAA+B,EACpC,wBAAwB,GACzB,MAAM,eAAe,CAAA"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Flight-specific payment integration: maps a `FlightOrder` onto finance's
3
- * generic order-payment-session service and (optionally) a card provider.
3
+ * generic order-payment-session service and, for card-capable flows,
4
+ * optionally starts a card provider.
4
5
  *
5
6
  * The flights module stays payment-provider agnostic AND finance-agnostic — the
6
7
  * deps below are typed STRUCTURALLY so this file imports neither
@@ -53,6 +54,7 @@ export interface OrderPaymentSessionsLike {
53
54
  payerEmail?: string | null;
54
55
  payerName?: string | null;
55
56
  notes?: string | null;
57
+ paymentMethod?: "bank_transfer" | "credit_card" | null;
56
58
  }, startProvider?: (db: AnyDrizzleDb, sessionId: string) => Promise<void>): Promise<{
57
59
  sessionId: string;
58
60
  status: string;
@@ -62,6 +64,10 @@ export interface OrderPaymentSessionsLike {
62
64
  status: string;
63
65
  }>>;
64
66
  }
67
+ export interface FlightOrderPaymentSessionOptions {
68
+ paymentMethod?: "bank_transfer" | "credit_card";
69
+ startCardPayment?: boolean;
70
+ }
65
71
  export interface FlightOrderPaymentIntegrationDeps {
66
72
  /** Finance's generic order-payment-session service (structural). */
67
73
  orderPaymentSessions: OrderPaymentSessionsLike;
@@ -1 +1 @@
1
- {"version":3,"file":"payment-integration.d.ts","sourceRoot":"","sources":["../src/payment-integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACvE,OAAO,KAAK,EAA6B,wBAAwB,EAAE,MAAM,WAAW,CAAA;AAQpF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,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,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GACtD,iBAAiB,CAYnB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI7C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAgB7D;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,aAAa,CACX,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAA;QAChB,QAAQ,EAAE,MAAM,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KACtB,EACD,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACrE,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACxD,aAAa,CACX,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,iCAAiC;IAChD,oEAAoE;IACpE,oBAAoB,EAAE,wBAAwB,CAAA;IAC9C;;;;;;;;OAQG;IACH,gBAAgB,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5F;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,CACjD,IAAI,EAAE,iCAAiC,GACtC,wBAAwB,CAoC1B"}
1
+ {"version":3,"file":"payment-integration.d.ts","sourceRoot":"","sources":["../src/payment-integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACvE,OAAO,KAAK,EAA6B,wBAAwB,EAAE,MAAM,WAAW,CAAA;AAQpF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,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,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GACtD,iBAAiB,CAYnB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI7C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAgB7D;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,aAAa,CACX,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAA;QAChB,QAAQ,EAAE,MAAM,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACrB,aAAa,CAAC,EAAE,eAAe,GAAG,aAAa,GAAG,IAAI,CAAA;KACvD,EACD,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACrE,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACxD,aAAa,CACX,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,gCAAgC;IAC/C,aAAa,CAAC,EAAE,eAAe,GAAG,aAAa,CAAA;IAC/C,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,MAAM,WAAW,iCAAiC;IAChD,oEAAoE;IACpE,oBAAoB,EAAE,wBAAwB,CAAA;IAC9C;;;;;;;;OAQG;IACH,gBAAgB,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5F;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,CACjD,IAAI,EAAE,iCAAiC,GACtC,wBAAwB,CAuC1B"}
@@ -58,13 +58,14 @@ export function buildFlightSummary(order) {
58
58
  export function createFlightOrderPaymentIntegration(deps) {
59
59
  const { orderPaymentSessions, startCardPayment } = deps;
60
60
  return {
61
- async ensureOrderSession(c, order, contact) {
61
+ async ensureOrderSession(c, order, contact, options) {
62
62
  const db = getDb(c);
63
63
  const passengerForBilling = order.passengers[0];
64
64
  if (!passengerForBilling)
65
65
  return null;
66
66
  const amountCents = parseAmountToCents(order.totalPrice.amount);
67
- const startProvider = startCardPayment
67
+ const shouldStartCardPayment = options?.startCardPayment ?? true;
68
+ const startProvider = startCardPayment && shouldStartCardPayment
68
69
  ? (_writer, sessionId) => startCardPayment(c, sessionId, synthesizeBilling(passengerForBilling, contact))
69
70
  : undefined;
70
71
  return orderPaymentSessions.ensureSession(db, {
@@ -74,6 +75,7 @@ export function createFlightOrderPaymentIntegration(deps) {
74
75
  payerEmail: contact?.email ?? passengerForBilling.email ?? null,
75
76
  payerName: `${passengerForBilling.firstName} ${passengerForBilling.lastName}`.trim(),
76
77
  notes: buildFlightSummary(order),
78
+ paymentMethod: options?.paymentMethod,
77
79
  }, startProvider);
78
80
  },
79
81
  async fetchOrderSessions(c, orderIds) {
@@ -83,6 +83,20 @@ describe("createFlightOrderPaymentIntegration", () => {
83
83
  notes: expect.stringContaining("LHR → CDG"),
84
84
  }), undefined);
85
85
  });
86
+ it("stamps bank-transfer sessions without starting the card provider", async () => {
87
+ const startCardPayment = vi.fn(async () => undefined);
88
+ const orderPaymentSessions = stubSessions();
89
+ const integration = createFlightOrderPaymentIntegration({
90
+ orderPaymentSessions,
91
+ startCardPayment,
92
+ });
93
+ await integration.ensureOrderSession(stubContext(), stubOrder(), undefined, {
94
+ paymentMethod: "bank_transfer",
95
+ startCardPayment: false,
96
+ });
97
+ expect(orderPaymentSessions.ensureSession).toHaveBeenCalledWith(DB, expect.objectContaining({ paymentMethod: "bank_transfer" }), undefined);
98
+ expect(startCardPayment).not.toHaveBeenCalled();
99
+ });
86
100
  it("returns null when the order has no passengers", async () => {
87
101
  const orderPaymentSessions = stubSessions();
88
102
  const integration = createFlightOrderPaymentIntegration({ orderPaymentSessions });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/flights",
3
- "version": "0.137.0",
3
+ "version": "0.137.2",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -81,18 +81,18 @@
81
81
  },
82
82
  "dependencies": {
83
83
  "drizzle-orm": "^0.45.2",
84
- "hono": "^4.12.10",
85
- "zod": "^4.3.6",
84
+ "hono": "^4.12.25",
85
+ "zod": "^4.4.3",
86
86
  "@voyant-travel/db": "^0.109.4",
87
- "@voyant-travel/catalog": "^0.135.0",
87
+ "@voyant-travel/catalog": "^0.135.6",
88
88
  "@voyant-travel/catalog-contracts": "^0.108.0",
89
- "@voyant-travel/flights-contracts": "^0.104.5",
90
- "@voyant-travel/hono": "^0.117.0"
89
+ "@voyant-travel/flights-contracts": "^0.104.6",
90
+ "@voyant-travel/hono": "^0.118.2"
91
91
  },
92
92
  "devDependencies": {
93
93
  "drizzle-kit": "^0.31.10",
94
- "typescript": "^6.0.2",
95
- "vitest": "^4.1.2",
94
+ "typescript": "^6.0.3",
95
+ "vitest": "^4.1.9",
96
96
  "@voyant-travel/voyant-typescript-config": "^0.1.0"
97
97
  },
98
98
  "repository": {
@@ -101,10 +101,10 @@
101
101
  "directory": "packages/flights"
102
102
  },
103
103
  "scripts": {
104
- "typecheck": "tsc --noEmit",
104
+ "typecheck": "tsc -p tsconfig.typecheck.json",
105
105
  "lint": "biome check src/",
106
106
  "test": "vitest run",
107
- "build": "tsc -p tsconfig.json",
107
+ "build": "tsc -p tsconfig.build.json",
108
108
  "clean": "rm -rf dist tsconfig.tsbuildinfo",
109
109
  "db:generate": "drizzle-kit generate --config=drizzle.migrations.config.ts --name=flights_baseline && node ../../scripts/migrations/guard-create-type.mjs ./migrations && node ../../scripts/migrations/ensure-extensions.mjs ./migrations"
110
110
  },