@voyant-travel/flights 0.119.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +116 -0
- package/dist/contract/adapter.d.ts +2 -0
- package/dist/contract/adapter.d.ts.map +1 -0
- package/dist/contract/adapter.js +1 -0
- package/dist/contract/adapter.test.d.ts +2 -0
- package/dist/contract/adapter.test.d.ts.map +1 -0
- package/dist/contract/adapter.test.js +171 -0
- package/dist/contract/post-book-types.d.ts +2 -0
- package/dist/contract/post-book-types.d.ts.map +1 -0
- package/dist/contract/post-book-types.js +1 -0
- package/dist/contract/schemas.d.ts +2 -0
- package/dist/contract/schemas.d.ts.map +1 -0
- package/dist/contract/schemas.js +1 -0
- package/dist/contract/schemas.test.d.ts +2 -0
- package/dist/contract/schemas.test.d.ts.map +1 -0
- package/dist/contract/schemas.test.js +360 -0
- package/dist/contract/types.d.ts +2 -0
- package/dist/contract/types.d.ts.map +1 -0
- package/dist/contract/types.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/orchestration/fan-out.d.ts +81 -0
- package/dist/orchestration/fan-out.d.ts.map +1 -0
- package/dist/orchestration/fan-out.js +132 -0
- package/dist/orchestration/fan-out.test.d.ts +2 -0
- package/dist/orchestration/fan-out.test.d.ts.map +1 -0
- package/dist/orchestration/fan-out.test.js +271 -0
- package/dist/orchestration/fingerprint.d.ts +20 -0
- package/dist/orchestration/fingerprint.d.ts.map +1 -0
- package/dist/orchestration/fingerprint.js +22 -0
- package/dist/orchestration/fingerprint.test.d.ts +2 -0
- package/dist/orchestration/fingerprint.test.d.ts.map +1 -0
- package/dist/orchestration/fingerprint.test.js +91 -0
- package/dist/reference/contract.d.ts +2 -0
- package/dist/reference/contract.d.ts.map +1 -0
- package/dist/reference/contract.js +1 -0
- package/dist/reference/local-postgres.d.ts +390 -0
- package/dist/reference/local-postgres.d.ts.map +1 -0
- package/dist/reference/local-postgres.js +194 -0
- package/dist/reference/static-bundle.d.ts +2 -0
- package/dist/reference/static-bundle.d.ts.map +1 -0
- package/dist/reference/static-bundle.js +1 -0
- package/dist/reference/static-bundle.test.d.ts +2 -0
- package/dist/reference/static-bundle.test.d.ts.map +1 -0
- package/dist/reference/static-bundle.test.js +75 -0
- package/dist/snapshot.d.ts +2 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +1 -0
- package/dist/snapshot.test.d.ts +2 -0
- package/dist/snapshot.test.d.ts.map +1 -0
- package/dist/snapshot.test.js +96 -0
- package/package.json +96 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { fanOutFlightSearch } from "./fan-out.js";
|
|
3
|
+
function makeOffer(overrides) {
|
|
4
|
+
return {
|
|
5
|
+
offerId: overrides.offerId ?? "ofr_default",
|
|
6
|
+
source: overrides.source ?? "test",
|
|
7
|
+
itineraries: [
|
|
8
|
+
{
|
|
9
|
+
segments: [
|
|
10
|
+
{
|
|
11
|
+
segmentId: "s1",
|
|
12
|
+
carrierCode: "BA",
|
|
13
|
+
flightNumber: "177",
|
|
14
|
+
departure: { iataCode: "LHR", at: "2026-10-15T11:00:00+00:00" },
|
|
15
|
+
arrival: { iataCode: "JFK", at: "2026-10-15T14:00:00-04:00" },
|
|
16
|
+
cabin: "economy",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
fareBreakdowns: [
|
|
22
|
+
{
|
|
23
|
+
passengerType: "adult",
|
|
24
|
+
passengerCount: 1,
|
|
25
|
+
baseFare: { amount: "500", currency: "USD" },
|
|
26
|
+
taxes: { amount: "100", currency: "USD" },
|
|
27
|
+
total: { amount: "600", currency: "USD" },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
totalPrice: overrides.totalPrice ?? { amount: "600", currency: "USD" },
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function makeAdapter(provider, behaviour = {}) {
|
|
35
|
+
return {
|
|
36
|
+
capabilities: {
|
|
37
|
+
provider,
|
|
38
|
+
declared: [],
|
|
39
|
+
maxSlicesPerSearch: behaviour.maxSlices,
|
|
40
|
+
},
|
|
41
|
+
async searchFlights(ctx) {
|
|
42
|
+
behaviour.captureContext?.(ctx);
|
|
43
|
+
if (behaviour.delayMs) {
|
|
44
|
+
await new Promise((r) => setTimeout(r, behaviour.delayMs));
|
|
45
|
+
}
|
|
46
|
+
if (behaviour.throws)
|
|
47
|
+
throw behaviour.throws;
|
|
48
|
+
return { offers: behaviour.offers ?? [] };
|
|
49
|
+
},
|
|
50
|
+
async priceOffer() {
|
|
51
|
+
throw new Error("not implemented in test");
|
|
52
|
+
},
|
|
53
|
+
async bookFlight() {
|
|
54
|
+
throw new Error("not implemented in test");
|
|
55
|
+
},
|
|
56
|
+
async getOrder() {
|
|
57
|
+
throw new Error("not implemented in test");
|
|
58
|
+
},
|
|
59
|
+
async cancelOrder() {
|
|
60
|
+
throw new Error("not implemented in test");
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const oneSliceRequest = {
|
|
65
|
+
slices: [{ origin: "LHR", destination: "JFK", departureDate: "2026-10-15" }],
|
|
66
|
+
passengers: { adults: 1 },
|
|
67
|
+
cabin: "economy",
|
|
68
|
+
};
|
|
69
|
+
describe("fanOutFlightSearch", () => {
|
|
70
|
+
it("merges identical itineraries from multiple providers, keeping cheapest as primary", async () => {
|
|
71
|
+
const result = await fanOutFlightSearch({
|
|
72
|
+
adapters: [
|
|
73
|
+
{
|
|
74
|
+
connectionId: "conn_amadeus",
|
|
75
|
+
adapter: makeAdapter("amadeus", {
|
|
76
|
+
offers: [
|
|
77
|
+
makeOffer({ source: "amadeus", totalPrice: { amount: "650", currency: "USD" } }),
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
connectionId: "conn_hisky",
|
|
83
|
+
adapter: makeAdapter("hisky", {
|
|
84
|
+
offers: [
|
|
85
|
+
makeOffer({ source: "hisky", totalPrice: { amount: "600", currency: "USD" } }),
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
request: oneSliceRequest,
|
|
91
|
+
});
|
|
92
|
+
expect(result.offers).toHaveLength(1);
|
|
93
|
+
expect(result.offers[0]?.cheapest.totalPrice.amount).toBe("600");
|
|
94
|
+
expect(result.offers[0]?.cheapest.source).toBe("hisky");
|
|
95
|
+
expect(result.offers[0]?.alternates).toHaveLength(1);
|
|
96
|
+
expect(result.offers[0]?.alternates[0]?.source).toBe("amadeus");
|
|
97
|
+
expect(result.offers[0]?.sourceConnectionIds.sort()).toEqual(["conn_amadeus", "conn_hisky"]);
|
|
98
|
+
});
|
|
99
|
+
it("sorts merged offers by cheapest price ascending", async () => {
|
|
100
|
+
const result = await fanOutFlightSearch({
|
|
101
|
+
adapters: [
|
|
102
|
+
{
|
|
103
|
+
connectionId: "conn_a",
|
|
104
|
+
adapter: makeAdapter("a", {
|
|
105
|
+
offers: [
|
|
106
|
+
makeOffer({
|
|
107
|
+
offerId: "ofr_expensive",
|
|
108
|
+
source: "a",
|
|
109
|
+
itineraries: [
|
|
110
|
+
{
|
|
111
|
+
segments: [
|
|
112
|
+
{
|
|
113
|
+
segmentId: "s",
|
|
114
|
+
carrierCode: "VS",
|
|
115
|
+
flightNumber: "3",
|
|
116
|
+
departure: { iataCode: "LHR", at: "2026-10-15T15:00:00+00:00" },
|
|
117
|
+
arrival: { iataCode: "JFK", at: "2026-10-15T18:00:00-04:00" },
|
|
118
|
+
cabin: "economy",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
totalPrice: { amount: "1000", currency: "USD" },
|
|
124
|
+
}),
|
|
125
|
+
makeOffer({
|
|
126
|
+
offerId: "ofr_cheap",
|
|
127
|
+
source: "a",
|
|
128
|
+
totalPrice: { amount: "300", currency: "USD" },
|
|
129
|
+
}),
|
|
130
|
+
],
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
request: oneSliceRequest,
|
|
135
|
+
});
|
|
136
|
+
expect(result.offers).toHaveLength(2);
|
|
137
|
+
expect(result.offers[0]?.cheapest.totalPrice.amount).toBe("300");
|
|
138
|
+
expect(result.offers[1]?.cheapest.totalPrice.amount).toBe("1000");
|
|
139
|
+
});
|
|
140
|
+
it("flags timed-out connections and returns results from responding ones", async () => {
|
|
141
|
+
const result = await fanOutFlightSearch({
|
|
142
|
+
adapters: [
|
|
143
|
+
{
|
|
144
|
+
connectionId: "conn_fast",
|
|
145
|
+
adapter: makeAdapter("fast", { offers: [makeOffer({ source: "fast" })] }),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
connectionId: "conn_slow",
|
|
149
|
+
adapter: makeAdapter("slow", { delayMs: 200 }),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
request: oneSliceRequest,
|
|
153
|
+
perConnectionTimeoutMs: 50,
|
|
154
|
+
});
|
|
155
|
+
expect(result.offers).toHaveLength(1);
|
|
156
|
+
const fast = result.perConnection.find((c) => c.connectionId === "conn_fast");
|
|
157
|
+
const slow = result.perConnection.find((c) => c.connectionId === "conn_slow");
|
|
158
|
+
expect(fast?.status).toBe("ok");
|
|
159
|
+
expect(slow?.status).toBe("timeout");
|
|
160
|
+
});
|
|
161
|
+
it("flags connections that throw as 'error' without losing other results", async () => {
|
|
162
|
+
const result = await fanOutFlightSearch({
|
|
163
|
+
adapters: [
|
|
164
|
+
{
|
|
165
|
+
connectionId: "conn_ok",
|
|
166
|
+
adapter: makeAdapter("ok", { offers: [makeOffer({})] }),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
connectionId: "conn_fail",
|
|
170
|
+
adapter: makeAdapter("fail", { throws: new Error("provider down") }),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
request: oneSliceRequest,
|
|
174
|
+
});
|
|
175
|
+
expect(result.offers).toHaveLength(1);
|
|
176
|
+
const fail = result.perConnection.find((c) => c.connectionId === "conn_fail");
|
|
177
|
+
expect(fail?.status).toBe("error");
|
|
178
|
+
expect(fail?.errorMessage).toBe("provider down");
|
|
179
|
+
});
|
|
180
|
+
it("flags connections that don't support the request's slice count as capability_missing", async () => {
|
|
181
|
+
const result = await fanOutFlightSearch({
|
|
182
|
+
adapters: [
|
|
183
|
+
{
|
|
184
|
+
connectionId: "conn_pp",
|
|
185
|
+
adapter: makeAdapter("point-to-point", {
|
|
186
|
+
maxSlices: 1,
|
|
187
|
+
offers: [makeOffer({})],
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
request: {
|
|
192
|
+
...oneSliceRequest,
|
|
193
|
+
slices: [
|
|
194
|
+
...oneSliceRequest.slices,
|
|
195
|
+
{ origin: "JFK", destination: "LAX", departureDate: "2026-10-20" },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
expect(result.offers).toHaveLength(0);
|
|
200
|
+
expect(result.perConnection[0]?.status).toBe("capability_missing");
|
|
201
|
+
});
|
|
202
|
+
it("respects the limit parameter on the merged result", async () => {
|
|
203
|
+
const result = await fanOutFlightSearch({
|
|
204
|
+
adapters: [
|
|
205
|
+
{
|
|
206
|
+
connectionId: "conn_a",
|
|
207
|
+
adapter: makeAdapter("a", {
|
|
208
|
+
offers: [
|
|
209
|
+
makeOffer({ offerId: "1", totalPrice: { amount: "100", currency: "USD" } }),
|
|
210
|
+
makeOffer({
|
|
211
|
+
offerId: "2",
|
|
212
|
+
itineraries: [
|
|
213
|
+
{
|
|
214
|
+
segments: [
|
|
215
|
+
{
|
|
216
|
+
segmentId: "s",
|
|
217
|
+
carrierCode: "AA",
|
|
218
|
+
flightNumber: "1",
|
|
219
|
+
departure: { iataCode: "LHR", at: "2026-10-15T20:00:00+00:00" },
|
|
220
|
+
arrival: { iataCode: "JFK", at: "2026-10-15T23:00:00-04:00" },
|
|
221
|
+
cabin: "economy",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
totalPrice: { amount: "200", currency: "USD" },
|
|
227
|
+
}),
|
|
228
|
+
],
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
request: oneSliceRequest,
|
|
233
|
+
limit: 1,
|
|
234
|
+
});
|
|
235
|
+
expect(result.offers).toHaveLength(1);
|
|
236
|
+
expect(result.offers[0]?.cheapest.totalPrice.amount).toBe("100");
|
|
237
|
+
});
|
|
238
|
+
it("propagates adapter context fields to each connection", async () => {
|
|
239
|
+
const controller = new AbortController();
|
|
240
|
+
const captured = [];
|
|
241
|
+
await fanOutFlightSearch({
|
|
242
|
+
adapters: [
|
|
243
|
+
{
|
|
244
|
+
connectionId: "conn_a",
|
|
245
|
+
adapter: makeAdapter("a", {
|
|
246
|
+
offers: [makeOffer({})],
|
|
247
|
+
captureContext: (ctx) => {
|
|
248
|
+
captured.push(ctx);
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
context: {
|
|
252
|
+
requestId: "req_1",
|
|
253
|
+
correlationId: "corr_1",
|
|
254
|
+
idempotencyKey: "idem_1",
|
|
255
|
+
environment: "sandbox",
|
|
256
|
+
signal: controller.signal,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
request: oneSliceRequest,
|
|
261
|
+
});
|
|
262
|
+
expect(captured[0]).toMatchObject({
|
|
263
|
+
connectionId: "conn_a",
|
|
264
|
+
requestId: "req_1",
|
|
265
|
+
correlationId: "corr_1",
|
|
266
|
+
idempotencyKey: "idem_1",
|
|
267
|
+
environment: "sandbox",
|
|
268
|
+
});
|
|
269
|
+
expect(captured[0]?.signal).toBe(controller.signal);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Itinerary fingerprint — deterministic key derived from a `FlightOffer`'s
|
|
3
|
+
* segments. Two providers selling the same flight produce identical
|
|
4
|
+
* fingerprints; the multi-connection fan-out uses this to merge offers
|
|
5
|
+
* across connections.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors voyant-cloud's `itineraryFingerprint` so fingerprints are
|
|
8
|
+
* portable: an offer fingerprinted in voyant-cloud and another fingerprinted
|
|
9
|
+
* here for the same physical flight match by string equality.
|
|
10
|
+
*
|
|
11
|
+
* See `docs/architecture/catalog-flights-architecture.md` §4.
|
|
12
|
+
*/
|
|
13
|
+
import type { FlightOffer } from "../contract/types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Deterministic key derived from segments: carrier code + flight number +
|
|
16
|
+
* departure/arrival airports + times + cabin. Two providers selling the
|
|
17
|
+
* same flight produce identical fingerprints.
|
|
18
|
+
*/
|
|
19
|
+
export declare function itineraryFingerprint(offer: FlightOffer): string;
|
|
20
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/orchestration/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAEvD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAS/D"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Itinerary fingerprint — deterministic key derived from a `FlightOffer`'s
|
|
3
|
+
* segments. Two providers selling the same flight produce identical
|
|
4
|
+
* fingerprints; the multi-connection fan-out uses this to merge offers
|
|
5
|
+
* across connections.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors voyant-cloud's `itineraryFingerprint` so fingerprints are
|
|
8
|
+
* portable: an offer fingerprinted in voyant-cloud and another fingerprinted
|
|
9
|
+
* here for the same physical flight match by string equality.
|
|
10
|
+
*
|
|
11
|
+
* See `docs/architecture/catalog-flights-architecture.md` §4.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Deterministic key derived from segments: carrier code + flight number +
|
|
15
|
+
* departure/arrival airports + times + cabin. Two providers selling the
|
|
16
|
+
* same flight produce identical fingerprints.
|
|
17
|
+
*/
|
|
18
|
+
export function itineraryFingerprint(offer) {
|
|
19
|
+
return offer.itineraries
|
|
20
|
+
.flatMap((itinerary) => itinerary.segments.map((s) => `${s.carrierCode}${s.flightNumber}|${s.departure.iataCode}|${s.departure.at}|${s.arrival.iataCode}|${s.arrival.at}|${s.cabin}`))
|
|
21
|
+
.join("→");
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.test.d.ts","sourceRoot":"","sources":["../../src/orchestration/fingerprint.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { itineraryFingerprint } from "./fingerprint.js";
|
|
3
|
+
const offerLondonToNYC = {
|
|
4
|
+
offerId: "ofr_a",
|
|
5
|
+
source: "amadeus",
|
|
6
|
+
itineraries: [
|
|
7
|
+
{
|
|
8
|
+
segments: [
|
|
9
|
+
{
|
|
10
|
+
segmentId: "s1",
|
|
11
|
+
carrierCode: "BA",
|
|
12
|
+
flightNumber: "177",
|
|
13
|
+
departure: { iataCode: "LHR", at: "2026-10-15T11:00:00+00:00" },
|
|
14
|
+
arrival: { iataCode: "JFK", at: "2026-10-15T14:00:00-04:00" },
|
|
15
|
+
cabin: "economy",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
fareBreakdowns: [
|
|
21
|
+
{
|
|
22
|
+
passengerType: "adult",
|
|
23
|
+
passengerCount: 1,
|
|
24
|
+
baseFare: { amount: "500", currency: "USD" },
|
|
25
|
+
taxes: { amount: "100", currency: "USD" },
|
|
26
|
+
total: { amount: "600", currency: "USD" },
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
totalPrice: { amount: "600", currency: "USD" },
|
|
30
|
+
};
|
|
31
|
+
describe("itineraryFingerprint", () => {
|
|
32
|
+
it("produces a deterministic key from segments", () => {
|
|
33
|
+
const fp = itineraryFingerprint(offerLondonToNYC);
|
|
34
|
+
expect(fp).toBe("BA177|LHR|2026-10-15T11:00:00+00:00|JFK|2026-10-15T14:00:00-04:00|economy");
|
|
35
|
+
});
|
|
36
|
+
it("two providers selling the same flight produce identical fingerprints", () => {
|
|
37
|
+
const fromAmadeus = { ...offerLondonToNYC, offerId: "ofr_amadeus", source: "amadeus" };
|
|
38
|
+
const fromHisky = { ...offerLondonToNYC, offerId: "ofr_hisky", source: "hisky" };
|
|
39
|
+
expect(itineraryFingerprint(fromAmadeus)).toBe(itineraryFingerprint(fromHisky));
|
|
40
|
+
});
|
|
41
|
+
it("differs when carrier changes", () => {
|
|
42
|
+
const altered = {
|
|
43
|
+
...offerLondonToNYC,
|
|
44
|
+
itineraries: [
|
|
45
|
+
{
|
|
46
|
+
segments: [
|
|
47
|
+
{
|
|
48
|
+
...offerLondonToNYC.itineraries[0].segments[0],
|
|
49
|
+
carrierCode: "VS",
|
|
50
|
+
flightNumber: "3",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
expect(itineraryFingerprint(altered)).not.toBe(itineraryFingerprint(offerLondonToNYC));
|
|
57
|
+
});
|
|
58
|
+
it("differs when cabin class changes (price tier matters for grouping)", () => {
|
|
59
|
+
const business = {
|
|
60
|
+
...offerLondonToNYC,
|
|
61
|
+
itineraries: [
|
|
62
|
+
{
|
|
63
|
+
segments: [{ ...offerLondonToNYC.itineraries[0].segments[0], cabin: "business" }],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
expect(itineraryFingerprint(business)).not.toBe(itineraryFingerprint(offerLondonToNYC));
|
|
68
|
+
});
|
|
69
|
+
it("multi-leg itineraries fingerprint with arrow separator between segments", () => {
|
|
70
|
+
const multiLeg = {
|
|
71
|
+
...offerLondonToNYC,
|
|
72
|
+
itineraries: [
|
|
73
|
+
{
|
|
74
|
+
segments: [
|
|
75
|
+
offerLondonToNYC.itineraries[0].segments[0],
|
|
76
|
+
{
|
|
77
|
+
segmentId: "s2",
|
|
78
|
+
carrierCode: "AA",
|
|
79
|
+
flightNumber: "100",
|
|
80
|
+
departure: { iataCode: "JFK", at: "2026-10-22T10:00:00-04:00" },
|
|
81
|
+
arrival: { iataCode: "LHR", at: "2026-10-22T22:00:00+00:00" },
|
|
82
|
+
cabin: "economy",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
expect(itineraryFingerprint(multiLeg)).toContain("→");
|
|
89
|
+
expect(itineraryFingerprint(multiLeg).split("→")).toHaveLength(2);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/reference/contract.ts"],"names":[],"mappings":"AAAA,cAAc,qDAAqD,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@voyant-travel/flights-contracts/reference/contract";
|