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.
- package/README.md +325 -283
- package/dist/{chunk-F5BBI6XX.mjs → chunk-6IWYHW62.mjs} +93 -47
- package/dist/cli.js +92 -47
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +80 -32
- package/dist/index.d.ts +80 -32
- package/dist/index.js +94 -47
- package/dist/index.mjs +3 -1
- package/package.json +56 -56
|
@@ -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
|
-
|
|
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
|
|
1819
|
-
// deliberate: a hotel search opens a real session at the
|
|
1820
|
-
// blocks a real rate, so a caller is never allowed to
|
|
1821
|
-
// commitment only to discover it cannot pay. The same
|
|
1822
|
-
// flight booking authorises hotels
|
|
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
|
-
//
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
1827
|
-
//
|
|
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
|
-
*
|
|
1849
|
-
*
|
|
1850
|
-
* `
|
|
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
|
-
*
|
|
1853
|
-
*
|
|
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
|
-
*
|
|
1875
|
-
*
|
|
1876
|
-
*
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
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
|
-
*
|
|
1881
|
-
*
|
|
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`
|
|
1884
|
-
* them
|
|
1885
|
-
*
|
|
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
|
|
1888
|
-
* the
|
|
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
|
-
|
|
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'
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
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
|
-
*
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
*
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
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
|
-
|
|
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
|
|
575
|
-
// deliberate: a hotel search opens a real session at the
|
|
576
|
-
// blocks a real rate, so a caller is never allowed to
|
|
577
|
-
// commitment only to discover it cannot pay. The same
|
|
578
|
-
// flight booking authorises hotels
|
|
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
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
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
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* `
|
|
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
|
-
*
|
|
609
|
-
*
|
|
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
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
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
|
-
*
|
|
637
|
-
*
|
|
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`
|
|
640
|
-
* them
|
|
641
|
-
*
|
|
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
|
|
644
|
-
* the
|
|
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
|
-
|
|
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'
|
|
673
|
-
*
|
|
674
|
-
*
|
|
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
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
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.
|