letsfg 2026.5.73 → 2026.5.75

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.
@@ -1564,6 +1564,7 @@ var LATE_MERGE_POLL_MS = 3e3;
1564
1564
  var LATE_MERGE_GRACE_MS = 9e4;
1565
1565
  var WAIT_FOR_SPLIT = (process.env.LETSFG_WAIT_FOR_SPLIT || "").trim() !== "0";
1566
1566
  var NON_TERMINAL = ["pending", "searching"];
1567
+ var HOTEL_BOOKING_FINAL_STATUSES = ["succeeded", "failed", "attention"];
1567
1568
  var LetsFG = class {
1568
1569
  bearerToken;
1569
1570
  apiKey;
@@ -1617,15 +1618,23 @@ var LetsFG = class {
1617
1618
  currency: options.currency ?? "EUR",
1618
1619
  limit: options.limit ?? 50
1619
1620
  };
1620
- if (options.returnDate) body.return_date = options.returnDate;
1621
- if (options.cabinClass) body.cabin_class = options.cabinClass;
1622
- if (options.maxStopovers != null) body.max_stopovers = options.maxStopovers;
1623
- if (options.sort) body.sort = options.sort;
1624
1621
  if (options.departureTimeFrom) body.departure_time_from = options.departureTimeFrom;
1625
1622
  if (options.departureTimeTo) body.departure_time_to = options.departureTimeTo;
1626
1623
  if (this.usingPFS) {
1627
- return this.searchPFS(body);
1624
+ if (options.returnDate) body.return_date = options.returnDate;
1625
+ if (options.cabinClass) body.cabin = options.cabinClass;
1626
+ if (options.maxStopovers != null) body.max_stops = options.maxStopovers;
1627
+ if (options.sort) body.sort_by = options.sort;
1628
+ const result = await this.searchPFS(body);
1629
+ if (options.limit != null && Array.isArray(result.offers)) {
1630
+ result.offers = result.offers.slice(0, Math.max(0, options.limit));
1631
+ }
1632
+ return result;
1628
1633
  }
1634
+ if (options.returnDate) body.return_from = options.returnDate;
1635
+ if (options.cabinClass) body.cabin_class = options.cabinClass;
1636
+ if (options.maxStopovers != null) body.max_stopovers = options.maxStopovers;
1637
+ if (options.sort) body.sort = options.sort;
1629
1638
  return this.post("/developers/api/v1/flights/search", body);
1630
1639
  }
1631
1640
  /** PFS path: POST /api/search -> poll /api/results/<id> */
@@ -1815,16 +1824,18 @@ var LetsFG = class {
1815
1824
  }
1816
1825
  // ── Hotels ──────────────────────────────────────────────────────────
1817
1826
  //
1818
- // A card on file is required for EVERY hotel call, search included. That is
1819
- // deliberate: a hotel search opens a real session at the supplier and booking
1820
- // blocks a real rate, so a caller is never allowed to reach the point of
1821
- // commitment only to discover it cannot pay. The same card that authorises
1822
- // flight booking authorises hotels — there is no separate hotel enrolment.
1827
+ // A connected payment method is required for EVERY hotel call, search
1828
+ // included. That is deliberate: a hotel search opens a real session at the
1829
+ // supplier and booking blocks a real rate, so a caller is never allowed to
1830
+ // reach the point of commitment only to discover it cannot pay. The same
1831
+ // method that authorises flight booking authorises hotels.
1823
1832
  //
1824
- // Only free-cancellation, pay-later rates are sold. Those are the rates where
1825
- // the guest's balance can safely be settled with the supplier after booking,
1826
- // which is what makes 5%-now/rest-later work. The result set is smaller than
1827
- // a metasearch's, and every row in it can actually be booked.
1833
+ // How a hotel is paid (since 2026-09-11): the full `price` is HELD on the
1834
+ // connected Revolut method, LetsFG books and pays the supplier itself, and the
1835
+ // hold is captured only once the supplier has confirmed. A booking that fails
1836
+ // releases the hold. There is no reservation fee, no deposit and no pay link —
1837
+ // those belonged to the process retired on 2026-09-11. Every rate type is
1838
+ // sold, refundable and non-refundable.
1828
1839
  /**
1829
1840
  * Resolve a place name to the city id that searchHotels() needs.
1830
1841
  *
@@ -1845,12 +1856,17 @@ var LetsFG = class {
1845
1856
  * Slow by nature — the supplier streams a whole city and every rate is priced
1846
1857
  * — so this gets its own generous timeout rather than the client default.
1847
1858
  *
1848
- * Each offer carries `price` (what the guest pays), `reservation_fee_now`
1849
- * (the 5% taken at booking), `balance_to_supplier`, `balance_due_by` and
1850
- * `free_cancellation_until`. There is no wholesale figure to quote by mistake.
1859
+ * The response carries `session_id`, `currency`, `supplier_currency`,
1860
+ * `markup_rate`, `fx_rate`, `fx_as_of`, `count`, `hotels`, `terms` and
1861
+ * `caveats`. Each offer carries `price` (what the guest pays, in `currency`),
1862
+ * `currency`, `fx_rate`, `expected_cost` (the supplier's cost, in PLN),
1863
+ * `refundable`, `free_cancellation_until` (refundable rates only),
1864
+ * `cancellation_policy` and its own `session_id`.
1851
1865
  *
1852
- * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
1853
- * identify the exact rate, and booking needs both.
1866
+ * `price` is the supplier's cost plus `markup_rate` (6.4% for Revolut Pay or an
1867
+ * EEA-issued card, 8.3% for a card issued outside the EEA); nothing is added at
1868
+ * booking. Keep the chosen offer whole: bookHotel() needs its `session_id`,
1869
+ * `combination_id_v2`, `price`, `expected_cost`, `currency` and `fx_rate`.
1854
1870
  */
1855
1871
  async searchHotels(params) {
1856
1872
  this.requireApiKey();
@@ -1863,7 +1879,8 @@ var LetsFG = class {
1863
1879
  children: params.children ?? 0,
1864
1880
  nationality: params.nationality ?? "PL",
1865
1881
  limit: params.limit ?? 40,
1866
- with_images: params.withImages ?? true
1882
+ with_images: params.withImages ?? true,
1883
+ currency: params.currency ?? "USD"
1867
1884
  };
1868
1885
  if (params.childAges?.length) body.child_ages = params.childAges;
1869
1886
  return this.post("/developers/api/v1/hotels/search", body, 24e4);
@@ -1871,30 +1888,42 @@ var LetsFG = class {
1871
1888
  /**
1872
1889
  * Start a booking. Returns a job immediately — it does NOT book inline.
1873
1890
  *
1874
- * A booking takes minutes: the rate is re-blocked at the supplier, every
1875
- * price and date rail is checked, the 5% reservation fee is charged to your
1876
- * card, and only then is the room committed. No proxy holds a connection that
1877
- * long, so this returns at once and you poll hotelBooking() for the outcome.
1878
- * Use bookHotelAndWait() if you would rather block.
1891
+ * What happens, in order: the offer's full `price` is HELD on the Revolut
1892
+ * payment method connected to this account (authorised, not taken); LetsFG
1893
+ * books the room with the supplier and pays the supplier itself; the hold is
1894
+ * captured only once the supplier has confirmed. If the booking fails for any
1895
+ * reason, the hold is released and nothing is charged. There is no
1896
+ * reservation fee, no deposit and no pay link.
1879
1897
  *
1880
- * Because the fee is taken BEFORE the commit, a declined card costs nothing
1881
- * to unwind: no reservation exists and nothing is charged.
1898
+ * A booking takes minutes and no proxy holds a connection that long, so this
1899
+ * returns at once and you poll hotelBooking() for the outcome. Use
1900
+ * bookHotelAndWait() if you would rather block.
1882
1901
  *
1883
- * Send `expectedPrice` and `expectedBalance` back exactly as search returned
1884
- * them — the booking is refused if the supplier has moved beyond tolerance,
1885
- * so a guest is never charged a price they did not agree to.
1902
+ * Send `expectedPrice` (the offer's `price`), `expectedCost`, `currency` and
1903
+ * `fxRate` exactly as search returned them. The booking is refused if the
1904
+ * supplier's live cost is above `expectedCost`, and a USD offer sent without
1905
+ * its `currency` is refused with 400 price_mismatch (the API assumes PLN).
1906
+ * Guest names, phone and e-mail are checked before anything is held; a problem
1907
+ * returns 400 invalid_details naming the fields.
1886
1908
  *
1887
- * Do NOT call this again for the same rate while a job is running: that books
1888
- * the room twice and charges two reservation fees.
1909
+ * Do NOT call this again for a booking whose job is still running: poll it.
1910
+ * A retry of the same booking returns the job already under way
1911
+ * (`duplicate: true`) rather than holding the money twice.
1889
1912
  */
1890
1913
  async bookHotel(params) {
1891
1914
  this.requireApiKey();
1915
+ if (typeof params.expectedCost !== "number") {
1916
+ throw new LetsFGError(
1917
+ "bookHotel() needs expectedCost: copy the offer's expected_cost, currency and fx_rate. " + ("expectedBalance" in params ? "expectedBalance belonged to the reservation-fee process retired on 2026-09-11 and is not sent. " : "") + "See https://letsfg.co/developers/api/docs",
1918
+ 400
1919
+ );
1920
+ }
1892
1921
  const body = {
1893
1922
  session_id: params.sessionId,
1894
1923
  hotel_code: params.hotelCode,
1895
1924
  combination_id_v2: params.combinationIdV2,
1896
1925
  expected_price: params.expectedPrice,
1897
- expected_balance: params.expectedBalance,
1926
+ expected_cost: params.expectedCost,
1898
1927
  city_id: params.cityId,
1899
1928
  city_name: params.cityName,
1900
1929
  check_in: params.checkIn,
@@ -1906,16 +1935,30 @@ var LetsFG = class {
1906
1935
  phone_country_code: params.phoneCountryCode ?? "48",
1907
1936
  special_requests: params.specialRequests ?? []
1908
1937
  };
1938
+ if (params.currency) body.currency = params.currency;
1939
+ if (params.fxRate != null) body.fx_rate = params.fxRate;
1909
1940
  if (params.combinationId != null) body.combination_id = params.combinationId;
1910
1941
  if (params.hotelName) body.hotel_name = params.hotelName;
1942
+ if (params.idempotencyKey) body.idempotency_key = params.idempotencyKey;
1911
1943
  return this.post("/developers/api/v1/hotels/book", body, 9e4);
1912
1944
  }
1913
1945
  /**
1914
1946
  * Collect the result of a booking started with bookHotel().
1915
1947
  *
1916
- * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
1917
- * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
1918
- * `balance_due_by` and `terms` (including the full cancellation ladder).
1948
+ * `status` is 'in_progress', 'succeeded', 'failed' or 'attention'; the last
1949
+ * three are final (HOTEL_BOOKING_FINAL_STATUSES).
1950
+ *
1951
+ * - succeeded: `confirmation`, `booking_id`, `hotel`, `room`, `total_price` +
1952
+ * `currency` (what the guest is charged), `supplier_paid` +
1953
+ * `supplier_currency` (what the supplier was paid), `payment_status`,
1954
+ * `refundable`, `free_cancellation_until`, `cancellation_ladder`, `terms`.
1955
+ * - failed: `error`, written for the guest. The hold has been released and
1956
+ * nothing was charged.
1957
+ * - attention: `error` (and `confirmation` when known). The outcome could not
1958
+ * be settled automatically; the hold is kept — nothing is charged — while a
1959
+ * person checks with the supplier. Do not book again.
1960
+ *
1961
+ * The guest is e-mailed in every case.
1919
1962
  */
1920
1963
  async hotelBooking(bookingJobId) {
1921
1964
  this.requireApiKey();
@@ -1927,13 +1970,15 @@ var LetsFG = class {
1927
1970
  /**
1928
1971
  * bookHotel(), then poll until the booking settles. Convenience only.
1929
1972
  *
1930
- * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
1931
- * still complete. The returned object carries `booking_job_id` so you can
1932
- * keep polling, and the confirmation is emailed to the guest regardless.
1973
+ * Stops at 'succeeded', 'failed' or 'attention' and never re-books. Giving up
1974
+ * after `maxWaitMs` (default 30 minutes: a booking usually takes 5-10, and
1975
+ * hotel bookings run one at a time) does NOT cancel anything — the booking may
1976
+ * still complete. The returned object carries `booking_job_id` so you can keep
1977
+ * polling, and the guest is e-mailed the outcome regardless.
1933
1978
  */
1934
1979
  async bookHotelAndWait(params) {
1935
1980
  const pollIntervalMs = params.pollIntervalMs ?? 2e4;
1936
- const maxWaitMs = params.maxWaitMs ?? 6e5;
1981
+ const maxWaitMs = params.maxWaitMs ?? 18e5;
1937
1982
  const job = await this.bookHotel(params);
1938
1983
  const jobId = job.booking_job_id;
1939
1984
  if (!jobId) return job;
@@ -1943,19 +1988,19 @@ var LetsFG = class {
1943
1988
  await new Promise((r) => setTimeout(r, pollIntervalMs));
1944
1989
  waited += pollIntervalMs;
1945
1990
  result = await this.hotelBooking(jobId);
1946
- const st = result.status;
1947
- if (st === "succeeded" || st === "failed") return result;
1991
+ if (HOTEL_BOOKING_FINAL_STATUSES.includes(result.status)) return result;
1948
1992
  }
1949
1993
  if (result.booking_job_id == null) result.booking_job_id = jobId;
1950
1994
  return result;
1951
1995
  }
1952
1996
  /**
1953
- * Release a reservation at the supplier.
1997
+ * Release a reservation at the supplier and refund the guest.
1954
1998
  *
1955
- * Free until `balance_due_by`; after that the hotel's own cancellation ladder
1956
- * applies and can reach 100%. The ladder ships in the booking's `terms`, so
1957
- * you can see the cost before calling this. The 5% reservation fee is NOT
1958
- * refunded.
1999
+ * Only this account's own bookings can be cancelled (anything else is 404). A
2000
+ * zero-charge cancellation — a refundable rate before its
2001
+ * `free_cancellation_until` — refunds the charge in full, or releases a hold
2002
+ * not yet captured. A cancellation that would cost money is refused with 409;
2003
+ * the hotel's own ladder is in the booking's `terms`.
1959
2004
  *
1960
2005
  * This drives a browser at the supplier and takes over a minute. If it times
1961
2006
  * out, do not assume it failed — re-check before retrying.
@@ -2108,6 +2153,7 @@ export {
2108
2153
  ValidationError,
2109
2154
  offerSummary,
2110
2155
  cheapestOffer,
2156
+ HOTEL_BOOKING_FINAL_STATUSES,
2111
2157
  LetsFG,
2112
2158
  index_default,
2113
2159
  BoostedTravel,
package/dist/cli.js CHANGED
@@ -320,6 +320,7 @@ var LATE_MERGE_POLL_MS = 3e3;
320
320
  var LATE_MERGE_GRACE_MS = 9e4;
321
321
  var WAIT_FOR_SPLIT = (process.env.LETSFG_WAIT_FOR_SPLIT || "").trim() !== "0";
322
322
  var NON_TERMINAL = ["pending", "searching"];
323
+ var HOTEL_BOOKING_FINAL_STATUSES = ["succeeded", "failed", "attention"];
323
324
  var LetsFG = class {
324
325
  bearerToken;
325
326
  apiKey;
@@ -373,15 +374,23 @@ var LetsFG = class {
373
374
  currency: options.currency ?? "EUR",
374
375
  limit: options.limit ?? 50
375
376
  };
376
- if (options.returnDate) body.return_date = options.returnDate;
377
- if (options.cabinClass) body.cabin_class = options.cabinClass;
378
- if (options.maxStopovers != null) body.max_stopovers = options.maxStopovers;
379
- if (options.sort) body.sort = options.sort;
380
377
  if (options.departureTimeFrom) body.departure_time_from = options.departureTimeFrom;
381
378
  if (options.departureTimeTo) body.departure_time_to = options.departureTimeTo;
382
379
  if (this.usingPFS) {
383
- return this.searchPFS(body);
380
+ if (options.returnDate) body.return_date = options.returnDate;
381
+ if (options.cabinClass) body.cabin = options.cabinClass;
382
+ if (options.maxStopovers != null) body.max_stops = options.maxStopovers;
383
+ if (options.sort) body.sort_by = options.sort;
384
+ const result = await this.searchPFS(body);
385
+ if (options.limit != null && Array.isArray(result.offers)) {
386
+ result.offers = result.offers.slice(0, Math.max(0, options.limit));
387
+ }
388
+ return result;
384
389
  }
390
+ if (options.returnDate) body.return_from = options.returnDate;
391
+ if (options.cabinClass) body.cabin_class = options.cabinClass;
392
+ if (options.maxStopovers != null) body.max_stopovers = options.maxStopovers;
393
+ if (options.sort) body.sort = options.sort;
385
394
  return this.post("/developers/api/v1/flights/search", body);
386
395
  }
387
396
  /** PFS path: POST /api/search -> poll /api/results/<id> */
@@ -571,16 +580,18 @@ var LetsFG = class {
571
580
  }
572
581
  // ── Hotels ──────────────────────────────────────────────────────────
573
582
  //
574
- // A card on file is required for EVERY hotel call, search included. That is
575
- // deliberate: a hotel search opens a real session at the supplier and booking
576
- // blocks a real rate, so a caller is never allowed to reach the point of
577
- // commitment only to discover it cannot pay. The same card that authorises
578
- // flight booking authorises hotels — there is no separate hotel enrolment.
583
+ // A connected payment method is required for EVERY hotel call, search
584
+ // included. That is deliberate: a hotel search opens a real session at the
585
+ // supplier and booking blocks a real rate, so a caller is never allowed to
586
+ // reach the point of commitment only to discover it cannot pay. The same
587
+ // method that authorises flight booking authorises hotels.
579
588
  //
580
- // Only free-cancellation, pay-later rates are sold. Those are the rates where
581
- // the guest's balance can safely be settled with the supplier after booking,
582
- // which is what makes 5%-now/rest-later work. The result set is smaller than
583
- // a metasearch's, and every row in it can actually be booked.
589
+ // How a hotel is paid (since 2026-09-11): the full `price` is HELD on the
590
+ // connected Revolut method, LetsFG books and pays the supplier itself, and the
591
+ // hold is captured only once the supplier has confirmed. A booking that fails
592
+ // releases the hold. There is no reservation fee, no deposit and no pay link —
593
+ // those belonged to the process retired on 2026-09-11. Every rate type is
594
+ // sold, refundable and non-refundable.
584
595
  /**
585
596
  * Resolve a place name to the city id that searchHotels() needs.
586
597
  *
@@ -601,12 +612,17 @@ var LetsFG = class {
601
612
  * Slow by nature — the supplier streams a whole city and every rate is priced
602
613
  * — so this gets its own generous timeout rather than the client default.
603
614
  *
604
- * Each offer carries `price` (what the guest pays), `reservation_fee_now`
605
- * (the 5% taken at booking), `balance_to_supplier`, `balance_due_by` and
606
- * `free_cancellation_until`. There is no wholesale figure to quote by mistake.
615
+ * The response carries `session_id`, `currency`, `supplier_currency`,
616
+ * `markup_rate`, `fx_rate`, `fx_as_of`, `count`, `hotels`, `terms` and
617
+ * `caveats`. Each offer carries `price` (what the guest pays, in `currency`),
618
+ * `currency`, `fx_rate`, `expected_cost` (the supplier's cost, in PLN),
619
+ * `refundable`, `free_cancellation_until` (refundable rates only),
620
+ * `cancellation_policy` and its own `session_id`.
607
621
  *
608
- * Keep `session_id` and the chosen offer's `combination_id_v2`: together they
609
- * identify the exact rate, and booking needs both.
622
+ * `price` is the supplier's cost plus `markup_rate` (6.4% for Revolut Pay or an
623
+ * EEA-issued card, 8.3% for a card issued outside the EEA); nothing is added at
624
+ * booking. Keep the chosen offer whole: bookHotel() needs its `session_id`,
625
+ * `combination_id_v2`, `price`, `expected_cost`, `currency` and `fx_rate`.
610
626
  */
611
627
  async searchHotels(params) {
612
628
  this.requireApiKey();
@@ -619,7 +635,8 @@ var LetsFG = class {
619
635
  children: params.children ?? 0,
620
636
  nationality: params.nationality ?? "PL",
621
637
  limit: params.limit ?? 40,
622
- with_images: params.withImages ?? true
638
+ with_images: params.withImages ?? true,
639
+ currency: params.currency ?? "USD"
623
640
  };
624
641
  if (params.childAges?.length) body.child_ages = params.childAges;
625
642
  return this.post("/developers/api/v1/hotels/search", body, 24e4);
@@ -627,30 +644,42 @@ var LetsFG = class {
627
644
  /**
628
645
  * Start a booking. Returns a job immediately — it does NOT book inline.
629
646
  *
630
- * A booking takes minutes: the rate is re-blocked at the supplier, every
631
- * price and date rail is checked, the 5% reservation fee is charged to your
632
- * card, and only then is the room committed. No proxy holds a connection that
633
- * long, so this returns at once and you poll hotelBooking() for the outcome.
634
- * Use bookHotelAndWait() if you would rather block.
647
+ * What happens, in order: the offer's full `price` is HELD on the Revolut
648
+ * payment method connected to this account (authorised, not taken); LetsFG
649
+ * books the room with the supplier and pays the supplier itself; the hold is
650
+ * captured only once the supplier has confirmed. If the booking fails for any
651
+ * reason, the hold is released and nothing is charged. There is no
652
+ * reservation fee, no deposit and no pay link.
635
653
  *
636
- * Because the fee is taken BEFORE the commit, a declined card costs nothing
637
- * to unwind: no reservation exists and nothing is charged.
654
+ * A booking takes minutes and no proxy holds a connection that long, so this
655
+ * returns at once and you poll hotelBooking() for the outcome. Use
656
+ * bookHotelAndWait() if you would rather block.
638
657
  *
639
- * Send `expectedPrice` and `expectedBalance` back exactly as search returned
640
- * them — the booking is refused if the supplier has moved beyond tolerance,
641
- * so a guest is never charged a price they did not agree to.
658
+ * Send `expectedPrice` (the offer's `price`), `expectedCost`, `currency` and
659
+ * `fxRate` exactly as search returned them. The booking is refused if the
660
+ * supplier's live cost is above `expectedCost`, and a USD offer sent without
661
+ * its `currency` is refused with 400 price_mismatch (the API assumes PLN).
662
+ * Guest names, phone and e-mail are checked before anything is held; a problem
663
+ * returns 400 invalid_details naming the fields.
642
664
  *
643
- * Do NOT call this again for the same rate while a job is running: that books
644
- * the room twice and charges two reservation fees.
665
+ * Do NOT call this again for a booking whose job is still running: poll it.
666
+ * A retry of the same booking returns the job already under way
667
+ * (`duplicate: true`) rather than holding the money twice.
645
668
  */
646
669
  async bookHotel(params) {
647
670
  this.requireApiKey();
671
+ if (typeof params.expectedCost !== "number") {
672
+ throw new LetsFGError(
673
+ "bookHotel() needs expectedCost: copy the offer's expected_cost, currency and fx_rate. " + ("expectedBalance" in params ? "expectedBalance belonged to the reservation-fee process retired on 2026-09-11 and is not sent. " : "") + "See https://letsfg.co/developers/api/docs",
674
+ 400
675
+ );
676
+ }
648
677
  const body = {
649
678
  session_id: params.sessionId,
650
679
  hotel_code: params.hotelCode,
651
680
  combination_id_v2: params.combinationIdV2,
652
681
  expected_price: params.expectedPrice,
653
- expected_balance: params.expectedBalance,
682
+ expected_cost: params.expectedCost,
654
683
  city_id: params.cityId,
655
684
  city_name: params.cityName,
656
685
  check_in: params.checkIn,
@@ -662,16 +691,30 @@ var LetsFG = class {
662
691
  phone_country_code: params.phoneCountryCode ?? "48",
663
692
  special_requests: params.specialRequests ?? []
664
693
  };
694
+ if (params.currency) body.currency = params.currency;
695
+ if (params.fxRate != null) body.fx_rate = params.fxRate;
665
696
  if (params.combinationId != null) body.combination_id = params.combinationId;
666
697
  if (params.hotelName) body.hotel_name = params.hotelName;
698
+ if (params.idempotencyKey) body.idempotency_key = params.idempotencyKey;
667
699
  return this.post("/developers/api/v1/hotels/book", body, 9e4);
668
700
  }
669
701
  /**
670
702
  * Collect the result of a booking started with bookHotel().
671
703
  *
672
- * `status` is 'in_progress', 'succeeded' or 'failed'. On success you get
673
- * `confirmation`, `reservation_fee_charged`, `pay_link`, `balance_due`,
674
- * `balance_due_by` and `terms` (including the full cancellation ladder).
704
+ * `status` is 'in_progress', 'succeeded', 'failed' or 'attention'; the last
705
+ * three are final (HOTEL_BOOKING_FINAL_STATUSES).
706
+ *
707
+ * - succeeded: `confirmation`, `booking_id`, `hotel`, `room`, `total_price` +
708
+ * `currency` (what the guest is charged), `supplier_paid` +
709
+ * `supplier_currency` (what the supplier was paid), `payment_status`,
710
+ * `refundable`, `free_cancellation_until`, `cancellation_ladder`, `terms`.
711
+ * - failed: `error`, written for the guest. The hold has been released and
712
+ * nothing was charged.
713
+ * - attention: `error` (and `confirmation` when known). The outcome could not
714
+ * be settled automatically; the hold is kept — nothing is charged — while a
715
+ * person checks with the supplier. Do not book again.
716
+ *
717
+ * The guest is e-mailed in every case.
675
718
  */
676
719
  async hotelBooking(bookingJobId) {
677
720
  this.requireApiKey();
@@ -683,13 +726,15 @@ var LetsFG = class {
683
726
  /**
684
727
  * bookHotel(), then poll until the booking settles. Convenience only.
685
728
  *
686
- * Giving up after `maxWaitMs` does NOT cancel anything — the booking may
687
- * still complete. The returned object carries `booking_job_id` so you can
688
- * keep polling, and the confirmation is emailed to the guest regardless.
729
+ * Stops at 'succeeded', 'failed' or 'attention' and never re-books. Giving up
730
+ * after `maxWaitMs` (default 30 minutes: a booking usually takes 5-10, and
731
+ * hotel bookings run one at a time) does NOT cancel anything — the booking may
732
+ * still complete. The returned object carries `booking_job_id` so you can keep
733
+ * polling, and the guest is e-mailed the outcome regardless.
689
734
  */
690
735
  async bookHotelAndWait(params) {
691
736
  const pollIntervalMs = params.pollIntervalMs ?? 2e4;
692
- const maxWaitMs = params.maxWaitMs ?? 6e5;
737
+ const maxWaitMs = params.maxWaitMs ?? 18e5;
693
738
  const job = await this.bookHotel(params);
694
739
  const jobId = job.booking_job_id;
695
740
  if (!jobId) return job;
@@ -699,19 +744,19 @@ var LetsFG = class {
699
744
  await new Promise((r) => setTimeout(r, pollIntervalMs));
700
745
  waited += pollIntervalMs;
701
746
  result = await this.hotelBooking(jobId);
702
- const st = result.status;
703
- if (st === "succeeded" || st === "failed") return result;
747
+ if (HOTEL_BOOKING_FINAL_STATUSES.includes(result.status)) return result;
704
748
  }
705
749
  if (result.booking_job_id == null) result.booking_job_id = jobId;
706
750
  return result;
707
751
  }
708
752
  /**
709
- * Release a reservation at the supplier.
753
+ * Release a reservation at the supplier and refund the guest.
710
754
  *
711
- * Free until `balance_due_by`; after that the hotel's own cancellation ladder
712
- * applies and can reach 100%. The ladder ships in the booking's `terms`, so
713
- * you can see the cost before calling this. The 5% reservation fee is NOT
714
- * refunded.
755
+ * Only this account's own bookings can be cancelled (anything else is 404). A
756
+ * zero-charge cancellation — a refundable rate before its
757
+ * `free_cancellation_until` — refunds the charge in full, or releases a hold
758
+ * not yet captured. A cancellation that would cost money is refused with 409;
759
+ * the hotel's own ladder is in the booking's `terms`.
715
760
  *
716
761
  * This drives a browser at the supplier and takes over a minute. If it times
717
762
  * out, do not assume it failed — re-check before retrying.
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  LetsFG,
4
4
  LetsFGError,
5
5
  offerSummary
6
- } from "./chunk-F5BBI6XX.mjs";
6
+ } from "./chunk-6IWYHW62.mjs";
7
7
 
8
8
  // src/auth.ts
9
9
  import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";