@panoptic-it-solutions/quickbooks-client 0.1.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 ADDED
@@ -0,0 +1,594 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ QB_ERROR_CODES: () => QB_ERROR_CODES,
24
+ QuickBooksClient: () => QuickBooksClient,
25
+ QuickBooksError: () => QuickBooksError,
26
+ calculateTokenExpiry: () => calculateTokenExpiry,
27
+ exchangeCodeForTokens: () => exchangeCodeForTokens,
28
+ generateAuthUrl: () => generateAuthUrl,
29
+ generateState: () => generateState,
30
+ handleQuickBooksError: () => handleQuickBooksError,
31
+ isTokenExpired: () => isTokenExpired,
32
+ refreshTokens: () => refreshTokens,
33
+ revokeTokens: () => revokeTokens
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/errors.ts
38
+ var QB_ERROR_CODES = {
39
+ TOKEN_EXPIRED: "QB_TOKEN_EXPIRED",
40
+ REFRESH_FAILED: "QB_REFRESH_FAILED",
41
+ UNAUTHORIZED: "QB_UNAUTHORIZED",
42
+ INVALID_REALM: "QB_INVALID_REALM",
43
+ API_ERROR: "QB_API_ERROR",
44
+ RATE_LIMIT: "QB_RATE_LIMIT",
45
+ NETWORK_ERROR: "QB_NETWORK_ERROR",
46
+ INVALID_CONFIG: "QB_INVALID_CONFIG",
47
+ TOKEN_STORE_ERROR: "QB_TOKEN_STORE_ERROR"
48
+ };
49
+ var QuickBooksError = class extends Error {
50
+ constructor(message, code, status, details) {
51
+ super(message);
52
+ this.code = code;
53
+ this.status = status;
54
+ this.details = details;
55
+ this.name = "QuickBooksError";
56
+ }
57
+ };
58
+ function handleQuickBooksError(error) {
59
+ if (error instanceof QuickBooksError) {
60
+ return error;
61
+ }
62
+ if (error instanceof TypeError && error.message.includes("fetch")) {
63
+ return new QuickBooksError(
64
+ "Network error connecting to QuickBooks API",
65
+ QB_ERROR_CODES.NETWORK_ERROR,
66
+ 0,
67
+ error
68
+ );
69
+ }
70
+ if (typeof error === "object" && error !== null) {
71
+ const err = error;
72
+ if (typeof err.status === "number") {
73
+ if (err.status === 401) {
74
+ return new QuickBooksError(
75
+ "Unauthorized access to QuickBooks API",
76
+ QB_ERROR_CODES.UNAUTHORIZED,
77
+ 401,
78
+ error
79
+ );
80
+ }
81
+ if (err.status === 403) {
82
+ return new QuickBooksError(
83
+ "Access forbidden - check API permissions",
84
+ QB_ERROR_CODES.UNAUTHORIZED,
85
+ 403,
86
+ error
87
+ );
88
+ }
89
+ if (err.status === 429) {
90
+ return new QuickBooksError(
91
+ "QuickBooks API rate limit exceeded",
92
+ QB_ERROR_CODES.RATE_LIMIT,
93
+ 429,
94
+ error
95
+ );
96
+ }
97
+ }
98
+ const message = String(err.message || "");
99
+ if (message.includes("token expired") || message.includes("invalid_token")) {
100
+ return new QuickBooksError(
101
+ "QuickBooks token expired",
102
+ QB_ERROR_CODES.TOKEN_EXPIRED,
103
+ 401,
104
+ error
105
+ );
106
+ }
107
+ }
108
+ return new QuickBooksError(
109
+ "An unexpected error occurred with QuickBooks integration",
110
+ QB_ERROR_CODES.API_ERROR,
111
+ 500,
112
+ error
113
+ );
114
+ }
115
+
116
+ // src/oauth.ts
117
+ var ENDPOINTS = {
118
+ sandbox: {
119
+ authorize: "https://appcenter.intuit.com/connect/oauth2",
120
+ token: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
121
+ revoke: "https://developer.api.intuit.com/v2/oauth2/tokens/revoke"
122
+ },
123
+ production: {
124
+ authorize: "https://appcenter.intuit.com/connect/oauth2",
125
+ token: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
126
+ revoke: "https://developer.api.intuit.com/v2/oauth2/tokens/revoke"
127
+ }
128
+ };
129
+ var DEFAULT_SCOPES = ["com.intuit.quickbooks.accounting"];
130
+ function generateAuthUrl(config, state) {
131
+ const env = config.environment || "production";
132
+ const scopes = config.scopes || DEFAULT_SCOPES;
133
+ const params = new URLSearchParams({
134
+ client_id: config.clientId,
135
+ redirect_uri: config.redirectUri,
136
+ response_type: "code",
137
+ scope: scopes.join(" "),
138
+ state
139
+ });
140
+ return `${ENDPOINTS[env].authorize}?${params.toString()}`;
141
+ }
142
+ async function exchangeCodeForTokens(config, code, realmId) {
143
+ const env = config.environment || "production";
144
+ const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
145
+ const response = await fetch(ENDPOINTS[env].token, {
146
+ method: "POST",
147
+ headers: {
148
+ "Content-Type": "application/x-www-form-urlencoded",
149
+ Authorization: `Basic ${credentials}`,
150
+ Accept: "application/json"
151
+ },
152
+ body: new URLSearchParams({
153
+ grant_type: "authorization_code",
154
+ code,
155
+ redirect_uri: config.redirectUri
156
+ }).toString()
157
+ });
158
+ if (!response.ok) {
159
+ const errorData = await response.json().catch(() => ({}));
160
+ throw new QuickBooksError(
161
+ `Failed to exchange code for tokens: ${response.status}`,
162
+ QB_ERROR_CODES.UNAUTHORIZED,
163
+ response.status,
164
+ errorData
165
+ );
166
+ }
167
+ const data = await response.json();
168
+ return {
169
+ access_token: data.access_token,
170
+ refresh_token: data.refresh_token,
171
+ realm_id: realmId,
172
+ expires_at: calculateTokenExpiry(data.expires_in),
173
+ token_type: data.token_type
174
+ };
175
+ }
176
+ async function refreshTokens(config, refreshToken, realmId) {
177
+ const env = config.environment || "production";
178
+ const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
179
+ const response = await fetch(ENDPOINTS[env].token, {
180
+ method: "POST",
181
+ headers: {
182
+ "Content-Type": "application/x-www-form-urlencoded",
183
+ Authorization: `Basic ${credentials}`,
184
+ Accept: "application/json"
185
+ },
186
+ body: new URLSearchParams({
187
+ grant_type: "refresh_token",
188
+ refresh_token: refreshToken
189
+ }).toString()
190
+ });
191
+ if (!response.ok) {
192
+ const errorData = await response.json().catch(() => ({}));
193
+ throw new QuickBooksError(
194
+ `Failed to refresh tokens: ${response.status}`,
195
+ QB_ERROR_CODES.REFRESH_FAILED,
196
+ response.status,
197
+ errorData
198
+ );
199
+ }
200
+ const data = await response.json();
201
+ return {
202
+ access_token: data.access_token,
203
+ refresh_token: data.refresh_token,
204
+ realm_id: realmId,
205
+ expires_at: calculateTokenExpiry(data.expires_in),
206
+ token_type: data.token_type
207
+ };
208
+ }
209
+ async function revokeTokens(config, token) {
210
+ const env = config.environment || "production";
211
+ const credentials = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
212
+ const response = await fetch(ENDPOINTS[env].revoke, {
213
+ method: "POST",
214
+ headers: {
215
+ "Content-Type": "application/json",
216
+ Authorization: `Basic ${credentials}`,
217
+ Accept: "application/json"
218
+ },
219
+ body: JSON.stringify({ token })
220
+ });
221
+ if (!response.ok) {
222
+ const errorData = await response.json().catch(() => ({}));
223
+ throw new QuickBooksError(
224
+ `Failed to revoke tokens: ${response.status}`,
225
+ QB_ERROR_CODES.API_ERROR,
226
+ response.status,
227
+ errorData
228
+ );
229
+ }
230
+ }
231
+ function calculateTokenExpiry(expiresIn) {
232
+ return Math.floor(Date.now() / 1e3) + expiresIn;
233
+ }
234
+ function isTokenExpired(expiresAt, bufferSeconds = 300) {
235
+ const now = Math.floor(Date.now() / 1e3);
236
+ return now >= expiresAt - bufferSeconds;
237
+ }
238
+ function generateState() {
239
+ const array = new Uint8Array(16);
240
+ crypto.getRandomValues(array);
241
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
242
+ }
243
+
244
+ // src/client.ts
245
+ var API_BASE = {
246
+ sandbox: "https://sandbox-quickbooks.api.intuit.com",
247
+ production: "https://quickbooks.api.intuit.com"
248
+ };
249
+ var RATE_LIMIT = 500;
250
+ var RATE_LIMIT_WINDOW_MS = 60 * 1e3;
251
+ var MAX_RETRIES = 3;
252
+ var INITIAL_RETRY_DELAY_MS = 1e3;
253
+ var QuickBooksClient = class {
254
+ config;
255
+ tokenStore;
256
+ requestTimestamps = [];
257
+ onLog;
258
+ constructor(options) {
259
+ this.validateConfig(options);
260
+ this.config = options;
261
+ this.tokenStore = options.tokenStore;
262
+ this.onLog = options.onLog;
263
+ }
264
+ validateConfig(options) {
265
+ if (!options.clientId) {
266
+ throw new QuickBooksError("clientId is required", QB_ERROR_CODES.INVALID_CONFIG);
267
+ }
268
+ if (!options.clientSecret) {
269
+ throw new QuickBooksError("clientSecret is required", QB_ERROR_CODES.INVALID_CONFIG);
270
+ }
271
+ if (!options.redirectUri) {
272
+ throw new QuickBooksError("redirectUri is required", QB_ERROR_CODES.INVALID_CONFIG);
273
+ }
274
+ if (!options.tokenStore) {
275
+ throw new QuickBooksError("tokenStore is required", QB_ERROR_CODES.INVALID_CONFIG);
276
+ }
277
+ }
278
+ log(level, message, data) {
279
+ if (this.onLog) {
280
+ this.onLog(level, message, data);
281
+ }
282
+ }
283
+ /**
284
+ * Rate limiting - ensures we don't exceed 500 requests/minute
285
+ */
286
+ async checkRateLimit() {
287
+ const now = Date.now();
288
+ this.requestTimestamps = this.requestTimestamps.filter(
289
+ (ts) => now - ts < RATE_LIMIT_WINDOW_MS
290
+ );
291
+ if (this.requestTimestamps.length >= RATE_LIMIT) {
292
+ const oldestTimestamp = this.requestTimestamps[0];
293
+ const waitTime = RATE_LIMIT_WINDOW_MS - (now - oldestTimestamp) + 100;
294
+ this.log("warn", `Rate limit reached, waiting ${waitTime}ms`);
295
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
296
+ }
297
+ this.requestTimestamps.push(Date.now());
298
+ }
299
+ /**
300
+ * Get current tokens, refreshing if necessary
301
+ */
302
+ async getValidTokens() {
303
+ const tokens = await this.tokenStore.getTokens();
304
+ if (!tokens) {
305
+ throw new QuickBooksError(
306
+ "No tokens found - please connect to QuickBooks first",
307
+ QB_ERROR_CODES.UNAUTHORIZED
308
+ );
309
+ }
310
+ if (isTokenExpired(tokens.expires_at)) {
311
+ this.log("info", "Token expired, refreshing...");
312
+ try {
313
+ const newTokens = await refreshTokens(
314
+ this.config,
315
+ tokens.refresh_token,
316
+ tokens.realm_id
317
+ );
318
+ await this.tokenStore.storeTokens(newTokens);
319
+ this.log("info", "Token refreshed successfully");
320
+ return newTokens;
321
+ } catch (error) {
322
+ if (error instanceof QuickBooksError && error.status === 401) {
323
+ this.log("warn", "Refresh token invalid, clearing tokens");
324
+ await this.tokenStore.clearTokens();
325
+ }
326
+ throw error;
327
+ }
328
+ }
329
+ return tokens;
330
+ }
331
+ /**
332
+ * Make an authenticated API request with retry logic
333
+ */
334
+ async request(method, endpoint, body, retryCount = 0) {
335
+ await this.checkRateLimit();
336
+ const tokens = await this.getValidTokens();
337
+ const env = this.config.environment || "production";
338
+ const baseUrl = API_BASE[env];
339
+ const url = `${baseUrl}/v3/company/${tokens.realm_id}${endpoint}`;
340
+ this.log("debug", `${method} ${endpoint}`, { body });
341
+ const headers = {
342
+ Authorization: `Bearer ${tokens.access_token}`,
343
+ Accept: "application/json"
344
+ };
345
+ if (body) {
346
+ headers["Content-Type"] = "application/json";
347
+ }
348
+ try {
349
+ const response = await fetch(url, {
350
+ method,
351
+ headers,
352
+ body: body ? JSON.stringify(body) : void 0
353
+ });
354
+ if (response.status === 429 && retryCount < MAX_RETRIES) {
355
+ 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})`);
358
+ await new Promise((resolve) => setTimeout(resolve, delay));
359
+ return this.request(method, endpoint, body, retryCount + 1);
360
+ }
361
+ if (response.status === 401 && retryCount < 1) {
362
+ this.log("warn", "Got 401, attempting token refresh");
363
+ const tokens2 = await this.tokenStore.getTokens();
364
+ if (tokens2) {
365
+ const newTokens = await refreshTokens(
366
+ this.config,
367
+ tokens2.refresh_token,
368
+ tokens2.realm_id
369
+ );
370
+ await this.tokenStore.storeTokens(newTokens);
371
+ return this.request(method, endpoint, body, retryCount + 1);
372
+ }
373
+ }
374
+ if (!response.ok) {
375
+ const errorData = await response.json().catch(() => ({}));
376
+ throw { status: response.status, ...errorData };
377
+ }
378
+ return await response.json();
379
+ } catch (error) {
380
+ throw handleQuickBooksError(error);
381
+ }
382
+ }
383
+ /**
384
+ * Execute a query using QuickBooks Query Language
385
+ */
386
+ async query(sql) {
387
+ const response = await this.request("POST", "/query", void 0);
388
+ const tokens = await this.getValidTokens();
389
+ const env = this.config.environment || "production";
390
+ const baseUrl = API_BASE[env];
391
+ const url = `${baseUrl}/v3/company/${tokens.realm_id}/query`;
392
+ await this.checkRateLimit();
393
+ const fetchResponse = await fetch(url, {
394
+ method: "POST",
395
+ headers: {
396
+ Authorization: `Bearer ${tokens.access_token}`,
397
+ Accept: "application/json",
398
+ "Content-Type": "application/text"
399
+ },
400
+ body: sql
401
+ });
402
+ if (!fetchResponse.ok) {
403
+ const errorData = await fetchResponse.json().catch(() => ({}));
404
+ throw handleQuickBooksError({ status: fetchResponse.status, ...errorData });
405
+ }
406
+ const data = await fetchResponse.json();
407
+ const keys = Object.keys(data.QueryResponse).filter(
408
+ (k) => !["startPosition", "maxResults", "totalCount"].includes(k)
409
+ );
410
+ const entityKey = keys[0];
411
+ return entityKey ? data.QueryResponse[entityKey] : [];
412
+ }
413
+ // ============================================
414
+ // Invoice Methods
415
+ // ============================================
416
+ async getInvoice(id) {
417
+ const response = await this.request("GET", `/invoice/${id}`);
418
+ return response.Invoice;
419
+ }
420
+ async getInvoices(where) {
421
+ const sql = where ? `SELECT * FROM Invoice WHERE ${where}` : "SELECT * FROM Invoice";
422
+ return this.query(sql);
423
+ }
424
+ async createInvoice(invoice) {
425
+ const response = await this.request("POST", "/invoice", invoice);
426
+ return response.Invoice;
427
+ }
428
+ async updateInvoice(invoice) {
429
+ const response = await this.request("POST", "/invoice", invoice);
430
+ return response.Invoice;
431
+ }
432
+ async deleteInvoice(id, syncToken) {
433
+ await this.request("POST", "/invoice", {
434
+ Id: id,
435
+ SyncToken: syncToken
436
+ });
437
+ }
438
+ // ============================================
439
+ // Customer Methods
440
+ // ============================================
441
+ async getCustomer(id) {
442
+ const response = await this.request("GET", `/customer/${id}`);
443
+ return response.Customer;
444
+ }
445
+ async getCustomers(where) {
446
+ const sql = where ? `SELECT * FROM Customer WHERE ${where}` : "SELECT * FROM Customer";
447
+ return this.query(sql);
448
+ }
449
+ async createCustomer(customer) {
450
+ const response = await this.request("POST", "/customer", customer);
451
+ return response.Customer;
452
+ }
453
+ async updateCustomer(customer) {
454
+ const response = await this.request("POST", "/customer", customer);
455
+ return response.Customer;
456
+ }
457
+ // ============================================
458
+ // Payment Methods
459
+ // ============================================
460
+ async getPayment(id) {
461
+ const response = await this.request("GET", `/payment/${id}`);
462
+ return response.Payment;
463
+ }
464
+ async getPayments(where) {
465
+ const sql = where ? `SELECT * FROM Payment WHERE ${where}` : "SELECT * FROM Payment";
466
+ return this.query(sql);
467
+ }
468
+ async createPayment(payment) {
469
+ const response = await this.request("POST", "/payment", payment);
470
+ return response.Payment;
471
+ }
472
+ // ============================================
473
+ // Account Methods
474
+ // ============================================
475
+ async getAccount(id) {
476
+ const response = await this.request("GET", `/account/${id}`);
477
+ return response.Account;
478
+ }
479
+ async getAccounts(where) {
480
+ const sql = where ? `SELECT * FROM Account WHERE ${where}` : "SELECT * FROM Account WHERE Active = true";
481
+ return this.query(sql);
482
+ }
483
+ // ============================================
484
+ // Vendor Methods
485
+ // ============================================
486
+ async getVendor(id) {
487
+ const response = await this.request("GET", `/vendor/${id}`);
488
+ return response.Vendor;
489
+ }
490
+ async getVendors(where) {
491
+ const sql = where ? `SELECT * FROM Vendor WHERE ${where}` : "SELECT * FROM Vendor";
492
+ return this.query(sql);
493
+ }
494
+ async createVendor(vendor) {
495
+ const response = await this.request("POST", "/vendor", vendor);
496
+ return response.Vendor;
497
+ }
498
+ async updateVendor(vendor) {
499
+ const response = await this.request("POST", "/vendor", vendor);
500
+ return response.Vendor;
501
+ }
502
+ // ============================================
503
+ // Bill Methods
504
+ // ============================================
505
+ async getBill(id) {
506
+ const response = await this.request("GET", `/bill/${id}`);
507
+ return response.Bill;
508
+ }
509
+ async getBills(where) {
510
+ const sql = where ? `SELECT * FROM Bill WHERE ${where}` : "SELECT * FROM Bill";
511
+ return this.query(sql);
512
+ }
513
+ async createBill(bill) {
514
+ const response = await this.request("POST", "/bill", bill);
515
+ return response.Bill;
516
+ }
517
+ async updateBill(bill) {
518
+ const response = await this.request("POST", "/bill", bill);
519
+ return response.Bill;
520
+ }
521
+ // ============================================
522
+ // Item Methods
523
+ // ============================================
524
+ async getItem(id) {
525
+ const response = await this.request("GET", `/item/${id}`);
526
+ return response.Item;
527
+ }
528
+ async getItems(where) {
529
+ const sql = where ? `SELECT * FROM Item WHERE ${where}` : "SELECT * FROM Item WHERE Active = true";
530
+ return this.query(sql);
531
+ }
532
+ async createItem(item) {
533
+ const response = await this.request("POST", "/item", item);
534
+ return response.Item;
535
+ }
536
+ async updateItem(item) {
537
+ const response = await this.request("POST", "/item", item);
538
+ return response.Item;
539
+ }
540
+ // ============================================
541
+ // Utility Methods
542
+ // ============================================
543
+ /**
544
+ * Get company info
545
+ */
546
+ async getCompanyInfo() {
547
+ const tokens = await this.getValidTokens();
548
+ const response = await this.request(
549
+ "GET",
550
+ `/companyinfo/${tokens.realm_id}`
551
+ );
552
+ return response.CompanyInfo;
553
+ }
554
+ /**
555
+ * Check if connected to QuickBooks
556
+ */
557
+ async isConnected() {
558
+ try {
559
+ const tokens = await this.tokenStore.getTokens();
560
+ return tokens !== null;
561
+ } catch {
562
+ return false;
563
+ }
564
+ }
565
+ /**
566
+ * Check connection status with details
567
+ */
568
+ async getConnectionStatus() {
569
+ const tokens = await this.tokenStore.getTokens();
570
+ if (!tokens) {
571
+ return { isConnected: false, needsRefresh: false };
572
+ }
573
+ return {
574
+ isConnected: true,
575
+ needsRefresh: isTokenExpired(tokens.expires_at),
576
+ realmId: tokens.realm_id,
577
+ expiresAt: tokens.expires_at
578
+ };
579
+ }
580
+ };
581
+ // Annotate the CommonJS export names for ESM import in node:
582
+ 0 && (module.exports = {
583
+ QB_ERROR_CODES,
584
+ QuickBooksClient,
585
+ QuickBooksError,
586
+ calculateTokenExpiry,
587
+ exchangeCodeForTokens,
588
+ generateAuthUrl,
589
+ generateState,
590
+ handleQuickBooksError,
591
+ isTokenExpired,
592
+ refreshTokens,
593
+ revokeTokens
594
+ });