lightspeed-retail-sdk 2.0.6 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,43 +1,29 @@
1
1
  # Another Unofficial Lightspeed Retail V3 API SDK
2
2
 
3
- ## Work In Progress
3
+ A JavaScript SDK for interacting with the Lightspeed Retail API. This SDK provides a convenient way to access Lightspeed Retail's functionalities, including customer, item, order management, and more.
4
4
 
5
- Constributions Welcome
5
+ ## Update
6
6
 
7
- All methods accept relations, these should be provided as strings i.e.: `["relation1", "relation2"]`
8
- We currently have the following methods available,
7
+ I have updated this package so it can be used with either index.cjs or index.mjs for imports.
9
8
 
10
- ```
11
- getCustomer(id: int, relations?: string)
12
- getCustomers(relations?: string)
13
- getItem(id: int, relations?: string)
14
- getItems(relations?: string)
15
- getMultipleItems(ids: string = "[102, 103]", relations?: string)
16
- getVendorItems(id: int relations?: string)
17
- getMatrixItems(relations? string)
18
- getMatrixItem(id: int, relations? string)
19
- getCategory(id: int, relations?: string)
20
- getCategories(relations?: string)
21
- getManufacturer(id: int, relations?: string)
22
- getManufacturers(relations?: string)
23
- getOrder(id: int, relations?: string)
24
- getOrders(relations?: string)
25
- getOrdersByVendorID(id: int, relations?: string)
26
- getOpenOrdersByVendorID(id: int, relations?: string)
27
- getVendor(id: int, relations?: string)
28
- getVendors(relations?: string)
29
- getSale(id: int, relations?: string)
30
- getSales(relations?: string)
31
- getMultipleSales(ids: string = "[102, 103]", relations?: string)
32
- getSaleLinesByItem(id: int, relations?: string)
33
- getSaleLinesByItems(ids: string = `39, 2126, 3505`, startDate?, endDate?, relations?: string)
34
- getSaleLinesByVendorID(id: int, startDate?, endDate?, relations?: string)
9
+ ## Features
10
+
11
+ - Easy-to-use methods for interacting with various Lightspeed Retail endpoints.
12
+ - Built-in handling of API rate limits.
13
+ - Automatic token management for authentication.
14
+ - Support for paginated responses from the Lightspeed API.
15
+ - Retry logic for handling transient network issues.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install lightspeed-retail-sdk
35
21
  ```
36
22
 
37
23
  ## Get started:
38
24
 
39
25
  ```
40
- import LightspeedRetailSDK from "lightspeed-retail-sdk";
26
+ import LightspeedRetailSDK from "lightspeed-retail-sdk/index.mjs";
41
27
 
42
28
  const api = new LightspeedRetailSDK({
43
29
  accountID: "Your Account No.",
@@ -58,6 +44,45 @@ console.log(item);
58
44
  7497 being the itemID. You can pass required relations as above.
59
45
  ```
60
46
 
47
+ ## Methods
48
+
49
+ - `getCustomer(id, relations)`: Fetches a specific customer by ID. Optionally, related data can be included.
50
+ - `getCustomers(relations)`: Retrieves all customers. Optionally, related data can be included.
51
+ - `getItem(id, relations)`: Fetches a specific item by ID. Optionally, related data can be included.
52
+ - `getMultipleItems(items, relations)`: Retrieves multiple items by their IDs. Optionally, related data can be included.
53
+ - `getItems(relations)`: Retrieves all items. Optionally, related data can be included.
54
+ - `getvendorItems(vendorID, relations)`: Retrieves all items for a specific vendor. Optionally, related data can be included.
55
+ - `getMatrixItems(relations)`: Fetches all matrix items. Optionally, related data can be included.
56
+ - `getMatrixItem(id, relations)`: Fetches a specific matrix item by ID. Optionally, related data can be included.
57
+ - `getCategory(id, relations)`: Retrieves a specific category by ID. Optionally, related data can be included.
58
+ - `getCategories(relations)`: Retrieves all categories. Optionally, related data can be included.
59
+ - `getManufacturer(id, relations)`: Fetches a specific manufacturer by ID. Optionally, related data can be included.
60
+ - `getManufacturers(relations)`: Retrieves all manufacturers. Optionally, related data can be included.
61
+ - `getOrder(id, relations)`: Fetches a specific order by ID. Optionally, related data can be included.
62
+ - `getOrders(relations)`: Retrieves all orders. Optionally, related data can be included.
63
+ - `getOrdersByVendorID(id, relations)`: Retrieves all orders for a specific vendor. Optionally, related data can be included.
64
+ - `getOpenOrdersByVendorID(id, relations)`: Fetches all open orders for a specific vendor. Optionally, related data can be included.
65
+ - `getVendor(id, relations)`: Fetches a specific vendor by ID. Optionally, related data can be included.
66
+ - `getVendors(relations)`: Retrieves all vendors. Optionally, related data can be included.
67
+ - `getSale(id, relations)`: Fetches a specific sale by ID. Optionally, related data can be included.
68
+ - `getSales(relations)`: Retrieves all sales. Optionally, related data can be included.
69
+ - `getMultipleSales(saleIDs, relations)`: Fetches multiple sales by their IDs. Optionally, related data can be included.
70
+ - `getSaleLinesByItem(itemID, relations)`: Retrieves sale lines for a specific item. Optionally, related data can be included.
71
+ - `getSaleLinesByItems(ids, startDate, endDate, relations)`: Retrieves sale lines for multiple items, filtered by date range. Optionally, related data can be included.
72
+ - `getSaleLinesByVendorID(id, startDate, endDate, relations)`: Fetches sale lines for a specific vendor, filtered by date range. Optionally, related data can be included.
73
+
74
+ ## Contributing
75
+
76
+ Contributions are welcome!
77
+
78
+ ## License
79
+
80
+ This project is licensed under the MIT License - see the LICENSE file for details.
81
+
82
+ ## Disclaimer
83
+
84
+ This SDK is not officially affiliated with Lightspeed HQ and is provided "as is" with no warranty.
85
+
61
86
  ## More Info
62
87
 
63
88
  The documentation for the Lightspeed Retail API can be found at https://developers.lightspeedhq.com/retail/introduction/introduction/
package/index.cjs ADDED
@@ -0,0 +1,646 @@
1
+ const axios = require("axios");
2
+
3
+ const operationUnits = { GET: 1, POST: 10, PUT: 10 };
4
+ const getRequestUnits = (operation) => operationUnits[operation] || 10;
5
+
6
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
+
8
+ class LightspeedRetailSDK {
9
+ static BASE_URL = "https://api.lightspeedapp.com/API/V3/Account";
10
+ static TOKEN_URL = "https://cloud.lightspeedapp.com/oauth/access_token.php";
11
+
12
+ constructor(opts) {
13
+ const { clientID, clientSecret, refreshToken, accountID } = opts;
14
+
15
+ this.clientID = clientID;
16
+ this.clientSecret = clientSecret;
17
+ this.refreshToken = refreshToken;
18
+ this.accountID = accountID;
19
+ this.baseUrl = LightspeedRetailSDK.BASE_URL;
20
+ this.tokenUrl = LightspeedRetailSDK.TOKEN_URL;
21
+ this.maxRetries = 3;
22
+ this.lastResponse = null;
23
+ this.token = null;
24
+ this.tokenExpiry = null;
25
+ }
26
+
27
+ // handleError function to handle errors
28
+ handleError(context, err, shouldThrow = true) {
29
+ // Context includes information about where the error occurred
30
+ const detailedMessage = `Error in ${context}: ${err.message}`;
31
+
32
+ // Log the error message
33
+ console.error(detailedMessage);
34
+
35
+ // Log the stack trace if available
36
+ if (err.stack) {
37
+ console.error("Stack trace:", err.stack);
38
+ }
39
+
40
+ // If the error has response data, log it
41
+ if (err.response) {
42
+ console.error("Error response:", {
43
+ status: err.response.status,
44
+ headers: err.response.headers,
45
+ data: err.response.data,
46
+ });
47
+ }
48
+
49
+ // Optionally rethrow the error with the detailed message
50
+ if (shouldThrow) {
51
+ throw new Error(detailedMessage);
52
+ }
53
+ }
54
+
55
+ // Update the last response
56
+ setLastResponse = (response) => (this.lastResponse = response);
57
+
58
+ // Handle rate limits
59
+ handleRateLimit = async (options) => {
60
+ if (!this.lastResponse) return null;
61
+
62
+ const { method } = options;
63
+ const requestUnits = getRequestUnits(method);
64
+ const rateHeader = this.lastResponse.headers["x-ls-api-bucket-level"];
65
+
66
+ if (!rateHeader) return null;
67
+
68
+ const [used, available] = rateHeader.split("/");
69
+ const availableUnits = available - used;
70
+ if (requestUnits <= availableUnits) return 0;
71
+
72
+ const dripRate = parseInt(this.lastResponse.headers["x-ls-api-drip-rate"], 10);
73
+
74
+ // Check if dripRate is a valid number greater than 0
75
+ if (isNaN(dripRate) || dripRate <= 0) {
76
+ this.handleError.error("Invalid drip rate received from API");
77
+ }
78
+
79
+ const unitWait = requestUnits - availableUnits;
80
+ const delay = Math.ceil((unitWait / dripRate) * 1000);
81
+ await sleep(delay);
82
+
83
+ return unitWait;
84
+ };
85
+
86
+ // Get a new token
87
+ getToken = async () => {
88
+ const now = new Date();
89
+ const bufferTime = 1 * 60 * 1000; // 1 minute buffer
90
+
91
+ // Check if the token exists and is still valid
92
+ if (this.token && this.tokenExpiry.getTime() - now.getTime() > bufferTime) {
93
+ return this.token;
94
+ }
95
+
96
+ // Fetch a new token if needed
97
+ const body = {
98
+ grant_type: "refresh_token",
99
+ client_id: this.clientID,
100
+ client_secret: this.clientSecret,
101
+ refresh_token: this.refreshToken,
102
+ };
103
+
104
+ try {
105
+ const response = await axios({
106
+ url: this.tokenUrl,
107
+ method: "post",
108
+ headers: {
109
+ "Content-Type": "application/json",
110
+ },
111
+ data: JSON.stringify(body),
112
+ });
113
+
114
+ const tokenData = await response.data;
115
+
116
+ // Set token and expiry time
117
+ this.token = tokenData.access_token;
118
+ this.tokenExpiry = new Date(now.getTime() + tokenData.expires_in * 1000);
119
+
120
+ return this.token;
121
+ } catch (error) {
122
+ return this.handleError("GET TOKEN ERROR", error);
123
+ }
124
+ };
125
+
126
+ // Fetch a resource
127
+ executeApiRequest = async (options, retries = 0) => {
128
+ await this.handleRateLimit(options);
129
+
130
+ const token = await this.getToken();
131
+ if (!token) throw new Error("Error Fetching Token");
132
+
133
+ // Set common headers
134
+ options.headers = {
135
+ Authorization: `Bearer ${token}`,
136
+ "Content-Type": "application/json",
137
+ ...options.headers, // Merge with any additional headers passed in options
138
+ };
139
+
140
+ try {
141
+ const res = await axios(options);
142
+ this.lastResponse = res;
143
+
144
+ // Return the data
145
+ if (options.method === "GET") {
146
+ return {
147
+ data: res.data,
148
+ next: res.next,
149
+ previous: res.previous,
150
+ };
151
+ } else {
152
+ return res.data; // For POST and PUT, typically just return the data
153
+ }
154
+ } catch (err) {
155
+ if (this.isRetryableError(err) && retries < this.maxRetries) {
156
+ this.handleError(`Network Error Retrying in 2 seconds...`, err.message, false);
157
+ await sleep(2000);
158
+ return this.executeApiRequest(options, retries + 1);
159
+ } else {
160
+ this.handleError(`Failed Request statusText: ${err.response?.statusText}`);
161
+ this.handleError(`Failed data: ${err.response?.data}`);
162
+ throw err;
163
+ }
164
+ }
165
+ };
166
+
167
+ // Get paginated data
168
+ async getAllData(options) {
169
+ let allData = [];
170
+ while (options.url) {
171
+ const { data } = await this.executeApiRequest(options);
172
+ let next = data["@attributes"].next;
173
+ let selectDataArray = Object.keys(data)[1];
174
+ let selectedData = data[selectDataArray];
175
+ allData = allData.concat(selectedData);
176
+ options.url = next;
177
+ }
178
+ return allData;
179
+ }
180
+
181
+ // Check if error is retryable
182
+ isRetryableError = (err) => {
183
+ if (!err.response) {
184
+ // No response (network error or timeout)
185
+ return true;
186
+ }
187
+
188
+ // Retry for server errors (500-599)
189
+ return err.response.status >= 500 && err.response.status <= 599;
190
+ };
191
+
192
+ // Get customer by ID
193
+ async getCustomer(id, relations) {
194
+ const options = {
195
+ url: `${this.baseUrl}/${this.accountID}/Customer/${id}.json`,
196
+ method: "GET",
197
+ };
198
+
199
+ if (!id) return this.handleError("You need to provide a customerID");
200
+
201
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
202
+
203
+ try {
204
+ const response = await this.getAllData(options);
205
+ return response;
206
+ } catch (error) {
207
+ return this.handleError("GET CUSTOMERS ERROR", error);
208
+ }
209
+ }
210
+
211
+ // Get all customers
212
+ async getCustomers(relations) {
213
+ const options = {
214
+ url: `${this.baseUrl}/${this.accountID}/Customer.json`,
215
+ method: "GET",
216
+ };
217
+
218
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
219
+
220
+ try {
221
+ const response = await this.getAllData(options);
222
+ return response;
223
+ } catch (error) {
224
+ return this.handleError("GET CUSTOMERS ERROR", error);
225
+ }
226
+ }
227
+
228
+ // Get item by ID
229
+ async getItem(id, relations) {
230
+ const options = {
231
+ url: `${this.baseUrl}/${this.accountID}/Item/${id}.json`,
232
+ method: "GET",
233
+ };
234
+
235
+ if (!id) return this.handleError("You need to provide a itemID");
236
+
237
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
238
+
239
+ try {
240
+ const response = await this.getAllData(options);
241
+ return response;
242
+ } catch (error) {
243
+ return this.handleError("GET ITEM ERROR", error);
244
+ }
245
+ }
246
+
247
+ // Get multiple items by ID
248
+ async getMultipleItems(items, relations) {
249
+ const options = {
250
+ url: `${this.baseUrl}/${this.accountID}/Item.json`,
251
+ method: "GET",
252
+ };
253
+
254
+ if (!items) this.handleError("You need to provide itemID's");
255
+
256
+ if (items) options.url = options.url + `?itemID=IN,${items}`;
257
+
258
+ if (relations) options.url = options.url + `&load_relations=${relations}`;
259
+
260
+ try {
261
+ const response = await this.getAllData(options);
262
+ return response;
263
+ } catch (error) {
264
+ return this.handleError("GET ITEMS ERROR", error);
265
+ }
266
+ }
267
+
268
+ // Get all items
269
+ async getItems(relations) {
270
+ const options = {
271
+ url: `${this.baseUrl}/${this.accountID}/Item.json`,
272
+ method: "GET",
273
+ };
274
+
275
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
276
+
277
+ try {
278
+ const response = await this.getAllData(options);
279
+ return response;
280
+ } catch (error) {
281
+ return this.handleError("GET ITEMS ERROR", error);
282
+ }
283
+ }
284
+
285
+ // Get all items by vendor
286
+ async getvendorItems(vendorID, relations) {
287
+ const options = {
288
+ url: `${this.baseUrl}/${this.accountID}/Item.json?defaultVendorID=${vendorID}`,
289
+ method: "GET",
290
+ };
291
+
292
+ if (relations) options.url = options.url + `&load_relations=${relations}`;
293
+
294
+ try {
295
+ const response = await this.getAllData(options);
296
+ return response;
297
+ } catch (error) {
298
+ return this.handleError("GET ITEMS ERROR", error);
299
+ }
300
+ }
301
+
302
+ // Get all Matrix Items
303
+ async getMatrixItems(relations) {
304
+ const options = {
305
+ url: `${this.baseUrl}/${this.accountID}/ItemMatrix.json`,
306
+ method: "GET",
307
+ };
308
+
309
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
310
+
311
+ try {
312
+ const response = await this.getAllData(options);
313
+ return response;
314
+ } catch (error) {
315
+ return this.handleError("GET ITEM ERROR", error);
316
+ }
317
+ }
318
+
319
+ // Get Matrix Item by ID
320
+ async getMatrixItem(id, relations) {
321
+ const options = {
322
+ url: `${this.baseUrl}/${this.accountID}/ItemMatrix/${id}.json`,
323
+ method: "GET",
324
+ };
325
+
326
+ if (!id) return this.handleError("You need to provide a itemID");
327
+
328
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
329
+
330
+ try {
331
+ const response = await this.getAllData(options);
332
+ return response;
333
+ } catch (error) {
334
+ return this.handleError("GET ITEM ERROR", error);
335
+ }
336
+ }
337
+
338
+ // Get category by ID
339
+ async getCategory(id, relations) {
340
+ const options = {
341
+ url: `${this.baseUrl}/${this.accountID}/Category/${id}.json`,
342
+ method: "GET",
343
+ };
344
+
345
+ if (!id) return this.handleError("You need to provide a categoryID");
346
+
347
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
348
+
349
+ try {
350
+ const response = await this.getAllData(options);
351
+ return response;
352
+ } catch (error) {
353
+ return this.handleError("GET CATEGORY ERROR", error);
354
+ }
355
+ }
356
+
357
+ // Get all categories
358
+ async getCategories(relations) {
359
+ const options = {
360
+ url: `${this.baseUrl}/${this.accountID}/Category.json`,
361
+ method: "GET",
362
+ };
363
+
364
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
365
+
366
+ try {
367
+ const response = await this.getAllData(options);
368
+ return response;
369
+ } catch (error) {
370
+ return this.handleError("GET CATEGORIES ERROR", error);
371
+ }
372
+ }
373
+
374
+ // Get Manufacturer by ID
375
+ async getManufacturer(id, relations) {
376
+ const options = {
377
+ url: `${this.baseUrl}/${this.accountID}/Manufacturer/${id}.json`,
378
+ method: "GET",
379
+ };
380
+
381
+ if (!id) return this.handleError("You need to provide a manufacturerID");
382
+
383
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
384
+
385
+ try {
386
+ const response = await this.getAllData(options);
387
+ return response;
388
+ } catch (error) {
389
+ return this.handleError("GET MANUFACTURER ERROR", error);
390
+ }
391
+ }
392
+
393
+ // Get all manufacturers
394
+ async getManufacturers(relations) {
395
+ const options = {
396
+ url: `${this.baseUrl}/${this.accountID}/Manufacturer.json`,
397
+ method: "GET",
398
+ };
399
+
400
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
401
+
402
+ try {
403
+ const response = await this.getAllData(options);
404
+ return response;
405
+ } catch (error) {
406
+ return this.handleError("GET MANUFACTURERS ERROR", error);
407
+ }
408
+ }
409
+
410
+ // Get order by ID
411
+ async getOrder(id, relations) {
412
+ const options = {
413
+ url: `${this.baseUrl}/${this.accountID}/Order/${id}.json`,
414
+ method: "GET",
415
+ };
416
+
417
+ if (!id) return this.handleError("You need to provide a orderID");
418
+
419
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
420
+
421
+ try {
422
+ const response = await this.getAllData(options);
423
+ return response;
424
+ } catch (error) {
425
+ return this.handleError("GET ORDER ERROR", error);
426
+ }
427
+ }
428
+
429
+ // Get all orders
430
+ async getOrders(relations) {
431
+ const options = {
432
+ url: `${this.baseUrl}/${this.accountID}/Order.json`,
433
+ method: "GET",
434
+ };
435
+
436
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
437
+
438
+ try {
439
+ const response = await this.getAllData(options);
440
+ return response;
441
+ } catch (error) {
442
+ return this.handleError("GET ORDERS ERROR", error);
443
+ }
444
+ }
445
+
446
+ // Get all orders by vendor
447
+ async getOrdersByVendorID(id, relations) {
448
+ const options = {
449
+ url: `${this.baseUrl}/${this.accountID}/Order.json?load_relations=["Vendor"]&vendorID=${id}`,
450
+ method: "GET",
451
+ };
452
+
453
+ if (!id) return this.handleError("You need to provide a vendorID");
454
+
455
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
456
+
457
+ try {
458
+ const response = await this.getAllData(options);
459
+ return response;
460
+ } catch (error) {
461
+ return this.handleError("GET ORDER ERROR", error);
462
+ }
463
+ }
464
+
465
+ // Get all open orders by vendor
466
+ async getOpenOrdersByVendorID(id, relations) {
467
+ const options = {
468
+ url: `${this.baseUrl}/${this.accountID}/Order.json?load_relations=["Vendor", "OrderLines"]&vendorID=${id}&complete=false`,
469
+ method: "GET",
470
+ };
471
+
472
+ if (!id) return this.handleError("You need to provide a vendorID");
473
+
474
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
475
+
476
+ try {
477
+ const response = await this.getAllData(options);
478
+ return response;
479
+ } catch (error) {
480
+ return this.handleError("GET ORDER ERROR", error);
481
+ }
482
+ }
483
+
484
+ // Get vendor by ID
485
+ async getVendor(id, relations) {
486
+ const options = {
487
+ url: `${this.baseUrl}/${this.accountID}/Vendor/${id}.json`,
488
+ method: "GET",
489
+ };
490
+
491
+ if (!id) return this.handleError("You need to provide a vendorID");
492
+
493
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
494
+
495
+ try {
496
+ const response = await this.getAllData(options);
497
+ return response;
498
+ } catch (error) {
499
+ return this.handleError("GET VENDOR ERROR", error);
500
+ }
501
+ }
502
+
503
+ // Get all vendors
504
+ async getVendors(relations) {
505
+ const options = {
506
+ url: `${this.baseUrl}/${this.accountID}/Vendor.json`,
507
+ method: "GET",
508
+ };
509
+
510
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
511
+
512
+ try {
513
+ const response = await this.getAllData(options);
514
+ return response;
515
+ } catch (error) {
516
+ return this.handleError("GET VENDORS ERROR", error);
517
+ }
518
+ }
519
+
520
+ // Get sale by ID
521
+ async getSale(id, relations) {
522
+ const options = {
523
+ url: `${this.baseUrl}/${this.accountID}/Sale/${id}.json`,
524
+ method: "GET",
525
+ };
526
+
527
+ if (!id) return this.handleError("You need to provide a saleID");
528
+
529
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
530
+
531
+ try {
532
+ const response = await this.getAllData(options);
533
+ return response;
534
+ } catch (error) {
535
+ return this.handleError("GET SALE ERROR", error);
536
+ }
537
+ }
538
+
539
+ // Get all sales
540
+ async getSales(relations) {
541
+ const options = {
542
+ url: `${this.baseUrl}/${this.accountID}/Sale.json`,
543
+ method: "GET",
544
+ };
545
+
546
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
547
+
548
+ try {
549
+ const response = await this.getAllData(options);
550
+ return response;
551
+ } catch (error) {
552
+ return this.handleError("GET SALES ERROR", error);
553
+ }
554
+ }
555
+
556
+ async getMultipleSales(saleIDs, relations) {
557
+ const options = {
558
+ url: `${this.baseUrl}/${this.accountID}/Sale.json?saleID=IN,${saleIDs}`,
559
+ method: "GET",
560
+ };
561
+
562
+ if (relations) options.url = options.url + `?load_relations=${relations}`;
563
+
564
+ try {
565
+ const response = await this.getAllData(options);
566
+ return response;
567
+ } catch (error) {
568
+ return this.handleError("GET SALE ERROR", error);
569
+ }
570
+ }
571
+
572
+ async getSaleLinesByItem(itemID, relations) {
573
+ const options = {
574
+ url: `${this.baseUrl}/${this.accountID}/SaleLine.json?itemID=${itemID}`,
575
+ method: "GET",
576
+ };
577
+
578
+ if (relations) options.url = options.url + `&load_relations=${relations}`;
579
+
580
+ try {
581
+ const response = await this.getAllData(options);
582
+ return response;
583
+ } catch (error) {
584
+ return this.handleError("GET SALE ERROR", error);
585
+ }
586
+ }
587
+
588
+ // Get sales lines by item ID's and date range
589
+ async getSaleLinesByItems(ids, startDate = undefined, endDate = undefined, relations) {
590
+ const options = {
591
+ url: `${this.baseUrl}/${this.accountID}/SaleLine.json?itemID=IN,[${ids}]`,
592
+ method: "GET",
593
+ };
594
+
595
+ if (!ids) return this.handleError("You need to provide itemIDs");
596
+ if (startDate && !endDate) return this.handleError("You need to provide an end date");
597
+ if (endDate && !startDate)
598
+ return this.handleError("You need to provide a start date");
599
+
600
+ if (relations) options.url = options.url + `&load_relations=${relations}`;
601
+
602
+ if (startDate && endDate)
603
+ options.url =
604
+ options.url + `&timeStamp=%3E%3C%2C${startDate}%2C${endDate}&sort=timeStamp`;
605
+
606
+ try {
607
+ const response = await this.getAllData(options);
608
+ return response;
609
+ } catch (error) {
610
+ return this.handleError("GET SALE ERROR", error);
611
+ }
612
+ }
613
+
614
+ // Get sales lines by vendor ID's and date range
615
+ async getSaleLinesByVendorID(
616
+ id,
617
+ startDate = undefined,
618
+ endDate = undefined,
619
+ relations
620
+ ) {
621
+ const options = {
622
+ url: `${this.baseUrl}/${this.accountID}/SaleLine.json?load_relations=${
623
+ relations ? relations : `["Item"]`
624
+ }&Item.defaultVendorID=${id}`,
625
+ method: "GET",
626
+ };
627
+
628
+ if (!id) return this.handleError("You need to provide a vendorID");
629
+ if (startDate && !endDate) return this.handleError("You need to provide an end date");
630
+ if (endDate && !startDate)
631
+ return this.handleError("You need to provide a start date");
632
+
633
+ if (startDate && endDate)
634
+ options.url =
635
+ options.url + `&timeStamp=%3E%3C%2C${startDate}%2C${endDate}&sort=timeStamp`;
636
+
637
+ try {
638
+ const response = await this.getAllData(options);
639
+ return response;
640
+ } catch (error) {
641
+ return this.handleError("GET SALE ERROR", error);
642
+ }
643
+ }
644
+ }
645
+
646
+ module.exports = LightspeedRetailSDK;
@@ -1,22 +1,14 @@
1
1
  import axios from "axios";
2
2
 
3
- // Cost per operation
4
- const getRequestUnits = (operation) => {
5
- switch (operation) {
6
- case "GET":
7
- return 1;
8
- case "POST":
9
- return 10;
10
- case "PUT":
11
- return 10;
12
- default:
13
- return 10;
14
- }
15
- };
3
+ const operationUnits = { GET: 1, POST: 10, PUT: 10 };
4
+ const getRequestUnits = (operation) => operationUnits[operation] || 10;
16
5
 
17
6
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
18
7
 
19
8
  class LightspeedRetailSDK {
9
+ static BASE_URL = "https://api.lightspeedapp.com/API/V3/Account";
10
+ static TOKEN_URL = "https://cloud.lightspeedapp.com/oauth/access_token.php";
11
+
20
12
  constructor(opts) {
21
13
  const { clientID, clientSecret, refreshToken, accountID } = opts;
22
14
 
@@ -24,23 +16,50 @@ class LightspeedRetailSDK {
24
16
  this.clientSecret = clientSecret;
25
17
  this.refreshToken = refreshToken;
26
18
  this.accountID = accountID;
27
- this.baseUrl = "https://api.lightspeedapp.com/API/V3/Account";
19
+ this.baseUrl = LightspeedRetailSDK.BASE_URL;
20
+ this.tokenUrl = LightspeedRetailSDK.TOKEN_URL;
28
21
  this.maxRetries = 3;
29
22
  this.lastResponse = null;
23
+ this.token = null;
24
+ this.tokenExpiry = null;
30
25
  }
31
26
 
32
- handleError(msg, err) {
33
- console.error(`${msg} - ${err}`);
34
- throw err;
27
+ // handleError function to handle errors
28
+ handleError(context, err, shouldThrow = true) {
29
+ // Context includes information about where the error occurred
30
+ const detailedMessage = `Error in ${context}: ${err.message}`;
31
+
32
+ // Log the error message
33
+ console.error(detailedMessage);
34
+
35
+ // Log the stack trace if available
36
+ if (err.stack) {
37
+ console.error("Stack trace:", err.stack);
38
+ }
39
+
40
+ // If the error has response data, log it
41
+ if (err.response) {
42
+ console.error("Error response:", {
43
+ status: err.response.status,
44
+ headers: err.response.headers,
45
+ data: err.response.data,
46
+ });
47
+ }
48
+
49
+ // Optionally rethrow the error with the detailed message
50
+ if (shouldThrow) {
51
+ throw new Error(detailedMessage);
52
+ }
35
53
  }
36
54
 
55
+ // Update the last response
37
56
  setLastResponse = (response) => (this.lastResponse = response);
38
57
 
58
+ // Handle rate limits
39
59
  handleRateLimit = async (options) => {
40
60
  if (!this.lastResponse) return null;
41
61
 
42
62
  const { method } = options;
43
-
44
63
  const requestUnits = getRequestUnits(method);
45
64
  const rateHeader = this.lastResponse.headers["x-ls-api-bucket-level"];
46
65
 
@@ -50,7 +69,13 @@ class LightspeedRetailSDK {
50
69
  const availableUnits = available - used;
51
70
  if (requestUnits <= availableUnits) return 0;
52
71
 
53
- const dripRate = this.lastResponse.headers["x-ls-api-drip-rate"];
72
+ const dripRate = parseInt(this.lastResponse.headers["x-ls-api-drip-rate"], 10);
73
+
74
+ // Check if dripRate is a valid number greater than 0
75
+ if (isNaN(dripRate) || dripRate <= 0) {
76
+ this.handleError.error("Invalid drip rate received from API");
77
+ }
78
+
54
79
  const unitWait = requestUnits - availableUnits;
55
80
  const delay = Math.ceil((unitWait / dripRate) * 1000);
56
81
  await sleep(delay);
@@ -58,7 +83,17 @@ class LightspeedRetailSDK {
58
83
  return unitWait;
59
84
  };
60
85
 
86
+ // Get a new token
61
87
  getToken = async () => {
88
+ const now = new Date();
89
+ const bufferTime = 1 * 60 * 1000; // 1 minute buffer
90
+
91
+ // Check if the token exists and is still valid
92
+ if (this.token && this.tokenExpiry.getTime() - now.getTime() > bufferTime) {
93
+ return this.token;
94
+ }
95
+
96
+ // Fetch a new token if needed
62
97
  const body = {
63
98
  grant_type: "refresh_token",
64
99
  client_id: this.clientID,
@@ -66,68 +101,94 @@ class LightspeedRetailSDK {
66
101
  refresh_token: this.refreshToken,
67
102
  };
68
103
 
69
- const response = await axios({
70
- url: "https://cloud.lightspeedapp.com/oauth/access_token.php",
71
- method: "post",
72
- headers: {
73
- "Content-Type": "application/json",
74
- },
75
- data: JSON.stringify(body),
76
- }).catch((error) => console.error(error.data));
77
-
78
- const tokenData = await response.data;
79
- const token = tokenData.access_token;
80
-
81
- return token;
104
+ try {
105
+ const response = await axios({
106
+ url: this.tokenUrl,
107
+ method: "post",
108
+ headers: {
109
+ "Content-Type": "application/json",
110
+ },
111
+ data: JSON.stringify(body),
112
+ });
113
+
114
+ const tokenData = await response.data;
115
+
116
+ // Set token and expiry time
117
+ this.token = tokenData.access_token;
118
+ this.tokenExpiry = new Date(now.getTime() + tokenData.expires_in * 1000);
119
+
120
+ return this.token;
121
+ } catch (error) {
122
+ return this.handleError("GET TOKEN ERROR", error);
123
+ }
82
124
  };
83
125
 
84
- getResource = async (options, retries = 0) => {
85
- this.handleRateLimit(options);
126
+ // Fetch a resource
127
+ executeApiRequest = async (options, retries = 0) => {
128
+ await this.handleRateLimit(options);
86
129
 
87
130
  const token = await this.getToken();
88
-
89
131
  if (!token) throw new Error("Error Fetching Token");
90
132
 
133
+ // Set common headers
91
134
  options.headers = {
92
135
  Authorization: `Bearer ${token}`,
93
136
  "Content-Type": "application/json",
137
+ ...options.headers, // Merge with any additional headers passed in options
94
138
  };
95
139
 
96
140
  try {
97
141
  const res = await axios(options);
98
142
  this.lastResponse = res;
99
- return {
100
- data: res.data,
101
- next: res.next,
102
- previous: res.previous,
103
- };
143
+
144
+ // Return the data
145
+ if (options.method === "GET") {
146
+ return {
147
+ data: res.data,
148
+ next: res.next,
149
+ previous: res.previous,
150
+ };
151
+ } else {
152
+ return res.data; // For POST and PUT, typically just return the data
153
+ }
104
154
  } catch (err) {
105
- if (retries < this.maxRetries) {
106
- console.log(`Error: ${err}, retrying in 2 seconds...`);
107
- await new Promise((resolve) => setTimeout(resolve, 2000));
108
- return await this.getResource(options.url, retries + 1);
155
+ if (this.isRetryableError(err) && retries < this.maxRetries) {
156
+ this.handleError(`Network Error Retrying in 2 seconds...`, err.message, false);
157
+ await sleep(2000);
158
+ return this.executeApiRequest(options, retries + 1);
109
159
  } else {
110
- console.error(`Failed Request statusText: `, res.statusText);
111
- console.log(`Failed data: `, response.data);
160
+ this.handleError(`Failed Request statusText: ${err.response?.statusText}`);
161
+ this.handleError(`Failed data: ${err.response?.data}`);
112
162
  throw err;
113
163
  }
114
164
  }
115
165
  };
116
166
 
167
+ // Get paginated data
117
168
  async getAllData(options) {
118
169
  let allData = [];
119
170
  while (options.url) {
120
- const { data } = await this.getResource(options);
171
+ const { data } = await this.executeApiRequest(options);
121
172
  let next = data["@attributes"].next;
122
173
  let selectDataArray = Object.keys(data)[1];
123
174
  let selectedData = data[selectDataArray];
124
175
  allData = allData.concat(selectedData);
125
176
  options.url = next;
126
177
  }
127
- // console.log(allData);
128
178
  return allData;
129
179
  }
130
180
 
181
+ // Check if error is retryable
182
+ isRetryableError = (err) => {
183
+ if (!err.response) {
184
+ // No response (network error or timeout)
185
+ return true;
186
+ }
187
+
188
+ // Retry for server errors (500-599)
189
+ return err.response.status >= 500 && err.response.status <= 599;
190
+ };
191
+
131
192
  // Get customer by ID
132
193
  async getCustomer(id, relations) {
133
194
  const options = {
package/package.json CHANGED
@@ -1,22 +1,25 @@
1
1
  {
2
2
  "name": "lightspeed-retail-sdk",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Another unofficial Lightspeed Retail API SDK for Node.js",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
5
+ "type": "module",
6
+ "main": "index.cjs",
7
+ "module": "index.mjs",
8
+ "exports": {
9
+ ".": {
10
+ "require": "./index.cjs",
11
+ "default": "./index.mjs"
12
+ }
8
13
  },
9
- "author": "@morley_darryl",
14
+ "author": "https://github.com/darrylmorley",
10
15
  "license": "ISC",
11
16
  "dependencies": {
12
17
  "axios": "^1.3.4",
13
- "qs": "^6.11.1",
14
- "retry": "^0.13.1"
18
+ "dotenv": "^16.4.4"
15
19
  },
16
- "type": "module",
17
20
  "repository": {
18
21
  "type": "git",
19
- "url": "https://github.com/darrylmorley/lightspeed-retail-sdk"
22
+ "url": "git+https://github.com/darrylmorley/lightspeed-retail-sdk.git"
20
23
  },
21
24
  "keywords": [
22
25
  "lightspeed retail",