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/LICENSE +1 -1
- package/README.es.md +294 -0
- package/README.md +237 -91
- package/dist/index.d.ts +1160 -30424
- package/dist/index.js +1244 -307
- package/dist/index.js.map +1 -1
- package/package.json +22 -40
- package/dist/index.cjs +0 -397
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -30470
package/dist/index.js
CHANGED
|
@@ -1,370 +1,1307 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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/
|
|
110
|
-
var
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
return this.sabre.post(path, payload);
|
|
391
|
+
bargainFinderMax(request, options) {
|
|
392
|
+
return bargainFinderMax(this.#transport, request, options);
|
|
142
393
|
}
|
|
143
394
|
/**
|
|
144
|
-
*
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
return this.sabre.post(path, payload);
|
|
398
|
+
revalidateItinerary(request, options) {
|
|
399
|
+
return revalidateItinerary(this.#transport, request, options);
|
|
153
400
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
*
|
|
193
|
-
*
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
*
|
|
205
|
-
*
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
*
|
|
217
|
-
*
|
|
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
|
|
224
|
-
const path =
|
|
225
|
-
|
|
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
|
-
//
|
|
230
|
-
var
|
|
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/
|
|
233
|
-
var
|
|
234
|
-
var
|
|
235
|
-
var
|
|
236
|
-
var
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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/
|
|
243
|
-
var
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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/
|
|
255
|
-
var
|
|
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
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
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
|
-
|
|
268
|
-
this
|
|
915
|
+
create(request, options) {
|
|
916
|
+
return createBooking(this.#transport, request, options);
|
|
269
917
|
}
|
|
270
918
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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/
|
|
284
|
-
var
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
this.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
return btoa(`${this.userID()}:${btoa(this.options.password)}`);
|
|
1236
|
+
get environment() {
|
|
1237
|
+
return this.#config.environment;
|
|
362
1238
|
}
|
|
363
|
-
|
|
364
|
-
return
|
|
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
|