sabre-client 0.3.0-next.1 → 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/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;
@@ -248,6 +256,104 @@ function classifyBargainFinderMax(body) {
248
256
  return classifyItineraryMessages(body, BARGAIN_FINDER_MAX_SERVICE, BARGAIN_FINDER_MAX_CATALOG);
249
257
  }
250
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
+
251
357
  // src/air/itinerary-flights.ts
252
358
  function itineraryFlights(group, itinerary) {
253
359
  const { legDescriptions } = group.groupDescription;
@@ -398,6 +504,15 @@ var Air = class {
398
504
  revalidateItinerary(request, options) {
399
505
  return revalidateItinerary(this.#transport, request, options);
400
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
+ }
401
516
  };
402
517
 
403
518
  // src/logging/redact.ts
@@ -447,6 +562,13 @@ function redactEntries(entries, seen) {
447
562
  }
448
563
  return out;
449
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
+ }
450
572
 
451
573
  // src/logging/logger.ts
452
574
  var EventLogger = class {
@@ -660,42 +782,6 @@ var AuthManager = class {
660
782
  }
661
783
  };
662
784
 
663
- // src/booking/booking-errors.ts
664
- var WARNING = "WARNING";
665
- function responseErrors(response) {
666
- return Array.isArray(response.errors) ? response.errors.filter(isObject2) : [];
667
- }
668
- function isWarning(item) {
669
- return typeof item.category === "string" && item.category.toUpperCase() === WARNING;
670
- }
671
- function toWarning(service, item) {
672
- return new SabreWarning(messageOf(service, item), { service, code: codeOf(item), raw: item });
673
- }
674
- function toBusinessError(service, item, { warnings = [], errors = [], retryable = false } = {}) {
675
- return new SabreBusinessError(messageOf(service, item), {
676
- service,
677
- code: codeOf(item),
678
- raw: item,
679
- warnings,
680
- errors,
681
- retryable
682
- });
683
- }
684
- function codeOf(item) {
685
- return typeof item.type === "string" && item.type !== "" ? item.type : String(item.category);
686
- }
687
- function messageOf(service, item) {
688
- const where = typeof item.fieldPath === "string" && item.fieldPath !== "" ? ` en ${item.fieldPath}` : "";
689
- const description = typeof item.description === "string" && item.description !== "" ? `: ${item.description}` : "";
690
- return `${service}: ${String(item.category)} ${codeOf(item)}${where}${description}`;
691
- }
692
- function nonEmpty(value) {
693
- return typeof value === "string" && value !== "";
694
- }
695
- function isObject2(value) {
696
- return typeof value === "object" && value !== null;
697
- }
698
-
699
785
  // src/booking/cancel-booking.ts
700
786
  var CANCEL_BOOKING_SERVICE = "cancelBooking";
701
787
  var PATH3 = "/v1/trip/orders/cancelBooking";
@@ -763,12 +849,123 @@ function errorOf(item) {
763
849
  return toBusinessError(CANCEL_BOOKING_SERVICE, item);
764
850
  }
765
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
+
766
960
  // src/booking/create-booking.ts
767
961
  var CREATE_BOOKING_SERVICE = "createBooking";
768
962
  var PATH4 = "/v1/trip/orders/createBooking";
769
963
  var UNABLE_TO_RETRIEVE_BOOKING = "UNABLE_TO_RETRIEVE_BOOKING";
770
964
  async function createBooking(transport, request, options = {}) {
771
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 });
968
+ }
772
969
  const body = await transport.request({
773
970
  method: "POST",
774
971
  path: PATH4,
@@ -791,6 +988,16 @@ function validateCreateBookingRequest(request) {
791
988
  "sabre-client: booking.create con agency.ticketingPolicy FUTURE_TICKETING necesita agency.futureTicketingPolicy"
792
989
  );
793
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
+ }
794
1001
  }
795
1002
  function classifyCreateBooking(body) {
796
1003
  if (!isObject2(body)) {
@@ -910,7 +1117,9 @@ var BookingManagement = class {
910
1117
  * Nunca se reintenta automáticamente. Si la reserva se crea con avisos o errores no fatales,
911
1118
  * no lanza: los devuelve en `warnings` y `errors`. Si no se crea, lanza `SabreBusinessError`.
912
1119
  * Con `agency.ticketingPolicy: 'FUTURE_TICKETING'` y sin `agency.futureTicketingPolicy`, lanza
913
- * `TypeError` sin llamar a la red: Sabre lo rechaza seguro.
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`.
914
1123
  */
915
1124
  create(request, options) {
916
1125
  return createBooking(this.#transport, request, options);
@@ -958,12 +1167,30 @@ function validateRetryOptions(retry, where) {
958
1167
  throw new TypeError(`sabre-client: ${where}.baseDelayMs debe ser un n\xFAmero >= 0`);
959
1168
  }
960
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
+ }
961
1184
 
962
1185
  // src/config.ts
963
1186
  var BASE_URLS = Object.freeze({
964
1187
  sandbox: "https://api.cert.platform.sabre.com",
965
1188
  production: "https://api.platform.sabre.com"
966
1189
  });
1190
+ var SOAP_BASE_URLS = Object.freeze({
1191
+ sandbox: "https://webservices.cert.platform.sabre.com",
1192
+ production: "https://webservices.platform.sabre.com"
1193
+ });
967
1194
  var LOGGER_METHODS = ["debug", "info", "warn", "error"];
968
1195
  function fail(message) {
969
1196
  throw new TypeError(`sabre-client: ${message}`);
@@ -1016,6 +1243,7 @@ function resolveConfig(config) {
1016
1243
  credentials: Object.freeze({ ...credentials }),
1017
1244
  environment,
1018
1245
  baseUrl: BASE_URLS[environment],
1246
+ soapBaseUrl: SOAP_BASE_URLS[environment],
1019
1247
  authVersion,
1020
1248
  pcc: config.pcc,
1021
1249
  logger,
@@ -1044,6 +1272,10 @@ var RestTransport = class {
1044
1272
  this.#random = options.random ?? Math.random;
1045
1273
  this.#sleep = options.sleep ?? sleep;
1046
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
+ }
1047
1279
  /** Punto de enganche para `AuthManager`. */
1048
1280
  setAuthorizationProvider(provider) {
1049
1281
  this.#authorization = provider;
@@ -1177,13 +1409,17 @@ function parseBody(text) {
1177
1409
  }
1178
1410
  function gatewayError(service, status, body) {
1179
1411
  const code = extractCode(body);
1180
- return new SabreGatewayError(`${service}: HTTP ${status}${code === void 0 ? "" : ` ${code}`}`, {
1181
- protocol: "rest",
1182
- status,
1183
- retryable: RETRYABLE_STATUS.has(status),
1184
- raw: body,
1185
- ...code === void 0 ? {} : { code }
1186
- });
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
+ );
1187
1423
  }
1188
1424
  function extractCode(body) {
1189
1425
  if (typeof body !== "object" || body === null) return void 0;
@@ -1191,7 +1427,18 @@ function extractCode(body) {
1191
1427
  const value = body[key];
1192
1428
  if (typeof value === "string" && value !== "") return value;
1193
1429
  }
1194
- return void 0;
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;
1195
1442
  }
1196
1443
  function isUnauthorized(error) {
1197
1444
  return error instanceof SabreGatewayError && error.protocol === "rest" && error.status === 401;
@@ -1202,20 +1449,6 @@ function isAbort(error, signal) {
1202
1449
  function describe(error) {
1203
1450
  return error instanceof Error ? error.message : String(error);
1204
1451
  }
1205
- function sleep(ms, signal) {
1206
- return new Promise((resolve, reject) => {
1207
- if (signal?.aborted) return reject(signal.reason);
1208
- const timer = setTimeout(() => {
1209
- signal?.removeEventListener("abort", onAbort);
1210
- resolve();
1211
- }, ms);
1212
- const onAbort = () => {
1213
- clearTimeout(timer);
1214
- reject(signal?.reason);
1215
- };
1216
- signal?.addEventListener("abort", onAbort, { once: true });
1217
- });
1218
- }
1219
1452
 
1220
1453
  // src/sabre.ts
1221
1454
  var Sabre = class {
@@ -1241,54 +1474,1787 @@ var Sabre = class {
1241
1474
  }
1242
1475
  };
1243
1476
 
1244
- // src/booking/flight-details.ts
1245
- function buildBookingFlightDetails(source) {
1246
- const { group, itinerary, priceComparison } = source;
1247
- const flights = itineraryFlights(group, itinerary).map(
1248
- ({ schedule, departureDate, bookingCode }) => ({
1249
- flightNumber: schedule.carrier.marketingFlightNumber,
1250
- airlineCode: schedule.carrier.marketing,
1251
- fromAirportCode: schedule.departure.airport,
1252
- toAirportCode: schedule.arrival.airport,
1253
- departureDate,
1254
- departureTime: schedule.departure.time.slice(0, 5),
1255
- bookingClass: bookingCode,
1256
- flightStatusCode: "NN"
1257
- })
1258
- );
1259
- const fare = itinerary.pricingInformation[0]?.fare;
1260
- if (fare === void 0) {
1261
- throw new TypeError("sabre-client: el itinerario no trae pricingInformation");
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;
1262
1516
  }
1263
- const passengersPricing = fare.passengerInfoList.map(
1264
- ({ passengerInfo }) => ({
1265
- passengerCode: passengerInfo.passengerType,
1266
- numberOfpassengers: passengerInfo.passengerNumber ?? 1
1267
- })
1268
- );
1269
- let priceComparisons;
1270
- if (priceComparison !== void 0) {
1271
- const total = fare.totalFare?.totalPrice;
1272
- if (total === void 0) {
1273
- throw new TypeError("sabre-client: el itinerario no trae totalFare.totalPrice para priceComparison");
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 });
1274
1528
  }
1275
- priceComparisons = [{ desiredAmount: amount(total), ...priceComparison }];
1276
1529
  }
1277
- return {
1278
- flights,
1279
- flightPricing: [
1280
- {
1281
- ...priceComparisons === void 0 ? {} : { priceComparisons },
1282
- qualifiers: {
1283
- passengersPricing,
1284
- ...fare.validatingCarrierCode === void 0 ? {} : { validatingAirlineCode: fare.validatingCarrierCode }
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;
1285
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);
1286
1563
  }
1287
- ]
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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);
2026
+ return {
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 }
1288
2037
  };
1289
2038
  }
1290
- function amount(value) {
1291
- return String(Math.round(value * 1e3) / 1e3);
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" };
2368
+ }
2369
+ const action = await callback(step.item);
2370
+ visited++;
2371
+ step = await this.#processAction(action, options);
2372
+ }
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"
2717
+ };
2718
+ }
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
+ };
1292
3258
  }
1293
3259
 
1294
3260
  // src/index.ts
@@ -1298,10 +3264,15 @@ export {
1298
3264
  SabreBusinessError,
1299
3265
  SabreError,
1300
3266
  SabreGatewayError,
3267
+ SabreLegacy,
1301
3268
  SabreOrchestrationError,
3269
+ SabreSessionError,
1302
3270
  SabreWarning,
1303
3271
  VERSION,
1304
3272
  buildBookingFlightDetails,
1305
- buildRevalidateItineraryRequest
3273
+ buildRevalidateItineraryRequest,
3274
+ buildSeatAvailabilityRequest,
3275
+ parseQueueParagraph,
3276
+ resolveHostDate
1306
3277
  };
1307
3278
  //# sourceMappingURL=index.js.map