sabre-client 0.3.0-next.0 → 0.3.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.es.md +304 -3
- package/README.md +302 -4
- package/dist/index.d.ts +1061 -2
- package/dist/index.js +2121 -111
- package/dist/index.js.map +1 -1
- package/dist/queue-ref-BDGh8FrV.d.ts +11 -0
- package/dist/testing.d.ts +92 -0
- package/dist/testing.js +2874 -0
- package/dist/testing.js.map +1 -0
- package/package.json +8 -1
package/README.es.md
CHANGED
|
@@ -13,7 +13,7 @@ SDK de TypeScript para las APIs de Sabre.
|
|
|
13
13
|
## Estado
|
|
14
14
|
|
|
15
15
|
`0.3.0` **todavía no está publicada**. Se publica cuando estén terminados el Tier 1 y el
|
|
16
|
-
Tier 2 del flujo de Air Booking. Mientras tanto, `npm install sabre-client` instala `0.2.4`.
|
|
16
|
+
Tier 2 del flujo de Air Booking, y el Tier 3 (sesiones SOAP y colas). Mientras tanto, `npm install sabre-client` instala `0.2.4`.
|
|
17
17
|
|
|
18
18
|
Las prereleases de la reescritura se publican con el tag `next`:
|
|
19
19
|
|
|
@@ -33,7 +33,22 @@ El Tier 1 cubre el flujo básico de Air Booking por REST:
|
|
|
33
33
|
| Reserva | Create Booking (Booking Management API v1, ATPCO por `flightDetails`) | `sabre.booking.create` |
|
|
34
34
|
| Gestión de la reserva | Get Booking y Cancel Booking (Booking Management API v1) | `sabre.booking.get`, `sabre.booking.cancel` |
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
El Tier 2 agrega la elección de asiento:
|
|
37
|
+
|
|
38
|
+
| Paso | Servicio | Método |
|
|
39
|
+
| - | - | - |
|
|
40
|
+
| Mapa de asientos | Get Seats v3 (`byReservationPayload` y `byPnrLocator`) | `sabre.air.getSeats` |
|
|
41
|
+
|
|
42
|
+
El Tier 3 agrega el cliente SOAP `SabreLegacy`, con una sesión stateful y Queue Management (ver
|
|
43
|
+
[Sesiones SOAP y colas](#sesiones-soap-y-colas-sabrelegacy)):
|
|
44
|
+
|
|
45
|
+
| Paso | Servicio | Método |
|
|
46
|
+
| - | - | - |
|
|
47
|
+
| Sesión | `SessionCreateRQ` 2.0.0 / 1.0.0, `OTA_PingRQ`, `SessionCloseRQ` | `legacy.withSession`, `legacy.openSession` |
|
|
48
|
+
| Colas | Get Queue Count, Access Queue, Place Queue Message | `session.queue.count`, `list`, `access`, `ignore`, `remove`, `place`, `exit`, `process` |
|
|
49
|
+
| Párrafo de cola | — (función pura) | `parseQueueParagraph`, `resolveHostDate` |
|
|
50
|
+
|
|
51
|
+
Planeado después de `0.3.0`: Get Ancillaries (solo NDC).
|
|
37
52
|
|
|
38
53
|
## Requisitos
|
|
39
54
|
|
|
@@ -172,6 +187,247 @@ propia:
|
|
|
172
187
|
await sabre.air.bargainFinderMax(search, { signal, retry: { maxRetries: 1 } });
|
|
173
188
|
```
|
|
174
189
|
|
|
190
|
+
## Elegir un asiento
|
|
191
|
+
|
|
192
|
+
`sabre.air.getSeats` devuelve el mapa de asientos con disponibilidad y precio. Es una sola
|
|
193
|
+
función para los dos endpoints de Get Seats: sin `pnrLocator` va a `byReservationPayload`
|
|
194
|
+
(antes de reservar), y con `pnrLocator` va a `byPnrLocator` (un PNR ya creado).
|
|
195
|
+
|
|
196
|
+
Antes de reservar, arma el request desde el itinerario revalidado con
|
|
197
|
+
`buildSeatAvailabilityRequest`, elige un asiento y pásalo a `buildBookingFlightDetails`:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { buildBookingFlightDetails, buildSeatAvailabilityRequest } from 'sabre-client';
|
|
201
|
+
|
|
202
|
+
// Después del paso 2 de "Reservar un vuelo": `confirmedGroup` y `confirmed`.
|
|
203
|
+
const seatMap = await sabre.air.getSeats(
|
|
204
|
+
buildSeatAvailabilityRequest({ group: confirmedGroup, itinerary: confirmed, currencyCode: 'USD' }),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// seatMaps[].segmentRef es S1, S2, … en el orden del itinerario. `occupationStatusCode: 'F'` es
|
|
208
|
+
// un asiento libre; uno con `offerItemRefIds` puede tener precio (offerItems[].priceDefinitionRef
|
|
209
|
+
// → priceDefinitions[]). Acá: el primer asiento libre del primer vuelo sin offer item.
|
|
210
|
+
const rows = seatMap.response.seatMaps
|
|
211
|
+
.filter((map) => map.segmentRef === 'S1')
|
|
212
|
+
.flatMap((map) => map.cabinCompartments ?? [])
|
|
213
|
+
.flatMap((cabin) => cabin.seatRows ?? []);
|
|
214
|
+
const seat = rows
|
|
215
|
+
.flatMap((row) => row.seats.map((s) => ({ number: `${row.row}${s.column}`, ...s })))
|
|
216
|
+
.find((s) => s.occupationStatusCode === 'F' && (s.offerItemRefIds ?? []).length === 0);
|
|
217
|
+
|
|
218
|
+
const booking = await sabre.booking.create({
|
|
219
|
+
flightDetails: buildBookingFlightDetails({
|
|
220
|
+
group: confirmedGroup,
|
|
221
|
+
itinerary: confirmed,
|
|
222
|
+
// Los dos índices empiezan en 1: primer vuelo del itinerario, primer viajero.
|
|
223
|
+
seats: seat === undefined ? [] : [{ segmentIndex: 1, travelerIndex: 1, number: seat.number }],
|
|
224
|
+
seatAvailability: seatMap,
|
|
225
|
+
}),
|
|
226
|
+
travelers: [{ givenName: 'JANE', surname: 'DOE', passengerCode: 'ADT' }],
|
|
227
|
+
contactInfo: { emails: ['jane@example.com'], phones: ['5550000000'] },
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Para una reserva que ya existe, pide el mapa por localizador:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const pnrSeats = await sabre.air.getSeats({ pnrLocator: booking.confirmationId! });
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A tener en cuenta:
|
|
238
|
+
|
|
239
|
+
- **El asiento se reserva por número, solo por ATPCO.** Va en `flightDetails.flights[].seats[]`
|
|
240
|
+
(`BookSeat`). `seatOffers` y el `offerId` de Get Seats son solo NDC y no están soportados.
|
|
241
|
+
El helper tampoco soporta `areaPreferences` ni `changeOfGaugeSeats`.
|
|
242
|
+
- **Mejor asientos sin cargo.** No se sabe qué hace Sabre con un asiento cobrable pedido por
|
|
243
|
+
número por ATPCO. Si pasas `seatAvailability` y el asiento elegido tiene precio,
|
|
244
|
+
`sabre.booking.create` registra el aviso `sabre.booking.seat.priced` (segmento, vuelo,
|
|
245
|
+
asiento, monto y moneda, sin datos del pasajero) y reserva igual.
|
|
246
|
+
- **Validación local.** Un número de asiento que no cumple `^[0-9]+[A-Z]$`, un `segmentIndex` o
|
|
247
|
+
`travelerIndex` que no existe, o un `pnrLocator` vacío o mezclado con campos del payload
|
|
248
|
+
(`segments`, `passengers`, `returnSeatMapsOnlyForSegmentRefs`) lanzan `TypeError` antes de
|
|
249
|
+
enviar la request.
|
|
250
|
+
- **Errores.** Get Seats no tiene catálogo de errores publicado. Un `errors[]` sin mapa lanza
|
|
251
|
+
`SabreBusinessError` no reintentable; con mapa, los errores y los `warnings[]` vuelven en
|
|
252
|
+
`seatMap.errors` y `seatMap.warnings`. Los dos endpoints son de lectura, así que los `429` y
|
|
253
|
+
`5xx` del gateway se reintentan.
|
|
254
|
+
- **`byPnrLocator` solo lee PNR del PCC con el que te autenticas.** Su request no tiene
|
|
255
|
+
`targetPcc`, a diferencia de Create, Get y Cancel Booking. Si la reserva se creó con
|
|
256
|
+
`targetPcc` en otro PCC, Sabre responde `502` con `Viewership is restricted for the PNR`,
|
|
257
|
+
que llega como `SabreGatewayError`.
|
|
258
|
+
- **Qué está comprobado.** `byReservationPayload` se probó en producción, en solo lectura, con
|
|
259
|
+
un vuelo directo y una conexión. La reserva con asiento se probó en sandbox: el asiento llega
|
|
260
|
+
al PNR y `sabre.booking.get` lo devuelve en `flights[].seats[]`. `byPnrLocator` todavía no se
|
|
261
|
+
probó contra Sabre, porque las únicas reservas posibles en sandbox viven en otro PCC. Los
|
|
262
|
+
precios llegan como número aunque el spec diga string, así que los tipos aceptan los dos.
|
|
263
|
+
|
|
264
|
+
## Sesiones SOAP y colas (`SabreLegacy`)
|
|
265
|
+
|
|
266
|
+
`SabreLegacy` es el cliente de los servicios SOAP de Sabre. Recibe la misma `SabreConfig` que
|
|
267
|
+
`Sabre`. Usuario, PCC y domain de la sesión salen de `credentials.username`
|
|
268
|
+
(`V1:<user>:<PCC>:<domain>` o `<user>-<PCC>-<domain>`). La versión de `SessionCreateRQ` sale de
|
|
269
|
+
las credenciales: 2.0.0 con `clientId` y `clientSecret`, 1.0.0 sin ellos. Si pasas solo uno de los
|
|
270
|
+
dos, o un `username` que no está en ninguno de los dos formatos, se lanza un `TypeError` al crear
|
|
271
|
+
el cliente.
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { SabreLegacy } from 'sabre-client';
|
|
275
|
+
|
|
276
|
+
const legacy = new SabreLegacy({
|
|
277
|
+
environment: 'production',
|
|
278
|
+
authVersion: 'v2',
|
|
279
|
+
credentials: { username: 'V1:<user>:<PCC>:AA', password: process.env.SABRE_EPR_PASSWORD! },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const counts = await legacy.withSession((session) => session.queue.count());
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Sesiones
|
|
286
|
+
|
|
287
|
+
- **`withSession(callback)` es la forma recomendada.** Abre una sesión, se la presta al callback y
|
|
288
|
+
la cierra siempre, también si el callback lanza. Devuelve lo mismo que el callback. Si el cierre
|
|
289
|
+
falla, registra `sabre.session.closeFailed` y conserva el resultado o el error del callback.
|
|
290
|
+
- **`openSession(options?)`** sirve para controlar tú la vida de la sesión. Se cierra con
|
|
291
|
+
`try { … } finally { await session.close() }`. `close()` es idempotente. Si el runtime tiene
|
|
292
|
+
`Symbol.asyncDispose`, también funciona `await using`, pero los tipos no lo declaran.
|
|
293
|
+
- **Una sesión es exclusiva.** Sabre no admite llamadas concurrentes en una misma sesión, así que
|
|
294
|
+
una segunda llamada mientras otra está en vuelo lanza `TypeError: sabre-client: session busy`
|
|
295
|
+
sin llamar a la red. Las llamadas después de `close()` lanzan `session closed`.
|
|
296
|
+
- **Inactividad.** Sabre documenta un timeout de 15 minutos de inactividad (en producción, las
|
|
297
|
+
sesiones duraron más).
|
|
298
|
+
`session.idleMs` es el tiempo desde la última respuesta de Sabre, y `session.ping()` lo
|
|
299
|
+
reinicia. Con `openSession({ keepAlive: true })`, el SDK manda un ping a los 13 minutos de
|
|
300
|
+
inactividad. Viene apagado, porque un timer no sobrevive entre invocaciones serverless o edge.
|
|
301
|
+
- **Sin pool, y sin reabrir sola.** Cada `withSession` abre y cierra una sesión. Después de un
|
|
302
|
+
`SabreSessionError`, la sesión queda inutilizable: las llamadas siguientes lanzan
|
|
303
|
+
`session unusable` sin red, y `close()` resuelve sin red. El SDK no la reabre, porque el cursor
|
|
304
|
+
de la cola ya se perdió. Volver a empezar es decisión tuya.
|
|
305
|
+
|
|
306
|
+
### Colas
|
|
307
|
+
|
|
308
|
+
Las operaciones de cola están en `session.queue`. `pcc` es opcional en cada llamada, y por defecto
|
|
309
|
+
es el de la sesión. Una cola es un número de 0 a 511, o `{ name }` para las colas alfabéticas
|
|
310
|
+
(`QueueRef`). Las colas que devuelve `count` se pasan tal cual a las demás operaciones.
|
|
311
|
+
|
|
312
|
+
| Método | Servicio de Sabre | Devuelve |
|
|
313
|
+
| - | - | - |
|
|
314
|
+
| `count({ pcc? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, warnings }` |
|
|
315
|
+
| `list({ queue, pcc?, primaryPassenger? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, sin entrar a la cola |
|
|
316
|
+
| `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
|
|
317
|
+
| `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
|
|
318
|
+
| `remove({ from? })` | `QueueAccessLLSRQ` (`QR`) | `QueueStep` |
|
|
319
|
+
| `place({ to, locators? })` | `QueuePlaceLLSRQ` | `QueueStep` dentro de una cola, `{ placements, text, warnings }` fuera |
|
|
320
|
+
| `exit()` | `QueueAccessLLSRQ` (`QXI`) | `void` |
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
type QueueStep =
|
|
324
|
+
| { done: false; item: QueueItem; warnings: SabreWarning[] }
|
|
325
|
+
| { done: true; reason: 'end-of-display' | 'empty'; message: string; warnings: SabreWarning[] };
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
- **El fin de cola es un resultado, no un error.** `END OF DISPLAY FOR REQUESTED DATA` da
|
|
329
|
+
`{ done: true, reason: 'end-of-display' }`, y una cola vacía (`QUEUE SELECTED WAS EMPTY`) da
|
|
330
|
+
`reason: 'empty'`. En los dos casos la sesión queda fuera de la cola. Cualquier otro error de
|
|
331
|
+
Sabre se lanza como `SabreBusinessError`.
|
|
332
|
+
- **Las operaciones que avanzan devuelven el ítem siguiente**, así que no hace falta una llamada
|
|
333
|
+
extra por PNR. Dentro de una cola, `place` mueve el PNR actual a las colas de destino y devuelve
|
|
334
|
+
el siguiente.
|
|
335
|
+
- **El SDK lleva la cuenta de si la sesión está dentro de una cola** (`session.queue.state`:
|
|
336
|
+
`'outside'`, `'inside'` o `'unknown'`). Fuera de una cola, `ignore`, `remove` y `exit` lanzan un
|
|
337
|
+
`TypeError` sin llamar a la red. Lo mismo pasa con `access` estando dentro de una cola, y con
|
|
338
|
+
`place` con `locators` dentro de una cola o en `'unknown'`. Si una operación falla
|
|
339
|
+
dentro de una cola con un error que no es de sesión, el estado pasa a `'unknown'`, porque el SDK
|
|
340
|
+
no sabe si Sabre dejó la sesión adentro. Para salir, llama a `exit()`: con `'unknown'`, tolera
|
|
341
|
+
`CANNOT DO THIS IF OFF QUEUE`.
|
|
342
|
+
- **`place`**: dentro de una cola coloca el PNR actual. Con `locators` (hasta 6), coloca esos PNR
|
|
343
|
+
desde fuera de una cola. Sin ninguno de los dos, coloca el PNR del AAA.
|
|
344
|
+
- **`instructionCode` (el PIC) es obligatorio en cada destino de `place`.** Tiene que estar en
|
|
345
|
+
0–191 o 200–254. Producción rechaza un `place` sin PIC, aunque el design lo dé como opcional.
|
|
346
|
+
- **`QueueItem`** trae `locator`, el `placedAt` y el `placedBy` de `Line`, `primaryPassenger` y
|
|
347
|
+
`group` cuando Sabre los manda, y `paragraph`: el PNR en formato de host, línea por línea y
|
|
348
|
+
sin tocar. `placedAt` es el texto que llegó, con o sin año. `resolveHostDate` lo convierte en una
|
|
349
|
+
fecha completa.
|
|
350
|
+
- `count` dentro de una cola no cuenta el PNR que está en pantalla.
|
|
351
|
+
|
|
352
|
+
### Recorrer una cola con `process`
|
|
353
|
+
|
|
354
|
+
`process` entra a la cola, llama a tu callback por cada PNR, ejecuta la acción que devuelve y
|
|
355
|
+
termina en el fin de cola. No hace nada que no se pueda hacer con las primitivas: cubre el caso
|
|
356
|
+
común y se asegura de que la sesión salga de la cola.
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
const result = await legacy.withSession((session) =>
|
|
360
|
+
session.queue.process({ queue: 12, limit: 200 }, (item) => {
|
|
361
|
+
if (isDone(item)) return { action: 'remove' };
|
|
362
|
+
if (needsReview(item)) return { action: 'place', to: [{ queue: 50, instructionCode: 11 }] };
|
|
363
|
+
return { action: 'ignore' };
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
// result: { visited: number; ended: 'end-of-display' | 'empty' | 'limit' }
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
- Al llegar a `limit`, sale de la cola con `QXI`. `limit` es opcional y no tiene tope por defecto,
|
|
370
|
+
pero conviene pasarlo.
|
|
371
|
+
- Si el callback lanza, `process` sale de la cola con `QXI` y propaga el error. No lo convierte en
|
|
372
|
+
`ignore`. Para seguir de largo, atrapa el error en el callback y devuelve `ignore`.
|
|
373
|
+
|
|
374
|
+
### Leer el párrafo
|
|
375
|
+
|
|
376
|
+
`parseQueueParagraph(item.paragraph, { referenceDate })` es una función pura aparte, que no corre
|
|
377
|
+
sola. Devuelve `{ messages, passengers, segments, ticketing, receivedFrom, unparsed }`: los avisos
|
|
378
|
+
de cola (`011 SEE REMARKS` → `{ instructionCode: 11, text: 'SEE REMARKS' }`), los nombres, los
|
|
379
|
+
segmentos con su estado, el campo de ticketing con una marca `issued` y la línea de recibido.
|
|
380
|
+
|
|
381
|
+
- **Es best effort.** El formato de host no tiene contrato. Una línea que el parser no reconoce va
|
|
382
|
+
a `unparsed`, y el parser no lanza.
|
|
383
|
+
- **`referenceDate` es obligatorio.** Las fechas del host vienen sin año (`06SEP`), así que el año
|
|
384
|
+
se resuelve contra esa fecha. Los segmentos y el ticketing toman la fecha más cercana dentro de
|
|
385
|
+
±6 meses, y la línea de recibido se resuelve hacia atrás. Las fechas salen como strings ISO sin
|
|
386
|
+
zona horaria.
|
|
387
|
+
- No lee los plazos que las aerolíneas escriben como texto libre en remarks, SSR u OSI. Su formato
|
|
388
|
+
lo pone cada aerolínea. El texto sigue en `paragraph`.
|
|
389
|
+
|
|
390
|
+
### Probar tu lógica de colas: `sabre-client/testing`
|
|
391
|
+
|
|
392
|
+
`sabre-client/testing` es una entrada aparte, con un fake de las sesiones y las colas de Sabre. No
|
|
393
|
+
forma parte del bundle principal. `createQueueFake` devuelve un `fetch` que pasas en
|
|
394
|
+
`config.fetch`, así que tu test pasa por el transporte, la sesión y el parseo reales del SDK, sin
|
|
395
|
+
red:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
import { SabreLegacy } from 'sabre-client';
|
|
399
|
+
import { createQueueFake } from 'sabre-client/testing';
|
|
400
|
+
|
|
401
|
+
const fake = createQueueFake({
|
|
402
|
+
queues: {
|
|
403
|
+
'12': [
|
|
404
|
+
[' 1.1DOE/JANE', ' 1 XX 123Y 15DEC 2 MEXCUN HK1', 'ABCD.ABCD*AAA 1200/01NOV26 AAAAAA H'],
|
|
405
|
+
{ locator: 'BBBBBB', paragraph: [' 1.1ROE/JOHN'] },
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
const legacy = new SabreLegacy({ ...config, fetch: fake.fetch });
|
|
410
|
+
|
|
411
|
+
await legacy.withSession((session) => session.queue.process({ queue: 12 }, () => ({ action: 'remove' })));
|
|
412
|
+
|
|
413
|
+
fake.locators(12); // []
|
|
414
|
+
fake.openSessions; // 0: la sesión se cerró
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
- Cada PNR es un párrafo, con el localizador tomado de su línea de firma, o
|
|
418
|
+
`{ locator, paragraph?, placedAt?, placedBy?, primaryPassenger? }`. A un párrafo sin línea de
|
|
419
|
+
firma se le agrega una sintética. `pnrs` agrega PNR que no están en ninguna cola, para
|
|
420
|
+
`place({ locators })`. El fake no tiene AAA: fuera de una cola, `place` necesita `locators`.
|
|
421
|
+
- Responde como producción: colas vacías, el fin de cola, `place` que mueve el PNR, el PIC
|
|
422
|
+
obligatorio y una sesión vencida con `fake.expireSessions()`. `fake.calls` lista lo que pidió el
|
|
423
|
+
SDK.
|
|
424
|
+
- **No simula** los PCC (las colas son las mismas para cualquier `pcc`) ni `selection`, que recibe
|
|
425
|
+
un error en vez de la cola entera.
|
|
426
|
+
- `queueFixtures` trae las respuestas anonimizadas que se grabaron en producción (`walk`,
|
|
427
|
+
`emptyQueue`, `place`…), como `{ operation, status, body }`.
|
|
428
|
+
|
|
429
|
+
`examples/queue-review.mjs` es un ejemplo completo, y su test lo corre contra el fake.
|
|
430
|
+
|
|
175
431
|
## Manejo de errores
|
|
176
432
|
|
|
177
433
|
Todos los errores que lanza el SDK extienden `SabreError`:
|
|
@@ -181,6 +437,7 @@ Todos los errores que lanza el SDK extienden `SabreError`:
|
|
|
181
437
|
| `SabreGatewayError` | Falla HTTP o del gateway (`ERR.2SG.*`, `401`, `429`, `5xx`, errores de OAuth) | `protocol`, `status`, `code` |
|
|
182
438
|
| `SabreOrchestrationError` | Error de un servicio orquestado dentro de un `200` (`SP.*`) | `code` |
|
|
183
439
|
| `SabreBusinessError` | Error del catálogo propio de un servicio (p. ej. `No Availability`, una reserva que no se creó) | `service`, `code`, `warnings`, `errors` |
|
|
440
|
+
| `SabreSessionError` | Falla de la sesión SOAP de `SabreLegacy`, subclase de `SabreGatewayError` | `kind`: `'limit'` (más de 50 sesiones), `'invalid'`, `'expired'`, `'blocked'` |
|
|
184
441
|
| `SabreWarning` | Aviso no fatal (p. ej. `category: WARNING` en Create Booking) | `service`, `code` |
|
|
185
442
|
|
|
186
443
|
Todos tienen `retryable`, que indica si la política de reintentos puede volver a intentar.
|
|
@@ -206,6 +463,41 @@ try {
|
|
|
206
463
|
> `JSON.stringify` ni en un spread, pero sigue accesible como `error.raw`. Nunca lo mandes a un
|
|
207
464
|
> logger, a un tracker de errores ni a un servicio de monitoreo sin sanitizarlo antes.
|
|
208
465
|
|
|
466
|
+
## Respuesta cruda
|
|
467
|
+
|
|
468
|
+
Toda operación REST (`air.bargainFinderMax`, `air.revalidateItinerary`, `air.getSeats`,
|
|
469
|
+
`booking.create`, `booking.get` y `booking.cancel`), y toda operación de `SabreLegacy`, acepta un
|
|
470
|
+
callback `onRawResponse` en sus
|
|
471
|
+
opciones, junto a `signal`. Recibe la respuesta de Sabre tal cual llegó, por ejemplo para
|
|
472
|
+
guardarla junto al resultado tipado. En `air.getSeats`, `operation` indica el endpoint que se
|
|
473
|
+
llamó (`air.getSeatsByPayload` o `air.getSeatsByPnrLocator`):
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
import type { SabreRawResponse } from 'sabre-client';
|
|
477
|
+
|
|
478
|
+
let raw: SabreRawResponse | undefined;
|
|
479
|
+
const booking = await sabre.booking.get({ confirmationId }, { onRawResponse: (r) => (raw = r) });
|
|
480
|
+
// raw: { operation: 'booking.get', protocol: 'rest', status, headers, body, attempts }
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
- Se llama **una vez**, con el último intento HTTP, antes de que la operación devuelva o lance.
|
|
484
|
+
También se llama cuando la operación lanza `SabreBusinessError` (un `200` con `errors[]`) o
|
|
485
|
+
`SabreGatewayError` (un no `2xx`). Si hubo reintentos, `attempts` cuenta todos los intentos HTTP.
|
|
486
|
+
- No se llama si ningún intento tuvo respuesta HTTP (error de red, abort), ni con las llamadas de
|
|
487
|
+
token. Tras un `401`, recibe la respuesta del reintento con el token nuevo.
|
|
488
|
+
- Es síncrono y su retorno se ignora. Si lanza, el SDK lo atrapa, registra
|
|
489
|
+
`sabre.request.rawResponseHookFailed` sin el body y la operación sigue: un callback que falla
|
|
490
|
+
nunca hace que `booking.create` pierda su `confirmationId`. Para guardar de forma asíncrona,
|
|
491
|
+
conserva el valor y escríbelo cuando la llamada vuelva.
|
|
492
|
+
|
|
493
|
+
En `SabreLegacy`, `protocol` es `'soap'`, `body` es el sobre SOAP completo, y `operation` es
|
|
494
|
+
`'openSession'`, `'close'`, `'ping'` o `'queue.<método>'`. El token de la sesión
|
|
495
|
+
(`BinarySecurityToken`) se reemplaza por `[REDACTED]` donde aparezca. No se cambia nada más.
|
|
496
|
+
|
|
497
|
+
> [!WARNING]
|
|
498
|
+
> `body` es la respuesta cruda y **trae datos de pasajeros**. El SDK nunca lo manda al logger.
|
|
499
|
+
> Dónde lo guardas es tu responsabilidad.
|
|
500
|
+
|
|
209
501
|
## Reintentos
|
|
210
502
|
|
|
211
503
|
Solo se reintentan las operaciones idempotentes: Bargain Finder Max y Revalidate Itinerary
|
|
@@ -213,6 +505,12 @@ Solo se reintentan las operaciones idempotentes: Bargain Finder Max y Revalidate
|
|
|
213
505
|
backoff exponencial con full jitter y un piso de 500 ms, ante `429`, `500`, `503`, `504`,
|
|
214
506
|
`SP.PROVIDER_ERROR` y los errores temporales del catálogo de Bargain Finder Max.
|
|
215
507
|
|
|
508
|
+
En `SabreLegacy`, `count`, `list` y `ping` son lecturas y se reintentan ante fallos transitorios.
|
|
509
|
+
Las operaciones que mueven el cursor de la cola (`access`, `ignore`, `remove`, `place`, `exit`) se
|
|
510
|
+
reintentan solo ante los `USG_*_BUSY`, que se toman como señal de que Sabre no procesó el request, y ante
|
|
511
|
+
`PREVIOUS ENTRY ACTIVE` (3 × 1 s). Nunca se reintentan después de un fallo de red, porque el PNR
|
|
512
|
+
pudo haberse movido: el estado de la cola pasa a `'unknown'`.
|
|
513
|
+
|
|
216
514
|
## Reutilizar el token
|
|
217
515
|
|
|
218
516
|
Por defecto el token vive en memoria, uno por instancia de `Sabre`. Para compartirlo entre
|
|
@@ -255,7 +553,10 @@ const sabre = new Sabre({
|
|
|
255
553
|
```
|
|
256
554
|
|
|
257
555
|
El SDK registra eventos estructurados (`sabre.request.start`, `sabre.request.end`,
|
|
258
|
-
`sabre.request.retry`, `sabre.
|
|
556
|
+
`sabre.request.retry`, `sabre.request.rawResponseHookFailed`, `sabre.auth.token.refresh`,
|
|
557
|
+
`sabre.auth.token.store`, `sabre.booking.seat.priced`, `sabre.session.open`,
|
|
558
|
+
`sabre.session.close`, `sabre.session.closeFailed`, `sabre.session.keepAliveFailed`,
|
|
559
|
+
`sabre.queue.exitTolerated`, `sabre.queue.exitFailed`) y nunca los bodies
|
|
259
560
|
crudos de request o response. Los secretos, el header `Authorization` y los datos de pasajeros
|
|
260
561
|
se redactan siempre antes de llegar a tu logger.
|
|
261
562
|
|