letsfg 2026.5.71 → 2026.5.73

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.
@@ -1671,13 +1671,23 @@ var LetsFG = class {
1671
1671
  return Array.isArray(data) ? data : data.locations || [];
1672
1672
  }
1673
1673
  /**
1674
- * Unlock a flight offer — confirms live price, reveals direct airline booking URL.
1675
- * Developer API only, legacy — there is no unlock endpoint on a PFS Bearer
1676
- * token, so PFS callers use book() directly.
1674
+ * RETIRED 2026-09-08. Throws instead of calling the server.
1675
+ *
1676
+ * There is no unlock step on any lane. Unlock existed to confirm a live price
1677
+ * before charging; booking now HOLDS the fare on the connected payment method
1678
+ * and captures only once a real airline PNR exists, so a fare that moved
1679
+ * cannot become a charge for a ticket you did not get. If it moves at
1680
+ * checkout you get a `price_change` question to accept or decline instead.
1681
+ *
1682
+ * Kept as a method, and throwing locally rather than making the request, so an
1683
+ * older caller gets one clear sentence at the line that is actually wrong —
1684
+ * not a 410 body to decode, and not a TypeError somewhere else.
1677
1685
  */
1678
- async unlock(offerId) {
1679
- this.requireApiKey();
1680
- return this.post("/developers/api/v1/bookings/unlock", { offer_id: offerId });
1686
+ async unlock(_offerId) {
1687
+ throw new LetsFGError(
1688
+ "unlock() was retired on 2026-09-08 and the endpoint answers 410 Gone. There is no unlock step: call book() directly. The fare is held on the connected payment method and captured only against a real airline PNR. See https://letsfg.co/developers/api/docs",
1689
+ 410
1690
+ );
1681
1691
  }
1682
1692
  /**
1683
1693
  * Book a flight.
@@ -1688,9 +1698,14 @@ var LetsFG = class {
1688
1698
  * complete, { ok, booked: false, booking_url } — hand the link to the user,
1689
1699
  * nothing was charged.
1690
1700
  *
1691
- * Developer API (X-API-Key): charges ticket price + service fee via Stripe,
1692
- * creates a real PNR. Requires unlock() first. Always provide
1693
- * idempotencyKey to prevent double-bookings on retry.
1701
+ * Developer API (X-API-Key): POST /flights/book. NO unlock step. searchId is
1702
+ * REQUIRED — an offer is bookable only inside the search that produced it.
1703
+ * The connected Revolut method is HELD, not charged; a LetsFG booking agent
1704
+ * buys the ticket and the hold is captured only against a real airline PNR.
1705
+ * Returns the 202 { ok, booking_id, state, held, charged: 0, poll_url } —
1706
+ * poll getBooking(bookingId) until `terminal`, or use bookAndWait().
1707
+ * Always provide idempotencyKey: a retry with the same key returns the
1708
+ * existing booking instead of opening a second hold on the card.
1694
1709
  */
1695
1710
  async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "", searchId = "") {
1696
1711
  this.requireAuth();
@@ -1711,15 +1726,92 @@ var LetsFG = class {
1711
1726
  });
1712
1727
  }
1713
1728
  this.requireApiKey();
1729
+ if (!searchId) {
1730
+ throw new LetsFGError(
1731
+ "searchId is required to book on the Developer API \u2014 pass the search_id from search()'s result. An offer can only be booked inside the search that produced it. (Before 2026-09-08 this argument was ignored on this path.)",
1732
+ 400
1733
+ );
1734
+ }
1735
+ const pax = passengers.map((p) => ({ ...p }));
1736
+ if (contactPhone && pax.length && !pax[0].phone_number) pax[0].phone_number = contactPhone;
1714
1737
  const body = {
1738
+ search_id: searchId,
1715
1739
  offer_id: offerId,
1716
- booking_type: "flight",
1717
- passengers,
1718
- contact_email: contactEmail,
1719
- contact_phone: contactPhone
1740
+ passengers: pax,
1741
+ contact_email: contactEmail
1720
1742
  };
1721
1743
  if (idempotencyKey) body.idempotency_key = idempotencyKey;
1722
- return this.post("/developers/api/v1/bookings/book", body);
1744
+ return this.post("/developers/api/v1/flights/book", body);
1745
+ }
1746
+ /**
1747
+ * Poll a Developer API flight booking.
1748
+ *
1749
+ * Poll every few seconds until `terminal` is true. The poll is ALSO how LetsFG
1750
+ * knows you are still there, which is what keeps a booking paused on a
1751
+ * question alive — so do not back off to minutes.
1752
+ *
1753
+ * States: authorised, card_issued, booking_in_progress, awaiting_settlement,
1754
+ * then completed (with `pnr` and `charged_amount`), failed (hold released,
1755
+ * nothing charged) or needs_attention (a human at LetsFG is on it — do not
1756
+ * book again).
1757
+ */
1758
+ async getBooking(bookingId) {
1759
+ this.requireApiKey();
1760
+ return this.getWithAuth(
1761
+ `/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}`
1762
+ );
1763
+ }
1764
+ /**
1765
+ * Answer the open `question` on a booking.
1766
+ *
1767
+ * Echo the question's `round`. A stale round is refused with 409 rather than
1768
+ * guessed at, so an answer to an old question can never be applied to a new
1769
+ * one. Seat: { seats: [...] } or { skip: true }. Price change or paid extra:
1770
+ * { confirm: true } or { skip: true } — declining an extra still completes
1771
+ * the booking, without it.
1772
+ */
1773
+ async answerBooking(bookingId, round, answer = {}) {
1774
+ this.requireApiKey();
1775
+ return this.post(
1776
+ `/developers/api/v1/flights/bookings/${encodeURIComponent(bookingId)}/answer`,
1777
+ { round, ...answer }
1778
+ );
1779
+ }
1780
+ /**
1781
+ * Book and poll to a terminal state. Mirrors bookHotelAndWait().
1782
+ *
1783
+ * Blocks for as long as the booking takes (4–11 minutes typically), so use
1784
+ * book() + getBooking() instead if your caller has a request timeout.
1785
+ *
1786
+ * `onQuestion` returns the answer for answerBooking(). Without it, a fare
1787
+ * increase is ACCEPTED and a paid extra is DECLINED — the conservative
1788
+ * reading of "the traveller asked for this flight".
1789
+ */
1790
+ async bookAndWait(offerId, passengers, contactEmail, searchId, opts = {}) {
1791
+ const { contactPhone = "", idempotencyKey = "", pollMs = 5e3, timeoutMs = 9e5, onQuestion } = opts;
1792
+ const started = await this.book(
1793
+ offerId,
1794
+ passengers,
1795
+ contactEmail,
1796
+ contactPhone,
1797
+ idempotencyKey,
1798
+ searchId
1799
+ );
1800
+ if (!started || started.ok !== true) return started;
1801
+ const bookingId = String(started.booking_id);
1802
+ const deadline = Date.now() + timeoutMs;
1803
+ while (Date.now() < deadline) {
1804
+ await new Promise((r) => setTimeout(r, pollMs));
1805
+ const state = await this.getBooking(bookingId);
1806
+ const question = state.question;
1807
+ if (question) {
1808
+ const answer = onQuestion ? onQuestion(question) : question.kind === "extra" ? { skip: true } : { confirm: true };
1809
+ await this.answerBooking(bookingId, Number(question.round), answer);
1810
+ continue;
1811
+ }
1812
+ if (state.terminal) return state;
1813
+ }
1814
+ return this.getBooking(bookingId);
1723
1815
  }
1724
1816
  // ── Hotels ──────────────────────────────────────────────────────────
1725
1817
  //
@@ -1877,15 +1969,37 @@ var LetsFG = class {
1877
1969
  );
1878
1970
  }
1879
1971
  /**
1880
- * [Developer API only] Attach a card to a PAID prepaid Developer API account.
1972
+ * [Developer API] Mint a one-time link for connecting a Revolut payment method.
1973
+ *
1974
+ * This replaced setupPayment() on 2026-09-08. Nothing is charged to connect, and
1975
+ * card details never touch LetsFG: the returned `connect_url` opens a hosted page
1976
+ * where the developer saves a card, Revolut Pay or Google Pay. A PERSON must open
1977
+ * it in a browser — there is no endpoint that takes card details, so do not ask a
1978
+ * user for a card number and do not try to automate this step.
1881
1979
  *
1882
- * Most agents should NOT call this. It is unrelated to authenticating for
1883
- * search and booking — for that, run `letsfg auth`, which puts a card on file
1884
- * through a zero-amount setup and creates no billing account.
1980
+ * Most agents should NOT need a Developer API account at all. To authenticate for
1981
+ * search and booking, run `letsfg auth`, which creates no billing account.
1885
1982
  */
1886
- async setupPayment(token = "tok_visa") {
1983
+ async connectPayment() {
1887
1984
  this.requireApiKey();
1888
- return this.post("/developers/api/v1/agents/setup-payment", { token });
1985
+ return this.post("/developers/api/v1/agents/connect-payment", {});
1986
+ }
1987
+ /**
1988
+ * RETIRED 2026-09-08 with Stripe. Throws instead of calling the server.
1989
+ *
1990
+ * `/agents/setup-payment` answers 410 Gone. Payment enrolment moved onto the same
1991
+ * Revolut rail as the rest of the product: call connectPayment() and open the
1992
+ * `connect_url` it returns.
1993
+ *
1994
+ * Kept as a method, and throwing locally rather than making the request, for the
1995
+ * same reason as unlock() — an older caller gets one clear sentence at the line that
1996
+ * is actually wrong, not a 410 body to decode and not a TypeError somewhere else.
1997
+ */
1998
+ async setupPayment(_token) {
1999
+ throw new LetsFGError(
2000
+ "setupPayment() was retired on 2026-09-08 with Stripe and the endpoint answers 410 Gone. Call connectPayment() instead and open the connect_url it returns; nothing is charged to connect. See https://letsfg.co/developers/api/docs",
2001
+ 410
2002
+ );
1889
2003
  }
1890
2004
  /**
1891
2005
  * Get current agent profile and usage stats.