@happyvertical/accounting 0.74.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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/dist/chunks/OAuthClient-fC3cd77X.js +17475 -0
- package/dist/chunks/OAuthClient-fC3cd77X.js.map +1 -0
- package/dist/chunks/index-D0bqSiCo.js +893 -0
- package/dist/chunks/index-D0bqSiCo.js.map +1 -0
- package/dist/chunks/index-DO-cM79R.js +772 -0
- package/dist/chunks/index-DO-cM79R.js.map +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.d.ts.map +1 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/quickbooks/index.d.ts +43 -0
- package/dist/providers/quickbooks/index.d.ts.map +1 -0
- package/dist/providers/stripe/index.d.ts +26 -0
- package/dist/providers/stripe/index.d.ts.map +1 -0
- package/dist/types.d.ts +670 -0
- package/dist/types.d.ts.map +1 -0
- package/metadata.json +30 -0
- package/package.json +58 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
function formatLocalDate(date) {
|
|
3
|
+
const year = date.getFullYear();
|
|
4
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
5
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
6
|
+
return `${year}-${month}-${day}`;
|
|
7
|
+
}
|
|
8
|
+
function sleep(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
function valuesEqual(a, b) {
|
|
12
|
+
if (a === b) return true;
|
|
13
|
+
if (a instanceof Date && b instanceof Date) {
|
|
14
|
+
return a.getTime() === b.getTime();
|
|
15
|
+
}
|
|
16
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
17
|
+
return Math.abs(a - b) < 0.01;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
class QuickBooksProvider {
|
|
22
|
+
type = "quickbooks";
|
|
23
|
+
options;
|
|
24
|
+
accessToken = null;
|
|
25
|
+
tokenExpiresAt = null;
|
|
26
|
+
customers;
|
|
27
|
+
invoices;
|
|
28
|
+
vendors;
|
|
29
|
+
bills;
|
|
30
|
+
payments;
|
|
31
|
+
audit;
|
|
32
|
+
webhooks;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = {
|
|
35
|
+
timeout: 3e4,
|
|
36
|
+
maxRetries: 3,
|
|
37
|
+
...options
|
|
38
|
+
};
|
|
39
|
+
this.customers = new QuickBooksCustomerOperations(this);
|
|
40
|
+
this.invoices = new QuickBooksInvoiceOperations(this);
|
|
41
|
+
this.vendors = new QuickBooksVendorOperations(this);
|
|
42
|
+
this.bills = new QuickBooksBillOperations(this);
|
|
43
|
+
this.payments = new QuickBooksPaymentOperations(this);
|
|
44
|
+
this.audit = new QuickBooksAuditOperations(this);
|
|
45
|
+
this.webhooks = new QuickBooksWebhookOperations(this.options);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the QBO API base URL
|
|
49
|
+
*/
|
|
50
|
+
get baseUrl() {
|
|
51
|
+
return this.options.environment === "production" ? "https://quickbooks.api.intuit.com" : "https://sandbox-quickbooks.api.intuit.com";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the company/realm ID
|
|
55
|
+
*/
|
|
56
|
+
get realmId() {
|
|
57
|
+
return this.options.realmId;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ensure we have a valid access token, refreshing if needed
|
|
61
|
+
*/
|
|
62
|
+
async ensureAccessToken() {
|
|
63
|
+
if (this.accessToken && this.tokenExpiresAt) {
|
|
64
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
65
|
+
if ((/* @__PURE__ */ new Date()).getTime() < this.tokenExpiresAt.getTime() - bufferMs) {
|
|
66
|
+
return this.accessToken;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const tokens = await this.refreshAccessToken();
|
|
70
|
+
this.accessToken = tokens.accessToken;
|
|
71
|
+
this.tokenExpiresAt = tokens.expiresAt;
|
|
72
|
+
this.options.refreshToken = tokens.refreshToken;
|
|
73
|
+
if (this.options.onTokenRefresh) {
|
|
74
|
+
await this.options.onTokenRefresh(tokens);
|
|
75
|
+
}
|
|
76
|
+
return this.accessToken;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Refresh the OAuth access token
|
|
80
|
+
*/
|
|
81
|
+
async refreshAccessToken() {
|
|
82
|
+
const OAuthClient = (await import("./OAuthClient-fC3cd77X.js").then((n) => n.O)).default;
|
|
83
|
+
const redirectUri = this.options.redirectUri || "https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl";
|
|
84
|
+
const oauthClient = new OAuthClient({
|
|
85
|
+
clientId: this.options.clientId,
|
|
86
|
+
clientSecret: this.options.clientSecret,
|
|
87
|
+
environment: this.options.environment === "production" ? "production" : "sandbox",
|
|
88
|
+
redirectUri
|
|
89
|
+
});
|
|
90
|
+
oauthClient.setToken({
|
|
91
|
+
refresh_token: this.options.refreshToken,
|
|
92
|
+
access_token: this.accessToken || ""
|
|
93
|
+
});
|
|
94
|
+
const response = await oauthClient.refresh();
|
|
95
|
+
const token = response.getJson();
|
|
96
|
+
return {
|
|
97
|
+
accessToken: token.access_token,
|
|
98
|
+
refreshToken: token.refresh_token,
|
|
99
|
+
expiresAt: new Date(Date.now() + token.expires_in * 1e3),
|
|
100
|
+
tokenType: token.token_type
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Make an authenticated request to the QBO API with timeout and retry support
|
|
105
|
+
*/
|
|
106
|
+
async request(method, endpoint, body) {
|
|
107
|
+
const accessToken = await this.ensureAccessToken();
|
|
108
|
+
const url = `${this.baseUrl}/v3/company/${this.realmId}/${endpoint}`;
|
|
109
|
+
const timeout = this.options.timeout || 3e4;
|
|
110
|
+
const maxRetries = this.options.maxRetries || 3;
|
|
111
|
+
let lastError = null;
|
|
112
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
113
|
+
try {
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
method,
|
|
119
|
+
headers: {
|
|
120
|
+
Authorization: `Bearer ${accessToken}`,
|
|
121
|
+
Accept: "application/json",
|
|
122
|
+
"Content-Type": "application/json"
|
|
123
|
+
},
|
|
124
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
125
|
+
signal: controller.signal
|
|
126
|
+
});
|
|
127
|
+
clearTimeout(timeoutId);
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
const errorText = await response.text();
|
|
130
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`QBO API error (${response.status}): ${errorText}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`QBO API error (${response.status}): ${errorText}`);
|
|
136
|
+
}
|
|
137
|
+
return await response.json();
|
|
138
|
+
} finally {
|
|
139
|
+
clearTimeout(timeoutId);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
143
|
+
const errorMessage = lastError.message;
|
|
144
|
+
const statusMatch = errorMessage.match(/QBO API error \((\d+)\)/);
|
|
145
|
+
if (statusMatch) {
|
|
146
|
+
const status = Number.parseInt(statusMatch[1], 10);
|
|
147
|
+
if (status >= 400 && status < 500 && status !== 429) {
|
|
148
|
+
throw lastError;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (attempt < maxRetries) {
|
|
152
|
+
const backoffMs = 2 ** attempt * 1e3;
|
|
153
|
+
await sleep(backoffMs);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw lastError || new Error("Request failed after retries");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Execute a QBO query with pagination support
|
|
161
|
+
*/
|
|
162
|
+
async queryAll(entity, whereClause, maxResults = 1e3) {
|
|
163
|
+
const results = [];
|
|
164
|
+
let startPosition = 1;
|
|
165
|
+
const pageSize = 100;
|
|
166
|
+
while (results.length < maxResults) {
|
|
167
|
+
let query = `SELECT * FROM ${entity}`;
|
|
168
|
+
if (whereClause) {
|
|
169
|
+
query += ` WHERE ${whereClause}`;
|
|
170
|
+
}
|
|
171
|
+
query += ` MAXRESULTS ${pageSize} STARTPOSITION ${startPosition}`;
|
|
172
|
+
const response = await this.request("GET", `query?query=${encodeURIComponent(query)}`);
|
|
173
|
+
const items = response.QueryResponse[entity] || [];
|
|
174
|
+
results.push(...items);
|
|
175
|
+
if (items.length < pageSize) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
startPosition += pageSize;
|
|
179
|
+
}
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
class QuickBooksCustomerOperations {
|
|
184
|
+
constructor(provider) {
|
|
185
|
+
this.provider = provider;
|
|
186
|
+
}
|
|
187
|
+
async push(customer) {
|
|
188
|
+
const qboCustomer = mapCustomerToQBO(customer);
|
|
189
|
+
const response = await this.provider.request(
|
|
190
|
+
"POST",
|
|
191
|
+
"customer",
|
|
192
|
+
qboCustomer
|
|
193
|
+
);
|
|
194
|
+
return {
|
|
195
|
+
action: "created",
|
|
196
|
+
externalId: response.Customer.Id,
|
|
197
|
+
syncedAt: /* @__PURE__ */ new Date()
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async pull(externalId) {
|
|
201
|
+
const response = await this.provider.request(
|
|
202
|
+
"GET",
|
|
203
|
+
`customer/${externalId}`
|
|
204
|
+
);
|
|
205
|
+
return mapQBOToCustomer(response.Customer);
|
|
206
|
+
}
|
|
207
|
+
async list(options) {
|
|
208
|
+
const limit = options?.limit || 1e3;
|
|
209
|
+
const customers = await this.provider.queryAll(
|
|
210
|
+
"Customer",
|
|
211
|
+
void 0,
|
|
212
|
+
limit
|
|
213
|
+
);
|
|
214
|
+
return customers.map(mapQBOToCustomer);
|
|
215
|
+
}
|
|
216
|
+
async sync(customer) {
|
|
217
|
+
if (customer.externalId) {
|
|
218
|
+
const existing = await this.provider.request(
|
|
219
|
+
"GET",
|
|
220
|
+
`customer/${customer.externalId}`
|
|
221
|
+
);
|
|
222
|
+
const qboCustomer = {
|
|
223
|
+
...mapCustomerToQBO(customer),
|
|
224
|
+
Id: customer.externalId,
|
|
225
|
+
SyncToken: existing.Customer.SyncToken
|
|
226
|
+
};
|
|
227
|
+
await this.provider.request("POST", "customer", qboCustomer);
|
|
228
|
+
return {
|
|
229
|
+
action: "updated",
|
|
230
|
+
externalId: customer.externalId,
|
|
231
|
+
syncedAt: /* @__PURE__ */ new Date()
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return this.push(customer);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
class QuickBooksInvoiceOperations {
|
|
238
|
+
constructor(provider) {
|
|
239
|
+
this.provider = provider;
|
|
240
|
+
}
|
|
241
|
+
async push(invoice) {
|
|
242
|
+
const qboInvoice = mapInvoiceToQBO(invoice);
|
|
243
|
+
const response = await this.provider.request(
|
|
244
|
+
"POST",
|
|
245
|
+
"invoice",
|
|
246
|
+
qboInvoice
|
|
247
|
+
);
|
|
248
|
+
return {
|
|
249
|
+
action: "created",
|
|
250
|
+
externalId: response.Invoice.Id,
|
|
251
|
+
syncedAt: /* @__PURE__ */ new Date()
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async pull(externalId) {
|
|
255
|
+
const response = await this.provider.request(
|
|
256
|
+
"GET",
|
|
257
|
+
`invoice/${externalId}`
|
|
258
|
+
);
|
|
259
|
+
return mapQBOToInvoice(response.Invoice);
|
|
260
|
+
}
|
|
261
|
+
async list(options) {
|
|
262
|
+
const limit = options?.limit || 1e3;
|
|
263
|
+
let whereClause;
|
|
264
|
+
if (options?.startDate) {
|
|
265
|
+
whereClause = `TxnDate >= '${formatLocalDate(options.startDate)}'`;
|
|
266
|
+
if (options?.endDate) {
|
|
267
|
+
whereClause += ` AND TxnDate <= '${formatLocalDate(options.endDate)}'`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const invoices = await this.provider.queryAll(
|
|
271
|
+
"Invoice",
|
|
272
|
+
whereClause,
|
|
273
|
+
limit
|
|
274
|
+
);
|
|
275
|
+
return invoices.map(mapQBOToInvoice);
|
|
276
|
+
}
|
|
277
|
+
async sync(invoice) {
|
|
278
|
+
if (invoice.externalId) {
|
|
279
|
+
const existing = await this.provider.request(
|
|
280
|
+
"GET",
|
|
281
|
+
`invoice/${invoice.externalId}`
|
|
282
|
+
);
|
|
283
|
+
const qboInvoice = {
|
|
284
|
+
...mapInvoiceToQBO(invoice),
|
|
285
|
+
Id: invoice.externalId,
|
|
286
|
+
SyncToken: existing.Invoice.SyncToken
|
|
287
|
+
};
|
|
288
|
+
await this.provider.request("POST", "invoice", qboInvoice);
|
|
289
|
+
return {
|
|
290
|
+
action: "updated",
|
|
291
|
+
externalId: invoice.externalId,
|
|
292
|
+
syncedAt: /* @__PURE__ */ new Date()
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
return this.push(invoice);
|
|
296
|
+
}
|
|
297
|
+
async send(externalId) {
|
|
298
|
+
await this.provider.request("POST", `invoice/${externalId}/send`);
|
|
299
|
+
}
|
|
300
|
+
async void(externalId) {
|
|
301
|
+
const existing = await this.provider.request(
|
|
302
|
+
"GET",
|
|
303
|
+
`invoice/${externalId}`
|
|
304
|
+
);
|
|
305
|
+
await this.provider.request("POST", "invoice", {
|
|
306
|
+
Id: externalId,
|
|
307
|
+
SyncToken: existing.Invoice.SyncToken,
|
|
308
|
+
sparse: true,
|
|
309
|
+
// QBO doesn't have a Void field - you delete the invoice instead
|
|
310
|
+
// or create a credit memo. For now, we'll use sparse update to mark private note
|
|
311
|
+
PrivateNote: "VOIDED"
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
class QuickBooksVendorOperations {
|
|
316
|
+
constructor(provider) {
|
|
317
|
+
this.provider = provider;
|
|
318
|
+
}
|
|
319
|
+
async push(vendor) {
|
|
320
|
+
const qboVendor = mapVendorToQBO(vendor);
|
|
321
|
+
const response = await this.provider.request(
|
|
322
|
+
"POST",
|
|
323
|
+
"vendor",
|
|
324
|
+
qboVendor
|
|
325
|
+
);
|
|
326
|
+
return {
|
|
327
|
+
action: "created",
|
|
328
|
+
externalId: response.Vendor.Id,
|
|
329
|
+
syncedAt: /* @__PURE__ */ new Date()
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async pull(externalId) {
|
|
333
|
+
const response = await this.provider.request(
|
|
334
|
+
"GET",
|
|
335
|
+
`vendor/${externalId}`
|
|
336
|
+
);
|
|
337
|
+
return mapQBOToVendor(response.Vendor);
|
|
338
|
+
}
|
|
339
|
+
async list(options) {
|
|
340
|
+
const limit = options?.limit || 1e3;
|
|
341
|
+
const vendors = await this.provider.queryAll(
|
|
342
|
+
"Vendor",
|
|
343
|
+
void 0,
|
|
344
|
+
limit
|
|
345
|
+
);
|
|
346
|
+
return vendors.map(mapQBOToVendor);
|
|
347
|
+
}
|
|
348
|
+
async sync(vendor) {
|
|
349
|
+
if (vendor.externalId) {
|
|
350
|
+
const existing = await this.provider.request(
|
|
351
|
+
"GET",
|
|
352
|
+
`vendor/${vendor.externalId}`
|
|
353
|
+
);
|
|
354
|
+
const qboVendor = {
|
|
355
|
+
...mapVendorToQBO(vendor),
|
|
356
|
+
Id: vendor.externalId,
|
|
357
|
+
SyncToken: existing.Vendor.SyncToken
|
|
358
|
+
};
|
|
359
|
+
await this.provider.request("POST", "vendor", qboVendor);
|
|
360
|
+
return {
|
|
361
|
+
action: "updated",
|
|
362
|
+
externalId: vendor.externalId,
|
|
363
|
+
syncedAt: /* @__PURE__ */ new Date()
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return this.push(vendor);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
class QuickBooksBillOperations {
|
|
370
|
+
constructor(provider) {
|
|
371
|
+
this.provider = provider;
|
|
372
|
+
}
|
|
373
|
+
async push(bill) {
|
|
374
|
+
const qboBill = mapBillToQBO(bill);
|
|
375
|
+
const response = await this.provider.request(
|
|
376
|
+
"POST",
|
|
377
|
+
"bill",
|
|
378
|
+
qboBill
|
|
379
|
+
);
|
|
380
|
+
return {
|
|
381
|
+
action: "created",
|
|
382
|
+
externalId: response.Bill.Id,
|
|
383
|
+
syncedAt: /* @__PURE__ */ new Date()
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async pull(externalId) {
|
|
387
|
+
const response = await this.provider.request(
|
|
388
|
+
"GET",
|
|
389
|
+
`bill/${externalId}`
|
|
390
|
+
);
|
|
391
|
+
return mapQBOToBill(response.Bill);
|
|
392
|
+
}
|
|
393
|
+
async list(options) {
|
|
394
|
+
const limit = options?.limit || 1e3;
|
|
395
|
+
let whereClause;
|
|
396
|
+
if (options?.startDate) {
|
|
397
|
+
whereClause = `TxnDate >= '${formatLocalDate(options.startDate)}'`;
|
|
398
|
+
if (options?.endDate) {
|
|
399
|
+
whereClause += ` AND TxnDate <= '${formatLocalDate(options.endDate)}'`;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const bills = await this.provider.queryAll(
|
|
403
|
+
"Bill",
|
|
404
|
+
whereClause,
|
|
405
|
+
limit
|
|
406
|
+
);
|
|
407
|
+
return bills.map(mapQBOToBill);
|
|
408
|
+
}
|
|
409
|
+
async sync(bill) {
|
|
410
|
+
if (bill.externalId) {
|
|
411
|
+
const existing = await this.provider.request(
|
|
412
|
+
"GET",
|
|
413
|
+
`bill/${bill.externalId}`
|
|
414
|
+
);
|
|
415
|
+
const qboBill = {
|
|
416
|
+
...mapBillToQBO(bill),
|
|
417
|
+
Id: bill.externalId,
|
|
418
|
+
SyncToken: existing.Bill.SyncToken
|
|
419
|
+
};
|
|
420
|
+
await this.provider.request("POST", "bill", qboBill);
|
|
421
|
+
return {
|
|
422
|
+
action: "updated",
|
|
423
|
+
externalId: bill.externalId,
|
|
424
|
+
syncedAt: /* @__PURE__ */ new Date()
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return this.push(bill);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
class QuickBooksPaymentOperations {
|
|
431
|
+
constructor(provider) {
|
|
432
|
+
this.provider = provider;
|
|
433
|
+
}
|
|
434
|
+
async pull(externalId) {
|
|
435
|
+
const response = await this.provider.request(
|
|
436
|
+
"GET",
|
|
437
|
+
`payment/${externalId}`
|
|
438
|
+
);
|
|
439
|
+
return mapQBOToPayment(response.Payment);
|
|
440
|
+
}
|
|
441
|
+
async list(options) {
|
|
442
|
+
const limit = options?.limit || 1e3;
|
|
443
|
+
let whereClause;
|
|
444
|
+
if (options?.startDate) {
|
|
445
|
+
whereClause = `TxnDate >= '${formatLocalDate(options.startDate)}'`;
|
|
446
|
+
if (options?.endDate) {
|
|
447
|
+
whereClause += ` AND TxnDate <= '${formatLocalDate(options.endDate)}'`;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const payments = await this.provider.queryAll(
|
|
451
|
+
"Payment",
|
|
452
|
+
whereClause,
|
|
453
|
+
limit
|
|
454
|
+
);
|
|
455
|
+
return payments.map(mapQBOToPayment);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
class QuickBooksAuditOperations {
|
|
459
|
+
constructor(provider) {
|
|
460
|
+
this.provider = provider;
|
|
461
|
+
}
|
|
462
|
+
async reconcileCustomers(locals) {
|
|
463
|
+
const externals = await this.provider.customers.list({ limit: 1e4 });
|
|
464
|
+
return reconcileRecords(
|
|
465
|
+
locals,
|
|
466
|
+
externals,
|
|
467
|
+
(l) => l.externalId,
|
|
468
|
+
(e) => e.externalId,
|
|
469
|
+
compareCustomer
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
async reconcileInvoices(locals, dateRange) {
|
|
473
|
+
const externals = await this.provider.invoices.list({
|
|
474
|
+
limit: 1e4,
|
|
475
|
+
startDate: dateRange?.start,
|
|
476
|
+
endDate: dateRange?.end
|
|
477
|
+
});
|
|
478
|
+
return reconcileRecords(
|
|
479
|
+
locals,
|
|
480
|
+
externals,
|
|
481
|
+
(l) => l.externalId,
|
|
482
|
+
(e) => e.externalId,
|
|
483
|
+
compareInvoice
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
async reconcileVendors(locals) {
|
|
487
|
+
const externals = await this.provider.vendors.list({ limit: 1e4 });
|
|
488
|
+
return reconcileRecords(
|
|
489
|
+
locals,
|
|
490
|
+
externals,
|
|
491
|
+
(l) => l.externalId,
|
|
492
|
+
(e) => e.externalId,
|
|
493
|
+
compareVendor
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
async reconcileBills(locals, dateRange) {
|
|
497
|
+
const externals = await this.provider.bills.list({
|
|
498
|
+
limit: 1e4,
|
|
499
|
+
startDate: dateRange?.start,
|
|
500
|
+
endDate: dateRange?.end
|
|
501
|
+
});
|
|
502
|
+
return reconcileRecords(
|
|
503
|
+
locals,
|
|
504
|
+
externals,
|
|
505
|
+
(l) => l.externalId,
|
|
506
|
+
(e) => e.externalId,
|
|
507
|
+
compareBill
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
async reconcilePayments(locals, dateRange) {
|
|
511
|
+
const externals = await this.provider.payments.list({
|
|
512
|
+
limit: 1e4,
|
|
513
|
+
startDate: dateRange?.start,
|
|
514
|
+
endDate: dateRange?.end
|
|
515
|
+
});
|
|
516
|
+
return reconcileRecords(
|
|
517
|
+
locals,
|
|
518
|
+
externals,
|
|
519
|
+
(l) => l.externalId,
|
|
520
|
+
(e) => e.externalId,
|
|
521
|
+
comparePayment
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function reconcileRecords(locals, externals, getLocalExternalId, getExternalId, compareFields) {
|
|
526
|
+
const externalMap = new Map(externals.map((e) => [getExternalId(e), e]));
|
|
527
|
+
const matchedExternalIds = /* @__PURE__ */ new Set();
|
|
528
|
+
const matched = [];
|
|
529
|
+
const localOnly = [];
|
|
530
|
+
const discrepancies = [];
|
|
531
|
+
for (const local of locals) {
|
|
532
|
+
const externalId = getLocalExternalId(local);
|
|
533
|
+
if (externalId && externalMap.has(externalId)) {
|
|
534
|
+
const external = externalMap.get(externalId);
|
|
535
|
+
matchedExternalIds.add(externalId);
|
|
536
|
+
const differences = compareFields(local, external);
|
|
537
|
+
if (differences.length === 0) {
|
|
538
|
+
matched.push({
|
|
539
|
+
local,
|
|
540
|
+
external,
|
|
541
|
+
status: "identical"
|
|
542
|
+
});
|
|
543
|
+
} else {
|
|
544
|
+
discrepancies.push({
|
|
545
|
+
local,
|
|
546
|
+
external,
|
|
547
|
+
differences
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
localOnly.push(local);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const externalOnly = externals.filter(
|
|
555
|
+
(e) => !matchedExternalIds.has(getExternalId(e))
|
|
556
|
+
);
|
|
557
|
+
return {
|
|
558
|
+
provider: "quickbooks",
|
|
559
|
+
auditedAt: /* @__PURE__ */ new Date(),
|
|
560
|
+
matched,
|
|
561
|
+
localOnly,
|
|
562
|
+
externalOnly,
|
|
563
|
+
discrepancies,
|
|
564
|
+
summary: {
|
|
565
|
+
total: locals.length + externalOnly.length,
|
|
566
|
+
matched: matched.length,
|
|
567
|
+
localOnly: localOnly.length,
|
|
568
|
+
externalOnly: externalOnly.length,
|
|
569
|
+
discrepancies: discrepancies.length
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function compareCustomer(local, external) {
|
|
574
|
+
const diffs = [];
|
|
575
|
+
if (local.name !== external.name) {
|
|
576
|
+
diffs.push({
|
|
577
|
+
field: "name",
|
|
578
|
+
localValue: local.name,
|
|
579
|
+
externalValue: external.name
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (local.email !== external.email) {
|
|
583
|
+
diffs.push({
|
|
584
|
+
field: "email",
|
|
585
|
+
localValue: local.email,
|
|
586
|
+
externalValue: external.email
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return diffs;
|
|
590
|
+
}
|
|
591
|
+
function compareInvoice(local, external) {
|
|
592
|
+
const diffs = [];
|
|
593
|
+
if (!valuesEqual(local.totalAmount, external.totalAmount)) {
|
|
594
|
+
diffs.push({
|
|
595
|
+
field: "totalAmount",
|
|
596
|
+
localValue: local.totalAmount,
|
|
597
|
+
externalValue: external.totalAmount
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (local.invoiceNumber !== external.invoiceNumber) {
|
|
601
|
+
diffs.push({
|
|
602
|
+
field: "invoiceNumber",
|
|
603
|
+
localValue: local.invoiceNumber,
|
|
604
|
+
externalValue: external.invoiceNumber
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return diffs;
|
|
608
|
+
}
|
|
609
|
+
function compareVendor(local, external) {
|
|
610
|
+
const diffs = [];
|
|
611
|
+
if (local.name !== external.name) {
|
|
612
|
+
diffs.push({
|
|
613
|
+
field: "name",
|
|
614
|
+
localValue: local.name,
|
|
615
|
+
externalValue: external.name
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (local.email !== external.email) {
|
|
619
|
+
diffs.push({
|
|
620
|
+
field: "email",
|
|
621
|
+
localValue: local.email,
|
|
622
|
+
externalValue: external.email
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return diffs;
|
|
626
|
+
}
|
|
627
|
+
function compareBill(local, external) {
|
|
628
|
+
const diffs = [];
|
|
629
|
+
if (!valuesEqual(local.totalAmount, external.totalAmount)) {
|
|
630
|
+
diffs.push({
|
|
631
|
+
field: "totalAmount",
|
|
632
|
+
localValue: local.totalAmount,
|
|
633
|
+
externalValue: external.totalAmount
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
return diffs;
|
|
637
|
+
}
|
|
638
|
+
function comparePayment(local, external) {
|
|
639
|
+
const diffs = [];
|
|
640
|
+
if (!valuesEqual(local.amount, external.amount)) {
|
|
641
|
+
diffs.push({
|
|
642
|
+
field: "amount",
|
|
643
|
+
localValue: local.amount,
|
|
644
|
+
externalValue: external.amount
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return diffs;
|
|
648
|
+
}
|
|
649
|
+
class QuickBooksWebhookOperations {
|
|
650
|
+
constructor(options) {
|
|
651
|
+
this.options = options;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Verify webhook signature using HMAC-SHA256
|
|
655
|
+
* @see https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks/verify-webhooks
|
|
656
|
+
*/
|
|
657
|
+
verify(payload, signature, secret) {
|
|
658
|
+
const verifierToken = secret || this.options.webhookVerifierToken;
|
|
659
|
+
if (!verifierToken) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
"Webhook verifier token not provided. Pass it as the secret parameter or configure webhookVerifierToken in options."
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
const hmac = createHmac("sha256", verifierToken);
|
|
665
|
+
hmac.update(payload);
|
|
666
|
+
const expectedSignature = hmac.digest("base64");
|
|
667
|
+
if (signature.length !== expectedSignature.length) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
let result = 0;
|
|
671
|
+
for (let i = 0; i < signature.length; i++) {
|
|
672
|
+
result |= signature.charCodeAt(i) ^ expectedSignature.charCodeAt(i);
|
|
673
|
+
}
|
|
674
|
+
return result === 0;
|
|
675
|
+
}
|
|
676
|
+
parse(payload) {
|
|
677
|
+
let data;
|
|
678
|
+
try {
|
|
679
|
+
data = JSON.parse(payload);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
const message = error instanceof Error ? error.message : "Unknown JSON parse error";
|
|
682
|
+
throw new Error(`Invalid QuickBooks webhook payload: ${message}`);
|
|
683
|
+
}
|
|
684
|
+
const eventNotifications = data.eventNotifications || [];
|
|
685
|
+
const firstEvent = eventNotifications[0]?.dataChangeEvent?.entities?.[0];
|
|
686
|
+
return {
|
|
687
|
+
type: firstEvent?.operation || "unknown",
|
|
688
|
+
provider: "quickbooks",
|
|
689
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
690
|
+
payload: data,
|
|
691
|
+
resourceType: mapQBOResourceType(firstEvent?.name),
|
|
692
|
+
resourceId: firstEvent?.id
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function mapQBOResourceType(name) {
|
|
697
|
+
switch (name?.toLowerCase()) {
|
|
698
|
+
case "customer":
|
|
699
|
+
return "customer";
|
|
700
|
+
case "invoice":
|
|
701
|
+
return "invoice";
|
|
702
|
+
case "payment":
|
|
703
|
+
return "payment";
|
|
704
|
+
case "vendor":
|
|
705
|
+
return "vendor";
|
|
706
|
+
case "bill":
|
|
707
|
+
return "bill";
|
|
708
|
+
default:
|
|
709
|
+
return void 0;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function mapCustomerToQBO(customer) {
|
|
713
|
+
return {
|
|
714
|
+
DisplayName: customer.name,
|
|
715
|
+
PrimaryEmailAddr: customer.email ? { Address: customer.email } : void 0,
|
|
716
|
+
PrimaryPhone: customer.phone ? { FreeFormNumber: customer.phone } : void 0,
|
|
717
|
+
BillAddr: mapAddressToQBO(customer.billingAddress),
|
|
718
|
+
ShipAddr: mapAddressToQBO(customer.shippingAddress),
|
|
719
|
+
CurrencyRef: customer.currency ? { value: customer.currency } : void 0
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function mapQBOToCustomer(qbo) {
|
|
723
|
+
return {
|
|
724
|
+
externalId: qbo.Id,
|
|
725
|
+
provider: "quickbooks",
|
|
726
|
+
syncedAt: /* @__PURE__ */ new Date(),
|
|
727
|
+
name: qbo.DisplayName,
|
|
728
|
+
email: qbo.PrimaryEmailAddr?.Address,
|
|
729
|
+
phone: qbo.PrimaryPhone?.FreeFormNumber,
|
|
730
|
+
billingAddress: mapQBOToAddress(qbo.BillAddr),
|
|
731
|
+
balance: qbo.Balance,
|
|
732
|
+
currency: qbo.CurrencyRef?.value,
|
|
733
|
+
raw: qbo
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function mapInvoiceToQBO(invoice) {
|
|
737
|
+
return {
|
|
738
|
+
CustomerRef: { value: invoice.customerExternalId || invoice.customerId },
|
|
739
|
+
DocNumber: invoice.invoiceNumber,
|
|
740
|
+
TxnDate: formatLocalDate(invoice.issueDate),
|
|
741
|
+
DueDate: formatLocalDate(invoice.dueDate),
|
|
742
|
+
Line: invoice.lineItems.map((item, idx) => ({
|
|
743
|
+
LineNum: idx + 1,
|
|
744
|
+
Description: item.description,
|
|
745
|
+
Amount: item.amount ?? item.quantity * item.unitPrice,
|
|
746
|
+
DetailType: "SalesItemLineDetail",
|
|
747
|
+
SalesItemLineDetail: {
|
|
748
|
+
Qty: item.quantity,
|
|
749
|
+
UnitPrice: item.unitPrice
|
|
750
|
+
}
|
|
751
|
+
})),
|
|
752
|
+
CurrencyRef: invoice.currency ? { value: invoice.currency } : void 0,
|
|
753
|
+
CustomerMemo: invoice.memo ? { value: invoice.memo } : void 0
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function mapQBOToInvoice(qbo) {
|
|
757
|
+
const balance = qbo.Balance ?? 0;
|
|
758
|
+
const totalAmount = qbo.TotalAmt ?? 0;
|
|
759
|
+
return {
|
|
760
|
+
externalId: qbo.Id,
|
|
761
|
+
provider: "quickbooks",
|
|
762
|
+
syncedAt: /* @__PURE__ */ new Date(),
|
|
763
|
+
invoiceNumber: qbo.DocNumber || "",
|
|
764
|
+
customerExternalId: qbo.CustomerRef?.value || "",
|
|
765
|
+
issueDate: new Date(qbo.TxnDate),
|
|
766
|
+
dueDate: new Date(qbo.DueDate),
|
|
767
|
+
subtotal: totalAmount - (qbo.TxnTaxDetail?.TotalTax ?? 0),
|
|
768
|
+
taxAmount: qbo.TxnTaxDetail?.TotalTax ?? 0,
|
|
769
|
+
totalAmount,
|
|
770
|
+
amountPaid: totalAmount - balance,
|
|
771
|
+
balance,
|
|
772
|
+
status: mapInvoiceStatus(balance, totalAmount, qbo.DueDate),
|
|
773
|
+
currency: qbo.CurrencyRef?.value || "USD",
|
|
774
|
+
raw: qbo
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function mapInvoiceStatus(balance, total, dueDate) {
|
|
778
|
+
if (balance === 0) return "paid";
|
|
779
|
+
if (balance < total) return "viewed";
|
|
780
|
+
if (new Date(dueDate) < /* @__PURE__ */ new Date()) return "overdue";
|
|
781
|
+
return "sent";
|
|
782
|
+
}
|
|
783
|
+
function mapVendorToQBO(vendor) {
|
|
784
|
+
return {
|
|
785
|
+
DisplayName: vendor.name,
|
|
786
|
+
PrimaryEmailAddr: vendor.email ? { Address: vendor.email } : void 0,
|
|
787
|
+
PrimaryPhone: vendor.phone ? { FreeFormNumber: vendor.phone } : void 0,
|
|
788
|
+
BillAddr: mapAddressToQBO(vendor.address),
|
|
789
|
+
TaxIdentifier: vendor.taxId,
|
|
790
|
+
CurrencyRef: vendor.currency ? { value: vendor.currency } : void 0
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function mapQBOToVendor(qbo) {
|
|
794
|
+
return {
|
|
795
|
+
externalId: qbo.Id,
|
|
796
|
+
provider: "quickbooks",
|
|
797
|
+
syncedAt: /* @__PURE__ */ new Date(),
|
|
798
|
+
name: qbo.DisplayName,
|
|
799
|
+
email: qbo.PrimaryEmailAddr?.Address,
|
|
800
|
+
phone: qbo.PrimaryPhone?.FreeFormNumber,
|
|
801
|
+
address: mapQBOToAddress(qbo.BillAddr),
|
|
802
|
+
balance: qbo.Balance,
|
|
803
|
+
currency: qbo.CurrencyRef?.value,
|
|
804
|
+
raw: qbo
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function mapBillToQBO(bill) {
|
|
808
|
+
return {
|
|
809
|
+
VendorRef: { value: bill.vendorExternalId || bill.vendorId },
|
|
810
|
+
DocNumber: bill.billNumber,
|
|
811
|
+
TxnDate: formatLocalDate(bill.billDate),
|
|
812
|
+
DueDate: formatLocalDate(bill.dueDate),
|
|
813
|
+
Line: bill.lineItems.map((item, idx) => ({
|
|
814
|
+
LineNum: idx + 1,
|
|
815
|
+
Description: item.description,
|
|
816
|
+
Amount: item.amount ?? item.quantity * item.unitPrice,
|
|
817
|
+
DetailType: "AccountBasedExpenseLineDetail",
|
|
818
|
+
AccountBasedExpenseLineDetail: {
|
|
819
|
+
AccountRef: item.accountCode ? { value: item.accountCode } : void 0
|
|
820
|
+
}
|
|
821
|
+
})),
|
|
822
|
+
CurrencyRef: bill.currency ? { value: bill.currency } : void 0
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function mapQBOToBill(qbo) {
|
|
826
|
+
const balance = qbo.Balance ?? 0;
|
|
827
|
+
const totalAmount = qbo.TotalAmt ?? 0;
|
|
828
|
+
return {
|
|
829
|
+
externalId: qbo.Id,
|
|
830
|
+
provider: "quickbooks",
|
|
831
|
+
syncedAt: /* @__PURE__ */ new Date(),
|
|
832
|
+
billNumber: qbo.DocNumber,
|
|
833
|
+
vendorExternalId: qbo.VendorRef?.value || "",
|
|
834
|
+
billDate: new Date(qbo.TxnDate),
|
|
835
|
+
dueDate: new Date(qbo.DueDate),
|
|
836
|
+
subtotal: totalAmount - (qbo.TxnTaxDetail?.TotalTax ?? 0),
|
|
837
|
+
taxAmount: qbo.TxnTaxDetail?.TotalTax ?? 0,
|
|
838
|
+
totalAmount,
|
|
839
|
+
amountPaid: totalAmount - balance,
|
|
840
|
+
balance,
|
|
841
|
+
status: mapBillStatus(balance, totalAmount, qbo.DueDate),
|
|
842
|
+
currency: qbo.CurrencyRef?.value || "USD",
|
|
843
|
+
raw: qbo
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function mapBillStatus(balance, total, dueDate) {
|
|
847
|
+
if (balance === 0) return "paid";
|
|
848
|
+
if (new Date(dueDate) < /* @__PURE__ */ new Date()) return "overdue";
|
|
849
|
+
return "pending";
|
|
850
|
+
}
|
|
851
|
+
function mapQBOToPayment(qbo) {
|
|
852
|
+
return {
|
|
853
|
+
externalId: qbo.Id,
|
|
854
|
+
provider: "quickbooks",
|
|
855
|
+
syncedAt: /* @__PURE__ */ new Date(),
|
|
856
|
+
amount: qbo.TotalAmt ?? 0,
|
|
857
|
+
currency: qbo.CurrencyRef?.value || "USD",
|
|
858
|
+
paidAt: new Date(qbo.TxnDate),
|
|
859
|
+
method: qbo.PaymentMethodRef?.name,
|
|
860
|
+
transactionId: qbo.PaymentRefNum,
|
|
861
|
+
invoiceExternalIds: qbo.Line?.filter((l) => l.LinkedTxn).flatMap(
|
|
862
|
+
(l) => l.LinkedTxn?.map((t) => t.TxnId) || []
|
|
863
|
+
),
|
|
864
|
+
status: "completed",
|
|
865
|
+
raw: qbo
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function mapAddressToQBO(address) {
|
|
869
|
+
if (!address) return void 0;
|
|
870
|
+
return {
|
|
871
|
+
Line1: address.street1,
|
|
872
|
+
Line2: address.street2,
|
|
873
|
+
City: address.city,
|
|
874
|
+
CountrySubDivisionCode: address.state,
|
|
875
|
+
PostalCode: address.postalCode,
|
|
876
|
+
Country: address.country
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function mapQBOToAddress(qbo) {
|
|
880
|
+
if (!qbo) return void 0;
|
|
881
|
+
return {
|
|
882
|
+
street1: qbo.Line1,
|
|
883
|
+
street2: qbo.Line2,
|
|
884
|
+
city: qbo.City,
|
|
885
|
+
state: qbo.CountrySubDivisionCode,
|
|
886
|
+
postalCode: qbo.PostalCode,
|
|
887
|
+
country: qbo.Country
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
export {
|
|
891
|
+
QuickBooksProvider
|
|
892
|
+
};
|
|
893
|
+
//# sourceMappingURL=index-D0bqSiCo.js.map
|