sabre-client 0.5.1 → 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,20 +12,31 @@ SDK de TypeScript para las APIs de Sabre.
12
12
 
13
13
  ## Estado
14
14
 
15
- La `0.5.1` está publicada con el dist-tag `next` mientras se prueba; `latest` sigue en la `0.4.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 línea `0.5` trae tres cambios incompatibles (ver
19
- [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)).
20
23
 
21
24
  ```sh
22
- npm install sabre-client@next # 0.5.1
23
- npm install sabre-client # 0.4.1
25
+ npm install sabre-client@next # 0.7.0
26
+ npm install sabre-client # 0.6.0
24
27
  ```
25
28
 
26
29
  El paquete sigue en `0.x`: una versión minor puede traer cambios incompatibles, y el
27
30
  [CHANGELOG](./CHANGELOG.md) los marca.
28
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
+
29
40
  ### Actualizar desde la 0.4
30
41
 
31
42
  - **`pcc` ya no está en la config.** Pasarlo a `new Sabre` o a `new SabreLegacy` da un error de
@@ -48,6 +59,14 @@ El Tier 1 cubre el flujo básico de Air Booking por REST:
48
59
  | Reserva | Create Booking (Booking Management API v1, ATPCO por `flightDetails`) | `sabre.booking.create` |
49
60
  | Gestión de la reserva | Get Booking y Cancel Booking (Booking Management API v1) | `sabre.booking.get`, `sabre.booking.cancel` |
50
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
+
51
70
  El Tier 2 agrega la elección de asiento:
52
71
 
53
72
  | Paso | Servicio | Método |
@@ -64,6 +83,7 @@ El Tier 3 agrega el cliente SOAP `SabreLegacy`, con una sesión stateful y Queue
64
83
  | Párrafo de cola | — (función pura) | `parseQueueParagraph`, `parseQueuePlaceRemark`, `resolveHostDate` |
65
84
  | Historial del PNR | `TravelItineraryHistoryLLSRQ` 2.4.0 | `session.reservation.history` |
66
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` |
67
87
 
68
88
  Planeado después de `0.5.0`: Get Ancillaries (solo NDC).
69
89
 
@@ -232,9 +252,9 @@ Como `create`, **nunca se reintenta automáticamente**. Las combinaciones que Sa
232
252
  rechaza (`cancelAll` junto con listas, ni `cancelAll` ni listas, `offerItemId` junto con
233
253
  `flightTicketOperation`) lanzan un `TypeError` antes de enviar nada. Si Sabre responde
234
254
  `NO_ITEMS_CANCELLED`, la llamada lanza `SabreBusinessError` aunque Sabre lo mande como aviso,
235
- porque no se canceló nada; los errores que explican el motivo están en `error.errors`. El aviso
236
- `END_TRANSACTION_PROBLEM` significa que la confirmación sigue pendiente: verifica el resultado
237
- 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).
238
258
 
239
259
  Toda request acepta un `AbortSignal`. Las búsquedas aceptan además una política de reintentos
240
260
  propia:
@@ -243,6 +263,109 @@ propia:
243
263
  await sabre.air.bargainFinderMax(search, { signal, retry: { maxRetries: 1 } });
244
264
  ```
245
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
+
246
369
  ## Elegir un asiento
247
370
 
248
371
  `sabre.air.getSeats` devuelve el mapa de asientos con disponibilidad y precio. Es una sola
@@ -430,6 +553,44 @@ const { transactions } = await legacy.withSession((session) => session.reservati
430
553
  lanza un `TypeError` sin llamar a la red. Un PNR que no existe lanza un `SabreBusinessError`
431
554
  (`service: 'itineraryHistory'`).
432
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
+
433
594
  ### Colas
434
595
 
435
596
  Las operaciones de cola están en `session.queue`. `pcc` es opcional en cada llamada, y por defecto
@@ -464,8 +625,8 @@ const pnrQueues = queues.filter(({ queue }) => !isMessageQueue(queue));
464
625
 
465
626
  | Método | Servicio de Sabre | Devuelve |
466
627
  | - | - | - |
467
- | `count({ pcc? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, warnings }` |
468
- | `activity({ pcc? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
628
+ | `count({ pcc?, queue?, range?, selection? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, additionalInfo?, warnings }` |
629
+ | `activity({ pcc?, queue?, range? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
469
630
  | `list({ queue, pcc?, primaryPassenger?, selection? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, sin entrar a la cola |
470
631
  | `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
471
632
  | `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
@@ -489,12 +650,29 @@ type QueueStep =
489
650
  agente que lo colocó, PCC más firma, p. ej. `'PCC1AAT'`; todavía no se probó en la lista). Otro
490
651
  código lanza `TypeError` sin llamar a Sabre. El `placedAt` de una entrada de la lista es cuándo
491
652
  se colocó el PNR en la cola, la fecha por la que filtra `QD`.
653
+ - **Filtros de `count`**: `queue` cuenta una sola cola, y `range: { start, end }`, un rango de
654
+ colas numéricas; no se combinan. Con `range`, Sabre devuelve cada cola del rango, **también las
655
+ vacías**, con `count: 0` (sin filtros las omite). `selection` admite hasta 8 filtros, como
656
+ `access`, y cuenta solo los PNR que cumplen: el host acepta cualquier código (`QD`, `AK`, `AL`,
657
+ `PI`, `AC`… y combinaciones), y con el mismo `QD` la cuenta coincide con `list`. Necesita
658
+ `queue`: sin cola, Sabre responde `QUEUE SELECTED NOT VALID OR CLOSED`, y con un rango,
659
+ `KEYS NOT ALLOWED`, así que el SDK lanza `TypeError` antes. Con `selection`, `totals.unfiltered`
660
+ es cuántos PNR tiene la cola sin filtrar (el `1 MATCHING PNRS OUT OF 80` del host); con `queue`
661
+ sola, Sabre no manda `totals`. `additionalInfo` lee `AdditionalQueueInfo` (una cola transferida a
662
+ otra), tipado desde el design: ninguna respuesta real lo trajo todavía.
663
+
664
+ ```ts
665
+ const { queues, totals } = await session.queue.count({ queue: 20, selection: [{ function: 'QD', criteria: '08NOV' }] });
666
+ // queues: [{ queue: 20, count: 1 }], totals: { unfiltered: 80 }
667
+ ```
492
668
  - **`activity`** devuelve, por cola, los ocho contadores de la pantalla `QA` del host:
493
669
  `numMessages` (los que están hoy en la cola), `ended`, `ignored`, `moved`, `removed`,
494
670
  `transferred`, `leftMessageToContact` y `unableToReach`, más `totals` para el PCC. Sabre lista
495
671
  también colas vacías con contadores. Ninguna fuente dice desde cuándo cuentan, así que el SDK los
496
672
  devuelve tal cual. Como `count`, es una lectura: se reintenta, funciona dentro o fuera de una
497
- cola y no pide duty code 9.
673
+ cola y no pide duty code 9. Acepta los mismos `queue` o `range` que `count` (no los dos): con
674
+ `queue`, solo esa cola y sin `totals`; con `range`, cada cola del rango, también las que tienen
675
+ todos los contadores en 0, y los `totals` del rango.
498
676
  - **Cada resultado de colas trae `respondedAt`**: el `eb:Timestamp` de Sabre, en ISO 8601 UTC
499
677
  (`'2026-11-06T17:34:16Z'`). A diferencia de `countedAt` y `placedAt`, que llegan como los manda
500
678
  el host (sin año y en hora local del PCC), trae año y zona. Solo falta si Sabre no lo manda.
@@ -623,8 +801,31 @@ fake.openSessions; // 0: la sesión se cerró
623
801
  - Responde como producción: colas vacías, el fin de cola, `place` que mueve el PNR, el PIC
624
802
  obligatorio y una sesión vencida con `fake.expireSessions()`. `fake.calls` lista lo que pidió el
625
803
  SDK.
626
- - **No simula** los PCC (las colas son las mismas para cualquier `pcc`) ni `selection`, que recibe
627
- un error en vez de la cola entera, en `access` y en `list`.
804
+ - `queue.count` y `queue.activity` con `queue` o `range` responden como producción: una cola sin
805
+ `totals`, o cada cola del rango, también las vacías.
806
+ - `selection` se simula para `QD`, `AK`, `AL`, `PI` y `AC` (`QUEUE_FAKE_SELECTION_FUNCTIONS`), en
807
+ `access`, `list` y `count`, combinados con Y:
808
+ - `QD` filtra por `placedAt` (`'05NOV'` o `'5NOV'`);
809
+ - `AK` filtra por `placedBy`, PCC más firma (`'ABCDAAT'`);
810
+ - `AL` filtra por la aerolínea de los segmentos aéreos del párrafo, y `AC` por su estado (una
811
+ letra cubre todos los estados que empiezan con ella);
812
+ - `PI` filtra por el PIC con que lo colocó `place` o, si no, por el aviso de PIC del párrafo
813
+ (`011 SEE REMARKS`).
814
+
815
+ `access` recorre solo los PNR que cumplen y termina en `END OF DISPLAY`, también al entrar si
816
+ ninguno cumple (un supuesto del fake), e `ignore`, `remove` y `place` saltan los demás. `list`
817
+ devuelve solo las `Line` que cumplen y, como Sabre, rechaza un código que no sea `QD` ni `AK`.
818
+ `count` con `queue` cuenta los que cumplen y devuelve `totals.unfiltered`; con `range` o sin
819
+ cola, da los errores de Sabre. Otro código recibe un error de host marcado como no simulado,
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.
827
+ - **No simula** los PCC (las colas son las mismas para cualquier `pcc`) ni las reglas de
828
+ combinación de `selection` del host.
628
829
  - `queueFixtures` trae las respuestas anonimizadas que se grabaron en producción (`walk`,
629
830
  `emptyQueue`, `place`…), como `{ operation, status, body }`. Cada una tiene sus propios alias de
630
831
  localizador (`walk`: `AAAAAA`–`AAAAAD`, `place`: `AAAAAE`–`AAAAAF`,
@@ -648,6 +849,50 @@ fake.openSessions; // 0: la sesión se cerró
648
849
  }));
649
850
  ```
650
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
+
651
896
  `examples/queue-review.mjs` es un ejemplo completo, y su test lo corre contra el fake.
652
897
 
653
898
  #### Anonimizar respuestas grabadas
@@ -694,6 +939,7 @@ Todos los errores que lanza el SDK extienden `SabreError`:
694
939
  | `SabreOrchestrationError` | Error de un servicio orquestado dentro de un `200` (`SP.*`) | `code` |
695
940
  | `SabreBusinessError` | Error del catálogo propio de un servicio (p. ej. `No Availability`, una reserva que no se creó) | `service`, `code`, `warnings`, `errors` |
696
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` |
697
943
  | `SabreWarning` | Aviso no fatal (p. ej. `category: WARNING` en Create Booking) | `service`, `code` |
698
944
 
699
945
  Todos tienen `retryable`, que indica si la política de reintentos puede volver a intentar.
@@ -722,7 +968,8 @@ try {
722
968
  ## Respuesta cruda
723
969
 
724
970
  Toda operación REST (`air.bargainFinderMax`, `air.revalidateItinerary`, `air.getSeats`,
725
- `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
726
973
  callback `onRawResponse` en sus
727
974
  opciones, junto a `signal`. Recibe la respuesta de Sabre tal cual llegó, por ejemplo para
728
975
  guardarla junto al resultado tipado. En `air.getSeats`, `operation` indica el endpoint que se
@@ -747,7 +994,8 @@ const booking = await sabre.booking.get({ confirmationId }, { onRawResponse: (r)
747
994
  conserva el valor y escríbelo cuando la llamada vuelva.
748
995
 
749
996
  En `SabreLegacy`, `protocol` es `'soap'`, `body` es el sobre SOAP completo, y `operation` es
750
- `'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
751
999
  (`BinarySecurityToken`) se reemplaza por `[REDACTED]` donde aparezca. No se cambia nada más.
752
1000
 
753
1001
  > [!WARNING]
@@ -790,7 +1038,8 @@ await sabre.booking.get({ confirmationId }, { onRawRequest: (r) => (sent = r) })
790
1038
  ## Reintentos
791
1039
 
792
1040
  Solo se reintentan las operaciones idempotentes: Bargain Finder Max y Revalidate Itinerary
793
- (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
794
1043
  backoff exponencial con full jitter y un piso de 500 ms, ante `429`, `500`, `503`, `504`,
795
1044
  `SP.PROVIDER_ERROR` y los errores temporales del catálogo de Bargain Finder Max.
796
1045