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.md
CHANGED
|
@@ -12,20 +12,31 @@ TypeScript SDK for the Sabre APIs.
|
|
|
12
12
|
|
|
13
13
|
## Status
|
|
14
14
|
|
|
15
|
-
`0.
|
|
15
|
+
`0.7.0` is published under the `next` dist-tag while it is tested; `latest` stays on `0.6.0`.
|
|
16
16
|
It covers Tier 1 and Tier 2 of the Air Booking flow over REST, Tier 3 (SOAP sessions and queues)
|
|
17
17
|
and Tier 4: queue activity, PNR history, the `S` message queue, `selection` in `list` and
|
|
18
|
-
`onRawRequest
|
|
19
|
-
[
|
|
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)).
|
|
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
|
The package is still `0.x`: a minor version may include breaking changes, and the
|
|
27
30
|
[CHANGELOG](./CHANGELOG.md) marks them.
|
|
28
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
|
+
|
|
29
40
|
### Upgrading from 0.4
|
|
30
41
|
|
|
31
42
|
- **`pcc` is no longer in the config.** Passing it to `new Sabre` or `new SabreLegacy` is a type
|
|
@@ -48,6 +59,14 @@ Tier 1 covers the core Air Booking flow over REST:
|
|
|
48
59
|
| Booking | Create Booking (Booking Management API v1, ATPCO by `flightDetails`) | `sabre.booking.create` |
|
|
49
60
|
| Booking management | Get Booking and Cancel Booking (Booking Management API v1) | `sabre.booking.get`, `sabre.booking.cancel` |
|
|
50
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
|
+
|
|
51
70
|
Tier 2 adds seat selection:
|
|
52
71
|
|
|
53
72
|
| Step | Service | Method |
|
|
@@ -64,6 +83,7 @@ Tier 3 adds the `SabreLegacy` SOAP client, with a stateful session and Queue Man
|
|
|
64
83
|
| Queue paragraph | — (pure function) | `parseQueueParagraph`, `parseQueuePlaceRemark`, `resolveHostDate` |
|
|
65
84
|
| PNR history | `TravelItineraryHistoryLLSRQ` 2.4.0 | `session.reservation.history` |
|
|
66
85
|
| Host commands | `SabreCommandLLSRQ` 2.0.0 | `session.command` |
|
|
86
|
+
| Daily sales | `TKT_TravelAgencyReportsRQ` / `DailySalesSummaryRQ` 1.2.2 (the DQB «Summary») | `session.reporting.dailySalesSummary` |
|
|
67
87
|
|
|
68
88
|
Planned after `0.5.0`: Get Ancillaries (NDC only).
|
|
69
89
|
|
|
@@ -232,9 +252,10 @@ Like `create`, it is **never retried automatically**. Combinations that Sabre al
|
|
|
232
252
|
(`cancelAll` together with item lists, no `cancelAll` and no list, `offerItemId` together with
|
|
233
253
|
`flightTicketOperation`) throw a `TypeError` before any request is sent. If Sabre answers
|
|
234
254
|
`NO_ITEMS_CANCELLED`, the call throws `SabreBusinessError` even though Sabre sends it as a
|
|
235
|
-
warning, because nothing was cancelled; the errors that explain why are in `error.errors`.
|
|
236
|
-
`END_TRANSACTION_PROBLEM` warning means the confirmation is still pending:
|
|
237
|
-
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).
|
|
238
259
|
|
|
239
260
|
Every request accepts an `AbortSignal`. Shopping calls also accept a per-request retry policy:
|
|
240
261
|
|
|
@@ -242,6 +263,109 @@ Every request accepts an `AbortSignal`. Shopping calls also accept a per-request
|
|
|
242
263
|
await sabre.air.bargainFinderMax(search, { signal, retry: { maxRetries: 1 } });
|
|
243
264
|
```
|
|
244
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
|
+
|
|
245
369
|
## Choosing a seat
|
|
246
370
|
|
|
247
371
|
`sabre.air.getSeats` returns the seat map with availability and prices. It is one function
|
|
@@ -426,6 +550,44 @@ const { transactions } = await legacy.withSession((session) => session.reservati
|
|
|
426
550
|
`TypeError` without calling the network. A PNR that does not exist throws a `SabreBusinessError`
|
|
427
551
|
(`service: 'itineraryHistory'`).
|
|
428
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
|
+
|
|
429
591
|
### Queues
|
|
430
592
|
|
|
431
593
|
The queue operations live in `session.queue`. `pcc` is optional in every call and defaults to the
|
|
@@ -460,8 +622,8 @@ const pnrQueues = queues.filter(({ queue }) => !isMessageQueue(queue));
|
|
|
460
622
|
|
|
461
623
|
| Method | Sabre service | Returns |
|
|
462
624
|
| - | - | - |
|
|
463
|
-
| `count({ pcc? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, warnings }` |
|
|
464
|
-
| `activity({ pcc? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
|
|
625
|
+
| `count({ pcc?, queue?, range?, selection? })` | `QueueCountLLSRQ` | `{ queues: [{ queue, count }], totals, countedAt, additionalInfo?, warnings }` |
|
|
626
|
+
| `activity({ pcc?, queue?, range? })` | `QueueAnalysisLLSRQ` | `{ queues: [{ queue, numMessages, ended, ignored, … }], totals, analyzedAt, warnings }` |
|
|
465
627
|
| `list({ queue, pcc?, primaryPassenger?, selection? })` | `QueueAccessLLSRQ` (`List`) | `{ locators, entries, paragraph, warnings }`, without entering the queue |
|
|
466
628
|
| `access({ queue, pcc?, selection? })` | `QueueAccessLLSRQ` | `QueueStep` |
|
|
467
629
|
| `ignore()` | `QueueAccessLLSRQ` (`I`) | `QueueStep` |
|
|
@@ -485,12 +647,30 @@ type QueueStep =
|
|
|
485
647
|
(the agent who placed it, PCC plus sine, e.g. `'PCC1AAT'`; not tested in the list yet).
|
|
486
648
|
Any other code throws a `TypeError` without calling Sabre. The `placedAt` of a list entry is
|
|
487
649
|
when the PNR was placed on the queue, the date `QD` filters on.
|
|
650
|
+
- **`count` filters**: `queue` counts one queue, and `range: { start, end }` a range of numeric
|
|
651
|
+
queues; the two do not combine. With `range`, Sabre returns every queue in the range, **empty
|
|
652
|
+
ones included** with `count: 0` (without filters it leaves them out). `selection` takes up to 8
|
|
653
|
+
filters, like `access`, and counts only the PNRs that match: the host accepts any code (`QD`,
|
|
654
|
+
`AK`, `AL`, `PI`, `AC`… and combinations), and with the same `QD` the count matches `list`. It
|
|
655
|
+
needs `queue`: without a queue Sabre answers `QUEUE SELECTED NOT VALID OR CLOSED`, and with a
|
|
656
|
+
range, `KEYS NOT ALLOWED`, so the SDK throws a `TypeError` first. With `selection`,
|
|
657
|
+
`totals.unfiltered` is how many PNRs the queue has before filtering (the host's
|
|
658
|
+
`1 MATCHING PNRS OUT OF 80`); with `queue` alone Sabre sends no `totals`. `additionalInfo` reads
|
|
659
|
+
`AdditionalQueueInfo` (a queue transferred elsewhere), typed from the design: no real response
|
|
660
|
+
has carried it yet.
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
const { queues, totals } = await session.queue.count({ queue: 20, selection: [{ function: 'QD', criteria: '08NOV' }] });
|
|
664
|
+
// queues: [{ queue: 20, count: 1 }], totals: { unfiltered: 80 }
|
|
665
|
+
```
|
|
488
666
|
- **`activity`** returns, per queue, the eight counters of the host's `QA` screen: `numMessages`
|
|
489
667
|
(on the queue now), `ended`, `ignored`, `moved`, `removed`, `transferred`,
|
|
490
668
|
`leftMessageToContact` and `unableToReach`, plus `totals` for the PCC. Sabre also lists empty
|
|
491
669
|
queues that have counters. No source says since when the counters run, so the SDK returns them
|
|
492
670
|
as they come. Like `count`, it is a read: it is retried, it works inside or outside a queue, and
|
|
493
|
-
it does not need duty code 9.
|
|
671
|
+
it does not need duty code 9. It takes the same `queue` or `range` as `count` (not both): with
|
|
672
|
+
`queue`, only that queue and no `totals`; with `range`, every queue in the range, even those
|
|
673
|
+
with all counters at 0, and the range's `totals`.
|
|
494
674
|
- **Every queue result carries `respondedAt`**: Sabre's `eb:Timestamp`, in ISO 8601 UTC
|
|
495
675
|
(`'2026-11-06T17:34:16Z'`). Unlike `countedAt` and `placedAt`, which come as the host sends them
|
|
496
676
|
(no year, PCC local time), it has a year and a zone. It is missing only if Sabre does not send it.
|
|
@@ -617,8 +797,31 @@ fake.openSessions; // 0: the session was closed
|
|
|
617
797
|
- It answers like production: empty queues, the end of the queue, `place` moving the PNR, the
|
|
618
798
|
required PIC, and an expired session with `fake.expireSessions()`. `fake.calls` lists what the
|
|
619
799
|
SDK asked for.
|
|
620
|
-
-
|
|
621
|
-
|
|
800
|
+
- `queue.count` and `queue.activity` with `queue` or `range` answer like production: one queue
|
|
801
|
+
without `totals`, or every queue in the range, empty ones included.
|
|
802
|
+
- `selection` is simulated for `QD`, `AK`, `AL`, `PI` and `AC` (`QUEUE_FAKE_SELECTION_FUNCTIONS`),
|
|
803
|
+
in `access`, `list` and `count`, combined with AND:
|
|
804
|
+
- `QD` filters on `placedAt` (`'05NOV'` or `'5NOV'`);
|
|
805
|
+
- `AK` filters on `placedBy`, PCC plus sine (`'ABCDAAT'`);
|
|
806
|
+
- `AL` filters on the airline of the paragraph's air segments, and `AC` on their status (one
|
|
807
|
+
letter matches every status that starts with it);
|
|
808
|
+
- `PI` filters on the PIC `place` used, or else on the paragraph's PIC notice (`011 SEE REMARKS`).
|
|
809
|
+
|
|
810
|
+
`access` walks only the PNRs that match and ends with `END OF DISPLAY`, also on entering when
|
|
811
|
+
none match (an assumption of the fake), and `ignore`, `remove` and `place` skip the rest. `list`
|
|
812
|
+
returns only the matching `Line`s and, like Sabre, rejects any code other than `QD` or `AK`.
|
|
813
|
+
`count` with `queue` counts the matches and returns `totals.unfiltered`; with `range` or without
|
|
814
|
+
a queue it gives Sabre's errors. Any other code gets a host error marked as not simulated,
|
|
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.
|
|
823
|
+
- **Not simulated**: PCCs (the queues are the same for any `pcc`) and the host's combination rules
|
|
824
|
+
for `selection`.
|
|
622
825
|
- `queueFixtures` holds the anonymized responses recorded in production (`walk`, `emptyQueue`,
|
|
623
826
|
`place`, …), as `{ operation, status, body }`. Each one has its own locator aliases (`walk`:
|
|
624
827
|
`AAAAAA`–`AAAAAD`, `place`: `AAAAAE`–`AAAAAF`, `placeWithoutInstructionCodeInside`:
|
|
@@ -641,6 +844,49 @@ fake.openSessions; // 0: the session was closed
|
|
|
641
844
|
}));
|
|
642
845
|
```
|
|
643
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
|
+
|
|
644
890
|
`examples/queue-review.mjs` is a full example, and its test runs it against the fake.
|
|
645
891
|
|
|
646
892
|
#### Anonymizing recorded responses
|
|
@@ -687,6 +933,7 @@ Every error thrown by the SDK extends `SabreError`:
|
|
|
687
933
|
| `SabreOrchestrationError` | Orchestrated service error inside a `200` (`SP.*`) | `code` |
|
|
688
934
|
| `SabreBusinessError` | Error from a service's own catalog (e.g. `No Availability`, a booking that was not created) | `service`, `code`, `warnings`, `errors` |
|
|
689
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` |
|
|
690
937
|
| `SabreWarning` | Non-fatal warning (e.g. `category: WARNING` in Create Booking) | `service`, `code` |
|
|
691
938
|
|
|
692
939
|
All of them have `retryable`, which says whether the retry policy may try again.
|
|
@@ -715,7 +962,8 @@ try {
|
|
|
715
962
|
## Raw response
|
|
716
963
|
|
|
717
964
|
Every REST operation (`air.bargainFinderMax`, `air.revalidateItinerary`, `air.getSeats`,
|
|
718
|
-
`booking.create`, `booking.get
|
|
965
|
+
`booking.create`, `booking.get`, `booking.cancel`, `tickets.fulfill` and `tickets.verify`), and
|
|
966
|
+
every `SabreLegacy` operation, accepts
|
|
719
967
|
an `onRawResponse` callback in its
|
|
720
968
|
options, next to `signal`. It receives Sabre's response as it arrived, for example to store it
|
|
721
969
|
next to the typed result. For `air.getSeats`, `operation` names the endpoint that was called
|
|
@@ -740,7 +988,8 @@ const booking = await sabre.booking.get({ confirmationId }, { onRawResponse: (r)
|
|
|
740
988
|
asynchronously, keep the value and write it after the call returns.
|
|
741
989
|
|
|
742
990
|
In `SabreLegacy`, `protocol` is `'soap'`, `body` is the whole SOAP envelope, and `operation` is
|
|
743
|
-
`'openSession'`, `'close'`, `'ping'`, `'command'
|
|
991
|
+
`'openSession'`, `'close'`, `'ping'`, `'command'`, `'queue.<method>'`, `'reservation.history'` or
|
|
992
|
+
`'reporting.dailySalesSummary'`. `tickets.verify` reports `'booking.get'`, the Get Booking it runs. The session token
|
|
744
993
|
(`BinarySecurityToken`) is replaced by `[REDACTED]` wherever it appears. Nothing else is changed.
|
|
745
994
|
|
|
746
995
|
> [!WARNING]
|
|
@@ -783,7 +1032,8 @@ await sabre.booking.get({ confirmationId }, { onRawRequest: (r) => (sent = r) })
|
|
|
783
1032
|
## Retries
|
|
784
1033
|
|
|
785
1034
|
Only idempotent operations are retried: Bargain Finder Max and Revalidate Itinerary (both
|
|
786
|
-
are read-only `POST`s)
|
|
1035
|
+
are read-only `POST`s), plus Get Booking and `tickets.verify`. Create Booking, Cancel Booking and
|
|
1036
|
+
`tickets.fulfill` are never retried. Retries use exponential backoff with
|
|
787
1037
|
full jitter and a 500 ms floor, on `429`, `500`, `503`, `504`, `SP.PROVIDER_ERROR` and the
|
|
788
1038
|
temporary errors in the Bargain Finder Max catalog.
|
|
789
1039
|
|