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