sabre-client 0.6.0 → 0.7.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 +226 -12
- package/README.md +227 -12
- package/dist/index.d.ts +297 -4
- package/dist/index.js +707 -165
- package/dist/index.js.map +1 -1
- package/dist/{reservation-history-C6giSdFd.d.ts → reservation-history-BRKQBHsj.d.ts} +150 -1
- package/dist/testing.d.ts +116 -5
- package/dist/testing.js +388 -27
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/README.es.md
CHANGED
|
@@ -12,21 +12,31 @@ SDK de TypeScript para las APIs de Sabre.
|
|
|
12
12
|
|
|
13
13
|
## Estado
|
|
14
14
|
|
|
15
|
-
La `0.
|
|
15
|
+
La `0.7.0` está publicada con el dist-tag `next` mientras se prueba; `latest` sigue en la `0.6.0`.
|
|
16
16
|
Cubre el Tier 1 y el Tier 2 del flujo de Air Booking por REST, el Tier 3 (sesiones SOAP y colas) y
|
|
17
17
|
el Tier 4: actividad de colas, historial del PNR, la cola de mensajes `S`, `selection` en `list` y
|
|
18
|
-
`onRawRequest
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
`onRawRequest`, además de los filtros de cola de la `0.6.0`. La `0.7.0` suma la emisión y el void
|
|
19
|
+
de boletos ([Emitir y anular boletos](#emitir-y-anular-boletos)), las ventas del día
|
|
20
|
+
([Ventas del día](#ventas-del-día)) y un fake REST de reservas y emisión en
|
|
21
|
+
`sabre-client/testing`. Trae un cambio incompatible, en el void (ver
|
|
22
|
+
[Actualizar desde la 0.6](#actualizar-desde-la-06)).
|
|
21
23
|
|
|
22
24
|
```sh
|
|
23
|
-
npm install sabre-client@next # 0.
|
|
24
|
-
npm install sabre-client # 0.
|
|
25
|
+
npm install sabre-client@next # 0.7.0
|
|
26
|
+
npm install sabre-client # 0.6.0
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
El paquete sigue en `0.x`: una versión minor puede traer cambios incompatibles, y el
|
|
28
30
|
[CHANGELOG](./CHANGELOG.md) los marca.
|
|
29
31
|
|
|
32
|
+
### Actualizar desde la 0.6
|
|
33
|
+
|
|
34
|
+
- **Un void que pudo no haberse hecho lanza.** Con `flightTicketOperation: 'VOID'`,
|
|
35
|
+
`booking.cancel` ya no devuelve `END_TRANSACTION_PROBLEM` como aviso: lanza
|
|
36
|
+
`SabreUncertainOutcomeError`, igual que con un fallo de red, un `5xx`, un cuerpo ilegible o un
|
|
37
|
+
abort con el request ya mandado. Atrápalo y verifica con `sabre.tickets.verify` antes de
|
|
38
|
+
reintentar (ver [Anular](#anular)). Un cancel sin `VOID` no cambia.
|
|
39
|
+
|
|
30
40
|
### Actualizar desde la 0.4
|
|
31
41
|
|
|
32
42
|
- **`pcc` ya no está en la config.** Pasarlo a `new Sabre` o a `new SabreLegacy` da un error de
|
|
@@ -49,6 +59,14 @@ El Tier 1 cubre el flujo básico de Air Booking por REST:
|
|
|
49
59
|
| Reserva | Create Booking (Booking Management API v1, ATPCO por `flightDetails`) | `sabre.booking.create` |
|
|
50
60
|
| Gestión de la reserva | Get Booking y Cancel Booking (Booking Management API v1) | `sabre.booking.get`, `sabre.booking.cancel` |
|
|
51
61
|
|
|
62
|
+
Emisión (flujo W4, `0.7.0`):
|
|
63
|
+
|
|
64
|
+
| Paso | Servicio | Método |
|
|
65
|
+
| - | - | - |
|
|
66
|
+
| Emisión | Fulfill Flight Tickets (Booking Management API v1) | `sabre.tickets.fulfill` |
|
|
67
|
+
| Verificación | Get Booking | `sabre.tickets.verify` |
|
|
68
|
+
| Void | Cancel Booking con `flightTicketOperation: 'VOID'` | `sabre.booking.cancel` |
|
|
69
|
+
|
|
52
70
|
El Tier 2 agrega la elección de asiento:
|
|
53
71
|
|
|
54
72
|
| Paso | Servicio | Método |
|
|
@@ -65,6 +83,7 @@ El Tier 3 agrega el cliente SOAP `SabreLegacy`, con una sesión stateful y Queue
|
|
|
65
83
|
| Párrafo de cola | — (función pura) | `parseQueueParagraph`, `parseQueuePlaceRemark`, `resolveHostDate` |
|
|
66
84
|
| Historial del PNR | `TravelItineraryHistoryLLSRQ` 2.4.0 | `session.reservation.history` |
|
|
67
85
|
| Comandos de host | `SabreCommandLLSRQ` 2.0.0 | `session.command` |
|
|
86
|
+
| Ventas del día | `TKT_TravelAgencyReportsRQ` / `DailySalesSummaryRQ` 1.2.2 (el «Summary» del DQB) | `session.reporting.dailySalesSummary` |
|
|
68
87
|
|
|
69
88
|
Planeado después de `0.5.0`: Get Ancillaries (solo NDC).
|
|
70
89
|
|
|
@@ -233,9 +252,9 @@ Como `create`, **nunca se reintenta automáticamente**. Las combinaciones que Sa
|
|
|
233
252
|
rechaza (`cancelAll` junto con listas, ni `cancelAll` ni listas, `offerItemId` junto con
|
|
234
253
|
`flightTicketOperation`) lanzan un `TypeError` antes de enviar nada. Si Sabre responde
|
|
235
254
|
`NO_ITEMS_CANCELLED`, la llamada lanza `SabreBusinessError` aunque Sabre lo mande como aviso,
|
|
236
|
-
porque no se canceló nada; los errores que explican el motivo están en `error.errors`.
|
|
237
|
-
`END_TRANSACTION_PROBLEM` significa que la confirmación sigue pendiente: verifica
|
|
238
|
-
con `sabre.booking.get`.
|
|
255
|
+
porque no se canceló nada; los errores que explican el motivo están en `error.errors`. Sin
|
|
256
|
+
`VOID`, el aviso `END_TRANSACTION_PROBLEM` significa que la confirmación sigue pendiente: verifica
|
|
257
|
+
el resultado con `sabre.booking.get`. Con `VOID`, lanza: ver [Anular](#anular).
|
|
239
258
|
|
|
240
259
|
Toda request acepta un `AbortSignal`. Las búsquedas aceptan además una política de reintentos
|
|
241
260
|
propia:
|
|
@@ -244,6 +263,109 @@ propia:
|
|
|
244
263
|
await sabre.air.bargainFinderMax(search, { signal, retry: { maxRetries: 1 } });
|
|
245
264
|
```
|
|
246
265
|
|
|
266
|
+
## Emitir y anular boletos
|
|
267
|
+
|
|
268
|
+
`sabre.tickets.fulfill` emite los boletos (y EMD) de una reserva con su PQ guardado (Fulfill
|
|
269
|
+
Flight Tickets). **Mueve dinero y nunca se reintenta**, ni siquiera tras un fallo de red; pasar
|
|
270
|
+
`retry.maxRetries` distinto de 0 lanza `TypeError` sin llamar a Sabre.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { SabreUncertainOutcomeError } from 'sabre-client';
|
|
274
|
+
|
|
275
|
+
const before = await sabre.booking.get({ confirmationId });
|
|
276
|
+
const knownTickets = (before.flightTickets ?? []).map((ticket) => ticket.number);
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const issued = await sabre.tickets.fulfill({
|
|
280
|
+
confirmationId,
|
|
281
|
+
fulfillments: [{ payment: { primaryFormOfPayment: 1 }, ticketingQualifiers: { commissionPercentage: '0.00' } }],
|
|
282
|
+
formsOfPayment: [{ type: 'CASH' }],
|
|
283
|
+
designatePrinters: [{ profileNumber: 1 }],
|
|
284
|
+
});
|
|
285
|
+
if (issued.status === 'partial') {
|
|
286
|
+
// Hay boletos emitidos y faltan otros: guarda estos y verifica antes de completar.
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!(error instanceof SabreUncertainOutcomeError)) throw error; // no se emitió
|
|
290
|
+
const check = await sabre.tickets.verify({
|
|
291
|
+
confirmationId,
|
|
292
|
+
knownTickets,
|
|
293
|
+
expectedTickets: error.tickets.map((ticket) => ticket.number),
|
|
294
|
+
});
|
|
295
|
+
// check.status: 'issued' | 'notCommitted' | 'notFound'
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
El SDK manda `acceptPriceChanges: false` y `priceQuoteExpirationMethod: 'Quit'` salvo que pases
|
|
300
|
+
otros valores: una emisión automática nunca paga un precio que nadie vio. Con un precio nuevo,
|
|
301
|
+
Sabre responde `PRICE_MISMATCH`.
|
|
302
|
+
|
|
303
|
+
Hay cuatro desenlaces:
|
|
304
|
+
|
|
305
|
+
| Desenlace | Qué recibes | Qué hacer |
|
|
306
|
+
| - | - | - |
|
|
307
|
+
| Emitido | `status: 'issued'`, todos los boletos con `isCommitted: true` | Guardar `tickets` |
|
|
308
|
+
| Parcial | `status: 'partial'`: algunos boletos, y `PARTIAL_FULFILLMENT` o un error | Guardar los emitidos y verificar antes de completar |
|
|
309
|
+
| No emitido | `SabreBusinessError` (`PRICE_MISMATCH`, `BOOKING_FULFILLED`…) o `SabreGatewayError` (400, 401, 403, 429) | Corregir la causa; no se emitió nada |
|
|
310
|
+
| Incierto | `SabreUncertainOutcomeError`, con `reason`, `confirmationId`, `tickets` y `cause` | Verificar con `tickets.verify` antes de pensar en reintentar |
|
|
311
|
+
|
|
312
|
+
> [!WARNING]
|
|
313
|
+
> **`partial` no es una emisión completa**: hay pasajeros sin boleto. No vuelvas a emitir a
|
|
314
|
+
> ciegas: los boletos emitidos son reales, y un segundo fulfill puede dar `BOOKING_FULFILLED` o
|
|
315
|
+
> emitir dos veces. Verifica primero.
|
|
316
|
+
|
|
317
|
+
- **Incierto** cubre `UNABLE_TO_RETRIEVE_TICKETS`, `FULFILLMENT_NOT_CONFIRMED`,
|
|
318
|
+
`INTERNAL_SERVER_TIMEOUT` (salvo que Sabre diga que no emitió boletos nuevos),
|
|
319
|
+
`DOWNLINE_SERVICE_ERROR`, `PAYMENT_CARD_ISSUE`, un fallo de red, un `5xx`, un cuerpo ilegible y
|
|
320
|
+
un abort con el request ya mandado (`ABORTED`, con el `AbortError` en `cause`). Un abort antes
|
|
321
|
+
de mandarlo se propaga tal cual.
|
|
322
|
+
- Con `reason: 'NOT_COMMITTED'`, `tickets` trae los boletos que Sabre devolvió con
|
|
323
|
+
`isCommitted: false`. **Esos boletos existen**, aunque el PNR todavía no los muestre: no vuelvas
|
|
324
|
+
a emitir.
|
|
325
|
+
- Los errores de configuración del PCC (comisión, impresora, Secure Flight) llegan como
|
|
326
|
+
`DOWNLINE_SERVICE_ERROR` y se tratan como incierto (CERT).
|
|
327
|
+
- `BOOKING_FULFILLED` quiere decir que la reserva ya tiene boletos, **no** que sean de esta
|
|
328
|
+
emisión: verifica.
|
|
329
|
+
|
|
330
|
+
`sabre.tickets.verify` lee la reserva con Get Booking y nunca emite. Mira el `ticketStatusCode`
|
|
331
|
+
de cada boleto (`TE`, `ME` o `TO` están vigentes, `TV` está anulado), no `isTicketed`, que sigue
|
|
332
|
+
en `true` después de un void:
|
|
333
|
+
|
|
334
|
+
- `issued`: hay boletos vigentes nuevos (fuera de `knownTickets`), y están todos los
|
|
335
|
+
`expectedTickets`.
|
|
336
|
+
- `notCommitted`: faltan `expectedTickets` en el PNR (un *ghost ticket*). Están emitidos: no
|
|
337
|
+
vuelvas a emitir.
|
|
338
|
+
- `notFound`: ningún boleto vigente nuevo. **No prueba que no se haya emitido**: el guardado en el
|
|
339
|
+
PNR puede tardar. Espera y verifica otra vez, y en producción revisa también las ventas del día
|
|
340
|
+
([Ventas del día](#ventas-del-día), por `pnrLocator`) antes de reintentar.
|
|
341
|
+
|
|
342
|
+
Si emitiste con `targetPcc` en `fulfill`, pasa el mismo `targetPcc` a `verify`.
|
|
343
|
+
|
|
344
|
+
### Anular
|
|
345
|
+
|
|
346
|
+
El void va por `sabre.booking.cancel` con `flightTicketOperation: 'VOID'`. Con `cancelAll` y
|
|
347
|
+
`HALT_ON_ERROR`, una sola llamada anula los boletos y cancela la reserva, y `voidedTickets` trae
|
|
348
|
+
los números anulados (CERT):
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
const result = await sabre.booking.cancel({
|
|
352
|
+
confirmationId,
|
|
353
|
+
cancelAll: true,
|
|
354
|
+
flightTicketOperation: 'VOID',
|
|
355
|
+
errorHandlingPolicy: 'HALT_ON_ERROR',
|
|
356
|
+
});
|
|
357
|
+
result.voidedTickets; // ['0060000000001']
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
- **Un void no se puede deshacer.** Un fallo de red, un `5xx`, un cuerpo ilegible, un abort con
|
|
361
|
+
el request ya mandado o `END_TRANSACTION_PROBLEM` lanzan `SabreUncertainOutcomeError`
|
|
362
|
+
(`operation: 'booking.cancel'`): no se sabe si se anularon los boletos. Verifica con
|
|
363
|
+
`sabre.tickets.verify` (un boleto anulado queda en `TV`) o con `booking.get` antes de reintentar.
|
|
364
|
+
- `VOID` sobre una reserva sin boletos lanza `NO_ITEMS_CANCELLED` con `UNABLE_TO_VOID_TICKET` y
|
|
365
|
+
**no cancela nada**. Para una reserva que nunca se emitió, cancela sin `VOID`.
|
|
366
|
+
- Una venta anulada sigue en las ventas del día como `SALE` con `documentStatusCode: 'V'`, y no
|
|
367
|
+
cuenta en `totals` (CERT).
|
|
368
|
+
|
|
247
369
|
## Elegir un asiento
|
|
248
370
|
|
|
249
371
|
`sabre.air.getSeats` devuelve el mapa de asientos con disponibilidad y precio. Es una sola
|
|
@@ -431,6 +553,44 @@ const { transactions } = await legacy.withSession((session) => session.reservati
|
|
|
431
553
|
lanza un `TypeError` sin llamar a la red. Un PNR que no existe lanza un `SabreBusinessError`
|
|
432
554
|
(`service: 'itineraryHistory'`).
|
|
433
555
|
|
|
556
|
+
### Ventas del día
|
|
557
|
+
|
|
558
|
+
`session.reporting.dailySalesSummary(request?, options?)` lee las ventas del día (el «Summary» del
|
|
559
|
+
DQB, `TKT_TravelAgencyReportsRQ` / `DailySalesSummaryRQ` 1.2.2): una transacción por movimiento del
|
|
560
|
+
PCC en ese día (ventas, reembolsos, voids, EMD y MCO), sin tope, y los totales del día.
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
const report = await legacy.withSession((session) =>
|
|
564
|
+
session.reporting.dailySalesSummary({ date: '2026-11-12' }),
|
|
565
|
+
);
|
|
566
|
+
// report.status → 'complete' | 'incomplete' | 'deferred'
|
|
567
|
+
// report.transactions[0] → { documentType: 'TKT', transactionCode: 'SALE', documentNumber,
|
|
568
|
+
// documentStatusCode, pnrLocator, passengerName, payments,
|
|
569
|
+
// transactionDateTime: '2026-11-12T11:02:00.000', … }
|
|
570
|
+
// report.totals → por tipo de transacción, settlement y moneda
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
- **Un día por llamada**, sin rango. Sin `date`, es el día en curso, hasta minutos antes de la
|
|
574
|
+
llamada (producción). Los filtros son `pcc`, `documentType`, `settlementType`, `airline` y
|
|
575
|
+
`documentNumber`.
|
|
576
|
+
- **Las fechas y horas llegan tal como las manda Sabre, sin zona**: `transactionDateTime` es
|
|
577
|
+
`'YYYY-MM-DDTHH:MM:SS.sss'`, en hora local del PCC en producción. El SDK no las convierte,
|
|
578
|
+
porque no conoce la zona.
|
|
579
|
+
- **Solo `status: 'complete'` es el día entero.** `incomplete` quiere decir que Sabre no pudo leer
|
|
580
|
+
algunos documentos (`failedDocuments`), así que faltan transacciones. `deferred` quiere decir
|
|
581
|
+
que Sabre dejó el reporte en caché (`deferred.reportId`) o sigue corriendo: vuelve a pedirlo más
|
|
582
|
+
tarde. El SDK no manda `CachedReportRQ`.
|
|
583
|
+
- Un void no agrega una transacción: la venta original sigue como `SALE` con
|
|
584
|
+
`documentStatusCode: 'V'`, y no cuenta en `totals` (CERT).
|
|
585
|
+
- Los importes van como string, tal como los manda Sabre. **Trae nombres de pasajeros**
|
|
586
|
+
(`passengerName`); el SDK nunca los registra en el log.
|
|
587
|
+
- Otro PCC necesita Branch Access. Sin él, Sabre responde `45100` y el SDK lanza
|
|
588
|
+
`SabreBusinessError` (`service: 'dailySalesSummary'`). Un filtro con forma equivocada lanza
|
|
589
|
+
`TypeError` sin llamar a la red.
|
|
590
|
+
- **En CERT el reporte puede no tener datos**: un PCC que no emite recibe un reporte `complete`
|
|
591
|
+
sin transacciones. Un reporte vacío en CERT no prueba nada.
|
|
592
|
+
- Es una lectura: se reintenta ante fallos de transporte, no ante errores de negocio.
|
|
593
|
+
|
|
434
594
|
### Colas
|
|
435
595
|
|
|
436
596
|
Las operaciones de cola están en `session.queue`. `pcc` es opcional en cada llamada, y por defecto
|
|
@@ -658,6 +818,12 @@ fake.openSessions; // 0: la sesión se cerró
|
|
|
658
818
|
`count` con `queue` cuenta los que cumplen y devuelve `totals.unfiltered`; con `range` o sin
|
|
659
819
|
cola, da los errores de Sabre. Otro código recibe un error de host marcado como no simulado,
|
|
660
820
|
nunca la cola entera.
|
|
821
|
+
- `reporting.dailySalesSummary`: `dailySales` siembra las transacciones por PCC y fecha
|
|
822
|
+
(`{ ABCD: { '2026-11-12': [transacciones] } }`, con la forma de `DailySalesTransaction`), o un
|
|
823
|
+
día con `status: 'incomplete'` o `'deferred'`, o un `errorCode` como `'45100'`. Sin `pcc` usa el
|
|
824
|
+
PCC de la sesión; sin `date`, el día de `now()` en `dailySalesTimeZone` (una zona IANA, `'UTC'`
|
|
825
|
+
por defecto). Un PCC o un día sin sembrar da un reporte `complete` vacío. El filtro
|
|
826
|
+
`documentType` se simula, los demás no, y los `totals` salen vacíos.
|
|
661
827
|
- **No simula** los PCC (las colas son las mismas para cualquier `pcc`) ni las reglas de
|
|
662
828
|
combinación de `selection` del host.
|
|
663
829
|
- `queueFixtures` trae las respuestas anonimizadas que se grabaron en producción (`walk`,
|
|
@@ -683,6 +849,50 @@ fake.openSessions; // 0: la sesión se cerró
|
|
|
683
849
|
}));
|
|
684
850
|
```
|
|
685
851
|
|
|
852
|
+
#### Reservas y emisión (REST)
|
|
853
|
+
|
|
854
|
+
El mismo `fake.fetch` responde también al cliente REST (`Sabre`): el token, Create, Get y Cancel
|
|
855
|
+
Booking, y Fulfill Flight Tickets. Pásalo en la config de `Sabre` para correr la emisión y el void
|
|
856
|
+
sin red:
|
|
857
|
+
|
|
858
|
+
```ts
|
|
859
|
+
import { Sabre } from 'sabre-client';
|
|
860
|
+
import { createQueueFake } from 'sabre-client/testing';
|
|
861
|
+
|
|
862
|
+
const fake = createQueueFake({
|
|
863
|
+
bookings: { ABCDEF: {} }, // o deja que booking.create cree FAKEAA, FAKEAB…
|
|
864
|
+
ticketing: { fulfill: [{ downline: 'COMMISSION_REQUIRED' }, 'issued'] },
|
|
865
|
+
});
|
|
866
|
+
const sabre = new Sabre({ ...config, fetch: fake.fetch });
|
|
867
|
+
|
|
868
|
+
await sabre.tickets.fulfill(request); // SabreUncertainOutcomeError, reason DOWNLINE_SERVICE_ERROR
|
|
869
|
+
await sabre.tickets.fulfill(request); // issued: un boleto TE, que aparece en booking.get
|
|
870
|
+
await sabre.booking.cancel({ confirmationId: 'ABCDEF', cancelAll: true, flightTicketOperation: 'VOID' });
|
|
871
|
+
await sabre.tickets.verify({ confirmationId: 'ABCDEF' }); // notFound: el boleto ya está en TV
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
- `tickets.fulfill` emite un boleto `TE` por reserva, que después aparece en `booking.get` y en
|
|
875
|
+
`tickets.verify`. Una segunda emisión sobre una reserva con un boleto vigente da
|
|
876
|
+
`BOOKING_FULFILLED`.
|
|
877
|
+
- `booking.cancel` con `VOID` deja los boletos vigentes en `TV`, devuelve sus números en
|
|
878
|
+
`voidedTickets` y, con `cancelAll`, cancela la reserva en la misma llamada. `isTicketed` sigue en
|
|
879
|
+
`true`, como en CERT. `VOID` sobre una reserva sin boletos da `NO_ITEMS_CANCELLED` con
|
|
880
|
+
`UNABLE_TO_VOID_TICKET` y no cancela nada.
|
|
881
|
+
- `ticketing.fulfill` y `ticketing.void` fuerzan los desenlaces siguientes, en orden:
|
|
882
|
+
- `'issued'` o `'partial'` (`PARTIAL_FULFILLMENT`);
|
|
883
|
+
- `{ uncertain: reason, issued? }`, para cada reason de `SabreUncertainOutcomeError`. `issued`
|
|
884
|
+
dice si la reserva quedó emitida de verdad, para probar tu verificación. Con `NOT_COMMITTED`
|
|
885
|
+
el boleto existe pero Get Booking no lo muestra (un *ghost ticket*);
|
|
886
|
+
- `{ downline }`, para los cuatro `DOWNLINE_SERVICE_ERROR` vistos en CERT, con su texto real:
|
|
887
|
+
`INVALID_TICKET_STOCK`, `COMMISSION_REQUIRED`, `HARDCOPY_PRINTER` y `SECURE_FLIGHT`;
|
|
888
|
+
- `{ error, description? }`, para cualquier otro error de Fulfill, como `PRICE_MISMATCH`;
|
|
889
|
+
- para el void, `'voided'` o `{ uncertain, voided? }`.
|
|
890
|
+
- `fake.calls` registra `token`, `createBooking`, `getBooking`, `cancelBooking` y `fulfill`, con el
|
|
891
|
+
localizador.
|
|
892
|
+
- **No se simula**: el itinerario, las tarifas ni los pasajeros (Get Booking devuelve una reserva
|
|
893
|
+
mínima), más de un boleto por reserva, Void Flight Tickets ni `targetPcc` (cualquier PCC ve las
|
|
894
|
+
mismas reservas).
|
|
895
|
+
|
|
686
896
|
`examples/queue-review.mjs` es un ejemplo completo, y su test lo corre contra el fake.
|
|
687
897
|
|
|
688
898
|
#### Anonimizar respuestas grabadas
|
|
@@ -729,6 +939,7 @@ Todos los errores que lanza el SDK extienden `SabreError`:
|
|
|
729
939
|
| `SabreOrchestrationError` | Error de un servicio orquestado dentro de un `200` (`SP.*`) | `code` |
|
|
730
940
|
| `SabreBusinessError` | Error del catálogo propio de un servicio (p. ej. `No Availability`, una reserva que no se creó) | `service`, `code`, `warnings`, `errors` |
|
|
731
941
|
| `SabreSessionError` | Falla de la sesión SOAP de `SabreLegacy`, subclase de `SabreGatewayError` | `kind`: `'limit'` (más de 50 sesiones), `'invalid'`, `'expired'`, `'blocked'` |
|
|
942
|
+
| `SabreUncertainOutcomeError` | La emisión o el void pudieron haberse hecho, o no (`tickets.fulfill`, `booking.cancel` con `VOID`): verifica antes de reintentar | `operation`, `confirmationId`, `reason`, `tickets`, `warnings` |
|
|
732
943
|
| `SabreWarning` | Aviso no fatal (p. ej. `category: WARNING` en Create Booking) | `service`, `code` |
|
|
733
944
|
|
|
734
945
|
Todos tienen `retryable`, que indica si la política de reintentos puede volver a intentar.
|
|
@@ -757,7 +968,8 @@ try {
|
|
|
757
968
|
## Respuesta cruda
|
|
758
969
|
|
|
759
970
|
Toda operación REST (`air.bargainFinderMax`, `air.revalidateItinerary`, `air.getSeats`,
|
|
760
|
-
`booking.create`, `booking.get` y `
|
|
971
|
+
`booking.create`, `booking.get`, `booking.cancel`, `tickets.fulfill` y `tickets.verify`), y toda
|
|
972
|
+
operación de `SabreLegacy`, acepta un
|
|
761
973
|
callback `onRawResponse` en sus
|
|
762
974
|
opciones, junto a `signal`. Recibe la respuesta de Sabre tal cual llegó, por ejemplo para
|
|
763
975
|
guardarla junto al resultado tipado. En `air.getSeats`, `operation` indica el endpoint que se
|
|
@@ -782,7 +994,8 @@ const booking = await sabre.booking.get({ confirmationId }, { onRawResponse: (r)
|
|
|
782
994
|
conserva el valor y escríbelo cuando la llamada vuelva.
|
|
783
995
|
|
|
784
996
|
En `SabreLegacy`, `protocol` es `'soap'`, `body` es el sobre SOAP completo, y `operation` es
|
|
785
|
-
`'openSession'`, `'close'`, `'ping'`, `'command'
|
|
997
|
+
`'openSession'`, `'close'`, `'ping'`, `'command'`, `'queue.<método>'`, `'reservation.history'` o
|
|
998
|
+
`'reporting.dailySalesSummary'`. `tickets.verify` reporta `'booking.get'`, el Get Booking que corre. El token de la sesión
|
|
786
999
|
(`BinarySecurityToken`) se reemplaza por `[REDACTED]` donde aparezca. No se cambia nada más.
|
|
787
1000
|
|
|
788
1001
|
> [!WARNING]
|
|
@@ -825,7 +1038,8 @@ await sabre.booking.get({ confirmationId }, { onRawRequest: (r) => (sent = r) })
|
|
|
825
1038
|
## Reintentos
|
|
826
1039
|
|
|
827
1040
|
Solo se reintentan las operaciones idempotentes: Bargain Finder Max y Revalidate Itinerary
|
|
828
|
-
(ambas son `POST` de solo lectura)
|
|
1041
|
+
(ambas son `POST` de solo lectura), además de Get Booking y `tickets.verify`. Create Booking,
|
|
1042
|
+
Cancel Booking y `tickets.fulfill` nunca se reintentan. Los reintentos usan
|
|
829
1043
|
backoff exponencial con full jitter y un piso de 500 ms, ante `429`, `500`, `503`, `504`,
|
|
830
1044
|
`SP.PROVIDER_ERROR` y los errores temporales del catálogo de Bargain Finder Max.
|
|
831
1045
|
|
package/README.md
CHANGED
|
@@ -12,21 +12,31 @@ TypeScript SDK for the Sabre APIs.
|
|
|
12
12
|
|
|
13
13
|
## Status
|
|
14
14
|
|
|
15
|
-
`0.
|
|
15
|
+
`0.7.0` is published under the `next` dist-tag while it is tested; `latest` stays on `0.6.0`.
|
|
16
16
|
It covers Tier 1 and Tier 2 of the Air Booking flow over REST, Tier 3 (SOAP sessions and queues)
|
|
17
17
|
and Tier 4: queue activity, PNR history, the `S` message queue, `selection` in `list` and
|
|
18
|
-
`onRawRequest
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
`onRawRequest`, plus the queue filters of `0.6.0`. `0.7.0` adds ticket issuance and void
|
|
19
|
+
([Issuing and voiding tickets](#issuing-and-voiding-tickets)), the day's sales report
|
|
20
|
+
([Daily sales](#daily-sales)) and a REST fake of bookings and ticketing in
|
|
21
|
+
`sabre-client/testing`. It has one breaking change, in the void (see
|
|
22
|
+
[Upgrading from 0.6](#upgrading-from-06)).
|
|
21
23
|
|
|
22
24
|
```sh
|
|
23
|
-
npm install sabre-client@next # 0.
|
|
24
|
-
npm install sabre-client # 0.
|
|
25
|
+
npm install sabre-client@next # 0.7.0
|
|
26
|
+
npm install sabre-client # 0.6.0
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
The package is still `0.x`: a minor version may include breaking changes, and the
|
|
28
30
|
[CHANGELOG](./CHANGELOG.md) marks them.
|
|
29
31
|
|
|
32
|
+
### Upgrading from 0.6
|
|
33
|
+
|
|
34
|
+
- **A void that may not have happened throws.** With `flightTicketOperation: 'VOID'`,
|
|
35
|
+
`booking.cancel` no longer returns `END_TRANSACTION_PROBLEM` as a warning: it throws
|
|
36
|
+
`SabreUncertainOutcomeError`, and so do a network failure, a `5xx`, an unreadable body and an
|
|
37
|
+
abort after the request was sent. Catch it and check with `sabre.tickets.verify` before
|
|
38
|
+
retrying (see [Voiding](#voiding)). A cancel without `VOID` is unchanged.
|
|
39
|
+
|
|
30
40
|
### Upgrading from 0.4
|
|
31
41
|
|
|
32
42
|
- **`pcc` is no longer in the config.** Passing it to `new Sabre` or `new SabreLegacy` is a type
|
|
@@ -49,6 +59,14 @@ Tier 1 covers the core Air Booking flow over REST:
|
|
|
49
59
|
| Booking | Create Booking (Booking Management API v1, ATPCO by `flightDetails`) | `sabre.booking.create` |
|
|
50
60
|
| Booking management | Get Booking and Cancel Booking (Booking Management API v1) | `sabre.booking.get`, `sabre.booking.cancel` |
|
|
51
61
|
|
|
62
|
+
Ticketing (flow W4, `0.7.0`):
|
|
63
|
+
|
|
64
|
+
| Step | Service | Method |
|
|
65
|
+
| - | - | - |
|
|
66
|
+
| Issuance | Fulfill Flight Tickets (Booking Management API v1) | `sabre.tickets.fulfill` |
|
|
67
|
+
| Verification | Get Booking | `sabre.tickets.verify` |
|
|
68
|
+
| Void | Cancel Booking with `flightTicketOperation: 'VOID'` | `sabre.booking.cancel` |
|
|
69
|
+
|
|
52
70
|
Tier 2 adds seat selection:
|
|
53
71
|
|
|
54
72
|
| Step | Service | Method |
|
|
@@ -65,6 +83,7 @@ Tier 3 adds the `SabreLegacy` SOAP client, with a stateful session and Queue Man
|
|
|
65
83
|
| Queue paragraph | — (pure function) | `parseQueueParagraph`, `parseQueuePlaceRemark`, `resolveHostDate` |
|
|
66
84
|
| PNR history | `TravelItineraryHistoryLLSRQ` 2.4.0 | `session.reservation.history` |
|
|
67
85
|
| Host commands | `SabreCommandLLSRQ` 2.0.0 | `session.command` |
|
|
86
|
+
| Daily sales | `TKT_TravelAgencyReportsRQ` / `DailySalesSummaryRQ` 1.2.2 (the DQB «Summary») | `session.reporting.dailySalesSummary` |
|
|
68
87
|
|
|
69
88
|
Planned after `0.5.0`: Get Ancillaries (NDC only).
|
|
70
89
|
|
|
@@ -233,9 +252,10 @@ Like `create`, it is **never retried automatically**. Combinations that Sabre al
|
|
|
233
252
|
(`cancelAll` together with item lists, no `cancelAll` and no list, `offerItemId` together with
|
|
234
253
|
`flightTicketOperation`) throw a `TypeError` before any request is sent. If Sabre answers
|
|
235
254
|
`NO_ITEMS_CANCELLED`, the call throws `SabreBusinessError` even though Sabre sends it as a
|
|
236
|
-
warning, because nothing was cancelled; the errors that explain why are in `error.errors`.
|
|
237
|
-
`END_TRANSACTION_PROBLEM` warning means the confirmation is still pending:
|
|
238
|
-
with `sabre.booking.get`.
|
|
255
|
+
warning, because nothing was cancelled; the errors that explain why are in `error.errors`.
|
|
256
|
+
Without `VOID`, an `END_TRANSACTION_PROBLEM` warning means the confirmation is still pending:
|
|
257
|
+
check the result with `sabre.booking.get`. With `VOID` it throws instead: see
|
|
258
|
+
[Voiding](#voiding).
|
|
239
259
|
|
|
240
260
|
Every request accepts an `AbortSignal`. Shopping calls also accept a per-request retry policy:
|
|
241
261
|
|
|
@@ -243,6 +263,109 @@ Every request accepts an `AbortSignal`. Shopping calls also accept a per-request
|
|
|
243
263
|
await sabre.air.bargainFinderMax(search, { signal, retry: { maxRetries: 1 } });
|
|
244
264
|
```
|
|
245
265
|
|
|
266
|
+
## Issuing and voiding tickets
|
|
267
|
+
|
|
268
|
+
`sabre.tickets.fulfill` issues the tickets (and EMDs) of a booking with its stored price quote
|
|
269
|
+
(Fulfill Flight Tickets). **It moves money and is never retried**, not even after a network
|
|
270
|
+
failure; passing `retry.maxRetries` other than 0 throws a `TypeError` without calling Sabre.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { SabreUncertainOutcomeError } from 'sabre-client';
|
|
274
|
+
|
|
275
|
+
const before = await sabre.booking.get({ confirmationId });
|
|
276
|
+
const knownTickets = (before.flightTickets ?? []).map((ticket) => ticket.number);
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const issued = await sabre.tickets.fulfill({
|
|
280
|
+
confirmationId,
|
|
281
|
+
fulfillments: [{ payment: { primaryFormOfPayment: 1 }, ticketingQualifiers: { commissionPercentage: '0.00' } }],
|
|
282
|
+
formsOfPayment: [{ type: 'CASH' }],
|
|
283
|
+
designatePrinters: [{ profileNumber: 1 }],
|
|
284
|
+
});
|
|
285
|
+
if (issued.status === 'partial') {
|
|
286
|
+
// Some tickets were issued and some are missing: store these and verify before completing.
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!(error instanceof SabreUncertainOutcomeError)) throw error; // not issued
|
|
290
|
+
const check = await sabre.tickets.verify({
|
|
291
|
+
confirmationId,
|
|
292
|
+
knownTickets,
|
|
293
|
+
expectedTickets: error.tickets.map((ticket) => ticket.number),
|
|
294
|
+
});
|
|
295
|
+
// check.status: 'issued' | 'notCommitted' | 'notFound'
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The SDK sends `acceptPriceChanges: false` and `priceQuoteExpirationMethod: 'Quit'` unless you
|
|
300
|
+
pass other values: an automatic issuance never pays a price nobody saw. With a new price, Sabre
|
|
301
|
+
answers `PRICE_MISMATCH`.
|
|
302
|
+
|
|
303
|
+
There are four outcomes:
|
|
304
|
+
|
|
305
|
+
| Outcome | What you get | What to do |
|
|
306
|
+
| - | - | - |
|
|
307
|
+
| Issued | `status: 'issued'`, every ticket with `isCommitted: true` | Store `tickets` |
|
|
308
|
+
| Partial | `status: 'partial'`: some tickets, and `PARTIAL_FULFILLMENT` or an error | Store the issued ones and verify before completing |
|
|
309
|
+
| Not issued | `SabreBusinessError` (`PRICE_MISMATCH`, `BOOKING_FULFILLED`…) or `SabreGatewayError` (400, 401, 403, 429) | Fix the cause; nothing was issued |
|
|
310
|
+
| Uncertain | `SabreUncertainOutcomeError`, with `reason`, `confirmationId`, `tickets` and `cause` | Verify with `tickets.verify` before thinking of retrying |
|
|
311
|
+
|
|
312
|
+
> [!WARNING]
|
|
313
|
+
> **`partial` is not a complete issuance**: some passengers have no ticket. Do not issue again
|
|
314
|
+
> blindly: the tickets that were issued are real, and a second fulfill can give
|
|
315
|
+
> `BOOKING_FULFILLED` or issue twice. Verify first.
|
|
316
|
+
|
|
317
|
+
- **Uncertain** covers `UNABLE_TO_RETRIEVE_TICKETS`, `FULFILLMENT_NOT_CONFIRMED`,
|
|
318
|
+
`INTERNAL_SERVER_TIMEOUT` (unless Sabre says no new tickets were issued),
|
|
319
|
+
`DOWNLINE_SERVICE_ERROR`, `PAYMENT_CARD_ISSUE`, a network failure, a `5xx`, an unreadable body
|
|
320
|
+
and an abort after the request was sent (`ABORTED`, with the `AbortError` in `cause`). An abort
|
|
321
|
+
before sending propagates as is.
|
|
322
|
+
- With `reason: 'NOT_COMMITTED'`, `tickets` holds the tickets Sabre returned with
|
|
323
|
+
`isCommitted: false`. **Those tickets exist**, even if the PNR does not show them yet: do not
|
|
324
|
+
issue again.
|
|
325
|
+
- PCC configuration errors (commission, printer, Secure Flight) come as `DOWNLINE_SERVICE_ERROR`
|
|
326
|
+
and are treated as uncertain (CERT).
|
|
327
|
+
- `BOOKING_FULFILLED` means the booking already has tickets, **not** that they are from this
|
|
328
|
+
issuance: verify.
|
|
329
|
+
|
|
330
|
+
`sabre.tickets.verify` reads the booking with Get Booking and never issues. It looks at each
|
|
331
|
+
ticket's `ticketStatusCode` (`TE`, `ME` or `TO` are live, `TV` is voided), not at `isTicketed`,
|
|
332
|
+
which stays `true` after a void:
|
|
333
|
+
|
|
334
|
+
- `issued`: there are new live tickets (outside `knownTickets`), and every `expectedTickets` is
|
|
335
|
+
there.
|
|
336
|
+
- `notCommitted`: some `expectedTickets` are missing from the PNR (a ghost ticket). They are
|
|
337
|
+
issued: do not issue again.
|
|
338
|
+
- `notFound`: no new live ticket. **It does not prove nothing was issued**: saving to the PNR can
|
|
339
|
+
take a while. Wait and verify again, and in production check the day's sales too
|
|
340
|
+
([Daily sales](#daily-sales), by `pnrLocator`) before retrying.
|
|
341
|
+
|
|
342
|
+
With `targetPcc` in `fulfill`, pass the same `targetPcc` to `verify`.
|
|
343
|
+
|
|
344
|
+
### Voiding
|
|
345
|
+
|
|
346
|
+
A void goes through `sabre.booking.cancel` with `flightTicketOperation: 'VOID'`. With `cancelAll`
|
|
347
|
+
and `HALT_ON_ERROR`, one call voids the tickets and cancels the booking, and `voidedTickets`
|
|
348
|
+
holds the voided numbers (CERT):
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
const result = await sabre.booking.cancel({
|
|
352
|
+
confirmationId,
|
|
353
|
+
cancelAll: true,
|
|
354
|
+
flightTicketOperation: 'VOID',
|
|
355
|
+
errorHandlingPolicy: 'HALT_ON_ERROR',
|
|
356
|
+
});
|
|
357
|
+
result.voidedTickets; // ['0060000000001']
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
- **A void cannot be undone.** A network failure, a `5xx`, an unreadable body, an abort after the
|
|
361
|
+
request was sent or `END_TRANSACTION_PROBLEM` throw `SabreUncertainOutcomeError`
|
|
362
|
+
(`operation: 'booking.cancel'`): it is not known whether the tickets were voided. Check with
|
|
363
|
+
`sabre.tickets.verify` (a voided ticket is `TV`) or `booking.get` before retrying.
|
|
364
|
+
- `VOID` on a booking without tickets throws `NO_ITEMS_CANCELLED` with `UNABLE_TO_VOID_TICKET`
|
|
365
|
+
and **cancels nothing**. For a booking that was never issued, cancel without `VOID`.
|
|
366
|
+
- A voided sale stays in the day's sales as a `SALE` with `documentStatusCode: 'V'`, and does not
|
|
367
|
+
count in `totals` (CERT).
|
|
368
|
+
|
|
246
369
|
## Choosing a seat
|
|
247
370
|
|
|
248
371
|
`sabre.air.getSeats` returns the seat map with availability and prices. It is one function
|
|
@@ -427,6 +550,44 @@ const { transactions } = await legacy.withSession((session) => session.reservati
|
|
|
427
550
|
`TypeError` without calling the network. A PNR that does not exist throws a `SabreBusinessError`
|
|
428
551
|
(`service: 'itineraryHistory'`).
|
|
429
552
|
|
|
553
|
+
### Daily sales
|
|
554
|
+
|
|
555
|
+
`session.reporting.dailySalesSummary(request?, options?)` reads the day's sales (the DQB
|
|
556
|
+
«Summary», `TKT_TravelAgencyReportsRQ` / `DailySalesSummaryRQ` 1.2.2): one transaction per movement
|
|
557
|
+
of the PCC on that day (sales, refunds, voids, EMDs and MCOs), with no cap, and the day's totals.
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
const report = await legacy.withSession((session) =>
|
|
561
|
+
session.reporting.dailySalesSummary({ date: '2026-11-12' }),
|
|
562
|
+
);
|
|
563
|
+
// report.status → 'complete' | 'incomplete' | 'deferred'
|
|
564
|
+
// report.transactions[0] → { documentType: 'TKT', transactionCode: 'SALE', documentNumber,
|
|
565
|
+
// documentStatusCode, pnrLocator, passengerName, payments,
|
|
566
|
+
// transactionDateTime: '2026-11-12T11:02:00.000', … }
|
|
567
|
+
// report.totals → per transaction type, settlement and currency
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
- **One day per call**, no range. Without `date`, it is the current day, up to a few minutes
|
|
571
|
+
before the call (production). The filters are `pcc`, `documentType`, `settlementType`,
|
|
572
|
+
`airline` and `documentNumber`.
|
|
573
|
+
- **Dates and times come as Sabre sends them, without a time zone**: `transactionDateTime` is
|
|
574
|
+
`'YYYY-MM-DDTHH:MM:SS.sss'`, in the PCC's local time in production. The SDK does not convert
|
|
575
|
+
them, because it does not know the zone.
|
|
576
|
+
- **Only `status: 'complete'` is the whole day.** `incomplete` means Sabre could not read some
|
|
577
|
+
documents (`failedDocuments`), so transactions are missing. `deferred` means Sabre cached the
|
|
578
|
+
report (`deferred.reportId`) or is still running it: ask again later. The SDK does not send
|
|
579
|
+
`CachedReportRQ`.
|
|
580
|
+
- A void does not add a transaction: the original sale stays as a `SALE` with
|
|
581
|
+
`documentStatusCode: 'V'`, and does not count in `totals` (CERT).
|
|
582
|
+
- Amounts are strings, as Sabre sends them. **It carries passenger names** (`passengerName`); the
|
|
583
|
+
SDK never logs them.
|
|
584
|
+
- Another PCC needs Branch Access. Without it Sabre answers `45100`, and the SDK throws
|
|
585
|
+
`SabreBusinessError` (`service: 'dailySalesSummary'`). A filter with the wrong shape throws a
|
|
586
|
+
`TypeError` without calling the network.
|
|
587
|
+
- **In CERT the report may have no data**: a PCC that does not issue gets a `complete` report
|
|
588
|
+
with no transactions. Do not read an empty CERT report as proof of anything.
|
|
589
|
+
- It is a read: it is retried on transport failures, not on business errors.
|
|
590
|
+
|
|
430
591
|
### Queues
|
|
431
592
|
|
|
432
593
|
The queue operations live in `session.queue`. `pcc` is optional in every call and defaults to the
|
|
@@ -652,6 +813,13 @@ fake.openSessions; // 0: the session was closed
|
|
|
652
813
|
`count` with `queue` counts the matches and returns `totals.unfiltered`; with `range` or without
|
|
653
814
|
a queue it gives Sabre's errors. Any other code gets a host error marked as not simulated,
|
|
654
815
|
never the whole queue.
|
|
816
|
+
- `reporting.dailySalesSummary`: `dailySales` seeds the transactions per PCC and date
|
|
817
|
+
(`{ ABCD: { '2026-11-12': [transactions] } }`, in the shape of `DailySalesTransaction`), or a
|
|
818
|
+
day with `status: 'incomplete'` or `'deferred'`, or an `errorCode` like `'45100'`. Without
|
|
819
|
+
`pcc` it uses the session's PCC; without `date`, the day of `now()` in `dailySalesTimeZone`
|
|
820
|
+
(an IANA zone, `'UTC'` by default). A PCC or day that is not seeded gives an empty `complete`
|
|
821
|
+
report. The `documentType` filter is simulated, the other filters are not, and `totals` come
|
|
822
|
+
back empty.
|
|
655
823
|
- **Not simulated**: PCCs (the queues are the same for any `pcc`) and the host's combination rules
|
|
656
824
|
for `selection`.
|
|
657
825
|
- `queueFixtures` holds the anonymized responses recorded in production (`walk`, `emptyQueue`,
|
|
@@ -676,6 +844,49 @@ fake.openSessions; // 0: the session was closed
|
|
|
676
844
|
}));
|
|
677
845
|
```
|
|
678
846
|
|
|
847
|
+
#### Bookings and ticketing (REST)
|
|
848
|
+
|
|
849
|
+
The same `fake.fetch` also answers the REST client (`Sabre`): the token, Create, Get and Cancel
|
|
850
|
+
Booking, and Fulfill Flight Tickets. Pass it in the `Sabre` config to run the issue-and-void flow
|
|
851
|
+
without the network:
|
|
852
|
+
|
|
853
|
+
```ts
|
|
854
|
+
import { Sabre } from 'sabre-client';
|
|
855
|
+
import { createQueueFake } from 'sabre-client/testing';
|
|
856
|
+
|
|
857
|
+
const fake = createQueueFake({
|
|
858
|
+
bookings: { ABCDEF: {} }, // or let booking.create make FAKEAA, FAKEAB…
|
|
859
|
+
ticketing: { fulfill: [{ downline: 'COMMISSION_REQUIRED' }, 'issued'] },
|
|
860
|
+
});
|
|
861
|
+
const sabre = new Sabre({ ...config, fetch: fake.fetch });
|
|
862
|
+
|
|
863
|
+
await sabre.tickets.fulfill(request); // SabreUncertainOutcomeError, reason DOWNLINE_SERVICE_ERROR
|
|
864
|
+
await sabre.tickets.fulfill(request); // issued: one TE ticket, shown by booking.get
|
|
865
|
+
await sabre.booking.cancel({ confirmationId: 'ABCDEF', cancelAll: true, flightTicketOperation: 'VOID' });
|
|
866
|
+
await sabre.tickets.verify({ confirmationId: 'ABCDEF' }); // notFound: the ticket is now TV
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
- `tickets.fulfill` issues one `TE` ticket per booking, which then shows in `booking.get` and
|
|
870
|
+
`tickets.verify`. A second fulfill on a booking with a live ticket gives `BOOKING_FULFILLED`.
|
|
871
|
+
- `booking.cancel` with `VOID` turns the live tickets into `TV`, returns their numbers in
|
|
872
|
+
`voidedTickets` and, with `cancelAll`, cancels the booking in the same call. `isTicketed` stays
|
|
873
|
+
`true`, as in CERT. `VOID` on a booking without tickets gives `NO_ITEMS_CANCELLED` with
|
|
874
|
+
`UNABLE_TO_VOID_TICKET` and cancels nothing.
|
|
875
|
+
- `ticketing.fulfill` and `ticketing.void` force the next outcomes, in order:
|
|
876
|
+
- `'issued'` or `'partial'` (`PARTIAL_FULFILLMENT`);
|
|
877
|
+
- `{ uncertain: reason, issued? }` for every reason of `SabreUncertainOutcomeError`. `issued` says
|
|
878
|
+
whether the booking really got the ticket, so you can test your verification. With
|
|
879
|
+
`NOT_COMMITTED` the ticket exists but Get Booking does not show it (a ghost ticket);
|
|
880
|
+
- `{ downline }` for the four `DOWNLINE_SERVICE_ERROR`s seen in CERT, with their real text:
|
|
881
|
+
`INVALID_TICKET_STOCK`, `COMMISSION_REQUIRED`, `HARDCOPY_PRINTER` and `SECURE_FLIGHT`;
|
|
882
|
+
- `{ error, description? }` for any other Fulfill error, such as `PRICE_MISMATCH`;
|
|
883
|
+
- for the void, `'voided'` or `{ uncertain, voided? }`.
|
|
884
|
+
- `fake.calls` records `token`, `createBooking`, `getBooking`, `cancelBooking` and `fulfill`, with
|
|
885
|
+
the locator.
|
|
886
|
+
- **Not simulated**: the itinerary, fares and passengers (Get Booking returns a minimal booking),
|
|
887
|
+
more than one ticket per booking, Void Flight Tickets, and `targetPcc` (any PCC sees the same
|
|
888
|
+
bookings).
|
|
889
|
+
|
|
679
890
|
`examples/queue-review.mjs` is a full example, and its test runs it against the fake.
|
|
680
891
|
|
|
681
892
|
#### Anonymizing recorded responses
|
|
@@ -722,6 +933,7 @@ Every error thrown by the SDK extends `SabreError`:
|
|
|
722
933
|
| `SabreOrchestrationError` | Orchestrated service error inside a `200` (`SP.*`) | `code` |
|
|
723
934
|
| `SabreBusinessError` | Error from a service's own catalog (e.g. `No Availability`, a booking that was not created) | `service`, `code`, `warnings`, `errors` |
|
|
724
935
|
| `SabreSessionError` | SOAP session failure in `SabreLegacy`, a subclass of `SabreGatewayError` | `kind`: `'limit'` (more than 50 sessions), `'invalid'`, `'expired'`, `'blocked'` |
|
|
936
|
+
| `SabreUncertainOutcomeError` | Issuance or void may or may not have happened (`tickets.fulfill`, `booking.cancel` with `VOID`): verify before retrying | `operation`, `confirmationId`, `reason`, `tickets`, `warnings` |
|
|
725
937
|
| `SabreWarning` | Non-fatal warning (e.g. `category: WARNING` in Create Booking) | `service`, `code` |
|
|
726
938
|
|
|
727
939
|
All of them have `retryable`, which says whether the retry policy may try again.
|
|
@@ -750,7 +962,8 @@ try {
|
|
|
750
962
|
## Raw response
|
|
751
963
|
|
|
752
964
|
Every REST operation (`air.bargainFinderMax`, `air.revalidateItinerary`, `air.getSeats`,
|
|
753
|
-
`booking.create`, `booking.get
|
|
965
|
+
`booking.create`, `booking.get`, `booking.cancel`, `tickets.fulfill` and `tickets.verify`), and
|
|
966
|
+
every `SabreLegacy` operation, accepts
|
|
754
967
|
an `onRawResponse` callback in its
|
|
755
968
|
options, next to `signal`. It receives Sabre's response as it arrived, for example to store it
|
|
756
969
|
next to the typed result. For `air.getSeats`, `operation` names the endpoint that was called
|
|
@@ -775,7 +988,8 @@ const booking = await sabre.booking.get({ confirmationId }, { onRawResponse: (r)
|
|
|
775
988
|
asynchronously, keep the value and write it after the call returns.
|
|
776
989
|
|
|
777
990
|
In `SabreLegacy`, `protocol` is `'soap'`, `body` is the whole SOAP envelope, and `operation` is
|
|
778
|
-
`'openSession'`, `'close'`, `'ping'`, `'command'
|
|
991
|
+
`'openSession'`, `'close'`, `'ping'`, `'command'`, `'queue.<method>'`, `'reservation.history'` or
|
|
992
|
+
`'reporting.dailySalesSummary'`. `tickets.verify` reports `'booking.get'`, the Get Booking it runs. The session token
|
|
779
993
|
(`BinarySecurityToken`) is replaced by `[REDACTED]` wherever it appears. Nothing else is changed.
|
|
780
994
|
|
|
781
995
|
> [!WARNING]
|
|
@@ -818,7 +1032,8 @@ await sabre.booking.get({ confirmationId }, { onRawRequest: (r) => (sent = r) })
|
|
|
818
1032
|
## Retries
|
|
819
1033
|
|
|
820
1034
|
Only idempotent operations are retried: Bargain Finder Max and Revalidate Itinerary (both
|
|
821
|
-
are read-only `POST`s)
|
|
1035
|
+
are read-only `POST`s), plus Get Booking and `tickets.verify`. Create Booking, Cancel Booking and
|
|
1036
|
+
`tickets.fulfill` are never retried. Retries use exponential backoff with
|
|
822
1037
|
full jitter and a 500 ms floor, on `429`, `500`, `503`, `504`, `SP.PROVIDER_ERROR` and the
|
|
823
1038
|
temporary errors in the Bargain Finder Max catalog.
|
|
824
1039
|
|