@panoptic-it-solutions/quickbooks-client 0.1.3 → 0.2.0

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/dist/index.mjs CHANGED
@@ -105,7 +105,9 @@ function generateAuthUrl(config, state) {
105
105
  }
106
106
  async function exchangeCodeForTokens(config, code, realmId) {
107
107
  const env = config.environment || "production";
108
- const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
108
+ const credentials = Buffer.from(
109
+ `${config.clientId}:${config.clientSecret}`
110
+ ).toString("base64");
109
111
  const response = await fetch(ENDPOINTS[env].token, {
110
112
  method: "POST",
111
113
  headers: {
@@ -139,7 +141,9 @@ async function exchangeCodeForTokens(config, code, realmId) {
139
141
  }
140
142
  async function refreshTokens(config, refreshToken, realmId) {
141
143
  const env = config.environment || "production";
142
- const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
144
+ const credentials = Buffer.from(
145
+ `${config.clientId}:${config.clientSecret}`
146
+ ).toString("base64");
143
147
  const response = await fetch(ENDPOINTS[env].token, {
144
148
  method: "POST",
145
149
  headers: {
@@ -172,7 +176,9 @@ async function refreshTokens(config, refreshToken, realmId) {
172
176
  }
173
177
  async function revokeTokens(config, token) {
174
178
  const env = config.environment || "production";
175
- const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
179
+ const credentials = Buffer.from(
180
+ `${config.clientId}:${config.clientSecret}`
181
+ ).toString("base64");
176
182
  const response = await fetch(ENDPOINTS[env].revoke, {
177
183
  method: "POST",
178
184
  headers: {
@@ -202,7 +208,9 @@ function isTokenExpired(expiresAt, bufferSeconds = 300) {
202
208
  function generateState() {
203
209
  const array = new Uint8Array(16);
204
210
  crypto.getRandomValues(array);
205
- return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
211
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
212
+ ""
213
+ );
206
214
  }
207
215
 
208
216
  // src/client.ts
@@ -219,24 +227,38 @@ var QuickBooksClient = class {
219
227
  tokenStore;
220
228
  requestTimestamps = [];
221
229
  onLog;
230
+ minorVersion;
222
231
  constructor(options) {
223
232
  this.validateConfig(options);
224
233
  this.config = options;
225
234
  this.tokenStore = options.tokenStore;
226
235
  this.onLog = options.onLog;
236
+ this.minorVersion = options.minorVersion;
227
237
  }
228
238
  validateConfig(options) {
229
239
  if (!options.clientId) {
230
- throw new QuickBooksError("clientId is required", QB_ERROR_CODES.INVALID_CONFIG);
240
+ throw new QuickBooksError(
241
+ "clientId is required",
242
+ QB_ERROR_CODES.INVALID_CONFIG
243
+ );
231
244
  }
232
245
  if (!options.clientSecret) {
233
- throw new QuickBooksError("clientSecret is required", QB_ERROR_CODES.INVALID_CONFIG);
246
+ throw new QuickBooksError(
247
+ "clientSecret is required",
248
+ QB_ERROR_CODES.INVALID_CONFIG
249
+ );
234
250
  }
235
251
  if (!options.redirectUri) {
236
- throw new QuickBooksError("redirectUri is required", QB_ERROR_CODES.INVALID_CONFIG);
252
+ throw new QuickBooksError(
253
+ "redirectUri is required",
254
+ QB_ERROR_CODES.INVALID_CONFIG
255
+ );
237
256
  }
238
257
  if (!options.tokenStore) {
239
- throw new QuickBooksError("tokenStore is required", QB_ERROR_CODES.INVALID_CONFIG);
258
+ throw new QuickBooksError(
259
+ "tokenStore is required",
260
+ QB_ERROR_CODES.INVALID_CONFIG
261
+ );
240
262
  }
241
263
  }
242
264
  log(level, message, data) {
@@ -244,6 +266,14 @@ var QuickBooksClient = class {
244
266
  this.onLog(level, message, data);
245
267
  }
246
268
  }
269
+ /**
270
+ * Append minorversion query param to a URL if configured
271
+ */
272
+ appendMinorVersion(url) {
273
+ if (this.minorVersion == null) return url;
274
+ const separator = url.includes("?") ? "&" : "?";
275
+ return `${url}${separator}minorversion=${this.minorVersion}`;
276
+ }
247
277
  /**
248
278
  * Rate limiting - ensures we don't exceed 500 requests/minute
249
279
  */
@@ -300,7 +330,9 @@ var QuickBooksClient = class {
300
330
  const tokens = await this.getValidTokens();
301
331
  const env = this.config.environment || "production";
302
332
  const baseUrl = API_BASE[env];
303
- const url = `${baseUrl}/v3/company/${tokens.realm_id}${endpoint}`;
333
+ const url = this.appendMinorVersion(
334
+ `${baseUrl}/v3/company/${tokens.realm_id}${endpoint}`
335
+ );
304
336
  this.log("debug", `${method} ${endpoint}`, { body });
305
337
  const headers = {
306
338
  Authorization: `Bearer ${tokens.access_token}`,
@@ -317,8 +349,11 @@ var QuickBooksClient = class {
317
349
  });
318
350
  if (response.status === 429 && retryCount < MAX_RETRIES) {
319
351
  const retryAfter = response.headers.get("Retry-After");
320
- const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
321
- this.log("warn", `Rate limited, retrying in ${delay}ms (attempt ${retryCount + 1})`);
352
+ const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_RETRY_DELAY_MS * 2 ** retryCount;
353
+ this.log(
354
+ "warn",
355
+ `Rate limited, retrying in ${delay}ms (attempt ${retryCount + 1})`
356
+ );
322
357
  await new Promise((resolve) => setTimeout(resolve, delay));
323
358
  return this.request(method, endpoint, body, retryCount + 1);
324
359
  }
@@ -351,7 +386,9 @@ var QuickBooksClient = class {
351
386
  const tokens = await this.getValidTokens();
352
387
  const env = this.config.environment || "production";
353
388
  const baseUrl = API_BASE[env];
354
- const url = `${baseUrl}/v3/company/${tokens.realm_id}/query`;
389
+ const url = this.appendMinorVersion(
390
+ `${baseUrl}/v3/company/${tokens.realm_id}/query`
391
+ );
355
392
  await this.checkRateLimit();
356
393
  const fetchResponse = await fetch(url, {
357
394
  method: "POST",
@@ -364,7 +401,10 @@ var QuickBooksClient = class {
364
401
  });
365
402
  if (!fetchResponse.ok) {
366
403
  const errorData = await fetchResponse.json().catch(() => ({}));
367
- throw handleQuickBooksError({ status: fetchResponse.status, ...errorData });
404
+ throw handleQuickBooksError({
405
+ status: fetchResponse.status,
406
+ ...errorData
407
+ });
368
408
  }
369
409
  const data = await fetchResponse.json();
370
410
  const keys = Object.keys(data.QueryResponse).filter(
@@ -387,7 +427,9 @@ var QuickBooksClient = class {
387
427
  const tokens = await this.getValidTokens();
388
428
  const env = this.config.environment || "production";
389
429
  const baseUrl = API_BASE[env];
390
- const url = `${baseUrl}/v3/company/${tokens.realm_id}/query`;
430
+ const url = this.appendMinorVersion(
431
+ `${baseUrl}/v3/company/${tokens.realm_id}/query`
432
+ );
391
433
  await this.checkRateLimit();
392
434
  const fetchResponse = await fetch(url, {
393
435
  method: "POST",
@@ -400,7 +442,10 @@ var QuickBooksClient = class {
400
442
  });
401
443
  if (!fetchResponse.ok) {
402
444
  const errorData = await fetchResponse.json().catch(() => ({}));
403
- throw handleQuickBooksError({ status: fetchResponse.status, ...errorData });
445
+ throw handleQuickBooksError({
446
+ status: fetchResponse.status,
447
+ ...errorData
448
+ });
404
449
  }
405
450
  const data = await fetchResponse.json();
406
451
  const keys = Object.keys(data.QueryResponse).filter(
@@ -409,7 +454,10 @@ var QuickBooksClient = class {
409
454
  const entityKey = keys[0];
410
455
  const pageResults = entityKey ? data.QueryResponse[entityKey] : [];
411
456
  allResults.push(...pageResults);
412
- this.log("debug", `Fetched page at position ${startPosition}, got ${pageResults.length} results (total: ${allResults.length})`);
457
+ this.log(
458
+ "debug",
459
+ `Fetched page at position ${startPosition}, got ${pageResults.length} results (total: ${allResults.length})`
460
+ );
413
461
  if (pageResults.length < maxResults) {
414
462
  break;
415
463
  }
@@ -421,23 +469,34 @@ var QuickBooksClient = class {
421
469
  // Invoice Methods
422
470
  // ============================================
423
471
  async getInvoice(id) {
424
- const response = await this.request("GET", `/invoice/${id}`);
472
+ const response = await this.request(
473
+ "GET",
474
+ `/invoice/${id}`
475
+ );
425
476
  return response.Invoice;
426
477
  }
427
478
  async getInvoices(where) {
428
479
  const sql = where ? `SELECT * FROM Invoice WHERE ${where}` : "SELECT * FROM Invoice";
429
- return this.query(sql);
480
+ return this.queryAll(sql);
430
481
  }
431
482
  async createInvoice(invoice) {
432
- const response = await this.request("POST", "/invoice", invoice);
483
+ const response = await this.request(
484
+ "POST",
485
+ "/invoice",
486
+ invoice
487
+ );
433
488
  return response.Invoice;
434
489
  }
435
490
  async updateInvoice(invoice) {
436
- const response = await this.request("POST", "/invoice", invoice);
491
+ const response = await this.request(
492
+ "POST",
493
+ "/invoice",
494
+ invoice
495
+ );
437
496
  return response.Invoice;
438
497
  }
439
498
  async deleteInvoice(id, syncToken) {
440
- await this.request("POST", "/invoice", {
499
+ await this.request("POST", "/invoice?operation=delete", {
441
500
  Id: id,
442
501
  SyncToken: syncToken
443
502
  });
@@ -446,64 +505,96 @@ var QuickBooksClient = class {
446
505
  // Customer Methods
447
506
  // ============================================
448
507
  async getCustomer(id) {
449
- const response = await this.request("GET", `/customer/${id}`);
508
+ const response = await this.request(
509
+ "GET",
510
+ `/customer/${id}`
511
+ );
450
512
  return response.Customer;
451
513
  }
452
514
  async getCustomers(where) {
453
515
  const sql = where ? `SELECT * FROM Customer WHERE ${where}` : "SELECT * FROM Customer";
454
- return this.query(sql);
516
+ return this.queryAll(sql);
455
517
  }
456
518
  async createCustomer(customer) {
457
- const response = await this.request("POST", "/customer", customer);
519
+ const response = await this.request(
520
+ "POST",
521
+ "/customer",
522
+ customer
523
+ );
458
524
  return response.Customer;
459
525
  }
460
526
  async updateCustomer(customer) {
461
- const response = await this.request("POST", "/customer", customer);
527
+ const response = await this.request(
528
+ "POST",
529
+ "/customer",
530
+ customer
531
+ );
462
532
  return response.Customer;
463
533
  }
464
534
  // ============================================
465
535
  // Payment Methods
466
536
  // ============================================
467
537
  async getPayment(id) {
468
- const response = await this.request("GET", `/payment/${id}`);
538
+ const response = await this.request(
539
+ "GET",
540
+ `/payment/${id}`
541
+ );
469
542
  return response.Payment;
470
543
  }
471
544
  async getPayments(where) {
472
545
  const sql = where ? `SELECT * FROM Payment WHERE ${where}` : "SELECT * FROM Payment";
473
- return this.query(sql);
546
+ return this.queryAll(sql);
474
547
  }
475
548
  async createPayment(payment) {
476
- const response = await this.request("POST", "/payment", payment);
549
+ const response = await this.request(
550
+ "POST",
551
+ "/payment",
552
+ payment
553
+ );
477
554
  return response.Payment;
478
555
  }
479
556
  // ============================================
480
557
  // Account Methods
481
558
  // ============================================
482
559
  async getAccount(id) {
483
- const response = await this.request("GET", `/account/${id}`);
560
+ const response = await this.request(
561
+ "GET",
562
+ `/account/${id}`
563
+ );
484
564
  return response.Account;
485
565
  }
486
566
  async getAccounts(where) {
487
567
  const sql = where ? `SELECT * FROM Account WHERE ${where}` : "SELECT * FROM Account WHERE Active = true";
488
- return this.query(sql);
568
+ return this.queryAll(sql);
489
569
  }
490
570
  // ============================================
491
571
  // Vendor Methods
492
572
  // ============================================
493
573
  async getVendor(id) {
494
- const response = await this.request("GET", `/vendor/${id}`);
574
+ const response = await this.request(
575
+ "GET",
576
+ `/vendor/${id}`
577
+ );
495
578
  return response.Vendor;
496
579
  }
497
580
  async getVendors(where) {
498
581
  const sql = where ? `SELECT * FROM Vendor WHERE ${where}` : "SELECT * FROM Vendor";
499
- return this.query(sql);
582
+ return this.queryAll(sql);
500
583
  }
501
584
  async createVendor(vendor) {
502
- const response = await this.request("POST", "/vendor", vendor);
585
+ const response = await this.request(
586
+ "POST",
587
+ "/vendor",
588
+ vendor
589
+ );
503
590
  return response.Vendor;
504
591
  }
505
592
  async updateVendor(vendor) {
506
- const response = await this.request("POST", "/vendor", vendor);
593
+ const response = await this.request(
594
+ "POST",
595
+ "/vendor",
596
+ vendor
597
+ );
507
598
  return response.Vendor;
508
599
  }
509
600
  // ============================================
@@ -515,7 +606,7 @@ var QuickBooksClient = class {
515
606
  }
516
607
  async getBills(where) {
517
608
  const sql = where ? `SELECT * FROM Bill WHERE ${where}` : "SELECT * FROM Bill";
518
- return this.query(sql);
609
+ return this.queryAll(sql);
519
610
  }
520
611
  async createBill(bill) {
521
612
  const response = await this.request("POST", "/bill", bill);
@@ -525,6 +616,148 @@ var QuickBooksClient = class {
525
616
  const response = await this.request("POST", "/bill", bill);
526
617
  return response.Bill;
527
618
  }
619
+ async deleteBill(id, syncToken) {
620
+ await this.request("POST", "/bill?operation=delete", {
621
+ Id: id,
622
+ SyncToken: syncToken
623
+ });
624
+ }
625
+ // ============================================
626
+ // BillPayment Methods
627
+ // ============================================
628
+ async getBillPayment(id) {
629
+ const response = await this.request(
630
+ "GET",
631
+ `/billpayment/${id}`
632
+ );
633
+ return response.BillPayment;
634
+ }
635
+ async getBillPayments(where) {
636
+ const sql = where ? `SELECT * FROM BillPayment WHERE ${where}` : "SELECT * FROM BillPayment";
637
+ return this.queryAll(sql);
638
+ }
639
+ async createBillPayment(billPayment) {
640
+ const response = await this.request(
641
+ "POST",
642
+ "/billpayment",
643
+ billPayment
644
+ );
645
+ return response.BillPayment;
646
+ }
647
+ async updateBillPayment(billPayment) {
648
+ const response = await this.request(
649
+ "POST",
650
+ "/billpayment",
651
+ billPayment
652
+ );
653
+ return response.BillPayment;
654
+ }
655
+ async deleteBillPayment(id, syncToken) {
656
+ await this.request("POST", "/billpayment?operation=delete", {
657
+ Id: id,
658
+ SyncToken: syncToken
659
+ });
660
+ }
661
+ // ============================================
662
+ // CreditMemo Methods (customer-facing credit notes)
663
+ // ============================================
664
+ async getCreditMemo(id) {
665
+ const response = await this.request(
666
+ "GET",
667
+ `/creditmemo/${id}`
668
+ );
669
+ return response.CreditMemo;
670
+ }
671
+ async getCreditMemos(where) {
672
+ const sql = where ? `SELECT * FROM CreditMemo WHERE ${where}` : "SELECT * FROM CreditMemo";
673
+ return this.queryAll(sql);
674
+ }
675
+ async createCreditMemo(creditMemo) {
676
+ const response = await this.request(
677
+ "POST",
678
+ "/creditmemo",
679
+ creditMemo
680
+ );
681
+ return response.CreditMemo;
682
+ }
683
+ async updateCreditMemo(creditMemo) {
684
+ const response = await this.request(
685
+ "POST",
686
+ "/creditmemo",
687
+ creditMemo
688
+ );
689
+ return response.CreditMemo;
690
+ }
691
+ async deleteCreditMemo(id, syncToken) {
692
+ await this.request("POST", "/creditmemo?operation=delete", {
693
+ Id: id,
694
+ SyncToken: syncToken
695
+ });
696
+ }
697
+ // ============================================
698
+ // VendorCredit Methods (supplier-side credit notes)
699
+ // ============================================
700
+ async getVendorCredit(id) {
701
+ const response = await this.request(
702
+ "GET",
703
+ `/vendorcredit/${id}`
704
+ );
705
+ return response.VendorCredit;
706
+ }
707
+ async getVendorCredits(where) {
708
+ const sql = where ? `SELECT * FROM VendorCredit WHERE ${where}` : "SELECT * FROM VendorCredit";
709
+ return this.queryAll(sql);
710
+ }
711
+ async createVendorCredit(vendorCredit) {
712
+ const response = await this.request(
713
+ "POST",
714
+ "/vendorcredit",
715
+ vendorCredit
716
+ );
717
+ return response.VendorCredit;
718
+ }
719
+ async updateVendorCredit(vendorCredit) {
720
+ const response = await this.request(
721
+ "POST",
722
+ "/vendorcredit",
723
+ vendorCredit
724
+ );
725
+ return response.VendorCredit;
726
+ }
727
+ async deleteVendorCredit(id, syncToken) {
728
+ await this.request("POST", "/vendorcredit?operation=delete", {
729
+ Id: id,
730
+ SyncToken: syncToken
731
+ });
732
+ }
733
+ // ============================================
734
+ // TaxCode Methods (read-only in QBO API)
735
+ // ============================================
736
+ async getTaxCode(id) {
737
+ const response = await this.request(
738
+ "GET",
739
+ `/taxcode/${id}`
740
+ );
741
+ return response.TaxCode;
742
+ }
743
+ async getTaxCodes(where) {
744
+ const sql = where ? `SELECT * FROM TaxCode WHERE ${where}` : "SELECT * FROM TaxCode";
745
+ return this.queryAll(sql);
746
+ }
747
+ // ============================================
748
+ // TaxRate Methods (read-only in QBO API)
749
+ // ============================================
750
+ async getTaxRate(id) {
751
+ const response = await this.request(
752
+ "GET",
753
+ `/taxrate/${id}`
754
+ );
755
+ return response.TaxRate;
756
+ }
757
+ async getTaxRates(where) {
758
+ const sql = where ? `SELECT * FROM TaxRate WHERE ${where}` : "SELECT * FROM TaxRate";
759
+ return this.queryAll(sql);
760
+ }
528
761
  // ============================================
529
762
  // Item Methods
530
763
  // ============================================
@@ -534,7 +767,7 @@ var QuickBooksClient = class {
534
767
  }
535
768
  async getItems(where) {
536
769
  const sql = where ? `SELECT * FROM Item WHERE ${where}` : "SELECT * FROM Item WHERE Active = true";
537
- return this.query(sql);
770
+ return this.queryAll(sql);
538
771
  }
539
772
  async createItem(item) {
540
773
  const response = await this.request("POST", "/item", item);
@@ -545,6 +778,113 @@ var QuickBooksClient = class {
545
778
  return response.Item;
546
779
  }
547
780
  // ============================================
781
+ // Attachable Methods
782
+ // ============================================
783
+ async getAttachable(id) {
784
+ const response = await this.request(
785
+ "GET",
786
+ `/attachable/${id}`
787
+ );
788
+ return response.Attachable;
789
+ }
790
+ async getAttachables(where) {
791
+ const sql = where ? `SELECT * FROM Attachable WHERE ${where}` : "SELECT * FROM Attachable";
792
+ return this.queryAll(sql);
793
+ }
794
+ /**
795
+ * Upload a file and attach it to an entity.
796
+ * Uses multipart/form-data — the QBO upload endpoint differs from standard CRUD.
797
+ */
798
+ async uploadAttachable(file, fileName, contentType, attachTo) {
799
+ await this.checkRateLimit();
800
+ const tokens = await this.getValidTokens();
801
+ const env = this.config.environment || "production";
802
+ const baseUrl = API_BASE[env];
803
+ const url = this.appendMinorVersion(
804
+ `${baseUrl}/v3/company/${tokens.realm_id}/upload`
805
+ );
806
+ const metadata = {
807
+ FileName: fileName,
808
+ ContentType: contentType
809
+ };
810
+ if (attachTo) {
811
+ metadata.AttachableRef = [
812
+ {
813
+ EntityRef: {
814
+ value: attachTo.entityId,
815
+ name: attachTo.entityType
816
+ }
817
+ }
818
+ ];
819
+ }
820
+ const formData = new FormData();
821
+ formData.append(
822
+ "file_metadata_0",
823
+ new Blob([JSON.stringify(metadata)], { type: "application/json" })
824
+ );
825
+ const fileBlob = file instanceof Buffer ? new Blob([file], { type: contentType }) : file;
826
+ formData.append("file_content_0", fileBlob, fileName);
827
+ try {
828
+ const response = await fetch(url, {
829
+ method: "POST",
830
+ headers: {
831
+ Authorization: `Bearer ${tokens.access_token}`,
832
+ Accept: "application/json"
833
+ },
834
+ body: formData
835
+ });
836
+ if (!response.ok) {
837
+ const errorData = await response.json().catch(() => ({}));
838
+ throw { status: response.status, ...errorData };
839
+ }
840
+ const data = await response.json();
841
+ return data.AttachableResponse[0].Attachable;
842
+ } catch (error) {
843
+ throw handleQuickBooksError(error);
844
+ }
845
+ }
846
+ async updateAttachable(attachable) {
847
+ const response = await this.request(
848
+ "POST",
849
+ "/attachable",
850
+ attachable
851
+ );
852
+ return response.Attachable;
853
+ }
854
+ async deleteAttachable(id, syncToken) {
855
+ await this.request("POST", "/attachable?operation=delete", {
856
+ Id: id,
857
+ SyncToken: syncToken
858
+ });
859
+ }
860
+ // ============================================
861
+ // Batch Operations
862
+ // ============================================
863
+ /**
864
+ * Execute a batch of up to 30 operations in a single API call.
865
+ * Each item needs a unique bId and the entity payload.
866
+ *
867
+ * @example
868
+ * ```ts
869
+ * const results = await client.batch([
870
+ * { bId: "1", operation: "create", Bill: { VendorRef: { value: "1" }, Line: [...] } },
871
+ * { bId: "2", operation: "query", optionsData: "SELECT * FROM Vendor WHERE Id = '1'" },
872
+ * ]);
873
+ * ```
874
+ */
875
+ async batch(items) {
876
+ if (items.length > 30) {
877
+ throw new QuickBooksError(
878
+ "Batch operations are limited to 30 items per request",
879
+ QB_ERROR_CODES.INVALID_CONFIG
880
+ );
881
+ }
882
+ const response = await this.request("POST", "/batch", {
883
+ BatchItemRequest: items
884
+ });
885
+ return response;
886
+ }
887
+ // ============================================
548
888
  // Utility Methods
549
889
  // ============================================
550
890
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panoptic-it-solutions/quickbooks-client",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "QuickBooks Online API client with OAuth 2.0, rate limiting, and typed entities",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",