@panoptic-it-solutions/quickbooks-client 0.1.4 → 0.3.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.js CHANGED
@@ -141,7 +141,9 @@ function generateAuthUrl(config, state) {
141
141
  }
142
142
  async function exchangeCodeForTokens(config, code, realmId) {
143
143
  const env = config.environment || "production";
144
- const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
144
+ const credentials = Buffer.from(
145
+ `${config.clientId}:${config.clientSecret}`
146
+ ).toString("base64");
145
147
  const response = await fetch(ENDPOINTS[env].token, {
146
148
  method: "POST",
147
149
  headers: {
@@ -175,7 +177,9 @@ async function exchangeCodeForTokens(config, code, realmId) {
175
177
  }
176
178
  async function refreshTokens(config, refreshToken, realmId) {
177
179
  const env = config.environment || "production";
178
- const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
180
+ const credentials = Buffer.from(
181
+ `${config.clientId}:${config.clientSecret}`
182
+ ).toString("base64");
179
183
  const response = await fetch(ENDPOINTS[env].token, {
180
184
  method: "POST",
181
185
  headers: {
@@ -208,7 +212,9 @@ async function refreshTokens(config, refreshToken, realmId) {
208
212
  }
209
213
  async function revokeTokens(config, token) {
210
214
  const env = config.environment || "production";
211
- const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
215
+ const credentials = Buffer.from(
216
+ `${config.clientId}:${config.clientSecret}`
217
+ ).toString("base64");
212
218
  const response = await fetch(ENDPOINTS[env].revoke, {
213
219
  method: "POST",
214
220
  headers: {
@@ -238,7 +244,9 @@ function isTokenExpired(expiresAt, bufferSeconds = 300) {
238
244
  function generateState() {
239
245
  const array = new Uint8Array(16);
240
246
  crypto.getRandomValues(array);
241
- return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
247
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
248
+ ""
249
+ );
242
250
  }
243
251
 
244
252
  // src/client.ts
@@ -255,24 +263,38 @@ var QuickBooksClient = class {
255
263
  tokenStore;
256
264
  requestTimestamps = [];
257
265
  onLog;
266
+ minorVersion;
258
267
  constructor(options) {
259
268
  this.validateConfig(options);
260
269
  this.config = options;
261
270
  this.tokenStore = options.tokenStore;
262
271
  this.onLog = options.onLog;
272
+ this.minorVersion = options.minorVersion;
263
273
  }
264
274
  validateConfig(options) {
265
275
  if (!options.clientId) {
266
- throw new QuickBooksError("clientId is required", QB_ERROR_CODES.INVALID_CONFIG);
276
+ throw new QuickBooksError(
277
+ "clientId is required",
278
+ QB_ERROR_CODES.INVALID_CONFIG
279
+ );
267
280
  }
268
281
  if (!options.clientSecret) {
269
- throw new QuickBooksError("clientSecret is required", QB_ERROR_CODES.INVALID_CONFIG);
282
+ throw new QuickBooksError(
283
+ "clientSecret is required",
284
+ QB_ERROR_CODES.INVALID_CONFIG
285
+ );
270
286
  }
271
287
  if (!options.redirectUri) {
272
- throw new QuickBooksError("redirectUri is required", QB_ERROR_CODES.INVALID_CONFIG);
288
+ throw new QuickBooksError(
289
+ "redirectUri is required",
290
+ QB_ERROR_CODES.INVALID_CONFIG
291
+ );
273
292
  }
274
293
  if (!options.tokenStore) {
275
- throw new QuickBooksError("tokenStore is required", QB_ERROR_CODES.INVALID_CONFIG);
294
+ throw new QuickBooksError(
295
+ "tokenStore is required",
296
+ QB_ERROR_CODES.INVALID_CONFIG
297
+ );
276
298
  }
277
299
  }
278
300
  log(level, message, data) {
@@ -280,6 +302,14 @@ var QuickBooksClient = class {
280
302
  this.onLog(level, message, data);
281
303
  }
282
304
  }
305
+ /**
306
+ * Append minorversion query param to a URL if configured
307
+ */
308
+ appendMinorVersion(url) {
309
+ if (this.minorVersion == null) return url;
310
+ const separator = url.includes("?") ? "&" : "?";
311
+ return `${url}${separator}minorversion=${this.minorVersion}`;
312
+ }
283
313
  /**
284
314
  * Rate limiting - ensures we don't exceed 500 requests/minute
285
315
  */
@@ -336,7 +366,9 @@ var QuickBooksClient = class {
336
366
  const tokens = await this.getValidTokens();
337
367
  const env = this.config.environment || "production";
338
368
  const baseUrl = API_BASE[env];
339
- const url = `${baseUrl}/v3/company/${tokens.realm_id}${endpoint}`;
369
+ const url = this.appendMinorVersion(
370
+ `${baseUrl}/v3/company/${tokens.realm_id}${endpoint}`
371
+ );
340
372
  this.log("debug", `${method} ${endpoint}`, { body });
341
373
  const headers = {
342
374
  Authorization: `Bearer ${tokens.access_token}`,
@@ -353,8 +385,11 @@ var QuickBooksClient = class {
353
385
  });
354
386
  if (response.status === 429 && retryCount < MAX_RETRIES) {
355
387
  const retryAfter = response.headers.get("Retry-After");
356
- const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
357
- this.log("warn", `Rate limited, retrying in ${delay}ms (attempt ${retryCount + 1})`);
388
+ const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_RETRY_DELAY_MS * 2 ** retryCount;
389
+ this.log(
390
+ "warn",
391
+ `Rate limited, retrying in ${delay}ms (attempt ${retryCount + 1})`
392
+ );
358
393
  await new Promise((resolve) => setTimeout(resolve, delay));
359
394
  return this.request(method, endpoint, body, retryCount + 1);
360
395
  }
@@ -387,7 +422,9 @@ var QuickBooksClient = class {
387
422
  const tokens = await this.getValidTokens();
388
423
  const env = this.config.environment || "production";
389
424
  const baseUrl = API_BASE[env];
390
- const url = `${baseUrl}/v3/company/${tokens.realm_id}/query`;
425
+ const url = this.appendMinorVersion(
426
+ `${baseUrl}/v3/company/${tokens.realm_id}/query`
427
+ );
391
428
  await this.checkRateLimit();
392
429
  const fetchResponse = await fetch(url, {
393
430
  method: "POST",
@@ -400,7 +437,10 @@ var QuickBooksClient = class {
400
437
  });
401
438
  if (!fetchResponse.ok) {
402
439
  const errorData = await fetchResponse.json().catch(() => ({}));
403
- throw handleQuickBooksError({ status: fetchResponse.status, ...errorData });
440
+ throw handleQuickBooksError({
441
+ status: fetchResponse.status,
442
+ ...errorData
443
+ });
404
444
  }
405
445
  const data = await fetchResponse.json();
406
446
  const keys = Object.keys(data.QueryResponse).filter(
@@ -423,7 +463,9 @@ var QuickBooksClient = class {
423
463
  const tokens = await this.getValidTokens();
424
464
  const env = this.config.environment || "production";
425
465
  const baseUrl = API_BASE[env];
426
- const url = `${baseUrl}/v3/company/${tokens.realm_id}/query`;
466
+ const url = this.appendMinorVersion(
467
+ `${baseUrl}/v3/company/${tokens.realm_id}/query`
468
+ );
427
469
  await this.checkRateLimit();
428
470
  const fetchResponse = await fetch(url, {
429
471
  method: "POST",
@@ -436,7 +478,10 @@ var QuickBooksClient = class {
436
478
  });
437
479
  if (!fetchResponse.ok) {
438
480
  const errorData = await fetchResponse.json().catch(() => ({}));
439
- throw handleQuickBooksError({ status: fetchResponse.status, ...errorData });
481
+ throw handleQuickBooksError({
482
+ status: fetchResponse.status,
483
+ ...errorData
484
+ });
440
485
  }
441
486
  const data = await fetchResponse.json();
442
487
  const keys = Object.keys(data.QueryResponse).filter(
@@ -445,7 +490,10 @@ var QuickBooksClient = class {
445
490
  const entityKey = keys[0];
446
491
  const pageResults = entityKey ? data.QueryResponse[entityKey] : [];
447
492
  allResults.push(...pageResults);
448
- this.log("debug", `Fetched page at position ${startPosition}, got ${pageResults.length} results (total: ${allResults.length})`);
493
+ this.log(
494
+ "debug",
495
+ `Fetched page at position ${startPosition}, got ${pageResults.length} results (total: ${allResults.length})`
496
+ );
449
497
  if (pageResults.length < maxResults) {
450
498
  break;
451
499
  }
@@ -457,23 +505,34 @@ var QuickBooksClient = class {
457
505
  // Invoice Methods
458
506
  // ============================================
459
507
  async getInvoice(id) {
460
- const response = await this.request("GET", `/invoice/${id}`);
508
+ const response = await this.request(
509
+ "GET",
510
+ `/invoice/${id}`
511
+ );
461
512
  return response.Invoice;
462
513
  }
463
514
  async getInvoices(where) {
464
515
  const sql = where ? `SELECT * FROM Invoice WHERE ${where}` : "SELECT * FROM Invoice";
465
- return this.query(sql);
516
+ return this.queryAll(sql);
466
517
  }
467
518
  async createInvoice(invoice) {
468
- const response = await this.request("POST", "/invoice", invoice);
519
+ const response = await this.request(
520
+ "POST",
521
+ "/invoice",
522
+ invoice
523
+ );
469
524
  return response.Invoice;
470
525
  }
471
526
  async updateInvoice(invoice) {
472
- const response = await this.request("POST", "/invoice", invoice);
527
+ const response = await this.request(
528
+ "POST",
529
+ "/invoice",
530
+ invoice
531
+ );
473
532
  return response.Invoice;
474
533
  }
475
534
  async deleteInvoice(id, syncToken) {
476
- await this.request("POST", "/invoice", {
535
+ await this.request("POST", "/invoice?operation=delete", {
477
536
  Id: id,
478
537
  SyncToken: syncToken
479
538
  });
@@ -482,64 +541,112 @@ var QuickBooksClient = class {
482
541
  // Customer Methods
483
542
  // ============================================
484
543
  async getCustomer(id) {
485
- const response = await this.request("GET", `/customer/${id}`);
544
+ const response = await this.request(
545
+ "GET",
546
+ `/customer/${id}`
547
+ );
486
548
  return response.Customer;
487
549
  }
488
550
  async getCustomers(where) {
489
551
  const sql = where ? `SELECT * FROM Customer WHERE ${where}` : "SELECT * FROM Customer";
490
- return this.query(sql);
552
+ return this.queryAll(sql);
491
553
  }
492
554
  async createCustomer(customer) {
493
- const response = await this.request("POST", "/customer", customer);
555
+ const response = await this.request(
556
+ "POST",
557
+ "/customer",
558
+ customer
559
+ );
494
560
  return response.Customer;
495
561
  }
496
562
  async updateCustomer(customer) {
497
- const response = await this.request("POST", "/customer", customer);
563
+ const response = await this.request(
564
+ "POST",
565
+ "/customer",
566
+ customer
567
+ );
498
568
  return response.Customer;
499
569
  }
500
570
  // ============================================
501
571
  // Payment Methods
502
572
  // ============================================
503
573
  async getPayment(id) {
504
- const response = await this.request("GET", `/payment/${id}`);
574
+ const response = await this.request(
575
+ "GET",
576
+ `/payment/${id}`
577
+ );
505
578
  return response.Payment;
506
579
  }
507
580
  async getPayments(where) {
508
581
  const sql = where ? `SELECT * FROM Payment WHERE ${where}` : "SELECT * FROM Payment";
509
- return this.query(sql);
582
+ return this.queryAll(sql);
510
583
  }
511
584
  async createPayment(payment) {
512
- const response = await this.request("POST", "/payment", payment);
585
+ const response = await this.request(
586
+ "POST",
587
+ "/payment",
588
+ payment
589
+ );
513
590
  return response.Payment;
514
591
  }
515
592
  // ============================================
516
593
  // Account Methods
517
594
  // ============================================
518
595
  async getAccount(id) {
519
- const response = await this.request("GET", `/account/${id}`);
596
+ const response = await this.request(
597
+ "GET",
598
+ `/account/${id}`
599
+ );
520
600
  return response.Account;
521
601
  }
522
602
  async getAccounts(where) {
523
603
  const sql = where ? `SELECT * FROM Account WHERE ${where}` : "SELECT * FROM Account WHERE Active = true";
524
- return this.query(sql);
604
+ return this.queryAll(sql);
605
+ }
606
+ async createAccount(account) {
607
+ const response = await this.request(
608
+ "POST",
609
+ "/account",
610
+ account
611
+ );
612
+ return response.Account;
613
+ }
614
+ async updateAccount(account) {
615
+ const response = await this.request(
616
+ "POST",
617
+ "/account",
618
+ account
619
+ );
620
+ return response.Account;
525
621
  }
526
622
  // ============================================
527
623
  // Vendor Methods
528
624
  // ============================================
529
625
  async getVendor(id) {
530
- const response = await this.request("GET", `/vendor/${id}`);
626
+ const response = await this.request(
627
+ "GET",
628
+ `/vendor/${id}`
629
+ );
531
630
  return response.Vendor;
532
631
  }
533
632
  async getVendors(where) {
534
633
  const sql = where ? `SELECT * FROM Vendor WHERE ${where}` : "SELECT * FROM Vendor";
535
- return this.query(sql);
634
+ return this.queryAll(sql);
536
635
  }
537
636
  async createVendor(vendor) {
538
- const response = await this.request("POST", "/vendor", vendor);
637
+ const response = await this.request(
638
+ "POST",
639
+ "/vendor",
640
+ vendor
641
+ );
539
642
  return response.Vendor;
540
643
  }
541
644
  async updateVendor(vendor) {
542
- const response = await this.request("POST", "/vendor", vendor);
645
+ const response = await this.request(
646
+ "POST",
647
+ "/vendor",
648
+ vendor
649
+ );
543
650
  return response.Vendor;
544
651
  }
545
652
  // ============================================
@@ -551,7 +658,7 @@ var QuickBooksClient = class {
551
658
  }
552
659
  async getBills(where) {
553
660
  const sql = where ? `SELECT * FROM Bill WHERE ${where}` : "SELECT * FROM Bill";
554
- return this.query(sql);
661
+ return this.queryAll(sql);
555
662
  }
556
663
  async createBill(bill) {
557
664
  const response = await this.request("POST", "/bill", bill);
@@ -561,6 +668,148 @@ var QuickBooksClient = class {
561
668
  const response = await this.request("POST", "/bill", bill);
562
669
  return response.Bill;
563
670
  }
671
+ async deleteBill(id, syncToken) {
672
+ await this.request("POST", "/bill?operation=delete", {
673
+ Id: id,
674
+ SyncToken: syncToken
675
+ });
676
+ }
677
+ // ============================================
678
+ // BillPayment Methods
679
+ // ============================================
680
+ async getBillPayment(id) {
681
+ const response = await this.request(
682
+ "GET",
683
+ `/billpayment/${id}`
684
+ );
685
+ return response.BillPayment;
686
+ }
687
+ async getBillPayments(where) {
688
+ const sql = where ? `SELECT * FROM BillPayment WHERE ${where}` : "SELECT * FROM BillPayment";
689
+ return this.queryAll(sql);
690
+ }
691
+ async createBillPayment(billPayment) {
692
+ const response = await this.request(
693
+ "POST",
694
+ "/billpayment",
695
+ billPayment
696
+ );
697
+ return response.BillPayment;
698
+ }
699
+ async updateBillPayment(billPayment) {
700
+ const response = await this.request(
701
+ "POST",
702
+ "/billpayment",
703
+ billPayment
704
+ );
705
+ return response.BillPayment;
706
+ }
707
+ async deleteBillPayment(id, syncToken) {
708
+ await this.request("POST", "/billpayment?operation=delete", {
709
+ Id: id,
710
+ SyncToken: syncToken
711
+ });
712
+ }
713
+ // ============================================
714
+ // CreditMemo Methods (customer-facing credit notes)
715
+ // ============================================
716
+ async getCreditMemo(id) {
717
+ const response = await this.request(
718
+ "GET",
719
+ `/creditmemo/${id}`
720
+ );
721
+ return response.CreditMemo;
722
+ }
723
+ async getCreditMemos(where) {
724
+ const sql = where ? `SELECT * FROM CreditMemo WHERE ${where}` : "SELECT * FROM CreditMemo";
725
+ return this.queryAll(sql);
726
+ }
727
+ async createCreditMemo(creditMemo) {
728
+ const response = await this.request(
729
+ "POST",
730
+ "/creditmemo",
731
+ creditMemo
732
+ );
733
+ return response.CreditMemo;
734
+ }
735
+ async updateCreditMemo(creditMemo) {
736
+ const response = await this.request(
737
+ "POST",
738
+ "/creditmemo",
739
+ creditMemo
740
+ );
741
+ return response.CreditMemo;
742
+ }
743
+ async deleteCreditMemo(id, syncToken) {
744
+ await this.request("POST", "/creditmemo?operation=delete", {
745
+ Id: id,
746
+ SyncToken: syncToken
747
+ });
748
+ }
749
+ // ============================================
750
+ // VendorCredit Methods (supplier-side credit notes)
751
+ // ============================================
752
+ async getVendorCredit(id) {
753
+ const response = await this.request(
754
+ "GET",
755
+ `/vendorcredit/${id}`
756
+ );
757
+ return response.VendorCredit;
758
+ }
759
+ async getVendorCredits(where) {
760
+ const sql = where ? `SELECT * FROM VendorCredit WHERE ${where}` : "SELECT * FROM VendorCredit";
761
+ return this.queryAll(sql);
762
+ }
763
+ async createVendorCredit(vendorCredit) {
764
+ const response = await this.request(
765
+ "POST",
766
+ "/vendorcredit",
767
+ vendorCredit
768
+ );
769
+ return response.VendorCredit;
770
+ }
771
+ async updateVendorCredit(vendorCredit) {
772
+ const response = await this.request(
773
+ "POST",
774
+ "/vendorcredit",
775
+ vendorCredit
776
+ );
777
+ return response.VendorCredit;
778
+ }
779
+ async deleteVendorCredit(id, syncToken) {
780
+ await this.request("POST", "/vendorcredit?operation=delete", {
781
+ Id: id,
782
+ SyncToken: syncToken
783
+ });
784
+ }
785
+ // ============================================
786
+ // TaxCode Methods (read-only in QBO API)
787
+ // ============================================
788
+ async getTaxCode(id) {
789
+ const response = await this.request(
790
+ "GET",
791
+ `/taxcode/${id}`
792
+ );
793
+ return response.TaxCode;
794
+ }
795
+ async getTaxCodes(where) {
796
+ const sql = where ? `SELECT * FROM TaxCode WHERE ${where}` : "SELECT * FROM TaxCode";
797
+ return this.queryAll(sql);
798
+ }
799
+ // ============================================
800
+ // TaxRate Methods (read-only in QBO API)
801
+ // ============================================
802
+ async getTaxRate(id) {
803
+ const response = await this.request(
804
+ "GET",
805
+ `/taxrate/${id}`
806
+ );
807
+ return response.TaxRate;
808
+ }
809
+ async getTaxRates(where) {
810
+ const sql = where ? `SELECT * FROM TaxRate WHERE ${where}` : "SELECT * FROM TaxRate";
811
+ return this.queryAll(sql);
812
+ }
564
813
  // ============================================
565
814
  // Item Methods
566
815
  // ============================================
@@ -570,7 +819,7 @@ var QuickBooksClient = class {
570
819
  }
571
820
  async getItems(where) {
572
821
  const sql = where ? `SELECT * FROM Item WHERE ${where}` : "SELECT * FROM Item WHERE Active = true";
573
- return this.query(sql);
822
+ return this.queryAll(sql);
574
823
  }
575
824
  async createItem(item) {
576
825
  const response = await this.request("POST", "/item", item);
@@ -581,6 +830,113 @@ var QuickBooksClient = class {
581
830
  return response.Item;
582
831
  }
583
832
  // ============================================
833
+ // Attachable Methods
834
+ // ============================================
835
+ async getAttachable(id) {
836
+ const response = await this.request(
837
+ "GET",
838
+ `/attachable/${id}`
839
+ );
840
+ return response.Attachable;
841
+ }
842
+ async getAttachables(where) {
843
+ const sql = where ? `SELECT * FROM Attachable WHERE ${where}` : "SELECT * FROM Attachable";
844
+ return this.queryAll(sql);
845
+ }
846
+ /**
847
+ * Upload a file and attach it to an entity.
848
+ * Uses multipart/form-data — the QBO upload endpoint differs from standard CRUD.
849
+ */
850
+ async uploadAttachable(file, fileName, contentType, attachTo) {
851
+ await this.checkRateLimit();
852
+ const tokens = await this.getValidTokens();
853
+ const env = this.config.environment || "production";
854
+ const baseUrl = API_BASE[env];
855
+ const url = this.appendMinorVersion(
856
+ `${baseUrl}/v3/company/${tokens.realm_id}/upload`
857
+ );
858
+ const metadata = {
859
+ FileName: fileName,
860
+ ContentType: contentType
861
+ };
862
+ if (attachTo) {
863
+ metadata.AttachableRef = [
864
+ {
865
+ EntityRef: {
866
+ value: attachTo.entityId,
867
+ name: attachTo.entityType
868
+ }
869
+ }
870
+ ];
871
+ }
872
+ const formData = new FormData();
873
+ formData.append(
874
+ "file_metadata_0",
875
+ new Blob([JSON.stringify(metadata)], { type: "application/json" })
876
+ );
877
+ const fileBlob = file instanceof Buffer ? new Blob([file], { type: contentType }) : file;
878
+ formData.append("file_content_0", fileBlob, fileName);
879
+ try {
880
+ const response = await fetch(url, {
881
+ method: "POST",
882
+ headers: {
883
+ Authorization: `Bearer ${tokens.access_token}`,
884
+ Accept: "application/json"
885
+ },
886
+ body: formData
887
+ });
888
+ if (!response.ok) {
889
+ const errorData = await response.json().catch(() => ({}));
890
+ throw { status: response.status, ...errorData };
891
+ }
892
+ const data = await response.json();
893
+ return data.AttachableResponse[0].Attachable;
894
+ } catch (error) {
895
+ throw handleQuickBooksError(error);
896
+ }
897
+ }
898
+ async updateAttachable(attachable) {
899
+ const response = await this.request(
900
+ "POST",
901
+ "/attachable",
902
+ attachable
903
+ );
904
+ return response.Attachable;
905
+ }
906
+ async deleteAttachable(id, syncToken) {
907
+ await this.request("POST", "/attachable?operation=delete", {
908
+ Id: id,
909
+ SyncToken: syncToken
910
+ });
911
+ }
912
+ // ============================================
913
+ // Batch Operations
914
+ // ============================================
915
+ /**
916
+ * Execute a batch of up to 30 operations in a single API call.
917
+ * Each item needs a unique bId and the entity payload.
918
+ *
919
+ * @example
920
+ * ```ts
921
+ * const results = await client.batch([
922
+ * { bId: "1", operation: "create", Bill: { VendorRef: { value: "1" }, Line: [...] } },
923
+ * { bId: "2", operation: "query", optionsData: "SELECT * FROM Vendor WHERE Id = '1'" },
924
+ * ]);
925
+ * ```
926
+ */
927
+ async batch(items) {
928
+ if (items.length > 30) {
929
+ throw new QuickBooksError(
930
+ "Batch operations are limited to 30 items per request",
931
+ QB_ERROR_CODES.INVALID_CONFIG
932
+ );
933
+ }
934
+ const response = await this.request("POST", "/batch", {
935
+ BatchItemRequest: items
936
+ });
937
+ return response;
938
+ }
939
+ // ============================================
584
940
  // Utility Methods
585
941
  // ============================================
586
942
  /**