sabre-client 0.3.0-next.0 → 0.3.0-next.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/README.es.md +304 -3
- package/README.md +302 -4
- package/dist/index.d.ts +1061 -2
- package/dist/index.js +2121 -111
- package/dist/index.js.map +1 -1
- package/dist/queue-ref-BDGh8FrV.d.ts +11 -0
- package/dist/testing.d.ts +92 -0
- package/dist/testing.js +2874 -0
- package/dist/testing.js.map +1 -0
- package/package.json +8 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,14 @@ var SabreGatewayError = class extends SabreError {
|
|
|
29
29
|
this.code = options.code;
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
|
+
var SabreSessionError = class extends SabreGatewayError {
|
|
33
|
+
name = "SabreSessionError";
|
|
34
|
+
kind;
|
|
35
|
+
constructor(message, options) {
|
|
36
|
+
super(message, { ...options, protocol: "soap" });
|
|
37
|
+
this.kind = options.kind;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
32
40
|
var SabreOrchestrationError = class extends SabreError {
|
|
33
41
|
name = "SabreOrchestrationError";
|
|
34
42
|
code;
|
|
@@ -239,7 +247,8 @@ async function bargainFinderMax(transport, request, options = {}) {
|
|
|
239
247
|
body: request,
|
|
240
248
|
classify: classifyBargainFinderMax,
|
|
241
249
|
...options.retry === void 0 ? {} : { retry: options.retry },
|
|
242
|
-
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
250
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
251
|
+
...options.onRawResponse === void 0 ? {} : { operation: "air.bargainFinderMax", onRawResponse: options.onRawResponse }
|
|
243
252
|
});
|
|
244
253
|
return denormalizeGroupedItineraryResponse(body.groupedItineraryResponse, BARGAIN_FINDER_MAX_SERVICE);
|
|
245
254
|
}
|
|
@@ -247,6 +256,104 @@ function classifyBargainFinderMax(body) {
|
|
|
247
256
|
return classifyItineraryMessages(body, BARGAIN_FINDER_MAX_SERVICE, BARGAIN_FINDER_MAX_CATALOG);
|
|
248
257
|
}
|
|
249
258
|
|
|
259
|
+
// src/booking/booking-errors.ts
|
|
260
|
+
var WARNING = "WARNING";
|
|
261
|
+
function responseErrors(response) {
|
|
262
|
+
return Array.isArray(response.errors) ? response.errors.filter(isObject2) : [];
|
|
263
|
+
}
|
|
264
|
+
function isWarning(item) {
|
|
265
|
+
return typeof item.category === "string" && item.category.toUpperCase() === WARNING;
|
|
266
|
+
}
|
|
267
|
+
function toWarning(service, item) {
|
|
268
|
+
return new SabreWarning(messageOf(service, item), { service, code: codeOf(item), raw: item });
|
|
269
|
+
}
|
|
270
|
+
function toBusinessError(service, item, { warnings = [], errors = [], retryable = false } = {}) {
|
|
271
|
+
return new SabreBusinessError(messageOf(service, item), {
|
|
272
|
+
service,
|
|
273
|
+
code: codeOf(item),
|
|
274
|
+
raw: item,
|
|
275
|
+
warnings,
|
|
276
|
+
errors,
|
|
277
|
+
retryable
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function codeOf(item) {
|
|
281
|
+
return typeof item.type === "string" && item.type !== "" ? item.type : String(item.category);
|
|
282
|
+
}
|
|
283
|
+
function messageOf(service, item) {
|
|
284
|
+
const where = typeof item.fieldPath === "string" && item.fieldPath !== "" ? ` en ${item.fieldPath}` : "";
|
|
285
|
+
const description = typeof item.description === "string" && item.description !== "" ? `: ${item.description}` : "";
|
|
286
|
+
return `${service}: ${String(item.category)} ${codeOf(item)}${where}${description}`;
|
|
287
|
+
}
|
|
288
|
+
function nonEmpty(value) {
|
|
289
|
+
return typeof value === "string" && value !== "";
|
|
290
|
+
}
|
|
291
|
+
function isObject2(value) {
|
|
292
|
+
return typeof value === "object" && value !== null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/air/get-seats.ts
|
|
296
|
+
var GET_SEATS_BY_PAYLOAD_SERVICE = "getSeatsByPayload";
|
|
297
|
+
var BY_PAYLOAD_PATH = "/v3/offers/getseats/byReservationPayload";
|
|
298
|
+
var GET_SEATS_BY_PNR_LOCATOR_SERVICE = "getSeatsByPnrLocator";
|
|
299
|
+
var BY_PNR_LOCATOR_PATH = "/v3/offers/getseats/byPnrLocator";
|
|
300
|
+
var PAYLOAD_FIELDS = ["segments", "passengers", "returnSeatMapsOnlyForSegmentRefs"];
|
|
301
|
+
async function getSeats(transport, request, options = {}) {
|
|
302
|
+
if (!isObject2(request)) {
|
|
303
|
+
throw new TypeError("sabre-client: air.getSeats espera un objeto");
|
|
304
|
+
}
|
|
305
|
+
if ("pnrLocator" in request) {
|
|
306
|
+
if (!nonEmpty(request.pnrLocator)) {
|
|
307
|
+
throw new TypeError("sabre-client: air.getSeats con pnrLocator vac\xEDo");
|
|
308
|
+
}
|
|
309
|
+
const mixed = PAYLOAD_FIELDS.filter((field) => field in request);
|
|
310
|
+
if (mixed.length > 0) {
|
|
311
|
+
throw new TypeError(`sabre-client: air.getSeats con pnrLocator no acepta ${mixed.join(", ")}`);
|
|
312
|
+
}
|
|
313
|
+
return send(transport, BY_PNR_LOCATOR_PATH, GET_SEATS_BY_PNR_LOCATOR_SERVICE, request, options);
|
|
314
|
+
}
|
|
315
|
+
if (!Array.isArray(request.segments) || !Array.isArray(request.passengers)) {
|
|
316
|
+
throw new TypeError("sabre-client: air.getSeats espera { segments: [...], passengers: [...], ... } o { pnrLocator }");
|
|
317
|
+
}
|
|
318
|
+
return send(transport, BY_PAYLOAD_PATH, GET_SEATS_BY_PAYLOAD_SERVICE, request, options);
|
|
319
|
+
}
|
|
320
|
+
async function send(transport, path, service, request, options) {
|
|
321
|
+
const body = await transport.request({
|
|
322
|
+
method: "POST",
|
|
323
|
+
path,
|
|
324
|
+
service,
|
|
325
|
+
idempotent: true,
|
|
326
|
+
body: request,
|
|
327
|
+
classify: (raw) => classifyGetSeats(raw, service),
|
|
328
|
+
...options.retry === void 0 ? {} : { retry: options.retry },
|
|
329
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
330
|
+
...options.onRawResponse === void 0 ? {} : { operation: `air.${service}`, onRawResponse: options.onRawResponse }
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
response: body.response,
|
|
334
|
+
warnings: issues(body.warnings).map((item) => toWarning(service, item)),
|
|
335
|
+
errors: issues(body.errors).map((item) => toBusinessError(service, item))
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function classifyGetSeats(body, service) {
|
|
339
|
+
if (!isObject2(body)) {
|
|
340
|
+
return new SabreError(`${service}: respuesta malformada, se esperaba un objeto`, { retryable: false, raw: body });
|
|
341
|
+
}
|
|
342
|
+
const rs = body;
|
|
343
|
+
if (isObject2(rs.response)) return void 0;
|
|
344
|
+
const [first, ...rest] = issues(rs.errors);
|
|
345
|
+
if (first !== void 0) {
|
|
346
|
+
return toBusinessError(service, first, {
|
|
347
|
+
warnings: issues(rs.warnings).map((item) => toWarning(service, item)),
|
|
348
|
+
errors: rest.map((item) => toBusinessError(service, item))
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return new SabreError(`${service}: respuesta malformada, sin response ni errors`, { retryable: false, raw: body });
|
|
352
|
+
}
|
|
353
|
+
function issues(list) {
|
|
354
|
+
return Array.isArray(list) ? list.filter(isObject2) : [];
|
|
355
|
+
}
|
|
356
|
+
|
|
250
357
|
// src/air/itinerary-flights.ts
|
|
251
358
|
function itineraryFlights(group, itinerary) {
|
|
252
359
|
const { legDescriptions } = group.groupDescription;
|
|
@@ -310,7 +417,8 @@ async function revalidateItinerary(transport, request, options = {}) {
|
|
|
310
417
|
body: request,
|
|
311
418
|
classify: classifyRevalidateItinerary,
|
|
312
419
|
...options.retry === void 0 ? {} : { retry: options.retry },
|
|
313
|
-
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
420
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
421
|
+
...options.onRawResponse === void 0 ? {} : { operation: "air.revalidateItinerary", onRawResponse: options.onRawResponse }
|
|
314
422
|
});
|
|
315
423
|
return denormalizeGroupedItineraryResponse(body.groupedItineraryResponse, REVALIDATE_ITINERARY_SERVICE);
|
|
316
424
|
}
|
|
@@ -396,6 +504,15 @@ var Air = class {
|
|
|
396
504
|
revalidateItinerary(request, options) {
|
|
397
505
|
return revalidateItinerary(this.#transport, request, options);
|
|
398
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Get Seats v3: mapa de asientos con disponibilidad y precio. Sin `pnrLocator` va a
|
|
509
|
+
* `byReservationPayload`, armado con `buildSeatAvailabilityRequest`; con `pnrLocator`, a
|
|
510
|
+
* `byPnrLocator`. Un `pnrLocator` vacío o mezclado con `segments`, `passengers` o
|
|
511
|
+
* `returnSeatMapsOnlyForSegmentRefs` lanza `TypeError` sin llamar a la red.
|
|
512
|
+
*/
|
|
513
|
+
getSeats(request, options) {
|
|
514
|
+
return getSeats(this.#transport, request, options);
|
|
515
|
+
}
|
|
399
516
|
};
|
|
400
517
|
|
|
401
518
|
// src/logging/redact.ts
|
|
@@ -445,6 +562,13 @@ function redactEntries(entries, seen) {
|
|
|
445
562
|
}
|
|
446
563
|
return out;
|
|
447
564
|
}
|
|
565
|
+
var BINARY_SECURITY_TOKEN = /(<(?:[A-Za-z_][\w.-]*:)?BinarySecurityToken(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*(?<!\/)>)([\s\S]*?)(<\/(?:[A-Za-z_][\w.-]*:)?BinarySecurityToken\s*>)/g;
|
|
566
|
+
function redactSoapEnvelope(envelope) {
|
|
567
|
+
return envelope.replace(
|
|
568
|
+
BINARY_SECURITY_TOKEN,
|
|
569
|
+
(match, open, value, close) => value.trim() === "" ? match : `${open}${REDACTED}${close}`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
448
572
|
|
|
449
573
|
// src/logging/logger.ts
|
|
450
574
|
var EventLogger = class {
|
|
@@ -658,42 +782,6 @@ var AuthManager = class {
|
|
|
658
782
|
}
|
|
659
783
|
};
|
|
660
784
|
|
|
661
|
-
// src/booking/booking-errors.ts
|
|
662
|
-
var WARNING = "WARNING";
|
|
663
|
-
function responseErrors(response) {
|
|
664
|
-
return Array.isArray(response.errors) ? response.errors.filter(isObject2) : [];
|
|
665
|
-
}
|
|
666
|
-
function isWarning(item) {
|
|
667
|
-
return typeof item.category === "string" && item.category.toUpperCase() === WARNING;
|
|
668
|
-
}
|
|
669
|
-
function toWarning(service, item) {
|
|
670
|
-
return new SabreWarning(messageOf(service, item), { service, code: codeOf(item), raw: item });
|
|
671
|
-
}
|
|
672
|
-
function toBusinessError(service, item, { warnings = [], errors = [], retryable = false } = {}) {
|
|
673
|
-
return new SabreBusinessError(messageOf(service, item), {
|
|
674
|
-
service,
|
|
675
|
-
code: codeOf(item),
|
|
676
|
-
raw: item,
|
|
677
|
-
warnings,
|
|
678
|
-
errors,
|
|
679
|
-
retryable
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
function codeOf(item) {
|
|
683
|
-
return typeof item.type === "string" && item.type !== "" ? item.type : String(item.category);
|
|
684
|
-
}
|
|
685
|
-
function messageOf(service, item) {
|
|
686
|
-
const where = typeof item.fieldPath === "string" && item.fieldPath !== "" ? ` en ${item.fieldPath}` : "";
|
|
687
|
-
const description = typeof item.description === "string" && item.description !== "" ? `: ${item.description}` : "";
|
|
688
|
-
return `${service}: ${String(item.category)} ${codeOf(item)}${where}${description}`;
|
|
689
|
-
}
|
|
690
|
-
function nonEmpty(value) {
|
|
691
|
-
return typeof value === "string" && value !== "";
|
|
692
|
-
}
|
|
693
|
-
function isObject2(value) {
|
|
694
|
-
return typeof value === "object" && value !== null;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
785
|
// src/booking/cancel-booking.ts
|
|
698
786
|
var CANCEL_BOOKING_SERVICE = "cancelBooking";
|
|
699
787
|
var PATH3 = "/v1/trip/orders/cancelBooking";
|
|
@@ -708,7 +796,8 @@ async function cancelBooking(transport, request, options = {}) {
|
|
|
708
796
|
idempotent: false,
|
|
709
797
|
body: request,
|
|
710
798
|
classify: classifyCancelBooking,
|
|
711
|
-
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
799
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
800
|
+
...options.onRawResponse === void 0 ? {} : { operation: "booking.cancel", onRawResponse: options.onRawResponse }
|
|
712
801
|
});
|
|
713
802
|
return toCancelBookingResponse(body);
|
|
714
803
|
}
|
|
@@ -760,13 +849,122 @@ function errorOf(item) {
|
|
|
760
849
|
return toBusinessError(CANCEL_BOOKING_SERVICE, item);
|
|
761
850
|
}
|
|
762
851
|
|
|
852
|
+
// src/booking/flight-details.ts
|
|
853
|
+
var SEAT_NUMBER = /^[0-9]+[A-Z]$/;
|
|
854
|
+
var pricedSeats = /* @__PURE__ */ new WeakMap();
|
|
855
|
+
function pricedSeatsOf(details) {
|
|
856
|
+
return details === void 0 ? [] : pricedSeats.get(details) ?? [];
|
|
857
|
+
}
|
|
858
|
+
function buildBookingFlightDetails(source) {
|
|
859
|
+
const { group, itinerary, priceComparison, seats = [], seatAvailability } = source;
|
|
860
|
+
const flights = itineraryFlights(group, itinerary).map(
|
|
861
|
+
({ schedule, departureDate, bookingCode }) => ({
|
|
862
|
+
flightNumber: schedule.carrier.marketingFlightNumber,
|
|
863
|
+
airlineCode: schedule.carrier.marketing,
|
|
864
|
+
fromAirportCode: schedule.departure.airport,
|
|
865
|
+
toAirportCode: schedule.arrival.airport,
|
|
866
|
+
departureDate,
|
|
867
|
+
departureTime: schedule.departure.time.slice(0, 5),
|
|
868
|
+
bookingClass: bookingCode,
|
|
869
|
+
flightStatusCode: "NN"
|
|
870
|
+
})
|
|
871
|
+
);
|
|
872
|
+
const fare = itinerary.pricingInformation[0]?.fare;
|
|
873
|
+
if (fare === void 0) {
|
|
874
|
+
throw new TypeError("sabre-client: el itinerario no trae pricingInformation");
|
|
875
|
+
}
|
|
876
|
+
const passengersPricing = fare.passengerInfoList.map(
|
|
877
|
+
({ passengerInfo }) => ({
|
|
878
|
+
passengerCode: passengerInfo.passengerType,
|
|
879
|
+
numberOfpassengers: passengerInfo.passengerNumber ?? 1
|
|
880
|
+
})
|
|
881
|
+
);
|
|
882
|
+
let priceComparisons;
|
|
883
|
+
if (priceComparison !== void 0) {
|
|
884
|
+
const total = fare.totalFare?.totalPrice;
|
|
885
|
+
if (total === void 0) {
|
|
886
|
+
throw new TypeError("sabre-client: el itinerario no trae totalFare.totalPrice para priceComparison");
|
|
887
|
+
}
|
|
888
|
+
priceComparisons = [{ desiredAmount: amount(total), ...priceComparison }];
|
|
889
|
+
}
|
|
890
|
+
const travelerCount = passengersPricing.reduce((n, p) => n + (p.numberOfpassengers ?? 1), 0);
|
|
891
|
+
const priced = [];
|
|
892
|
+
for (const selection of seats) {
|
|
893
|
+
const flight2 = flights[validSeat(selection, flights.length, travelerCount)];
|
|
894
|
+
(flight2.seats ??= []).push({ number: selection.number, travelerIndex: selection.travelerIndex });
|
|
895
|
+
const price = seatAvailability === void 0 ? void 0 : seatPrice(seatAvailability, flight2, selection.number);
|
|
896
|
+
if (price !== void 0) {
|
|
897
|
+
priced.push({
|
|
898
|
+
segmentIndex: selection.segmentIndex,
|
|
899
|
+
flight: `${flight2.airlineCode}${flight2.flightNumber}`,
|
|
900
|
+
seat: selection.number,
|
|
901
|
+
...price
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const details = {
|
|
906
|
+
flights,
|
|
907
|
+
flightPricing: [
|
|
908
|
+
{
|
|
909
|
+
...priceComparisons === void 0 ? {} : { priceComparisons },
|
|
910
|
+
qualifiers: {
|
|
911
|
+
passengersPricing,
|
|
912
|
+
...fare.validatingCarrierCode === void 0 ? {} : { validatingAirlineCode: fare.validatingCarrierCode }
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
]
|
|
916
|
+
};
|
|
917
|
+
if (priced.length > 0) pricedSeats.set(details, priced);
|
|
918
|
+
return details;
|
|
919
|
+
}
|
|
920
|
+
function validSeat(selection, flightCount, travelerCount) {
|
|
921
|
+
const { segmentIndex, travelerIndex, number } = selection;
|
|
922
|
+
if (typeof number !== "string" || !SEAT_NUMBER.test(number)) {
|
|
923
|
+
throw new TypeError(`sabre-client: el asiento ${String(number)} no cumple ^[0-9]+[A-Z]$ (p. ej. 13A)`);
|
|
924
|
+
}
|
|
925
|
+
if (!Number.isInteger(segmentIndex) || segmentIndex < 1 || segmentIndex > flightCount) {
|
|
926
|
+
throw new TypeError(
|
|
927
|
+
`sabre-client: el asiento ${number} pide el vuelo ${segmentIndex}, y hay ${flightCount} (empieza en 1)`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
if (!Number.isInteger(travelerIndex) || travelerIndex < 1 || travelerIndex > travelerCount) {
|
|
931
|
+
throw new TypeError(
|
|
932
|
+
`sabre-client: el asiento ${number} pide el viajero ${travelerIndex}, y hay ${travelerCount} (empieza en 1)`
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
return segmentIndex - 1;
|
|
936
|
+
}
|
|
937
|
+
function seatPrice(availability, flight2, number) {
|
|
938
|
+
const { response } = availability;
|
|
939
|
+
const segmentIds = new Set(
|
|
940
|
+
response.segments.filter(
|
|
941
|
+
(s) => s.bookingAirlineCode === flight2.airlineCode && s.bookingFlightNumber === flight2.flightNumber && s.departureAirportCode === flight2.fromAirportCode && s.departureDate === flight2.departureDate
|
|
942
|
+
).map((s) => s.id)
|
|
943
|
+
);
|
|
944
|
+
const row = Number(number.slice(0, -1));
|
|
945
|
+
const column = number.slice(-1);
|
|
946
|
+
const seat = response.seatMaps.filter((map) => segmentIds.has(map.segmentRef)).flatMap((map) => map.cabinCompartments ?? []).flatMap((cabin) => cabin.seatRows ?? []).filter((r) => r.row === row).flatMap((r) => r.seats).find((s) => s.column === column);
|
|
947
|
+
for (const itemId of seat?.offerItemRefIds ?? []) {
|
|
948
|
+
const item = response.offerItems.find((i) => i.id === itemId);
|
|
949
|
+
const price = response.priceDefinitions?.find((p) => p.id === item?.priceDefinitionRef);
|
|
950
|
+
if (price !== void 0 && Number(price.totalPrice.amount) > 0) {
|
|
951
|
+
return { amount: String(price.totalPrice.amount), currencyCode: price.currencyCode };
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return void 0;
|
|
955
|
+
}
|
|
956
|
+
function amount(value) {
|
|
957
|
+
return String(Math.round(value * 1e3) / 1e3);
|
|
958
|
+
}
|
|
959
|
+
|
|
763
960
|
// src/booking/create-booking.ts
|
|
764
961
|
var CREATE_BOOKING_SERVICE = "createBooking";
|
|
765
962
|
var PATH4 = "/v1/trip/orders/createBooking";
|
|
766
963
|
var UNABLE_TO_RETRIEVE_BOOKING = "UNABLE_TO_RETRIEVE_BOOKING";
|
|
767
964
|
async function createBooking(transport, request, options = {}) {
|
|
768
|
-
|
|
769
|
-
|
|
965
|
+
validateCreateBookingRequest(request);
|
|
966
|
+
for (const seat of pricedSeatsOf(request.flightDetails)) {
|
|
967
|
+
transport.log.emit("warn", "sabre.booking.seat.priced", { service: CREATE_BOOKING_SERVICE, ...seat });
|
|
770
968
|
}
|
|
771
969
|
const body = await transport.request({
|
|
772
970
|
method: "POST",
|
|
@@ -775,10 +973,32 @@ async function createBooking(transport, request, options = {}) {
|
|
|
775
973
|
idempotent: false,
|
|
776
974
|
body: request,
|
|
777
975
|
classify: classifyCreateBooking,
|
|
778
|
-
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
976
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
977
|
+
...options.onRawResponse === void 0 ? {} : { operation: "booking.create", onRawResponse: options.onRawResponse }
|
|
779
978
|
});
|
|
780
979
|
return toCreateBookingResponse(body);
|
|
781
980
|
}
|
|
981
|
+
function validateCreateBookingRequest(request) {
|
|
982
|
+
if (typeof request !== "object" || request === null) {
|
|
983
|
+
throw new TypeError("sabre-client: booking.create espera un objeto CreateBookingRequest");
|
|
984
|
+
}
|
|
985
|
+
const agency = request.agency;
|
|
986
|
+
if (agency?.ticketingPolicy === "FUTURE_TICKETING" && agency.futureTicketingPolicy === void 0) {
|
|
987
|
+
throw new TypeError(
|
|
988
|
+
"sabre-client: booking.create con agency.ticketingPolicy FUTURE_TICKETING necesita agency.futureTicketingPolicy"
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
const travelers = Array.isArray(request.travelers) ? request.travelers.length : 0;
|
|
992
|
+
for (const flight2 of request.flightDetails?.flights ?? []) {
|
|
993
|
+
for (const seat of flight2.seats ?? []) {
|
|
994
|
+
if (seat.travelerIndex < 1 || seat.travelerIndex > travelers) {
|
|
995
|
+
throw new TypeError(
|
|
996
|
+
`sabre-client: booking.create con un asiento para el viajero ${seat.travelerIndex}, y travelers tiene ${travelers}`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
782
1002
|
function classifyCreateBooking(body) {
|
|
783
1003
|
if (!isObject2(body)) {
|
|
784
1004
|
return new SabreError(`${CREATE_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
|
|
@@ -845,7 +1065,8 @@ async function getBooking(transport, request, options = {}) {
|
|
|
845
1065
|
idempotent: true,
|
|
846
1066
|
body: request,
|
|
847
1067
|
classify: classifyGetBooking,
|
|
848
|
-
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
1068
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
1069
|
+
...options.onRawResponse === void 0 ? {} : { operation: "booking.get", onRawResponse: options.onRawResponse }
|
|
849
1070
|
});
|
|
850
1071
|
return toGetBookingResponse(body);
|
|
851
1072
|
}
|
|
@@ -895,6 +1116,10 @@ var BookingManagement = class {
|
|
|
895
1116
|
*
|
|
896
1117
|
* Nunca se reintenta automáticamente. Si la reserva se crea con avisos o errores no fatales,
|
|
897
1118
|
* no lanza: los devuelve en `warnings` y `errors`. Si no se crea, lanza `SabreBusinessError`.
|
|
1119
|
+
* Con `agency.ticketingPolicy: 'FUTURE_TICKETING'` y sin `agency.futureTicketingPolicy`, lanza
|
|
1120
|
+
* `TypeError` sin llamar a la red: Sabre lo rechaza seguro. Lo mismo con un asiento cuyo
|
|
1121
|
+
* `travelerIndex` no está en `travelers`. Un asiento con precio en Get Seats (ver
|
|
1122
|
+
* `buildBookingFlightDetails`) solo emite `sabre.booking.seat.priced` en `warn`.
|
|
898
1123
|
*/
|
|
899
1124
|
create(request, options) {
|
|
900
1125
|
return createBooking(this.#transport, request, options);
|
|
@@ -942,12 +1167,30 @@ function validateRetryOptions(retry, where) {
|
|
|
942
1167
|
throw new TypeError(`sabre-client: ${where}.baseDelayMs debe ser un n\xFAmero >= 0`);
|
|
943
1168
|
}
|
|
944
1169
|
}
|
|
1170
|
+
function sleep(ms, signal) {
|
|
1171
|
+
return new Promise((resolve, reject) => {
|
|
1172
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
1173
|
+
const timer = setTimeout(() => {
|
|
1174
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1175
|
+
resolve();
|
|
1176
|
+
}, ms);
|
|
1177
|
+
const onAbort = () => {
|
|
1178
|
+
clearTimeout(timer);
|
|
1179
|
+
reject(signal?.reason);
|
|
1180
|
+
};
|
|
1181
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
945
1184
|
|
|
946
1185
|
// src/config.ts
|
|
947
1186
|
var BASE_URLS = Object.freeze({
|
|
948
1187
|
sandbox: "https://api.cert.platform.sabre.com",
|
|
949
1188
|
production: "https://api.platform.sabre.com"
|
|
950
1189
|
});
|
|
1190
|
+
var SOAP_BASE_URLS = Object.freeze({
|
|
1191
|
+
sandbox: "https://webservices.cert.platform.sabre.com",
|
|
1192
|
+
production: "https://webservices.platform.sabre.com"
|
|
1193
|
+
});
|
|
951
1194
|
var LOGGER_METHODS = ["debug", "info", "warn", "error"];
|
|
952
1195
|
function fail(message) {
|
|
953
1196
|
throw new TypeError(`sabre-client: ${message}`);
|
|
@@ -1000,6 +1243,7 @@ function resolveConfig(config) {
|
|
|
1000
1243
|
credentials: Object.freeze({ ...credentials }),
|
|
1001
1244
|
environment,
|
|
1002
1245
|
baseUrl: BASE_URLS[environment],
|
|
1246
|
+
soapBaseUrl: SOAP_BASE_URLS[environment],
|
|
1003
1247
|
authVersion,
|
|
1004
1248
|
pcc: config.pcc,
|
|
1005
1249
|
logger,
|
|
@@ -1028,18 +1272,30 @@ var RestTransport = class {
|
|
|
1028
1272
|
this.#random = options.random ?? Math.random;
|
|
1029
1273
|
this.#sleep = options.sleep ?? sleep;
|
|
1030
1274
|
}
|
|
1275
|
+
/** @internal Para los avisos de las operaciones que no son de una request (p. ej. asientos con precio). */
|
|
1276
|
+
get log() {
|
|
1277
|
+
return this.#log;
|
|
1278
|
+
}
|
|
1031
1279
|
/** Punto de enganche para `AuthManager`. */
|
|
1032
1280
|
setAuthorizationProvider(provider) {
|
|
1033
1281
|
this.#authorization = provider;
|
|
1034
1282
|
}
|
|
1035
1283
|
async request(request) {
|
|
1036
1284
|
if (request.retry !== void 0) validateRetryOptions(request.retry, "retry");
|
|
1285
|
+
const capture = request.onRawResponse === void 0 ? void 0 : { attempts: 0 };
|
|
1286
|
+
try {
|
|
1287
|
+
return await this.#attempts(request, capture);
|
|
1288
|
+
} finally {
|
|
1289
|
+
if (capture?.last !== void 0) this.#deliverRaw(request, { ...capture.last, attempts: capture.attempts });
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
async #attempts(request, capture) {
|
|
1037
1293
|
const retry = { ...this.#config.retry, ...definedOnly(request.retry ?? {}) };
|
|
1038
1294
|
let reauthorized = false;
|
|
1039
1295
|
for (let attempt = 0; ; attempt++) {
|
|
1040
1296
|
const authorization = await this.#authorizationFor(request);
|
|
1041
1297
|
try {
|
|
1042
|
-
return await this.#send(request, attempt, authorization);
|
|
1298
|
+
return await this.#send(request, attempt, authorization, capture);
|
|
1043
1299
|
} catch (error) {
|
|
1044
1300
|
if (authorization !== void 0 && !reauthorized && isUnauthorized(error)) {
|
|
1045
1301
|
reauthorized = true;
|
|
@@ -1070,11 +1326,24 @@ var RestTransport = class {
|
|
|
1070
1326
|
}
|
|
1071
1327
|
}
|
|
1072
1328
|
}
|
|
1329
|
+
/** Un callback que lanza no rompe la operación: Create Booking no puede perder su localizador. */
|
|
1330
|
+
#deliverRaw(request, raw) {
|
|
1331
|
+
const operation = request.operation ?? request.service;
|
|
1332
|
+
try {
|
|
1333
|
+
request.onRawResponse?.({ operation, protocol: "rest", ...raw });
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
this.#log.emit("warn", "sabre.request.rawResponseHookFailed", {
|
|
1336
|
+
service: request.service,
|
|
1337
|
+
operation,
|
|
1338
|
+
errorName: error instanceof Error ? error.name : typeof error
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1073
1342
|
async #authorizationFor(request) {
|
|
1074
1343
|
if (request.auth === false || this.#authorization === void 0) return void 0;
|
|
1075
1344
|
return this.#authorization.get();
|
|
1076
1345
|
}
|
|
1077
|
-
async #send(request, attempt, authorization) {
|
|
1346
|
+
async #send(request, attempt, authorization, capture) {
|
|
1078
1347
|
const url = this.#url(request);
|
|
1079
1348
|
const headers = new Headers({ Accept: "application/json", ...request.headers });
|
|
1080
1349
|
const form = request.body instanceof URLSearchParams;
|
|
@@ -1086,6 +1355,7 @@ var RestTransport = class {
|
|
|
1086
1355
|
this.#log.emit("debug", "sabre.request.start", logContext);
|
|
1087
1356
|
const startedAt = Date.now();
|
|
1088
1357
|
let response;
|
|
1358
|
+
if (capture !== void 0) capture.attempts++;
|
|
1089
1359
|
try {
|
|
1090
1360
|
response = await this.#config.fetch(url, {
|
|
1091
1361
|
method: request.method,
|
|
@@ -1103,7 +1373,9 @@ var RestTransport = class {
|
|
|
1103
1373
|
this.#log.emit("error", "sabre.request.end", { ...logContext, durationMs: Date.now() - startedAt, error });
|
|
1104
1374
|
throw error;
|
|
1105
1375
|
}
|
|
1106
|
-
const
|
|
1376
|
+
const text = await response.text();
|
|
1377
|
+
if (capture !== void 0) capture.last = { status: response.status, headers: response.headers, body: text };
|
|
1378
|
+
const body = parseBody(text);
|
|
1107
1379
|
const endContext = { ...logContext, status: response.status, durationMs: Date.now() - startedAt };
|
|
1108
1380
|
if (!response.ok) {
|
|
1109
1381
|
const error = gatewayError(request.service, response.status, body);
|
|
@@ -1127,8 +1399,7 @@ var RestTransport = class {
|
|
|
1127
1399
|
return url.toString();
|
|
1128
1400
|
}
|
|
1129
1401
|
};
|
|
1130
|
-
|
|
1131
|
-
const text = await response.text();
|
|
1402
|
+
function parseBody(text) {
|
|
1132
1403
|
if (text === "") return void 0;
|
|
1133
1404
|
try {
|
|
1134
1405
|
return JSON.parse(text);
|
|
@@ -1138,13 +1409,17 @@ async function readBody(response) {
|
|
|
1138
1409
|
}
|
|
1139
1410
|
function gatewayError(service, status, body) {
|
|
1140
1411
|
const code = extractCode(body);
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
status
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1412
|
+
const detail = extractDescription(body);
|
|
1413
|
+
return new SabreGatewayError(
|
|
1414
|
+
`${service}: HTTP ${status}${code === void 0 ? "" : ` ${code}`}${detail === void 0 ? "" : `: ${detail}`}`,
|
|
1415
|
+
{
|
|
1416
|
+
protocol: "rest",
|
|
1417
|
+
status,
|
|
1418
|
+
retryable: RETRYABLE_STATUS.has(status),
|
|
1419
|
+
raw: body,
|
|
1420
|
+
...code === void 0 ? {} : { code }
|
|
1421
|
+
}
|
|
1422
|
+
);
|
|
1148
1423
|
}
|
|
1149
1424
|
function extractCode(body) {
|
|
1150
1425
|
if (typeof body !== "object" || body === null) return void 0;
|
|
@@ -1152,7 +1427,18 @@ function extractCode(body) {
|
|
|
1152
1427
|
const value = body[key];
|
|
1153
1428
|
if (typeof value === "string" && value !== "") return value;
|
|
1154
1429
|
}
|
|
1155
|
-
return
|
|
1430
|
+
return firstErrorField(body, "code") ?? firstErrorField(body, "type");
|
|
1431
|
+
}
|
|
1432
|
+
function extractDescription(body) {
|
|
1433
|
+
return firstErrorField(body, "descriptionText") ?? firstErrorField(body, "description");
|
|
1434
|
+
}
|
|
1435
|
+
function firstErrorField(body, key) {
|
|
1436
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
1437
|
+
const errors = body.errors;
|
|
1438
|
+
const first = Array.isArray(errors) ? errors[0] : void 0;
|
|
1439
|
+
if (typeof first !== "object" || first === null) return void 0;
|
|
1440
|
+
const value = first[key];
|
|
1441
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1156
1442
|
}
|
|
1157
1443
|
function isUnauthorized(error) {
|
|
1158
1444
|
return error instanceof SabreGatewayError && error.protocol === "rest" && error.status === 401;
|
|
@@ -1163,20 +1449,6 @@ function isAbort(error, signal) {
|
|
|
1163
1449
|
function describe(error) {
|
|
1164
1450
|
return error instanceof Error ? error.message : String(error);
|
|
1165
1451
|
}
|
|
1166
|
-
function sleep(ms, signal) {
|
|
1167
|
-
return new Promise((resolve, reject) => {
|
|
1168
|
-
if (signal?.aborted) return reject(signal.reason);
|
|
1169
|
-
const timer = setTimeout(() => {
|
|
1170
|
-
signal?.removeEventListener("abort", onAbort);
|
|
1171
|
-
resolve();
|
|
1172
|
-
}, ms);
|
|
1173
|
-
const onAbort = () => {
|
|
1174
|
-
clearTimeout(timer);
|
|
1175
|
-
reject(signal?.reason);
|
|
1176
|
-
};
|
|
1177
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
1452
|
|
|
1181
1453
|
// src/sabre.ts
|
|
1182
1454
|
var Sabre = class {
|
|
@@ -1202,54 +1474,1787 @@ var Sabre = class {
|
|
|
1202
1474
|
}
|
|
1203
1475
|
};
|
|
1204
1476
|
|
|
1205
|
-
// src/
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1477
|
+
// src/http/soap-transport.ts
|
|
1478
|
+
import { XMLParser } from "fast-xml-parser";
|
|
1479
|
+
var TRANSIENT_STATUS = /* @__PURE__ */ new Set([429, 500, 503, 504]);
|
|
1480
|
+
var BUSY_CODES = /* @__PURE__ */ new Set(["USG_CONNECTOR_IS_BUSY", "USG_IS_BUSY", "USG_SERVICE_IS_BUSY"]);
|
|
1481
|
+
var TRANSIENT_CODES = /* @__PURE__ */ new Set([
|
|
1482
|
+
"USG_SECURITY_ICE_ERROR",
|
|
1483
|
+
"USG_SERVICE_PROVIDER_ERROR",
|
|
1484
|
+
"USG_NO_RESPONSE_FROM_JMSRECEIVER_IN_TIME",
|
|
1485
|
+
"SC_SERVICE_UNAVAILABLE",
|
|
1486
|
+
"USG_COULD_NOT_COMPLETE_REQUEST",
|
|
1487
|
+
"USG_IIOP_OBJECT_NOT_EXIST",
|
|
1488
|
+
"USG_IIOP_TRANSIENT_EXCEPTION",
|
|
1489
|
+
"USG_MOM_SERVICE_PROVIDER_INFRASTRUCTURE_ERROR"
|
|
1490
|
+
]);
|
|
1491
|
+
var PREVIOUS_ENTRY_ACTIVE_RETRIES = 3;
|
|
1492
|
+
var PREVIOUS_ENTRY_ACTIVE_DELAY_MS = 1e3;
|
|
1493
|
+
var PREVIOUS_ENTRY_ACTIVE = /PREVIOUS ENTRY ACTIVE/i;
|
|
1494
|
+
var parser = new XMLParser({
|
|
1495
|
+
ignoreAttributes: false,
|
|
1496
|
+
attributeNamePrefix: "@",
|
|
1497
|
+
removeNSPrefix: true,
|
|
1498
|
+
parseTagValue: false,
|
|
1499
|
+
parseAttributeValue: false,
|
|
1500
|
+
processEntities: true
|
|
1501
|
+
});
|
|
1502
|
+
var SoapTransport = class {
|
|
1503
|
+
#config;
|
|
1504
|
+
#log;
|
|
1505
|
+
#messageId;
|
|
1506
|
+
#now;
|
|
1507
|
+
#random;
|
|
1508
|
+
#sleep;
|
|
1509
|
+
constructor(config, options = {}) {
|
|
1510
|
+
this.#config = config;
|
|
1511
|
+
this.#log = new EventLogger(config.logger);
|
|
1512
|
+
this.#messageId = options.messageId ?? (() => globalThis.crypto.randomUUID());
|
|
1513
|
+
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1514
|
+
this.#random = options.random ?? Math.random;
|
|
1515
|
+
this.#sleep = options.sleep ?? sleep;
|
|
1223
1516
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1517
|
+
/** @internal Para los eventos de sesión que no son una request. */
|
|
1518
|
+
get log() {
|
|
1519
|
+
return this.#log;
|
|
1520
|
+
}
|
|
1521
|
+
async request(request) {
|
|
1522
|
+
if (request.retry !== void 0) validateRetryOptions(request.retry, "retry");
|
|
1523
|
+
const capture = { attempts: 0 };
|
|
1524
|
+
try {
|
|
1525
|
+
return await this.#attempts(request, capture);
|
|
1526
|
+
} finally {
|
|
1527
|
+
if (capture.last !== void 0) this.#deliverRaw(request, { ...capture.last, attempts: capture.attempts });
|
|
1235
1528
|
}
|
|
1236
|
-
priceComparisons = [{ desiredAmount: amount(total), ...priceComparison }];
|
|
1237
1529
|
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Dos presupuestos separados: el backoff genérico (`retry.maxRetries`, para `USG_*_BUSY` y
|
|
1532
|
+
* los transitorios) y el esquema fijo de `PREVIOUS ENTRY ACTIVE`, que no se ajusta a la config.
|
|
1533
|
+
*/
|
|
1534
|
+
async #attempts(request, capture) {
|
|
1535
|
+
const retry = { ...this.#config.retry, ...definedOnly(request.retry ?? {}) };
|
|
1536
|
+
let generic = 0;
|
|
1537
|
+
let previousEntryActive = 0;
|
|
1538
|
+
for (let attempt = 0; ; attempt++) {
|
|
1539
|
+
try {
|
|
1540
|
+
return await this.#send(request, attempt, capture);
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
const reason = retryReason(error, request.idempotent);
|
|
1543
|
+
let delayMs;
|
|
1544
|
+
if (reason === "previous-entry-active" && previousEntryActive < PREVIOUS_ENTRY_ACTIVE_RETRIES) {
|
|
1545
|
+
previousEntryActive++;
|
|
1546
|
+
delayMs = PREVIOUS_ENTRY_ACTIVE_DELAY_MS;
|
|
1547
|
+
} else if ((reason === "busy" || reason === "transient") && generic < retry.maxRetries) {
|
|
1548
|
+
delayMs = retryDelay(generic, retry.baseDelayMs, this.#random);
|
|
1549
|
+
generic++;
|
|
1550
|
+
} else {
|
|
1551
|
+
throw error;
|
|
1552
|
+
}
|
|
1553
|
+
this.#log.emit("warn", "sabre.request.retry", {
|
|
1554
|
+
protocol: "soap",
|
|
1555
|
+
service: request.service,
|
|
1556
|
+
action: request.action,
|
|
1557
|
+
attempt: attempt + 1,
|
|
1558
|
+
reason,
|
|
1559
|
+
delayMs,
|
|
1560
|
+
error
|
|
1561
|
+
});
|
|
1562
|
+
await this.#sleep(delayMs, request.signal);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
async #send(request, attempt, capture) {
|
|
1567
|
+
const envelope = this.#envelope(request);
|
|
1568
|
+
const logContext = { protocol: "soap", service: request.service, action: request.action, attempt };
|
|
1569
|
+
this.#log.emit("debug", "sabre.request.start", logContext);
|
|
1570
|
+
const startedAt = Date.now();
|
|
1571
|
+
let response;
|
|
1572
|
+
capture.attempts++;
|
|
1573
|
+
try {
|
|
1574
|
+
response = await this.#config.fetch(this.#config.soapBaseUrl, {
|
|
1575
|
+
method: "POST",
|
|
1576
|
+
headers: { "Content-Type": "text/xml; charset=utf-8", Accept: "text/xml" },
|
|
1577
|
+
body: envelope,
|
|
1578
|
+
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
1579
|
+
});
|
|
1580
|
+
} catch (cause) {
|
|
1581
|
+
if (isAbort2(cause, request.signal)) throw cause;
|
|
1582
|
+
const error = new SabreGatewayError(`${request.service}: fallo de red (${describe2(cause)})`, {
|
|
1583
|
+
protocol: "soap",
|
|
1584
|
+
retryable: false,
|
|
1585
|
+
cause
|
|
1586
|
+
});
|
|
1587
|
+
this.#log.emit("error", "sabre.request.end", { ...logContext, durationMs: Date.now() - startedAt, error });
|
|
1588
|
+
throw error;
|
|
1589
|
+
}
|
|
1590
|
+
const original = await response.text();
|
|
1591
|
+
const text = redactSoapEnvelope(scrubToken(original, request.security));
|
|
1592
|
+
capture.last = { status: response.status, headers: response.headers, body: text };
|
|
1593
|
+
const document = parseEnvelope(text);
|
|
1594
|
+
const messageId = stringAt(document?.Header, "MessageHeader", "MessageData", "MessageId");
|
|
1595
|
+
const endContext = {
|
|
1596
|
+
...logContext,
|
|
1597
|
+
status: response.status,
|
|
1598
|
+
durationMs: Date.now() - startedAt,
|
|
1599
|
+
...messageId === void 0 ? {} : { messageId }
|
|
1600
|
+
};
|
|
1601
|
+
try {
|
|
1602
|
+
const result = this.#classify(request, response, document, text);
|
|
1603
|
+
this.#log.emit("info", "sabre.request.end", { ...endContext, warnings: result.warnings.length });
|
|
1604
|
+
return {
|
|
1605
|
+
...result,
|
|
1606
|
+
messageId,
|
|
1607
|
+
raw: text,
|
|
1608
|
+
securityToken: request.readSecurityToken === true ? readSecurityToken(original) : void 0,
|
|
1609
|
+
status: response.status
|
|
1610
|
+
};
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
const level = error instanceof SabreBusinessError ? "warn" : "error";
|
|
1613
|
+
this.#log.emit(level, "sabre.request.end", { ...endContext, error });
|
|
1614
|
+
throw error;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
/** Orden de detección: fault (gateway), luego `stl:Error` (negocio), luego data. */
|
|
1618
|
+
#classify(request, response, document, text) {
|
|
1619
|
+
const { service } = request;
|
|
1620
|
+
if (document === void 0) {
|
|
1621
|
+
throw new SabreGatewayError(`${service}: HTTP ${response.status}, la respuesta no es un sobre SOAP`, {
|
|
1622
|
+
protocol: "soap",
|
|
1623
|
+
status: response.status,
|
|
1624
|
+
retryable: false,
|
|
1625
|
+
raw: text
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
const fault = asNode(document.Body.Fault);
|
|
1629
|
+
if (fault !== void 0) throw faultError(service, response.status, fault, text);
|
|
1630
|
+
const [payloadName, payloadValue] = firstElement(document.Body);
|
|
1631
|
+
const payload = asNode(payloadValue);
|
|
1632
|
+
if (payloadName === void 0 || payload === void 0) {
|
|
1633
|
+
throw new SabreGatewayError(`${service}: HTTP ${response.status}, el sobre SOAP no trae payload`, {
|
|
1634
|
+
protocol: "soap",
|
|
1635
|
+
status: response.status,
|
|
1636
|
+
retryable: false,
|
|
1637
|
+
raw: text
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
if (!response.ok) {
|
|
1641
|
+
throw new SabreGatewayError(`${service}: HTTP ${response.status}`, {
|
|
1642
|
+
protocol: "soap",
|
|
1643
|
+
status: response.status,
|
|
1644
|
+
retryable: false,
|
|
1645
|
+
raw: text
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
const results = asNode(payload.ApplicationResults);
|
|
1649
|
+
const warnings = asArray(results?.Warning).map((node2) => {
|
|
1650
|
+
const entry = resultEntry(node2);
|
|
1651
|
+
return new SabreWarning(`${service}: ${entry.description}`, { service, code: entry.code, raw: text });
|
|
1652
|
+
});
|
|
1653
|
+
const errors = asArray(results?.Error).map((node2) => {
|
|
1654
|
+
const entry = resultEntry(node2);
|
|
1655
|
+
return new SabreBusinessError(`${service}: ${entry.description}`, {
|
|
1656
|
+
service,
|
|
1657
|
+
code: entry.code,
|
|
1658
|
+
raw: text,
|
|
1659
|
+
retryable: PREVIOUS_ENTRY_ACTIVE.test(entry.description)
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1662
|
+
const [first, ...rest] = errors;
|
|
1663
|
+
if (first !== void 0) {
|
|
1664
|
+
throw new SabreBusinessError(first.message, {
|
|
1665
|
+
service,
|
|
1666
|
+
code: first.code,
|
|
1667
|
+
raw: text,
|
|
1668
|
+
retryable: first.retryable,
|
|
1669
|
+
warnings,
|
|
1670
|
+
errors: rest
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
return { payload, payloadName, warnings };
|
|
1674
|
+
}
|
|
1675
|
+
#envelope(request) {
|
|
1676
|
+
const timestamp = this.#now().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1677
|
+
return [
|
|
1678
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1679
|
+
'<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:eb="http://www.ebxml.org/namespaces/messageHeader">',
|
|
1680
|
+
"<soap-env:Header>",
|
|
1681
|
+
'<eb:MessageHeader soap-env:mustUnderstand="1" eb:version="1.0">',
|
|
1682
|
+
"<eb:From><eb:PartyId/></eb:From>",
|
|
1683
|
+
"<eb:To><eb:PartyId/></eb:To>",
|
|
1684
|
+
`<eb:CPAId>${xmlEscape(request.cpaId)}</eb:CPAId>`,
|
|
1685
|
+
`<eb:ConversationId>${xmlEscape(request.conversationId)}</eb:ConversationId>`,
|
|
1686
|
+
`<eb:Service>${xmlEscape(request.action)}</eb:Service>`,
|
|
1687
|
+
`<eb:Action>${xmlEscape(request.action)}</eb:Action>`,
|
|
1688
|
+
"<eb:MessageData>",
|
|
1689
|
+
`<eb:MessageId>mid:${xmlEscape(this.#messageId())}</eb:MessageId>`,
|
|
1690
|
+
`<eb:Timestamp>${timestamp}</eb:Timestamp>`,
|
|
1691
|
+
"</eb:MessageData>",
|
|
1692
|
+
"</eb:MessageHeader>",
|
|
1693
|
+
securityHeader(request.security),
|
|
1694
|
+
"</soap-env:Header>",
|
|
1695
|
+
`<soap-env:Body>${request.payload}</soap-env:Body>`,
|
|
1696
|
+
"</soap-env:Envelope>"
|
|
1697
|
+
].join("");
|
|
1698
|
+
}
|
|
1699
|
+
/** Un callback que lanza no rompe la operación (mismo criterio que REST). */
|
|
1700
|
+
#deliverRaw(request, raw) {
|
|
1701
|
+
if (request.onRawResponse === void 0) return;
|
|
1702
|
+
const operation = request.operation ?? request.service;
|
|
1703
|
+
try {
|
|
1704
|
+
request.onRawResponse({ operation, protocol: "soap", ...raw });
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
this.#log.emit("warn", "sabre.request.rawResponseHookFailed", {
|
|
1707
|
+
service: request.service,
|
|
1708
|
+
operation,
|
|
1709
|
+
errorName: error instanceof Error ? error.name : typeof error
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
function xmlEscape(value) {
|
|
1715
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1716
|
+
}
|
|
1717
|
+
function securityHeader(security) {
|
|
1718
|
+
const open = '<wsse:Security xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/12/secext">';
|
|
1719
|
+
if ("binarySecurityToken" in security) {
|
|
1720
|
+
return `${open}<wsse:BinarySecurityToken valueType="String" EncodingType="wsse:Base64Binary">${xmlEscape(security.binarySecurityToken)}</wsse:BinarySecurityToken></wsse:Security>`;
|
|
1721
|
+
}
|
|
1722
|
+
const token = security.usernameToken;
|
|
1723
|
+
return [
|
|
1724
|
+
open,
|
|
1725
|
+
"<wsse:UsernameToken>",
|
|
1726
|
+
`<wsse:Username>${xmlEscape(token.username)}</wsse:Username>`,
|
|
1727
|
+
`<wsse:Password>${xmlEscape(token.password)}</wsse:Password>`,
|
|
1728
|
+
`<Organization>${xmlEscape(token.organization)}</Organization>`,
|
|
1729
|
+
`<Domain>${xmlEscape(token.domain)}</Domain>`,
|
|
1730
|
+
token.clientId === void 0 ? "" : `<ClientId>${xmlEscape(token.clientId)}</ClientId>`,
|
|
1731
|
+
token.clientSecret === void 0 ? "" : `<ClientSecret>${xmlEscape(token.clientSecret)}</ClientSecret>`,
|
|
1732
|
+
"</wsse:UsernameToken>",
|
|
1733
|
+
"</wsse:Security>"
|
|
1734
|
+
].join("");
|
|
1735
|
+
}
|
|
1736
|
+
function scrubToken(text, security) {
|
|
1737
|
+
if (!("binarySecurityToken" in security) || security.binarySecurityToken === "") return text;
|
|
1738
|
+
const token = security.binarySecurityToken;
|
|
1739
|
+
return text.split(token).join(REDACTED).split(xmlEscape(token)).join(REDACTED);
|
|
1740
|
+
}
|
|
1741
|
+
function parseEnvelope(text) {
|
|
1742
|
+
if (text.trim() === "") return void 0;
|
|
1743
|
+
let parsed;
|
|
1744
|
+
try {
|
|
1745
|
+
parsed = parser.parse(text);
|
|
1746
|
+
} catch {
|
|
1747
|
+
return void 0;
|
|
1748
|
+
}
|
|
1749
|
+
const envelope = asNode(asNode(parsed)?.Envelope);
|
|
1750
|
+
const body = asNode(envelope?.Body);
|
|
1751
|
+
if (envelope === void 0 || body === void 0) return void 0;
|
|
1752
|
+
return { Header: envelope.Header, Body: body };
|
|
1753
|
+
}
|
|
1754
|
+
function readSecurityToken(original) {
|
|
1755
|
+
const document = parseEnvelope(original);
|
|
1756
|
+
const token = stringAt(document?.Header, "Security", "BinarySecurityToken");
|
|
1757
|
+
return token === void 0 || token.trim() === "" ? void 0 : token.trim();
|
|
1758
|
+
}
|
|
1759
|
+
var USG_CODE = /\bUSG_[A-Z0-9_]+\b/;
|
|
1760
|
+
function faultError(service, status, fault, text) {
|
|
1761
|
+
const faultcode = textOf(fault.faultcode);
|
|
1762
|
+
const faultstring = textOf(fault.faultstring);
|
|
1763
|
+
const haystack = [faultcode, faultstring, JSON.stringify(fault.detail ?? "")].join(" ");
|
|
1764
|
+
const code = USG_CODE.exec(haystack)?.[0] ?? faultcode;
|
|
1765
|
+
const detail = [faultcode, faultstring].filter((part) => part !== void 0 && part !== "").join(": ");
|
|
1766
|
+
const retryable = code !== void 0 && BUSY_CODES.has(code) || PREVIOUS_ENTRY_ACTIVE.test(haystack);
|
|
1767
|
+
return new SabreGatewayError(`${service}: SOAP fault${detail === "" ? "" : ` ${detail}`}`, {
|
|
1768
|
+
protocol: "soap",
|
|
1769
|
+
status,
|
|
1770
|
+
retryable,
|
|
1771
|
+
raw: text,
|
|
1772
|
+
...code === void 0 || code === "" ? {} : { code }
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
function resultEntry(node2) {
|
|
1776
|
+
const entry = asNode(node2) ?? {};
|
|
1777
|
+
const messages = [];
|
|
1778
|
+
const shortTexts = [];
|
|
1779
|
+
let code;
|
|
1780
|
+
for (const results of asArray(entry.SystemSpecificResults)) {
|
|
1781
|
+
const resultsNode = asNode(results) ?? {};
|
|
1782
|
+
for (const message of asArray(resultsNode.Message)) {
|
|
1783
|
+
code ??= nonEmpty2(asNode(message)?.["@code"]);
|
|
1784
|
+
const value = textOf(message);
|
|
1785
|
+
if (value !== void 0 && value !== "") messages.push(value);
|
|
1786
|
+
}
|
|
1787
|
+
for (const shortText of asArray(resultsNode.ShortText)) {
|
|
1788
|
+
const value = textOf(shortText);
|
|
1789
|
+
if (value !== void 0 && value !== "") shortTexts.push(value);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
const type = nonEmpty2(entry["@type"]);
|
|
1793
|
+
const description = [
|
|
1794
|
+
messages.join(" / ") || type || "sin detalle",
|
|
1795
|
+
...shortTexts.filter((short) => !messages.includes(short)).map((short) => `(${short})`)
|
|
1796
|
+
].join(" ");
|
|
1797
|
+
return { code: code ?? shortTexts[0] ?? type ?? "UNKNOWN", description };
|
|
1798
|
+
}
|
|
1799
|
+
function retryReason(error, idempotent) {
|
|
1800
|
+
if ((error instanceof SabreBusinessError || error instanceof SabreGatewayError) && PREVIOUS_ENTRY_ACTIVE.test(error.message)) {
|
|
1801
|
+
return "previous-entry-active";
|
|
1802
|
+
}
|
|
1803
|
+
if (!(error instanceof SabreGatewayError) || error.protocol !== "soap") return void 0;
|
|
1804
|
+
if (error.code !== void 0 && BUSY_CODES.has(error.code)) return "busy";
|
|
1805
|
+
if (!idempotent) return void 0;
|
|
1806
|
+
if (error.code !== void 0) return TRANSIENT_CODES.has(error.code) ? "transient" : void 0;
|
|
1807
|
+
if (error.status === void 0 || TRANSIENT_STATUS.has(error.status)) return "transient";
|
|
1808
|
+
return void 0;
|
|
1809
|
+
}
|
|
1810
|
+
function firstElement(node2) {
|
|
1811
|
+
for (const [key, value] of Object.entries(node2)) {
|
|
1812
|
+
if (!key.startsWith("@") && key !== "#text") return [key, Array.isArray(value) ? value[0] : value];
|
|
1813
|
+
}
|
|
1814
|
+
return [void 0, void 0];
|
|
1815
|
+
}
|
|
1816
|
+
function asNode(value) {
|
|
1817
|
+
if (Array.isArray(value)) return asNode(value[0]);
|
|
1818
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
1819
|
+
}
|
|
1820
|
+
function asArray(value) {
|
|
1821
|
+
if (value === void 0 || value === null || value === "") return [];
|
|
1822
|
+
return Array.isArray(value) ? value : [value];
|
|
1823
|
+
}
|
|
1824
|
+
function textOf(value) {
|
|
1825
|
+
if (typeof value === "string") return value;
|
|
1826
|
+
const node2 = asNode(value);
|
|
1827
|
+
const text = node2?.["#text"];
|
|
1828
|
+
return typeof text === "string" ? text : node2 === void 0 ? void 0 : "";
|
|
1829
|
+
}
|
|
1830
|
+
function stringAt(root2, ...path) {
|
|
1831
|
+
let node2 = root2;
|
|
1832
|
+
for (const key of path) node2 = asNode(node2)?.[key];
|
|
1833
|
+
return nonEmpty2(textOf(node2));
|
|
1834
|
+
}
|
|
1835
|
+
function nonEmpty2(value) {
|
|
1836
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1837
|
+
}
|
|
1838
|
+
function isAbort2(error, signal) {
|
|
1839
|
+
return signal?.aborted === true || error instanceof Error && error.name === "AbortError";
|
|
1840
|
+
}
|
|
1841
|
+
function describe2(error) {
|
|
1842
|
+
return error instanceof Error ? error.message : String(error);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// src/session/credentials.ts
|
|
1846
|
+
var PCC = /^[A-Za-z0-9]{3,4}$/;
|
|
1847
|
+
function isPseudoCityCode(value) {
|
|
1848
|
+
return typeof value === "string" && PCC.test(value);
|
|
1849
|
+
}
|
|
1850
|
+
var V2_PREFIX = "V1:";
|
|
1851
|
+
var FORMATS = "'V1:<user>:<PCC>:<domain>' (OAuth v2) o '<user>-<PCC>-<domain>' (OAuth v3)";
|
|
1852
|
+
function fail2(message) {
|
|
1853
|
+
throw new TypeError(`sabre-client: ${message}`);
|
|
1854
|
+
}
|
|
1855
|
+
function parseEprUsername(username) {
|
|
1856
|
+
const v2 = username.startsWith(V2_PREFIX);
|
|
1857
|
+
const fields = (v2 ? username.slice(V2_PREFIX.length) : username).split(v2 ? ":" : "-");
|
|
1858
|
+
if (fields.length < 3) fail2(`credentials.username debe tener el formato ${FORMATS} para abrir una sesi\xF3n SOAP`);
|
|
1859
|
+
const domain = fields.pop();
|
|
1860
|
+
const organization = fields.pop();
|
|
1861
|
+
const user = fields.join(v2 ? ":" : "-");
|
|
1862
|
+
if (user.trim() === "" || domain.trim() === "") {
|
|
1863
|
+
fail2(`credentials.username debe tener el formato ${FORMATS} para abrir una sesi\xF3n SOAP`);
|
|
1864
|
+
}
|
|
1865
|
+
if (!PCC.test(organization)) {
|
|
1866
|
+
fail2("el PCC de credentials.username debe tener 3 o 4 caracteres alfanum\xE9ricos");
|
|
1867
|
+
}
|
|
1868
|
+
return { username: user, organization, domain };
|
|
1869
|
+
}
|
|
1870
|
+
function resolveSessionCredentials(credentials) {
|
|
1871
|
+
const { clientId, clientSecret, password } = credentials;
|
|
1872
|
+
if (clientId === void 0 !== (clientSecret === void 0)) {
|
|
1873
|
+
fail2("credentials.clientId y credentials.clientSecret van juntos: pasa los dos (SessionCreateRQ 2.0.0) o ninguno (1.0.0)");
|
|
1874
|
+
}
|
|
1875
|
+
const epr = parseEprUsername(credentials.username);
|
|
1876
|
+
if (clientId === void 0 || clientSecret === void 0) return { version: "1.0.0", ...epr, password };
|
|
1877
|
+
if (typeof clientId !== "string" || clientId.trim() === "") fail2("credentials.clientId debe ser un string no vac\xEDo");
|
|
1878
|
+
if (typeof clientSecret !== "string" || clientSecret.trim() === "") {
|
|
1879
|
+
fail2("credentials.clientSecret debe ser un string no vac\xEDo");
|
|
1880
|
+
}
|
|
1881
|
+
return { version: "2.0.0", ...epr, password, clientId, clientSecret };
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// src/queue/queue-access.ts
|
|
1885
|
+
import { XMLParser as XMLParser2 } from "fast-xml-parser";
|
|
1886
|
+
|
|
1887
|
+
// src/queue/queue-ref.ts
|
|
1888
|
+
var MIN_QUEUE_NUMBER = 0;
|
|
1889
|
+
var MAX_QUEUE_NUMBER = 511;
|
|
1890
|
+
function validateQueueRef(value, label) {
|
|
1891
|
+
if (typeof value === "number") {
|
|
1892
|
+
if (Number.isInteger(value) && value >= MIN_QUEUE_NUMBER && value <= MAX_QUEUE_NUMBER) return;
|
|
1893
|
+
throw new TypeError(
|
|
1894
|
+
`sabre-client: ${label} debe ser un n\xFAmero de cola entero de ${MIN_QUEUE_NUMBER} a ${MAX_QUEUE_NUMBER}, o { name }`
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1898
|
+
const { name } = value;
|
|
1899
|
+
if (typeof name === "string" && name.trim() !== "") return;
|
|
1900
|
+
throw new TypeError(`sabre-client: ${label}.name debe ser un string no vac\xEDo`);
|
|
1901
|
+
}
|
|
1902
|
+
throw new TypeError(`sabre-client: ${label} debe ser un n\xFAmero de cola (0\u2013511) o { name }`);
|
|
1903
|
+
}
|
|
1904
|
+
function queueRefAttribute(queue) {
|
|
1905
|
+
return typeof queue === "number" ? `Number="${queue}"` : `Name="${xmlEscape(queue.name)}"`;
|
|
1906
|
+
}
|
|
1907
|
+
function toQueueRef(value) {
|
|
1908
|
+
const text = value?.trim();
|
|
1909
|
+
if (text === void 0 || text === "") return void 0;
|
|
1910
|
+
return /^\d+$/.test(text) ? Number(text) : { name: text };
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// src/queue/queue-access.ts
|
|
1914
|
+
var QUEUE_ACCESS_SERVICE = "queueAccess";
|
|
1915
|
+
var NAMESPACE = "http://webservices.sabre.com/sabreXML/2011/10";
|
|
1916
|
+
var END_OF_DISPLAY = "END OF DISPLAY FOR REQUESTED DATA";
|
|
1917
|
+
var QUEUE_EMPTY = "QUEUE SELECTED WAS EMPTY";
|
|
1918
|
+
var OFF_QUEUE = "CANNOT DO THIS IF OFF QUEUE";
|
|
1919
|
+
var MAX_SELECTIONS = 8;
|
|
1920
|
+
var MAX_REMOVE_FROM = 3;
|
|
1921
|
+
var OPERATIONS = { I: "queue.ignore", QR: "queue.remove", QXI: "queue.exit" };
|
|
1922
|
+
function root(body) {
|
|
1923
|
+
return `<QueueAccessRQ Version="2.1.1" xmlns="${NAMESPACE}">${body}</QueueAccessRQ>`;
|
|
1924
|
+
}
|
|
1925
|
+
function identifier(queue, pcc) {
|
|
1926
|
+
return `<QueueIdentifier ${queueRefAttribute(queue)} PseudoCityCode="${xmlEscape(pcc)}"/>`;
|
|
1927
|
+
}
|
|
1928
|
+
function queueAccessPayload(queue, pcc, selection = []) {
|
|
1929
|
+
const filters = selection.map((filter) => `<Selection Function="${xmlEscape(filter.function)}" Criteria="${xmlEscape(filter.criteria)}"/>`).join("");
|
|
1930
|
+
return root(`${identifier(queue, pcc)}${filters}`);
|
|
1931
|
+
}
|
|
1932
|
+
function queueNavigationPayload(action, from = []) {
|
|
1933
|
+
return root(`<Navigation Action="${action}"/>${from.map((target) => identifier(target.queue, target.pcc)).join("")}`);
|
|
1934
|
+
}
|
|
1935
|
+
function queueAccessRequest(payload, operation, options) {
|
|
1936
|
+
return { service: QUEUE_ACCESS_SERVICE, action: "QueueAccessLLSRQ", payload, idempotent: false, operation, ...options };
|
|
1937
|
+
}
|
|
1938
|
+
function navigationOperation(action) {
|
|
1939
|
+
return OPERATIONS[action];
|
|
1940
|
+
}
|
|
1941
|
+
var paragraphParser = new XMLParser2({
|
|
1942
|
+
ignoreAttributes: true,
|
|
1943
|
+
removeNSPrefix: true,
|
|
1944
|
+
parseTagValue: false,
|
|
1945
|
+
trimValues: false,
|
|
1946
|
+
processEntities: true,
|
|
1947
|
+
isArray: (name) => name === "Text"
|
|
1948
|
+
});
|
|
1949
|
+
function node(value) {
|
|
1950
|
+
if (Array.isArray(value)) return node(value[0]);
|
|
1951
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
1952
|
+
}
|
|
1953
|
+
function attribute(value, name) {
|
|
1954
|
+
const text = node(value)?.[`@${name}`];
|
|
1955
|
+
return typeof text === "string" && text.trim() !== "" ? text.trim() : void 0;
|
|
1956
|
+
}
|
|
1957
|
+
function element(value) {
|
|
1958
|
+
const text = textOf(Array.isArray(value) ? value[0] : value)?.trim();
|
|
1959
|
+
return text === void 0 || text === "" ? void 0 : text;
|
|
1960
|
+
}
|
|
1961
|
+
function textLines(raw, payloadName) {
|
|
1962
|
+
let parsed;
|
|
1963
|
+
try {
|
|
1964
|
+
parsed = paragraphParser.parse(raw);
|
|
1965
|
+
} catch {
|
|
1966
|
+
return [];
|
|
1967
|
+
}
|
|
1968
|
+
const payload = node(node(node(node(parsed)?.Envelope)?.Body)?.[payloadName]);
|
|
1969
|
+
const container = payloadName === "QueueAccessRS" ? node(payload?.Paragraph) : payload;
|
|
1970
|
+
return asArray(container?.Text).map((line) => typeof line === "string" ? line : textOf(line) ?? "");
|
|
1971
|
+
}
|
|
1972
|
+
function paragraphLines(raw) {
|
|
1973
|
+
return textLines(raw, "QueueAccessRS");
|
|
1974
|
+
}
|
|
1975
|
+
function isEndOfDisplay(text) {
|
|
1976
|
+
return text?.trim() === END_OF_DISPLAY;
|
|
1977
|
+
}
|
|
1978
|
+
function isEndOfDisplayResult(error) {
|
|
1979
|
+
return resultHasText(error, END_OF_DISPLAY);
|
|
1980
|
+
}
|
|
1981
|
+
function resultHasText(error, text) {
|
|
1982
|
+
const prefix = `${error.service}: `;
|
|
1983
|
+
const description = error.message.startsWith(prefix) ? error.message.slice(prefix.length) : error.message;
|
|
1984
|
+
return description.split(/ \/ | \(|\)/).some((part) => part.trim() === text);
|
|
1985
|
+
}
|
|
1986
|
+
function queueEndError(error, service) {
|
|
1987
|
+
if (!(error instanceof SabreBusinessError) || error.service !== service) return void 0;
|
|
1988
|
+
const results = [error, ...error.errors];
|
|
1989
|
+
if (results.some((result) => resultHasText(result, QUEUE_EMPTY))) return { reason: "empty", error };
|
|
1990
|
+
if (results.some(isEndOfDisplayResult)) return { reason: "end-of-display", error };
|
|
1991
|
+
return void 0;
|
|
1992
|
+
}
|
|
1993
|
+
function isOffQueueError(error) {
|
|
1994
|
+
return error instanceof SabreBusinessError && [error, ...error.errors].some((result) => resultHasText(result, OFF_QUEUE));
|
|
1995
|
+
}
|
|
1996
|
+
function endStep(warnings, reason = "end-of-display") {
|
|
1997
|
+
return { done: true, reason, message: reason === "empty" ? QUEUE_EMPTY : END_OF_DISPLAY, warnings };
|
|
1998
|
+
}
|
|
1999
|
+
function parseQueueStep(response) {
|
|
2000
|
+
const { warnings } = response;
|
|
2001
|
+
const paragraph = paragraphLines(response.raw);
|
|
2002
|
+
const firstLine = paragraph.find((line) => line.trim() !== "");
|
|
2003
|
+
if (isEndOfDisplay(firstLine) || warnings.some(isEndOfDisplayResult)) return endStep(warnings);
|
|
2004
|
+
const fields = lineFields(response.payload.Line);
|
|
2005
|
+
if (fields === void 0) {
|
|
2006
|
+
throw new SabreGatewayError(`${QUEUE_ACCESS_SERVICE}: respuesta no reconocida (sin Line/UniqueID ni fin de cola)`, {
|
|
2007
|
+
protocol: "soap",
|
|
2008
|
+
status: response.status,
|
|
2009
|
+
retryable: false,
|
|
2010
|
+
raw: response.raw
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
const item = { ...fields, paragraph };
|
|
2014
|
+
return { done: false, item, warnings };
|
|
2015
|
+
}
|
|
2016
|
+
function lineFields(value) {
|
|
2017
|
+
const line = node(value);
|
|
2018
|
+
const locator = attribute(line?.UniqueID, "ID");
|
|
2019
|
+
if (line === void 0 || locator === void 0) return void 0;
|
|
2020
|
+
const source = node(node(line.POS)?.Source);
|
|
2021
|
+
const agentSine = attribute(source, "AgentSine");
|
|
2022
|
+
const pseudoCityCode = attribute(source, "PseudoCityCode");
|
|
2023
|
+
const placedAt = element(line.DateTime);
|
|
2024
|
+
const primaryPassenger = element(line.PassengerName);
|
|
2025
|
+
const group = element(line.Group);
|
|
1238
2026
|
return {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
2027
|
+
locator,
|
|
2028
|
+
...placedAt === void 0 ? {} : { placedAt },
|
|
2029
|
+
...agentSine === void 0 && pseudoCityCode === void 0 ? {} : {
|
|
2030
|
+
placedBy: {
|
|
2031
|
+
...agentSine === void 0 ? {} : { agentSine },
|
|
2032
|
+
...pseudoCityCode === void 0 ? {} : { pseudoCityCode }
|
|
2033
|
+
}
|
|
2034
|
+
},
|
|
2035
|
+
...primaryPassenger === void 0 ? {} : { primaryPassenger },
|
|
2036
|
+
...group === void 0 ? {} : { group }
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/queue/queue-count.ts
|
|
2041
|
+
var QUEUE_COUNT_SERVICE = "queueCount";
|
|
2042
|
+
var NAMESPACE2 = "http://webservices.sabre.com/sabreXML/2011/10";
|
|
2043
|
+
var TOTAL_TYPES = { MESSAGES: "messages", SPECIALS: "specials", PNRS: "pnrs" };
|
|
2044
|
+
function queueCountPayload(pcc) {
|
|
2045
|
+
return `<QueueCountRQ Version="2.2.1" xmlns="${NAMESPACE2}"><QueueInfo><QueueIdentifier PseudoCityCode="${xmlEscape(pcc)}"/></QueueInfo></QueueCountRQ>`;
|
|
2046
|
+
}
|
|
2047
|
+
function queueCountRequest(pcc, options) {
|
|
2048
|
+
return {
|
|
2049
|
+
service: QUEUE_COUNT_SERVICE,
|
|
2050
|
+
action: "QueueCountLLSRQ",
|
|
2051
|
+
payload: queueCountPayload(pcc),
|
|
2052
|
+
idempotent: true,
|
|
2053
|
+
operation: "queue.count",
|
|
2054
|
+
...options
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
function parseHostCount(value) {
|
|
2058
|
+
const match = /^[.\s]*(\d+)\s*$/.exec(value ?? "");
|
|
2059
|
+
return match === null ? void 0 : Number(match[1]);
|
|
2060
|
+
}
|
|
2061
|
+
function attribute2(node2, name) {
|
|
2062
|
+
const value = node2?.[`@${name}`];
|
|
2063
|
+
return typeof value === "string" ? value : void 0;
|
|
2064
|
+
}
|
|
2065
|
+
function parseQueueCountResponse(response) {
|
|
2066
|
+
const queues = [];
|
|
2067
|
+
let countedAt;
|
|
2068
|
+
for (const info of asArray(response.payload.QueueInfo)) {
|
|
2069
|
+
countedAt ??= nonEmpty3(attribute2(info, "DateTime"));
|
|
2070
|
+
for (const identifier2 of asArray(info.QueueIdentifier)) {
|
|
2071
|
+
const number = attribute2(identifier2, "Number");
|
|
2072
|
+
const queue = toQueueRef(number);
|
|
2073
|
+
const count = parseHostCount(attribute2(identifier2, "Count"));
|
|
2074
|
+
if (queue === void 0 || count === void 0) {
|
|
2075
|
+
throw malformed(`QueueIdentifier con Number="${number ?? ""}" y Count="${attribute2(identifier2, "Count") ?? ""}"`, response);
|
|
2076
|
+
}
|
|
2077
|
+
queues.push({ queue, count });
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const totals = {};
|
|
2081
|
+
for (const total of asArray(response.payload.Totals)) {
|
|
2082
|
+
const key = TOTAL_TYPES[(attribute2(total, "Type") ?? "").toUpperCase()];
|
|
2083
|
+
if (key === void 0) continue;
|
|
2084
|
+
const count = parseHostCount(attribute2(total, "Count"));
|
|
2085
|
+
if (count === void 0) throw malformed(`Totals ${key} con Count="${attribute2(total, "Count") ?? ""}"`, response);
|
|
2086
|
+
totals[key] = count;
|
|
2087
|
+
}
|
|
2088
|
+
return { queues, totals, ...countedAt === void 0 ? {} : { countedAt }, warnings: response.warnings };
|
|
2089
|
+
}
|
|
2090
|
+
function malformed(detail, response) {
|
|
2091
|
+
return new SabreGatewayError(`${QUEUE_COUNT_SERVICE}: respuesta no reconocida (${detail})`, {
|
|
2092
|
+
protocol: "soap",
|
|
2093
|
+
status: response.status,
|
|
2094
|
+
retryable: false,
|
|
2095
|
+
raw: response.raw
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
function nonEmpty3(value) {
|
|
2099
|
+
const text = value?.trim();
|
|
2100
|
+
return text === void 0 || text === "" ? void 0 : text;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/queue/queue-list.ts
|
|
2104
|
+
var NAMESPACE3 = "http://webservices.sabre.com/sabreXML/2011/10";
|
|
2105
|
+
function queueListPayload(queue, pcc, primaryPassenger) {
|
|
2106
|
+
const list = primaryPassenger ? '<List Ind="true" PrimaryPassenger="true"/>' : '<List Ind="true"/>';
|
|
2107
|
+
return `<QueueAccessRQ Version="2.1.1" xmlns="${NAMESPACE3}"><QueueIdentifier ${queueRefAttribute(queue)} PseudoCityCode="${xmlEscape(pcc)}">${list}</QueueIdentifier></QueueAccessRQ>`;
|
|
2108
|
+
}
|
|
2109
|
+
function queueListRequest(payload, options) {
|
|
2110
|
+
return {
|
|
2111
|
+
service: QUEUE_ACCESS_SERVICE,
|
|
2112
|
+
action: "QueueAccessLLSRQ",
|
|
2113
|
+
payload,
|
|
2114
|
+
idempotent: true,
|
|
2115
|
+
operation: "queue.list",
|
|
2116
|
+
...options
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
function emptyList(warnings, paragraph = []) {
|
|
2120
|
+
return { locators: [], entries: [], paragraph, warnings };
|
|
2121
|
+
}
|
|
2122
|
+
function parseQueueList(response) {
|
|
2123
|
+
const { warnings } = response;
|
|
2124
|
+
const paragraph = paragraphLines(response.raw);
|
|
2125
|
+
const firstLine = paragraph.find((line) => line.trim() !== "");
|
|
2126
|
+
if (isEndOfDisplay(firstLine) || warnings.some(isEndOfDisplayResult)) return emptyList(warnings, paragraph);
|
|
2127
|
+
const entries = asArray(response.payload.Line).map((line) => {
|
|
2128
|
+
const fields = lineFields(line);
|
|
2129
|
+
if (fields === void 0) {
|
|
2130
|
+
throw new SabreGatewayError(`${QUEUE_ACCESS_SERVICE}: respuesta no reconocida (Line sin UniqueID en la lista)`, {
|
|
2131
|
+
protocol: "soap",
|
|
2132
|
+
status: response.status,
|
|
2133
|
+
retryable: false,
|
|
2134
|
+
raw: response.raw
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
const number = attribute(line, "Number");
|
|
2138
|
+
const position = number !== void 0 && /^\d+$/.test(number) ? Number(number) : void 0;
|
|
2139
|
+
return { ...position === void 0 ? {} : { position }, ...fields };
|
|
2140
|
+
});
|
|
2141
|
+
return { locators: entries.map((entry) => entry.locator), entries, paragraph, warnings };
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/queue/queue-place.ts
|
|
2145
|
+
var QUEUE_PLACE_SERVICE = "queuePlace";
|
|
2146
|
+
var NAMESPACE4 = "http://webservices.sabre.com/sabreXML/2011/10";
|
|
2147
|
+
var MAX_PLACE_TO = 10;
|
|
2148
|
+
var MAX_PLACE_LOCATORS = 6;
|
|
2149
|
+
var INSTRUCTION_CODE_RANGES = [
|
|
2150
|
+
[0, 191],
|
|
2151
|
+
[200, 254]
|
|
2152
|
+
];
|
|
2153
|
+
function isInstructionCode(value) {
|
|
2154
|
+
return typeof value === "number" && Number.isInteger(value) && INSTRUCTION_CODE_RANGES.some(([min, max]) => value >= min && value <= max);
|
|
2155
|
+
}
|
|
2156
|
+
function queuePlacePayload(to, locators = []) {
|
|
2157
|
+
const identifiers = to.map((target) => {
|
|
2158
|
+
const pic = target.instructionCode === void 0 ? "" : ` PrefatoryInstructionCode="${target.instructionCode}"`;
|
|
2159
|
+
return `<QueueIdentifier ${queueRefAttribute(target.queue)}${pic} PseudoCityCode="${xmlEscape(target.pcc)}"/>`;
|
|
2160
|
+
}).join("");
|
|
2161
|
+
const ids = locators.map((locator) => `<UniqueID ID="${xmlEscape(locator)}"/>`).join("");
|
|
2162
|
+
return `<QueuePlaceRQ Version="2.0.4" xmlns="${NAMESPACE4}"><QueueInfo>${identifiers}${ids}</QueueInfo></QueuePlaceRQ>`;
|
|
2163
|
+
}
|
|
2164
|
+
function queuePlaceRequest(payload, options) {
|
|
2165
|
+
return {
|
|
2166
|
+
service: QUEUE_PLACE_SERVICE,
|
|
2167
|
+
action: "QueuePlaceLLSRQ",
|
|
2168
|
+
payload,
|
|
2169
|
+
idempotent: false,
|
|
2170
|
+
operation: "queue.place",
|
|
2171
|
+
...options
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
var SIGNATURE_LINE = /\s\d{4}\/\d{2}[A-Z]{3}\d{2}\s+([A-Z0-9]{6})\s+H\s*$/;
|
|
2175
|
+
function signatureLocator(lines) {
|
|
2176
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
2177
|
+
const match = SIGNATURE_LINE.exec(lines[index] ?? "");
|
|
2178
|
+
if (match !== null) return match[1];
|
|
2179
|
+
}
|
|
2180
|
+
return void 0;
|
|
2181
|
+
}
|
|
2182
|
+
function isEnd(response, text) {
|
|
2183
|
+
return response.warnings.some(isEndOfDisplayResult) || isEndOfDisplay(text.find((line) => line.trim() !== ""));
|
|
2184
|
+
}
|
|
2185
|
+
function parsePlaceStep(response) {
|
|
2186
|
+
const text = textLines(response.raw, "QueuePlaceRS");
|
|
2187
|
+
if (isEnd(response, text)) return endStep(response.warnings);
|
|
2188
|
+
const locator = signatureLocator(text);
|
|
2189
|
+
if (locator === void 0) {
|
|
2190
|
+
throw new SabreGatewayError(`${QUEUE_PLACE_SERVICE}: respuesta no reconocida (sin l\xEDnea de firma del PNR ni fin de cola)`, {
|
|
2191
|
+
protocol: "soap",
|
|
2192
|
+
status: response.status,
|
|
2193
|
+
retryable: false,
|
|
2194
|
+
raw: response.raw
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
return { done: false, item: { locator, paragraph: text }, warnings: response.warnings };
|
|
2198
|
+
}
|
|
2199
|
+
function parsePlaceUnknown(response) {
|
|
2200
|
+
const text = textLines(response.raw, "QueuePlaceRS");
|
|
2201
|
+
if (isEnd(response, text) || signatureLocator(text) !== void 0) return parsePlaceStep(response);
|
|
2202
|
+
return parsePlaceResult(response);
|
|
2203
|
+
}
|
|
2204
|
+
function parsePlaceResult(response) {
|
|
2205
|
+
const placements = asArray(response.payload.QueueInfo).map((info) => {
|
|
2206
|
+
const identifier2 = node(node(info)?.QueueIdentifier);
|
|
2207
|
+
const number = attribute(identifier2, "Number");
|
|
2208
|
+
const name = attribute(identifier2, "Name");
|
|
2209
|
+
const queue = number !== void 0 ? toQueueRef(number) : name !== void 0 ? { name } : void 0;
|
|
2210
|
+
const pcc = attribute(identifier2, "PseudoCityCode");
|
|
2211
|
+
const locator = attribute(node(info)?.UniqueID, "ID");
|
|
2212
|
+
const placedAt = attribute(info, "DateTime");
|
|
2213
|
+
return {
|
|
2214
|
+
...queue === void 0 ? {} : { queue },
|
|
2215
|
+
...pcc === void 0 ? {} : { pcc },
|
|
2216
|
+
...locator === void 0 ? {} : { locator },
|
|
2217
|
+
...placedAt === void 0 ? {} : { placedAt }
|
|
2218
|
+
};
|
|
2219
|
+
});
|
|
2220
|
+
return { placements, text: textLines(response.raw, "QueuePlaceRS"), warnings: response.warnings };
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// src/queue/queue-management.ts
|
|
2224
|
+
function fail3(message) {
|
|
2225
|
+
throw new TypeError(`sabre-client: ${message}`);
|
|
2226
|
+
}
|
|
2227
|
+
function rejectedBySabre(error) {
|
|
2228
|
+
return error instanceof SabreBusinessError || error instanceof SabreGatewayError && error.code !== void 0;
|
|
2229
|
+
}
|
|
2230
|
+
var QueueManagement = class {
|
|
2231
|
+
#context;
|
|
2232
|
+
#state = "outside";
|
|
2233
|
+
/** @internal Lo construye `SabreSession`. */
|
|
2234
|
+
constructor(context) {
|
|
2235
|
+
this.#context = context;
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Si la sesión está dentro de una cola. Lo lleva el SDK: `access` con un PNR la deja en
|
|
2239
|
+
* `'inside'`, y el fin de cola o `exit()`, en `'outside'`.
|
|
2240
|
+
*/
|
|
2241
|
+
get state() {
|
|
2242
|
+
return this.#state;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Get Queue Count (`QueueCountLLSRQ` 2.2.1): cuántos mensajes o PNR hay en cada cola del PCC,
|
|
2246
|
+
* más los totales. Cada `queue` de la respuesta se puede pasar tal cual a las demás operaciones
|
|
2247
|
+
* de cola.
|
|
2248
|
+
*
|
|
2249
|
+
* Se puede llamar dentro o fuera de una cola, y no cambia `state`. Es una lectura: se reintenta
|
|
2250
|
+
* ante fallos transitorios. Un PCC que no tiene 3 o 4 caracteres alfanuméricos lanza
|
|
2251
|
+
* `TypeError` sin red.
|
|
2252
|
+
*/
|
|
2253
|
+
async count(request = {}, options = {}) {
|
|
2254
|
+
const pcc = this.#pcc(request.pcc, "pcc");
|
|
2255
|
+
const response = await this.#context.send(queueCountRequest(pcc, options));
|
|
2256
|
+
return parseQueueCountResponse(response);
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* La lista de localizadores de una cola (`QueueAccessLLSRQ` con `List`), sin recorrerla. Con
|
|
2260
|
+
* `primaryPassenger: true`, cada entrada trae también el pasajero principal. Una cola vacía da
|
|
2261
|
+
* una lista vacía.
|
|
2262
|
+
*
|
|
2263
|
+
* No cambia `state`. Es una lectura: se reintenta ante fallos transitorios. Lanza `TypeError`
|
|
2264
|
+
* sin red si `queue` o `pcc` no son válidos, o si `primaryPassenger` no es un boolean.
|
|
2265
|
+
*/
|
|
2266
|
+
async list(request, options = {}) {
|
|
2267
|
+
if (typeof request !== "object" || request === null) fail3("queue.list recibe { queue, pcc?, primaryPassenger? }");
|
|
2268
|
+
validateQueueRef(request.queue, "queue");
|
|
2269
|
+
const pcc = this.#pcc(request.pcc, "pcc");
|
|
2270
|
+
const { primaryPassenger = false } = request;
|
|
2271
|
+
if (typeof primaryPassenger !== "boolean") fail3("primaryPassenger debe ser un boolean");
|
|
2272
|
+
const send2 = queueListRequest(queueListPayload(request.queue, pcc, primaryPassenger), options);
|
|
2273
|
+
try {
|
|
2274
|
+
return parseQueueList(await this.#context.send(send2));
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
const end = queueEndError(error, send2.service);
|
|
2277
|
+
if (end !== void 0) return emptyList([...end.error.warnings]);
|
|
2278
|
+
throw error;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Access Queue (`QueueAccessLLSRQ` 2.1.1): entra a una cola y devuelve su primer PNR. Una cola
|
|
2283
|
+
* vacía, o sin PNR que cumplan `selection`, devuelve `{ done: true }` y la sesión sigue afuera.
|
|
2284
|
+
*
|
|
2285
|
+
* Lanza `TypeError` sin red si la sesión ya está dentro de una cola (o en `'unknown'`: hay que
|
|
2286
|
+
* llamar a `exit()` primero), si `queue` no es de 0 a 511 o `{ name }`, o con más de 8 filtros.
|
|
2287
|
+
* Mueve el cursor: nunca se reintenta ante un fallo de red, y ese fallo deja `state` en
|
|
2288
|
+
* `'unknown'`.
|
|
2289
|
+
*/
|
|
2290
|
+
async access(request, options = {}) {
|
|
2291
|
+
if (this.#state === "inside") fail3("la sesi\xF3n ya est\xE1 dentro de una cola: sal con queue.exit() antes de entrar a otra");
|
|
2292
|
+
if (this.#state === "unknown") fail3("no se sabe si la sesi\xF3n sigue dentro de una cola: llama a queue.exit() antes de entrar");
|
|
2293
|
+
if (typeof request !== "object" || request === null) fail3("queue.access recibe { queue, pcc?, selection? }");
|
|
2294
|
+
validateQueueRef(request.queue, "queue");
|
|
2295
|
+
const pcc = this.#pcc(request.pcc, "pcc");
|
|
2296
|
+
const selection = validateSelection(request.selection);
|
|
2297
|
+
return this.#step(queueAccessRequest(queueAccessPayload(request.queue, pcc, selection), "queue.access", options));
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Deja el PNR actual en la cola y pasa al siguiente (`I`). Fuera de una cola lanza `TypeError`
|
|
2301
|
+
* sin red.
|
|
2302
|
+
*/
|
|
2303
|
+
async ignore(options = {}) {
|
|
2304
|
+
this.#requireQueue("ignore");
|
|
2305
|
+
return this.#step(queueAccessRequest(queueNavigationPayload("I"), navigationOperation("I"), options));
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Saca el PNR actual de la cola y pasa al siguiente (`QR`). Con `from` (de 1 a 3 colas), lo saca
|
|
2309
|
+
* de esas colas a la vez. Fuera de una cola lanza `TypeError` sin red.
|
|
2310
|
+
*/
|
|
2311
|
+
async remove(request = {}, options = {}) {
|
|
2312
|
+
this.#requireQueue("remove");
|
|
2313
|
+
const from = this.#removeFrom(request.from);
|
|
2314
|
+
return this.#step(queueAccessRequest(queueNavigationPayload("QR", from), navigationOperation("QR"), options));
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Sale de la cola sin cerrar la transacción (`QXI`) y deja `state` en `'outside'`. Fuera de una
|
|
2318
|
+
* cola lanza `TypeError` sin red. En `'unknown'` manda `QXI` igual y, si Sabre lo rechaza con
|
|
2319
|
+
* `CANNOT DO THIS IF OFF QUEUE` (la sesión ya no estaba en una cola), lo da por bueno. Cualquier
|
|
2320
|
+
* otro error lanza y deja `'unknown'`.
|
|
2321
|
+
*/
|
|
2322
|
+
async exit(options = {}) {
|
|
2323
|
+
this.#requireQueue("exit");
|
|
2324
|
+
const before = this.#state;
|
|
2325
|
+
const request = queueAccessRequest(queueNavigationPayload("QXI"), navigationOperation("QXI"), options);
|
|
2326
|
+
try {
|
|
2327
|
+
await this.#context.send(request);
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
if (before === "unknown" && isOffQueueError(error)) {
|
|
2330
|
+
this.#context.log.emit("info", "sabre.queue.exitTolerated", { code: error.code, error });
|
|
2331
|
+
this.#state = "outside";
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
this.#onError(error);
|
|
2335
|
+
throw error;
|
|
2336
|
+
}
|
|
2337
|
+
this.#state = "outside";
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Recorre una cola: entra con `access`, llama a `callback` con cada PNR, ejecuta la acción que
|
|
2341
|
+
* devuelve (`remove`, `ignore` o `place`) y termina en el fin de cola. No hace nada que no se
|
|
2342
|
+
* pueda hacer con las primitivas.
|
|
2343
|
+
*
|
|
2344
|
+
* - Con `limit`, después de visitar esa cantidad de PNR sale con `QXI`. No tiene tope por
|
|
2345
|
+
* defecto: con una cola anómala que nunca llega al fin, el recorrido no termina. Conviene
|
|
2346
|
+
* pasarlo.
|
|
2347
|
+
* - Si el callback lanza, o devuelve una acción inválida, sale con `QXI` y propaga el error. No
|
|
2348
|
+
* lo convierte en `ignore`: para seguir de largo, el callback atrapa su error y devuelve
|
|
2349
|
+
* `{ action: 'ignore' }`.
|
|
2350
|
+
* - Si una operación falla, sale con `QXI` (tolerando que la sesión ya no esté en la cola) y
|
|
2351
|
+
* propaga el error. Con un `SabreSessionError` no intenta salir: la sesión ya no sirve.
|
|
2352
|
+
*
|
|
2353
|
+
* `options` se aplica a cada llamada a Sabre del recorrido.
|
|
2354
|
+
*/
|
|
2355
|
+
async process(request, callback, options = {}) {
|
|
2356
|
+
if (typeof request !== "object" || request === null) fail3("queue.process recibe { queue, pcc?, selection?, limit? }");
|
|
2357
|
+
if (typeof callback !== "function") fail3("queue.process necesita un callback");
|
|
2358
|
+
const { limit, ...access } = request;
|
|
2359
|
+
if (limit !== void 0 && !(Number.isInteger(limit) && limit >= 1)) fail3("limit debe ser un entero mayor o igual a 1");
|
|
2360
|
+
const startedOutside = this.#state === "outside";
|
|
2361
|
+
let visited = 0;
|
|
2362
|
+
try {
|
|
2363
|
+
let step = await this.access(access, options);
|
|
2364
|
+
while (!step.done) {
|
|
2365
|
+
if (limit !== void 0 && visited >= limit) {
|
|
2366
|
+
await this.exit(options);
|
|
2367
|
+
return { visited, ended: "limit" };
|
|
1246
2368
|
}
|
|
2369
|
+
const action = await callback(step.item);
|
|
2370
|
+
visited++;
|
|
2371
|
+
step = await this.#processAction(action, options);
|
|
1247
2372
|
}
|
|
1248
|
-
|
|
2373
|
+
return { visited, ended: step.reason };
|
|
2374
|
+
} catch (error) {
|
|
2375
|
+
if (startedOutside) await this.#exitQuietly(error, options);
|
|
2376
|
+
throw error;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
async #processAction(action, options) {
|
|
2380
|
+
const kind = typeof action === "object" && action !== null ? action.action : void 0;
|
|
2381
|
+
if (kind === "ignore") return this.ignore(options);
|
|
2382
|
+
if (kind === "remove") return this.remove({}, options);
|
|
2383
|
+
if (kind === "place") {
|
|
2384
|
+
const step = await this.place({ to: action.to }, options);
|
|
2385
|
+
if (!("done" in step)) throw new TypeError("sabre-client: queue.place no devolvi\xF3 un paso de la cola");
|
|
2386
|
+
return step;
|
|
2387
|
+
}
|
|
2388
|
+
throw new TypeError("sabre-client: el callback de queue.process debe devolver { action: 'remove' }, { action: 'ignore' } o { action: 'place', to }");
|
|
2389
|
+
}
|
|
2390
|
+
/** Sale de la cola después de un error del recorrido, sin pisar ese error. */
|
|
2391
|
+
async #exitQuietly(cause, options) {
|
|
2392
|
+
if (this.#state === "outside" || cause instanceof SabreSessionError) return;
|
|
2393
|
+
try {
|
|
2394
|
+
await this.exit(options);
|
|
2395
|
+
} catch (error) {
|
|
2396
|
+
this.#context.log.emit("warn", "sabre.queue.exitFailed", { error });
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
async place(request, options = {}) {
|
|
2400
|
+
if (typeof request !== "object" || request === null) fail3("queue.place recibe { to, locators? }");
|
|
2401
|
+
const to = this.#placeTo(request.to);
|
|
2402
|
+
const locators = validateLocators(request.locators);
|
|
2403
|
+
if (locators.length > 0 && this.#state === "inside") {
|
|
2404
|
+
fail3("queue.place con locators no se puede dentro de una cola, donde coloca el PNR actual: sal con queue.exit() antes");
|
|
2405
|
+
}
|
|
2406
|
+
if (locators.length > 0 && this.#state === "unknown") {
|
|
2407
|
+
fail3("no se sabe si la sesi\xF3n sigue dentro de una cola: llama a queue.exit() antes de colocar por localizador");
|
|
2408
|
+
}
|
|
2409
|
+
if (to.some((target) => target.instructionCode === void 0)) {
|
|
2410
|
+
fail3("queue.place necesita instructionCode en cada destino: Sabre lo exige, dentro y fuera de una cola");
|
|
2411
|
+
}
|
|
2412
|
+
const send2 = queuePlaceRequest(queuePlacePayload(to, locators), options);
|
|
2413
|
+
if (this.#state === "outside") return parsePlaceResult(await this.#context.send(send2));
|
|
2414
|
+
return this.#advance(send2, this.#state === "inside" ? parsePlaceStep : parsePlaceUnknown);
|
|
2415
|
+
}
|
|
2416
|
+
/** Manda una operación que avanza y actualiza `state` con el resultado. */
|
|
2417
|
+
#step(request) {
|
|
2418
|
+
return this.#advance(request, parseQueueStep);
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Manda una operación que puede mover el cursor. Un `QueueStep` fija el estado: `'outside'` con
|
|
2422
|
+
* el fin de cola e `'inside'` con un PNR. Cualquier otro resultado lo deja como estaba.
|
|
2423
|
+
*/
|
|
2424
|
+
async #advance(request, parse) {
|
|
2425
|
+
let result;
|
|
2426
|
+
try {
|
|
2427
|
+
result = parse(await this.#context.send(request));
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
const end = queueEndError(error, request.service);
|
|
2430
|
+
if (end !== void 0) {
|
|
2431
|
+
this.#state = "outside";
|
|
2432
|
+
return endStep([...end.error.warnings], end.reason);
|
|
2433
|
+
}
|
|
2434
|
+
this.#onError(error);
|
|
2435
|
+
throw error;
|
|
2436
|
+
}
|
|
2437
|
+
if ("done" in result) this.#state = result.done ? "outside" : "inside";
|
|
2438
|
+
return result;
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Qué pasa con `state` cuando una operación de cola falla. Los `TypeError` del uso exclusivo y
|
|
2442
|
+
* los errores de sesión no lo tocan: no llegaron a Sabre, o la sesión ya no sirve. Dentro de una
|
|
2443
|
+
* cola, cualquier otro error deja `'unknown'`. Fuera, solo lo que no es un rechazo claro de
|
|
2444
|
+
* Sabre (fallo de red, cancelación, respuesta ilegible).
|
|
2445
|
+
*/
|
|
2446
|
+
#onError(error) {
|
|
2447
|
+
if (error instanceof TypeError || error instanceof SabreSessionError) return;
|
|
2448
|
+
if (this.#state === "outside" && rejectedBySabre(error)) return;
|
|
2449
|
+
this.#state = "unknown";
|
|
2450
|
+
}
|
|
2451
|
+
#placeTo(to) {
|
|
2452
|
+
if (!Array.isArray(to) || to.length === 0 || to.length > MAX_PLACE_TO) {
|
|
2453
|
+
fail3(`to debe ser una lista de 1 a ${MAX_PLACE_TO} colas`);
|
|
2454
|
+
}
|
|
2455
|
+
return to.map((target, index) => {
|
|
2456
|
+
if (typeof target !== "object" || target === null) fail3(`to[${index}] debe ser { queue, pcc?, instructionCode }`);
|
|
2457
|
+
const { queue, pcc, instructionCode } = target;
|
|
2458
|
+
validateQueueRef(queue, `to[${index}].queue`);
|
|
2459
|
+
if (instructionCode !== void 0 && !isInstructionCode(instructionCode)) {
|
|
2460
|
+
fail3(`to[${index}].instructionCode debe ser un entero de 0 a 191 o de 200 a 254`);
|
|
2461
|
+
}
|
|
2462
|
+
return {
|
|
2463
|
+
queue,
|
|
2464
|
+
pcc: this.#pcc(pcc, `to[${index}].pcc`),
|
|
2465
|
+
...instructionCode === void 0 ? {} : { instructionCode }
|
|
2466
|
+
};
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
#requireQueue(operation) {
|
|
2470
|
+
if (this.#state === "outside") fail3(`queue.${operation} necesita estar dentro de una cola: entra con queue.access()`);
|
|
2471
|
+
}
|
|
2472
|
+
#pcc(value, label) {
|
|
2473
|
+
if (value === void 0) return this.#context.pcc;
|
|
2474
|
+
if (!isPseudoCityCode(value)) fail3(`${label} debe tener 3 o 4 caracteres alfanum\xE9ricos`);
|
|
2475
|
+
return value;
|
|
2476
|
+
}
|
|
2477
|
+
#removeFrom(from) {
|
|
2478
|
+
if (from === void 0) return [];
|
|
2479
|
+
if (!Array.isArray(from) || from.length === 0 || from.length > MAX_REMOVE_FROM) {
|
|
2480
|
+
fail3(`from debe ser una lista de 1 a ${MAX_REMOVE_FROM} colas`);
|
|
2481
|
+
}
|
|
2482
|
+
return from.map((target, index) => {
|
|
2483
|
+
if (typeof target !== "object" || target === null) fail3(`from[${index}] debe ser { queue, pcc? }`);
|
|
2484
|
+
const { queue, pcc } = target;
|
|
2485
|
+
validateQueueRef(queue, `from[${index}].queue`);
|
|
2486
|
+
return { queue, pcc: this.#pcc(pcc, `from[${index}].pcc`) };
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
function validateLocators(locators) {
|
|
2491
|
+
if (locators === void 0) return [];
|
|
2492
|
+
if (!Array.isArray(locators) || locators.length === 0 || locators.length > MAX_PLACE_LOCATORS) {
|
|
2493
|
+
fail3(`locators debe ser una lista de 1 a ${MAX_PLACE_LOCATORS} localizadores`);
|
|
2494
|
+
}
|
|
2495
|
+
return locators.map((locator, index) => {
|
|
2496
|
+
if (typeof locator !== "string" || locator.trim() === "") fail3(`locators[${index}] debe ser un string no vac\xEDo`);
|
|
2497
|
+
return locator;
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
function validateSelection(selection) {
|
|
2501
|
+
if (selection === void 0) return [];
|
|
2502
|
+
if (!Array.isArray(selection)) fail3("selection debe ser una lista de { function, criteria }");
|
|
2503
|
+
if (selection.length > MAX_SELECTIONS) fail3(`selection admite hasta ${MAX_SELECTIONS} filtros`);
|
|
2504
|
+
return selection.map((filter, index) => {
|
|
2505
|
+
const { function: fn, criteria } = typeof filter === "object" && filter !== null ? filter : {};
|
|
2506
|
+
if (typeof fn !== "string" || fn.trim() === "") fail3(`selection[${index}].function debe ser un string no vac\xEDo`);
|
|
2507
|
+
if (typeof criteria !== "string" || criteria.trim() === "") {
|
|
2508
|
+
fail3(`selection[${index}].criteria debe ser un string no vac\xEDo`);
|
|
2509
|
+
}
|
|
2510
|
+
return { function: fn, criteria };
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// src/session/session.ts
|
|
2515
|
+
var OTA_NAMESPACE = "http://www.opentravel.org/OTA/2002/11";
|
|
2516
|
+
var SWS_NAMESPACE = "http://webservices.sabre.com";
|
|
2517
|
+
var sessionSend = /* @__PURE__ */ Symbol("sabre-client.session.send");
|
|
2518
|
+
function fail4(message) {
|
|
2519
|
+
throw new TypeError(`sabre-client: ${message}`);
|
|
2520
|
+
}
|
|
2521
|
+
var SESSION_ERROR_KINDS = /* @__PURE__ */ new Map([
|
|
2522
|
+
["USG_RESOURCE_UNAVAILABLE", "limit"],
|
|
2523
|
+
["USG_INVALID_SESSION", "invalid"],
|
|
2524
|
+
["USG_INVALID_SECURITY_TOKEN", "expired"],
|
|
2525
|
+
["USG_AUTHENTICATION_FAILED", "expired"],
|
|
2526
|
+
["USG_RESOURCE_BLOCKED", "blocked"]
|
|
2527
|
+
]);
|
|
2528
|
+
var OPEN_ERROR_KINDS = /* @__PURE__ */ new Set(["limit", "blocked"]);
|
|
2529
|
+
function toSessionError(error, allowed) {
|
|
2530
|
+
if (!(error instanceof SabreGatewayError) || error instanceof SabreSessionError) return error;
|
|
2531
|
+
if (error.protocol !== "soap" || error.code === void 0) return error;
|
|
2532
|
+
const kind = SESSION_ERROR_KINDS.get(error.code);
|
|
2533
|
+
if (kind === void 0 || allowed !== void 0 && !allowed.has(kind)) return error;
|
|
2534
|
+
return new SabreSessionError(error.message, {
|
|
2535
|
+
kind,
|
|
2536
|
+
code: error.code,
|
|
2537
|
+
retryable: error.retryable,
|
|
2538
|
+
raw: error.raw,
|
|
2539
|
+
cause: error.cause,
|
|
2540
|
+
...error.status === void 0 ? {} : { status: error.status }
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
var KEEP_ALIVE_IDLE_MS = 13 * 6e4;
|
|
2544
|
+
var SabreSession = class {
|
|
2545
|
+
/** Queue Management: contar, recorrer y colocar en colas, dentro de esta sesión. */
|
|
2546
|
+
queue;
|
|
2547
|
+
#transport;
|
|
2548
|
+
#pcc;
|
|
2549
|
+
#conversationId;
|
|
2550
|
+
#security;
|
|
2551
|
+
#busy = false;
|
|
2552
|
+
#closing;
|
|
2553
|
+
/** El `SabreSessionError` que dejó la sesión inutilizable. */
|
|
2554
|
+
#unusable;
|
|
2555
|
+
/** Última vez que Sabre respondió en esta sesión (`Date.now()`). */
|
|
2556
|
+
#lastActivity = Date.now();
|
|
2557
|
+
#keepAliveTimer;
|
|
2558
|
+
/** El ping de `keepAlive` en vuelo: las llamadas del consumidor lo esperan en vez de chocar. */
|
|
2559
|
+
#keepAlivePing;
|
|
2560
|
+
static {
|
|
2561
|
+
installAsyncDispose(this.prototype, Symbol.asyncDispose);
|
|
2562
|
+
}
|
|
2563
|
+
/** @internal Se crea con `SabreLegacy.openSession()`. */
|
|
2564
|
+
constructor(internal) {
|
|
2565
|
+
this.#transport = internal.transport;
|
|
2566
|
+
this.#pcc = internal.pcc;
|
|
2567
|
+
this.#conversationId = internal.conversationId;
|
|
2568
|
+
this.#security = { binarySecurityToken: internal.token };
|
|
2569
|
+
this.queue = new QueueManagement({
|
|
2570
|
+
send: (request) => this[sessionSend](request),
|
|
2571
|
+
pcc: internal.pcc,
|
|
2572
|
+
log: internal.transport.log
|
|
2573
|
+
});
|
|
2574
|
+
if (internal.keepAlive) this.#scheduleKeepAlive(KEEP_ALIVE_IDLE_MS);
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Milisegundos desde la última respuesta de Sabre en esta sesión; `0` mientras hay una llamada
|
|
2578
|
+
* en vuelo. Sabre cierra la sesión a los 15 minutos de inactividad.
|
|
2579
|
+
*/
|
|
2580
|
+
get idleMs() {
|
|
2581
|
+
return this.#busy ? 0 : Math.max(0, Date.now() - this.#lastActivity);
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Manda `OTA_PingRQ`, que reinicia el contador de inactividad sin tocar el AAA. Durante un
|
|
2585
|
+
* recorrido de cola no hace falta: cada operación ya lo reinicia.
|
|
2586
|
+
*/
|
|
2587
|
+
async ping(options = {}) {
|
|
2588
|
+
checkPingResponse(await this[sessionSend]({ ...pingRequest(), ...options }));
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Cierra la sesión con `SessionCloseRQ`: Sabre descarta el AAA e invalida el token. Es
|
|
2592
|
+
* idempotente: las llamadas siguientes devuelven la misma promesa, sin volver a la red. Si el
|
|
2593
|
+
* cierre falla, la sesión queda cerrada igual del lado del SDK; la de Sabre vence sola a los
|
|
2594
|
+
* 15 minutos. Una sesión inutilizable (ver `SabreSessionError`) se cierra sin red: Sabre ya no
|
|
2595
|
+
* la reconoce.
|
|
2596
|
+
*/
|
|
2597
|
+
close(options = {}) {
|
|
2598
|
+
if (this.#closing !== void 0) return this.#closing;
|
|
2599
|
+
if (this.#busy && this.#keepAlivePing === void 0) {
|
|
2600
|
+
return Promise.reject(new TypeError("sabre-client: session busy"));
|
|
2601
|
+
}
|
|
2602
|
+
this.#stopKeepAlive();
|
|
2603
|
+
if (this.#unusable !== void 0) {
|
|
2604
|
+
this.#closing = Promise.resolve();
|
|
2605
|
+
return this.#closing;
|
|
2606
|
+
}
|
|
2607
|
+
const before = this.#keepAlivePing ?? Promise.resolve();
|
|
2608
|
+
this.#closing = before.then(() => this.#sendClose(options));
|
|
2609
|
+
return this.#closing;
|
|
2610
|
+
}
|
|
2611
|
+
/** @internal Una llamada a Sabre dentro de la sesión, con uso exclusivo. */
|
|
2612
|
+
async [sessionSend](request) {
|
|
2613
|
+
if (this.#keepAlivePing !== void 0 && this.#closing === void 0) await this.#keepAlivePing;
|
|
2614
|
+
return this.#send(request);
|
|
2615
|
+
}
|
|
2616
|
+
async #send(request) {
|
|
2617
|
+
if (this.#closing !== void 0) fail4("session closed");
|
|
2618
|
+
if (this.#unusable !== void 0) {
|
|
2619
|
+
throw new TypeError(`sabre-client: session unusable (${this.#unusable.kind})`, { cause: this.#unusable });
|
|
2620
|
+
}
|
|
2621
|
+
if (this.#busy) fail4("session busy");
|
|
2622
|
+
this.#busy = true;
|
|
2623
|
+
try {
|
|
2624
|
+
const response = await this.#transport.request({ ...request, ...this.#envelopeFields() });
|
|
2625
|
+
this.#touch();
|
|
2626
|
+
return response;
|
|
2627
|
+
} catch (error) {
|
|
2628
|
+
if (reachedSabre(error)) this.#touch();
|
|
2629
|
+
throw this.#markUnusable(error);
|
|
2630
|
+
} finally {
|
|
2631
|
+
this.#busy = false;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
async #sendClose(options) {
|
|
2635
|
+
if (this.#unusable !== void 0) return;
|
|
2636
|
+
this.#busy = true;
|
|
2637
|
+
try {
|
|
2638
|
+
await this.#transport.request({
|
|
2639
|
+
service: "sessionClose",
|
|
2640
|
+
action: "SessionCloseRQ",
|
|
2641
|
+
payload: `<SessionCloseRQ Version="1.0.1"><POS><Source PseudoCityCode="${xmlEscape(this.#pcc)}"/></POS></SessionCloseRQ>`,
|
|
2642
|
+
...this.#envelopeFields(),
|
|
2643
|
+
// Solo se reintenta ante `USG_*_BUSY`: tras un fallo de red no se sabe si ya se cerró.
|
|
2644
|
+
idempotent: false,
|
|
2645
|
+
operation: "close",
|
|
2646
|
+
...options
|
|
2647
|
+
});
|
|
2648
|
+
} catch (error) {
|
|
2649
|
+
throw this.#markUnusable(error);
|
|
2650
|
+
} finally {
|
|
2651
|
+
this.#busy = false;
|
|
2652
|
+
}
|
|
2653
|
+
this.#transport.log.emit("info", "sabre.session.close", { pseudoCityCode: this.#pcc });
|
|
2654
|
+
}
|
|
2655
|
+
/** Un error de sesión deja la sesión inutilizable. No se reabre sola: el cursor ya se perdió. */
|
|
2656
|
+
#markUnusable(error) {
|
|
2657
|
+
const mapped = toSessionError(error);
|
|
2658
|
+
if (mapped instanceof SabreSessionError) {
|
|
2659
|
+
this.#unusable = mapped;
|
|
2660
|
+
this.#stopKeepAlive();
|
|
2661
|
+
}
|
|
2662
|
+
return mapped;
|
|
2663
|
+
}
|
|
2664
|
+
#touch() {
|
|
2665
|
+
this.#lastActivity = Date.now();
|
|
2666
|
+
}
|
|
2667
|
+
#envelopeFields() {
|
|
2668
|
+
return { cpaId: this.#pcc, conversationId: this.#conversationId, security: this.#security };
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Un solo timer, que se reprograma según `idleMs`: cada respuesta de Sabre corre el ping. Si el
|
|
2672
|
+
* ping falla (ya con los reintentos del transporte), se avisa con un `warn` y `keepAlive` se
|
|
2673
|
+
* apaga: reintentar sin fin no tiene sentido con una sesión que vence a los 15 minutos.
|
|
2674
|
+
*/
|
|
2675
|
+
#scheduleKeepAlive(delayMs) {
|
|
2676
|
+
const timer = setTimeout(() => this.#onKeepAlive(), delayMs);
|
|
2677
|
+
timer.unref?.();
|
|
2678
|
+
this.#keepAliveTimer = timer;
|
|
2679
|
+
}
|
|
2680
|
+
#onKeepAlive() {
|
|
2681
|
+
this.#keepAliveTimer = void 0;
|
|
2682
|
+
if (this.#closing !== void 0 || this.#unusable !== void 0) return;
|
|
2683
|
+
if (this.#busy) {
|
|
2684
|
+
this.#scheduleKeepAlive(KEEP_ALIVE_IDLE_MS);
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
const remaining = KEEP_ALIVE_IDLE_MS - this.idleMs;
|
|
2688
|
+
if (remaining > 0) {
|
|
2689
|
+
this.#scheduleKeepAlive(remaining);
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
this.#keepAlivePing = this.#send(pingRequest()).then(
|
|
2693
|
+
() => {
|
|
2694
|
+
if (this.#closing === void 0 && this.#unusable === void 0) this.#scheduleKeepAlive(KEEP_ALIVE_IDLE_MS);
|
|
2695
|
+
},
|
|
2696
|
+
(error) => {
|
|
2697
|
+
this.#transport.log.emit("warn", "sabre.session.keepAliveFailed", { error });
|
|
2698
|
+
}
|
|
2699
|
+
);
|
|
2700
|
+
void this.#keepAlivePing.finally(() => {
|
|
2701
|
+
this.#keepAlivePing = void 0;
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
#stopKeepAlive() {
|
|
2705
|
+
if (this.#keepAliveTimer !== void 0) clearTimeout(this.#keepAliveTimer);
|
|
2706
|
+
this.#keepAliveTimer = void 0;
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
function pingRequest() {
|
|
2710
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
2711
|
+
return {
|
|
2712
|
+
service: "ping",
|
|
2713
|
+
action: "OTA_PingRQ",
|
|
2714
|
+
payload: `<OTA_PingRQ xmlns="${OTA_PING_NAMESPACE}" TimeStamp="${timestamp}" Version="1.0.0"><EchoData>sabre-client</EchoData></OTA_PingRQ>`,
|
|
2715
|
+
idempotent: true,
|
|
2716
|
+
operation: "ping"
|
|
1249
2717
|
};
|
|
1250
2718
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
2719
|
+
var OTA_PING_NAMESPACE = "http://www.opentravel.org/OTA/2003/05";
|
|
2720
|
+
function checkPingResponse(response) {
|
|
2721
|
+
const errors = asArray(response.payload.Errors)[0];
|
|
2722
|
+
const error = asArray(errors?.Error)[0];
|
|
2723
|
+
if (error !== void 0) {
|
|
2724
|
+
const type = error["@Type"];
|
|
2725
|
+
const text = textOf(error)?.trim();
|
|
2726
|
+
throw new SabreGatewayError(`ping: OTA_PingRS con error${text ? `: ${text}` : ""}`, {
|
|
2727
|
+
protocol: "soap",
|
|
2728
|
+
status: response.status,
|
|
2729
|
+
...typeof type === "string" && type !== "" && { code: type },
|
|
2730
|
+
retryable: false,
|
|
2731
|
+
raw: response.raw
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
if (response.payloadName !== "OTA_PingRS" || !("Success" in response.payload)) {
|
|
2735
|
+
throw new SabreGatewayError("ping: respuesta no reconocida (sin OTA_PingRS/Success)", {
|
|
2736
|
+
protocol: "soap",
|
|
2737
|
+
status: response.status,
|
|
2738
|
+
retryable: false,
|
|
2739
|
+
raw: response.raw
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
function reachedSabre(error) {
|
|
2744
|
+
if (!(error instanceof SabreError)) return false;
|
|
2745
|
+
return !(error instanceof SabreGatewayError && error.status === void 0);
|
|
2746
|
+
}
|
|
2747
|
+
function installAsyncDispose(target, symbol) {
|
|
2748
|
+
if (symbol === void 0) return;
|
|
2749
|
+
Object.defineProperty(target, symbol, {
|
|
2750
|
+
value() {
|
|
2751
|
+
return this.close();
|
|
2752
|
+
},
|
|
2753
|
+
configurable: true,
|
|
2754
|
+
writable: true
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
function sessionCreatePayload(credentials) {
|
|
2758
|
+
const pos = `<POS><Source PseudoCityCode="${xmlEscape(credentials.organization)}"/></POS>`;
|
|
2759
|
+
return credentials.version === "2.0.0" ? `<SessionCreateRQ xmlns="${SWS_NAMESPACE}" Version="2.0.0">${pos}</SessionCreateRQ>` : `<SessionCreateRQ xmlns="${OTA_NAMESPACE}" Version="1.0.0" returnContextID="true">${pos}</SessionCreateRQ>`;
|
|
2760
|
+
}
|
|
2761
|
+
function usernameToken(credentials) {
|
|
2762
|
+
const { username, password, organization, domain, clientId, clientSecret } = credentials;
|
|
2763
|
+
return {
|
|
2764
|
+
usernameToken: {
|
|
2765
|
+
username,
|
|
2766
|
+
password,
|
|
2767
|
+
organization,
|
|
2768
|
+
domain,
|
|
2769
|
+
...clientId === void 0 ? {} : { clientId },
|
|
2770
|
+
...clientSecret === void 0 ? {} : { clientSecret }
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
async function openSession(transport, credentials, options = {}) {
|
|
2775
|
+
const { keepAlive = false, ...requestOptions } = options;
|
|
2776
|
+
if (typeof keepAlive !== "boolean") fail4("keepAlive debe ser un boolean");
|
|
2777
|
+
const conversationId = globalThis.crypto.randomUUID();
|
|
2778
|
+
const request = {
|
|
2779
|
+
service: "sessionCreate",
|
|
2780
|
+
action: "SessionCreateRQ",
|
|
2781
|
+
payload: sessionCreatePayload(credentials),
|
|
2782
|
+
cpaId: credentials.organization,
|
|
2783
|
+
conversationId,
|
|
2784
|
+
security: usernameToken(credentials),
|
|
2785
|
+
idempotent: false,
|
|
2786
|
+
readSecurityToken: true,
|
|
2787
|
+
operation: "openSession",
|
|
2788
|
+
...requestOptions
|
|
2789
|
+
};
|
|
2790
|
+
const response = await transport.request(request).catch((error) => {
|
|
2791
|
+
throw toSessionError(error, OPEN_ERROR_KINDS);
|
|
2792
|
+
});
|
|
2793
|
+
const status = response.payload["@status"];
|
|
2794
|
+
if (typeof status === "string" && status !== "Approved") {
|
|
2795
|
+
throw new SabreGatewayError(`sessionCreate: SessionCreateRS con status ${status}`, {
|
|
2796
|
+
protocol: "soap",
|
|
2797
|
+
status: response.status,
|
|
2798
|
+
retryable: false
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
if (response.securityToken === void 0) {
|
|
2802
|
+
throw new SabreGatewayError("sessionCreate: la respuesta no trae BinarySecurityToken", {
|
|
2803
|
+
protocol: "soap",
|
|
2804
|
+
status: response.status,
|
|
2805
|
+
retryable: false
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
transport.log.emit("info", "sabre.session.open", {
|
|
2809
|
+
version: credentials.version,
|
|
2810
|
+
pseudoCityCode: credentials.organization
|
|
2811
|
+
});
|
|
2812
|
+
return new SabreSession({
|
|
2813
|
+
transport,
|
|
2814
|
+
pcc: credentials.organization,
|
|
2815
|
+
conversationId,
|
|
2816
|
+
token: response.securityToken,
|
|
2817
|
+
keepAlive
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// src/sabre-legacy.ts
|
|
2822
|
+
var SabreLegacy = class {
|
|
2823
|
+
#config;
|
|
2824
|
+
/** Usuario, PCC, domain y versión de `SessionCreateRQ`, validados al crear el cliente. */
|
|
2825
|
+
#session;
|
|
2826
|
+
#transport;
|
|
2827
|
+
/**
|
|
2828
|
+
* Recibe la misma config que `Sabre`. Usuario, PCC y domain de la sesión salen de
|
|
2829
|
+
* `credentials.username` (`V1:<user>:<PCC>:<domain>` o `<user>-<PCC>-<domain>`), y la versión
|
|
2830
|
+
* de `SessionCreateRQ` de las credenciales: 2.0.0 con `clientId` y `clientSecret`, 1.0.0 sin
|
|
2831
|
+
* ellos. Una config inválida lanza `TypeError` aquí, sin llamar a la red.
|
|
2832
|
+
*/
|
|
2833
|
+
constructor(config) {
|
|
2834
|
+
this.#config = resolveConfig(config);
|
|
2835
|
+
this.#session = Object.freeze(resolveSessionCredentials(this.#config.credentials));
|
|
2836
|
+
this.#transport = new SoapTransport(this.#config);
|
|
2837
|
+
}
|
|
2838
|
+
get environment() {
|
|
2839
|
+
return this.#config.environment;
|
|
2840
|
+
}
|
|
2841
|
+
/** Endpoint SOAP del entorno. */
|
|
2842
|
+
get baseUrl() {
|
|
2843
|
+
return this.#config.soapBaseUrl;
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Abre una sesión SOAP. Quien la abre la cierra: `try { … } finally { await session.close() }`.
|
|
2847
|
+
* Para el caso común, `withSession` la cierra sola. Con `keepAlive: true`, manda un ping a los
|
|
2848
|
+
* 13 minutos de inactividad.
|
|
2849
|
+
*/
|
|
2850
|
+
openSession(options = {}) {
|
|
2851
|
+
return openSession(this.#transport, this.#session, options);
|
|
2852
|
+
}
|
|
2853
|
+
/**
|
|
2854
|
+
* Abre una sesión, se la presta al callback y la cierra en un `finally`. Devuelve lo mismo que
|
|
2855
|
+
* el callback y propaga su error. Si el cierre falla, emite `sabre.session.closeFailed` en
|
|
2856
|
+
* `warn` y no pisa el resultado ni el error del callback.
|
|
2857
|
+
*/
|
|
2858
|
+
async withSession(callback) {
|
|
2859
|
+
const session = await this.openSession();
|
|
2860
|
+
let result;
|
|
2861
|
+
try {
|
|
2862
|
+
result = await callback(session);
|
|
2863
|
+
} catch (error) {
|
|
2864
|
+
await this.#closeQuietly(session);
|
|
2865
|
+
throw error;
|
|
2866
|
+
}
|
|
2867
|
+
await this.#closeQuietly(session);
|
|
2868
|
+
return result;
|
|
2869
|
+
}
|
|
2870
|
+
async #closeQuietly(session) {
|
|
2871
|
+
try {
|
|
2872
|
+
await session.close();
|
|
2873
|
+
} catch (error) {
|
|
2874
|
+
this.#transport.log.emit("warn", "sabre.session.closeFailed", { error });
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
};
|
|
2878
|
+
|
|
2879
|
+
// src/queue/host-date.ts
|
|
2880
|
+
var MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
|
|
2881
|
+
var DAY_MS = 864e5;
|
|
2882
|
+
var HALF_YEAR_DAYS = 184;
|
|
2883
|
+
function resolveHostDate(text, referenceDate, options = {}) {
|
|
2884
|
+
const reference = referenceDay(referenceDate);
|
|
2885
|
+
const direction = options.direction ?? "past";
|
|
2886
|
+
if (direction !== "past" && direction !== "nearest") {
|
|
2887
|
+
throw new TypeError("sabre-client: direction debe ser 'past' o 'nearest'");
|
|
2888
|
+
}
|
|
2889
|
+
if (typeof text !== "string") return void 0;
|
|
2890
|
+
const parsed = parseHostDate(text.trim().toUpperCase());
|
|
2891
|
+
if (parsed === void 0) return void 0;
|
|
2892
|
+
const year = parsed.year ?? resolveYear(parsed.month, parsed.day, reference, direction);
|
|
2893
|
+
if (year === void 0 || !isValidDate(year, parsed.month, parsed.day)) return void 0;
|
|
2894
|
+
const date = `${pad(year, 4)}-${pad(parsed.month, 2)}-${pad(parsed.day, 2)}`;
|
|
2895
|
+
return parsed.time === void 0 ? date : `${date}T${parsed.time}`;
|
|
2896
|
+
}
|
|
2897
|
+
function parseHostDate(text) {
|
|
2898
|
+
let match = /^(\d{1,2})([A-Z]{3})(\d{2})?$/.exec(text);
|
|
2899
|
+
if (match !== null) {
|
|
2900
|
+
const month2 = MONTHS.indexOf(match[2]) + 1;
|
|
2901
|
+
if (month2 === 0) return void 0;
|
|
2902
|
+
return {
|
|
2903
|
+
...match[3] !== void 0 && { year: 2e3 + Number(match[3]) },
|
|
2904
|
+
month: month2,
|
|
2905
|
+
day: Number(match[1])
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
match = /^(?:(\d{4})-)?(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?$/.exec(text);
|
|
2909
|
+
if (match === null) return void 0;
|
|
2910
|
+
const [, year, month, day, hours, minutes] = match;
|
|
2911
|
+
if (hours !== void 0 && (Number(hours) > 23 || Number(minutes) > 59)) return void 0;
|
|
2912
|
+
return {
|
|
2913
|
+
...year !== void 0 && { year: Number(year) },
|
|
2914
|
+
month: Number(month),
|
|
2915
|
+
day: Number(day),
|
|
2916
|
+
...hours !== void 0 && { time: `${hours}:${minutes}` }
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
function resolveYear(month, day, reference, direction) {
|
|
2920
|
+
const year = new Date(reference).getUTCFullYear();
|
|
2921
|
+
if (direction === "past") {
|
|
2922
|
+
for (let candidate = year; candidate >= year - 8; candidate--) {
|
|
2923
|
+
if (isValidDate(candidate, month, day) && dayNumber(candidate, month, day) <= reference) return candidate;
|
|
2924
|
+
}
|
|
2925
|
+
return void 0;
|
|
2926
|
+
}
|
|
2927
|
+
let best;
|
|
2928
|
+
for (const candidate of [year + 1, year, year - 1]) {
|
|
2929
|
+
if (!isValidDate(candidate, month, day)) continue;
|
|
2930
|
+
const distance = Math.abs(dayNumber(candidate, month, day) - reference);
|
|
2931
|
+
if (best === void 0 || distance < best.distance) best = { year: candidate, distance };
|
|
2932
|
+
}
|
|
2933
|
+
return best !== void 0 && best.distance <= HALF_YEAR_DAYS * DAY_MS ? best.year : void 0;
|
|
2934
|
+
}
|
|
2935
|
+
function referenceDay(referenceDate) {
|
|
2936
|
+
if (referenceDate instanceof Date) {
|
|
2937
|
+
const time = referenceDate.getTime();
|
|
2938
|
+
if (Number.isNaN(time)) throw new TypeError("sabre-client: referenceDate es un Date inv\xE1lido");
|
|
2939
|
+
return time - (time % DAY_MS + DAY_MS) % DAY_MS;
|
|
2940
|
+
}
|
|
2941
|
+
if (typeof referenceDate === "string") {
|
|
2942
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})(?:T.*)?$/.exec(referenceDate.trim());
|
|
2943
|
+
if (match !== null) {
|
|
2944
|
+
const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
2945
|
+
if (isValidDate(year, month, day)) return dayNumber(year, month, day);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
throw new TypeError("sabre-client: referenceDate es obligatoria, como 'YYYY-MM-DD' o un Date v\xE1lido");
|
|
2949
|
+
}
|
|
2950
|
+
function dayNumber(year, month, day) {
|
|
2951
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
2952
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
2953
|
+
return date.getTime();
|
|
2954
|
+
}
|
|
2955
|
+
function isValidDate(year, month, day) {
|
|
2956
|
+
if (month < 1 || month > 12 || day < 1) return false;
|
|
2957
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
2958
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
2959
|
+
return date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
2960
|
+
}
|
|
2961
|
+
function pad(value, length) {
|
|
2962
|
+
return String(value).padStart(length, "0");
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// src/queue/queue-paragraph.ts
|
|
2966
|
+
var MESSAGE = /^\s*(\d{3})\s{2,}(\S.*?)\s*$/;
|
|
2967
|
+
var PASSENGER_LINE = /^\s*\d+\.\d+[A-Z]/;
|
|
2968
|
+
var PASSENGER_SPLIT = /\s+(?=\d+\.\d+[A-Z])/;
|
|
2969
|
+
var PASSENGER = /^(\d+\.\d+)([^/*]+?)\s*(?:\/([^*]*?))?\s*(?:\*(.*?))?\s*$/;
|
|
2970
|
+
var AIR_SEGMENT = /^\s*(\d{1,2})\s+([A-Z0-9]{2})\s*(\d{1,4})([A-Z])\s+(\d{2}[A-Z]{3})\s+[A-Z0-9]\s+([A-Z]{3})([A-Z]{3})\s+([A-Z]{2})(\d{1,3})(?=\s|$)(.*)$/;
|
|
2971
|
+
var SEGMENT_TIMES = /^\s+(\d{3,4}[APNM]?)\s+(\d{3,4}[APNM]?)(?=\s|$)/;
|
|
2972
|
+
var OTHER_SEGMENT = /^\s*(\d{1,2})\s+([A-Z]{3})\s+([A-Z0-9]{2})\s+(\d{2}[A-Z]{3})\s+[A-Z0-9]\s+([A-Z]{2})(\d{1,3})(?=\s|$)/;
|
|
2973
|
+
var NO_ITINERARY = /^\s*NO ITIN\s*$/;
|
|
2974
|
+
var TICKETING_HEADER = /^\s*TKT\/TIME LIMIT\s*$/;
|
|
2975
|
+
var TICKETING_ENTRY = /^\s*(\d+)\.(.*?)\s*$/;
|
|
2976
|
+
var TICKETED = /^T-(\d{2}[A-Z]{3})?(?:-([A-Z0-9]{3,4})\*([A-Z0-9]+))?(?:\/.*)?$/;
|
|
2977
|
+
var ARRANGEMENT = /^(TA[WCMTX])([A-Z0-9]{3,4})?(\d{2}[A-Z]{3})?\d{0,3}\/.*$/;
|
|
2978
|
+
var TICKET = /^(TE|TK)\s+(\d{13})\b/;
|
|
2979
|
+
var RECEIVED = /^\s*RECEIVED FROM -\s?(.*?)\s*$/;
|
|
2980
|
+
var SIGNATURE = /^\s*([A-Z0-9]{3,4})\.(\S+)\s+(\d{2})(\d{2})\/(\d{2}[A-Z]{3}\d{2})\s+([A-Z0-9]{6})\s+H\s*$/;
|
|
2981
|
+
var Phase = { Messages: 0, Passengers: 1, Segments: 2, Body: 3 };
|
|
2982
|
+
function parseQueueParagraph(lines, options) {
|
|
2983
|
+
if (!Array.isArray(lines)) throw new TypeError("sabre-client: lines debe ser un array de strings");
|
|
2984
|
+
if (typeof options !== "object" || options === null) {
|
|
2985
|
+
throw new TypeError("sabre-client: referenceDate es obligatoria, como 'YYYY-MM-DD' o un Date v\xE1lido");
|
|
2986
|
+
}
|
|
2987
|
+
const { referenceDate } = options;
|
|
2988
|
+
referenceDay(referenceDate);
|
|
2989
|
+
const result = { messages: [], passengers: [], segments: [], unparsed: [] };
|
|
2990
|
+
const signatureIndex = lastSignatureIndex(lines);
|
|
2991
|
+
let phase = Phase.Messages;
|
|
2992
|
+
let inTicketing = false;
|
|
2993
|
+
lines.forEach((value, index) => {
|
|
2994
|
+
const line = typeof value === "string" ? value : "";
|
|
2995
|
+
if (line.trim() === "") return;
|
|
2996
|
+
if (index === signatureIndex) {
|
|
2997
|
+
result.receivedFrom = { ...result.receivedFrom, ...parseSignature(line, referenceDate) };
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
if (inTicketing) {
|
|
3001
|
+
const entry = TICKETING_ENTRY.exec(line);
|
|
3002
|
+
if (entry !== null) {
|
|
3003
|
+
const parsed = parseTicketingEntry(Number(entry[1]), entry[2], referenceDate);
|
|
3004
|
+
if (parsed === void 0) result.unparsed.push(line);
|
|
3005
|
+
else result.ticketing.entries.push(parsed);
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
inTicketing = false;
|
|
3009
|
+
}
|
|
3010
|
+
if (TICKETING_HEADER.test(line)) {
|
|
3011
|
+
phase = Phase.Body;
|
|
3012
|
+
inTicketing = true;
|
|
3013
|
+
result.ticketing ??= { entries: [], issued: false };
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
const received = RECEIVED.exec(line);
|
|
3017
|
+
if (received !== null) {
|
|
3018
|
+
phase = Phase.Body;
|
|
3019
|
+
result.receivedFrom = { ...result.receivedFrom, ...received[1] !== "" && { name: received[1] } };
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
if (phase === Phase.Messages) {
|
|
3023
|
+
const message = MESSAGE.exec(line);
|
|
3024
|
+
if (message !== null) {
|
|
3025
|
+
result.messages.push({ instructionCode: Number(message[1]), text: message[2] });
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
if (phase <= Phase.Passengers && PASSENGER_LINE.test(line)) {
|
|
3030
|
+
const passengers = parsePassengers(line);
|
|
3031
|
+
if (passengers !== void 0) {
|
|
3032
|
+
phase = Phase.Passengers;
|
|
3033
|
+
result.passengers.push(...passengers);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
if (phase <= Phase.Segments) {
|
|
3038
|
+
if (NO_ITINERARY.test(line)) {
|
|
3039
|
+
phase = Phase.Segments;
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
const segment = parseSegment(line, referenceDate);
|
|
3043
|
+
if (segment !== void 0) {
|
|
3044
|
+
phase = Phase.Segments;
|
|
3045
|
+
result.segments.push(segment);
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
result.unparsed.push(line);
|
|
3050
|
+
});
|
|
3051
|
+
if (result.ticketing !== void 0) {
|
|
3052
|
+
result.ticketing.issued = result.ticketing.entries.some((entry) => entry.ticketNumber !== void 0);
|
|
3053
|
+
}
|
|
3054
|
+
return result;
|
|
3055
|
+
}
|
|
3056
|
+
function parsePassengers(line) {
|
|
3057
|
+
const passengers = [];
|
|
3058
|
+
for (const part of line.trim().split(PASSENGER_SPLIT)) {
|
|
3059
|
+
const match = PASSENGER.exec(part);
|
|
3060
|
+
if (match === null) return void 0;
|
|
3061
|
+
const [, nameNumber, surname, givenName, nameRemark] = match;
|
|
3062
|
+
passengers.push({
|
|
3063
|
+
nameNumber,
|
|
3064
|
+
surname: surname.trim(),
|
|
3065
|
+
...givenName !== void 0 && givenName !== "" && { givenName },
|
|
3066
|
+
...nameRemark !== void 0 && nameRemark !== "" && { nameRemark },
|
|
3067
|
+
text: part.slice(nameNumber.length).trim()
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
return passengers;
|
|
3071
|
+
}
|
|
3072
|
+
function parseSegment(line, referenceDate) {
|
|
3073
|
+
const air = AIR_SEGMENT.exec(line);
|
|
3074
|
+
if (air !== null) {
|
|
3075
|
+
const [, number2, airlineCode, flightNumber, bookingClass, date2, from, to, status2, quantity2, rest] = air;
|
|
3076
|
+
const times = SEGMENT_TIMES.exec(rest);
|
|
3077
|
+
const departureTime = times === null ? void 0 : hostTime(times[1]);
|
|
3078
|
+
const arrivalTime = times === null ? void 0 : hostTime(times[2]);
|
|
3079
|
+
return {
|
|
3080
|
+
kind: "air",
|
|
3081
|
+
number: Number(number2),
|
|
3082
|
+
...withDate(date2, referenceDate),
|
|
3083
|
+
status: status2,
|
|
3084
|
+
quantity: Number(quantity2),
|
|
3085
|
+
airlineCode,
|
|
3086
|
+
flightNumber: Number(flightNumber),
|
|
3087
|
+
bookingClass,
|
|
3088
|
+
fromAirportCode: from,
|
|
3089
|
+
toAirportCode: to,
|
|
3090
|
+
...departureTime !== void 0 && { departureTime },
|
|
3091
|
+
...arrivalTime !== void 0 && { arrivalTime },
|
|
3092
|
+
text: line.trim()
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
const other = OTHER_SEGMENT.exec(line);
|
|
3096
|
+
if (other === null) return void 0;
|
|
3097
|
+
const [, number, segmentType, vendorCode, date, status, quantity] = other;
|
|
3098
|
+
return {
|
|
3099
|
+
kind: "other",
|
|
3100
|
+
number: Number(number),
|
|
3101
|
+
...withDate(date, referenceDate),
|
|
3102
|
+
status,
|
|
3103
|
+
quantity: Number(quantity),
|
|
3104
|
+
segmentType,
|
|
3105
|
+
vendorCode,
|
|
3106
|
+
text: line.trim()
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
function parseTicketingEntry(number, text, referenceDate) {
|
|
3110
|
+
const ticketed = TICKETED.exec(text);
|
|
3111
|
+
if (ticketed !== null) {
|
|
3112
|
+
const [, date, pseudoCityCode, agentSine] = ticketed;
|
|
3113
|
+
return {
|
|
3114
|
+
number,
|
|
3115
|
+
code: "T-",
|
|
3116
|
+
...date !== void 0 && withDate(date, referenceDate),
|
|
3117
|
+
...pseudoCityCode !== void 0 && { pseudoCityCode },
|
|
3118
|
+
...agentSine !== void 0 && { agentSine },
|
|
3119
|
+
text
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
const arrangement = ARRANGEMENT.exec(text);
|
|
3123
|
+
if (arrangement !== null) {
|
|
3124
|
+
const [, code, pseudoCityCode, date] = arrangement;
|
|
3125
|
+
return {
|
|
3126
|
+
number,
|
|
3127
|
+
code,
|
|
3128
|
+
...date !== void 0 && withDate(date, referenceDate),
|
|
3129
|
+
...pseudoCityCode !== void 0 && { pseudoCityCode },
|
|
3130
|
+
text
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
const ticket = TICKET.exec(text);
|
|
3134
|
+
if (ticket !== null) return { number, code: ticket[1], ticketNumber: ticket[2], text };
|
|
3135
|
+
return void 0;
|
|
3136
|
+
}
|
|
3137
|
+
function lastSignatureIndex(lines) {
|
|
3138
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
3139
|
+
const line = lines[index];
|
|
3140
|
+
if (typeof line === "string" && SIGNATURE.test(line)) return index;
|
|
3141
|
+
}
|
|
3142
|
+
return -1;
|
|
3143
|
+
}
|
|
3144
|
+
function parseSignature(line, referenceDate) {
|
|
3145
|
+
const [, pseudoCityCode, sign, hours, minutes, date, locator] = SIGNATURE.exec(line);
|
|
3146
|
+
const agentSine = /\*([A-Z0-9]+)$/.exec(sign)?.[1];
|
|
3147
|
+
const day = Number(hours) <= 23 && Number(minutes) <= 59 ? resolveHostDate(date, referenceDate) : void 0;
|
|
3148
|
+
return {
|
|
3149
|
+
pseudoCityCode,
|
|
3150
|
+
...agentSine !== void 0 && { agentSine },
|
|
3151
|
+
...day !== void 0 && { receivedAt: `${day}T${hours}:${minutes}` },
|
|
3152
|
+
locator
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
function withDate(text, referenceDate) {
|
|
3156
|
+
const date = resolveHostDate(text, referenceDate, { direction: "nearest" });
|
|
3157
|
+
return date === void 0 ? {} : { date };
|
|
3158
|
+
}
|
|
3159
|
+
function hostTime(text) {
|
|
3160
|
+
const match = /^(\d{1,2})(\d{2})([APNM])?$/.exec(text);
|
|
3161
|
+
if (match === null) return void 0;
|
|
3162
|
+
let hours = Number(match[1]);
|
|
3163
|
+
const minutes = Number(match[2]);
|
|
3164
|
+
const suffix = match[3];
|
|
3165
|
+
if (minutes > 59) return void 0;
|
|
3166
|
+
if (suffix === void 0) {
|
|
3167
|
+
if (hours > 23) return void 0;
|
|
3168
|
+
} else {
|
|
3169
|
+
if (hours < 1 || hours > 12) return void 0;
|
|
3170
|
+
if (suffix === "A" || suffix === "M") hours %= 12;
|
|
3171
|
+
else if (suffix === "P") hours = hours % 12 + 12;
|
|
3172
|
+
else if (hours !== 12) return void 0;
|
|
3173
|
+
}
|
|
3174
|
+
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// src/air/seat-availability-request.ts
|
|
3178
|
+
function buildSeatAvailabilityRequest(source) {
|
|
3179
|
+
const { group, itinerary, segments: only, currencyCode } = source;
|
|
3180
|
+
const flights = itineraryFlights(group, itinerary);
|
|
3181
|
+
const segments = flights.map(
|
|
3182
|
+
({ schedule, departureDate, arrivalDate, bookingCode }, i) => {
|
|
3183
|
+
const { carrier } = schedule;
|
|
3184
|
+
const operating = carrier.operating !== void 0 && (carrier.operating !== carrier.marketing || carrier.operatingFlightNumber !== void 0 && carrier.operatingFlightNumber !== carrier.marketingFlightNumber);
|
|
3185
|
+
return {
|
|
3186
|
+
id: segmentId(i),
|
|
3187
|
+
bookingAirlineCode: carrier.marketing,
|
|
3188
|
+
bookingFlightNumber: carrier.marketingFlightNumber,
|
|
3189
|
+
departureAirportCode: schedule.departure.airport,
|
|
3190
|
+
arrivalAirportCode: schedule.arrival.airport,
|
|
3191
|
+
departureDate,
|
|
3192
|
+
departureTime: schedule.departure.time.slice(0, 5),
|
|
3193
|
+
arrivalDate,
|
|
3194
|
+
arrivalTime: schedule.arrival.time.slice(0, 5),
|
|
3195
|
+
bookingClassCode: bookingCode,
|
|
3196
|
+
...operating ? { operatingAirlineCode: carrier.operating } : {},
|
|
3197
|
+
...operating && carrier.operatingFlightNumber !== void 0 ? { operatingFlightNumber: carrier.operatingFlightNumber } : {}
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
);
|
|
3201
|
+
const fare = itinerary.pricingInformation[0]?.fare;
|
|
3202
|
+
if (fare === void 0) {
|
|
3203
|
+
throw new TypeError("sabre-client: el itinerario no trae pricingInformation");
|
|
3204
|
+
}
|
|
3205
|
+
const fareComponents = /* @__PURE__ */ new Map();
|
|
3206
|
+
const passengers = [];
|
|
3207
|
+
for (const { passengerInfo } of fare.passengerInfoList) {
|
|
3208
|
+
const fareSegmentAssociation = [];
|
|
3209
|
+
let next = 0;
|
|
3210
|
+
for (const component of passengerInfo.fareComponents ?? []) {
|
|
3211
|
+
const fareComponentId = `FC${component.id}`;
|
|
3212
|
+
if (!fareComponents.has(fareComponentId)) fareComponents.set(fareComponentId, fareComponent(fareComponentId, component));
|
|
3213
|
+
for (const { segment } of component.segments ?? []) {
|
|
3214
|
+
const flight2 = segments[next];
|
|
3215
|
+
if (flight2 === void 0) break;
|
|
3216
|
+
if (flight2.cabinCode === void 0 && segment.cabinCode !== void 0) flight2.cabinCode = segment.cabinCode;
|
|
3217
|
+
fareSegmentAssociation.push({ segmentRef: flight2.id, fareComponentRef: fareComponentId });
|
|
3218
|
+
next++;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
for (let n = 0; n < (passengerInfo.passengerNumber ?? 1); n++) {
|
|
3222
|
+
passengers.push({
|
|
3223
|
+
id: `P${passengers.length + 1}`,
|
|
3224
|
+
passengerType: passengerInfo.passengerType,
|
|
3225
|
+
...fareSegmentAssociation.length === 0 ? {} : { fareSegmentAssociation }
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
const refs = (only ?? segments.map((_s, i) => i + 1)).map((n) => {
|
|
3230
|
+
if (!Number.isInteger(n) || n < 1 || n > segments.length) {
|
|
3231
|
+
throw new TypeError(`sabre-client: segments pide el vuelo ${n}, y hay ${segments.length} (empieza en 1)`);
|
|
3232
|
+
}
|
|
3233
|
+
return segmentId(n - 1);
|
|
3234
|
+
});
|
|
3235
|
+
if (refs.length === 0) {
|
|
3236
|
+
throw new TypeError("sabre-client: segments no puede ir vac\xEDo");
|
|
3237
|
+
}
|
|
3238
|
+
return {
|
|
3239
|
+
returnSeatMapsOnlyForSegmentRefs: refs,
|
|
3240
|
+
segments,
|
|
3241
|
+
passengers,
|
|
3242
|
+
...fareComponents.size === 0 ? {} : { fareComponents: [...fareComponents.values()] },
|
|
3243
|
+
...currencyCode === void 0 ? {} : { currencyCode }
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
function segmentId(index) {
|
|
3247
|
+
return `S${index + 1}`;
|
|
3248
|
+
}
|
|
3249
|
+
function fareComponent(fareComponentId, component) {
|
|
3250
|
+
const { fareBasisCode, governingCarrier, fareType, fareTariff, fareRule, fareTypeBitmap } = component;
|
|
3251
|
+
const fareDetail = definedOnly({ fareType, fareTariff, fareRule, fareTypeBitmap });
|
|
3252
|
+
return {
|
|
3253
|
+
fareComponentId,
|
|
3254
|
+
fareBasis: fareBasisCode,
|
|
3255
|
+
...governingCarrier === void 0 ? {} : { governingCarrier },
|
|
3256
|
+
...Object.keys(fareDetail).length === 0 ? {} : { fareDetail }
|
|
3257
|
+
};
|
|
1253
3258
|
}
|
|
1254
3259
|
|
|
1255
3260
|
// src/index.ts
|
|
@@ -1259,10 +3264,15 @@ export {
|
|
|
1259
3264
|
SabreBusinessError,
|
|
1260
3265
|
SabreError,
|
|
1261
3266
|
SabreGatewayError,
|
|
3267
|
+
SabreLegacy,
|
|
1262
3268
|
SabreOrchestrationError,
|
|
3269
|
+
SabreSessionError,
|
|
1263
3270
|
SabreWarning,
|
|
1264
3271
|
VERSION,
|
|
1265
3272
|
buildBookingFlightDetails,
|
|
1266
|
-
buildRevalidateItineraryRequest
|
|
3273
|
+
buildRevalidateItineraryRequest,
|
|
3274
|
+
buildSeatAvailabilityRequest,
|
|
3275
|
+
parseQueueParagraph,
|
|
3276
|
+
resolveHostDate
|
|
1267
3277
|
};
|
|
1268
3278
|
//# sourceMappingURL=index.js.map
|