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 CHANGED
@@ -12,21 +12,31 @@ SDK de TypeScript para las APIs de Sabre.
12
12
 
13
13
  ## Estado
14
14
 
15
- La `0.6.0` está publicada con el dist-tag `next` mientras se prueba; `latest` sigue en la `0.5.1`.
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`. La `0.6.0` suma filtros de cola: `count` con una cola, un rango o `selection`,
19
- `activity` con una cola o un rango, y `selection` en el fake de colas. No trae cambios
20
- incompatibles; la línea `0.5` trae tres (ver [Actualizar desde la 0.4](#actualizar-desde-la-04)).
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.6.0
24
- npm install sabre-client # 0.5.1
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`. El aviso
237
- `END_TRANSACTION_PROBLEM` significa que la confirmación sigue pendiente: verifica el resultado
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 `booking.cancel`), y toda operación de `SabreLegacy`, acepta un
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'` o `'queue.<método>'`. El token de la sesión
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). Create Booking nunca se reintenta. Los reintentos usan
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.6.0` is published under the `next` dist-tag while it is tested; `latest` stays on `0.5.1`.
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`. `0.6.0` adds queue filters: `count` with one queue, a range or `selection`,
19
- `activity` with one queue or a range, and `selection` in the queue fake. It has no breaking
20
- changes; the `0.5` line has three (see [Upgrading from 0.4](#upgrading-from-04)).
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.6.0
24
- npm install sabre-client # 0.5.1
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`. An
237
- `END_TRANSACTION_PROBLEM` warning means the confirmation is still pending: check the result
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` and `booking.cancel`), and every `SabreLegacy` operation, accepts
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'` or `'queue.<method>'`. The session token
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). Create Booking is never retried. Retries use exponential backoff with
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