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 +265 -16
- package/README.md +266 -16
- package/dist/index.d.ts +354 -10
- package/dist/index.js +786 -181
- package/dist/index.js.map +1 -1
- package/dist/{reservation-history-BKRFlxpA.d.ts → reservation-history-BRKQBHsj.d.ts} +178 -3
- package/dist/testing.d.ts +127 -8
- package/dist/testing.js +811 -51
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
19
|
-
[
|
|
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.
|
|
23
|
-
npm install sabre-client # 0.
|
|
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`.
|
|
236
|
-
`END_TRANSACTION_PROBLEM` significa que la confirmación sigue pendiente: verifica
|
|
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
|
-
-
|
|
627
|
-
|
|
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 `
|
|
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'
|
|
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)
|
|
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
|
|