@mattywhite/skyscanner-api 1.0.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.
@@ -0,0 +1,634 @@
1
+ const axios = require('axios');
2
+ const config = require('./config');
3
+ const PXSolver = require('./px');
4
+ const {
5
+ Location,
6
+ Airport,
7
+ CabinClass,
8
+ SpecialTypes,
9
+ SkyscannerResponse,
10
+ Coordinates,
11
+ } = require('./types');
12
+ const {
13
+ AttemptsExhaustedIncompleteResponse,
14
+ BannedWithCaptcha,
15
+ GenericError
16
+ } = require('./errors');
17
+ const crypto = require('crypto');
18
+
19
+ class SkyScanner {
20
+ /**
21
+ * A client for interacting with the Skyscanner flight and car rental APIs.
22
+ *
23
+ * @param {Object} options - Configuration options
24
+ * @param {string} options.locale - Locale code for results (default: "en-US")
25
+ * @param {string} options.currency - Currency code for pricing (default: "USD")
26
+ * @param {string} options.market - Market region code (default: "US")
27
+ * @param {number} options.retryDelay - Seconds to wait between polling retries (default: 2)
28
+ * @param {number} options.maxRetries - Maximum number of polling retries (default: 15)
29
+ * @param {string} options.proxy - Proxy URL for HTTP requests
30
+ * @param {string} options.pxAuthorization - Optional pre-generated PX authorization token
31
+ * @param {boolean} options.verify - If set to false, SSL certificates won't be verified (default: true)
32
+ */
33
+ constructor(options = {}) {
34
+ this.locale = options.locale || "en-US";
35
+ this.currency = options.currency || "USD";
36
+ this.market = options.market || "US";
37
+ this.retryDelay = options.retryDelay || 2;
38
+ this.maxRetries = options.maxRetries || 15;
39
+ this.proxy = options.proxy || "";
40
+ this.verify = options.verify !== undefined ? options.verify : true;
41
+
42
+ this.pxAuthorization = null;
43
+ this.UUID = null;
44
+
45
+ this.initialized = false;
46
+ }
47
+
48
+ /**
49
+ * Initialize the client (async initialization)
50
+ * @private
51
+ */
52
+ async _initialize() {
53
+ if (this.initialized) return;
54
+
55
+ if (!this.pxAuthorization) {
56
+ const solver = new PXSolver(this.proxy, this.verify);
57
+ [this.pxAuthorization, this.UUID] = await solver.genPxAuthorization();
58
+ }
59
+
60
+ this.headers = {
61
+ "X-Skyscanner-ChannelId": "goandroid",
62
+ "X-Skyscanner-Currency": this.currency,
63
+ "X-Skyscanner-Locale": this.locale,
64
+ "X-Skyscanner-Market": this.market,
65
+ "X-Skyscanner-Device": "Android-phone",
66
+ "X-Skyscanner-Device-Class": "phone",
67
+ "X-Skyscanner-Client-Type": "net.skyscanner.android.main",
68
+ "X-Skyscanner-Client-Network-Type": "WIFI",
69
+ "Content-Type": "application/json; charset=UTF-8",
70
+ "X-Px-Authorization": this.pxAuthorization,
71
+ "X-PX-Os": "Android",
72
+ "X-Px-Uuid": this.UUID,
73
+ "X-Px-Mobile-Sdk-Version": "3.4.4",
74
+ };
75
+
76
+ this.axiosConfig = {
77
+ headers: this.headers,
78
+ proxy: this.proxy ? {
79
+ host: new URL(this.proxy).hostname,
80
+ port: new URL(this.proxy).port
81
+ } : false
82
+ };
83
+
84
+ this.initialized = true;
85
+ }
86
+
87
+ /**
88
+ * Search for flight prices between two points.
89
+ *
90
+ * @param {Object} params - Search parameters
91
+ * @param {Airport} params.origin - Origin airport object
92
+ * @param {Airport|SpecialTypes} params.destination - Destination airport or special search type
93
+ * @param {Date|string|null} params.departDate - Departure date or special enum (default: now)
94
+ * @param {Date|string|null} params.returnDate - Return date or special enum (optional)
95
+ * @param {string} params.cabinClass - Cabin class for travel (default: ECONOMY)
96
+ * @param {number} params.adults - Number of adult passengers (max 8)
97
+ * @param {number[]} params.childAges - List of child ages (each 0-17, max 8 children)
98
+ * @returns {Promise<SkyscannerResponse>} Parsed response containing pricing and itinerary data
99
+ */
100
+ async getFlightPrices({
101
+ origin,
102
+ destination,
103
+ departDate = null,
104
+ returnDate = null,
105
+ cabinClass = CabinClass.ECONOMY,
106
+ adults = 1,
107
+ childAges = []
108
+ }) {
109
+ await this._initialize();
110
+
111
+ if (!departDate) {
112
+ departDate = new Date();
113
+ }
114
+
115
+ // Validate child ages
116
+ if (!childAges.every(age => age >= 0 && age < 18)) {
117
+ throw new Error("Child ages must be >= 0 and < 18");
118
+ }
119
+
120
+ // Validate dates
121
+ if (departDate instanceof Date && returnDate instanceof Date) {
122
+ if (returnDate < departDate) {
123
+ throw new Error("Return date cannot be past departure");
124
+ }
125
+ }
126
+
127
+ // Validate passenger counts
128
+ if (!(adults <= 8 && childAges.length <= 8)) {
129
+ throw new Error("Max 8 adults and 8 children");
130
+ }
131
+
132
+ // Validate cabin class for special searches
133
+ const specialValues = [SpecialTypes.ANYTIME, SpecialTypes.EVERYWHERE];
134
+ if ((specialValues.includes(departDate) || specialValues.includes(returnDate) ||
135
+ specialValues.includes(destination)) && cabinClass !== CabinClass.ECONOMY) {
136
+ throw new Error("To search for cabin class that's not economy enter departDate / returnDate and destination");
137
+ }
138
+
139
+ // Validate dates are not in the past
140
+ const now = new Date();
141
+ if ((departDate instanceof Date && departDate < now) ||
142
+ (returnDate instanceof Date && returnDate < now)) {
143
+ throw new Error("Depart date or return date cannot be in the past");
144
+ }
145
+
146
+ const jsonData = {
147
+ adults: adults,
148
+ childAges: childAges,
149
+ cabinClass: cabinClass,
150
+ legs: [
151
+ this._genLeg({ departDate, origin, destination }),
152
+ ],
153
+ options: null,
154
+ };
155
+
156
+ if (returnDate) {
157
+ const returnLeg = this._genLeg({
158
+ departDate: returnDate,
159
+ origin: destination,
160
+ destination: origin
161
+ });
162
+ jsonData.legs.push(returnLeg);
163
+ }
164
+
165
+ const customHeaders = {
166
+ ...this.headers,
167
+ "X-Skyscanner-Viewid": crypto.randomUUID(),
168
+ "Content-Type": "application/json; charset=UTF-8",
169
+ "Accept-Encoding": "gzip, deflate, br",
170
+ };
171
+
172
+ let req;
173
+ try {
174
+ req = await axios.post(config.UNIFIED_SEARCH_ENDPOINT, jsonData, {
175
+ ...this.axiosConfig,
176
+ headers: customHeaders
177
+ });
178
+ } catch (error) {
179
+ if (error.response && error.response.status === 403) {
180
+ throw new BannedWithCaptcha(
181
+ "https://www.skyscanner.net" + error.response.data.redirect_to
182
+ );
183
+ }
184
+ throw error;
185
+ }
186
+
187
+ let data = req.data;
188
+
189
+ if (data.context.status === "complete") {
190
+ return new SkyscannerResponse(
191
+ data,
192
+ this._getSessionId(data),
193
+ jsonData,
194
+ origin,
195
+ destination
196
+ );
197
+ }
198
+
199
+ let retries = 0;
200
+ let sessionId = data.context.sessionId;
201
+
202
+ while (retries < this.maxRetries) {
203
+ await this._sleep(this.retryDelay * 1000);
204
+ const url = config.UNIFIED_SEARCH_ENDPOINT + sessionId;
205
+
206
+ try {
207
+ req = await axios.get(url, {
208
+ ...this.axiosConfig,
209
+ headers: customHeaders
210
+ });
211
+ } catch (error) {
212
+ throw new Error(`Error while scraping flight, status_code: ${error.response?.status} response: ${error.response?.data}`);
213
+ }
214
+
215
+ data = req.data;
216
+
217
+ if (req.status !== 200) {
218
+ throw new Error(`Error while scraping flight, status_code: ${req.status} response: ${req.data}`);
219
+ }
220
+
221
+ if (data.context.status === "complete") {
222
+ return new SkyscannerResponse(
223
+ data,
224
+ this._getSessionId(data),
225
+ jsonData,
226
+ origin,
227
+ destination
228
+ );
229
+ }
230
+ sessionId = data.context.sessionId;
231
+ retries += 1;
232
+ }
233
+
234
+ throw new AttemptsExhaustedIncompleteResponse();
235
+ }
236
+
237
+ /**
238
+ * Auto-suggest airports based on a query string.
239
+ *
240
+ * @param {string} query - Text to search for airport names or codes
241
+ * @param {Date|null} departDate - Optional outbound date for context
242
+ * @param {Date|null} returnDate - Optional inbound date for context
243
+ * @returns {Promise<Airport[]>} List of suggested Airport objects
244
+ */
245
+ async searchAirports(query, departDate = null, returnDate = null) {
246
+ await this._initialize();
247
+
248
+ const params = {
249
+ query: query,
250
+ inboundDate: departDate ? this._formatDate(departDate) : "",
251
+ outboundDate: returnDate ? this._formatDate(returnDate) : "",
252
+ };
253
+
254
+ let req;
255
+ try {
256
+ req = await axios.get(config.SEARCH_ORIGIN_ENDPOINT, {
257
+ ...this.axiosConfig,
258
+ params
259
+ });
260
+ } catch (error) {
261
+ if (error.response && error.response.status === 403) {
262
+ throw new BannedWithCaptcha(
263
+ "https://www.skyscanner.net" + error.response.data.redirect_to
264
+ );
265
+ }
266
+ throw error;
267
+ }
268
+
269
+ if (req.status !== 200) {
270
+ throw new GenericError(
271
+ `Error when scraping airports, code: ${req.status} text: ${req.data}`
272
+ );
273
+ }
274
+
275
+ const data = req.data;
276
+ return data.inputSuggest.map(e => new Airport(
277
+ e.presentation.title,
278
+ e.navigation.entityId,
279
+ e.navigation.relevantFlightParams.skyId
280
+ ));
281
+ }
282
+
283
+ /**
284
+ * Auto-suggest locations based on a query.
285
+ *
286
+ * @param {string} query - Text to search for locations
287
+ * @returns {Promise<Location[]>} List of suggested Location objects
288
+ */
289
+ async searchLocations(query) {
290
+ await this._initialize();
291
+
292
+ const url = config.LOCATION_SEARCH_ENDPOINT
293
+ .replace('{locale}', this.locale)
294
+ .replace('{market}', this.market) + query;
295
+
296
+ const params = { autosuggestExp: "neighborhood_b" };
297
+
298
+ let req;
299
+ try {
300
+ req = await axios.get(url, {
301
+ ...this.axiosConfig,
302
+ params
303
+ });
304
+ } catch (error) {
305
+ if (error.response && error.response.status === 403) {
306
+ throw new BannedWithCaptcha(
307
+ "https://www.skyscanner.net" + error.response.data.redirect_to
308
+ );
309
+ }
310
+ throw error;
311
+ }
312
+
313
+ if (req.status !== 200) {
314
+ throw new GenericError(
315
+ `Error when scraping locations, code: ${req.status} text: ${req.data}`
316
+ );
317
+ }
318
+
319
+ return req.data.map(location => new Location(
320
+ location.entity_name,
321
+ location.entity_id,
322
+ location.location
323
+ ));
324
+ }
325
+
326
+ /**
327
+ * Retrieve a single Airport by its IATA code.
328
+ *
329
+ * @param {string} airportCode - Three-letter IATA code to look up
330
+ * @returns {Promise<Airport>} Matching Airport object
331
+ */
332
+ async getAirportByCode(airportCode) {
333
+ const airports = await this.searchAirports(airportCode);
334
+ for (const airport of airports) {
335
+ if (airport.skyId === airportCode) {
336
+ return airport;
337
+ }
338
+ }
339
+ throw new GenericError(`IATA code not found: ${airportCode}`);
340
+ }
341
+
342
+ /**
343
+ * Retrieve detailed information for a specific flight itinerary.
344
+ *
345
+ * @param {string} itineraryId - Unique identifier for the itinerary
346
+ * @param {SkyscannerResponse} response - The response object from a flight search
347
+ * @returns {Promise<Object>} Parsed JSON response with itinerary details
348
+ */
349
+ async getItineraryDetails(itineraryId, response) {
350
+ await this._initialize();
351
+
352
+ const jsonData = {
353
+ itineraryId: itineraryId,
354
+ searchSessionId: response.sessionId,
355
+ featuresEnabled: ["FEATURES_ENABLED_ITINERARY_LEGACY_INFO"],
356
+ userPreferences: {
357
+ market: this.market,
358
+ currencyCode: this.currency,
359
+ locale: this.locale,
360
+ },
361
+ searchRequestDetails: {
362
+ adults: response.searchPayload.adults,
363
+ cabinClass: response.searchPayload.cabinClass,
364
+ legs: [],
365
+ },
366
+ options: {
367
+ totalCostOptions: {
368
+ fareAttributeFilters: [
369
+ "ATTRIBUTE_CABIN_BAGGAGE",
370
+ "ATTRIBUTE_CHECKED_BAGGAGE",
371
+ ]
372
+ }
373
+ },
374
+ };
375
+
376
+ if (response.searchPayload.childAges) {
377
+ jsonData.searchRequestDetails.childAges = response.searchPayload.childAges;
378
+ }
379
+
380
+ for (const leg of response.searchPayload.legs) {
381
+ const originId = leg.legOrigin?.entityId || leg.legOrigin;
382
+ const destinationId = leg.legDestination?.entityId || leg.legDestination;
383
+ const originIata = response.origin.entityId === originId
384
+ ? response.origin.skyId
385
+ : response.destination.skyId;
386
+ const destinationIata = response.destination.entityId === destinationId
387
+ ? response.destination.skyId
388
+ : response.origin.skyId;
389
+ const date = leg.dates;
390
+
391
+ const res = {
392
+ originIata: originIata,
393
+ destinationIata: destinationIata,
394
+ date: {
395
+ year: date.year,
396
+ month: date.month,
397
+ day: date.day,
398
+ },
399
+ addAlternativeOrigins: false,
400
+ addAlternativeDestinations: false,
401
+ originSkyscannerCode: originIata,
402
+ destinationSkyscannerCode: destinationIata,
403
+ originEntityId: "",
404
+ destinationEntityId: "",
405
+ };
406
+ jsonData.searchRequestDetails.legs.push(res);
407
+ }
408
+
409
+ const headers = {
410
+ "grpc-metadata-x-skyscanner-devicedetection-istablet": "false",
411
+ "grpc-metadata-x-skyscanner-devicedetection-ismobile": "true",
412
+ "grpc-metadata-x-skyscanner-channelid": "goandroid",
413
+ "grpc-metadata-x-skyscanner-viewid": crypto.randomUUID(),
414
+ "grpc-metadata-x-skyscanner-clientid": "skyscanner_app",
415
+ "grpc-metadata-x-skyscanner-client-type": "net.skyscanner.android.main",
416
+ "grpc-metadata-skyscanner-flights-config-session-id": crypto.randomUUID(),
417
+ "grpc-metadata-x-skyscanner-consent-information": "true",
418
+ "grpc-metadata-x-skyscanner-consent-adverts": "true",
419
+ "content-type": "application/json; charset=utf-8",
420
+ "accept-encoding": "gzip",
421
+ };
422
+
423
+ let req;
424
+ try {
425
+ req = await axios.post(config.ITINERARY_DETAILS_ENDPOINT, jsonData, {
426
+ ...this.axiosConfig,
427
+ headers: { ...this.headers, ...headers }
428
+ });
429
+ } catch (error) {
430
+ if (error.response && error.response.status === 403) {
431
+ throw new BannedWithCaptcha(
432
+ "https://www.skyscanner.net" + error.response.data.redirect_to
433
+ );
434
+ }
435
+ throw error;
436
+ }
437
+
438
+ if (req.status !== 200) {
439
+ throw new GenericError(
440
+ `Error fetching itinerary details, code: ${req.status}, text: ${req.data}`
441
+ );
442
+ }
443
+
444
+ return req.data;
445
+ }
446
+
447
+ /**
448
+ * Parse a Skyscanner car rental URL and fetch rental options.
449
+ *
450
+ * @param {string} url - Skyscanner car hire URL
451
+ * @returns {Promise<Object>} Car rental search results
452
+ */
453
+ async getCarRentalFromUrl(url) {
454
+ url = url.split("?")[0];
455
+ const args = url.split("/");
456
+
457
+ if (args.length < 14) {
458
+ throw new Error("URL not valid");
459
+ }
460
+
461
+ const isDriverOver25 = parseInt(args[8]) >= 25;
462
+ const origin = new Location("", args[9], "");
463
+ const destination = new Location("", args[10], "");
464
+ const departTime = new Date(args[11]);
465
+ const returnTime = new Date(args[12]);
466
+
467
+ return this.getCarRental({
468
+ origin,
469
+ departTime,
470
+ returnTime,
471
+ isDriverOver25,
472
+ destination
473
+ });
474
+ }
475
+
476
+ /**
477
+ * Search for car rental options between two locations and times.
478
+ *
479
+ * @param {Object} params - Search parameters
480
+ * @param {Location|Coordinates|Airport} params.origin - Pickup location
481
+ * @param {Date} params.departTime - Pickup datetime
482
+ * @param {Date} params.returnTime - Drop-off datetime
483
+ * @param {Location|Coordinates|Airport|null} params.destination - Drop-off location (defaults to origin)
484
+ * @param {boolean} params.isDriverOver25 - Flag for driver age pricing threshold
485
+ * @returns {Promise<Object>} JSON response containing car rental group listings and metadata
486
+ */
487
+ async getCarRental({
488
+ origin,
489
+ departTime,
490
+ returnTime,
491
+ destination = null,
492
+ isDriverOver25 = true
493
+ }) {
494
+ await this._initialize();
495
+
496
+ if (!destination) {
497
+ destination = origin;
498
+ }
499
+
500
+ if (returnTime < departTime) {
501
+ throw new Error("Return time cannot be past depart time");
502
+ }
503
+
504
+ const now = new Date();
505
+ if (returnTime < now || departTime < now) {
506
+ throw new Error("Return or depart time cannot be in the past");
507
+ }
508
+
509
+ const firstLocation = origin instanceof Coordinates
510
+ ? `${origin.latitude},${origin.longitude}`
511
+ : origin.entityId;
512
+ const secondLocation = destination instanceof Coordinates
513
+ ? `${destination.latitude},${destination.longitude}`
514
+ : destination.entityId;
515
+
516
+ const ageValue = isDriverOver25 ? "30" : "21";
517
+ const firstDate = this._formatDateTime(departTime);
518
+ const secondDate = this._formatDateTime(returnTime);
519
+
520
+ const url = config.CAR_RENTAL_ENDPOINT
521
+ .replace('{first_location}', firstLocation)
522
+ .replace('{second_location}', secondLocation)
523
+ .replace('{driver_age}', ageValue)
524
+ .replace('{first_date}', firstDate)
525
+ .replace('{second_date}', secondDate)
526
+ .replace('{market}', this.market)
527
+ .replace('{currency}', this.currency)
528
+ .replace('{locale}', this.locale);
529
+
530
+ const params = {
531
+ group: "true",
532
+ sipp_map: "true",
533
+ channel: "android",
534
+ vndr_img_rounded: "true",
535
+ ranking_enable: "false",
536
+ reqn: "0",
537
+ version: "6.9",
538
+ include_location: "true",
539
+ city_search_enable: "true",
540
+ };
541
+
542
+ let lastCount = null;
543
+
544
+ for (let i = 0; i < this.maxRetries; i++) {
545
+ const req = await axios.get(url, {
546
+ ...this.axiosConfig,
547
+ params
548
+ });
549
+ const reqData = req.data;
550
+ params.reqn = String(parseInt(params.reqn) + 1);
551
+
552
+ const count = reqData.groups_count;
553
+ if (!lastCount) {
554
+ lastCount = count;
555
+ await this._sleep(this.retryDelay * 1000);
556
+ continue;
557
+ }
558
+
559
+ if (count === lastCount) {
560
+ return reqData;
561
+ }
562
+
563
+ lastCount = count;
564
+ await this._sleep(this.retryDelay * 1000);
565
+ }
566
+
567
+ throw new AttemptsExhaustedIncompleteResponse();
568
+ }
569
+
570
+ // Private helper methods
571
+
572
+ _getSessionId(data) {
573
+ if (!data.itineraries) {
574
+ return null;
575
+ }
576
+ return data.itineraries.context.sessionId;
577
+ }
578
+
579
+ _genLeg({ departDate = null, returnDate = null, origin = null, destination = null }) {
580
+ const res = {};
581
+ const date = departDate || returnDate;
582
+
583
+ if (date instanceof Date) {
584
+ res.dates = {
585
+ "@type": "date",
586
+ year: date.getFullYear(),
587
+ month: date.getMonth() + 1,
588
+ day: date.getDate()
589
+ };
590
+ } else {
591
+ res.dates = { "@type": date };
592
+ }
593
+
594
+ if (origin instanceof Airport) {
595
+ res.legOrigin = { "@type": "entity", entityId: origin.entityId };
596
+ } else {
597
+ res.legOrigin = { "@type": origin };
598
+ }
599
+
600
+ if (destination instanceof Airport) {
601
+ res.legDestination = { "@type": "entity", entityId: destination.entityId };
602
+ } else {
603
+ res.legDestination = { "@type": destination };
604
+ }
605
+
606
+ res.placeOfStay = destination instanceof Airport
607
+ ? destination.entityId
608
+ : origin.entityId;
609
+
610
+ return res;
611
+ }
612
+
613
+ _formatDate(date) {
614
+ const year = date.getFullYear();
615
+ const month = String(date.getMonth() + 1).padStart(2, '0');
616
+ const day = String(date.getDate()).padStart(2, '0');
617
+ return `${year}-${month}-${day}`;
618
+ }
619
+
620
+ _formatDateTime(date) {
621
+ const year = date.getFullYear();
622
+ const month = String(date.getMonth() + 1).padStart(2, '0');
623
+ const day = String(date.getDate()).padStart(2, '0');
624
+ const hours = String(date.getHours()).padStart(2, '0');
625
+ const minutes = String(date.getMinutes()).padStart(2, '0');
626
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
627
+ }
628
+
629
+ _sleep(ms) {
630
+ return new Promise(resolve => setTimeout(resolve, ms));
631
+ }
632
+ }
633
+
634
+ module.exports = SkyScanner;
package/src/types.js ADDED
@@ -0,0 +1,78 @@
1
+ class Location {
2
+ /**
3
+ * Represents an auto-suggested location from the Skyscanner API.
4
+ * @param {string} entityName - Human-readable name of the location
5
+ * @param {string} entityId - Internal Skyscanner identifier for the location
6
+ * @param {string} rawLocation - Latitude and longitude as comma-separated string
7
+ */
8
+ constructor(entityName, entityId, rawLocation) {
9
+ this.entityName = entityName;
10
+ this.entityId = entityId;
11
+ this.location = rawLocation.split(',');
12
+ }
13
+ }
14
+
15
+ class Airport {
16
+ /**
17
+ * Represents an airport in the Skyscanner system.
18
+ * @param {string} title - The display name of the airport (e.g. "London Heathrow")
19
+ * @param {string} entityId - The internal Skyscanner entity ID for the airport
20
+ * @param {string} skyId - The IATA-style Skyscanner identifier (used in search requests)
21
+ */
22
+ constructor(title, entityId, skyId) {
23
+ this.title = title;
24
+ this.entityId = entityId;
25
+ this.skyId = skyId;
26
+ }
27
+ }
28
+
29
+ class SkyscannerResponse {
30
+ /**
31
+ * Represents the response from a Skyscanner flight search.
32
+ * @param {Object} json - The raw JSON response returned by Skyscanner
33
+ * @param {string} sessionId - The unique session identifier associated with the search
34
+ * @param {Object} searchPayload - The original payload used to perform the search request
35
+ * @param {Airport} origin - The airport from which the search was initiated
36
+ * @param {Airport} destination - The destination airport, if specified
37
+ */
38
+ constructor(json, sessionId, searchPayload, origin, destination = null) {
39
+ this.json = json;
40
+ this.sessionId = sessionId;
41
+ this.searchPayload = searchPayload;
42
+ this.origin = origin;
43
+ this.destination = destination;
44
+ }
45
+ }
46
+
47
+ class Coordinates {
48
+ /**
49
+ * Represents geographic coordinates.
50
+ * @param {number} latitude - Latitude coordinate
51
+ * @param {number} longitude - Longitude coordinate
52
+ */
53
+ constructor(latitude, longitude) {
54
+ this.latitude = latitude;
55
+ this.longitude = longitude;
56
+ }
57
+ }
58
+
59
+ const CabinClass = {
60
+ ECONOMY: "economy",
61
+ PREMIUM_ECONOMY: "premium_economy",
62
+ BUSINESS: "business",
63
+ FIRST: "first"
64
+ };
65
+
66
+ const SpecialTypes = {
67
+ ANYTIME: "anytime",
68
+ EVERYWHERE: "everywhere"
69
+ };
70
+
71
+ module.exports = {
72
+ Location,
73
+ Airport,
74
+ SkyscannerResponse,
75
+ Coordinates,
76
+ CabinClass,
77
+ SpecialTypes
78
+ };