sabre-client 0.2.4 → 0.3.0-next.1

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
@@ -1,370 +1,1307 @@
1
- // src/air/search.ts
2
- var Search = class {
3
- /**
4
- * Bargain Finder Max v5
5
- *
6
- * Bargain Finder Max (BFM),
7
- * nuestro producto de búsqueda de tarifas bajas líder en su clase,
8
- * se utiliza para buscar los itinerarios con los precios más bajos disponibles según una fecha específica.
9
- *
10
- * Revalidate Itinerary v5
11
- *
12
- * La API Revalidate Itinerary permite volver a consultar la disponibilidad
13
- * y el precio de una opción de itinerario específica sin tener que reservarla.
14
- * La solución revalida si la opción de itinerario es válida para la compra.
15
- * Revalidate Itinerary no admite contenido NDC.
16
- *
17
- * @param sabre Sabre Client Reference
18
- */
19
- constructor(sabre) {
20
- this.sabre = sabre;
1
+ // src/errors/sabre-error.ts
2
+ var SabreError = class extends Error {
3
+ name = "SabreError";
4
+ retryable;
5
+ constructor(message, options) {
6
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
7
+ this.retryable = options.retryable;
8
+ Object.defineProperty(this, "raw", {
9
+ value: options.raw,
10
+ enumerable: false,
11
+ writable: false,
12
+ configurable: false
13
+ });
21
14
  }
22
- /**
23
- * Crear solicitud de Buscador de ofertas Max.
24
- *
25
- * payload - El mensaje de solicitud de búsqueda de tarifa baja solicita opciones
26
- * de itinerario para vuelos entre pares de ciudades específicos, en fechas específicas,
27
- * para un número y tipo de pasajeros específicos.
28
- * La información opcional de la solicitud puede incluir:
29
- * hora/ventana horaria, ciudades de conexión
30
- * y preferencias del cliente (aerolíneas, cabina, tipo de vuelo, etc.).
31
- * La solicitud de búsqueda de tarifa baja contiene información similar a
32
- * la de una búsqueda de tarifa baja en el CRS o GDS de una aerolínea.
33
- * @param payload BargainFinderMaxOptions
34
- * @returns {Promise<BargainFinderMaxResponseSuccess>}
35
- */
36
- async shop(payload) {
37
- const path = "/v5/offers/shop";
38
- return this.sabre.post(path, payload);
15
+ toJSON() {
16
+ const fields = Object.fromEntries(Object.entries(this));
17
+ return { ...fields, name: this.name, message: this.message };
39
18
  }
40
- /**
41
- * Crear solicitud de revalidación de itinerario.
42
- *
43
- * payload - El mensaje de solicitud de búsqueda de tarifa baja solicita opciones
44
- * de itinerario para vuelos entre pares de ciudades específicos, en fechas específicas,
45
- * para un número y tipo de pasajeros específicos.
46
- * La información opcional de la solicitud puede incluir:
47
- * hora/ventana horaria, ciudades de conexión
48
- * y preferencias del cliente (aerolíneas, cabina, tipo de vuelo, etc.).
49
- * La solicitud de búsqueda de tarifa baja contiene información similar a
50
- * la de una búsqueda de tarifa baja en el CRS o GDS de una aerolínea.
51
- * @param payload RevalidateItineraryOptions
52
- * @returns {Promise<RevalidateItineraryResponseSuccess>}
53
- */
54
- async revalidate(payload) {
55
- const path = "/v5/shop/flights/revalidate";
56
- return this.sabre.post(path, payload);
19
+ };
20
+ var SabreGatewayError = class extends SabreError {
21
+ name = "SabreGatewayError";
22
+ protocol;
23
+ status;
24
+ code;
25
+ constructor(message, options) {
26
+ super(message, options);
27
+ this.protocol = options.protocol;
28
+ this.status = options.status;
29
+ this.code = options.code;
30
+ }
31
+ };
32
+ var SabreOrchestrationError = class extends SabreError {
33
+ name = "SabreOrchestrationError";
34
+ code;
35
+ constructor(message, options) {
36
+ super(message, { ...options, retryable: options.code === "SP.PROVIDER_ERROR" });
37
+ this.code = options.code;
38
+ }
39
+ };
40
+ var SabreBusinessError = class extends SabreError {
41
+ name = "SabreBusinessError";
42
+ service;
43
+ code;
44
+ warnings;
45
+ errors;
46
+ constructor(message, options) {
47
+ super(message, { ...options, retryable: options.retryable ?? false });
48
+ this.service = options.service;
49
+ this.code = options.code;
50
+ this.warnings = options.warnings ?? [];
51
+ this.errors = options.errors ?? [];
52
+ }
53
+ };
54
+ var SabreWarning = class extends SabreError {
55
+ name = "SabreWarning";
56
+ service;
57
+ code;
58
+ constructor(message, options) {
59
+ super(message, { ...options, retryable: false });
60
+ this.service = options.service;
61
+ this.code = options.code;
57
62
  }
58
63
  };
59
64
 
60
- // src/air/ticketing.ts
61
- var Ticketing = class {
62
- /**
63
- * Enhanced Air Ticket v1.3.0
64
- *
65
- * La API de boletos aéreos mejorados se utiliza para emitir uno o varios boletos aéreos
66
- * y/o EMD (documentos electrónicos varios) dentro de una sola llamada API.
67
- *
68
- * @param sabre Sabre Client Reference
69
- */
70
- constructor(sabre) {
71
- this.sabre = sabre;
65
+ // src/air/grouped-itinerary.ts
66
+ function denormalizeGroupedItineraryResponse(raw, service) {
67
+ const resolver = new Resolver(raw, service);
68
+ return {
69
+ version: raw.version,
70
+ messages: raw.messages ?? [],
71
+ ...raw.statistics === void 0 ? {} : { statistics: raw.statistics },
72
+ itineraryGroups: (raw.itineraryGroups ?? []).map((group) => ({
73
+ groupDescription: group.groupDescription,
74
+ itineraries: group.itineraries.map((itinerary) => resolver.itinerary(itinerary))
75
+ }))
76
+ };
77
+ }
78
+ var Resolver = class {
79
+ #service;
80
+ #schedules;
81
+ #legs;
82
+ #taxes;
83
+ #taxSummaries;
84
+ #obFees;
85
+ #fareComponents;
86
+ #validatingCarriers;
87
+ #baggageAllowances;
88
+ constructor(raw, service) {
89
+ this.#service = service;
90
+ this.#schedules = byId(raw.scheduleDescs);
91
+ this.#legs = byId(raw.legDescs);
92
+ this.#taxes = byId(raw.taxDescs);
93
+ this.#taxSummaries = byId(raw.taxSummaryDescs);
94
+ this.#obFees = byId(raw.obFeeDescs);
95
+ this.#fareComponents = byId(raw.fareComponentDescs);
96
+ this.#validatingCarriers = byId(raw.validatingCarrierDescs);
97
+ this.#baggageAllowances = byId(raw.baggageAllowanceDescs);
72
98
  }
73
- /**
74
- * La API de boletos aéreos mejorados (AirTicketRQ)
75
- * se utiliza para emitir boletos aéreos y documentos electrónicos varios (EMD) en una sola solicitud.
76
- *
77
- * payload - Solicitud de API de boletos aéreos orquestada.
78
- * @param payload EnhancedAirTicketOptions
79
- * @returns {Promise<EnhancedAirTicketResponseSuccess>}
80
- */
81
- async issue(payload) {
82
- const path = "/v1.3.0/air/ticket";
83
- return this.sabre.post(path, payload);
99
+ itinerary(itinerary) {
100
+ return {
101
+ ...itinerary,
102
+ legs: itinerary.legs.map((ref) => this.#leg(ref)),
103
+ pricingInformation: itinerary.pricingInformation.map((pricing) => ({
104
+ ...pricing,
105
+ fare: this.#fare(pricing.fare)
106
+ }))
107
+ };
108
+ }
109
+ #leg(ref) {
110
+ const leg = this.#merge(this.#legs, "legDescs", ref);
111
+ return { ...leg, schedules: leg.schedules.map((schedule) => this.#merge(this.#schedules, "scheduleDescs", schedule)) };
112
+ }
113
+ #fare(fare) {
114
+ const { validatingCarriers, ...rest } = fare;
115
+ return {
116
+ ...rest,
117
+ passengerInfoList: fare.passengerInfoList.map(({ passengerInfo, ...entry }) => ({
118
+ ...entry,
119
+ passengerInfo: this.#passengerInfo(passengerInfo)
120
+ })),
121
+ ...validatingCarriers === void 0 ? {} : {
122
+ validatingCarriers: validatingCarriers.map(
123
+ (ref) => this.#merge(this.#validatingCarriers, "validatingCarrierDescs", ref)
124
+ )
125
+ }
126
+ };
127
+ }
128
+ #passengerInfo(info) {
129
+ const { fareComponents, taxes, taxSummaries, obFees, baggageInformation, ...rest } = info;
130
+ const result = { ...rest };
131
+ if (fareComponents !== void 0) result.fareComponents = fareComponents.map((ref) => this.#fareComponent(ref));
132
+ if (taxes !== void 0) result.taxes = taxes.map((ref) => this.#merge(this.#taxes, "taxDescs", ref));
133
+ if (taxSummaries !== void 0) {
134
+ result.taxSummaries = taxSummaries.map((ref) => this.#merge(this.#taxSummaries, "taxSummaryDescs", ref));
135
+ }
136
+ if (obFees !== void 0) result.obFees = obFees.map((ref) => this.#merge(this.#obFees, "obFeeDescs", ref));
137
+ if (baggageInformation !== void 0) {
138
+ result.baggageInformation = baggageInformation.map(({ allowance, ...bag }) => ({
139
+ ...bag,
140
+ ...allowance === void 0 ? {} : { allowance: this.#merge(this.#baggageAllowances, "baggageAllowanceDescs", allowance) }
141
+ }));
142
+ }
143
+ return result;
144
+ }
145
+ #fareComponent(ref) {
146
+ const desc = this.#lookup(this.#fareComponents, "fareComponentDescs", ref.ref);
147
+ const { ref: _ref, segments: siteSegments, ...site } = ref;
148
+ const { segments: descSegments, ...descRest } = desc;
149
+ if (descSegments !== void 0 && siteSegments !== void 0 && descSegments.length !== siteSegments.length) {
150
+ throw this.#malformed(
151
+ `fareComponents con ${siteSegments.length} segmentos y fareComponentDescs ${ref.ref} con ${descSegments.length}`
152
+ );
153
+ }
154
+ const segments = mergeSegments(descSegments, siteSegments);
155
+ return { ...descRest, ...site, ...segments === void 0 ? {} : { segments } };
156
+ }
157
+ /** `{ ref, ...resto }` → `{ ...descriptor, ...resto }`. */
158
+ #merge(index, kind, ref) {
159
+ const { ref: id, ...site } = ref;
160
+ return { ...this.#lookup(index, kind, id), ...site };
161
+ }
162
+ #lookup(index, kind, id) {
163
+ const desc = index.get(id);
164
+ if (desc === void 0) throw this.#malformed(`ref ${id} sin descriptor en ${kind}`);
165
+ return desc;
166
+ }
167
+ #malformed(detail) {
168
+ return new SabreError(`${this.#service}: respuesta malformada, ${detail}`, { retryable: false });
84
169
  }
85
170
  };
171
+ function byId(descs) {
172
+ return new Map((descs ?? []).map((desc) => [desc.id, desc]));
173
+ }
174
+ function mergeSegments(desc, site) {
175
+ if (desc === void 0 && site === void 0) return void 0;
176
+ const length = desc?.length ?? site?.length ?? 0;
177
+ return Array.from({ length }, (_, i) => ({
178
+ segment: { ...desc?.[i]?.segment, ...site?.[i]?.segment }
179
+ }));
180
+ }
86
181
 
87
- // src/authentication/authentication.ts
88
- var grantType = new URLSearchParams({ "grant_type": "client_credentials" }).toString();
89
- var Authentication = class {
90
- constructor(sabre) {
91
- this.sabre = sabre;
182
+ // src/air/itinerary-messages.ts
183
+ function classifyItineraryMessages(body, service, catalog) {
184
+ const response = isObject(body) ? body.groupedItineraryResponse : void 0;
185
+ if (!isObject(response)) {
186
+ return new SabreError(`${service}: respuesta malformada, falta groupedItineraryResponse`, {
187
+ retryable: false,
188
+ raw: body
189
+ });
92
190
  }
93
- /**
94
- * OAuth Token Create REST API v2
95
- * See https://developer.sabre.com/docs/rest_apis/session_management/token_create_api/v2
96
- * @param payload El tipo de concesión utilizado para obtener el token. Sólo se admiten 'client_credentials'.
97
- * @returns TokenResponse
98
- */
99
- async OAuthTokenV2(payload = grantType) {
100
- const data = await this.sabre.auth(
101
- "/v2/auth/token",
102
- payload
191
+ const messages = Array.isArray(response.messages) ? response.messages : [];
192
+ const error = messages.find((message) => message.severity?.toLowerCase() === "error");
193
+ if (error === void 0) return void 0;
194
+ const match = findInCatalog(messages, catalog);
195
+ const text = match?.text ?? (typeof error.text === "string" ? error.text : "");
196
+ const known = match?.entry;
197
+ const code = known?.code ?? error.code ?? text;
198
+ return new SabreBusinessError(`${service}: ${text || code}`, {
199
+ service,
200
+ code,
201
+ retryable: known?.retryable ?? false,
202
+ raw: body
203
+ });
204
+ }
205
+ function findInCatalog(messages, catalog) {
206
+ for (const message of messages) {
207
+ if (typeof message.text !== "string") continue;
208
+ const text = message.text.toLowerCase();
209
+ const entry = catalog.find((candidate) => text.includes(candidate.code.toLowerCase()));
210
+ if (entry !== void 0) return { entry, text: message.text };
211
+ }
212
+ return void 0;
213
+ }
214
+ function isObject(value) {
215
+ return typeof value === "object" && value !== null;
216
+ }
217
+
218
+ // src/air/bargain-finder-max.ts
219
+ var BARGAIN_FINDER_MAX_SERVICE = "bargainFinderMax";
220
+ var PATH = "/v5/offers/shop";
221
+ var BARGAIN_FINDER_MAX_CATALOG = [
222
+ { code: "No Availability", retryable: false },
223
+ { code: "NO FLIGHT SCHEDULES FOR QUALIFIERS USED", retryable: false },
224
+ { code: "Connection reset by peer", retryable: true },
225
+ { code: "Transmission timeout", retryable: true },
226
+ { code: "Insufficient resources", retryable: true },
227
+ { code: "MAX NBR COMBINATIONS EXCEEDED", retryable: true }
228
+ ];
229
+ async function bargainFinderMax(transport, request, options = {}) {
230
+ if (typeof request !== "object" || request === null || typeof request.OTA_AirLowFareSearchRQ !== "object") {
231
+ throw new TypeError("sabre-client: bargainFinderMax espera { OTA_AirLowFareSearchRQ: { ... } }");
232
+ }
233
+ const body = await transport.request({
234
+ method: "POST",
235
+ path: PATH,
236
+ service: BARGAIN_FINDER_MAX_SERVICE,
237
+ // POST, pero de solo lectura: se puede reintentar sin efectos duplicados.
238
+ idempotent: true,
239
+ body: request,
240
+ classify: classifyBargainFinderMax,
241
+ ...options.retry === void 0 ? {} : { retry: options.retry },
242
+ ...options.signal === void 0 ? {} : { signal: options.signal },
243
+ ...options.onRawResponse === void 0 ? {} : { operation: "air.bargainFinderMax", onRawResponse: options.onRawResponse }
244
+ });
245
+ return denormalizeGroupedItineraryResponse(body.groupedItineraryResponse, BARGAIN_FINDER_MAX_SERVICE);
246
+ }
247
+ function classifyBargainFinderMax(body) {
248
+ return classifyItineraryMessages(body, BARGAIN_FINDER_MAX_SERVICE, BARGAIN_FINDER_MAX_CATALOG);
249
+ }
250
+
251
+ // src/air/itinerary-flights.ts
252
+ function itineraryFlights(group, itinerary) {
253
+ const { legDescriptions } = group.groupDescription;
254
+ if (legDescriptions.length !== itinerary.legs.length) {
255
+ throw new TypeError(
256
+ `sabre-client: el itinerario tiene ${itinerary.legs.length} tramos y el grupo ${legDescriptions.length}`
103
257
  );
104
- this.sabre.setAuthorization(data.access_token);
105
- return data;
106
258
  }
107
- };
259
+ const classes = bookingCodes(itinerary);
260
+ const flightCount = itinerary.legs.reduce((n, leg) => n + leg.schedules.length, 0);
261
+ if (classes.length !== flightCount) {
262
+ throw new TypeError(
263
+ `sabre-client: el itinerario tiene ${flightCount} vuelos y ${classes.length} clases de servicio en fareComponents`
264
+ );
265
+ }
266
+ let next = 0;
267
+ return itinerary.legs.flatMap((leg, legIndex) => {
268
+ const legDate = legDescriptions[legIndex].departureDate;
269
+ return leg.schedules.map((schedule) => {
270
+ const departureDate = addDays(legDate, schedule.departureDateAdjustment ?? 0);
271
+ return {
272
+ legIndex,
273
+ schedule,
274
+ departureDate,
275
+ arrivalDate: addDays(departureDate, schedule.arrival.dateAdjustment ?? 0),
276
+ bookingCode: classes[next++]
277
+ };
278
+ });
279
+ });
280
+ }
281
+ function bookingCodes(itinerary) {
282
+ const passenger = itinerary.pricingInformation[0]?.fare.passengerInfoList[0]?.passengerInfo;
283
+ return (passenger?.fareComponents ?? []).flatMap(
284
+ (component) => (component.segments ?? []).map(({ segment }) => {
285
+ if (segment.bookingCode === void 0) {
286
+ throw new TypeError("sabre-client: un segmento de fareComponents no trae bookingCode");
287
+ }
288
+ return segment.bookingCode;
289
+ })
290
+ );
291
+ }
292
+ function addDays(date, days) {
293
+ if (days === 0) return date;
294
+ const [year, month, day] = date.split("-").map(Number);
295
+ return new Date(Date.UTC(year, month - 1, day + days)).toISOString().slice(0, 10);
296
+ }
108
297
 
109
- // src/booking-management/booking.ts
110
- var Booking = class {
111
- /**
112
- * Booking
113
- *
114
- * La API de reservas está diseñada para proporcionar una vista normalizada de una reserva de Sabre combinando el PNR de Sabre y el pedido de Sabre.
115
- *
116
- * @param sabre Sabre Client Reference
117
- */
118
- constructor(sabre) {
119
- this.sabre = sabre;
298
+ // src/air/revalidate-itinerary.ts
299
+ var REVALIDATE_ITINERARY_SERVICE = "revalidateItinerary";
300
+ var PATH2 = "/v5/shop/flights/revalidate";
301
+ async function revalidateItinerary(transport, request, options = {}) {
302
+ if (typeof request !== "object" || request === null || typeof request.OTA_AirLowFareSearchRQ !== "object") {
303
+ throw new TypeError("sabre-client: revalidateItinerary espera { OTA_AirLowFareSearchRQ: { ... } }");
120
304
  }
121
- /**
122
- * Crea una solicitud que contiene un "Id. de confirmación" y un "apellido" del viajero para obtener información de la reserva.
123
- *
124
- * payload - Contiene elementos tanto obligatorios como opcionales para realizar una solicitud de reserva.
125
- * @param payload GetBookingOptions
126
- * @returns {Promise<GetBookingResponseSuccess>}
127
- */
128
- get(payload) {
129
- const path = "/v1/trip/orders/getBooking";
130
- return this.sabre.post(path, payload);
305
+ const body = await transport.request({
306
+ method: "POST",
307
+ path: PATH2,
308
+ service: REVALIDATE_ITINERARY_SERVICE,
309
+ // POST, pero de solo lectura: no sostiene inventario, se puede reintentar.
310
+ idempotent: true,
311
+ body: request,
312
+ classify: classifyRevalidateItinerary,
313
+ ...options.retry === void 0 ? {} : { retry: options.retry },
314
+ ...options.signal === void 0 ? {} : { signal: options.signal },
315
+ ...options.onRawResponse === void 0 ? {} : { operation: "air.revalidateItinerary", onRawResponse: options.onRawResponse }
316
+ });
317
+ return denormalizeGroupedItineraryResponse(body.groupedItineraryResponse, REVALIDATE_ITINERARY_SERVICE);
318
+ }
319
+ var REVALIDATE_ITINERARY_CATALOG = [
320
+ { code: "NO COMBINABLE FARES FOR CLASS USED", retryable: false },
321
+ { code: "NO FARE FOR CLASS USED", retryable: false },
322
+ { code: "NO FLIGHT SCHEDULES FOR QUALIFIERS USED", retryable: false },
323
+ { code: "NO SUCH FLIGHT", retryable: false },
324
+ { code: "NO AVAILABILITY", retryable: false },
325
+ ...BARGAIN_FINDER_MAX_CATALOG.filter((entry) => entry.retryable)
326
+ ];
327
+ function classifyRevalidateItinerary(body) {
328
+ return classifyItineraryMessages(body, REVALIDATE_ITINERARY_SERVICE, REVALIDATE_ITINERARY_CATALOG);
329
+ }
330
+ function buildRevalidateItineraryRequest(source) {
331
+ const { search, group, itinerary } = source;
332
+ const flights = itineraryFlights(group, itinerary);
333
+ const originDestinations = itinerary.legs.map((_leg, i) => {
334
+ const legFlights = flights.filter((f) => f.legIndex === i).map(flight);
335
+ const first = legFlights[0];
336
+ const last = legFlights.at(-1);
337
+ return {
338
+ Fixed: false,
339
+ DepartureDateTime: first.DepartureDateTime,
340
+ OriginLocation: { LocationCode: first.OriginLocation.LocationCode },
341
+ DestinationLocation: { LocationCode: last.DestinationLocation.LocationCode },
342
+ TPA_Extensions: { Flight: legFlights }
343
+ };
344
+ });
345
+ const { Version, POS, TravelerInfoSummary, TPA_Extensions } = search.OTA_AirLowFareSearchRQ;
346
+ return {
347
+ OTA_AirLowFareSearchRQ: {
348
+ Version,
349
+ POS,
350
+ OriginDestinationInformation: originDestinations,
351
+ TravelPreferences: {
352
+ MaxStopsQuantity: Math.max(...itinerary.legs.map((leg) => leg.schedules.length - 1)),
353
+ TPA_Extensions: { VerificationItinCallLogic: { Value: "L" } }
354
+ },
355
+ TravelerInfoSummary,
356
+ ...TPA_Extensions === void 0 ? {} : { TPA_Extensions }
357
+ }
358
+ };
359
+ }
360
+ function flight({ schedule, departureDate, arrivalDate, bookingCode }) {
361
+ const { carrier } = schedule;
362
+ return {
363
+ Type: "A",
364
+ Number: carrier.marketingFlightNumber,
365
+ DepartureDateTime: `${departureDate}T${localTime(schedule.departure.time)}`,
366
+ ArrivalDateTime: `${arrivalDate}T${localTime(schedule.arrival.time)}`,
367
+ ClassOfService: bookingCode,
368
+ OriginLocation: { LocationCode: schedule.departure.airport },
369
+ DestinationLocation: { LocationCode: schedule.arrival.airport },
370
+ Airline: {
371
+ Marketing: carrier.marketing,
372
+ ...carrier.operating === void 0 ? {} : { Operating: carrier.operating }
373
+ }
374
+ };
375
+ }
376
+ function localTime(time) {
377
+ return time.slice(0, 8);
378
+ }
379
+
380
+ // src/air/air.ts
381
+ var Air = class {
382
+ #transport;
383
+ /** @internal Lo construye el cliente `Sabre`. */
384
+ constructor(transport) {
385
+ this.#transport = transport;
131
386
  }
132
387
  /**
133
- * Crea una reserva aérea (NDC/ATPCO/LCC).
134
- *
135
- * payload - Contiene elementos obligatorios y opcionales para realizar una solicitud de reserva.
136
- * @param payload CreateBookingOptions
137
- * @returns {Promise<CreateBookingResponseSuccess>}
388
+ * Bargain Finder Max v5: busca itinerarios de menor precio. Devuelve el
389
+ * `groupedItineraryResponse` con las referencias ya resueltas.
138
390
  */
139
- async create(payload) {
140
- const path = "/v1/trip/orders/createBooking";
141
- return this.sabre.post(path, payload);
391
+ bargainFinderMax(request, options) {
392
+ return bargainFinderMax(this.#transport, request, options);
142
393
  }
143
394
  /**
144
- * Crea una lista de elementos de reserva que deben cancelarse.
145
- *
146
- * payload - Contiene elementos obligatorios y opcionales para cancelar la totalidad o partes de una reserva.
147
- * @param payload CancelBookingOptions
148
- * @returns {Promise<CancelBookingResponseSuccess>}
395
+ * Revalidate Itinerary v5: revalida disponibilidad y precio de un itinerario ya elegido,
396
+ * sin sostener inventario. El request se arma con `buildRevalidateItineraryRequest`.
149
397
  */
150
- async cancel(payload) {
151
- const path = "/v1/trip/orders/cancelBooking";
152
- return this.sabre.post(path, payload);
398
+ revalidateItinerary(request, options) {
399
+ return revalidateItinerary(this.#transport, request, options);
153
400
  }
154
- /**
155
- * Crea una lista de elementos de reserva que deben modificarse.
156
- *
157
- * payload - Contiene elementos obligatorios y opcionales para modificar datos no relacionados con el itinerario en la reserva existente.
158
- * @param payload ModifyBookingOptions
159
- * @returns {Promise<ModifyBookingResponseSuccess>}
160
- */
161
- async modify(payload) {
162
- const path = "/v1/trip/orders/modifyBooking";
163
- return this.sabre.post(path, payload);
401
+ };
402
+
403
+ // src/logging/redact.ts
404
+ var REDACTED = "[REDACTED]";
405
+ var SENSITIVE_KEYS = new Set(
406
+ [
407
+ // Credenciales
408
+ "clientSecret",
409
+ "password",
410
+ "username",
411
+ "authorization",
412
+ "binarySecurityToken",
413
+ "accessToken",
414
+ "refreshToken",
415
+ "token",
416
+ // Datos de pasajero
417
+ "travelers",
418
+ "passengers"
419
+ ].map(normalizeKey)
420
+ );
421
+ function normalizeKey(key) {
422
+ return key.replace(/[-_\s]/g, "").toLowerCase();
423
+ }
424
+ function isSensitiveKey(key) {
425
+ return SENSITIVE_KEYS.has(normalizeKey(key));
426
+ }
427
+ function redact(value) {
428
+ return redactValue(value, /* @__PURE__ */ new WeakSet());
429
+ }
430
+ function redactValue(value, seen) {
431
+ if (value === null || typeof value !== "object") return value;
432
+ if (seen.has(value)) return "[Circular]";
433
+ seen.add(value);
434
+ if (Array.isArray(value)) return value.map((item) => redactValue(item, seen));
435
+ if (value instanceof Headers) return redactEntries(value.entries(), seen);
436
+ if (value instanceof Date) return value.toISOString();
437
+ if (value instanceof Error) {
438
+ const json = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : void 0;
439
+ return redactValue(json ?? { name: value.name, message: value.message }, seen);
440
+ }
441
+ return redactEntries(Object.entries(value), seen);
442
+ }
443
+ function redactEntries(entries, seen) {
444
+ const out = {};
445
+ for (const [key, item] of entries) {
446
+ out[key] = isSensitiveKey(key) ? REDACTED : redactValue(item, seen);
447
+ }
448
+ return out;
449
+ }
450
+
451
+ // src/logging/logger.ts
452
+ var EventLogger = class {
453
+ #logger;
454
+ constructor(logger) {
455
+ this.#logger = logger;
456
+ }
457
+ emit(level, event, context = {}) {
458
+ if (this.#logger === void 0) return;
459
+ try {
460
+ this.#logger[level](event, redact(context));
461
+ } catch {
462
+ }
164
463
  }
165
464
  };
166
465
 
167
- // src/booking-management/flight-tickets.ts
168
- var FlightTickets = class {
169
- /**
170
- * Flight Tickets
171
- *
172
- * Los endpoint de Flight Tickets son un conjunto de métodos diseñados para obtener detalles de los billetes y
173
- * realizar operaciones específicas (anulación/reembolso) sobre ellos.
174
- *
175
- * @param sabre Sabre Client Reference
176
- */
177
- constructor(sabre) {
178
- this.sabre = sabre;
466
+ // src/auth/token-request.ts
467
+ function buildTokenRequestV3(credentials) {
468
+ return {
469
+ path: "/v3/auth/token",
470
+ authorization: `Basic ${base64(`${credentials.clientId}:${credentials.clientSecret}`)}`,
471
+ body: new URLSearchParams({
472
+ grant_type: "password",
473
+ username: credentials.username,
474
+ password: credentials.password
475
+ })
476
+ };
477
+ }
478
+ function buildTokenRequestV2(credentials) {
479
+ return {
480
+ path: "/v2/auth/token",
481
+ authorization: `Basic ${base64(`${base64(credentials.username)}:${base64(credentials.password)}`)}`,
482
+ body: new URLSearchParams({ grant_type: "client_credentials" })
483
+ };
484
+ }
485
+ function buildTokenRequest(authVersion, credentials) {
486
+ return authVersion === "v3" ? buildTokenRequestV3(credentials) : buildTokenRequestV2(credentials);
487
+ }
488
+ function base64(text) {
489
+ let binary = "";
490
+ for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
491
+ return btoa(binary);
492
+ }
493
+
494
+ // src/auth/token-store.ts
495
+ async function tokenStoreKey(environment, authVersion, credentials) {
496
+ const identity = authVersion === "v3" ? `${credentials.clientId}
497
+ ${credentials.username}` : credentials.username;
498
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity));
499
+ const hash = [...new Uint8Array(digest).slice(0, 16)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
500
+ return `sabre-client:${environment}:${authVersion}:${hash}`;
501
+ }
502
+ function isStoredToken(value) {
503
+ if (typeof value !== "object" || value === null) return false;
504
+ const { accessToken, obtainedAt, expiresAt } = value;
505
+ return typeof accessToken === "string" && accessToken !== "" && typeof obtainedAt === "number" && typeof expiresAt === "number" && Number.isFinite(obtainedAt) && expiresAt > obtainedAt;
506
+ }
507
+
508
+ // src/auth/auth-manager.ts
509
+ var TOKEN_SERVICE = "oauthTokenCreate";
510
+ var REFRESH_THRESHOLD_S = 3600;
511
+ var AuthManager = class {
512
+ #config;
513
+ #transport;
514
+ #now;
515
+ #log;
516
+ #token;
517
+ #inFlight;
518
+ /** El último token cacheado fue rechazado con 401 (el próximo refresco es reactivo). */
519
+ #invalidated = false;
520
+ /** Token rechazado con 401: si el store todavía lo tiene, no se vuelve a usar. */
521
+ #rejected;
522
+ #store;
523
+ #storeKey;
524
+ constructor(config, transport, options = {}) {
525
+ this.#config = config;
526
+ this.#transport = transport;
527
+ this.#now = options.now ?? Date.now;
528
+ this.#log = new EventLogger(config.logger);
529
+ this.#store = config.tokenStore;
179
530
  }
180
- /**
181
- * Procesa tickets proporcionando una solicitud que contiene una lista de números de ticket que se anularán.
182
- *
183
- * payload - Contiene elementos obligatorios y opcionales para anular los billetes.
184
- * @param payload VoidTicketsOptions
185
- * @returns {Promise<VoidTicketsResponseSuccess>}
186
- */
187
- async void(payload) {
188
- const path = "/v1/trip/orders/voidFlightTickets";
189
- return this.sabre.post(path, payload);
531
+ /** Token vigente; pide uno nuevo si no hay o si cruzó el umbral de refresco. */
532
+ async getToken() {
533
+ const token = this.#token;
534
+ if (token !== void 0 && !this.#needsRefresh(token)) return token.accessToken;
535
+ const reason = token !== void 0 ? "proactive" : this.#invalidated ? "reactive" : "initial";
536
+ return (await this.#refresh(reason)).accessToken;
190
537
  }
191
538
  /**
192
- * Procesa los boletos proporcionando una solicitud que contiene una lista de números de boletos que deben reembolsarse.
193
- * Actualmente, los EMD no son compatibles.
194
- *
195
- * payload - Contiene elementos obligatorios y opcionales para el reembolso de los billetes.
196
- * @param payload RefundTicketsOptions
197
- * @returns {Promise<RefundTicketsResponseSuccess>}
539
+ * Descarta `accessToken` tras un 401 de un servicio. Si el caché ya tiene otro token
540
+ * (otra request lo renovó), no hace nada: los 401 concurrentes comparten un refresco.
198
541
  */
199
- async refund(payload) {
200
- const path = "/v1/trip/orders/refundFlightTickets";
201
- return this.sabre.post(path, payload);
542
+ invalidate(accessToken) {
543
+ if (this.#token?.accessToken !== accessToken) return;
544
+ this.#token = void 0;
545
+ this.#invalidated = true;
546
+ this.#rejected = accessToken;
547
+ }
548
+ /** Adaptador de salida para el transporte REST: `Authorization: Bearer <token>`. */
549
+ bearerProvider() {
550
+ return {
551
+ get: async () => `Bearer ${await this.getToken()}`,
552
+ invalidate: (authorization) => this.invalidate(authorization.replace(/^Bearer /, ""))
553
+ };
554
+ }
555
+ #needsRefresh(token) {
556
+ const thresholdMs = Math.min(REFRESH_THRESHOLD_S, token.expiresIn / 2) * 1e3;
557
+ return this.#now() >= token.obtainedAt + token.expiresIn * 1e3 - thresholdMs;
558
+ }
559
+ /** Una sola petición en vuelo: las llamadas concurrentes esperan la misma promesa. */
560
+ #refresh(reason) {
561
+ this.#inFlight ??= this.#obtain(reason).finally(() => {
562
+ this.#inFlight = void 0;
563
+ });
564
+ return this.#inFlight;
565
+ }
566
+ /** Token del store si sigue vigente (otro proceso pudo renovarlo); si no, uno nuevo. */
567
+ async #obtain(reason) {
568
+ const stored = await this.#readStore();
569
+ if (stored !== void 0 && stored.accessToken !== this.#rejected && !this.#needsRefresh(stored)) {
570
+ this.#token = stored;
571
+ this.#invalidated = false;
572
+ return stored;
573
+ }
574
+ const token = await this.fetchToken();
575
+ this.#token = token;
576
+ this.#invalidated = false;
577
+ this.#rejected = void 0;
578
+ this.#log.emit("info", "sabre.auth.token.refresh", {
579
+ authVersion: this.#config.authVersion,
580
+ reason,
581
+ expiresIn: token.expiresIn
582
+ });
583
+ await this.#writeStore(token);
584
+ return token;
585
+ }
586
+ async #readStore() {
587
+ if (this.#store === void 0) return void 0;
588
+ try {
589
+ const value = await this.#store.get(await this.#key());
590
+ if (!isStoredToken(value)) return void 0;
591
+ return {
592
+ accessToken: value.accessToken,
593
+ expiresIn: (value.expiresAt - value.obtainedAt) / 1e3,
594
+ obtainedAt: value.obtainedAt
595
+ };
596
+ } catch (error) {
597
+ this.#storeFailed("get", error);
598
+ return void 0;
599
+ }
600
+ }
601
+ async #writeStore(token) {
602
+ if (this.#store === void 0) return;
603
+ const stored = {
604
+ accessToken: token.accessToken,
605
+ obtainedAt: token.obtainedAt,
606
+ expiresAt: token.obtainedAt + token.expiresIn * 1e3
607
+ };
608
+ try {
609
+ await this.#store.set(await this.#key(), stored);
610
+ } catch (error) {
611
+ this.#storeFailed("set", error);
612
+ }
613
+ }
614
+ #key() {
615
+ const { environment, authVersion, credentials } = this.#config;
616
+ this.#storeKey ??= tokenStoreKey(environment, authVersion, credentials);
617
+ return this.#storeKey;
202
618
  }
203
619
  /**
204
- * Procesa tickets proporcionando una solicitud que contiene una lista de números de ticket que se verificarán.
205
- * Actualmente, los EMD no son compatibles.
206
- *
207
- * payload - Contiene elementos obligatorios y opcionales para consultar los tickets.
208
- * @param payload CheckTicketsOptions
209
- * @returns {Promise<CheckTicketsResponseSuccess>} CheckTicketsResponse
620
+ * El store nunca rompe la llamada: se sigue con el caché en memoria. Del error solo se
621
+ * loguea el nombre: el mensaje lo arma el store del consumidor y podría traer el token.
210
622
  */
211
- async check(payload) {
212
- const path = "/v1/trip/orders/checkFlightTickets";
213
- return this.sabre.post(path, payload);
623
+ #storeFailed(operation, error) {
624
+ this.#log.emit("warn", "sabre.auth.token.store", {
625
+ authVersion: this.#config.authVersion,
626
+ operation,
627
+ errorName: error instanceof Error ? error.name : typeof error
628
+ });
214
629
  }
215
630
  /**
216
- * Procesa la emisión de billetes y documentos varios electrónicos (EMD).
217
- * Actualmente, los EMD no son compatibles.
218
- *
219
- * payload - Contiene elementos obligatorios y opcionales para la emisión de billetes.
220
- * @param payload FulfillTicketsOptions
221
- * @returns {Promise<FulfillTicketsResponseSuccess>}
631
+ * Pide un token nuevo. Los errores del endpoint (`invalid_request`/`invalid_client` 401,
632
+ * `unauthorized` 403) salen como `SabreGatewayError` con `retryable: false`.
222
633
  */
223
- async fulfill(payload) {
224
- const path = "/v1/trip/orders/fulfillFlightTickets";
225
- return this.sabre.post(path, payload);
634
+ async fetchToken() {
635
+ const { path, authorization, body } = buildTokenRequest(this.#config.authVersion, this.#config.credentials);
636
+ const response = await this.#transport.request({
637
+ method: "POST",
638
+ path,
639
+ service: TOKEN_SERVICE,
640
+ // Pedir otro token no tiene efectos duplicados: 429/5xx entran en la política genérica.
641
+ idempotent: true,
642
+ auth: false,
643
+ headers: { Authorization: authorization },
644
+ body
645
+ });
646
+ return this.#parse(response);
647
+ }
648
+ #parse(response) {
649
+ const accessToken = response?.access_token;
650
+ const expiresIn = response?.expires_in;
651
+ if (typeof accessToken !== "string" || accessToken === "" || typeof expiresIn !== "number" || !(expiresIn > 0)) {
652
+ throw new SabreGatewayError(`${TOKEN_SERVICE}: respuesta sin access_token/expires_in v\xE1lidos`, {
653
+ protocol: "rest",
654
+ status: 200,
655
+ retryable: false,
656
+ raw: response
657
+ });
658
+ }
659
+ return { accessToken, expiresIn, obtainedAt: this.#now() };
226
660
  }
227
661
  };
228
662
 
229
- // package.json
230
- var version = "0.2.4";
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
+ }
231
698
 
232
- // src/config.ts
233
- var defaultBaseUrl = "https://api.platform.sabre.com";
234
- var defaultUserAgent = `sabre-client-node:${version}`;
235
- var defaultConversationId = `sabre.client.node:${version}`;
236
- var defaultDomain = "AA";
237
- var baseUrl = typeof process !== "undefined" && process.env ? process.env.SABRE_REST_URL || defaultBaseUrl : defaultBaseUrl;
238
- var userAgent = typeof process !== "undefined" && process.env ? process.env.SABRE_USER_AGENT || defaultUserAgent : defaultUserAgent;
239
- var domain = typeof process !== "undefined" && process.env ? process.env.SABRE_DOMAIN || defaultDomain : defaultDomain;
240
- var conversationId = typeof process !== "undefined" && process.env ? process.env.SABRE_CONVERSATION_ID || defaultConversationId : defaultConversationId;
699
+ // src/booking/cancel-booking.ts
700
+ var CANCEL_BOOKING_SERVICE = "cancelBooking";
701
+ var PATH3 = "/v1/trip/orders/cancelBooking";
702
+ var NO_ITEMS_CANCELLED = "NO_ITEMS_CANCELLED";
703
+ var ITEM_LISTS = ["flights", "hotels", "cars", "trains", "cruises", "segments"];
704
+ async function cancelBooking(transport, request, options = {}) {
705
+ validateCancelBookingRequest(request);
706
+ const body = await transport.request({
707
+ method: "POST",
708
+ path: PATH3,
709
+ service: CANCEL_BOOKING_SERVICE,
710
+ idempotent: false,
711
+ body: request,
712
+ classify: classifyCancelBooking,
713
+ ...options.signal === void 0 ? {} : { signal: options.signal },
714
+ ...options.onRawResponse === void 0 ? {} : { operation: "booking.cancel", onRawResponse: options.onRawResponse }
715
+ });
716
+ return toCancelBookingResponse(body);
717
+ }
718
+ function validateCancelBookingRequest(request) {
719
+ if (!isObject2(request) || !nonEmpty(request.confirmationId)) {
720
+ throw new TypeError("sabre-client: booking.cancel espera { confirmationId: string, ... }");
721
+ }
722
+ const lists = ITEM_LISTS.join(", ");
723
+ if (request.cancelAll === true) {
724
+ if (ITEM_LISTS.some((list) => request[list] !== void 0)) {
725
+ throw new TypeError(`sabre-client: booking.cancel no combina cancelAll con listas (${lists})`);
726
+ }
727
+ } else if (!ITEM_LISTS.some((list) => Array.isArray(request[list]) && request[list].length > 0)) {
728
+ throw new TypeError(`sabre-client: booking.cancel necesita cancelAll: true o al menos una lista (${lists})`);
729
+ }
730
+ if (request.offerItemId !== void 0 && request.flightTicketOperation !== void 0) {
731
+ throw new TypeError("sabre-client: booking.cancel no combina offerItemId con flightTicketOperation");
732
+ }
733
+ }
734
+ function classifyCancelBooking(body) {
735
+ if (!isObject2(body)) {
736
+ return new SabreError(`${CANCEL_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
737
+ retryable: false,
738
+ raw: body
739
+ });
740
+ }
741
+ const items = responseErrors(body);
742
+ const nothingCancelled = items.find(isNoItemsCancelled);
743
+ const warnings = items.filter((item) => isWarning(item) && !isNoItemsCancelled(item)).map(warningOf);
744
+ const fatal = items.filter((item) => !isWarning(item));
745
+ if (nothingCancelled !== void 0) {
746
+ return toBusinessError(CANCEL_BOOKING_SERVICE, nothingCancelled, { warnings, errors: fatal.map(errorOf) });
747
+ }
748
+ const [first, ...rest] = fatal;
749
+ if (first === void 0) return void 0;
750
+ return toBusinessError(CANCEL_BOOKING_SERVICE, first, { warnings, errors: rest.map(errorOf) });
751
+ }
752
+ function toCancelBookingResponse(body) {
753
+ const { errors: _errors, ...rest } = body;
754
+ return { ...rest, warnings: responseErrors(body).map(warningOf) };
755
+ }
756
+ function isNoItemsCancelled(item) {
757
+ return item.type === NO_ITEMS_CANCELLED;
758
+ }
759
+ function warningOf(item) {
760
+ return toWarning(CANCEL_BOOKING_SERVICE, item);
761
+ }
762
+ function errorOf(item) {
763
+ return toBusinessError(CANCEL_BOOKING_SERVICE, item);
764
+ }
241
765
 
242
- // src/errors.ts
243
- var SabreError = class extends Error {
244
- details;
245
- statusCode;
246
- constructor(message, statusCode, details) {
247
- super(message);
248
- this.name = "SabreError";
249
- this.statusCode = statusCode;
250
- this.details = details;
766
+ // src/booking/create-booking.ts
767
+ var CREATE_BOOKING_SERVICE = "createBooking";
768
+ var PATH4 = "/v1/trip/orders/createBooking";
769
+ var UNABLE_TO_RETRIEVE_BOOKING = "UNABLE_TO_RETRIEVE_BOOKING";
770
+ async function createBooking(transport, request, options = {}) {
771
+ validateCreateBookingRequest(request);
772
+ const body = await transport.request({
773
+ method: "POST",
774
+ path: PATH4,
775
+ service: CREATE_BOOKING_SERVICE,
776
+ idempotent: false,
777
+ body: request,
778
+ classify: classifyCreateBooking,
779
+ ...options.signal === void 0 ? {} : { signal: options.signal },
780
+ ...options.onRawResponse === void 0 ? {} : { operation: "booking.create", onRawResponse: options.onRawResponse }
781
+ });
782
+ return toCreateBookingResponse(body);
783
+ }
784
+ function validateCreateBookingRequest(request) {
785
+ if (typeof request !== "object" || request === null) {
786
+ throw new TypeError("sabre-client: booking.create espera un objeto CreateBookingRequest");
251
787
  }
252
- };
788
+ const agency = request.agency;
789
+ if (agency?.ticketingPolicy === "FUTURE_TICKETING" && agency.futureTicketingPolicy === void 0) {
790
+ throw new TypeError(
791
+ "sabre-client: booking.create con agency.ticketingPolicy FUTURE_TICKETING necesita agency.futureTicketingPolicy"
792
+ );
793
+ }
794
+ }
795
+ function classifyCreateBooking(body) {
796
+ if (!isObject2(body)) {
797
+ return new SabreError(`${CREATE_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
798
+ retryable: false,
799
+ raw: body
800
+ });
801
+ }
802
+ const response = body;
803
+ if (confirmationIdOf(response) !== void 0) return void 0;
804
+ if (reservationWithoutId(response)) {
805
+ return new SabreError(
806
+ `${CREATE_BOOKING_SERVICE}: Sabre indica que la reserva se cre\xF3, pero no devolvi\xF3 su confirmationId`,
807
+ { retryable: false, raw: body }
808
+ );
809
+ }
810
+ const items = responseErrors(response);
811
+ const fatal = items.find((item) => !isWarning(item));
812
+ if (fatal !== void 0) return toBusinessError(CREATE_BOOKING_SERVICE, fatal, { warnings: items.filter(isWarning).map(warningOf2) });
813
+ return new SabreError(`${CREATE_BOOKING_SERVICE}: respuesta malformada, sin confirmationId ni errores fatales`, {
814
+ retryable: false,
815
+ raw: body
816
+ });
817
+ }
818
+ function toCreateBookingResponse(body) {
819
+ const items = responseErrors(body);
820
+ return {
821
+ confirmationId: confirmationIdOf(body),
822
+ ...body.timestamp === void 0 ? {} : { timestamp: body.timestamp },
823
+ ...body.booking === void 0 ? {} : { booking: body.booking },
824
+ ...body.request === void 0 ? {} : { request: body.request },
825
+ warnings: items.filter(isWarning).map(warningOf2),
826
+ errors: items.filter((item) => !isWarning(item)).map((item) => toBusinessError(CREATE_BOOKING_SERVICE, item))
827
+ };
828
+ }
829
+ function confirmationIdOf(response) {
830
+ if (nonEmpty(response.confirmationId)) return response.confirmationId;
831
+ if (nonEmpty(response.booking?.bookingId)) return response.booking.bookingId;
832
+ const notRetrieved = responseErrors(response).find((item) => item.type === UNABLE_TO_RETRIEVE_BOOKING);
833
+ return notRetrieved === void 0 ? void 0 : locatorIn(notRetrieved.description);
834
+ }
835
+ function reservationWithoutId(response) {
836
+ return response.booking !== void 0 || responseErrors(response).some((item) => item.type === UNABLE_TO_RETRIEVE_BOOKING);
837
+ }
838
+ function locatorIn(description) {
839
+ if (description === void 0) return void 0;
840
+ return /\(([A-Z0-9]{6,})\)/.exec(description)?.[1] ?? /\b([A-Z0-9]{6,})\b/.exec(description)?.[1];
841
+ }
842
+ function warningOf2(item) {
843
+ return toWarning(CREATE_BOOKING_SERVICE, item);
844
+ }
845
+
846
+ // src/booking/get-booking.ts
847
+ var GET_BOOKING_SERVICE = "getBooking";
848
+ var PATH5 = "/v1/trip/orders/getBooking";
849
+ var RETRYABLE_TYPES = /* @__PURE__ */ new Set(["INTERNAL_SERVER_TIMEOUT", "DOWNLINE_SERVICE_FAILURE"]);
850
+ async function getBooking(transport, request, options = {}) {
851
+ if (!isObject2(request) || !nonEmpty(request.confirmationId)) {
852
+ throw new TypeError("sabre-client: booking.get espera { confirmationId: string, ... }");
853
+ }
854
+ const body = await transport.request({
855
+ method: "POST",
856
+ path: PATH5,
857
+ service: GET_BOOKING_SERVICE,
858
+ idempotent: true,
859
+ body: request,
860
+ classify: classifyGetBooking,
861
+ ...options.signal === void 0 ? {} : { signal: options.signal },
862
+ ...options.onRawResponse === void 0 ? {} : { operation: "booking.get", onRawResponse: options.onRawResponse }
863
+ });
864
+ return toGetBookingResponse(body);
865
+ }
866
+ function classifyGetBooking(body) {
867
+ if (!isObject2(body)) {
868
+ return new SabreError(`${GET_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
869
+ retryable: false,
870
+ raw: body
871
+ });
872
+ }
873
+ const response = body;
874
+ if (nonEmpty(response.bookingId)) return void 0;
875
+ const items = responseErrors(response);
876
+ const fatal = items.find((item) => !isWarning(item));
877
+ if (fatal !== void 0) return errorOf2(fatal, items.filter(isWarning).map(warningOf3));
878
+ return new SabreError(`${GET_BOOKING_SERVICE}: respuesta malformada, sin bookingId ni errores fatales`, {
879
+ retryable: false,
880
+ raw: body
881
+ });
882
+ }
883
+ function toGetBookingResponse(body) {
884
+ const items = responseErrors(body);
885
+ return {
886
+ ...body,
887
+ bookingId: body.bookingId,
888
+ warnings: items.filter(isWarning).map(warningOf3),
889
+ errors: items.filter((item) => !isWarning(item)).map((item) => errorOf2(item, []))
890
+ };
891
+ }
892
+ function warningOf3(item) {
893
+ return toWarning(GET_BOOKING_SERVICE, item);
894
+ }
895
+ function errorOf2(item, warnings) {
896
+ return toBusinessError(GET_BOOKING_SERVICE, item, { warnings, retryable: RETRYABLE_TYPES.has(item.type) });
897
+ }
253
898
 
254
- // src/trip-management/reservation.ts
255
- var Reservation = class {
899
+ // src/booking/booking-management.ts
900
+ var BookingManagement = class {
901
+ #transport;
902
+ /** @internal Lo construye el cliente `Sabre`. */
903
+ constructor(transport) {
904
+ this.#transport = transport;
905
+ }
256
906
  /**
257
- * Reservation
258
- *
259
- * La API para crear un registro de nombre de pasajero es una API
260
- * orquestada que integra varias funciones y operaciones en una sola llamada.
261
- *
262
- * Esta API permite crear un registro de nombre de pasajero (PNR),
263
- * incluyendo segmentos aéreos, en una sola llamada.
264
- *
265
- * @param sabre Sabre Client Reference
907
+ * Create Booking: crea la reserva. Para un itinerario de BFM/Revalidate, `flightDetails` se
908
+ * arma con `buildBookingFlightDetails`.
909
+ *
910
+ * Nunca se reintenta automáticamente. Si la reserva se crea con avisos o errores no fatales,
911
+ * no lanza: los devuelve en `warnings` y `errors`. Si no se crea, lanza `SabreBusinessError`.
912
+ * Con `agency.ticketingPolicy: 'FUTURE_TICKETING'` y sin `agency.futureTicketingPolicy`, lanza
913
+ * `TypeError` sin llamar a la red: Sabre lo rechaza seguro.
266
914
  */
267
- constructor(sabre) {
268
- this.sabre = sabre;
915
+ create(request, options) {
916
+ return createBooking(this.#transport, request, options);
269
917
  }
270
918
  /**
271
- * Cree una reserva completa de contenido aéreo, hotel y automóvil de alquiler, todo dentro de una sola solicitud.
272
- *
273
- * payload - Contiene elementos obligatorios y opcionales para realizar una solicitud de reserva.
274
- * @param payload CreatePassengerNameRecordOptions
275
- * @returns {Promise<CreatePassengerNameRecordResponseSuccess>}
919
+ * Get Booking: lee la reserva por `confirmationId`. El `Booking` va en el primer nivel de la
920
+ * respuesta, a diferencia de `create`. Usa `returnOnly` para pedir solo las secciones que
921
+ * hacen falta: además acelera la respuesta.
922
+ *
923
+ * Si la reserva existe, no lanza: los avisos y errores parciales van en `warnings` y
924
+ * `errors`. Si no existe o no se pudo leer (p. ej. `BOOKING_NOT_FOUND`), lanza
925
+ * `SabreBusinessError`. Es una lectura: se reintenta ante fallos transitorios.
276
926
  */
277
- create(payload) {
278
- const path = "/v2.5.0/passenger/records?mode=create";
279
- return this.sabre.post(path, payload);
927
+ get(request, options) {
928
+ return getBooking(this.#transport, request, options);
929
+ }
930
+ /**
931
+ * Cancel Booking: cancela la reserva entera (`cancelAll: true`) o por `itemId` (p. ej. los
932
+ * `flights[].itemId` de `get`). Valida antes de enviar las combinaciones que Sabre rechaza y,
933
+ * si encuentra una, lanza `TypeError` sin llamar a la red.
934
+ *
935
+ * Nunca se reintenta automáticamente. Lanza `SabreBusinessError` si algo falló o si Sabre
936
+ * responde `NO_ITEMS_CANCELLED` (no se canceló nada), aunque llegue como aviso. Con
937
+ * `END_TRANSACTION_PROBLEM` en `warnings`, la confirmación quedó pendiente: verifica con `get`.
938
+ */
939
+ cancel(request, options) {
940
+ return cancelBooking(this.#transport, request, options);
280
941
  }
281
942
  };
282
943
 
283
- // src/sabre.ts
284
- var Sabre = class {
285
- constructor(options = {}) {
286
- this.options = options;
287
- const processEnv = typeof process !== "undefined" && process.env;
288
- if (!this.options.username && processEnv)
289
- this.options.username = processEnv.SABRE_USERNAME;
290
- if (!this.options.password && processEnv)
291
- this.options.password = processEnv.SABRE_PASSWORD;
292
- if (!this.options.organization && processEnv)
293
- this.options.organization = processEnv.SABRE_ORGANIZATION;
294
- if (!this.options.username || !this.options.password || !this.options.organization) {
295
- throw new Error(`Missing Sabre authorization. Pass it to the constructor new Sabre({
296
- username: '773400',
297
- password: 'PASSWORD_GOES_HERE',
298
- organization: '7TZA', // pcc
299
- });})`);
944
+ // src/http/retry.ts
945
+ var DEFAULT_RETRY = Object.freeze({ maxRetries: 3, baseDelayMs: 500 });
946
+ var MIN_RETRY_DELAY_MS = 500;
947
+ function retryDelay(retry, baseDelayMs, random = Math.random) {
948
+ return Math.max(MIN_RETRY_DELAY_MS, Math.floor(random() * baseDelayMs * 2 ** retry));
949
+ }
950
+ function shouldRetry(error, idempotent) {
951
+ return idempotent && error instanceof SabreError && error.retryable;
952
+ }
953
+ function validateRetryOptions(retry, where) {
954
+ if (retry.maxRetries !== void 0 && !(Number.isInteger(retry.maxRetries) && retry.maxRetries >= 0)) {
955
+ throw new TypeError(`sabre-client: ${where}.maxRetries debe ser un entero >= 0`);
956
+ }
957
+ if (retry.baseDelayMs !== void 0 && !(Number.isFinite(retry.baseDelayMs) && retry.baseDelayMs >= 0)) {
958
+ throw new TypeError(`sabre-client: ${where}.baseDelayMs debe ser un n\xFAmero >= 0`);
959
+ }
960
+ }
961
+
962
+ // src/config.ts
963
+ var BASE_URLS = Object.freeze({
964
+ sandbox: "https://api.cert.platform.sabre.com",
965
+ production: "https://api.platform.sabre.com"
966
+ });
967
+ var LOGGER_METHODS = ["debug", "info", "warn", "error"];
968
+ function fail(message) {
969
+ throw new TypeError(`sabre-client: ${message}`);
970
+ }
971
+ function isObject3(value) {
972
+ return typeof value === "object" && value !== null;
973
+ }
974
+ function requireString(value, field) {
975
+ if (typeof value !== "string" || value.trim() === "") fail(`${field} es requerido y debe ser un string no vac\xEDo`);
976
+ }
977
+ function resolveConfig(config) {
978
+ if (!isObject3(config)) fail("la config debe ser un objeto");
979
+ const { environment } = config;
980
+ if (environment !== "sandbox" && environment !== "production") {
981
+ fail(`environment debe ser 'sandbox' o 'production' (recibido: ${JSON.stringify(environment)})`);
982
+ }
983
+ const authVersion = config.authVersion ?? "v3";
984
+ if (authVersion !== "v3" && authVersion !== "v2") {
985
+ fail(`authVersion debe ser 'v3' o 'v2' (recibido: ${JSON.stringify(authVersion)})`);
986
+ }
987
+ const { credentials } = config;
988
+ if (!isObject3(credentials)) fail("credentials es requerido");
989
+ requireString(credentials.username, "credentials.username");
990
+ requireString(credentials.password, "credentials.password");
991
+ if (authVersion === "v3") {
992
+ requireString(credentials.clientId, "credentials.clientId (requerido con authVersion v3)");
993
+ requireString(credentials.clientSecret, "credentials.clientSecret (requerido con authVersion v3)");
994
+ }
995
+ if (config.pcc !== void 0) requireString(config.pcc, "pcc");
996
+ const { logger } = config;
997
+ if (logger !== void 0) {
998
+ if (!isObject3(logger)) fail("logger debe ser un objeto con debug/info/warn/error");
999
+ for (const method of LOGGER_METHODS) {
1000
+ if (typeof logger[method] !== "function") fail(`logger.${method} debe ser una funci\xF3n`);
300
1001
  }
301
- this.headers = new Headers({
302
- "User-Agent": userAgent,
303
- "Conversation-ID": conversationId
304
- });
305
- this.secret = this.getSecret();
306
- }
307
- headers;
308
- secret;
309
- authentication = new Authentication(this);
310
- booking = new Booking(this);
311
- flightTickets = new FlightTickets(this);
312
- search = new Search(this);
313
- ticketing = new Ticketing(this);
314
- reservation = new Reservation(this);
315
- setAuthorization(token) {
316
- this.headers.set("Authorization", `Bearer ${token}`);
317
- this.headers.set("Content-Type", "application/json");
318
- }
319
- async fetchRequest(path, options = {}) {
320
- const response = await fetch(`${baseUrl}${path}`, options);
321
- if (!response.ok) {
322
- let errorDetails = `Request failed with status ${response.status}`;
1002
+ }
1003
+ if (config.retry !== void 0) {
1004
+ if (!isObject3(config.retry)) fail("retry debe ser un objeto");
1005
+ validateRetryOptions(config.retry, "retry");
1006
+ }
1007
+ const { tokenStore } = config;
1008
+ if (tokenStore !== void 0) {
1009
+ if (!isObject3(tokenStore) || typeof tokenStore.get !== "function" || typeof tokenStore.set !== "function") {
1010
+ fail("tokenStore debe ser un objeto con get(key) y set(key, token)");
1011
+ }
1012
+ }
1013
+ const fetchImpl = config.fetch ?? globalThis.fetch;
1014
+ if (typeof fetchImpl !== "function") fail("no hay fetch disponible: pasa config.fetch");
1015
+ return {
1016
+ credentials: Object.freeze({ ...credentials }),
1017
+ environment,
1018
+ baseUrl: BASE_URLS[environment],
1019
+ authVersion,
1020
+ pcc: config.pcc,
1021
+ logger,
1022
+ retry: Object.freeze({ ...DEFAULT_RETRY, ...definedOnly(config.retry ?? {}) }),
1023
+ // Enlazado al global para evitar "Illegal invocation" en algunos runtimes.
1024
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
1025
+ tokenStore
1026
+ };
1027
+ }
1028
+ function definedOnly(value) {
1029
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
1030
+ }
1031
+
1032
+ // src/http/rest-transport.ts
1033
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 503, 504]);
1034
+ var RestTransport = class {
1035
+ #config;
1036
+ #log;
1037
+ #random;
1038
+ #sleep;
1039
+ #authorization;
1040
+ constructor(config, options = {}) {
1041
+ this.#config = config;
1042
+ this.#log = new EventLogger(config.logger);
1043
+ this.#authorization = options.authorization;
1044
+ this.#random = options.random ?? Math.random;
1045
+ this.#sleep = options.sleep ?? sleep;
1046
+ }
1047
+ /** Punto de enganche para `AuthManager`. */
1048
+ setAuthorizationProvider(provider) {
1049
+ this.#authorization = provider;
1050
+ }
1051
+ async request(request) {
1052
+ if (request.retry !== void 0) validateRetryOptions(request.retry, "retry");
1053
+ const capture = request.onRawResponse === void 0 ? void 0 : { attempts: 0 };
1054
+ try {
1055
+ return await this.#attempts(request, capture);
1056
+ } finally {
1057
+ if (capture?.last !== void 0) this.#deliverRaw(request, { ...capture.last, attempts: capture.attempts });
1058
+ }
1059
+ }
1060
+ async #attempts(request, capture) {
1061
+ const retry = { ...this.#config.retry, ...definedOnly(request.retry ?? {}) };
1062
+ let reauthorized = false;
1063
+ for (let attempt = 0; ; attempt++) {
1064
+ const authorization = await this.#authorizationFor(request);
323
1065
  try {
324
- const jsonError = await response.json();
325
- errorDetails = JSON.stringify(jsonError);
326
- } catch (e) {
327
- console.error(e);
328
- errorDetails = response.statusText;
1066
+ return await this.#send(request, attempt, authorization, capture);
1067
+ } catch (error) {
1068
+ if (authorization !== void 0 && !reauthorized && isUnauthorized(error)) {
1069
+ reauthorized = true;
1070
+ this.#authorization?.invalidate(authorization);
1071
+ this.#log.emit("warn", "sabre.request.retry", {
1072
+ service: request.service,
1073
+ method: request.method,
1074
+ path: request.path,
1075
+ attempt: attempt + 1,
1076
+ reason: "unauthorized",
1077
+ error
1078
+ });
1079
+ attempt--;
1080
+ continue;
1081
+ }
1082
+ if (attempt >= retry.maxRetries || !shouldRetry(error, request.idempotent)) throw error;
1083
+ const delayMs = retryDelay(attempt, retry.baseDelayMs, this.#random);
1084
+ this.#log.emit("warn", "sabre.request.retry", {
1085
+ service: request.service,
1086
+ method: request.method,
1087
+ path: request.path,
1088
+ attempt: attempt + 1,
1089
+ maxRetries: retry.maxRetries,
1090
+ delayMs,
1091
+ error
1092
+ });
1093
+ await this.#sleep(delayMs, request.signal);
329
1094
  }
330
- throw new SabreError(
331
- `Request failed with status ${response.status}`,
332
- response.status,
333
- errorDetails
334
- );
335
1095
  }
336
- const data = await response.json();
337
- return data;
338
1096
  }
339
- async post(path, entity, options = {}) {
340
- const requestOptions = {
341
- method: "POST",
342
- headers: this.headers,
343
- body: JSON.stringify(entity),
344
- ...options
345
- };
346
- return this.fetchRequest(path, requestOptions);
1097
+ /** Un callback que lanza no rompe la operación: Create Booking no puede perder su localizador. */
1098
+ #deliverRaw(request, raw) {
1099
+ const operation = request.operation ?? request.service;
1100
+ try {
1101
+ request.onRawResponse?.({ operation, protocol: "rest", ...raw });
1102
+ } catch (error) {
1103
+ this.#log.emit("warn", "sabre.request.rawResponseHookFailed", {
1104
+ service: request.service,
1105
+ operation,
1106
+ errorName: error instanceof Error ? error.name : typeof error
1107
+ });
1108
+ }
347
1109
  }
348
- async auth(path, entity, options = {}) {
349
- this.headers.set("Authorization", `Basic ${this.secret}`);
350
- this.headers.set("Content-Type", "application/x-www-form-urlencoded");
351
- const requestOptions = {
352
- method: "POST",
353
- headers: this.headers,
354
- body: entity,
355
- ...options
1110
+ async #authorizationFor(request) {
1111
+ if (request.auth === false || this.#authorization === void 0) return void 0;
1112
+ return this.#authorization.get();
1113
+ }
1114
+ async #send(request, attempt, authorization, capture) {
1115
+ const url = this.#url(request);
1116
+ const headers = new Headers({ Accept: "application/json", ...request.headers });
1117
+ const form = request.body instanceof URLSearchParams;
1118
+ if (request.body !== void 0 && !headers.has("Content-Type")) {
1119
+ headers.set("Content-Type", form ? "application/x-www-form-urlencoded" : "application/json");
1120
+ }
1121
+ if (authorization !== void 0) headers.set("Authorization", authorization);
1122
+ const logContext = { service: request.service, method: request.method, path: request.path, attempt };
1123
+ this.#log.emit("debug", "sabre.request.start", logContext);
1124
+ const startedAt = Date.now();
1125
+ let response;
1126
+ if (capture !== void 0) capture.attempts++;
1127
+ try {
1128
+ response = await this.#config.fetch(url, {
1129
+ method: request.method,
1130
+ headers,
1131
+ ...request.body === void 0 ? {} : { body: form ? String(request.body) : JSON.stringify(request.body) },
1132
+ ...request.signal === void 0 ? {} : { signal: request.signal }
1133
+ });
1134
+ } catch (cause) {
1135
+ if (isAbort(cause, request.signal)) throw cause;
1136
+ const error = new SabreGatewayError(`${request.service}: fallo de red (${describe(cause)})`, {
1137
+ protocol: "rest",
1138
+ retryable: true,
1139
+ cause
1140
+ });
1141
+ this.#log.emit("error", "sabre.request.end", { ...logContext, durationMs: Date.now() - startedAt, error });
1142
+ throw error;
1143
+ }
1144
+ const text = await response.text();
1145
+ if (capture !== void 0) capture.last = { status: response.status, headers: response.headers, body: text };
1146
+ const body = parseBody(text);
1147
+ const endContext = { ...logContext, status: response.status, durationMs: Date.now() - startedAt };
1148
+ if (!response.ok) {
1149
+ const error = gatewayError(request.service, response.status, body);
1150
+ this.#log.emit("error", "sabre.request.end", { ...endContext, error });
1151
+ throw error;
1152
+ }
1153
+ const serviceError = request.classify?.(body, response);
1154
+ if (serviceError !== void 0) {
1155
+ this.#log.emit("warn", "sabre.request.end", { ...endContext, error: serviceError });
1156
+ throw serviceError;
1157
+ }
1158
+ this.#log.emit("info", "sabre.request.end", endContext);
1159
+ return body;
1160
+ }
1161
+ #url(request) {
1162
+ const path = request.path.startsWith("/") ? request.path : `/${request.path}`;
1163
+ const url = new URL(path, this.#config.baseUrl);
1164
+ for (const [key, value] of Object.entries(request.query ?? {})) {
1165
+ if (value !== void 0) url.searchParams.set(key, String(value));
1166
+ }
1167
+ return url.toString();
1168
+ }
1169
+ };
1170
+ function parseBody(text) {
1171
+ if (text === "") return void 0;
1172
+ try {
1173
+ return JSON.parse(text);
1174
+ } catch {
1175
+ return text;
1176
+ }
1177
+ }
1178
+ function gatewayError(service, status, body) {
1179
+ 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
+ });
1187
+ }
1188
+ function extractCode(body) {
1189
+ if (typeof body !== "object" || body === null) return void 0;
1190
+ for (const key of ["errorCode", "error"]) {
1191
+ const value = body[key];
1192
+ if (typeof value === "string" && value !== "") return value;
1193
+ }
1194
+ return void 0;
1195
+ }
1196
+ function isUnauthorized(error) {
1197
+ return error instanceof SabreGatewayError && error.protocol === "rest" && error.status === 401;
1198
+ }
1199
+ function isAbort(error, signal) {
1200
+ return signal?.aborted === true || error instanceof Error && error.name === "AbortError";
1201
+ }
1202
+ function describe(error) {
1203
+ return error instanceof Error ? error.message : String(error);
1204
+ }
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);
356
1215
  };
357
- return this.fetchRequest(path, requestOptions);
1216
+ signal?.addEventListener("abort", onAbort, { once: true });
1217
+ });
1218
+ }
1219
+
1220
+ // src/sabre.ts
1221
+ var Sabre = class {
1222
+ #config;
1223
+ #transport;
1224
+ /** Servicios de Air: shopping, revalidación. */
1225
+ air;
1226
+ /** Booking Management: creación de reservas. */
1227
+ booking;
1228
+ constructor(config) {
1229
+ this.#config = resolveConfig(config);
1230
+ this.#transport = new RestTransport(this.#config);
1231
+ const auth = new AuthManager(this.#config, this.#transport);
1232
+ this.#transport.setAuthorizationProvider(auth.bearerProvider());
1233
+ this.air = new Air(this.#transport);
1234
+ this.booking = new BookingManagement(this.#transport);
358
1235
  }
359
- getSecret() {
360
- if (!this.options.password) throw new Error("Missing Sabre password");
361
- return btoa(`${this.userID()}:${btoa(this.options.password)}`);
1236
+ get environment() {
1237
+ return this.#config.environment;
362
1238
  }
363
- userID() {
364
- return btoa(`V1:${this.options.username}:${this.options.organization}:${domain}`);
1239
+ get baseUrl() {
1240
+ return this.#config.baseUrl;
365
1241
  }
366
1242
  };
1243
+
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");
1262
+ }
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");
1274
+ }
1275
+ priceComparisons = [{ desiredAmount: amount(total), ...priceComparison }];
1276
+ }
1277
+ return {
1278
+ flights,
1279
+ flightPricing: [
1280
+ {
1281
+ ...priceComparisons === void 0 ? {} : { priceComparisons },
1282
+ qualifiers: {
1283
+ passengersPricing,
1284
+ ...fare.validatingCarrierCode === void 0 ? {} : { validatingAirlineCode: fare.validatingCarrierCode }
1285
+ }
1286
+ }
1287
+ ]
1288
+ };
1289
+ }
1290
+ function amount(value) {
1291
+ return String(Math.round(value * 1e3) / 1e3);
1292
+ }
1293
+
1294
+ // src/index.ts
1295
+ var VERSION = "0.3.0";
367
1296
  export {
368
- Sabre
1297
+ Sabre,
1298
+ SabreBusinessError,
1299
+ SabreError,
1300
+ SabreGatewayError,
1301
+ SabreOrchestrationError,
1302
+ SabreWarning,
1303
+ VERSION,
1304
+ buildBookingFlightDetails,
1305
+ buildRevalidateItineraryRequest
369
1306
  };
370
1307
  //# sourceMappingURL=index.js.map