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.md CHANGED
@@ -12,20 +12,31 @@ TypeScript SDK for the Sabre APIs.
12
12
 
13
13
  ## Status
14
14
 
15
- `0.5.1` is published under the `next` dist-tag while it is tested; `latest` stays on `0.4.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`. The `0.5` line has three breaking changes (see
19
- [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)).
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
  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`. An
236
- `END_TRANSACTION_PROBLEM` warning means the confirmation is still pending: check the result
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
- - **Not simulated**: PCCs (the queues are the same for any `pcc`) and `selection`, which gets an
621
- error instead of the whole queue, in `access` and in `list`.
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` 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
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'` 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
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). 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
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