@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.
@@ -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