@memberjunction/integration-connectors 5.50.0 → 6.1.0-edge.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/LICENSE +7 -0
- package/README.md +109 -277
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -38
- package/dist/index.js.map +1 -1
- package/package.json +14 -19
- package/dist/AptifyConnector.d.ts +0 -95
- package/dist/AptifyConnector.d.ts.map +0 -1
- package/dist/AptifyConnector.js +0 -760
- package/dist/AptifyConnector.js.map +0 -1
- package/dist/BlackbaudConnector.d.ts +0 -64
- package/dist/BlackbaudConnector.d.ts.map +0 -1
- package/dist/BlackbaudConnector.js +0 -916
- package/dist/BlackbaudConnector.js.map +0 -1
- package/dist/ConstantContactConnector.d.ts +0 -137
- package/dist/ConstantContactConnector.d.ts.map +0 -1
- package/dist/ConstantContactConnector.js +0 -663
- package/dist/ConstantContactConnector.js.map +0 -1
- package/dist/CventConnector.d.ts +0 -146
- package/dist/CventConnector.d.ts.map +0 -1
- package/dist/CventConnector.js +0 -540
- package/dist/CventConnector.js.map +0 -1
- package/dist/DynamicsDataverseConnector.d.ts +0 -296
- package/dist/DynamicsDataverseConnector.d.ts.map +0 -1
- package/dist/DynamicsDataverseConnector.js +0 -790
- package/dist/DynamicsDataverseConnector.js.map +0 -1
- package/dist/FileFeedConnector.d.ts +0 -64
- package/dist/FileFeedConnector.d.ts.map +0 -1
- package/dist/FileFeedConnector.js +0 -190
- package/dist/FileFeedConnector.js.map +0 -1
- package/dist/FontevaConnector.d.ts +0 -153
- package/dist/FontevaConnector.d.ts.map +0 -1
- package/dist/FontevaConnector.js +0 -421
- package/dist/FontevaConnector.js.map +0 -1
- package/dist/GrowthZoneConnector.d.ts +0 -183
- package/dist/GrowthZoneConnector.d.ts.map +0 -1
- package/dist/GrowthZoneConnector.js +0 -702
- package/dist/GrowthZoneConnector.js.map +0 -1
- package/dist/HivebriteConnector.d.ts +0 -151
- package/dist/HivebriteConnector.d.ts.map +0 -1
- package/dist/HivebriteConnector.js +0 -533
- package/dist/HivebriteConnector.js.map +0 -1
- package/dist/HubSpotConnector.d.ts +0 -523
- package/dist/HubSpotConnector.d.ts.map +0 -1
- package/dist/HubSpotConnector.js +0 -3110
- package/dist/HubSpotConnector.js.map +0 -1
- package/dist/IMISConnector.d.ts +0 -171
- package/dist/IMISConnector.d.ts.map +0 -1
- package/dist/IMISConnector.js +0 -695
- package/dist/IMISConnector.js.map +0 -1
- package/dist/MJToMJConnector.d.ts +0 -279
- package/dist/MJToMJConnector.d.ts.map +0 -1
- package/dist/MJToMJConnector.js +0 -893
- package/dist/MJToMJConnector.js.map +0 -1
- package/dist/MagnetMailConnector.d.ts +0 -163
- package/dist/MagnetMailConnector.d.ts.map +0 -1
- package/dist/MagnetMailConnector.js +0 -823
- package/dist/MagnetMailConnector.js.map +0 -1
- package/dist/MailchimpConnector.d.ts +0 -197
- package/dist/MailchimpConnector.d.ts.map +0 -1
- package/dist/MailchimpConnector.js +0 -791
- package/dist/MailchimpConnector.js.map +0 -1
- package/dist/MemberSuiteConnector.d.ts +0 -185
- package/dist/MemberSuiteConnector.d.ts.map +0 -1
- package/dist/MemberSuiteConnector.js +0 -762
- package/dist/MemberSuiteConnector.js.map +0 -1
- package/dist/NeonCRMConnector.d.ts +0 -273
- package/dist/NeonCRMConnector.d.ts.map +0 -1
- package/dist/NeonCRMConnector.js +0 -1039
- package/dist/NeonCRMConnector.js.map +0 -1
- package/dist/NetForumConnector.d.ts +0 -170
- package/dist/NetForumConnector.d.ts.map +0 -1
- package/dist/NetForumConnector.js +0 -778
- package/dist/NetForumConnector.js.map +0 -1
- package/dist/NetSuiteConnector.d.ts +0 -136
- package/dist/NetSuiteConnector.d.ts.map +0 -1
- package/dist/NetSuiteConnector.js +0 -703
- package/dist/NetSuiteConnector.js.map +0 -1
- package/dist/NimbleAMSConnector.d.ts +0 -142
- package/dist/NimbleAMSConnector.d.ts.map +0 -1
- package/dist/NimbleAMSConnector.js +0 -703
- package/dist/NimbleAMSConnector.js.map +0 -1
- package/dist/NoviConnector.d.ts +0 -178
- package/dist/NoviConnector.d.ts.map +0 -1
- package/dist/NoviConnector.js +0 -566
- package/dist/NoviConnector.js.map +0 -1
- package/dist/ORCIDConnector.d.ts +0 -207
- package/dist/ORCIDConnector.d.ts.map +0 -1
- package/dist/ORCIDConnector.js +0 -708
- package/dist/ORCIDConnector.js.map +0 -1
- package/dist/OpenWaterConnector.d.ts +0 -173
- package/dist/OpenWaterConnector.d.ts.map +0 -1
- package/dist/OpenWaterConnector.js +0 -893
- package/dist/OpenWaterConnector.js.map +0 -1
- package/dist/PathLMSConnector.d.ts +0 -395
- package/dist/PathLMSConnector.d.ts.map +0 -1
- package/dist/PathLMSConnector.js +0 -1281
- package/dist/PathLMSConnector.js.map +0 -1
- package/dist/PheedLoopConnector.d.ts +0 -119
- package/dist/PheedLoopConnector.d.ts.map +0 -1
- package/dist/PheedLoopConnector.js +0 -429
- package/dist/PheedLoopConnector.js.map +0 -1
- package/dist/PropFuelConnector.d.ts +0 -153
- package/dist/PropFuelConnector.d.ts.map +0 -1
- package/dist/PropFuelConnector.js +0 -450
- package/dist/PropFuelConnector.js.map +0 -1
- package/dist/QuickBooksConnector.d.ts +0 -92
- package/dist/QuickBooksConnector.d.ts.map +0 -1
- package/dist/QuickBooksConnector.js +0 -713
- package/dist/QuickBooksConnector.js.map +0 -1
- package/dist/RasaConnector.d.ts +0 -152
- package/dist/RasaConnector.d.ts.map +0 -1
- package/dist/RasaConnector.js +0 -929
- package/dist/RasaConnector.js.map +0 -1
- package/dist/Reach360Connector.d.ts +0 -175
- package/dist/Reach360Connector.d.ts.map +0 -1
- package/dist/Reach360Connector.js +0 -796
- package/dist/Reach360Connector.js.map +0 -1
- package/dist/RelationalDBConnector.d.ts +0 -108
- package/dist/RelationalDBConnector.d.ts.map +0 -1
- package/dist/RelationalDBConnector.js +0 -228
- package/dist/RelationalDBConnector.js.map +0 -1
- package/dist/RhythmConnector.d.ts +0 -188
- package/dist/RhythmConnector.d.ts.map +0 -1
- package/dist/RhythmConnector.js +0 -784
- package/dist/RhythmConnector.js.map +0 -1
- package/dist/SageIntacctConnector.d.ts +0 -337
- package/dist/SageIntacctConnector.d.ts.map +0 -1
- package/dist/SageIntacctConnector.js +0 -2081
- package/dist/SageIntacctConnector.js.map +0 -1
- package/dist/SalesforceConnector.d.ts +0 -461
- package/dist/SalesforceConnector.d.ts.map +0 -1
- package/dist/SalesforceConnector.js +0 -1862
- package/dist/SalesforceConnector.js.map +0 -1
- package/dist/SharePointConnector.d.ts +0 -188
- package/dist/SharePointConnector.d.ts.map +0 -1
- package/dist/SharePointConnector.js +0 -716
- package/dist/SharePointConnector.js.map +0 -1
- package/dist/WicketConnector.d.ts +0 -252
- package/dist/WicketConnector.d.ts.map +0 -1
- package/dist/WicketConnector.js +0 -938
- package/dist/WicketConnector.js.map +0 -1
- package/dist/WildApricotConnector.d.ts +0 -201
- package/dist/WildApricotConnector.d.ts.map +0 -1
- package/dist/WildApricotConnector.js +0 -1129
- package/dist/WildApricotConnector.js.map +0 -1
- package/dist/YourMembershipConnector.d.ts +0 -349
- package/dist/YourMembershipConnector.d.ts.map +0 -1
- package/dist/YourMembershipConnector.js +0 -4062
- package/dist/YourMembershipConnector.js.map +0 -1
- package/dist/generate-integration-actions.d.ts +0 -23
- package/dist/generate-integration-actions.d.ts.map +0 -1
- package/dist/generate-integration-actions.js +0 -201
- package/dist/generate-integration-actions.js.map +0 -1
|
@@ -1,713 +0,0 @@
|
|
|
1
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
-
};
|
|
7
|
-
import { RegisterClass } from '@memberjunction/global';
|
|
8
|
-
import { Metadata } from '@memberjunction/core';
|
|
9
|
-
import { BaseIntegrationConnector, } from '@memberjunction/integration-engine';
|
|
10
|
-
// ─── Constants ────────────────────────────────────────────────────────
|
|
11
|
-
const QBO_PRODUCTION_BASE = 'https://quickbooks.api.intuit.com';
|
|
12
|
-
const QBO_SANDBOX_BASE = 'https://sandbox-quickbooks.api.intuit.com';
|
|
13
|
-
const QBO_OAUTH_TOKEN_URL = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer';
|
|
14
|
-
/** Minor version to pin API behavior */
|
|
15
|
-
const QBO_MINOR_VERSION = '73';
|
|
16
|
-
/** Default max retries */
|
|
17
|
-
const DEFAULT_MAX_RETRIES = 3;
|
|
18
|
-
/** Default HTTP timeout in ms */
|
|
19
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
20
|
-
/** Default min interval between requests in ms */
|
|
21
|
-
const DEFAULT_MIN_REQUEST_INTERVAL_MS = 200;
|
|
22
|
-
/** Default page size for queries */
|
|
23
|
-
const DEFAULT_PAGE_SIZE = 100;
|
|
24
|
-
/** Max records per query (QBO limit) */
|
|
25
|
-
const MAX_QUERY_RESULTS = 1000;
|
|
26
|
-
/** Access token refresh threshold — refresh 5 min before expiry */
|
|
27
|
-
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
28
|
-
/** QBO data type to generic type mapping */
|
|
29
|
-
const QBO_TYPE_MAP = {
|
|
30
|
-
'String': 'string',
|
|
31
|
-
'Numeric': 'decimal',
|
|
32
|
-
'DateTime': 'datetime',
|
|
33
|
-
'Date': 'date',
|
|
34
|
-
'Boolean': 'boolean',
|
|
35
|
-
'EmailAddress': 'string',
|
|
36
|
-
'PhysicalAddress': 'string',
|
|
37
|
-
'TelephoneNumber': 'string',
|
|
38
|
-
'IdType': 'string',
|
|
39
|
-
'BigDecimal': 'decimal',
|
|
40
|
-
'Number': 'decimal',
|
|
41
|
-
'Integer': 'integer',
|
|
42
|
-
};
|
|
43
|
-
// ─── QuickBooks Object Metadata for Action Generation ─────────────────
|
|
44
|
-
const QUICKBOOKS_OBJECTS = [
|
|
45
|
-
{
|
|
46
|
-
Name: 'Customer', DisplayName: 'Customer',
|
|
47
|
-
Description: 'A customer in QuickBooks Online', SupportsWrite: true,
|
|
48
|
-
Fields: [
|
|
49
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
50
|
-
{ Name: 'DisplayName', DisplayName: 'Display Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Customer display name' },
|
|
51
|
-
{ Name: 'GivenName', DisplayName: 'First Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'First/given name' },
|
|
52
|
-
{ Name: 'FamilyName', DisplayName: 'Last Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Last/family name' },
|
|
53
|
-
{ Name: 'CompanyName', DisplayName: 'Company Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company name' },
|
|
54
|
-
{ Name: 'PrimaryEmailAddr', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary email address (value in .Address)' },
|
|
55
|
-
{ Name: 'PrimaryPhone', DisplayName: 'Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary phone number (value in .FreeFormNumber)' },
|
|
56
|
-
{ Name: 'BillAddr', DisplayName: 'Billing Address', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Billing address (composite object)' },
|
|
57
|
-
{ Name: 'Balance', DisplayName: 'Balance', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Open balance' },
|
|
58
|
-
{ Name: 'Active', DisplayName: 'Active', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether the customer is active' },
|
|
59
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
60
|
-
],
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
Name: 'Vendor', DisplayName: 'Vendor',
|
|
64
|
-
Description: 'A vendor/supplier in QuickBooks Online', SupportsWrite: true,
|
|
65
|
-
Fields: [
|
|
66
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
67
|
-
{ Name: 'DisplayName', DisplayName: 'Display Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Vendor display name' },
|
|
68
|
-
{ Name: 'GivenName', DisplayName: 'First Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'First/given name' },
|
|
69
|
-
{ Name: 'FamilyName', DisplayName: 'Last Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Last/family name' },
|
|
70
|
-
{ Name: 'CompanyName', DisplayName: 'Company Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Company name' },
|
|
71
|
-
{ Name: 'PrimaryEmailAddr', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary email address' },
|
|
72
|
-
{ Name: 'PrimaryPhone', DisplayName: 'Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary phone number' },
|
|
73
|
-
{ Name: 'Balance', DisplayName: 'Balance', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Open balance' },
|
|
74
|
-
{ Name: 'Active', DisplayName: 'Active', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether the vendor is active' },
|
|
75
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
Name: 'Account', DisplayName: 'Account',
|
|
80
|
-
Description: 'A Chart of Accounts entry in QuickBooks Online', SupportsWrite: true,
|
|
81
|
-
Fields: [
|
|
82
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
83
|
-
{ Name: 'Name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Account name' },
|
|
84
|
-
{ Name: 'AccountType', DisplayName: 'Account Type', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Account type (Bank, Expense, Income, etc.)' },
|
|
85
|
-
{ Name: 'AccountSubType', DisplayName: 'Sub-Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Account sub-type' },
|
|
86
|
-
{ Name: 'CurrentBalance', DisplayName: 'Current Balance', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Current balance' },
|
|
87
|
-
{ Name: 'Active', DisplayName: 'Active', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether the account is active' },
|
|
88
|
-
{ Name: 'Classification', DisplayName: 'Classification', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Asset, Equity, Expense, Liability, Revenue' },
|
|
89
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
90
|
-
],
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
Name: 'Invoice', DisplayName: 'Invoice',
|
|
94
|
-
Description: 'A sales invoice in QuickBooks Online', SupportsWrite: true,
|
|
95
|
-
Fields: [
|
|
96
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
97
|
-
{ Name: 'DocNumber', DisplayName: 'Invoice Number', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'User-visible invoice number' },
|
|
98
|
-
{ Name: 'CustomerRef', DisplayName: 'Customer Ref', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Customer reference (value = customer ID)' },
|
|
99
|
-
{ Name: 'TxnDate', DisplayName: 'Transaction Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Invoice date' },
|
|
100
|
-
{ Name: 'DueDate', DisplayName: 'Due Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Payment due date' },
|
|
101
|
-
{ Name: 'TotalAmt', DisplayName: 'Total Amount', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total invoice amount' },
|
|
102
|
-
{ Name: 'Balance', DisplayName: 'Balance', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Remaining balance' },
|
|
103
|
-
{ Name: 'EmailStatus', DisplayName: 'Email Status', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Email delivery status' },
|
|
104
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
Name: 'Bill', DisplayName: 'Bill',
|
|
109
|
-
Description: 'A payable bill in QuickBooks Online', SupportsWrite: true,
|
|
110
|
-
Fields: [
|
|
111
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
112
|
-
{ Name: 'DocNumber', DisplayName: 'Bill Number', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'User-visible bill number' },
|
|
113
|
-
{ Name: 'VendorRef', DisplayName: 'Vendor Ref', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Vendor reference (value = vendor ID)' },
|
|
114
|
-
{ Name: 'TxnDate', DisplayName: 'Transaction Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Bill date' },
|
|
115
|
-
{ Name: 'DueDate', DisplayName: 'Due Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Payment due date' },
|
|
116
|
-
{ Name: 'TotalAmt', DisplayName: 'Total Amount', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total bill amount' },
|
|
117
|
-
{ Name: 'Balance', DisplayName: 'Balance', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Remaining balance' },
|
|
118
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
119
|
-
],
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
Name: 'Item', DisplayName: 'Item',
|
|
123
|
-
Description: 'A product or service item in QuickBooks Online', SupportsWrite: true,
|
|
124
|
-
Fields: [
|
|
125
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
126
|
-
{ Name: 'Name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Item name' },
|
|
127
|
-
{ Name: 'Type', DisplayName: 'Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Item type (Inventory, Service, NonInventory)' },
|
|
128
|
-
{ Name: 'UnitPrice', DisplayName: 'Unit Price', Type: 'decimal', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Sales price' },
|
|
129
|
-
{ Name: 'PurchaseCost', DisplayName: 'Purchase Cost', Type: 'decimal', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Purchase cost' },
|
|
130
|
-
{ Name: 'QtyOnHand', DisplayName: 'Quantity On Hand', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Current inventory quantity' },
|
|
131
|
-
{ Name: 'Active', DisplayName: 'Active', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether the item is active' },
|
|
132
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
Name: 'Payment', DisplayName: 'Payment',
|
|
137
|
-
Description: 'A customer payment in QuickBooks Online', SupportsWrite: true,
|
|
138
|
-
Fields: [
|
|
139
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
140
|
-
{ Name: 'CustomerRef', DisplayName: 'Customer Ref', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Customer reference' },
|
|
141
|
-
{ Name: 'TotalAmt', DisplayName: 'Total Amount', Type: 'decimal', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Payment amount' },
|
|
142
|
-
{ Name: 'TxnDate', DisplayName: 'Transaction Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Payment date' },
|
|
143
|
-
{ Name: 'PaymentMethodRef', DisplayName: 'Payment Method', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Payment method reference' },
|
|
144
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
145
|
-
],
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
Name: 'Employee', DisplayName: 'Employee',
|
|
149
|
-
Description: 'An employee in QuickBooks Online', SupportsWrite: true,
|
|
150
|
-
Fields: [
|
|
151
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
152
|
-
{ Name: 'DisplayName', DisplayName: 'Display Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Employee display name' },
|
|
153
|
-
{ Name: 'GivenName', DisplayName: 'First Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'First name' },
|
|
154
|
-
{ Name: 'FamilyName', DisplayName: 'Last Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Last name' },
|
|
155
|
-
{ Name: 'PrimaryEmailAddr', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary email' },
|
|
156
|
-
{ Name: 'PrimaryPhone', DisplayName: 'Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary phone' },
|
|
157
|
-
{ Name: 'Active', DisplayName: 'Active', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether the employee is active' },
|
|
158
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
Name: 'Department', DisplayName: 'Department',
|
|
163
|
-
Description: 'A department/location in QuickBooks Online', SupportsWrite: true,
|
|
164
|
-
Fields: [
|
|
165
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
166
|
-
{ Name: 'Name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Department name' },
|
|
167
|
-
{ Name: 'ParentRef', DisplayName: 'Parent Ref', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Parent department reference' },
|
|
168
|
-
{ Name: 'Active', DisplayName: 'Active', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether the department is active' },
|
|
169
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
170
|
-
],
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
Name: 'Class', DisplayName: 'Class',
|
|
174
|
-
Description: 'A class for categorizing transactions in QuickBooks Online', SupportsWrite: true,
|
|
175
|
-
Fields: [
|
|
176
|
-
{ Name: 'Id', DisplayName: 'ID', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'QuickBooks internal ID' },
|
|
177
|
-
{ Name: 'Name', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Class name' },
|
|
178
|
-
{ Name: 'ParentRef', DisplayName: 'Parent Ref', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Parent class reference' },
|
|
179
|
-
{ Name: 'Active', DisplayName: 'Active', Type: 'boolean', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Whether the class is active' },
|
|
180
|
-
{ Name: 'MetaData', DisplayName: 'Metadata', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Create/update timestamps' },
|
|
181
|
-
],
|
|
182
|
-
},
|
|
183
|
-
];
|
|
184
|
-
/**
|
|
185
|
-
* Provable foreign keys: <Sibling>Ref fields whose <Sibling> matches another
|
|
186
|
-
* object declared in QUICKBOOKS_OBJECTS above. Keyed by "<ObjectName>.<FieldName>",
|
|
187
|
-
* the value is the external name of the referenced sibling object.
|
|
188
|
-
*
|
|
189
|
-
* Derived purely from this connector's own declared object/field metadata
|
|
190
|
-
* (e.g. Invoice.CustomerRef → Customer is a declared object). Self/ambiguous
|
|
191
|
-
* refs (ParentRef on Department/Class) and refs with no sibling object
|
|
192
|
-
* (PaymentMethodRef) are intentionally excluded.
|
|
193
|
-
*/
|
|
194
|
-
const QUICKBOOKS_FOREIGN_KEYS = {
|
|
195
|
-
'Invoice.CustomerRef': 'Customer',
|
|
196
|
-
'Payment.CustomerRef': 'Customer',
|
|
197
|
-
'Bill.VendorRef': 'Vendor',
|
|
198
|
-
};
|
|
199
|
-
// ─── Connector Implementation ─────────────────────────────────────────
|
|
200
|
-
/**
|
|
201
|
-
* Connector for QuickBooks Online REST API v3.
|
|
202
|
-
*
|
|
203
|
-
* Extends BaseIntegrationConnector directly. While QBO is REST/JSON, its
|
|
204
|
-
* query model uses a custom SQL-like query language and its update operations
|
|
205
|
-
* require the full object with SyncToken, making it distinct from the
|
|
206
|
-
* generic BaseRESTIntegrationConnector pagination pattern.
|
|
207
|
-
*
|
|
208
|
-
* Auth flow:
|
|
209
|
-
* 1. Exchange refresh token for access token via OAuth 2.0 token endpoint
|
|
210
|
-
* 2. Access token is Bearer-authenticated on all API calls
|
|
211
|
-
* 3. Refresh tokens are rotated — the connector stores the latest refresh token
|
|
212
|
-
*
|
|
213
|
-
* API operations:
|
|
214
|
-
* - Query: SQL-like SELECT statements (e.g., "SELECT * FROM Customer WHERE Active = true")
|
|
215
|
-
* - Read: GET /v3/company/{realmId}/{objectType}/{id}
|
|
216
|
-
* - Create: POST /v3/company/{realmId}/{objectType}
|
|
217
|
-
* - Update: POST /v3/company/{realmId}/{objectType} (full object with SyncToken)
|
|
218
|
-
* - Delete: POST /v3/company/{realmId}/{objectType}?operation=delete
|
|
219
|
-
*
|
|
220
|
-
* Pagination:
|
|
221
|
-
* - Query supports STARTPOSITION and MAXRESULTS
|
|
222
|
-
* - COUNT query to determine total records
|
|
223
|
-
*/
|
|
224
|
-
let QuickBooksConnector = class QuickBooksConnector extends BaseIntegrationConnector {
|
|
225
|
-
constructor() {
|
|
226
|
-
// ── Auth State ───────────────────────────────────────────────────
|
|
227
|
-
super(...arguments);
|
|
228
|
-
this.authState = null;
|
|
229
|
-
/** Timestamp of the last API request, used for throttling */
|
|
230
|
-
this.lastRequestTime = 0;
|
|
231
|
-
}
|
|
232
|
-
// ── Capability Getters ───────────────────────────────────────────
|
|
233
|
-
get SupportsCreate() { return true; }
|
|
234
|
-
get SupportsUpdate() { return true; }
|
|
235
|
-
get SupportsDelete() { return true; }
|
|
236
|
-
get SupportsSearch() { return true; }
|
|
237
|
-
get SupportsListing() { return true; }
|
|
238
|
-
get IntegrationName() { return 'QuickBooks'; }
|
|
239
|
-
// ── Action Generation ────────────────────────────────────────────
|
|
240
|
-
GetIntegrationObjects() {
|
|
241
|
-
return QUICKBOOKS_OBJECTS;
|
|
242
|
-
}
|
|
243
|
-
GetActionGeneratorConfig() {
|
|
244
|
-
const objects = this.GetIntegrationObjects();
|
|
245
|
-
if (objects.length === 0)
|
|
246
|
-
return null;
|
|
247
|
-
return {
|
|
248
|
-
IntegrationName: 'QuickBooks',
|
|
249
|
-
CategoryName: 'QuickBooks',
|
|
250
|
-
IconClass: 'fa-solid fa-book',
|
|
251
|
-
Objects: objects,
|
|
252
|
-
IncludeSearch: true,
|
|
253
|
-
IncludeList: true,
|
|
254
|
-
CategoryDescription: 'QuickBooks Online accounting integration actions',
|
|
255
|
-
ParentCategoryName: 'Business Apps',
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
// ── Default Configuration ────────────────────────────────────────
|
|
259
|
-
GetDefaultConfiguration() {
|
|
260
|
-
return {
|
|
261
|
-
DefaultSchemaName: 'QuickBooks',
|
|
262
|
-
DefaultObjects: [
|
|
263
|
-
{
|
|
264
|
-
SourceObjectName: 'Customer',
|
|
265
|
-
TargetTableName: 'QuickBooks_Customer',
|
|
266
|
-
TargetEntityName: 'QuickBooks Customers',
|
|
267
|
-
SyncEnabled: true,
|
|
268
|
-
FieldMappings: this.GetDefaultFieldMappings('Customer', 'Contacts'),
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
SourceObjectName: 'Vendor',
|
|
272
|
-
TargetTableName: 'QuickBooks_Vendor',
|
|
273
|
-
TargetEntityName: 'QuickBooks Vendors',
|
|
274
|
-
SyncEnabled: true,
|
|
275
|
-
FieldMappings: this.GetDefaultFieldMappings('Vendor', 'Companies'),
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
SourceObjectName: 'Account',
|
|
279
|
-
TargetTableName: 'QuickBooks_Account',
|
|
280
|
-
TargetEntityName: 'QuickBooks Accounts',
|
|
281
|
-
SyncEnabled: true,
|
|
282
|
-
FieldMappings: [],
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
GetDefaultFieldMappings(objectName, _entityName) {
|
|
288
|
-
switch (objectName) {
|
|
289
|
-
case 'Customer':
|
|
290
|
-
return [
|
|
291
|
-
{ SourceFieldName: 'Id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
292
|
-
{ SourceFieldName: 'DisplayName', DestinationFieldName: 'Name' },
|
|
293
|
-
{ SourceFieldName: 'PrimaryEmailAddr', DestinationFieldName: 'Email' },
|
|
294
|
-
{ SourceFieldName: 'PrimaryPhone', DestinationFieldName: 'Phone' },
|
|
295
|
-
{ SourceFieldName: 'Active', DestinationFieldName: 'Status' },
|
|
296
|
-
];
|
|
297
|
-
case 'Vendor':
|
|
298
|
-
return [
|
|
299
|
-
{ SourceFieldName: 'Id', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
300
|
-
{ SourceFieldName: 'DisplayName', DestinationFieldName: 'Name' },
|
|
301
|
-
{ SourceFieldName: 'PrimaryEmailAddr', DestinationFieldName: 'Email' },
|
|
302
|
-
{ SourceFieldName: 'PrimaryPhone', DestinationFieldName: 'Phone' },
|
|
303
|
-
{ SourceFieldName: 'Active', DestinationFieldName: 'Status' },
|
|
304
|
-
];
|
|
305
|
-
default:
|
|
306
|
-
return [];
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// ─── TestConnection ──────────────────────────────────────────────
|
|
310
|
-
async TestConnection(companyIntegration, contextUser) {
|
|
311
|
-
try {
|
|
312
|
-
const auth = await this.GetAuth(companyIntegration, contextUser, true);
|
|
313
|
-
// Hit the CompanyInfo endpoint to verify connectivity
|
|
314
|
-
const url = `${auth.BaseUrl}/v3/company/${auth.RealmId}/companyinfo/${auth.RealmId}?minorversion=${QBO_MINOR_VERSION}`;
|
|
315
|
-
const response = await this.MakeRequest(auth, url, 'GET');
|
|
316
|
-
const body = response.Body;
|
|
317
|
-
const companyName = body.CompanyInfo?.CompanyName ?? 'Unknown';
|
|
318
|
-
return {
|
|
319
|
-
Success: true,
|
|
320
|
-
Message: `Successfully connected to QuickBooks Online (${companyName})`,
|
|
321
|
-
ServerVersion: `QuickBooks Online API v3 (minor ${QBO_MINOR_VERSION})`,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
326
|
-
return { Success: false, Message: `Connection failed: ${message}` };
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
// ─── Discovery ───────────────────────────────────────────────────
|
|
330
|
-
async DiscoverObjects(_companyIntegration, _contextUser) {
|
|
331
|
-
// QBO doesn't have a dynamic discovery API — return known objects
|
|
332
|
-
return QUICKBOOKS_OBJECTS.map(obj => ({
|
|
333
|
-
Name: obj.Name,
|
|
334
|
-
Label: obj.DisplayName,
|
|
335
|
-
Description: obj.Description,
|
|
336
|
-
SupportsIncrementalSync: true,
|
|
337
|
-
SupportsWrite: obj.SupportsWrite,
|
|
338
|
-
}));
|
|
339
|
-
}
|
|
340
|
-
async DiscoverFields(_companyIntegration, objectName, _contextUser) {
|
|
341
|
-
const obj = QUICKBOOKS_OBJECTS.find(o => o.Name === objectName);
|
|
342
|
-
if (!obj)
|
|
343
|
-
throw new Error(`Unknown QuickBooks object: ${objectName}`);
|
|
344
|
-
return obj.Fields.map(f => {
|
|
345
|
-
const fkTarget = QUICKBOOKS_FOREIGN_KEYS[`${obj.Name}.${f.Name}`];
|
|
346
|
-
return {
|
|
347
|
-
Name: f.Name,
|
|
348
|
-
Label: f.DisplayName,
|
|
349
|
-
Description: f.Description,
|
|
350
|
-
DataType: f.Type,
|
|
351
|
-
IsRequired: f.IsRequired,
|
|
352
|
-
IsUniqueKey: f.IsPrimaryKey,
|
|
353
|
-
IsReadOnly: f.IsReadOnly,
|
|
354
|
-
IsForeignKey: fkTarget != null,
|
|
355
|
-
ForeignKeyTarget: fkTarget ?? null,
|
|
356
|
-
};
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
// ─── FetchChanges ────────────────────────────────────────────────
|
|
360
|
-
async FetchChanges(ctx) {
|
|
361
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
362
|
-
const pageSize = Math.min(ctx.BatchSize || DEFAULT_PAGE_SIZE, MAX_QUERY_RESULTS);
|
|
363
|
-
const startPosition = ctx.CurrentOffset ?? 1; // QBO uses 1-based STARTPOSITION
|
|
364
|
-
const whereClause = ctx.WatermarkValue
|
|
365
|
-
? ` WHERE MetaData.LastUpdatedTime >= '${ctx.WatermarkValue}'`
|
|
366
|
-
: '';
|
|
367
|
-
const orderBy = ' ORDERBY MetaData.LastUpdatedTime ASC';
|
|
368
|
-
const query = `SELECT * FROM ${ctx.ObjectName}${whereClause}${orderBy} STARTPOSITION ${startPosition} MAXRESULTS ${pageSize}`;
|
|
369
|
-
const result = await this.ExecuteQuery(auth, query);
|
|
370
|
-
const records = this.ExtractQueryRecords(result, ctx.ObjectName);
|
|
371
|
-
const externalRecords = records.map(r => ({
|
|
372
|
-
ExternalID: String(r['Id'] ?? ''),
|
|
373
|
-
ObjectType: ctx.ObjectName,
|
|
374
|
-
Fields: r,
|
|
375
|
-
ModifiedAt: this.ExtractLastUpdatedTime(r),
|
|
376
|
-
}));
|
|
377
|
-
const newWatermark = this.ComputeWatermark(records);
|
|
378
|
-
const hasMore = records.length >= pageSize;
|
|
379
|
-
return {
|
|
380
|
-
Records: externalRecords,
|
|
381
|
-
HasMore: hasMore,
|
|
382
|
-
NewWatermarkValue: newWatermark,
|
|
383
|
-
NextOffset: hasMore ? startPosition + records.length : undefined,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
// ─── CRUD Operations ─────────────────────────────────────────────
|
|
387
|
-
async CreateRecord(ctx) {
|
|
388
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
389
|
-
const url = `${auth.BaseUrl}/v3/company/${auth.RealmId}/${ctx.ObjectName.toLowerCase()}?minorversion=${QBO_MINOR_VERSION}`;
|
|
390
|
-
try {
|
|
391
|
-
const response = await this.MakeRequest(auth, url, 'POST', ctx.Attributes);
|
|
392
|
-
const body = response.Body;
|
|
393
|
-
const created = body[ctx.ObjectName];
|
|
394
|
-
const id = created ? String(created['Id'] ?? '') : '';
|
|
395
|
-
return this.BuildCreatedResult(id, response.Status, ctx.ObjectName);
|
|
396
|
-
}
|
|
397
|
-
catch (err) {
|
|
398
|
-
return this.BuildCRUDError(err, 'CreateRecord', ctx.ObjectName);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
async UpdateRecord(ctx) {
|
|
402
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
403
|
-
// QBO requires full object with SyncToken for updates — fetch current first
|
|
404
|
-
try {
|
|
405
|
-
const current = await this.FetchSingleRecord(auth, ctx.ObjectName, ctx.ExternalID);
|
|
406
|
-
if (!current) {
|
|
407
|
-
return { Success: false, ErrorMessage: `Record ${ctx.ExternalID} not found`, StatusCode: 404 };
|
|
408
|
-
}
|
|
409
|
-
const merged = { ...current, ...ctx.Attributes };
|
|
410
|
-
const url = `${auth.BaseUrl}/v3/company/${auth.RealmId}/${ctx.ObjectName.toLowerCase()}?minorversion=${QBO_MINOR_VERSION}`;
|
|
411
|
-
const response = await this.MakeRequest(auth, url, 'POST', merged);
|
|
412
|
-
const body = response.Body;
|
|
413
|
-
const updated = body[ctx.ObjectName];
|
|
414
|
-
const id = updated ? String(updated['Id'] ?? ctx.ExternalID) : ctx.ExternalID;
|
|
415
|
-
return { Success: true, ExternalID: id, StatusCode: response.Status };
|
|
416
|
-
}
|
|
417
|
-
catch (err) {
|
|
418
|
-
return this.BuildCRUDError(err, 'UpdateRecord', ctx.ObjectName);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
async DeleteRecord(ctx) {
|
|
422
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
423
|
-
try {
|
|
424
|
-
// QBO delete requires Id and SyncToken
|
|
425
|
-
const current = await this.FetchSingleRecord(auth, ctx.ObjectName, ctx.ExternalID);
|
|
426
|
-
if (!current) {
|
|
427
|
-
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: 200 }; // Already gone
|
|
428
|
-
}
|
|
429
|
-
const url = `${auth.BaseUrl}/v3/company/${auth.RealmId}/${ctx.ObjectName.toLowerCase()}?operation=delete&minorversion=${QBO_MINOR_VERSION}`;
|
|
430
|
-
await this.MakeRequest(auth, url, 'POST', {
|
|
431
|
-
Id: ctx.ExternalID,
|
|
432
|
-
SyncToken: current['SyncToken'],
|
|
433
|
-
});
|
|
434
|
-
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: 200 };
|
|
435
|
-
}
|
|
436
|
-
catch (err) {
|
|
437
|
-
return this.BuildCRUDError(err, 'DeleteRecord', ctx.ObjectName);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
async GetRecord(ctx) {
|
|
441
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
442
|
-
try {
|
|
443
|
-
const record = await this.FetchSingleRecord(auth, ctx.ObjectName, ctx.ExternalID);
|
|
444
|
-
if (!record)
|
|
445
|
-
return null;
|
|
446
|
-
return {
|
|
447
|
-
ExternalID: String(record['Id'] ?? ctx.ExternalID),
|
|
448
|
-
ObjectType: ctx.ObjectName,
|
|
449
|
-
Fields: record,
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
return null;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
async SearchRecords(ctx) {
|
|
457
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
458
|
-
const pageSize = ctx.PageSize ?? DEFAULT_PAGE_SIZE;
|
|
459
|
-
const startPos = ctx.Page ? ((ctx.Page - 1) * pageSize) + 1 : 1;
|
|
460
|
-
const whereParts = Object.entries(ctx.Filters).map(([field, value]) => `${field} = '${value.replace(/'/g, "\\'")}'`);
|
|
461
|
-
const whereClause = whereParts.length > 0 ? ` WHERE ${whereParts.join(' AND ')}` : '';
|
|
462
|
-
const query = `SELECT * FROM ${ctx.ObjectName}${whereClause} STARTPOSITION ${startPos} MAXRESULTS ${pageSize}`;
|
|
463
|
-
const result = await this.ExecuteQuery(auth, query);
|
|
464
|
-
const records = this.ExtractQueryRecords(result, ctx.ObjectName);
|
|
465
|
-
return {
|
|
466
|
-
Records: records.map(r => ({
|
|
467
|
-
ExternalID: String(r['Id'] ?? ''),
|
|
468
|
-
ObjectType: ctx.ObjectName,
|
|
469
|
-
Fields: r,
|
|
470
|
-
})),
|
|
471
|
-
TotalCount: records.length, // QBO doesn't return total in queries
|
|
472
|
-
HasMore: records.length >= pageSize,
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
async ListRecords(ctx) {
|
|
476
|
-
const auth = await this.GetAuth(ctx.CompanyIntegration, ctx.ContextUser);
|
|
477
|
-
const pageSize = ctx.PageSize ?? DEFAULT_PAGE_SIZE;
|
|
478
|
-
const startPos = ctx.Cursor ? parseInt(ctx.Cursor, 10) : 1;
|
|
479
|
-
let whereClause = '';
|
|
480
|
-
if (ctx.Filter) {
|
|
481
|
-
const parts = Object.entries(ctx.Filter).map(([k, v]) => `${k} = '${v.replace(/'/g, "\\'")}'`);
|
|
482
|
-
whereClause = ` WHERE ${parts.join(' AND ')}`;
|
|
483
|
-
}
|
|
484
|
-
const query = `SELECT * FROM ${ctx.ObjectName}${whereClause} STARTPOSITION ${startPos} MAXRESULTS ${pageSize}`;
|
|
485
|
-
const result = await this.ExecuteQuery(auth, query);
|
|
486
|
-
const records = this.ExtractQueryRecords(result, ctx.ObjectName);
|
|
487
|
-
const hasMore = records.length >= pageSize;
|
|
488
|
-
const nextCursor = hasMore ? String(startPos + records.length) : undefined;
|
|
489
|
-
return {
|
|
490
|
-
Records: records.map(r => ({
|
|
491
|
-
ExternalID: String(r['Id'] ?? ''),
|
|
492
|
-
ObjectType: ctx.ObjectName,
|
|
493
|
-
Fields: r,
|
|
494
|
-
})),
|
|
495
|
-
HasMore: hasMore,
|
|
496
|
-
NextCursor: nextCursor,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
// ─── OAuth 2.0 Authentication ────────────────────────────────────
|
|
500
|
-
async GetAuth(companyIntegration, contextUser, forceRefresh = false) {
|
|
501
|
-
if (!forceRefresh && this.authState && this.IsTokenValid()) {
|
|
502
|
-
return this.authState;
|
|
503
|
-
}
|
|
504
|
-
const config = await this.ParseConfig(companyIntegration, contextUser);
|
|
505
|
-
const auth = await this.RefreshAccessToken(config);
|
|
506
|
-
this.authState = auth;
|
|
507
|
-
return auth;
|
|
508
|
-
}
|
|
509
|
-
IsTokenValid() {
|
|
510
|
-
if (!this.authState)
|
|
511
|
-
return false;
|
|
512
|
-
return Date.now() < (this.authState.ExpiresAt - TOKEN_REFRESH_BUFFER_MS);
|
|
513
|
-
}
|
|
514
|
-
async RefreshAccessToken(config) {
|
|
515
|
-
const basicAuth = Buffer.from(`${config.ClientId}:${config.ClientSecret}`).toString('base64');
|
|
516
|
-
const response = await fetch(QBO_OAUTH_TOKEN_URL, {
|
|
517
|
-
method: 'POST',
|
|
518
|
-
headers: {
|
|
519
|
-
'Authorization': `Basic ${basicAuth}`,
|
|
520
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
521
|
-
'Accept': 'application/json',
|
|
522
|
-
},
|
|
523
|
-
body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(config.RefreshToken)}`,
|
|
524
|
-
});
|
|
525
|
-
if (!response.ok) {
|
|
526
|
-
const errorText = await response.text();
|
|
527
|
-
throw new Error(`OAuth token refresh failed (${response.status}): ${errorText.slice(0, 500)}`);
|
|
528
|
-
}
|
|
529
|
-
const tokenData = await response.json();
|
|
530
|
-
const baseUrl = config.Environment === 'sandbox' ? QBO_SANDBOX_BASE : QBO_PRODUCTION_BASE;
|
|
531
|
-
return {
|
|
532
|
-
AccessToken: tokenData.access_token,
|
|
533
|
-
RefreshToken: tokenData.refresh_token,
|
|
534
|
-
ExpiresAt: Date.now() + (tokenData.expires_in * 1000),
|
|
535
|
-
BaseUrl: baseUrl,
|
|
536
|
-
RealmId: config.RealmId,
|
|
537
|
-
Config: config,
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
// ─── Configuration Parsing ───────────────────────────────────────
|
|
541
|
-
async ParseConfig(companyIntegration, contextUser) {
|
|
542
|
-
const credentialID = companyIntegration.Get('CredentialID');
|
|
543
|
-
if (credentialID) {
|
|
544
|
-
const config = await this.LoadFromCredentialEntity(credentialID, contextUser);
|
|
545
|
-
if (config)
|
|
546
|
-
return config;
|
|
547
|
-
}
|
|
548
|
-
const configJson = companyIntegration.Get('Configuration');
|
|
549
|
-
if (configJson) {
|
|
550
|
-
const parsed = JSON.parse(configJson);
|
|
551
|
-
return this.ValidateConfig(parsed);
|
|
552
|
-
}
|
|
553
|
-
throw new Error('QuickBooksConnector: No credentials or configuration found on CompanyIntegration');
|
|
554
|
-
}
|
|
555
|
-
async LoadFromCredentialEntity(credentialID, contextUser, provider) {
|
|
556
|
-
const md = provider ?? new Metadata();
|
|
557
|
-
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
558
|
-
const loaded = await credential.Load(credentialID);
|
|
559
|
-
if (!loaded || !credential.Values)
|
|
560
|
-
return null;
|
|
561
|
-
try {
|
|
562
|
-
const parsed = JSON.parse(credential.Values);
|
|
563
|
-
return this.ValidateConfig(parsed);
|
|
564
|
-
}
|
|
565
|
-
catch {
|
|
566
|
-
return null;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
ValidateConfig(raw) {
|
|
570
|
-
if (!raw.ClientId)
|
|
571
|
-
throw new Error('QuickBooksConnector: ClientId is required');
|
|
572
|
-
if (!raw.ClientSecret)
|
|
573
|
-
throw new Error('QuickBooksConnector: ClientSecret is required');
|
|
574
|
-
if (!raw.RefreshToken)
|
|
575
|
-
throw new Error('QuickBooksConnector: RefreshToken is required');
|
|
576
|
-
if (!raw.RealmId)
|
|
577
|
-
throw new Error('QuickBooksConnector: RealmId is required');
|
|
578
|
-
return {
|
|
579
|
-
ClientId: raw.ClientId,
|
|
580
|
-
ClientSecret: raw.ClientSecret,
|
|
581
|
-
RefreshToken: raw.RefreshToken,
|
|
582
|
-
RealmId: raw.RealmId,
|
|
583
|
-
Environment: raw.Environment ?? 'production',
|
|
584
|
-
MaxRetries: raw.MaxRetries ?? DEFAULT_MAX_RETRIES,
|
|
585
|
-
RequestTimeoutMs: raw.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
586
|
-
MinRequestIntervalMs: raw.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
// ─── HTTP Transport ──────────────────────────────────────────────
|
|
590
|
-
async MakeRequest(auth, url, method, body) {
|
|
591
|
-
const config = auth.Config;
|
|
592
|
-
const maxRetries = config.MaxRetries ?? DEFAULT_MAX_RETRIES;
|
|
593
|
-
const timeoutMs = config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
594
|
-
const minInterval = config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
|
|
595
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
596
|
-
await this.Throttle(minInterval);
|
|
597
|
-
try {
|
|
598
|
-
const controller = new AbortController();
|
|
599
|
-
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
600
|
-
const headers = {
|
|
601
|
-
'Authorization': `Bearer ${auth.AccessToken}`,
|
|
602
|
-
'Accept': 'application/json',
|
|
603
|
-
};
|
|
604
|
-
if (body) {
|
|
605
|
-
headers['Content-Type'] = 'application/json';
|
|
606
|
-
}
|
|
607
|
-
const response = await fetch(url, {
|
|
608
|
-
method,
|
|
609
|
-
headers,
|
|
610
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
611
|
-
signal: controller.signal,
|
|
612
|
-
});
|
|
613
|
-
clearTimeout(timeoutHandle);
|
|
614
|
-
this.lastRequestTime = Date.now();
|
|
615
|
-
const responseBody = await response.json();
|
|
616
|
-
if (!response.ok) {
|
|
617
|
-
const errorMsg = this.ExtractQBOErrorMessage(responseBody);
|
|
618
|
-
throw new Error(`QBO API ${response.status}: ${errorMsg}`);
|
|
619
|
-
}
|
|
620
|
-
return { Status: response.status, Body: responseBody };
|
|
621
|
-
}
|
|
622
|
-
catch (err) {
|
|
623
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
624
|
-
const isRetryable = message.includes('abort') || message.includes('timeout')
|
|
625
|
-
|| message.includes('ECONNRESET') || message.includes('429')
|
|
626
|
-
|| message.includes('503');
|
|
627
|
-
if (!isRetryable || attempt === maxRetries) {
|
|
628
|
-
throw new Error(`QuickBooks API request failed after ${attempt + 1} attempt(s): ${message}`);
|
|
629
|
-
}
|
|
630
|
-
const delay = Math.pow(2, attempt) * 1000;
|
|
631
|
-
await this.Sleep(delay);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
throw new Error('QuickBooks API request failed: max retries exceeded');
|
|
635
|
-
}
|
|
636
|
-
// ─── Query Execution ─────────────────────────────────────────────
|
|
637
|
-
async ExecuteQuery(auth, query) {
|
|
638
|
-
const url = `${auth.BaseUrl}/v3/company/${auth.RealmId}/query?query=${encodeURIComponent(query)}&minorversion=${QBO_MINOR_VERSION}`;
|
|
639
|
-
const response = await this.MakeRequest(auth, url, 'GET');
|
|
640
|
-
return response.Body;
|
|
641
|
-
}
|
|
642
|
-
async FetchSingleRecord(auth, objectName, id) {
|
|
643
|
-
const url = `${auth.BaseUrl}/v3/company/${auth.RealmId}/${objectName.toLowerCase()}/${id}?minorversion=${QBO_MINOR_VERSION}`;
|
|
644
|
-
try {
|
|
645
|
-
const response = await this.MakeRequest(auth, url, 'GET');
|
|
646
|
-
const body = response.Body;
|
|
647
|
-
return body[objectName] ?? null;
|
|
648
|
-
}
|
|
649
|
-
catch {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
// ─── Response Parsing ────────────────────────────────────────────
|
|
654
|
-
ExtractQueryRecords(body, objectName) {
|
|
655
|
-
const typedBody = body;
|
|
656
|
-
const queryResponse = typedBody?.QueryResponse;
|
|
657
|
-
if (!queryResponse)
|
|
658
|
-
return [];
|
|
659
|
-
const records = queryResponse[objectName];
|
|
660
|
-
return records ?? [];
|
|
661
|
-
}
|
|
662
|
-
ExtractLastUpdatedTime(record) {
|
|
663
|
-
const metaData = record['MetaData'];
|
|
664
|
-
if (metaData?.LastUpdatedTime) {
|
|
665
|
-
return new Date(metaData.LastUpdatedTime);
|
|
666
|
-
}
|
|
667
|
-
return undefined;
|
|
668
|
-
}
|
|
669
|
-
ComputeWatermark(records) {
|
|
670
|
-
let latest;
|
|
671
|
-
for (const record of records) {
|
|
672
|
-
const metaData = record['MetaData'];
|
|
673
|
-
const ts = metaData?.LastUpdatedTime;
|
|
674
|
-
if (ts && (!latest || ts > latest)) {
|
|
675
|
-
latest = ts;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
return latest;
|
|
679
|
-
}
|
|
680
|
-
ExtractQBOErrorMessage(body) {
|
|
681
|
-
const typed = body;
|
|
682
|
-
if (typed?.Fault?.Error && typed.Fault.Error.length > 0) {
|
|
683
|
-
const err = typed.Fault.Error[0];
|
|
684
|
-
return err.Detail || err.Message || 'Unknown QBO error';
|
|
685
|
-
}
|
|
686
|
-
return typeof body === 'string' ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500);
|
|
687
|
-
}
|
|
688
|
-
// ─── Utility Helpers ─────────────────────────────────────────────
|
|
689
|
-
async Throttle(minIntervalMs) {
|
|
690
|
-
const elapsed = Date.now() - this.lastRequestTime;
|
|
691
|
-
if (elapsed < minIntervalMs) {
|
|
692
|
-
await this.Sleep(minIntervalMs - elapsed);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
Sleep(ms) {
|
|
696
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
697
|
-
}
|
|
698
|
-
BuildCRUDError(err, operation, objectName) {
|
|
699
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
700
|
-
return {
|
|
701
|
-
Success: false,
|
|
702
|
-
ErrorMessage: `${operation} failed for ${objectName}: ${message}`,
|
|
703
|
-
StatusCode: 500,
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
};
|
|
707
|
-
QuickBooksConnector = __decorate([
|
|
708
|
-
RegisterClass(BaseIntegrationConnector, 'QuickBooksConnector')
|
|
709
|
-
], QuickBooksConnector);
|
|
710
|
-
export { QuickBooksConnector };
|
|
711
|
-
/** Tree-shaking prevention function — import and call from module entry point */
|
|
712
|
-
export function LoadQuickBooksConnector() { }
|
|
713
|
-
//# sourceMappingURL=QuickBooksConnector.js.map
|