@memberjunction/integration-connectors 5.13.0 → 5.15.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/README.md +241 -50
- package/dist/HubSpotConnector.d.ts +82 -4
- package/dist/HubSpotConnector.d.ts.map +1 -1
- package/dist/HubSpotConnector.js +649 -104
- package/dist/HubSpotConnector.js.map +1 -1
- package/dist/QuickBooksConnector.d.ts +92 -0
- package/dist/QuickBooksConnector.d.ts.map +1 -0
- package/dist/QuickBooksConnector.js +693 -0
- package/dist/QuickBooksConnector.js.map +1 -0
- package/dist/RasaConnector.d.ts +4 -19
- package/dist/RasaConnector.d.ts.map +1 -1
- package/dist/RasaConnector.js +74 -0
- package/dist/RasaConnector.js.map +1 -1
- package/dist/SageIntacctConnector.d.ts +179 -0
- package/dist/SageIntacctConnector.d.ts.map +1 -0
- package/dist/SageIntacctConnector.js +1008 -0
- package/dist/SageIntacctConnector.js.map +1 -0
- package/dist/SalesforceConnector.d.ts +233 -18
- package/dist/SalesforceConnector.d.ts.map +1 -1
- package/dist/SalesforceConnector.js +1136 -32
- package/dist/SalesforceConnector.js.map +1 -1
- package/dist/YourMembershipConnector.d.ts +4 -25
- package/dist/YourMembershipConnector.d.ts.map +1 -1
- package/dist/YourMembershipConnector.js +147 -3
- package/dist/YourMembershipConnector.js.map +1 -1
- package/dist/generate-integration-actions.d.ts +23 -0
- package/dist/generate-integration-actions.d.ts.map +1 -0
- package/dist/generate-integration-actions.js +201 -0
- package/dist/generate-integration-actions.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
|
@@ -0,0 +1,1008 @@
|
|
|
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
|
+
/** Sage Intacct XML Gateway endpoint */
|
|
12
|
+
const INTACCT_API_ENDPOINT = 'https://api.intacct.com/ia/xml/xmlgw.phtml';
|
|
13
|
+
/** Control ID prefix for XML requests */
|
|
14
|
+
const CONTROL_ID_PREFIX = 'mj-intacct';
|
|
15
|
+
/** DTD version for Sage Intacct API */
|
|
16
|
+
const DTD_VERSION = '3.0';
|
|
17
|
+
/** Default maximum retries */
|
|
18
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
19
|
+
/** Default HTTP request timeout in milliseconds */
|
|
20
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
21
|
+
/** Default minimum milliseconds between API requests */
|
|
22
|
+
const DEFAULT_MIN_REQUEST_INTERVAL_MS = 200;
|
|
23
|
+
/** Default page size for readByQuery */
|
|
24
|
+
const DEFAULT_PAGE_SIZE = 100;
|
|
25
|
+
/** Session lifetime — Sage Intacct sessions last ~10 minutes; refresh at 8 min */
|
|
26
|
+
const SESSION_LIFETIME_MS = 8 * 60 * 1000;
|
|
27
|
+
/** Sage Intacct data type to generic type mapping */
|
|
28
|
+
const INTACCT_TYPE_MAP = {
|
|
29
|
+
'string': 'string',
|
|
30
|
+
'varchar': 'string',
|
|
31
|
+
'text': 'text',
|
|
32
|
+
'integer': 'integer',
|
|
33
|
+
'int': 'integer',
|
|
34
|
+
'decimal': 'decimal',
|
|
35
|
+
'number': 'decimal',
|
|
36
|
+
'float': 'decimal',
|
|
37
|
+
'double': 'decimal',
|
|
38
|
+
'currency': 'decimal',
|
|
39
|
+
'boolean': 'boolean',
|
|
40
|
+
'date': 'date',
|
|
41
|
+
'datetime': 'datetime',
|
|
42
|
+
'timestamp': 'datetime',
|
|
43
|
+
'percent': 'decimal',
|
|
44
|
+
};
|
|
45
|
+
// ─── Sage Intacct Object Metadata for Action Generation ───────────────
|
|
46
|
+
const SAGE_INTACCT_OBJECTS = [
|
|
47
|
+
{
|
|
48
|
+
Name: 'CUSTOMER', DisplayName: 'Customer',
|
|
49
|
+
Description: 'A customer record in Sage Intacct', SupportsWrite: true,
|
|
50
|
+
Fields: [
|
|
51
|
+
{ Name: 'CUSTOMERID', DisplayName: 'Customer ID', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: true, Description: 'Unique customer identifier' },
|
|
52
|
+
{ Name: 'NAME', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Customer name' },
|
|
53
|
+
{ Name: 'DISPLAYCONTACT.EMAIL1', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary email address' },
|
|
54
|
+
{ Name: 'DISPLAYCONTACT.PHONE1', DisplayName: 'Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary phone number' },
|
|
55
|
+
{ Name: 'DISPLAYCONTACT.MAILADDRESS.ADDRESS1', DisplayName: 'Address Line 1', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Mailing address line 1' },
|
|
56
|
+
{ Name: 'DISPLAYCONTACT.MAILADDRESS.CITY', DisplayName: 'City', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'City' },
|
|
57
|
+
{ Name: 'DISPLAYCONTACT.MAILADDRESS.STATE', DisplayName: 'State', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'State/province' },
|
|
58
|
+
{ Name: 'DISPLAYCONTACT.MAILADDRESS.ZIP', DisplayName: 'Zip Code', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Postal/zip code' },
|
|
59
|
+
{ Name: 'DISPLAYCONTACT.MAILADDRESS.COUNTRY', DisplayName: 'Country', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Country' },
|
|
60
|
+
{ Name: 'STATUS', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Customer status (active/inactive)' },
|
|
61
|
+
{ Name: 'TOTALDUE', DisplayName: 'Total Due', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total amount due' },
|
|
62
|
+
{ Name: 'WHENMODIFIED', DisplayName: 'When Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modified timestamp' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
Name: 'VENDOR', DisplayName: 'Vendor',
|
|
67
|
+
Description: 'A vendor/supplier record in Sage Intacct', SupportsWrite: true,
|
|
68
|
+
Fields: [
|
|
69
|
+
{ Name: 'VENDORID', DisplayName: 'Vendor ID', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: true, Description: 'Unique vendor identifier' },
|
|
70
|
+
{ Name: 'NAME', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Vendor name' },
|
|
71
|
+
{ Name: 'DISPLAYCONTACT.EMAIL1', DisplayName: 'Email', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary email address' },
|
|
72
|
+
{ Name: 'DISPLAYCONTACT.PHONE1', DisplayName: 'Phone', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Primary phone number' },
|
|
73
|
+
{ Name: 'STATUS', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Vendor status' },
|
|
74
|
+
{ Name: 'VENDTYPE', DisplayName: 'Vendor Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Vendor type category' },
|
|
75
|
+
{ Name: 'TOTALDUE', DisplayName: 'Total Due', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total amount due' },
|
|
76
|
+
{ Name: 'WHENMODIFIED', DisplayName: 'When Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modified timestamp' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
Name: 'GLACCOUNT', DisplayName: 'GL Account',
|
|
81
|
+
Description: 'A General Ledger account in Sage Intacct', SupportsWrite: true,
|
|
82
|
+
Fields: [
|
|
83
|
+
{ Name: 'ACCOUNTNO', DisplayName: 'Account Number', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: true, Description: 'GL account number' },
|
|
84
|
+
{ Name: 'TITLE', DisplayName: 'Title', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Account title' },
|
|
85
|
+
{ Name: 'ACCOUNTTYPE', DisplayName: 'Account Type', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Account type (e.g., balancesheet, incomestatement)' },
|
|
86
|
+
{ Name: 'NORMALBALANCE', DisplayName: 'Normal Balance', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Normal balance (debit/credit)' },
|
|
87
|
+
{ Name: 'STATUS', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Account status' },
|
|
88
|
+
{ Name: 'CATEGORY', DisplayName: 'Category', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Account category' },
|
|
89
|
+
{ Name: 'WHENMODIFIED', DisplayName: 'When Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modified timestamp' },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
Name: 'APBILL', DisplayName: 'AP Bill',
|
|
94
|
+
Description: 'An Accounts Payable bill in Sage Intacct', SupportsWrite: true,
|
|
95
|
+
Fields: [
|
|
96
|
+
{ Name: 'RECORDNO', DisplayName: 'Record Number', Type: 'integer', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'System-assigned record number' },
|
|
97
|
+
{ Name: 'VENDORID', DisplayName: 'Vendor ID', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Vendor ID' },
|
|
98
|
+
{ Name: 'VENDORNAME', DisplayName: 'Vendor Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Vendor name' },
|
|
99
|
+
{ Name: 'WHENCREATED', DisplayName: 'Date Created', Type: 'date', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Bill date' },
|
|
100
|
+
{ Name: 'WHENDUE', DisplayName: 'Due Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Payment due date' },
|
|
101
|
+
{ Name: 'TOTALAMOUNT', DisplayName: 'Total Amount', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total bill amount' },
|
|
102
|
+
{ Name: 'TOTALDUE', DisplayName: 'Total Due', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Amount remaining' },
|
|
103
|
+
{ Name: 'STATE', DisplayName: 'State', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Bill state (Draft, Pending, etc.)' },
|
|
104
|
+
{ Name: 'DESCRIPTION', DisplayName: 'Description', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Bill description/memo' },
|
|
105
|
+
{ Name: 'WHENMODIFIED', DisplayName: 'When Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modified timestamp' },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
Name: 'ARINVOICE', DisplayName: 'AR Invoice',
|
|
110
|
+
Description: 'An Accounts Receivable invoice in Sage Intacct', SupportsWrite: true,
|
|
111
|
+
Fields: [
|
|
112
|
+
{ Name: 'RECORDNO', DisplayName: 'Record Number', Type: 'integer', IsRequired: false, IsReadOnly: true, IsPrimaryKey: true, Description: 'System-assigned record number' },
|
|
113
|
+
{ Name: 'CUSTOMERID', DisplayName: 'Customer ID', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Customer ID' },
|
|
114
|
+
{ Name: 'CUSTOMERNAME', DisplayName: 'Customer Name', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Customer name' },
|
|
115
|
+
{ Name: 'WHENCREATED', DisplayName: 'Date Created', Type: 'date', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Invoice date' },
|
|
116
|
+
{ Name: 'WHENDUE', DisplayName: 'Due Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Payment due date' },
|
|
117
|
+
{ Name: 'TOTALAMOUNT', DisplayName: 'Total Amount', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Total invoice amount' },
|
|
118
|
+
{ Name: 'TOTALDUE', DisplayName: 'Total Due', Type: 'decimal', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Amount remaining' },
|
|
119
|
+
{ Name: 'STATE', DisplayName: 'State', Type: 'string', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Invoice state' },
|
|
120
|
+
{ Name: 'DESCRIPTION', DisplayName: 'Description', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Invoice description/memo' },
|
|
121
|
+
{ Name: 'WHENMODIFIED', DisplayName: 'When Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modified timestamp' },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
Name: 'PROJECT', DisplayName: 'Project',
|
|
126
|
+
Description: 'A project in Sage Intacct', SupportsWrite: true,
|
|
127
|
+
Fields: [
|
|
128
|
+
{ Name: 'PROJECTID', DisplayName: 'Project ID', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: true, Description: 'Unique project identifier' },
|
|
129
|
+
{ Name: 'NAME', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Project name' },
|
|
130
|
+
{ Name: 'PROJECTCATEGORY', DisplayName: 'Category', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Project category' },
|
|
131
|
+
{ Name: 'STATUS', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Project status' },
|
|
132
|
+
{ Name: 'CUSTOMERID', DisplayName: 'Customer ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Associated customer ID' },
|
|
133
|
+
{ Name: 'BEGINDATE', DisplayName: 'Begin Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Project start date' },
|
|
134
|
+
{ Name: 'ENDDATE', DisplayName: 'End Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Project end date' },
|
|
135
|
+
{ Name: 'WHENMODIFIED', DisplayName: 'When Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modified timestamp' },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
Name: 'EMPLOYEE', DisplayName: 'Employee',
|
|
140
|
+
Description: 'An employee record in Sage Intacct', SupportsWrite: true,
|
|
141
|
+
Fields: [
|
|
142
|
+
{ Name: 'EMPLOYEEID', DisplayName: 'Employee ID', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: true, Description: 'Unique employee identifier' },
|
|
143
|
+
{ Name: 'PERSONALINFO.CONTACTNAME', DisplayName: 'Contact Name', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Employee contact name' },
|
|
144
|
+
{ Name: 'TITLE', DisplayName: 'Title', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Job title' },
|
|
145
|
+
{ Name: 'STATUS', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Employee status' },
|
|
146
|
+
{ Name: 'DEPARTMENTID', DisplayName: 'Department ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Department identifier' },
|
|
147
|
+
{ Name: 'LOCATIONID', DisplayName: 'Location ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Location identifier' },
|
|
148
|
+
{ Name: 'STARTDATE', DisplayName: 'Start Date', Type: 'date', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Employment start date' },
|
|
149
|
+
{ Name: 'WHENMODIFIED', DisplayName: 'When Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modified timestamp' },
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
Name: 'DEPARTMENT', DisplayName: 'Department',
|
|
154
|
+
Description: 'A department in Sage Intacct', SupportsWrite: true,
|
|
155
|
+
Fields: [
|
|
156
|
+
{ Name: 'DEPARTMENTID', DisplayName: 'Department ID', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: true, Description: 'Unique department identifier' },
|
|
157
|
+
{ Name: 'TITLE', DisplayName: 'Title', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Department title' },
|
|
158
|
+
{ Name: 'PARENTID', DisplayName: 'Parent ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Parent department ID' },
|
|
159
|
+
{ Name: 'STATUS', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Department status' },
|
|
160
|
+
{ Name: 'WHENMODIFIED', DisplayName: 'When Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modified timestamp' },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
Name: 'CLASS', DisplayName: 'Class',
|
|
165
|
+
Description: 'A class dimension in Sage Intacct for categorizing transactions', SupportsWrite: true,
|
|
166
|
+
Fields: [
|
|
167
|
+
{ Name: 'CLASSID', DisplayName: 'Class ID', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: true, Description: 'Unique class identifier' },
|
|
168
|
+
{ Name: 'NAME', DisplayName: 'Name', Type: 'string', IsRequired: true, IsReadOnly: false, IsPrimaryKey: false, Description: 'Class name' },
|
|
169
|
+
{ Name: 'PARENTID', DisplayName: 'Parent ID', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Parent class ID' },
|
|
170
|
+
{ Name: 'STATUS', DisplayName: 'Status', Type: 'string', IsRequired: false, IsReadOnly: false, IsPrimaryKey: false, Description: 'Class status' },
|
|
171
|
+
{ Name: 'WHENMODIFIED', DisplayName: 'When Modified', Type: 'datetime', IsRequired: false, IsReadOnly: true, IsPrimaryKey: false, Description: 'Last modified timestamp' },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
/** Primary key field name for each known Sage Intacct object */
|
|
176
|
+
const OBJECT_PK_MAP = {
|
|
177
|
+
CUSTOMER: 'CUSTOMERID',
|
|
178
|
+
VENDOR: 'VENDORID',
|
|
179
|
+
GLACCOUNT: 'ACCOUNTNO',
|
|
180
|
+
APBILL: 'RECORDNO',
|
|
181
|
+
ARINVOICE: 'RECORDNO',
|
|
182
|
+
PROJECT: 'PROJECTID',
|
|
183
|
+
EMPLOYEE: 'EMPLOYEEID',
|
|
184
|
+
DEPARTMENT: 'DEPARTMENTID',
|
|
185
|
+
CLASS: 'CLASSID',
|
|
186
|
+
};
|
|
187
|
+
/** Objects that use legacy API functions (create_*, update_*, delete_*) instead of the
|
|
188
|
+
* generic CRUD API. These require different XML structures. */
|
|
189
|
+
const LEGACY_FUNCTION_OBJECTS = new Set(['APBILL', 'ARINVOICE']);
|
|
190
|
+
// ─── Connector Implementation ─────────────────────────────────────────
|
|
191
|
+
/**
|
|
192
|
+
* Connector for the Sage Intacct Web Services API (XML over HTTPS).
|
|
193
|
+
*
|
|
194
|
+
* Extends BaseIntegrationConnector directly (not BaseRESTIntegrationConnector)
|
|
195
|
+
* because Sage Intacct uses XML request/response bodies over a single gateway
|
|
196
|
+
* endpoint, not standard REST resource URLs.
|
|
197
|
+
*
|
|
198
|
+
* Auth flow:
|
|
199
|
+
* 1. POST XML to https://api.intacct.com/ia/xml/xmlgw.phtml
|
|
200
|
+
* 2. The request includes Sender ID/Password (app credentials) and User ID/Password
|
|
201
|
+
* 3. A getAPISession function returns a sessionid + endpoint
|
|
202
|
+
* 4. Subsequent requests use the sessionid for authentication
|
|
203
|
+
*
|
|
204
|
+
* API operations:
|
|
205
|
+
* - readByQuery: Query records with SQL-like filter expressions
|
|
206
|
+
* - readMore: Fetch next page of a paginated result set
|
|
207
|
+
* - read: Get a single record by key
|
|
208
|
+
* - create / update / delete: Standard CRUD on objects
|
|
209
|
+
* - inspect: Discover object fields and types
|
|
210
|
+
*
|
|
211
|
+
* Pagination:
|
|
212
|
+
* - readByQuery returns a resultId + numRemaining
|
|
213
|
+
* - readMore uses the resultId to fetch subsequent pages
|
|
214
|
+
*/
|
|
215
|
+
let SageIntacctConnector = class SageIntacctConnector extends BaseIntegrationConnector {
|
|
216
|
+
constructor() {
|
|
217
|
+
// ── Session Cache ────────────────────────────────────────────────
|
|
218
|
+
super(...arguments);
|
|
219
|
+
this.cachedSession = null;
|
|
220
|
+
/** Timestamp of the last API request, used for throttling */
|
|
221
|
+
this.lastRequestTime = 0;
|
|
222
|
+
}
|
|
223
|
+
// ── Capability Getters ───────────────────────────────────────────
|
|
224
|
+
get SupportsCreate() { return true; }
|
|
225
|
+
get SupportsUpdate() { return true; }
|
|
226
|
+
get SupportsDelete() { return true; }
|
|
227
|
+
get SupportsSearch() { return true; }
|
|
228
|
+
get SupportsListing() { return true; }
|
|
229
|
+
get IntegrationName() { return 'Sage Intacct'; }
|
|
230
|
+
// ── Action Generation ────────────────────────────────────────────
|
|
231
|
+
GetIntegrationObjects() {
|
|
232
|
+
return SAGE_INTACCT_OBJECTS;
|
|
233
|
+
}
|
|
234
|
+
GetActionGeneratorConfig() {
|
|
235
|
+
const objects = this.GetIntegrationObjects();
|
|
236
|
+
if (objects.length === 0)
|
|
237
|
+
return null;
|
|
238
|
+
return {
|
|
239
|
+
IntegrationName: 'Sage Intacct',
|
|
240
|
+
CategoryName: 'Sage Intacct',
|
|
241
|
+
IconClass: 'fa-solid fa-file-invoice-dollar',
|
|
242
|
+
Objects: objects,
|
|
243
|
+
IncludeSearch: true,
|
|
244
|
+
IncludeList: true,
|
|
245
|
+
CategoryDescription: 'Sage Intacct accounting and ERP integration actions',
|
|
246
|
+
ParentCategoryName: 'Business Apps',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// ── Default Configuration ────────────────────────────────────────
|
|
250
|
+
GetDefaultConfiguration() {
|
|
251
|
+
return {
|
|
252
|
+
DefaultSchemaName: 'SageIntacct',
|
|
253
|
+
DefaultObjects: [
|
|
254
|
+
{
|
|
255
|
+
SourceObjectName: 'CUSTOMER',
|
|
256
|
+
TargetTableName: 'SageIntacct_Customer',
|
|
257
|
+
TargetEntityName: 'Sage Intacct Customers',
|
|
258
|
+
SyncEnabled: true,
|
|
259
|
+
FieldMappings: this.GetDefaultFieldMappings('CUSTOMER', 'Contacts'),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
SourceObjectName: 'VENDOR',
|
|
263
|
+
TargetTableName: 'SageIntacct_Vendor',
|
|
264
|
+
TargetEntityName: 'Sage Intacct Vendors',
|
|
265
|
+
SyncEnabled: true,
|
|
266
|
+
FieldMappings: this.GetDefaultFieldMappings('VENDOR', 'Companies'),
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
SourceObjectName: 'GLACCOUNT',
|
|
270
|
+
TargetTableName: 'SageIntacct_GLAccount',
|
|
271
|
+
TargetEntityName: 'Sage Intacct GL Accounts',
|
|
272
|
+
SyncEnabled: true,
|
|
273
|
+
FieldMappings: [],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
GetDefaultFieldMappings(objectName, _entityName) {
|
|
279
|
+
switch (objectName) {
|
|
280
|
+
case 'CUSTOMER':
|
|
281
|
+
return [
|
|
282
|
+
{ SourceFieldName: 'CUSTOMERID', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
283
|
+
{ SourceFieldName: 'NAME', DestinationFieldName: 'Name' },
|
|
284
|
+
{ SourceFieldName: 'DISPLAYCONTACT.EMAIL1', DestinationFieldName: 'Email' },
|
|
285
|
+
{ SourceFieldName: 'DISPLAYCONTACT.PHONE1', DestinationFieldName: 'Phone' },
|
|
286
|
+
{ SourceFieldName: 'STATUS', DestinationFieldName: 'Status' },
|
|
287
|
+
];
|
|
288
|
+
case 'VENDOR':
|
|
289
|
+
return [
|
|
290
|
+
{ SourceFieldName: 'VENDORID', DestinationFieldName: 'ExternalID', IsKeyField: true },
|
|
291
|
+
{ SourceFieldName: 'NAME', DestinationFieldName: 'Name' },
|
|
292
|
+
{ SourceFieldName: 'DISPLAYCONTACT.EMAIL1', DestinationFieldName: 'Email' },
|
|
293
|
+
{ SourceFieldName: 'DISPLAYCONTACT.PHONE1', DestinationFieldName: 'Phone' },
|
|
294
|
+
{ SourceFieldName: 'STATUS', DestinationFieldName: 'Status' },
|
|
295
|
+
];
|
|
296
|
+
default:
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// ─── TestConnection ──────────────────────────────────────────────
|
|
301
|
+
async TestConnection(companyIntegration, contextUser) {
|
|
302
|
+
try {
|
|
303
|
+
const session = await this.GetSession(companyIntegration, contextUser, true);
|
|
304
|
+
return {
|
|
305
|
+
Success: true,
|
|
306
|
+
Message: `Successfully connected to Sage Intacct (Company: ${session.Config.CompanyId})`,
|
|
307
|
+
ServerVersion: `Sage Intacct Web Services API ${DTD_VERSION}`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
312
|
+
return { Success: false, Message: `Connection failed: ${message}` };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ─── Discovery ───────────────────────────────────────────────────
|
|
316
|
+
/**
|
|
317
|
+
* Discovers available objects using the inspect API function with '*' wildcard.
|
|
318
|
+
* Falls back to the known object list if the API call fails.
|
|
319
|
+
*/
|
|
320
|
+
async DiscoverObjects(companyIntegration, contextUser) {
|
|
321
|
+
try {
|
|
322
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
323
|
+
const xml = this.BuildInspectRequest(session, '*');
|
|
324
|
+
const response = await this.SendXMLRequest(session, xml);
|
|
325
|
+
const types = this.ParseInspectObjectsResponse(response);
|
|
326
|
+
return types.map(name => ({
|
|
327
|
+
Name: name,
|
|
328
|
+
Label: this.FormatLabel(name),
|
|
329
|
+
SupportsIncrementalSync: true,
|
|
330
|
+
SupportsWrite: true,
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// Fallback to known objects
|
|
335
|
+
return SAGE_INTACCT_OBJECTS.map(obj => ({
|
|
336
|
+
Name: obj.Name,
|
|
337
|
+
Label: obj.DisplayName,
|
|
338
|
+
Description: obj.Description,
|
|
339
|
+
SupportsIncrementalSync: true,
|
|
340
|
+
SupportsWrite: obj.SupportsWrite,
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Discovers fields on a specific object using the inspect API function.
|
|
346
|
+
*/
|
|
347
|
+
async DiscoverFields(companyIntegration, objectName, contextUser) {
|
|
348
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
349
|
+
const xml = this.BuildInspectRequest(session, objectName);
|
|
350
|
+
const response = await this.SendXMLRequest(session, xml);
|
|
351
|
+
const fields = this.ParseInspectFieldsResponse(response);
|
|
352
|
+
return fields.map(f => ({
|
|
353
|
+
Name: f.Name,
|
|
354
|
+
Label: f.Label || this.FormatLabel(f.Name),
|
|
355
|
+
DataType: INTACCT_TYPE_MAP[f.DataType.toLowerCase()] ?? 'string',
|
|
356
|
+
IsRequired: f.IsRequired,
|
|
357
|
+
IsUniqueKey: f.Name === this.GetPrimaryKeyField(objectName),
|
|
358
|
+
IsReadOnly: f.IsReadOnly,
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
// ─── FetchChanges ────────────────────────────────────────────────
|
|
362
|
+
/**
|
|
363
|
+
* Fetches records using readByQuery with WHENMODIFIED watermark filtering.
|
|
364
|
+
* Supports pagination via readMore with resultId tokens.
|
|
365
|
+
*/
|
|
366
|
+
async FetchChanges(ctx) {
|
|
367
|
+
const session = await this.GetSession(ctx.CompanyIntegration, ctx.ContextUser);
|
|
368
|
+
const pkField = this.GetPrimaryKeyField(ctx.ObjectName);
|
|
369
|
+
const pageSize = Math.min(ctx.BatchSize || DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE);
|
|
370
|
+
// If we have a cursor (resultId) from a previous call, use readMore
|
|
371
|
+
if (ctx.CurrentCursor) {
|
|
372
|
+
return this.FetchMoreRecords(session, ctx.CurrentCursor, ctx.ObjectName, pkField);
|
|
373
|
+
}
|
|
374
|
+
// Build the query filter
|
|
375
|
+
const filter = ctx.WatermarkValue
|
|
376
|
+
? `WHENMODIFIED >= '${ctx.WatermarkValue}'`
|
|
377
|
+
: '';
|
|
378
|
+
const xml = this.BuildReadByQueryRequest(session, ctx.ObjectName, filter, pageSize);
|
|
379
|
+
const response = await this.SendXMLRequest(session, xml);
|
|
380
|
+
return this.ParseReadByQueryResponse(response, ctx.ObjectName, pkField);
|
|
381
|
+
}
|
|
382
|
+
// ─── CRUD Operations ─────────────────────────────────────────────
|
|
383
|
+
async CreateRecord(ctx) {
|
|
384
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
385
|
+
const contextUser = ctx.ContextUser;
|
|
386
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
387
|
+
const xml = this.BuildCreateRequest(session, ctx.ObjectName, ctx.Attributes);
|
|
388
|
+
try {
|
|
389
|
+
const response = await this.SendXMLRequest(session, xml);
|
|
390
|
+
const recordKey = this.ExtractRecordKey(response, ctx.ObjectName);
|
|
391
|
+
return { Success: true, ExternalID: recordKey, StatusCode: 200 };
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
return this.BuildCRUDError(err, 'CreateRecord', ctx.ObjectName);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async UpdateRecord(ctx) {
|
|
398
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
399
|
+
const contextUser = ctx.ContextUser;
|
|
400
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
401
|
+
const xml = this.BuildUpdateRequest(session, ctx.ObjectName, ctx.ExternalID, ctx.Attributes);
|
|
402
|
+
try {
|
|
403
|
+
await this.SendXMLRequest(session, xml);
|
|
404
|
+
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: 200 };
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
return this.BuildCRUDError(err, 'UpdateRecord', ctx.ObjectName);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async DeleteRecord(ctx) {
|
|
411
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
412
|
+
const contextUser = ctx.ContextUser;
|
|
413
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
414
|
+
const xml = this.BuildDeleteRequest(session, ctx.ObjectName, ctx.ExternalID);
|
|
415
|
+
try {
|
|
416
|
+
await this.SendXMLRequest(session, xml);
|
|
417
|
+
return { Success: true, ExternalID: ctx.ExternalID, StatusCode: 200 };
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
return this.BuildCRUDError(err, 'DeleteRecord', ctx.ObjectName);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async GetRecord(ctx) {
|
|
424
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
425
|
+
const contextUser = ctx.ContextUser;
|
|
426
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
427
|
+
const pkField = this.GetPrimaryKeyField(ctx.ObjectName);
|
|
428
|
+
const xml = this.BuildReadRequest(session, ctx.ObjectName, ctx.ExternalID);
|
|
429
|
+
try {
|
|
430
|
+
const response = await this.SendXMLRequest(session, xml);
|
|
431
|
+
const record = this.ParseSingleRecord(response, ctx.ObjectName);
|
|
432
|
+
if (!record)
|
|
433
|
+
return null;
|
|
434
|
+
return {
|
|
435
|
+
ExternalID: String(record[pkField] ?? ctx.ExternalID),
|
|
436
|
+
ObjectType: ctx.ObjectName,
|
|
437
|
+
Fields: record,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async SearchRecords(ctx) {
|
|
445
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
446
|
+
const contextUser = ctx.ContextUser;
|
|
447
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
448
|
+
const pkField = this.GetPrimaryKeyField(ctx.ObjectName);
|
|
449
|
+
const pageSize = ctx.PageSize ?? DEFAULT_PAGE_SIZE;
|
|
450
|
+
const filterParts = Object.entries(ctx.Filters).map(([field, value]) => `${field} = '${this.EscapeXmlValue(String(value))}'`);
|
|
451
|
+
const filter = filterParts.join(' AND ');
|
|
452
|
+
const xml = this.BuildReadByQueryRequest(session, ctx.ObjectName, filter, pageSize);
|
|
453
|
+
const response = await this.SendXMLRequest(session, xml);
|
|
454
|
+
const result = this.ParseReadByQueryResponse(response, ctx.ObjectName, pkField);
|
|
455
|
+
return {
|
|
456
|
+
Records: result.Records,
|
|
457
|
+
TotalCount: result.Records.length + (result.HasMore ? 1 : 0), // Intacct doesn't give exact total in readByQuery
|
|
458
|
+
HasMore: result.HasMore,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
async ListRecords(ctx) {
|
|
462
|
+
const companyIntegration = ctx.CompanyIntegration;
|
|
463
|
+
const contextUser = ctx.ContextUser;
|
|
464
|
+
const session = await this.GetSession(companyIntegration, contextUser);
|
|
465
|
+
const pkField = this.GetPrimaryKeyField(ctx.ObjectName);
|
|
466
|
+
const pageSize = ctx.PageSize ?? DEFAULT_PAGE_SIZE;
|
|
467
|
+
if (ctx.Cursor) {
|
|
468
|
+
const result = await this.FetchMoreRecords(session, ctx.Cursor, ctx.ObjectName, pkField);
|
|
469
|
+
return {
|
|
470
|
+
Records: result.Records,
|
|
471
|
+
HasMore: result.HasMore,
|
|
472
|
+
NextCursor: result.HasMore ? result.NextCursor : undefined,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const filter = ctx.Filter
|
|
476
|
+
? Object.entries(ctx.Filter).map(([k, v]) => `${k} = '${this.EscapeXmlValue(String(v))}'`).join(' AND ')
|
|
477
|
+
: '';
|
|
478
|
+
const xml = this.BuildReadByQueryRequest(session, ctx.ObjectName, filter, pageSize);
|
|
479
|
+
const response = await this.SendXMLRequest(session, xml);
|
|
480
|
+
const result = this.ParseReadByQueryResponse(response, ctx.ObjectName, pkField);
|
|
481
|
+
return {
|
|
482
|
+
Records: result.Records,
|
|
483
|
+
HasMore: result.HasMore,
|
|
484
|
+
NextCursor: result.HasMore ? result.NextCursor : undefined,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
// ─── Session Management ──────────────────────────────────────────
|
|
488
|
+
/**
|
|
489
|
+
* Gets or refreshes a Sage Intacct API session.
|
|
490
|
+
* Sessions are cached and reused until they approach expiration.
|
|
491
|
+
*/
|
|
492
|
+
async GetSession(companyIntegration, contextUser, forceRefresh = false) {
|
|
493
|
+
if (!forceRefresh && this.cachedSession && this.IsSessionValid()) {
|
|
494
|
+
return this.cachedSession;
|
|
495
|
+
}
|
|
496
|
+
const config = await this.ParseConfig(companyIntegration, contextUser);
|
|
497
|
+
const xml = this.BuildSessionRequest(config);
|
|
498
|
+
const responseXml = await this.PostXML(INTACCT_API_ENDPOINT, xml, config);
|
|
499
|
+
const session = this.ParseSessionResponse(responseXml, config);
|
|
500
|
+
this.cachedSession = session;
|
|
501
|
+
return session;
|
|
502
|
+
}
|
|
503
|
+
IsSessionValid() {
|
|
504
|
+
if (!this.cachedSession)
|
|
505
|
+
return false;
|
|
506
|
+
return (Date.now() - this.cachedSession.CreatedAt) < SESSION_LIFETIME_MS;
|
|
507
|
+
}
|
|
508
|
+
// ─── Configuration Parsing ───────────────────────────────────────
|
|
509
|
+
/**
|
|
510
|
+
* Parses connection configuration from CompanyIntegration credentials.
|
|
511
|
+
* Looks for a linked MJCredentialEntity or falls back to Configuration JSON.
|
|
512
|
+
*/
|
|
513
|
+
async ParseConfig(companyIntegration, contextUser) {
|
|
514
|
+
// Try credential entity first
|
|
515
|
+
const credentialID = companyIntegration.Get('CredentialID');
|
|
516
|
+
if (credentialID) {
|
|
517
|
+
const config = await this.LoadFromCredentialEntity(credentialID, contextUser);
|
|
518
|
+
if (config)
|
|
519
|
+
return config;
|
|
520
|
+
}
|
|
521
|
+
// Fall back to Configuration JSON
|
|
522
|
+
const configJson = companyIntegration.Get('Configuration');
|
|
523
|
+
if (configJson) {
|
|
524
|
+
const parsed = JSON.parse(configJson);
|
|
525
|
+
return this.ValidateConfig(parsed);
|
|
526
|
+
}
|
|
527
|
+
throw new Error('SageIntacctConnector: No credentials or configuration found on CompanyIntegration');
|
|
528
|
+
}
|
|
529
|
+
async LoadFromCredentialEntity(credentialID, contextUser) {
|
|
530
|
+
const md = new Metadata();
|
|
531
|
+
const credential = await md.GetEntityObject('MJ: Credentials', contextUser);
|
|
532
|
+
const loaded = await credential.Load(credentialID);
|
|
533
|
+
if (!loaded || !credential.Values)
|
|
534
|
+
return null;
|
|
535
|
+
try {
|
|
536
|
+
const parsed = JSON.parse(credential.Values);
|
|
537
|
+
return this.ValidateConfig(parsed);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
ValidateConfig(raw) {
|
|
544
|
+
if (!raw.CompanyId)
|
|
545
|
+
throw new Error('SageIntacctConnector: CompanyId is required');
|
|
546
|
+
if (!raw.SenderId)
|
|
547
|
+
throw new Error('SageIntacctConnector: SenderId is required');
|
|
548
|
+
if (!raw.SenderPassword)
|
|
549
|
+
throw new Error('SageIntacctConnector: SenderPassword is required');
|
|
550
|
+
if (!raw.UserId)
|
|
551
|
+
throw new Error('SageIntacctConnector: UserId is required');
|
|
552
|
+
if (!raw.UserPassword)
|
|
553
|
+
throw new Error('SageIntacctConnector: UserPassword is required');
|
|
554
|
+
return {
|
|
555
|
+
CompanyId: raw.CompanyId,
|
|
556
|
+
SenderId: raw.SenderId,
|
|
557
|
+
SenderPassword: raw.SenderPassword,
|
|
558
|
+
UserId: raw.UserId,
|
|
559
|
+
UserPassword: raw.UserPassword,
|
|
560
|
+
EntityId: raw.EntityId,
|
|
561
|
+
MaxRetries: raw.MaxRetries ?? DEFAULT_MAX_RETRIES,
|
|
562
|
+
RequestTimeoutMs: raw.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
563
|
+
MinRequestIntervalMs: raw.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
// ─── XML Request Builders ────────────────────────────────────────
|
|
567
|
+
BuildSessionRequest(config) {
|
|
568
|
+
const entityTag = config.EntityId
|
|
569
|
+
? `<locationid>${this.EscapeXmlValue(config.EntityId)}</locationid>`
|
|
570
|
+
: '';
|
|
571
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
572
|
+
<request>
|
|
573
|
+
<control>
|
|
574
|
+
<senderid>${this.EscapeXmlValue(config.SenderId)}</senderid>
|
|
575
|
+
<password>${this.EscapeXmlValue(config.SenderPassword)}</password>
|
|
576
|
+
<controlid>${CONTROL_ID_PREFIX}-session-${Date.now()}</controlid>
|
|
577
|
+
<uniqueid>false</uniqueid>
|
|
578
|
+
<dtdversion>${DTD_VERSION}</dtdversion>
|
|
579
|
+
<includewhitespace>false</includewhitespace>
|
|
580
|
+
</control>
|
|
581
|
+
<operation>
|
|
582
|
+
<authentication>
|
|
583
|
+
<login>
|
|
584
|
+
<userid>${this.EscapeXmlValue(config.UserId)}</userid>
|
|
585
|
+
<companyid>${this.EscapeXmlValue(config.CompanyId)}</companyid>
|
|
586
|
+
<password>${this.EscapeXmlValue(config.UserPassword)}</password>
|
|
587
|
+
${entityTag}
|
|
588
|
+
</login>
|
|
589
|
+
</authentication>
|
|
590
|
+
<content>
|
|
591
|
+
<function controlid="${CONTROL_ID_PREFIX}-getSession">
|
|
592
|
+
<getAPISession/>
|
|
593
|
+
</function>
|
|
594
|
+
</content>
|
|
595
|
+
</operation>
|
|
596
|
+
</request>`;
|
|
597
|
+
}
|
|
598
|
+
BuildReadByQueryRequest(session, objectName, filter, pageSize) {
|
|
599
|
+
return this.WrapInSessionRequest(session, `
|
|
600
|
+
<function controlid="${CONTROL_ID_PREFIX}-readByQuery-${Date.now()}">
|
|
601
|
+
<readByQuery>
|
|
602
|
+
<object>${this.EscapeXmlValue(objectName)}</object>
|
|
603
|
+
<fields>*</fields>
|
|
604
|
+
<query>${this.EscapeXmlValue(filter)}</query>
|
|
605
|
+
<pagesize>${pageSize}</pagesize>
|
|
606
|
+
</readByQuery>
|
|
607
|
+
</function>`);
|
|
608
|
+
}
|
|
609
|
+
BuildReadMoreRequest(session, resultId) {
|
|
610
|
+
return this.WrapInSessionRequest(session, `
|
|
611
|
+
<function controlid="${CONTROL_ID_PREFIX}-readMore-${Date.now()}">
|
|
612
|
+
<readMore>
|
|
613
|
+
<resultId>${this.EscapeXmlValue(resultId)}</resultId>
|
|
614
|
+
</readMore>
|
|
615
|
+
</function>`);
|
|
616
|
+
}
|
|
617
|
+
BuildReadRequest(session, objectName, key) {
|
|
618
|
+
return this.WrapInSessionRequest(session, `
|
|
619
|
+
<function controlid="${CONTROL_ID_PREFIX}-read-${Date.now()}">
|
|
620
|
+
<read>
|
|
621
|
+
<object>${this.EscapeXmlValue(objectName)}</object>
|
|
622
|
+
<keys>${this.EscapeXmlValue(key)}</keys>
|
|
623
|
+
<fields>*</fields>
|
|
624
|
+
</read>
|
|
625
|
+
</function>`);
|
|
626
|
+
}
|
|
627
|
+
BuildCreateRequest(session, objectName, attributes) {
|
|
628
|
+
const fieldsXml = this.AttributesToXml(attributes);
|
|
629
|
+
return this.WrapInSessionRequest(session, `
|
|
630
|
+
<function controlid="${CONTROL_ID_PREFIX}-create-${Date.now()}">
|
|
631
|
+
<create>
|
|
632
|
+
<${objectName}>
|
|
633
|
+
${fieldsXml}
|
|
634
|
+
</${objectName}>
|
|
635
|
+
</create>
|
|
636
|
+
</function>`);
|
|
637
|
+
}
|
|
638
|
+
BuildUpdateRequest(session, objectName, key, attributes) {
|
|
639
|
+
const pkField = this.GetPrimaryKeyField(objectName);
|
|
640
|
+
const fieldsXml = this.AttributesToXml({ [pkField]: key, ...attributes });
|
|
641
|
+
return this.WrapInSessionRequest(session, `
|
|
642
|
+
<function controlid="${CONTROL_ID_PREFIX}-update-${Date.now()}">
|
|
643
|
+
<update>
|
|
644
|
+
<${objectName}>
|
|
645
|
+
${fieldsXml}
|
|
646
|
+
</${objectName}>
|
|
647
|
+
</update>
|
|
648
|
+
</function>`);
|
|
649
|
+
}
|
|
650
|
+
BuildDeleteRequest(session, objectName, key) {
|
|
651
|
+
return this.WrapInSessionRequest(session, `
|
|
652
|
+
<function controlid="${CONTROL_ID_PREFIX}-delete-${Date.now()}">
|
|
653
|
+
<delete>
|
|
654
|
+
<object>${this.EscapeXmlValue(objectName)}</object>
|
|
655
|
+
<keys>${this.EscapeXmlValue(key)}</keys>
|
|
656
|
+
</delete>
|
|
657
|
+
</function>`);
|
|
658
|
+
}
|
|
659
|
+
BuildInspectRequest(session, objectNameOrWildcard) {
|
|
660
|
+
return this.WrapInSessionRequest(session, `
|
|
661
|
+
<function controlid="${CONTROL_ID_PREFIX}-inspect-${Date.now()}">
|
|
662
|
+
<inspect>
|
|
663
|
+
<object>${this.EscapeXmlValue(objectNameOrWildcard)}</object>
|
|
664
|
+
</inspect>
|
|
665
|
+
</function>`);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Wraps a function XML fragment in a session-authenticated request envelope.
|
|
669
|
+
*/
|
|
670
|
+
WrapInSessionRequest(session, functionXml) {
|
|
671
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
672
|
+
<request>
|
|
673
|
+
<control>
|
|
674
|
+
<senderid>${this.EscapeXmlValue(session.Config.SenderId)}</senderid>
|
|
675
|
+
<password>${this.EscapeXmlValue(session.Config.SenderPassword)}</password>
|
|
676
|
+
<controlid>${CONTROL_ID_PREFIX}-${Date.now()}</controlid>
|
|
677
|
+
<uniqueid>false</uniqueid>
|
|
678
|
+
<dtdversion>${DTD_VERSION}</dtdversion>
|
|
679
|
+
<includewhitespace>false</includewhitespace>
|
|
680
|
+
</control>
|
|
681
|
+
<operation>
|
|
682
|
+
<authentication>
|
|
683
|
+
<sessionid>${this.EscapeXmlValue(session.SessionId)}</sessionid>
|
|
684
|
+
</authentication>
|
|
685
|
+
<content>
|
|
686
|
+
${functionXml}
|
|
687
|
+
</content>
|
|
688
|
+
</operation>
|
|
689
|
+
</request>`;
|
|
690
|
+
}
|
|
691
|
+
// ─── XML Response Parsers ────────────────────────────────────────
|
|
692
|
+
ParseSessionResponse(xml, config) {
|
|
693
|
+
this.CheckForErrors(xml);
|
|
694
|
+
const sessionId = this.ExtractXmlValue(xml, 'sessionid');
|
|
695
|
+
const endpoint = this.ExtractXmlValue(xml, 'endpoint') || INTACCT_API_ENDPOINT;
|
|
696
|
+
if (!sessionId) {
|
|
697
|
+
throw new Error('SageIntacctConnector: getAPISession did not return a sessionid');
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
SessionId: sessionId,
|
|
701
|
+
Endpoint: endpoint,
|
|
702
|
+
CreatedAt: Date.now(),
|
|
703
|
+
Config: config,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
ParseReadByQueryResponse(xml, objectName, pkField) {
|
|
707
|
+
this.CheckForErrors(xml);
|
|
708
|
+
const numRemaining = parseInt(this.ExtractXmlValue(xml, 'numremaining') || '0', 10);
|
|
709
|
+
const resultId = this.ExtractXmlValue(xml, 'resultId') || undefined;
|
|
710
|
+
const records = this.ExtractRecords(xml, objectName);
|
|
711
|
+
const externalRecords = records.map(r => ({
|
|
712
|
+
ExternalID: String(r[pkField] ?? ''),
|
|
713
|
+
ObjectType: objectName,
|
|
714
|
+
Fields: r,
|
|
715
|
+
ModifiedAt: r['WHENMODIFIED'] ? new Date(String(r['WHENMODIFIED'])) : undefined,
|
|
716
|
+
}));
|
|
717
|
+
// Compute watermark from the latest WHENMODIFIED in this batch
|
|
718
|
+
const newWatermark = this.ComputeWatermark(records);
|
|
719
|
+
return {
|
|
720
|
+
Records: externalRecords,
|
|
721
|
+
HasMore: numRemaining > 0,
|
|
722
|
+
NewWatermarkValue: newWatermark,
|
|
723
|
+
NextCursor: numRemaining > 0 ? resultId : undefined,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
ParseSingleRecord(xml, objectName) {
|
|
727
|
+
this.CheckForErrors(xml);
|
|
728
|
+
const records = this.ExtractRecords(xml, objectName);
|
|
729
|
+
return records.length > 0 ? records[0] : null;
|
|
730
|
+
}
|
|
731
|
+
ParseInspectObjectsResponse(xml) {
|
|
732
|
+
this.CheckForErrors(xml);
|
|
733
|
+
const objectNames = [];
|
|
734
|
+
// inspect with '*' returns <type>OBJECTNAME</type> elements
|
|
735
|
+
const typeRegex = /<type[^>]*>([^<]+)<\/type>/gi;
|
|
736
|
+
let match;
|
|
737
|
+
while ((match = typeRegex.exec(xml)) !== null) {
|
|
738
|
+
objectNames.push(match[1].trim());
|
|
739
|
+
}
|
|
740
|
+
return objectNames;
|
|
741
|
+
}
|
|
742
|
+
ParseInspectFieldsResponse(xml) {
|
|
743
|
+
this.CheckForErrors(xml);
|
|
744
|
+
const fields = [];
|
|
745
|
+
// inspect returns <Field> elements with <Name>, <DataType>, etc.
|
|
746
|
+
const fieldRegex = /<Field>\s*([\s\S]*?)\s*<\/Field>/gi;
|
|
747
|
+
let match;
|
|
748
|
+
while ((match = fieldRegex.exec(xml)) !== null) {
|
|
749
|
+
const fieldXml = match[1];
|
|
750
|
+
const name = this.ExtractXmlValueFromFragment(fieldXml, 'Name');
|
|
751
|
+
const label = this.ExtractXmlValueFromFragment(fieldXml, 'DisplayLabel') || name;
|
|
752
|
+
const dataType = this.ExtractXmlValueFromFragment(fieldXml, 'DataName')
|
|
753
|
+
|| this.ExtractXmlValueFromFragment(fieldXml, 'DataType')
|
|
754
|
+
|| 'string';
|
|
755
|
+
const isRequired = this.ExtractXmlValueFromFragment(fieldXml, 'Required') === 'true';
|
|
756
|
+
const isReadOnly = this.ExtractXmlValueFromFragment(fieldXml, 'ReadOnly') === 'true'
|
|
757
|
+
|| this.ExtractXmlValueFromFragment(fieldXml, 'SystemGenerated') === 'true';
|
|
758
|
+
if (name) {
|
|
759
|
+
fields.push({ Name: name, Label: label, DataType: dataType, IsRequired: isRequired, IsReadOnly: isReadOnly });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return fields;
|
|
763
|
+
}
|
|
764
|
+
// ─── HTTP Transport ──────────────────────────────────────────────
|
|
765
|
+
/**
|
|
766
|
+
* Sends an XML request to the Sage Intacct API using a session.
|
|
767
|
+
* Handles throttling and retry logic.
|
|
768
|
+
*/
|
|
769
|
+
async SendXMLRequest(session, xml) {
|
|
770
|
+
return this.PostXML(session.Endpoint, xml, session.Config);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Posts XML to the Sage Intacct gateway with throttling and retry support.
|
|
774
|
+
*/
|
|
775
|
+
async PostXML(url, xml, config) {
|
|
776
|
+
const maxRetries = config.MaxRetries ?? DEFAULT_MAX_RETRIES;
|
|
777
|
+
const timeoutMs = config.RequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
778
|
+
const minInterval = config.MinRequestIntervalMs ?? DEFAULT_MIN_REQUEST_INTERVAL_MS;
|
|
779
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
780
|
+
await this.Throttle(minInterval);
|
|
781
|
+
try {
|
|
782
|
+
const controller = new AbortController();
|
|
783
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
784
|
+
const response = await fetch(url, {
|
|
785
|
+
method: 'POST',
|
|
786
|
+
headers: {
|
|
787
|
+
'Content-Type': 'application/xml',
|
|
788
|
+
'Accept': 'application/xml',
|
|
789
|
+
},
|
|
790
|
+
body: xml,
|
|
791
|
+
signal: controller.signal,
|
|
792
|
+
});
|
|
793
|
+
clearTimeout(timeoutHandle);
|
|
794
|
+
this.lastRequestTime = Date.now();
|
|
795
|
+
const responseText = await response.text();
|
|
796
|
+
if (!response.ok) {
|
|
797
|
+
throw new Error(`HTTP ${response.status}: ${responseText.slice(0, 500)}`);
|
|
798
|
+
}
|
|
799
|
+
return responseText;
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
803
|
+
const isRetryable = message.includes('abort') || message.includes('timeout')
|
|
804
|
+
|| message.includes('ECONNRESET') || message.includes('429')
|
|
805
|
+
|| message.includes('503');
|
|
806
|
+
if (!isRetryable || attempt === maxRetries) {
|
|
807
|
+
throw new Error(`Sage Intacct API request failed after ${attempt + 1} attempt(s): ${message}`);
|
|
808
|
+
}
|
|
809
|
+
// Exponential backoff: 1s, 2s, 4s, ...
|
|
810
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
811
|
+
await this.Sleep(delay);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
throw new Error('Sage Intacct API request failed: max retries exceeded');
|
|
815
|
+
}
|
|
816
|
+
// ─── XML Parsing Helpers ─────────────────────────────────────────
|
|
817
|
+
/**
|
|
818
|
+
* Checks the API response for error elements and throws if found.
|
|
819
|
+
*/
|
|
820
|
+
CheckForErrors(xml) {
|
|
821
|
+
// Check for control-level errors
|
|
822
|
+
const statusMatch = xml.match(/<status>(.*?)<\/status>/i);
|
|
823
|
+
if (statusMatch && statusMatch[1].toLowerCase() === 'failure') {
|
|
824
|
+
const errMsg = this.ExtractErrorMessage(xml);
|
|
825
|
+
throw new Error(`Sage Intacct API error: ${errMsg}`);
|
|
826
|
+
}
|
|
827
|
+
// Check for function-level errors
|
|
828
|
+
const errNoMatch = xml.match(/<errorno>(.*?)<\/errorno>/i);
|
|
829
|
+
if (errNoMatch) {
|
|
830
|
+
const errDesc = this.ExtractXmlValue(xml, 'description2')
|
|
831
|
+
|| this.ExtractXmlValue(xml, 'description')
|
|
832
|
+
|| this.ExtractXmlValue(xml, 'errormessage')
|
|
833
|
+
|| 'Unknown error';
|
|
834
|
+
throw new Error(`Sage Intacct error ${errNoMatch[1]}: ${errDesc}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
ExtractErrorMessage(xml) {
|
|
838
|
+
const desc2 = this.ExtractXmlValue(xml, 'description2');
|
|
839
|
+
if (desc2)
|
|
840
|
+
return desc2;
|
|
841
|
+
const desc = this.ExtractXmlValue(xml, 'description');
|
|
842
|
+
if (desc)
|
|
843
|
+
return desc;
|
|
844
|
+
const errMsg = this.ExtractXmlValue(xml, 'errormessage');
|
|
845
|
+
if (errMsg)
|
|
846
|
+
return errMsg;
|
|
847
|
+
return 'Unknown error (no description in response)';
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Extracts the text content of an XML element by tag name.
|
|
851
|
+
* Simple regex-based parsing — sufficient for Sage Intacct's flat response format.
|
|
852
|
+
*/
|
|
853
|
+
ExtractXmlValue(xml, tagName) {
|
|
854
|
+
const regex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'i');
|
|
855
|
+
const match = xml.match(regex);
|
|
856
|
+
return match ? match[1].trim() : null;
|
|
857
|
+
}
|
|
858
|
+
ExtractXmlValueFromFragment(fragment, tagName) {
|
|
859
|
+
const regex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'i');
|
|
860
|
+
const match = fragment.match(regex);
|
|
861
|
+
return match ? match[1].trim() : '';
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Extracts records from a readByQuery/read response.
|
|
865
|
+
* Records are child elements within the <data> element, tagged by object name
|
|
866
|
+
* (e.g., <CUSTOMER>, <VENDOR>, etc.) or a generic lowercase tag.
|
|
867
|
+
*/
|
|
868
|
+
ExtractRecords(xml, objectName) {
|
|
869
|
+
const records = [];
|
|
870
|
+
// Look for the data section
|
|
871
|
+
const dataMatch = xml.match(/<data[^>]*>([\s\S]*?)<\/data>/i);
|
|
872
|
+
if (!dataMatch)
|
|
873
|
+
return records;
|
|
874
|
+
const dataContent = dataMatch[1];
|
|
875
|
+
// Extract individual record elements — try both uppercase and lowercase
|
|
876
|
+
const recordRegex = new RegExp(`<${objectName}[^>]*>([\\s\\S]*?)</${objectName}>`, 'gi');
|
|
877
|
+
let match;
|
|
878
|
+
while ((match = recordRegex.exec(dataContent)) !== null) {
|
|
879
|
+
const recordXml = match[1];
|
|
880
|
+
const record = this.ParseRecordFields(recordXml);
|
|
881
|
+
records.push(record);
|
|
882
|
+
}
|
|
883
|
+
return records;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Parses field name/value pairs from a record XML fragment.
|
|
887
|
+
* Handles flat fields only (nested objects are returned as string values).
|
|
888
|
+
*/
|
|
889
|
+
ParseRecordFields(recordXml) {
|
|
890
|
+
const fields = {};
|
|
891
|
+
const fieldRegex = /<(\w+)(?:\s[^>]*)?>([\s\S]*?)<\/\1>/g;
|
|
892
|
+
let match;
|
|
893
|
+
while ((match = fieldRegex.exec(recordXml)) !== null) {
|
|
894
|
+
const fieldName = match[1];
|
|
895
|
+
const value = match[2].trim();
|
|
896
|
+
// Attempt to parse numeric values
|
|
897
|
+
if (/^-?\d+$/.test(value)) {
|
|
898
|
+
fields[fieldName] = parseInt(value, 10);
|
|
899
|
+
}
|
|
900
|
+
else if (/^-?\d+\.\d+$/.test(value)) {
|
|
901
|
+
fields[fieldName] = parseFloat(value);
|
|
902
|
+
}
|
|
903
|
+
else if (value === '' || value === null) {
|
|
904
|
+
fields[fieldName] = null;
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
fields[fieldName] = value;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return fields;
|
|
911
|
+
}
|
|
912
|
+
ExtractRecordKey(xml, objectName) {
|
|
913
|
+
// After create, Sage Intacct typically returns the key in a <RECORDNO> or object-specific PK field
|
|
914
|
+
const pkField = this.GetPrimaryKeyField(objectName);
|
|
915
|
+
const value = this.ExtractXmlValue(xml, pkField)
|
|
916
|
+
|| this.ExtractXmlValue(xml, 'RECORDNO')
|
|
917
|
+
|| this.ExtractXmlValue(xml, 'key')
|
|
918
|
+
|| '';
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
// ─── Utility Helpers ─────────────────────────────────────────────
|
|
922
|
+
/**
|
|
923
|
+
* Converts an attributes object to XML field elements.
|
|
924
|
+
*/
|
|
925
|
+
AttributesToXml(attributes) {
|
|
926
|
+
return Object.entries(attributes)
|
|
927
|
+
.filter(([, v]) => v != null)
|
|
928
|
+
.map(([key, value]) => `<${key}>${this.EscapeXmlValue(String(value))}</${key}>`)
|
|
929
|
+
.join('\n ');
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Escapes special characters for XML content.
|
|
933
|
+
*/
|
|
934
|
+
EscapeXmlValue(value) {
|
|
935
|
+
return value
|
|
936
|
+
.replace(/&/g, '&')
|
|
937
|
+
.replace(/</g, '<')
|
|
938
|
+
.replace(/>/g, '>')
|
|
939
|
+
.replace(/"/g, '"')
|
|
940
|
+
.replace(/'/g, ''');
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Gets the primary key field name for a Sage Intacct object.
|
|
944
|
+
*/
|
|
945
|
+
GetPrimaryKeyField(objectName) {
|
|
946
|
+
return OBJECT_PK_MAP[objectName.toUpperCase()] || 'RECORDNO';
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Formats an API object name into a human-readable label.
|
|
950
|
+
*/
|
|
951
|
+
FormatLabel(name) {
|
|
952
|
+
// Convert ALLCAPS to Title Case with spaces before capitals
|
|
953
|
+
return name
|
|
954
|
+
.replace(/_/g, ' ')
|
|
955
|
+
.replace(/([A-Z])([A-Z]+)/g, (_match, first, rest) => first + rest.toLowerCase())
|
|
956
|
+
.replace(/^\w/, c => c.toUpperCase());
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Computes the latest WHENMODIFIED watermark from a batch of records.
|
|
960
|
+
*/
|
|
961
|
+
ComputeWatermark(records) {
|
|
962
|
+
let latest;
|
|
963
|
+
for (const record of records) {
|
|
964
|
+
const modified = record['WHENMODIFIED'];
|
|
965
|
+
if (modified && typeof modified === 'string') {
|
|
966
|
+
if (!latest || modified > latest) {
|
|
967
|
+
latest = modified;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return latest;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Fetches more records using a readMore request with a resultId cursor.
|
|
975
|
+
*/
|
|
976
|
+
async FetchMoreRecords(session, resultId, objectName, pkField) {
|
|
977
|
+
const xml = this.BuildReadMoreRequest(session, resultId);
|
|
978
|
+
const response = await this.SendXMLRequest(session, xml);
|
|
979
|
+
return this.ParseReadByQueryResponse(response, objectName, pkField);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Throttles API requests to respect rate limits.
|
|
983
|
+
*/
|
|
984
|
+
async Throttle(minIntervalMs) {
|
|
985
|
+
const elapsed = Date.now() - this.lastRequestTime;
|
|
986
|
+
if (elapsed < minIntervalMs) {
|
|
987
|
+
await this.Sleep(minIntervalMs - elapsed);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
Sleep(ms) {
|
|
991
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
992
|
+
}
|
|
993
|
+
BuildCRUDError(err, operation, objectName) {
|
|
994
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
995
|
+
return {
|
|
996
|
+
Success: false,
|
|
997
|
+
ErrorMessage: `${operation} failed for ${objectName}: ${message}`,
|
|
998
|
+
StatusCode: 500,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
SageIntacctConnector = __decorate([
|
|
1003
|
+
RegisterClass(BaseIntegrationConnector, 'SageIntacctConnector')
|
|
1004
|
+
], SageIntacctConnector);
|
|
1005
|
+
export { SageIntacctConnector };
|
|
1006
|
+
/** Tree-shaking prevention function — import and call from module entry point */
|
|
1007
|
+
export function LoadSageIntacctConnector() { }
|
|
1008
|
+
//# sourceMappingURL=SageIntacctConnector.js.map
|