sabre-client 0.2.3 → 0.3.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.es.md +264 -0
- package/README.md +207 -91
- package/dist/index.d.ts +1119 -30411
- package/dist/index.js +1205 -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 -30457
package/dist/index.js
CHANGED
|
@@ -1,370 +1,1268 @@
|
|
|
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
|
+
});
|
|
244
|
+
return denormalizeGroupedItineraryResponse(body.groupedItineraryResponse, BARGAIN_FINDER_MAX_SERVICE);
|
|
245
|
+
}
|
|
246
|
+
function classifyBargainFinderMax(body) {
|
|
247
|
+
return classifyItineraryMessages(body, BARGAIN_FINDER_MAX_SERVICE, BARGAIN_FINDER_MAX_CATALOG);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/air/itinerary-flights.ts
|
|
251
|
+
function itineraryFlights(group, itinerary) {
|
|
252
|
+
const { legDescriptions } = group.groupDescription;
|
|
253
|
+
if (legDescriptions.length !== itinerary.legs.length) {
|
|
254
|
+
throw new TypeError(
|
|
255
|
+
`sabre-client: el itinerario tiene ${itinerary.legs.length} tramos y el grupo ${legDescriptions.length}`
|
|
103
256
|
);
|
|
104
|
-
this.sabre.setAuthorization(data.access_token);
|
|
105
|
-
return data;
|
|
106
257
|
}
|
|
107
|
-
|
|
258
|
+
const classes = bookingCodes(itinerary);
|
|
259
|
+
const flightCount = itinerary.legs.reduce((n, leg) => n + leg.schedules.length, 0);
|
|
260
|
+
if (classes.length !== flightCount) {
|
|
261
|
+
throw new TypeError(
|
|
262
|
+
`sabre-client: el itinerario tiene ${flightCount} vuelos y ${classes.length} clases de servicio en fareComponents`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
let next = 0;
|
|
266
|
+
return itinerary.legs.flatMap((leg, legIndex) => {
|
|
267
|
+
const legDate = legDescriptions[legIndex].departureDate;
|
|
268
|
+
return leg.schedules.map((schedule) => {
|
|
269
|
+
const departureDate = addDays(legDate, schedule.departureDateAdjustment ?? 0);
|
|
270
|
+
return {
|
|
271
|
+
legIndex,
|
|
272
|
+
schedule,
|
|
273
|
+
departureDate,
|
|
274
|
+
arrivalDate: addDays(departureDate, schedule.arrival.dateAdjustment ?? 0),
|
|
275
|
+
bookingCode: classes[next++]
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function bookingCodes(itinerary) {
|
|
281
|
+
const passenger = itinerary.pricingInformation[0]?.fare.passengerInfoList[0]?.passengerInfo;
|
|
282
|
+
return (passenger?.fareComponents ?? []).flatMap(
|
|
283
|
+
(component) => (component.segments ?? []).map(({ segment }) => {
|
|
284
|
+
if (segment.bookingCode === void 0) {
|
|
285
|
+
throw new TypeError("sabre-client: un segmento de fareComponents no trae bookingCode");
|
|
286
|
+
}
|
|
287
|
+
return segment.bookingCode;
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
function addDays(date, days) {
|
|
292
|
+
if (days === 0) return date;
|
|
293
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
294
|
+
return new Date(Date.UTC(year, month - 1, day + days)).toISOString().slice(0, 10);
|
|
295
|
+
}
|
|
108
296
|
|
|
109
|
-
// src/
|
|
110
|
-
var
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
*
|
|
116
|
-
* @param sabre Sabre Client Reference
|
|
117
|
-
*/
|
|
118
|
-
constructor(sabre) {
|
|
119
|
-
this.sabre = sabre;
|
|
297
|
+
// src/air/revalidate-itinerary.ts
|
|
298
|
+
var REVALIDATE_ITINERARY_SERVICE = "revalidateItinerary";
|
|
299
|
+
var PATH2 = "/v5/shop/flights/revalidate";
|
|
300
|
+
async function revalidateItinerary(transport, request, options = {}) {
|
|
301
|
+
if (typeof request !== "object" || request === null || typeof request.OTA_AirLowFareSearchRQ !== "object") {
|
|
302
|
+
throw new TypeError("sabre-client: revalidateItinerary espera { OTA_AirLowFareSearchRQ: { ... } }");
|
|
120
303
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
304
|
+
const body = await transport.request({
|
|
305
|
+
method: "POST",
|
|
306
|
+
path: PATH2,
|
|
307
|
+
service: REVALIDATE_ITINERARY_SERVICE,
|
|
308
|
+
// POST, pero de solo lectura: no sostiene inventario, se puede reintentar.
|
|
309
|
+
idempotent: true,
|
|
310
|
+
body: request,
|
|
311
|
+
classify: classifyRevalidateItinerary,
|
|
312
|
+
...options.retry === void 0 ? {} : { retry: options.retry },
|
|
313
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
314
|
+
});
|
|
315
|
+
return denormalizeGroupedItineraryResponse(body.groupedItineraryResponse, REVALIDATE_ITINERARY_SERVICE);
|
|
316
|
+
}
|
|
317
|
+
var REVALIDATE_ITINERARY_CATALOG = [
|
|
318
|
+
{ code: "NO COMBINABLE FARES FOR CLASS USED", retryable: false },
|
|
319
|
+
{ code: "NO FARE FOR CLASS USED", retryable: false },
|
|
320
|
+
{ code: "NO FLIGHT SCHEDULES FOR QUALIFIERS USED", retryable: false },
|
|
321
|
+
{ code: "NO SUCH FLIGHT", retryable: false },
|
|
322
|
+
{ code: "NO AVAILABILITY", retryable: false },
|
|
323
|
+
...BARGAIN_FINDER_MAX_CATALOG.filter((entry) => entry.retryable)
|
|
324
|
+
];
|
|
325
|
+
function classifyRevalidateItinerary(body) {
|
|
326
|
+
return classifyItineraryMessages(body, REVALIDATE_ITINERARY_SERVICE, REVALIDATE_ITINERARY_CATALOG);
|
|
327
|
+
}
|
|
328
|
+
function buildRevalidateItineraryRequest(source) {
|
|
329
|
+
const { search, group, itinerary } = source;
|
|
330
|
+
const flights = itineraryFlights(group, itinerary);
|
|
331
|
+
const originDestinations = itinerary.legs.map((_leg, i) => {
|
|
332
|
+
const legFlights = flights.filter((f) => f.legIndex === i).map(flight);
|
|
333
|
+
const first = legFlights[0];
|
|
334
|
+
const last = legFlights.at(-1);
|
|
335
|
+
return {
|
|
336
|
+
Fixed: false,
|
|
337
|
+
DepartureDateTime: first.DepartureDateTime,
|
|
338
|
+
OriginLocation: { LocationCode: first.OriginLocation.LocationCode },
|
|
339
|
+
DestinationLocation: { LocationCode: last.DestinationLocation.LocationCode },
|
|
340
|
+
TPA_Extensions: { Flight: legFlights }
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
const { Version, POS, TravelerInfoSummary, TPA_Extensions } = search.OTA_AirLowFareSearchRQ;
|
|
344
|
+
return {
|
|
345
|
+
OTA_AirLowFareSearchRQ: {
|
|
346
|
+
Version,
|
|
347
|
+
POS,
|
|
348
|
+
OriginDestinationInformation: originDestinations,
|
|
349
|
+
TravelPreferences: {
|
|
350
|
+
MaxStopsQuantity: Math.max(...itinerary.legs.map((leg) => leg.schedules.length - 1)),
|
|
351
|
+
TPA_Extensions: { VerificationItinCallLogic: { Value: "L" } }
|
|
352
|
+
},
|
|
353
|
+
TravelerInfoSummary,
|
|
354
|
+
...TPA_Extensions === void 0 ? {} : { TPA_Extensions }
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function flight({ schedule, departureDate, arrivalDate, bookingCode }) {
|
|
359
|
+
const { carrier } = schedule;
|
|
360
|
+
return {
|
|
361
|
+
Type: "A",
|
|
362
|
+
Number: carrier.marketingFlightNumber,
|
|
363
|
+
DepartureDateTime: `${departureDate}T${localTime(schedule.departure.time)}`,
|
|
364
|
+
ArrivalDateTime: `${arrivalDate}T${localTime(schedule.arrival.time)}`,
|
|
365
|
+
ClassOfService: bookingCode,
|
|
366
|
+
OriginLocation: { LocationCode: schedule.departure.airport },
|
|
367
|
+
DestinationLocation: { LocationCode: schedule.arrival.airport },
|
|
368
|
+
Airline: {
|
|
369
|
+
Marketing: carrier.marketing,
|
|
370
|
+
...carrier.operating === void 0 ? {} : { Operating: carrier.operating }
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function localTime(time) {
|
|
375
|
+
return time.slice(0, 8);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/air/air.ts
|
|
379
|
+
var Air = class {
|
|
380
|
+
#transport;
|
|
381
|
+
/** @internal Lo construye el cliente `Sabre`. */
|
|
382
|
+
constructor(transport) {
|
|
383
|
+
this.#transport = transport;
|
|
131
384
|
}
|
|
132
385
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* payload - Contiene elementos obligatorios y opcionales para realizar una solicitud de reserva.
|
|
136
|
-
* @param payload CreateBookingOptions
|
|
137
|
-
* @returns {Promise<CreateBookingResponseSuccess>}
|
|
386
|
+
* Bargain Finder Max v5: busca itinerarios de menor precio. Devuelve el
|
|
387
|
+
* `groupedItineraryResponse` con las referencias ya resueltas.
|
|
138
388
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return this.sabre.post(path, payload);
|
|
389
|
+
bargainFinderMax(request, options) {
|
|
390
|
+
return bargainFinderMax(this.#transport, request, options);
|
|
142
391
|
}
|
|
143
392
|
/**
|
|
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>}
|
|
393
|
+
* Revalidate Itinerary v5: revalida disponibilidad y precio de un itinerario ya elegido,
|
|
394
|
+
* sin sostener inventario. El request se arma con `buildRevalidateItineraryRequest`.
|
|
149
395
|
*/
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return this.sabre.post(path, payload);
|
|
396
|
+
revalidateItinerary(request, options) {
|
|
397
|
+
return revalidateItinerary(this.#transport, request, options);
|
|
153
398
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// src/logging/redact.ts
|
|
402
|
+
var REDACTED = "[REDACTED]";
|
|
403
|
+
var SENSITIVE_KEYS = new Set(
|
|
404
|
+
[
|
|
405
|
+
// Credenciales
|
|
406
|
+
"clientSecret",
|
|
407
|
+
"password",
|
|
408
|
+
"username",
|
|
409
|
+
"authorization",
|
|
410
|
+
"binarySecurityToken",
|
|
411
|
+
"accessToken",
|
|
412
|
+
"refreshToken",
|
|
413
|
+
"token",
|
|
414
|
+
// Datos de pasajero
|
|
415
|
+
"travelers",
|
|
416
|
+
"passengers"
|
|
417
|
+
].map(normalizeKey)
|
|
418
|
+
);
|
|
419
|
+
function normalizeKey(key) {
|
|
420
|
+
return key.replace(/[-_\s]/g, "").toLowerCase();
|
|
421
|
+
}
|
|
422
|
+
function isSensitiveKey(key) {
|
|
423
|
+
return SENSITIVE_KEYS.has(normalizeKey(key));
|
|
424
|
+
}
|
|
425
|
+
function redact(value) {
|
|
426
|
+
return redactValue(value, /* @__PURE__ */ new WeakSet());
|
|
427
|
+
}
|
|
428
|
+
function redactValue(value, seen) {
|
|
429
|
+
if (value === null || typeof value !== "object") return value;
|
|
430
|
+
if (seen.has(value)) return "[Circular]";
|
|
431
|
+
seen.add(value);
|
|
432
|
+
if (Array.isArray(value)) return value.map((item) => redactValue(item, seen));
|
|
433
|
+
if (value instanceof Headers) return redactEntries(value.entries(), seen);
|
|
434
|
+
if (value instanceof Date) return value.toISOString();
|
|
435
|
+
if (value instanceof Error) {
|
|
436
|
+
const json = "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : void 0;
|
|
437
|
+
return redactValue(json ?? { name: value.name, message: value.message }, seen);
|
|
438
|
+
}
|
|
439
|
+
return redactEntries(Object.entries(value), seen);
|
|
440
|
+
}
|
|
441
|
+
function redactEntries(entries, seen) {
|
|
442
|
+
const out = {};
|
|
443
|
+
for (const [key, item] of entries) {
|
|
444
|
+
out[key] = isSensitiveKey(key) ? REDACTED : redactValue(item, seen);
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/logging/logger.ts
|
|
450
|
+
var EventLogger = class {
|
|
451
|
+
#logger;
|
|
452
|
+
constructor(logger) {
|
|
453
|
+
this.#logger = logger;
|
|
454
|
+
}
|
|
455
|
+
emit(level, event, context = {}) {
|
|
456
|
+
if (this.#logger === void 0) return;
|
|
457
|
+
try {
|
|
458
|
+
this.#logger[level](event, redact(context));
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
164
461
|
}
|
|
165
462
|
};
|
|
166
463
|
|
|
167
|
-
// src/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
464
|
+
// src/auth/token-request.ts
|
|
465
|
+
function buildTokenRequestV3(credentials) {
|
|
466
|
+
return {
|
|
467
|
+
path: "/v3/auth/token",
|
|
468
|
+
authorization: `Basic ${base64(`${credentials.clientId}:${credentials.clientSecret}`)}`,
|
|
469
|
+
body: new URLSearchParams({
|
|
470
|
+
grant_type: "password",
|
|
471
|
+
username: credentials.username,
|
|
472
|
+
password: credentials.password
|
|
473
|
+
})
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function buildTokenRequestV2(credentials) {
|
|
477
|
+
return {
|
|
478
|
+
path: "/v2/auth/token",
|
|
479
|
+
authorization: `Basic ${base64(`${base64(credentials.username)}:${base64(credentials.password)}`)}`,
|
|
480
|
+
body: new URLSearchParams({ grant_type: "client_credentials" })
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function buildTokenRequest(authVersion, credentials) {
|
|
484
|
+
return authVersion === "v3" ? buildTokenRequestV3(credentials) : buildTokenRequestV2(credentials);
|
|
485
|
+
}
|
|
486
|
+
function base64(text) {
|
|
487
|
+
let binary = "";
|
|
488
|
+
for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
|
|
489
|
+
return btoa(binary);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/auth/token-store.ts
|
|
493
|
+
async function tokenStoreKey(environment, authVersion, credentials) {
|
|
494
|
+
const identity = authVersion === "v3" ? `${credentials.clientId}
|
|
495
|
+
${credentials.username}` : credentials.username;
|
|
496
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity));
|
|
497
|
+
const hash = [...new Uint8Array(digest).slice(0, 16)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
498
|
+
return `sabre-client:${environment}:${authVersion}:${hash}`;
|
|
499
|
+
}
|
|
500
|
+
function isStoredToken(value) {
|
|
501
|
+
if (typeof value !== "object" || value === null) return false;
|
|
502
|
+
const { accessToken, obtainedAt, expiresAt } = value;
|
|
503
|
+
return typeof accessToken === "string" && accessToken !== "" && typeof obtainedAt === "number" && typeof expiresAt === "number" && Number.isFinite(obtainedAt) && expiresAt > obtainedAt;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/auth/auth-manager.ts
|
|
507
|
+
var TOKEN_SERVICE = "oauthTokenCreate";
|
|
508
|
+
var REFRESH_THRESHOLD_S = 3600;
|
|
509
|
+
var AuthManager = class {
|
|
510
|
+
#config;
|
|
511
|
+
#transport;
|
|
512
|
+
#now;
|
|
513
|
+
#log;
|
|
514
|
+
#token;
|
|
515
|
+
#inFlight;
|
|
516
|
+
/** El último token cacheado fue rechazado con 401 (el próximo refresco es reactivo). */
|
|
517
|
+
#invalidated = false;
|
|
518
|
+
/** Token rechazado con 401: si el store todavía lo tiene, no se vuelve a usar. */
|
|
519
|
+
#rejected;
|
|
520
|
+
#store;
|
|
521
|
+
#storeKey;
|
|
522
|
+
constructor(config, transport, options = {}) {
|
|
523
|
+
this.#config = config;
|
|
524
|
+
this.#transport = transport;
|
|
525
|
+
this.#now = options.now ?? Date.now;
|
|
526
|
+
this.#log = new EventLogger(config.logger);
|
|
527
|
+
this.#store = config.tokenStore;
|
|
179
528
|
}
|
|
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);
|
|
529
|
+
/** Token vigente; pide uno nuevo si no hay o si cruzó el umbral de refresco. */
|
|
530
|
+
async getToken() {
|
|
531
|
+
const token = this.#token;
|
|
532
|
+
if (token !== void 0 && !this.#needsRefresh(token)) return token.accessToken;
|
|
533
|
+
const reason = token !== void 0 ? "proactive" : this.#invalidated ? "reactive" : "initial";
|
|
534
|
+
return (await this.#refresh(reason)).accessToken;
|
|
190
535
|
}
|
|
191
536
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* payload - Contiene elementos obligatorios y opcionales para el reembolso de los billetes.
|
|
196
|
-
* @param payload RefundTicketsOptions
|
|
197
|
-
* @returns {Promise<RefundTicketsResponseSuccess>}
|
|
537
|
+
* Descarta `accessToken` tras un 401 de un servicio. Si el caché ya tiene otro token
|
|
538
|
+
* (otra request lo renovó), no hace nada: los 401 concurrentes comparten un refresco.
|
|
198
539
|
*/
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
540
|
+
invalidate(accessToken) {
|
|
541
|
+
if (this.#token?.accessToken !== accessToken) return;
|
|
542
|
+
this.#token = void 0;
|
|
543
|
+
this.#invalidated = true;
|
|
544
|
+
this.#rejected = accessToken;
|
|
545
|
+
}
|
|
546
|
+
/** Adaptador de salida para el transporte REST: `Authorization: Bearer <token>`. */
|
|
547
|
+
bearerProvider() {
|
|
548
|
+
return {
|
|
549
|
+
get: async () => `Bearer ${await this.getToken()}`,
|
|
550
|
+
invalidate: (authorization) => this.invalidate(authorization.replace(/^Bearer /, ""))
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
#needsRefresh(token) {
|
|
554
|
+
const thresholdMs = Math.min(REFRESH_THRESHOLD_S, token.expiresIn / 2) * 1e3;
|
|
555
|
+
return this.#now() >= token.obtainedAt + token.expiresIn * 1e3 - thresholdMs;
|
|
556
|
+
}
|
|
557
|
+
/** Una sola petición en vuelo: las llamadas concurrentes esperan la misma promesa. */
|
|
558
|
+
#refresh(reason) {
|
|
559
|
+
this.#inFlight ??= this.#obtain(reason).finally(() => {
|
|
560
|
+
this.#inFlight = void 0;
|
|
561
|
+
});
|
|
562
|
+
return this.#inFlight;
|
|
563
|
+
}
|
|
564
|
+
/** Token del store si sigue vigente (otro proceso pudo renovarlo); si no, uno nuevo. */
|
|
565
|
+
async #obtain(reason) {
|
|
566
|
+
const stored = await this.#readStore();
|
|
567
|
+
if (stored !== void 0 && stored.accessToken !== this.#rejected && !this.#needsRefresh(stored)) {
|
|
568
|
+
this.#token = stored;
|
|
569
|
+
this.#invalidated = false;
|
|
570
|
+
return stored;
|
|
571
|
+
}
|
|
572
|
+
const token = await this.fetchToken();
|
|
573
|
+
this.#token = token;
|
|
574
|
+
this.#invalidated = false;
|
|
575
|
+
this.#rejected = void 0;
|
|
576
|
+
this.#log.emit("info", "sabre.auth.token.refresh", {
|
|
577
|
+
authVersion: this.#config.authVersion,
|
|
578
|
+
reason,
|
|
579
|
+
expiresIn: token.expiresIn
|
|
580
|
+
});
|
|
581
|
+
await this.#writeStore(token);
|
|
582
|
+
return token;
|
|
583
|
+
}
|
|
584
|
+
async #readStore() {
|
|
585
|
+
if (this.#store === void 0) return void 0;
|
|
586
|
+
try {
|
|
587
|
+
const value = await this.#store.get(await this.#key());
|
|
588
|
+
if (!isStoredToken(value)) return void 0;
|
|
589
|
+
return {
|
|
590
|
+
accessToken: value.accessToken,
|
|
591
|
+
expiresIn: (value.expiresAt - value.obtainedAt) / 1e3,
|
|
592
|
+
obtainedAt: value.obtainedAt
|
|
593
|
+
};
|
|
594
|
+
} catch (error) {
|
|
595
|
+
this.#storeFailed("get", error);
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async #writeStore(token) {
|
|
600
|
+
if (this.#store === void 0) return;
|
|
601
|
+
const stored = {
|
|
602
|
+
accessToken: token.accessToken,
|
|
603
|
+
obtainedAt: token.obtainedAt,
|
|
604
|
+
expiresAt: token.obtainedAt + token.expiresIn * 1e3
|
|
605
|
+
};
|
|
606
|
+
try {
|
|
607
|
+
await this.#store.set(await this.#key(), stored);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
this.#storeFailed("set", error);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
#key() {
|
|
613
|
+
const { environment, authVersion, credentials } = this.#config;
|
|
614
|
+
this.#storeKey ??= tokenStoreKey(environment, authVersion, credentials);
|
|
615
|
+
return this.#storeKey;
|
|
202
616
|
}
|
|
203
617
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* payload - Contiene elementos obligatorios y opcionales para consultar los tickets.
|
|
208
|
-
* @param payload CheckTicketsOptions
|
|
209
|
-
* @returns {Promise<CheckTicketsResponseSuccess>} CheckTicketsResponse
|
|
618
|
+
* El store nunca rompe la llamada: se sigue con el caché en memoria. Del error solo se
|
|
619
|
+
* loguea el nombre: el mensaje lo arma el store del consumidor y podría traer el token.
|
|
210
620
|
*/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
621
|
+
#storeFailed(operation, error) {
|
|
622
|
+
this.#log.emit("warn", "sabre.auth.token.store", {
|
|
623
|
+
authVersion: this.#config.authVersion,
|
|
624
|
+
operation,
|
|
625
|
+
errorName: error instanceof Error ? error.name : typeof error
|
|
626
|
+
});
|
|
214
627
|
}
|
|
215
628
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* payload - Contiene elementos obligatorios y opcionales para la emisión de billetes.
|
|
220
|
-
* @param payload FulfillTicketsOptions
|
|
221
|
-
* @returns {Promise<FulfillTicketsResponseSuccess>}
|
|
629
|
+
* Pide un token nuevo. Los errores del endpoint (`invalid_request`/`invalid_client` 401,
|
|
630
|
+
* `unauthorized` 403) salen como `SabreGatewayError` con `retryable: false`.
|
|
222
631
|
*/
|
|
223
|
-
async
|
|
224
|
-
const path =
|
|
225
|
-
|
|
632
|
+
async fetchToken() {
|
|
633
|
+
const { path, authorization, body } = buildTokenRequest(this.#config.authVersion, this.#config.credentials);
|
|
634
|
+
const response = await this.#transport.request({
|
|
635
|
+
method: "POST",
|
|
636
|
+
path,
|
|
637
|
+
service: TOKEN_SERVICE,
|
|
638
|
+
// Pedir otro token no tiene efectos duplicados: 429/5xx entran en la política genérica.
|
|
639
|
+
idempotent: true,
|
|
640
|
+
auth: false,
|
|
641
|
+
headers: { Authorization: authorization },
|
|
642
|
+
body
|
|
643
|
+
});
|
|
644
|
+
return this.#parse(response);
|
|
645
|
+
}
|
|
646
|
+
#parse(response) {
|
|
647
|
+
const accessToken = response?.access_token;
|
|
648
|
+
const expiresIn = response?.expires_in;
|
|
649
|
+
if (typeof accessToken !== "string" || accessToken === "" || typeof expiresIn !== "number" || !(expiresIn > 0)) {
|
|
650
|
+
throw new SabreGatewayError(`${TOKEN_SERVICE}: respuesta sin access_token/expires_in v\xE1lidos`, {
|
|
651
|
+
protocol: "rest",
|
|
652
|
+
status: 200,
|
|
653
|
+
retryable: false,
|
|
654
|
+
raw: response
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
return { accessToken, expiresIn, obtainedAt: this.#now() };
|
|
226
658
|
}
|
|
227
659
|
};
|
|
228
660
|
|
|
229
|
-
//
|
|
230
|
-
var
|
|
661
|
+
// src/booking/booking-errors.ts
|
|
662
|
+
var WARNING = "WARNING";
|
|
663
|
+
function responseErrors(response) {
|
|
664
|
+
return Array.isArray(response.errors) ? response.errors.filter(isObject2) : [];
|
|
665
|
+
}
|
|
666
|
+
function isWarning(item) {
|
|
667
|
+
return typeof item.category === "string" && item.category.toUpperCase() === WARNING;
|
|
668
|
+
}
|
|
669
|
+
function toWarning(service, item) {
|
|
670
|
+
return new SabreWarning(messageOf(service, item), { service, code: codeOf(item), raw: item });
|
|
671
|
+
}
|
|
672
|
+
function toBusinessError(service, item, { warnings = [], errors = [], retryable = false } = {}) {
|
|
673
|
+
return new SabreBusinessError(messageOf(service, item), {
|
|
674
|
+
service,
|
|
675
|
+
code: codeOf(item),
|
|
676
|
+
raw: item,
|
|
677
|
+
warnings,
|
|
678
|
+
errors,
|
|
679
|
+
retryable
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
function codeOf(item) {
|
|
683
|
+
return typeof item.type === "string" && item.type !== "" ? item.type : String(item.category);
|
|
684
|
+
}
|
|
685
|
+
function messageOf(service, item) {
|
|
686
|
+
const where = typeof item.fieldPath === "string" && item.fieldPath !== "" ? ` en ${item.fieldPath}` : "";
|
|
687
|
+
const description = typeof item.description === "string" && item.description !== "" ? `: ${item.description}` : "";
|
|
688
|
+
return `${service}: ${String(item.category)} ${codeOf(item)}${where}${description}`;
|
|
689
|
+
}
|
|
690
|
+
function nonEmpty(value) {
|
|
691
|
+
return typeof value === "string" && value !== "";
|
|
692
|
+
}
|
|
693
|
+
function isObject2(value) {
|
|
694
|
+
return typeof value === "object" && value !== null;
|
|
695
|
+
}
|
|
231
696
|
|
|
232
|
-
// src/
|
|
233
|
-
var
|
|
234
|
-
var
|
|
235
|
-
var
|
|
236
|
-
var
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
697
|
+
// src/booking/cancel-booking.ts
|
|
698
|
+
var CANCEL_BOOKING_SERVICE = "cancelBooking";
|
|
699
|
+
var PATH3 = "/v1/trip/orders/cancelBooking";
|
|
700
|
+
var NO_ITEMS_CANCELLED = "NO_ITEMS_CANCELLED";
|
|
701
|
+
var ITEM_LISTS = ["flights", "hotels", "cars", "trains", "cruises", "segments"];
|
|
702
|
+
async function cancelBooking(transport, request, options = {}) {
|
|
703
|
+
validateCancelBookingRequest(request);
|
|
704
|
+
const body = await transport.request({
|
|
705
|
+
method: "POST",
|
|
706
|
+
path: PATH3,
|
|
707
|
+
service: CANCEL_BOOKING_SERVICE,
|
|
708
|
+
idempotent: false,
|
|
709
|
+
body: request,
|
|
710
|
+
classify: classifyCancelBooking,
|
|
711
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
712
|
+
});
|
|
713
|
+
return toCancelBookingResponse(body);
|
|
714
|
+
}
|
|
715
|
+
function validateCancelBookingRequest(request) {
|
|
716
|
+
if (!isObject2(request) || !nonEmpty(request.confirmationId)) {
|
|
717
|
+
throw new TypeError("sabre-client: booking.cancel espera { confirmationId: string, ... }");
|
|
718
|
+
}
|
|
719
|
+
const lists = ITEM_LISTS.join(", ");
|
|
720
|
+
if (request.cancelAll === true) {
|
|
721
|
+
if (ITEM_LISTS.some((list) => request[list] !== void 0)) {
|
|
722
|
+
throw new TypeError(`sabre-client: booking.cancel no combina cancelAll con listas (${lists})`);
|
|
723
|
+
}
|
|
724
|
+
} else if (!ITEM_LISTS.some((list) => Array.isArray(request[list]) && request[list].length > 0)) {
|
|
725
|
+
throw new TypeError(`sabre-client: booking.cancel necesita cancelAll: true o al menos una lista (${lists})`);
|
|
726
|
+
}
|
|
727
|
+
if (request.offerItemId !== void 0 && request.flightTicketOperation !== void 0) {
|
|
728
|
+
throw new TypeError("sabre-client: booking.cancel no combina offerItemId con flightTicketOperation");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function classifyCancelBooking(body) {
|
|
732
|
+
if (!isObject2(body)) {
|
|
733
|
+
return new SabreError(`${CANCEL_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
|
|
734
|
+
retryable: false,
|
|
735
|
+
raw: body
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const items = responseErrors(body);
|
|
739
|
+
const nothingCancelled = items.find(isNoItemsCancelled);
|
|
740
|
+
const warnings = items.filter((item) => isWarning(item) && !isNoItemsCancelled(item)).map(warningOf);
|
|
741
|
+
const fatal = items.filter((item) => !isWarning(item));
|
|
742
|
+
if (nothingCancelled !== void 0) {
|
|
743
|
+
return toBusinessError(CANCEL_BOOKING_SERVICE, nothingCancelled, { warnings, errors: fatal.map(errorOf) });
|
|
744
|
+
}
|
|
745
|
+
const [first, ...rest] = fatal;
|
|
746
|
+
if (first === void 0) return void 0;
|
|
747
|
+
return toBusinessError(CANCEL_BOOKING_SERVICE, first, { warnings, errors: rest.map(errorOf) });
|
|
748
|
+
}
|
|
749
|
+
function toCancelBookingResponse(body) {
|
|
750
|
+
const { errors: _errors, ...rest } = body;
|
|
751
|
+
return { ...rest, warnings: responseErrors(body).map(warningOf) };
|
|
752
|
+
}
|
|
753
|
+
function isNoItemsCancelled(item) {
|
|
754
|
+
return item.type === NO_ITEMS_CANCELLED;
|
|
755
|
+
}
|
|
756
|
+
function warningOf(item) {
|
|
757
|
+
return toWarning(CANCEL_BOOKING_SERVICE, item);
|
|
758
|
+
}
|
|
759
|
+
function errorOf(item) {
|
|
760
|
+
return toBusinessError(CANCEL_BOOKING_SERVICE, item);
|
|
761
|
+
}
|
|
241
762
|
|
|
242
|
-
// src/
|
|
243
|
-
var
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.statusCode = statusCode;
|
|
250
|
-
this.details = details;
|
|
763
|
+
// src/booking/create-booking.ts
|
|
764
|
+
var CREATE_BOOKING_SERVICE = "createBooking";
|
|
765
|
+
var PATH4 = "/v1/trip/orders/createBooking";
|
|
766
|
+
var UNABLE_TO_RETRIEVE_BOOKING = "UNABLE_TO_RETRIEVE_BOOKING";
|
|
767
|
+
async function createBooking(transport, request, options = {}) {
|
|
768
|
+
if (typeof request !== "object" || request === null) {
|
|
769
|
+
throw new TypeError("sabre-client: booking.create espera un objeto CreateBookingRequest");
|
|
251
770
|
}
|
|
252
|
-
|
|
771
|
+
const body = await transport.request({
|
|
772
|
+
method: "POST",
|
|
773
|
+
path: PATH4,
|
|
774
|
+
service: CREATE_BOOKING_SERVICE,
|
|
775
|
+
idempotent: false,
|
|
776
|
+
body: request,
|
|
777
|
+
classify: classifyCreateBooking,
|
|
778
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
779
|
+
});
|
|
780
|
+
return toCreateBookingResponse(body);
|
|
781
|
+
}
|
|
782
|
+
function classifyCreateBooking(body) {
|
|
783
|
+
if (!isObject2(body)) {
|
|
784
|
+
return new SabreError(`${CREATE_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
|
|
785
|
+
retryable: false,
|
|
786
|
+
raw: body
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
const response = body;
|
|
790
|
+
if (confirmationIdOf(response) !== void 0) return void 0;
|
|
791
|
+
if (reservationWithoutId(response)) {
|
|
792
|
+
return new SabreError(
|
|
793
|
+
`${CREATE_BOOKING_SERVICE}: Sabre indica que la reserva se cre\xF3, pero no devolvi\xF3 su confirmationId`,
|
|
794
|
+
{ retryable: false, raw: body }
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const items = responseErrors(response);
|
|
798
|
+
const fatal = items.find((item) => !isWarning(item));
|
|
799
|
+
if (fatal !== void 0) return toBusinessError(CREATE_BOOKING_SERVICE, fatal, { warnings: items.filter(isWarning).map(warningOf2) });
|
|
800
|
+
return new SabreError(`${CREATE_BOOKING_SERVICE}: respuesta malformada, sin confirmationId ni errores fatales`, {
|
|
801
|
+
retryable: false,
|
|
802
|
+
raw: body
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
function toCreateBookingResponse(body) {
|
|
806
|
+
const items = responseErrors(body);
|
|
807
|
+
return {
|
|
808
|
+
confirmationId: confirmationIdOf(body),
|
|
809
|
+
...body.timestamp === void 0 ? {} : { timestamp: body.timestamp },
|
|
810
|
+
...body.booking === void 0 ? {} : { booking: body.booking },
|
|
811
|
+
...body.request === void 0 ? {} : { request: body.request },
|
|
812
|
+
warnings: items.filter(isWarning).map(warningOf2),
|
|
813
|
+
errors: items.filter((item) => !isWarning(item)).map((item) => toBusinessError(CREATE_BOOKING_SERVICE, item))
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function confirmationIdOf(response) {
|
|
817
|
+
if (nonEmpty(response.confirmationId)) return response.confirmationId;
|
|
818
|
+
if (nonEmpty(response.booking?.bookingId)) return response.booking.bookingId;
|
|
819
|
+
const notRetrieved = responseErrors(response).find((item) => item.type === UNABLE_TO_RETRIEVE_BOOKING);
|
|
820
|
+
return notRetrieved === void 0 ? void 0 : locatorIn(notRetrieved.description);
|
|
821
|
+
}
|
|
822
|
+
function reservationWithoutId(response) {
|
|
823
|
+
return response.booking !== void 0 || responseErrors(response).some((item) => item.type === UNABLE_TO_RETRIEVE_BOOKING);
|
|
824
|
+
}
|
|
825
|
+
function locatorIn(description) {
|
|
826
|
+
if (description === void 0) return void 0;
|
|
827
|
+
return /\(([A-Z0-9]{6,})\)/.exec(description)?.[1] ?? /\b([A-Z0-9]{6,})\b/.exec(description)?.[1];
|
|
828
|
+
}
|
|
829
|
+
function warningOf2(item) {
|
|
830
|
+
return toWarning(CREATE_BOOKING_SERVICE, item);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/booking/get-booking.ts
|
|
834
|
+
var GET_BOOKING_SERVICE = "getBooking";
|
|
835
|
+
var PATH5 = "/v1/trip/orders/getBooking";
|
|
836
|
+
var RETRYABLE_TYPES = /* @__PURE__ */ new Set(["INTERNAL_SERVER_TIMEOUT", "DOWNLINE_SERVICE_FAILURE"]);
|
|
837
|
+
async function getBooking(transport, request, options = {}) {
|
|
838
|
+
if (!isObject2(request) || !nonEmpty(request.confirmationId)) {
|
|
839
|
+
throw new TypeError("sabre-client: booking.get espera { confirmationId: string, ... }");
|
|
840
|
+
}
|
|
841
|
+
const body = await transport.request({
|
|
842
|
+
method: "POST",
|
|
843
|
+
path: PATH5,
|
|
844
|
+
service: GET_BOOKING_SERVICE,
|
|
845
|
+
idempotent: true,
|
|
846
|
+
body: request,
|
|
847
|
+
classify: classifyGetBooking,
|
|
848
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
849
|
+
});
|
|
850
|
+
return toGetBookingResponse(body);
|
|
851
|
+
}
|
|
852
|
+
function classifyGetBooking(body) {
|
|
853
|
+
if (!isObject2(body)) {
|
|
854
|
+
return new SabreError(`${GET_BOOKING_SERVICE}: respuesta malformada, se esperaba un objeto`, {
|
|
855
|
+
retryable: false,
|
|
856
|
+
raw: body
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
const response = body;
|
|
860
|
+
if (nonEmpty(response.bookingId)) return void 0;
|
|
861
|
+
const items = responseErrors(response);
|
|
862
|
+
const fatal = items.find((item) => !isWarning(item));
|
|
863
|
+
if (fatal !== void 0) return errorOf2(fatal, items.filter(isWarning).map(warningOf3));
|
|
864
|
+
return new SabreError(`${GET_BOOKING_SERVICE}: respuesta malformada, sin bookingId ni errores fatales`, {
|
|
865
|
+
retryable: false,
|
|
866
|
+
raw: body
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
function toGetBookingResponse(body) {
|
|
870
|
+
const items = responseErrors(body);
|
|
871
|
+
return {
|
|
872
|
+
...body,
|
|
873
|
+
bookingId: body.bookingId,
|
|
874
|
+
warnings: items.filter(isWarning).map(warningOf3),
|
|
875
|
+
errors: items.filter((item) => !isWarning(item)).map((item) => errorOf2(item, []))
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function warningOf3(item) {
|
|
879
|
+
return toWarning(GET_BOOKING_SERVICE, item);
|
|
880
|
+
}
|
|
881
|
+
function errorOf2(item, warnings) {
|
|
882
|
+
return toBusinessError(GET_BOOKING_SERVICE, item, { warnings, retryable: RETRYABLE_TYPES.has(item.type) });
|
|
883
|
+
}
|
|
253
884
|
|
|
254
|
-
// src/
|
|
255
|
-
var
|
|
885
|
+
// src/booking/booking-management.ts
|
|
886
|
+
var BookingManagement = class {
|
|
887
|
+
#transport;
|
|
888
|
+
/** @internal Lo construye el cliente `Sabre`. */
|
|
889
|
+
constructor(transport) {
|
|
890
|
+
this.#transport = transport;
|
|
891
|
+
}
|
|
256
892
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
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
|
|
893
|
+
* Create Booking: crea la reserva. Para un itinerario de BFM/Revalidate, `flightDetails` se
|
|
894
|
+
* arma con `buildBookingFlightDetails`.
|
|
895
|
+
*
|
|
896
|
+
* Nunca se reintenta automáticamente. Si la reserva se crea con avisos o errores no fatales,
|
|
897
|
+
* no lanza: los devuelve en `warnings` y `errors`. Si no se crea, lanza `SabreBusinessError`.
|
|
266
898
|
*/
|
|
267
|
-
|
|
268
|
-
this
|
|
899
|
+
create(request, options) {
|
|
900
|
+
return createBooking(this.#transport, request, options);
|
|
269
901
|
}
|
|
270
902
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
903
|
+
* Get Booking: lee la reserva por `confirmationId`. El `Booking` va en el primer nivel de la
|
|
904
|
+
* respuesta, a diferencia de `create`. Usa `returnOnly` para pedir solo las secciones que
|
|
905
|
+
* hacen falta: además acelera la respuesta.
|
|
906
|
+
*
|
|
907
|
+
* Si la reserva existe, no lanza: los avisos y errores parciales van en `warnings` y
|
|
908
|
+
* `errors`. Si no existe o no se pudo leer (p. ej. `BOOKING_NOT_FOUND`), lanza
|
|
909
|
+
* `SabreBusinessError`. Es una lectura: se reintenta ante fallos transitorios.
|
|
276
910
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
911
|
+
get(request, options) {
|
|
912
|
+
return getBooking(this.#transport, request, options);
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Cancel Booking: cancela la reserva entera (`cancelAll: true`) o por `itemId` (p. ej. los
|
|
916
|
+
* `flights[].itemId` de `get`). Valida antes de enviar las combinaciones que Sabre rechaza y,
|
|
917
|
+
* si encuentra una, lanza `TypeError` sin llamar a la red.
|
|
918
|
+
*
|
|
919
|
+
* Nunca se reintenta automáticamente. Lanza `SabreBusinessError` si algo falló o si Sabre
|
|
920
|
+
* responde `NO_ITEMS_CANCELLED` (no se canceló nada), aunque llegue como aviso. Con
|
|
921
|
+
* `END_TRANSACTION_PROBLEM` en `warnings`, la confirmación quedó pendiente: verifica con `get`.
|
|
922
|
+
*/
|
|
923
|
+
cancel(request, options) {
|
|
924
|
+
return cancelBooking(this.#transport, request, options);
|
|
280
925
|
}
|
|
281
926
|
};
|
|
282
927
|
|
|
283
|
-
// src/
|
|
284
|
-
var
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
928
|
+
// src/http/retry.ts
|
|
929
|
+
var DEFAULT_RETRY = Object.freeze({ maxRetries: 3, baseDelayMs: 500 });
|
|
930
|
+
var MIN_RETRY_DELAY_MS = 500;
|
|
931
|
+
function retryDelay(retry, baseDelayMs, random = Math.random) {
|
|
932
|
+
return Math.max(MIN_RETRY_DELAY_MS, Math.floor(random() * baseDelayMs * 2 ** retry));
|
|
933
|
+
}
|
|
934
|
+
function shouldRetry(error, idempotent) {
|
|
935
|
+
return idempotent && error instanceof SabreError && error.retryable;
|
|
936
|
+
}
|
|
937
|
+
function validateRetryOptions(retry, where) {
|
|
938
|
+
if (retry.maxRetries !== void 0 && !(Number.isInteger(retry.maxRetries) && retry.maxRetries >= 0)) {
|
|
939
|
+
throw new TypeError(`sabre-client: ${where}.maxRetries debe ser un entero >= 0`);
|
|
940
|
+
}
|
|
941
|
+
if (retry.baseDelayMs !== void 0 && !(Number.isFinite(retry.baseDelayMs) && retry.baseDelayMs >= 0)) {
|
|
942
|
+
throw new TypeError(`sabre-client: ${where}.baseDelayMs debe ser un n\xFAmero >= 0`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/config.ts
|
|
947
|
+
var BASE_URLS = Object.freeze({
|
|
948
|
+
sandbox: "https://api.cert.platform.sabre.com",
|
|
949
|
+
production: "https://api.platform.sabre.com"
|
|
950
|
+
});
|
|
951
|
+
var LOGGER_METHODS = ["debug", "info", "warn", "error"];
|
|
952
|
+
function fail(message) {
|
|
953
|
+
throw new TypeError(`sabre-client: ${message}`);
|
|
954
|
+
}
|
|
955
|
+
function isObject3(value) {
|
|
956
|
+
return typeof value === "object" && value !== null;
|
|
957
|
+
}
|
|
958
|
+
function requireString(value, field) {
|
|
959
|
+
if (typeof value !== "string" || value.trim() === "") fail(`${field} es requerido y debe ser un string no vac\xEDo`);
|
|
960
|
+
}
|
|
961
|
+
function resolveConfig(config) {
|
|
962
|
+
if (!isObject3(config)) fail("la config debe ser un objeto");
|
|
963
|
+
const { environment } = config;
|
|
964
|
+
if (environment !== "sandbox" && environment !== "production") {
|
|
965
|
+
fail(`environment debe ser 'sandbox' o 'production' (recibido: ${JSON.stringify(environment)})`);
|
|
966
|
+
}
|
|
967
|
+
const authVersion = config.authVersion ?? "v3";
|
|
968
|
+
if (authVersion !== "v3" && authVersion !== "v2") {
|
|
969
|
+
fail(`authVersion debe ser 'v3' o 'v2' (recibido: ${JSON.stringify(authVersion)})`);
|
|
970
|
+
}
|
|
971
|
+
const { credentials } = config;
|
|
972
|
+
if (!isObject3(credentials)) fail("credentials es requerido");
|
|
973
|
+
requireString(credentials.username, "credentials.username");
|
|
974
|
+
requireString(credentials.password, "credentials.password");
|
|
975
|
+
if (authVersion === "v3") {
|
|
976
|
+
requireString(credentials.clientId, "credentials.clientId (requerido con authVersion v3)");
|
|
977
|
+
requireString(credentials.clientSecret, "credentials.clientSecret (requerido con authVersion v3)");
|
|
978
|
+
}
|
|
979
|
+
if (config.pcc !== void 0) requireString(config.pcc, "pcc");
|
|
980
|
+
const { logger } = config;
|
|
981
|
+
if (logger !== void 0) {
|
|
982
|
+
if (!isObject3(logger)) fail("logger debe ser un objeto con debug/info/warn/error");
|
|
983
|
+
for (const method of LOGGER_METHODS) {
|
|
984
|
+
if (typeof logger[method] !== "function") fail(`logger.${method} debe ser una funci\xF3n`);
|
|
300
985
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
986
|
+
}
|
|
987
|
+
if (config.retry !== void 0) {
|
|
988
|
+
if (!isObject3(config.retry)) fail("retry debe ser un objeto");
|
|
989
|
+
validateRetryOptions(config.retry, "retry");
|
|
990
|
+
}
|
|
991
|
+
const { tokenStore } = config;
|
|
992
|
+
if (tokenStore !== void 0) {
|
|
993
|
+
if (!isObject3(tokenStore) || typeof tokenStore.get !== "function" || typeof tokenStore.set !== "function") {
|
|
994
|
+
fail("tokenStore debe ser un objeto con get(key) y set(key, token)");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
998
|
+
if (typeof fetchImpl !== "function") fail("no hay fetch disponible: pasa config.fetch");
|
|
999
|
+
return {
|
|
1000
|
+
credentials: Object.freeze({ ...credentials }),
|
|
1001
|
+
environment,
|
|
1002
|
+
baseUrl: BASE_URLS[environment],
|
|
1003
|
+
authVersion,
|
|
1004
|
+
pcc: config.pcc,
|
|
1005
|
+
logger,
|
|
1006
|
+
retry: Object.freeze({ ...DEFAULT_RETRY, ...definedOnly(config.retry ?? {}) }),
|
|
1007
|
+
// Enlazado al global para evitar "Illegal invocation" en algunos runtimes.
|
|
1008
|
+
fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
|
|
1009
|
+
tokenStore
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
function definedOnly(value) {
|
|
1013
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/http/rest-transport.ts
|
|
1017
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 503, 504]);
|
|
1018
|
+
var RestTransport = class {
|
|
1019
|
+
#config;
|
|
1020
|
+
#log;
|
|
1021
|
+
#random;
|
|
1022
|
+
#sleep;
|
|
1023
|
+
#authorization;
|
|
1024
|
+
constructor(config, options = {}) {
|
|
1025
|
+
this.#config = config;
|
|
1026
|
+
this.#log = new EventLogger(config.logger);
|
|
1027
|
+
this.#authorization = options.authorization;
|
|
1028
|
+
this.#random = options.random ?? Math.random;
|
|
1029
|
+
this.#sleep = options.sleep ?? sleep;
|
|
1030
|
+
}
|
|
1031
|
+
/** Punto de enganche para `AuthManager`. */
|
|
1032
|
+
setAuthorizationProvider(provider) {
|
|
1033
|
+
this.#authorization = provider;
|
|
1034
|
+
}
|
|
1035
|
+
async request(request) {
|
|
1036
|
+
if (request.retry !== void 0) validateRetryOptions(request.retry, "retry");
|
|
1037
|
+
const retry = { ...this.#config.retry, ...definedOnly(request.retry ?? {}) };
|
|
1038
|
+
let reauthorized = false;
|
|
1039
|
+
for (let attempt = 0; ; attempt++) {
|
|
1040
|
+
const authorization = await this.#authorizationFor(request);
|
|
323
1041
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
1042
|
+
return await this.#send(request, attempt, authorization);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
if (authorization !== void 0 && !reauthorized && isUnauthorized(error)) {
|
|
1045
|
+
reauthorized = true;
|
|
1046
|
+
this.#authorization?.invalidate(authorization);
|
|
1047
|
+
this.#log.emit("warn", "sabre.request.retry", {
|
|
1048
|
+
service: request.service,
|
|
1049
|
+
method: request.method,
|
|
1050
|
+
path: request.path,
|
|
1051
|
+
attempt: attempt + 1,
|
|
1052
|
+
reason: "unauthorized",
|
|
1053
|
+
error
|
|
1054
|
+
});
|
|
1055
|
+
attempt--;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (attempt >= retry.maxRetries || !shouldRetry(error, request.idempotent)) throw error;
|
|
1059
|
+
const delayMs = retryDelay(attempt, retry.baseDelayMs, this.#random);
|
|
1060
|
+
this.#log.emit("warn", "sabre.request.retry", {
|
|
1061
|
+
service: request.service,
|
|
1062
|
+
method: request.method,
|
|
1063
|
+
path: request.path,
|
|
1064
|
+
attempt: attempt + 1,
|
|
1065
|
+
maxRetries: retry.maxRetries,
|
|
1066
|
+
delayMs,
|
|
1067
|
+
error
|
|
1068
|
+
});
|
|
1069
|
+
await this.#sleep(delayMs, request.signal);
|
|
329
1070
|
}
|
|
330
|
-
throw new SabreError(
|
|
331
|
-
`Request failed with status ${response.status}`,
|
|
332
|
-
response.status,
|
|
333
|
-
errorDetails
|
|
334
|
-
);
|
|
335
1071
|
}
|
|
336
|
-
const data = await response.json();
|
|
337
|
-
return data;
|
|
338
1072
|
}
|
|
339
|
-
async
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
headers: this.headers,
|
|
343
|
-
body: JSON.stringify(entity),
|
|
344
|
-
...options
|
|
345
|
-
};
|
|
346
|
-
return this.fetchRequest(path, requestOptions);
|
|
1073
|
+
async #authorizationFor(request) {
|
|
1074
|
+
if (request.auth === false || this.#authorization === void 0) return void 0;
|
|
1075
|
+
return this.#authorization.get();
|
|
347
1076
|
}
|
|
348
|
-
async
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
headers:
|
|
354
|
-
|
|
355
|
-
|
|
1077
|
+
async #send(request, attempt, authorization) {
|
|
1078
|
+
const url = this.#url(request);
|
|
1079
|
+
const headers = new Headers({ Accept: "application/json", ...request.headers });
|
|
1080
|
+
const form = request.body instanceof URLSearchParams;
|
|
1081
|
+
if (request.body !== void 0 && !headers.has("Content-Type")) {
|
|
1082
|
+
headers.set("Content-Type", form ? "application/x-www-form-urlencoded" : "application/json");
|
|
1083
|
+
}
|
|
1084
|
+
if (authorization !== void 0) headers.set("Authorization", authorization);
|
|
1085
|
+
const logContext = { service: request.service, method: request.method, path: request.path, attempt };
|
|
1086
|
+
this.#log.emit("debug", "sabre.request.start", logContext);
|
|
1087
|
+
const startedAt = Date.now();
|
|
1088
|
+
let response;
|
|
1089
|
+
try {
|
|
1090
|
+
response = await this.#config.fetch(url, {
|
|
1091
|
+
method: request.method,
|
|
1092
|
+
headers,
|
|
1093
|
+
...request.body === void 0 ? {} : { body: form ? String(request.body) : JSON.stringify(request.body) },
|
|
1094
|
+
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
1095
|
+
});
|
|
1096
|
+
} catch (cause) {
|
|
1097
|
+
if (isAbort(cause, request.signal)) throw cause;
|
|
1098
|
+
const error = new SabreGatewayError(`${request.service}: fallo de red (${describe(cause)})`, {
|
|
1099
|
+
protocol: "rest",
|
|
1100
|
+
retryable: true,
|
|
1101
|
+
cause
|
|
1102
|
+
});
|
|
1103
|
+
this.#log.emit("error", "sabre.request.end", { ...logContext, durationMs: Date.now() - startedAt, error });
|
|
1104
|
+
throw error;
|
|
1105
|
+
}
|
|
1106
|
+
const body = await readBody(response);
|
|
1107
|
+
const endContext = { ...logContext, status: response.status, durationMs: Date.now() - startedAt };
|
|
1108
|
+
if (!response.ok) {
|
|
1109
|
+
const error = gatewayError(request.service, response.status, body);
|
|
1110
|
+
this.#log.emit("error", "sabre.request.end", { ...endContext, error });
|
|
1111
|
+
throw error;
|
|
1112
|
+
}
|
|
1113
|
+
const serviceError = request.classify?.(body, response);
|
|
1114
|
+
if (serviceError !== void 0) {
|
|
1115
|
+
this.#log.emit("warn", "sabre.request.end", { ...endContext, error: serviceError });
|
|
1116
|
+
throw serviceError;
|
|
1117
|
+
}
|
|
1118
|
+
this.#log.emit("info", "sabre.request.end", endContext);
|
|
1119
|
+
return body;
|
|
1120
|
+
}
|
|
1121
|
+
#url(request) {
|
|
1122
|
+
const path = request.path.startsWith("/") ? request.path : `/${request.path}`;
|
|
1123
|
+
const url = new URL(path, this.#config.baseUrl);
|
|
1124
|
+
for (const [key, value] of Object.entries(request.query ?? {})) {
|
|
1125
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
1126
|
+
}
|
|
1127
|
+
return url.toString();
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
async function readBody(response) {
|
|
1131
|
+
const text = await response.text();
|
|
1132
|
+
if (text === "") return void 0;
|
|
1133
|
+
try {
|
|
1134
|
+
return JSON.parse(text);
|
|
1135
|
+
} catch {
|
|
1136
|
+
return text;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function gatewayError(service, status, body) {
|
|
1140
|
+
const code = extractCode(body);
|
|
1141
|
+
return new SabreGatewayError(`${service}: HTTP ${status}${code === void 0 ? "" : ` ${code}`}`, {
|
|
1142
|
+
protocol: "rest",
|
|
1143
|
+
status,
|
|
1144
|
+
retryable: RETRYABLE_STATUS.has(status),
|
|
1145
|
+
raw: body,
|
|
1146
|
+
...code === void 0 ? {} : { code }
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
function extractCode(body) {
|
|
1150
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
1151
|
+
for (const key of ["errorCode", "error"]) {
|
|
1152
|
+
const value = body[key];
|
|
1153
|
+
if (typeof value === "string" && value !== "") return value;
|
|
1154
|
+
}
|
|
1155
|
+
return void 0;
|
|
1156
|
+
}
|
|
1157
|
+
function isUnauthorized(error) {
|
|
1158
|
+
return error instanceof SabreGatewayError && error.protocol === "rest" && error.status === 401;
|
|
1159
|
+
}
|
|
1160
|
+
function isAbort(error, signal) {
|
|
1161
|
+
return signal?.aborted === true || error instanceof Error && error.name === "AbortError";
|
|
1162
|
+
}
|
|
1163
|
+
function describe(error) {
|
|
1164
|
+
return error instanceof Error ? error.message : String(error);
|
|
1165
|
+
}
|
|
1166
|
+
function sleep(ms, signal) {
|
|
1167
|
+
return new Promise((resolve, reject) => {
|
|
1168
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
1169
|
+
const timer = setTimeout(() => {
|
|
1170
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1171
|
+
resolve();
|
|
1172
|
+
}, ms);
|
|
1173
|
+
const onAbort = () => {
|
|
1174
|
+
clearTimeout(timer);
|
|
1175
|
+
reject(signal?.reason);
|
|
356
1176
|
};
|
|
357
|
-
|
|
1177
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/sabre.ts
|
|
1182
|
+
var Sabre = class {
|
|
1183
|
+
#config;
|
|
1184
|
+
#transport;
|
|
1185
|
+
/** Servicios de Air: shopping, revalidación. */
|
|
1186
|
+
air;
|
|
1187
|
+
/** Booking Management: creación de reservas. */
|
|
1188
|
+
booking;
|
|
1189
|
+
constructor(config) {
|
|
1190
|
+
this.#config = resolveConfig(config);
|
|
1191
|
+
this.#transport = new RestTransport(this.#config);
|
|
1192
|
+
const auth = new AuthManager(this.#config, this.#transport);
|
|
1193
|
+
this.#transport.setAuthorizationProvider(auth.bearerProvider());
|
|
1194
|
+
this.air = new Air(this.#transport);
|
|
1195
|
+
this.booking = new BookingManagement(this.#transport);
|
|
358
1196
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return btoa(`${this.userID()}:${btoa(this.options.password)}`);
|
|
1197
|
+
get environment() {
|
|
1198
|
+
return this.#config.environment;
|
|
362
1199
|
}
|
|
363
|
-
|
|
364
|
-
return
|
|
1200
|
+
get baseUrl() {
|
|
1201
|
+
return this.#config.baseUrl;
|
|
365
1202
|
}
|
|
366
1203
|
};
|
|
1204
|
+
|
|
1205
|
+
// src/booking/flight-details.ts
|
|
1206
|
+
function buildBookingFlightDetails(source) {
|
|
1207
|
+
const { group, itinerary, priceComparison } = source;
|
|
1208
|
+
const flights = itineraryFlights(group, itinerary).map(
|
|
1209
|
+
({ schedule, departureDate, bookingCode }) => ({
|
|
1210
|
+
flightNumber: schedule.carrier.marketingFlightNumber,
|
|
1211
|
+
airlineCode: schedule.carrier.marketing,
|
|
1212
|
+
fromAirportCode: schedule.departure.airport,
|
|
1213
|
+
toAirportCode: schedule.arrival.airport,
|
|
1214
|
+
departureDate,
|
|
1215
|
+
departureTime: schedule.departure.time.slice(0, 5),
|
|
1216
|
+
bookingClass: bookingCode,
|
|
1217
|
+
flightStatusCode: "NN"
|
|
1218
|
+
})
|
|
1219
|
+
);
|
|
1220
|
+
const fare = itinerary.pricingInformation[0]?.fare;
|
|
1221
|
+
if (fare === void 0) {
|
|
1222
|
+
throw new TypeError("sabre-client: el itinerario no trae pricingInformation");
|
|
1223
|
+
}
|
|
1224
|
+
const passengersPricing = fare.passengerInfoList.map(
|
|
1225
|
+
({ passengerInfo }) => ({
|
|
1226
|
+
passengerCode: passengerInfo.passengerType,
|
|
1227
|
+
numberOfpassengers: passengerInfo.passengerNumber ?? 1
|
|
1228
|
+
})
|
|
1229
|
+
);
|
|
1230
|
+
let priceComparisons;
|
|
1231
|
+
if (priceComparison !== void 0) {
|
|
1232
|
+
const total = fare.totalFare?.totalPrice;
|
|
1233
|
+
if (total === void 0) {
|
|
1234
|
+
throw new TypeError("sabre-client: el itinerario no trae totalFare.totalPrice para priceComparison");
|
|
1235
|
+
}
|
|
1236
|
+
priceComparisons = [{ desiredAmount: amount(total), ...priceComparison }];
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1239
|
+
flights,
|
|
1240
|
+
flightPricing: [
|
|
1241
|
+
{
|
|
1242
|
+
...priceComparisons === void 0 ? {} : { priceComparisons },
|
|
1243
|
+
qualifiers: {
|
|
1244
|
+
passengersPricing,
|
|
1245
|
+
...fare.validatingCarrierCode === void 0 ? {} : { validatingAirlineCode: fare.validatingCarrierCode }
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
]
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function amount(value) {
|
|
1252
|
+
return String(Math.round(value * 1e3) / 1e3);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/index.ts
|
|
1256
|
+
var VERSION = "0.3.0";
|
|
367
1257
|
export {
|
|
368
|
-
Sabre
|
|
1258
|
+
Sabre,
|
|
1259
|
+
SabreBusinessError,
|
|
1260
|
+
SabreError,
|
|
1261
|
+
SabreGatewayError,
|
|
1262
|
+
SabreOrchestrationError,
|
|
1263
|
+
SabreWarning,
|
|
1264
|
+
VERSION,
|
|
1265
|
+
buildBookingFlightDetails,
|
|
1266
|
+
buildRevalidateItineraryRequest
|
|
369
1267
|
};
|
|
370
1268
|
//# sourceMappingURL=index.js.map
|