boostedtravel 0.2.1 → 0.2.3

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.
@@ -1,26 +1,111 @@
1
1
  // src/index.ts
2
+ var ErrorCode = {
3
+ // Transient (safe to retry after short delay)
4
+ SUPPLIER_TIMEOUT: "SUPPLIER_TIMEOUT",
5
+ RATE_LIMITED: "RATE_LIMITED",
6
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
7
+ NETWORK_ERROR: "NETWORK_ERROR",
8
+ // Validation (fix input, then retry)
9
+ INVALID_IATA: "INVALID_IATA",
10
+ INVALID_DATE: "INVALID_DATE",
11
+ INVALID_PASSENGERS: "INVALID_PASSENGERS",
12
+ UNSUPPORTED_ROUTE: "UNSUPPORTED_ROUTE",
13
+ MISSING_PARAMETER: "MISSING_PARAMETER",
14
+ INVALID_PARAMETER: "INVALID_PARAMETER",
15
+ // Business (requires human decision)
16
+ AUTH_INVALID: "AUTH_INVALID",
17
+ PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
18
+ PAYMENT_DECLINED: "PAYMENT_DECLINED",
19
+ OFFER_EXPIRED: "OFFER_EXPIRED",
20
+ OFFER_NOT_UNLOCKED: "OFFER_NOT_UNLOCKED",
21
+ FARE_CHANGED: "FARE_CHANGED",
22
+ ALREADY_BOOKED: "ALREADY_BOOKED",
23
+ BOOKING_FAILED: "BOOKING_FAILED"
24
+ };
25
+ var ErrorCategory = {
26
+ TRANSIENT: "transient",
27
+ VALIDATION: "validation",
28
+ BUSINESS: "business"
29
+ };
30
+ var CODE_TO_CATEGORY = {
31
+ [ErrorCode.SUPPLIER_TIMEOUT]: ErrorCategory.TRANSIENT,
32
+ [ErrorCode.RATE_LIMITED]: ErrorCategory.TRANSIENT,
33
+ [ErrorCode.SERVICE_UNAVAILABLE]: ErrorCategory.TRANSIENT,
34
+ [ErrorCode.NETWORK_ERROR]: ErrorCategory.TRANSIENT,
35
+ [ErrorCode.INVALID_IATA]: ErrorCategory.VALIDATION,
36
+ [ErrorCode.INVALID_DATE]: ErrorCategory.VALIDATION,
37
+ [ErrorCode.INVALID_PASSENGERS]: ErrorCategory.VALIDATION,
38
+ [ErrorCode.UNSUPPORTED_ROUTE]: ErrorCategory.VALIDATION,
39
+ [ErrorCode.MISSING_PARAMETER]: ErrorCategory.VALIDATION,
40
+ [ErrorCode.INVALID_PARAMETER]: ErrorCategory.VALIDATION,
41
+ [ErrorCode.AUTH_INVALID]: ErrorCategory.BUSINESS,
42
+ [ErrorCode.PAYMENT_REQUIRED]: ErrorCategory.BUSINESS,
43
+ [ErrorCode.PAYMENT_DECLINED]: ErrorCategory.BUSINESS,
44
+ [ErrorCode.OFFER_EXPIRED]: ErrorCategory.BUSINESS,
45
+ [ErrorCode.OFFER_NOT_UNLOCKED]: ErrorCategory.BUSINESS,
46
+ [ErrorCode.FARE_CHANGED]: ErrorCategory.BUSINESS,
47
+ [ErrorCode.ALREADY_BOOKED]: ErrorCategory.BUSINESS,
48
+ [ErrorCode.BOOKING_FAILED]: ErrorCategory.BUSINESS
49
+ };
50
+ function inferErrorCode(statusCode, detail) {
51
+ const d = detail.toLowerCase();
52
+ if (statusCode === 401) return ErrorCode.AUTH_INVALID;
53
+ if (statusCode === 402) return d.includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
54
+ if (statusCode === 410) return ErrorCode.OFFER_EXPIRED;
55
+ if (statusCode === 422) {
56
+ if (d.includes("iata") || d.includes("airport")) return ErrorCode.INVALID_IATA;
57
+ if (d.includes("date")) return ErrorCode.INVALID_DATE;
58
+ if (d.includes("passenger")) return ErrorCode.INVALID_PASSENGERS;
59
+ if (d.includes("route")) return ErrorCode.UNSUPPORTED_ROUTE;
60
+ return ErrorCode.INVALID_PARAMETER;
61
+ }
62
+ if (statusCode === 429) return ErrorCode.RATE_LIMITED;
63
+ if (statusCode === 503) return ErrorCode.SERVICE_UNAVAILABLE;
64
+ if (statusCode === 504) return ErrorCode.SUPPLIER_TIMEOUT;
65
+ if (statusCode === 409) return ErrorCode.ALREADY_BOOKED;
66
+ return statusCode >= 500 ? ErrorCode.BOOKING_FAILED : ErrorCode.INVALID_PARAMETER;
67
+ }
2
68
  var BoostedTravelError = class extends Error {
3
69
  statusCode;
4
70
  response;
5
- constructor(message, statusCode = 0, response = {}) {
71
+ errorCode;
72
+ errorCategory;
73
+ isRetryable;
74
+ constructor(message, statusCode = 0, response = {}, errorCode = "") {
6
75
  super(message);
7
76
  this.name = "BoostedTravelError";
8
77
  this.statusCode = statusCode;
9
78
  this.response = response;
79
+ this.errorCode = errorCode || response.error_code || "";
80
+ this.errorCategory = CODE_TO_CATEGORY[this.errorCode] || ErrorCategory.BUSINESS;
81
+ this.isRetryable = this.errorCategory === ErrorCategory.TRANSIENT;
10
82
  }
11
83
  };
12
84
  var AuthenticationError = class extends BoostedTravelError {
13
85
  constructor(message, response = {}) {
14
- super(message, 401, response);
86
+ super(message, 401, response, ErrorCode.AUTH_INVALID);
15
87
  this.name = "AuthenticationError";
16
88
  }
17
89
  };
18
90
  var PaymentRequiredError = class extends BoostedTravelError {
19
91
  constructor(message, response = {}) {
20
- super(message, 402, response);
92
+ const code = message.toLowerCase().includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
93
+ super(message, 402, response, code);
21
94
  this.name = "PaymentRequiredError";
22
95
  }
23
96
  };
97
+ var OfferExpiredError = class extends BoostedTravelError {
98
+ constructor(message, response = {}) {
99
+ super(message, 410, response, ErrorCode.OFFER_EXPIRED);
100
+ this.name = "OfferExpiredError";
101
+ }
102
+ };
103
+ var ValidationError = class extends BoostedTravelError {
104
+ constructor(message, statusCode = 422, response = {}, errorCode = "") {
105
+ super(message, statusCode, response, errorCode || ErrorCode.INVALID_PARAMETER);
106
+ this.name = "ValidationError";
107
+ }
108
+ };
24
109
  function routeStr(route) {
25
110
  if (!route.segments.length) return "";
26
111
  const codes = [route.segments[0].origin, ...route.segments.map((s) => s.destination)];
@@ -154,16 +239,20 @@ var BoostedTravel = class {
154
239
  /**
155
240
  * Book a flight — FREE after unlock.
156
241
  * Creates a real airline reservation with PNR.
242
+ *
243
+ * Always provide idempotencyKey to prevent double-bookings on retry.
157
244
  */
158
- async book(offerId, passengers, contactEmail, contactPhone = "") {
245
+ async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
159
246
  this.requireApiKey();
160
- return this.post("/api/v1/bookings/book", {
247
+ const body = {
161
248
  offer_id: offerId,
162
249
  booking_type: "flight",
163
250
  passengers,
164
251
  contact_email: contactEmail,
165
252
  contact_phone: contactPhone
166
- });
253
+ };
254
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
255
+ return this.post("/api/v1/bookings/book", body);
167
256
  }
168
257
  /**
169
258
  * Set up payment method (payment token).
@@ -232,9 +321,12 @@ var BoostedTravel = class {
232
321
  const data = await resp.json();
233
322
  if (!resp.ok) {
234
323
  const detail = data.detail || `API error (${resp.status})`;
324
+ const code = data.error_code || inferErrorCode(resp.status, detail);
235
325
  if (resp.status === 401) throw new AuthenticationError(detail, data);
236
326
  if (resp.status === 402) throw new PaymentRequiredError(detail, data);
237
- throw new BoostedTravelError(detail, resp.status, data);
327
+ if (resp.status === 410) throw new OfferExpiredError(detail, data);
328
+ if (resp.status === 422) throw new ValidationError(detail, resp.status, data, code);
329
+ throw new BoostedTravelError(detail, resp.status, data, code);
238
330
  }
239
331
  return data;
240
332
  } finally {
@@ -245,9 +337,13 @@ var BoostedTravel = class {
245
337
  var index_default = BoostedTravel;
246
338
 
247
339
  export {
340
+ ErrorCode,
341
+ ErrorCategory,
248
342
  BoostedTravelError,
249
343
  AuthenticationError,
250
344
  PaymentRequiredError,
345
+ OfferExpiredError,
346
+ ValidationError,
251
347
  offerSummary,
252
348
  cheapestOffer,
253
349
  searchLocal,
package/dist/cli.js CHANGED
@@ -2,28 +2,113 @@
2
2
  "use strict";
3
3
 
4
4
  // src/index.ts
5
+ var ErrorCode = {
6
+ // Transient (safe to retry after short delay)
7
+ SUPPLIER_TIMEOUT: "SUPPLIER_TIMEOUT",
8
+ RATE_LIMITED: "RATE_LIMITED",
9
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
10
+ NETWORK_ERROR: "NETWORK_ERROR",
11
+ // Validation (fix input, then retry)
12
+ INVALID_IATA: "INVALID_IATA",
13
+ INVALID_DATE: "INVALID_DATE",
14
+ INVALID_PASSENGERS: "INVALID_PASSENGERS",
15
+ UNSUPPORTED_ROUTE: "UNSUPPORTED_ROUTE",
16
+ MISSING_PARAMETER: "MISSING_PARAMETER",
17
+ INVALID_PARAMETER: "INVALID_PARAMETER",
18
+ // Business (requires human decision)
19
+ AUTH_INVALID: "AUTH_INVALID",
20
+ PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
21
+ PAYMENT_DECLINED: "PAYMENT_DECLINED",
22
+ OFFER_EXPIRED: "OFFER_EXPIRED",
23
+ OFFER_NOT_UNLOCKED: "OFFER_NOT_UNLOCKED",
24
+ FARE_CHANGED: "FARE_CHANGED",
25
+ ALREADY_BOOKED: "ALREADY_BOOKED",
26
+ BOOKING_FAILED: "BOOKING_FAILED"
27
+ };
28
+ var ErrorCategory = {
29
+ TRANSIENT: "transient",
30
+ VALIDATION: "validation",
31
+ BUSINESS: "business"
32
+ };
33
+ var CODE_TO_CATEGORY = {
34
+ [ErrorCode.SUPPLIER_TIMEOUT]: ErrorCategory.TRANSIENT,
35
+ [ErrorCode.RATE_LIMITED]: ErrorCategory.TRANSIENT,
36
+ [ErrorCode.SERVICE_UNAVAILABLE]: ErrorCategory.TRANSIENT,
37
+ [ErrorCode.NETWORK_ERROR]: ErrorCategory.TRANSIENT,
38
+ [ErrorCode.INVALID_IATA]: ErrorCategory.VALIDATION,
39
+ [ErrorCode.INVALID_DATE]: ErrorCategory.VALIDATION,
40
+ [ErrorCode.INVALID_PASSENGERS]: ErrorCategory.VALIDATION,
41
+ [ErrorCode.UNSUPPORTED_ROUTE]: ErrorCategory.VALIDATION,
42
+ [ErrorCode.MISSING_PARAMETER]: ErrorCategory.VALIDATION,
43
+ [ErrorCode.INVALID_PARAMETER]: ErrorCategory.VALIDATION,
44
+ [ErrorCode.AUTH_INVALID]: ErrorCategory.BUSINESS,
45
+ [ErrorCode.PAYMENT_REQUIRED]: ErrorCategory.BUSINESS,
46
+ [ErrorCode.PAYMENT_DECLINED]: ErrorCategory.BUSINESS,
47
+ [ErrorCode.OFFER_EXPIRED]: ErrorCategory.BUSINESS,
48
+ [ErrorCode.OFFER_NOT_UNLOCKED]: ErrorCategory.BUSINESS,
49
+ [ErrorCode.FARE_CHANGED]: ErrorCategory.BUSINESS,
50
+ [ErrorCode.ALREADY_BOOKED]: ErrorCategory.BUSINESS,
51
+ [ErrorCode.BOOKING_FAILED]: ErrorCategory.BUSINESS
52
+ };
53
+ function inferErrorCode(statusCode, detail) {
54
+ const d = detail.toLowerCase();
55
+ if (statusCode === 401) return ErrorCode.AUTH_INVALID;
56
+ if (statusCode === 402) return d.includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
57
+ if (statusCode === 410) return ErrorCode.OFFER_EXPIRED;
58
+ if (statusCode === 422) {
59
+ if (d.includes("iata") || d.includes("airport")) return ErrorCode.INVALID_IATA;
60
+ if (d.includes("date")) return ErrorCode.INVALID_DATE;
61
+ if (d.includes("passenger")) return ErrorCode.INVALID_PASSENGERS;
62
+ if (d.includes("route")) return ErrorCode.UNSUPPORTED_ROUTE;
63
+ return ErrorCode.INVALID_PARAMETER;
64
+ }
65
+ if (statusCode === 429) return ErrorCode.RATE_LIMITED;
66
+ if (statusCode === 503) return ErrorCode.SERVICE_UNAVAILABLE;
67
+ if (statusCode === 504) return ErrorCode.SUPPLIER_TIMEOUT;
68
+ if (statusCode === 409) return ErrorCode.ALREADY_BOOKED;
69
+ return statusCode >= 500 ? ErrorCode.BOOKING_FAILED : ErrorCode.INVALID_PARAMETER;
70
+ }
5
71
  var BoostedTravelError = class extends Error {
6
72
  statusCode;
7
73
  response;
8
- constructor(message, statusCode = 0, response = {}) {
74
+ errorCode;
75
+ errorCategory;
76
+ isRetryable;
77
+ constructor(message, statusCode = 0, response = {}, errorCode = "") {
9
78
  super(message);
10
79
  this.name = "BoostedTravelError";
11
80
  this.statusCode = statusCode;
12
81
  this.response = response;
82
+ this.errorCode = errorCode || response.error_code || "";
83
+ this.errorCategory = CODE_TO_CATEGORY[this.errorCode] || ErrorCategory.BUSINESS;
84
+ this.isRetryable = this.errorCategory === ErrorCategory.TRANSIENT;
13
85
  }
14
86
  };
15
87
  var AuthenticationError = class extends BoostedTravelError {
16
88
  constructor(message, response = {}) {
17
- super(message, 401, response);
89
+ super(message, 401, response, ErrorCode.AUTH_INVALID);
18
90
  this.name = "AuthenticationError";
19
91
  }
20
92
  };
21
93
  var PaymentRequiredError = class extends BoostedTravelError {
22
94
  constructor(message, response = {}) {
23
- super(message, 402, response);
95
+ const code = message.toLowerCase().includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
96
+ super(message, 402, response, code);
24
97
  this.name = "PaymentRequiredError";
25
98
  }
26
99
  };
100
+ var OfferExpiredError = class extends BoostedTravelError {
101
+ constructor(message, response = {}) {
102
+ super(message, 410, response, ErrorCode.OFFER_EXPIRED);
103
+ this.name = "OfferExpiredError";
104
+ }
105
+ };
106
+ var ValidationError = class extends BoostedTravelError {
107
+ constructor(message, statusCode = 422, response = {}, errorCode = "") {
108
+ super(message, statusCode, response, errorCode || ErrorCode.INVALID_PARAMETER);
109
+ this.name = "ValidationError";
110
+ }
111
+ };
27
112
  function routeStr(route) {
28
113
  if (!route.segments.length) return "";
29
114
  const codes = [route.segments[0].origin, ...route.segments.map((s) => s.destination)];
@@ -105,16 +190,20 @@ var BoostedTravel = class {
105
190
  /**
106
191
  * Book a flight — FREE after unlock.
107
192
  * Creates a real airline reservation with PNR.
193
+ *
194
+ * Always provide idempotencyKey to prevent double-bookings on retry.
108
195
  */
109
- async book(offerId, passengers, contactEmail, contactPhone = "") {
196
+ async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
110
197
  this.requireApiKey();
111
- return this.post("/api/v1/bookings/book", {
198
+ const body = {
112
199
  offer_id: offerId,
113
200
  booking_type: "flight",
114
201
  passengers,
115
202
  contact_email: contactEmail,
116
203
  contact_phone: contactPhone
117
- });
204
+ };
205
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
206
+ return this.post("/api/v1/bookings/book", body);
118
207
  }
119
208
  /**
120
209
  * Set up payment method (payment token).
@@ -183,9 +272,12 @@ var BoostedTravel = class {
183
272
  const data = await resp.json();
184
273
  if (!resp.ok) {
185
274
  const detail = data.detail || `API error (${resp.status})`;
275
+ const code = data.error_code || inferErrorCode(resp.status, detail);
186
276
  if (resp.status === 401) throw new AuthenticationError(detail, data);
187
277
  if (resp.status === 402) throw new PaymentRequiredError(detail, data);
188
- throw new BoostedTravelError(detail, resp.status, data);
278
+ if (resp.status === 410) throw new OfferExpiredError(detail, data);
279
+ if (resp.status === 422) throw new ValidationError(detail, resp.status, data, code);
280
+ throw new BoostedTravelError(detail, resp.status, data, code);
189
281
  }
190
282
  return data;
191
283
  } finally {
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  BoostedTravel,
4
4
  BoostedTravelError,
5
5
  offerSummary
6
- } from "./chunk-AVADUS2V.mjs";
6
+ } from "./chunk-RJB7OAS2.mjs";
7
7
 
8
8
  // src/cli.ts
9
9
  function getFlag(args, flag, alias) {
package/dist/index.d.mts CHANGED
@@ -118,10 +118,40 @@ interface BoostedTravelConfig {
118
118
  baseUrl?: string;
119
119
  timeout?: number;
120
120
  }
121
+ declare const ErrorCode: {
122
+ readonly SUPPLIER_TIMEOUT: "SUPPLIER_TIMEOUT";
123
+ readonly RATE_LIMITED: "RATE_LIMITED";
124
+ readonly SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE";
125
+ readonly NETWORK_ERROR: "NETWORK_ERROR";
126
+ readonly INVALID_IATA: "INVALID_IATA";
127
+ readonly INVALID_DATE: "INVALID_DATE";
128
+ readonly INVALID_PASSENGERS: "INVALID_PASSENGERS";
129
+ readonly UNSUPPORTED_ROUTE: "UNSUPPORTED_ROUTE";
130
+ readonly MISSING_PARAMETER: "MISSING_PARAMETER";
131
+ readonly INVALID_PARAMETER: "INVALID_PARAMETER";
132
+ readonly AUTH_INVALID: "AUTH_INVALID";
133
+ readonly PAYMENT_REQUIRED: "PAYMENT_REQUIRED";
134
+ readonly PAYMENT_DECLINED: "PAYMENT_DECLINED";
135
+ readonly OFFER_EXPIRED: "OFFER_EXPIRED";
136
+ readonly OFFER_NOT_UNLOCKED: "OFFER_NOT_UNLOCKED";
137
+ readonly FARE_CHANGED: "FARE_CHANGED";
138
+ readonly ALREADY_BOOKED: "ALREADY_BOOKED";
139
+ readonly BOOKING_FAILED: "BOOKING_FAILED";
140
+ };
141
+ type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode];
142
+ declare const ErrorCategory: {
143
+ readonly TRANSIENT: "transient";
144
+ readonly VALIDATION: "validation";
145
+ readonly BUSINESS: "business";
146
+ };
147
+ type ErrorCategoryType = (typeof ErrorCategory)[keyof typeof ErrorCategory];
121
148
  declare class BoostedTravelError extends Error {
122
149
  statusCode: number;
123
150
  response: Record<string, unknown>;
124
- constructor(message: string, statusCode?: number, response?: Record<string, unknown>);
151
+ errorCode: string;
152
+ errorCategory: ErrorCategoryType;
153
+ isRetryable: boolean;
154
+ constructor(message: string, statusCode?: number, response?: Record<string, unknown>, errorCode?: string);
125
155
  }
126
156
  declare class AuthenticationError extends BoostedTravelError {
127
157
  constructor(message: string, response?: Record<string, unknown>);
@@ -129,6 +159,12 @@ declare class AuthenticationError extends BoostedTravelError {
129
159
  declare class PaymentRequiredError extends BoostedTravelError {
130
160
  constructor(message: string, response?: Record<string, unknown>);
131
161
  }
162
+ declare class OfferExpiredError extends BoostedTravelError {
163
+ constructor(message: string, response?: Record<string, unknown>);
164
+ }
165
+ declare class ValidationError extends BoostedTravelError {
166
+ constructor(message: string, statusCode?: number, response?: Record<string, unknown>, errorCode?: string);
167
+ }
132
168
  /** One-line offer summary */
133
169
  declare function offerSummary(offer: FlightOffer): string;
134
170
  /** Get cheapest offer from search results */
@@ -171,8 +207,10 @@ declare class BoostedTravel {
171
207
  /**
172
208
  * Book a flight — FREE after unlock.
173
209
  * Creates a real airline reservation with PNR.
210
+ *
211
+ * Always provide idempotencyKey to prevent double-bookings on retry.
174
212
  */
175
- book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string): Promise<BookingResult>;
213
+ book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise<BookingResult>;
176
214
  /**
177
215
  * Set up payment method (payment token).
178
216
  */
@@ -190,4 +228,4 @@ declare class BoostedTravel {
190
228
  private request;
191
229
  }
192
230
 
193
- export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, type Passenger, PaymentRequiredError, type SearchOptions, type UnlockResult, cheapestOffer, BoostedTravel as default, searchLocal as localSearch, offerSummary, searchLocal };
231
+ export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, OfferExpiredError, type Passenger, PaymentRequiredError, type SearchOptions, type UnlockResult, ValidationError, cheapestOffer, BoostedTravel as default, searchLocal as localSearch, offerSummary, searchLocal };
package/dist/index.d.ts CHANGED
@@ -118,10 +118,40 @@ interface BoostedTravelConfig {
118
118
  baseUrl?: string;
119
119
  timeout?: number;
120
120
  }
121
+ declare const ErrorCode: {
122
+ readonly SUPPLIER_TIMEOUT: "SUPPLIER_TIMEOUT";
123
+ readonly RATE_LIMITED: "RATE_LIMITED";
124
+ readonly SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE";
125
+ readonly NETWORK_ERROR: "NETWORK_ERROR";
126
+ readonly INVALID_IATA: "INVALID_IATA";
127
+ readonly INVALID_DATE: "INVALID_DATE";
128
+ readonly INVALID_PASSENGERS: "INVALID_PASSENGERS";
129
+ readonly UNSUPPORTED_ROUTE: "UNSUPPORTED_ROUTE";
130
+ readonly MISSING_PARAMETER: "MISSING_PARAMETER";
131
+ readonly INVALID_PARAMETER: "INVALID_PARAMETER";
132
+ readonly AUTH_INVALID: "AUTH_INVALID";
133
+ readonly PAYMENT_REQUIRED: "PAYMENT_REQUIRED";
134
+ readonly PAYMENT_DECLINED: "PAYMENT_DECLINED";
135
+ readonly OFFER_EXPIRED: "OFFER_EXPIRED";
136
+ readonly OFFER_NOT_UNLOCKED: "OFFER_NOT_UNLOCKED";
137
+ readonly FARE_CHANGED: "FARE_CHANGED";
138
+ readonly ALREADY_BOOKED: "ALREADY_BOOKED";
139
+ readonly BOOKING_FAILED: "BOOKING_FAILED";
140
+ };
141
+ type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode];
142
+ declare const ErrorCategory: {
143
+ readonly TRANSIENT: "transient";
144
+ readonly VALIDATION: "validation";
145
+ readonly BUSINESS: "business";
146
+ };
147
+ type ErrorCategoryType = (typeof ErrorCategory)[keyof typeof ErrorCategory];
121
148
  declare class BoostedTravelError extends Error {
122
149
  statusCode: number;
123
150
  response: Record<string, unknown>;
124
- constructor(message: string, statusCode?: number, response?: Record<string, unknown>);
151
+ errorCode: string;
152
+ errorCategory: ErrorCategoryType;
153
+ isRetryable: boolean;
154
+ constructor(message: string, statusCode?: number, response?: Record<string, unknown>, errorCode?: string);
125
155
  }
126
156
  declare class AuthenticationError extends BoostedTravelError {
127
157
  constructor(message: string, response?: Record<string, unknown>);
@@ -129,6 +159,12 @@ declare class AuthenticationError extends BoostedTravelError {
129
159
  declare class PaymentRequiredError extends BoostedTravelError {
130
160
  constructor(message: string, response?: Record<string, unknown>);
131
161
  }
162
+ declare class OfferExpiredError extends BoostedTravelError {
163
+ constructor(message: string, response?: Record<string, unknown>);
164
+ }
165
+ declare class ValidationError extends BoostedTravelError {
166
+ constructor(message: string, statusCode?: number, response?: Record<string, unknown>, errorCode?: string);
167
+ }
132
168
  /** One-line offer summary */
133
169
  declare function offerSummary(offer: FlightOffer): string;
134
170
  /** Get cheapest offer from search results */
@@ -171,8 +207,10 @@ declare class BoostedTravel {
171
207
  /**
172
208
  * Book a flight — FREE after unlock.
173
209
  * Creates a real airline reservation with PNR.
210
+ *
211
+ * Always provide idempotencyKey to prevent double-bookings on retry.
174
212
  */
175
- book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string): Promise<BookingResult>;
213
+ book(offerId: string, passengers: Passenger[], contactEmail: string, contactPhone?: string, idempotencyKey?: string): Promise<BookingResult>;
176
214
  /**
177
215
  * Set up payment method (payment token).
178
216
  */
@@ -190,4 +228,4 @@ declare class BoostedTravel {
190
228
  private request;
191
229
  }
192
230
 
193
- export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, type Passenger, PaymentRequiredError, type SearchOptions, type UnlockResult, cheapestOffer, BoostedTravel as default, searchLocal as localSearch, offerSummary, searchLocal };
231
+ export { AuthenticationError, type BookingResult, BoostedTravel, type BoostedTravelConfig, BoostedTravelError, ErrorCategory, type ErrorCategoryType, ErrorCode, type ErrorCodeType, type FlightOffer, type FlightRoute, type FlightSearchResult, type FlightSegment, OfferExpiredError, type Passenger, PaymentRequiredError, type SearchOptions, type UnlockResult, ValidationError, cheapestOffer, BoostedTravel as default, searchLocal as localSearch, offerSummary, searchLocal };
package/dist/index.js CHANGED
@@ -33,7 +33,11 @@ __export(index_exports, {
33
33
  AuthenticationError: () => AuthenticationError,
34
34
  BoostedTravel: () => BoostedTravel,
35
35
  BoostedTravelError: () => BoostedTravelError,
36
+ ErrorCategory: () => ErrorCategory,
37
+ ErrorCode: () => ErrorCode,
38
+ OfferExpiredError: () => OfferExpiredError,
36
39
  PaymentRequiredError: () => PaymentRequiredError,
40
+ ValidationError: () => ValidationError,
37
41
  cheapestOffer: () => cheapestOffer,
38
42
  default: () => index_default,
39
43
  localSearch: () => searchLocal,
@@ -41,28 +45,113 @@ __export(index_exports, {
41
45
  searchLocal: () => searchLocal
42
46
  });
43
47
  module.exports = __toCommonJS(index_exports);
48
+ var ErrorCode = {
49
+ // Transient (safe to retry after short delay)
50
+ SUPPLIER_TIMEOUT: "SUPPLIER_TIMEOUT",
51
+ RATE_LIMITED: "RATE_LIMITED",
52
+ SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
53
+ NETWORK_ERROR: "NETWORK_ERROR",
54
+ // Validation (fix input, then retry)
55
+ INVALID_IATA: "INVALID_IATA",
56
+ INVALID_DATE: "INVALID_DATE",
57
+ INVALID_PASSENGERS: "INVALID_PASSENGERS",
58
+ UNSUPPORTED_ROUTE: "UNSUPPORTED_ROUTE",
59
+ MISSING_PARAMETER: "MISSING_PARAMETER",
60
+ INVALID_PARAMETER: "INVALID_PARAMETER",
61
+ // Business (requires human decision)
62
+ AUTH_INVALID: "AUTH_INVALID",
63
+ PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
64
+ PAYMENT_DECLINED: "PAYMENT_DECLINED",
65
+ OFFER_EXPIRED: "OFFER_EXPIRED",
66
+ OFFER_NOT_UNLOCKED: "OFFER_NOT_UNLOCKED",
67
+ FARE_CHANGED: "FARE_CHANGED",
68
+ ALREADY_BOOKED: "ALREADY_BOOKED",
69
+ BOOKING_FAILED: "BOOKING_FAILED"
70
+ };
71
+ var ErrorCategory = {
72
+ TRANSIENT: "transient",
73
+ VALIDATION: "validation",
74
+ BUSINESS: "business"
75
+ };
76
+ var CODE_TO_CATEGORY = {
77
+ [ErrorCode.SUPPLIER_TIMEOUT]: ErrorCategory.TRANSIENT,
78
+ [ErrorCode.RATE_LIMITED]: ErrorCategory.TRANSIENT,
79
+ [ErrorCode.SERVICE_UNAVAILABLE]: ErrorCategory.TRANSIENT,
80
+ [ErrorCode.NETWORK_ERROR]: ErrorCategory.TRANSIENT,
81
+ [ErrorCode.INVALID_IATA]: ErrorCategory.VALIDATION,
82
+ [ErrorCode.INVALID_DATE]: ErrorCategory.VALIDATION,
83
+ [ErrorCode.INVALID_PASSENGERS]: ErrorCategory.VALIDATION,
84
+ [ErrorCode.UNSUPPORTED_ROUTE]: ErrorCategory.VALIDATION,
85
+ [ErrorCode.MISSING_PARAMETER]: ErrorCategory.VALIDATION,
86
+ [ErrorCode.INVALID_PARAMETER]: ErrorCategory.VALIDATION,
87
+ [ErrorCode.AUTH_INVALID]: ErrorCategory.BUSINESS,
88
+ [ErrorCode.PAYMENT_REQUIRED]: ErrorCategory.BUSINESS,
89
+ [ErrorCode.PAYMENT_DECLINED]: ErrorCategory.BUSINESS,
90
+ [ErrorCode.OFFER_EXPIRED]: ErrorCategory.BUSINESS,
91
+ [ErrorCode.OFFER_NOT_UNLOCKED]: ErrorCategory.BUSINESS,
92
+ [ErrorCode.FARE_CHANGED]: ErrorCategory.BUSINESS,
93
+ [ErrorCode.ALREADY_BOOKED]: ErrorCategory.BUSINESS,
94
+ [ErrorCode.BOOKING_FAILED]: ErrorCategory.BUSINESS
95
+ };
96
+ function inferErrorCode(statusCode, detail) {
97
+ const d = detail.toLowerCase();
98
+ if (statusCode === 401) return ErrorCode.AUTH_INVALID;
99
+ if (statusCode === 402) return d.includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
100
+ if (statusCode === 410) return ErrorCode.OFFER_EXPIRED;
101
+ if (statusCode === 422) {
102
+ if (d.includes("iata") || d.includes("airport")) return ErrorCode.INVALID_IATA;
103
+ if (d.includes("date")) return ErrorCode.INVALID_DATE;
104
+ if (d.includes("passenger")) return ErrorCode.INVALID_PASSENGERS;
105
+ if (d.includes("route")) return ErrorCode.UNSUPPORTED_ROUTE;
106
+ return ErrorCode.INVALID_PARAMETER;
107
+ }
108
+ if (statusCode === 429) return ErrorCode.RATE_LIMITED;
109
+ if (statusCode === 503) return ErrorCode.SERVICE_UNAVAILABLE;
110
+ if (statusCode === 504) return ErrorCode.SUPPLIER_TIMEOUT;
111
+ if (statusCode === 409) return ErrorCode.ALREADY_BOOKED;
112
+ return statusCode >= 500 ? ErrorCode.BOOKING_FAILED : ErrorCode.INVALID_PARAMETER;
113
+ }
44
114
  var BoostedTravelError = class extends Error {
45
115
  statusCode;
46
116
  response;
47
- constructor(message, statusCode = 0, response = {}) {
117
+ errorCode;
118
+ errorCategory;
119
+ isRetryable;
120
+ constructor(message, statusCode = 0, response = {}, errorCode = "") {
48
121
  super(message);
49
122
  this.name = "BoostedTravelError";
50
123
  this.statusCode = statusCode;
51
124
  this.response = response;
125
+ this.errorCode = errorCode || response.error_code || "";
126
+ this.errorCategory = CODE_TO_CATEGORY[this.errorCode] || ErrorCategory.BUSINESS;
127
+ this.isRetryable = this.errorCategory === ErrorCategory.TRANSIENT;
52
128
  }
53
129
  };
54
130
  var AuthenticationError = class extends BoostedTravelError {
55
131
  constructor(message, response = {}) {
56
- super(message, 401, response);
132
+ super(message, 401, response, ErrorCode.AUTH_INVALID);
57
133
  this.name = "AuthenticationError";
58
134
  }
59
135
  };
60
136
  var PaymentRequiredError = class extends BoostedTravelError {
61
137
  constructor(message, response = {}) {
62
- super(message, 402, response);
138
+ const code = message.toLowerCase().includes("declined") ? ErrorCode.PAYMENT_DECLINED : ErrorCode.PAYMENT_REQUIRED;
139
+ super(message, 402, response, code);
63
140
  this.name = "PaymentRequiredError";
64
141
  }
65
142
  };
143
+ var OfferExpiredError = class extends BoostedTravelError {
144
+ constructor(message, response = {}) {
145
+ super(message, 410, response, ErrorCode.OFFER_EXPIRED);
146
+ this.name = "OfferExpiredError";
147
+ }
148
+ };
149
+ var ValidationError = class extends BoostedTravelError {
150
+ constructor(message, statusCode = 422, response = {}, errorCode = "") {
151
+ super(message, statusCode, response, errorCode || ErrorCode.INVALID_PARAMETER);
152
+ this.name = "ValidationError";
153
+ }
154
+ };
66
155
  function routeStr(route) {
67
156
  if (!route.segments.length) return "";
68
157
  const codes = [route.segments[0].origin, ...route.segments.map((s) => s.destination)];
@@ -196,16 +285,20 @@ var BoostedTravel = class {
196
285
  /**
197
286
  * Book a flight — FREE after unlock.
198
287
  * Creates a real airline reservation with PNR.
288
+ *
289
+ * Always provide idempotencyKey to prevent double-bookings on retry.
199
290
  */
200
- async book(offerId, passengers, contactEmail, contactPhone = "") {
291
+ async book(offerId, passengers, contactEmail, contactPhone = "", idempotencyKey = "") {
201
292
  this.requireApiKey();
202
- return this.post("/api/v1/bookings/book", {
293
+ const body = {
203
294
  offer_id: offerId,
204
295
  booking_type: "flight",
205
296
  passengers,
206
297
  contact_email: contactEmail,
207
298
  contact_phone: contactPhone
208
- });
299
+ };
300
+ if (idempotencyKey) body.idempotency_key = idempotencyKey;
301
+ return this.post("/api/v1/bookings/book", body);
209
302
  }
210
303
  /**
211
304
  * Set up payment method (payment token).
@@ -274,9 +367,12 @@ var BoostedTravel = class {
274
367
  const data = await resp.json();
275
368
  if (!resp.ok) {
276
369
  const detail = data.detail || `API error (${resp.status})`;
370
+ const code = data.error_code || inferErrorCode(resp.status, detail);
277
371
  if (resp.status === 401) throw new AuthenticationError(detail, data);
278
372
  if (resp.status === 402) throw new PaymentRequiredError(detail, data);
279
- throw new BoostedTravelError(detail, resp.status, data);
373
+ if (resp.status === 410) throw new OfferExpiredError(detail, data);
374
+ if (resp.status === 422) throw new ValidationError(detail, resp.status, data, code);
375
+ throw new BoostedTravelError(detail, resp.status, data, code);
280
376
  }
281
377
  return data;
282
378
  } finally {
@@ -290,7 +386,11 @@ var index_default = BoostedTravel;
290
386
  AuthenticationError,
291
387
  BoostedTravel,
292
388
  BoostedTravelError,
389
+ ErrorCategory,
390
+ ErrorCode,
391
+ OfferExpiredError,
293
392
  PaymentRequiredError,
393
+ ValidationError,
294
394
  cheapestOffer,
295
395
  localSearch,
296
396
  offerSummary,
package/dist/index.mjs CHANGED
@@ -2,17 +2,25 @@ import {
2
2
  AuthenticationError,
3
3
  BoostedTravel,
4
4
  BoostedTravelError,
5
+ ErrorCategory,
6
+ ErrorCode,
7
+ OfferExpiredError,
5
8
  PaymentRequiredError,
9
+ ValidationError,
6
10
  cheapestOffer,
7
11
  index_default,
8
12
  offerSummary,
9
13
  searchLocal
10
- } from "./chunk-AVADUS2V.mjs";
14
+ } from "./chunk-RJB7OAS2.mjs";
11
15
  export {
12
16
  AuthenticationError,
13
17
  BoostedTravel,
14
18
  BoostedTravelError,
19
+ ErrorCategory,
20
+ ErrorCode,
21
+ OfferExpiredError,
15
22
  PaymentRequiredError,
23
+ ValidationError,
16
24
  cheapestOffer,
17
25
  index_default as default,
18
26
  searchLocal as localSearch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boostedtravel",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Agent-native flight search & booking. 48 LCC scrapers run locally + GDS/NDC APIs. Built for autonomous AI agents.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",