@xenterprises/fastify-xplaid 1.0.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/.gitlab-ci.yml +45 -0
- package/README.md +393 -0
- package/package.json +36 -0
- package/src/xPlaid.js +764 -0
- package/test/xPlaid.test.js +563 -0
package/src/xPlaid.js
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xPlaid - Plaid Financial Data Integration
|
|
3
|
+
*
|
|
4
|
+
* A Fastify plugin for integrating with Plaid's banking API.
|
|
5
|
+
* Provides Link token management, account access, transactions, balances, and identity verification.
|
|
6
|
+
*
|
|
7
|
+
* @see https://plaid.com/docs/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fp from "fastify-plugin";
|
|
11
|
+
import { Configuration, PlaidApi, PlaidEnvironments, Products, CountryCode } from "plaid";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Plaid environments
|
|
15
|
+
*/
|
|
16
|
+
const ENVIRONMENTS = {
|
|
17
|
+
SANDBOX: "sandbox",
|
|
18
|
+
DEVELOPMENT: "development",
|
|
19
|
+
PRODUCTION: "production",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Plaid products
|
|
24
|
+
*/
|
|
25
|
+
const PRODUCTS = {
|
|
26
|
+
AUTH: "auth",
|
|
27
|
+
TRANSACTIONS: "transactions",
|
|
28
|
+
IDENTITY: "identity",
|
|
29
|
+
ASSETS: "assets",
|
|
30
|
+
INVESTMENTS: "investments",
|
|
31
|
+
LIABILITIES: "liabilities",
|
|
32
|
+
BALANCE: "balance",
|
|
33
|
+
INCOME: "income",
|
|
34
|
+
TRANSFER: "transfer",
|
|
35
|
+
SIGNAL: "signal",
|
|
36
|
+
STATEMENTS: "statements",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Country codes
|
|
41
|
+
*/
|
|
42
|
+
const COUNTRY_CODES = {
|
|
43
|
+
US: "US",
|
|
44
|
+
CA: "CA",
|
|
45
|
+
GB: "GB",
|
|
46
|
+
ES: "ES",
|
|
47
|
+
FR: "FR",
|
|
48
|
+
IE: "IE",
|
|
49
|
+
NL: "NL",
|
|
50
|
+
DE: "DE",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* xPlaid Plugin
|
|
55
|
+
*
|
|
56
|
+
* @param {import('fastify').FastifyInstance} fastify - Fastify instance
|
|
57
|
+
* @param {Object} options - Plugin options
|
|
58
|
+
* @param {string} options.clientId - Plaid client ID (required)
|
|
59
|
+
* @param {string} options.secret - Plaid secret (required)
|
|
60
|
+
* @param {string} [options.environment] - Plaid environment (sandbox, development, production)
|
|
61
|
+
* @param {string} [options.version] - Plaid API version (default: 2020-09-14)
|
|
62
|
+
* @param {boolean} [options.active] - Enable/disable the plugin (default: true)
|
|
63
|
+
*/
|
|
64
|
+
async function xPlaid(fastify, options) {
|
|
65
|
+
// Check if plugin is disabled
|
|
66
|
+
if (options.active === false) {
|
|
67
|
+
fastify.log.info("# ⏸️ xPlaid Disabled");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Validate required configuration
|
|
72
|
+
if (!options.clientId) {
|
|
73
|
+
throw new Error("xPlaid: clientId is required");
|
|
74
|
+
}
|
|
75
|
+
if (!options.secret) {
|
|
76
|
+
throw new Error("xPlaid: secret is required");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const version = options.version || "2020-09-14";
|
|
80
|
+
|
|
81
|
+
// Determine Plaid environment and base path
|
|
82
|
+
let environment;
|
|
83
|
+
let basePath;
|
|
84
|
+
switch (options.environment) {
|
|
85
|
+
case ENVIRONMENTS.PRODUCTION:
|
|
86
|
+
environment = ENVIRONMENTS.PRODUCTION;
|
|
87
|
+
basePath = PlaidEnvironments.production;
|
|
88
|
+
break;
|
|
89
|
+
case ENVIRONMENTS.DEVELOPMENT:
|
|
90
|
+
environment = ENVIRONMENTS.DEVELOPMENT;
|
|
91
|
+
basePath = PlaidEnvironments.development;
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
environment = ENVIRONMENTS.SANDBOX;
|
|
95
|
+
basePath = PlaidEnvironments.sandbox;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Configure Plaid client
|
|
99
|
+
const configuration = new Configuration({
|
|
100
|
+
basePath,
|
|
101
|
+
baseOptions: {
|
|
102
|
+
headers: {
|
|
103
|
+
"PLAID-CLIENT-ID": options.clientId,
|
|
104
|
+
"PLAID-SECRET": options.secret,
|
|
105
|
+
"Plaid-Version": version,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const plaidClient = new PlaidApi(configuration);
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Link Token Service
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a Link token for initializing Plaid Link
|
|
118
|
+
* @param {Object} params - Link token parameters
|
|
119
|
+
* @param {string} params.userId - Unique client user ID
|
|
120
|
+
* @param {string} [params.clientName] - Application name (max 30 chars)
|
|
121
|
+
* @param {string[]} [params.products] - Products to enable
|
|
122
|
+
* @param {string[]} [params.countryCodes] - Country codes
|
|
123
|
+
* @param {string} [params.language] - Display language
|
|
124
|
+
* @param {string} [params.webhook] - Webhook URL
|
|
125
|
+
* @param {string} [params.redirectUri] - OAuth redirect URI
|
|
126
|
+
* @param {string} [params.accessToken] - For update mode
|
|
127
|
+
* @returns {Promise<Object>} Link token response
|
|
128
|
+
*/
|
|
129
|
+
async function createLinkToken(params) {
|
|
130
|
+
const {
|
|
131
|
+
userId,
|
|
132
|
+
clientName = "App",
|
|
133
|
+
products = [Products.Transactions],
|
|
134
|
+
countryCodes = [CountryCode.Us],
|
|
135
|
+
language = "en",
|
|
136
|
+
webhook,
|
|
137
|
+
redirectUri,
|
|
138
|
+
accessToken,
|
|
139
|
+
} = params;
|
|
140
|
+
|
|
141
|
+
const request = {
|
|
142
|
+
user: {
|
|
143
|
+
client_user_id: userId,
|
|
144
|
+
},
|
|
145
|
+
client_name: clientName.slice(0, 30),
|
|
146
|
+
products: accessToken ? undefined : products,
|
|
147
|
+
country_codes: countryCodes,
|
|
148
|
+
language,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (webhook) request.webhook = webhook;
|
|
152
|
+
if (redirectUri) request.redirect_uri = redirectUri;
|
|
153
|
+
if (accessToken) request.access_token = accessToken;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const response = await plaidClient.linkTokenCreate(request);
|
|
157
|
+
return {
|
|
158
|
+
linkToken: response.data.link_token,
|
|
159
|
+
expiration: response.data.expiration,
|
|
160
|
+
requestId: response.data.request_id,
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
fastify.log.error({ error: error.response?.data }, "Plaid linkTokenCreate failed");
|
|
164
|
+
throw new Error(error.response?.data?.error_message || "Failed to create link token");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get information about a Link token
|
|
170
|
+
* @param {string} linkToken - Link token
|
|
171
|
+
* @returns {Promise<Object>} Link token info
|
|
172
|
+
*/
|
|
173
|
+
async function getLinkToken(linkToken) {
|
|
174
|
+
try {
|
|
175
|
+
const response = await plaidClient.linkTokenGet({ link_token: linkToken });
|
|
176
|
+
return response.data;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
fastify.log.error({ error: error.response?.data }, "Plaid linkTokenGet failed");
|
|
179
|
+
throw new Error(error.response?.data?.error_message || "Failed to get link token");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Exchange a public token for an access token
|
|
185
|
+
* @param {string} publicToken - Public token from Link
|
|
186
|
+
* @returns {Promise<Object>} Access token and item ID
|
|
187
|
+
*/
|
|
188
|
+
async function exchangePublicToken(publicToken) {
|
|
189
|
+
try {
|
|
190
|
+
const response = await plaidClient.itemPublicTokenExchange({
|
|
191
|
+
public_token: publicToken,
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
accessToken: response.data.access_token,
|
|
195
|
+
itemId: response.data.item_id,
|
|
196
|
+
requestId: response.data.request_id,
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
fastify.log.error({ error: error.response?.data }, "Plaid exchangePublicToken failed");
|
|
200
|
+
throw new Error(error.response?.data?.error_message || "Failed to exchange public token");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// Accounts Service
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get accounts for an Item
|
|
210
|
+
* @param {string} accessToken - Access token
|
|
211
|
+
* @param {Object} [options] - Options
|
|
212
|
+
* @param {string[]} [options.accountIds] - Filter by account IDs
|
|
213
|
+
* @returns {Promise<Object>} Accounts response
|
|
214
|
+
*/
|
|
215
|
+
async function getAccounts(accessToken, options = {}) {
|
|
216
|
+
const request = { access_token: accessToken };
|
|
217
|
+
|
|
218
|
+
if (options.accountIds) {
|
|
219
|
+
request.options = { account_ids: options.accountIds };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const response = await plaidClient.accountsGet(request);
|
|
224
|
+
return {
|
|
225
|
+
accounts: response.data.accounts,
|
|
226
|
+
item: response.data.item,
|
|
227
|
+
requestId: response.data.request_id,
|
|
228
|
+
};
|
|
229
|
+
} catch (error) {
|
|
230
|
+
fastify.log.error({ error: error.response?.data }, "Plaid accountsGet failed");
|
|
231
|
+
throw new Error(error.response?.data?.error_message || "Failed to get accounts");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get real-time balance for accounts
|
|
237
|
+
* @param {string} accessToken - Access token
|
|
238
|
+
* @param {Object} [options] - Options
|
|
239
|
+
* @param {string[]} [options.accountIds] - Filter by account IDs
|
|
240
|
+
* @returns {Promise<Object>} Balance response
|
|
241
|
+
*/
|
|
242
|
+
async function getBalance(accessToken, options = {}) {
|
|
243
|
+
const request = { access_token: accessToken };
|
|
244
|
+
|
|
245
|
+
if (options.accountIds) {
|
|
246
|
+
request.options = { account_ids: options.accountIds };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const response = await plaidClient.accountsBalanceGet(request);
|
|
251
|
+
return {
|
|
252
|
+
accounts: response.data.accounts,
|
|
253
|
+
item: response.data.item,
|
|
254
|
+
requestId: response.data.request_id,
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
fastify.log.error({ error: error.response?.data }, "Plaid accountsBalanceGet failed");
|
|
258
|
+
throw new Error(error.response?.data?.error_message || "Failed to get balance");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// Transactions Service
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Sync transactions (recommended approach)
|
|
268
|
+
* @param {string} accessToken - Access token
|
|
269
|
+
* @param {Object} [options] - Options
|
|
270
|
+
* @param {string} [options.cursor] - Cursor for pagination
|
|
271
|
+
* @param {number} [options.count] - Number of transactions (max 500)
|
|
272
|
+
* @param {string[]} [options.accountIds] - Filter by account IDs
|
|
273
|
+
* @returns {Promise<Object>} Transaction sync response
|
|
274
|
+
*/
|
|
275
|
+
async function syncTransactions(accessToken, options = {}) {
|
|
276
|
+
const request = { access_token: accessToken };
|
|
277
|
+
|
|
278
|
+
if (options.cursor) request.cursor = options.cursor;
|
|
279
|
+
if (options.count) request.count = Math.min(options.count, 500);
|
|
280
|
+
if (options.accountIds) {
|
|
281
|
+
request.options = { account_ids: options.accountIds };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const response = await plaidClient.transactionsSync(request);
|
|
286
|
+
return {
|
|
287
|
+
added: response.data.added,
|
|
288
|
+
modified: response.data.modified,
|
|
289
|
+
removed: response.data.removed,
|
|
290
|
+
nextCursor: response.data.next_cursor,
|
|
291
|
+
hasMore: response.data.has_more,
|
|
292
|
+
requestId: response.data.request_id,
|
|
293
|
+
};
|
|
294
|
+
} catch (error) {
|
|
295
|
+
fastify.log.error({ error: error.response?.data }, "Plaid transactionsSync failed");
|
|
296
|
+
throw new Error(error.response?.data?.error_message || "Failed to sync transactions");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get transactions within a date range (legacy approach)
|
|
302
|
+
* @param {string} accessToken - Access token
|
|
303
|
+
* @param {string} startDate - Start date (YYYY-MM-DD)
|
|
304
|
+
* @param {string} endDate - End date (YYYY-MM-DD)
|
|
305
|
+
* @param {Object} [options] - Options
|
|
306
|
+
* @param {number} [options.count] - Number of transactions (max 500)
|
|
307
|
+
* @param {number} [options.offset] - Pagination offset
|
|
308
|
+
* @param {string[]} [options.accountIds] - Filter by account IDs
|
|
309
|
+
* @returns {Promise<Object>} Transactions response
|
|
310
|
+
*/
|
|
311
|
+
async function getTransactions(accessToken, startDate, endDate, options = {}) {
|
|
312
|
+
const request = {
|
|
313
|
+
access_token: accessToken,
|
|
314
|
+
start_date: startDate,
|
|
315
|
+
end_date: endDate,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const requestOptions = {};
|
|
319
|
+
if (options.count) requestOptions.count = Math.min(options.count, 500);
|
|
320
|
+
if (options.offset) requestOptions.offset = options.offset;
|
|
321
|
+
if (options.accountIds) requestOptions.account_ids = options.accountIds;
|
|
322
|
+
|
|
323
|
+
if (Object.keys(requestOptions).length > 0) {
|
|
324
|
+
request.options = requestOptions;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const response = await plaidClient.transactionsGet(request);
|
|
329
|
+
return {
|
|
330
|
+
accounts: response.data.accounts,
|
|
331
|
+
transactions: response.data.transactions,
|
|
332
|
+
totalTransactions: response.data.total_transactions,
|
|
333
|
+
item: response.data.item,
|
|
334
|
+
requestId: response.data.request_id,
|
|
335
|
+
};
|
|
336
|
+
} catch (error) {
|
|
337
|
+
fastify.log.error({ error: error.response?.data }, "Plaid transactionsGet failed");
|
|
338
|
+
throw new Error(error.response?.data?.error_message || "Failed to get transactions");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Refresh transactions
|
|
344
|
+
* @param {string} accessToken - Access token
|
|
345
|
+
* @returns {Promise<Object>} Refresh response
|
|
346
|
+
*/
|
|
347
|
+
async function refreshTransactions(accessToken) {
|
|
348
|
+
try {
|
|
349
|
+
const response = await plaidClient.transactionsRefresh({
|
|
350
|
+
access_token: accessToken,
|
|
351
|
+
});
|
|
352
|
+
return {
|
|
353
|
+
requestId: response.data.request_id,
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
fastify.log.error({ error: error.response?.data }, "Plaid transactionsRefresh failed");
|
|
357
|
+
throw new Error(error.response?.data?.error_message || "Failed to refresh transactions");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================================================
|
|
362
|
+
// Identity Service
|
|
363
|
+
// ============================================================================
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get identity information for accounts
|
|
367
|
+
* @param {string} accessToken - Access token
|
|
368
|
+
* @param {Object} [options] - Options
|
|
369
|
+
* @param {string[]} [options.accountIds] - Filter by account IDs
|
|
370
|
+
* @returns {Promise<Object>} Identity response
|
|
371
|
+
*/
|
|
372
|
+
async function getIdentity(accessToken, options = {}) {
|
|
373
|
+
const request = { access_token: accessToken };
|
|
374
|
+
|
|
375
|
+
if (options.accountIds) {
|
|
376
|
+
request.options = { account_ids: options.accountIds };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const response = await plaidClient.identityGet(request);
|
|
381
|
+
return {
|
|
382
|
+
accounts: response.data.accounts,
|
|
383
|
+
item: response.data.item,
|
|
384
|
+
requestId: response.data.request_id,
|
|
385
|
+
};
|
|
386
|
+
} catch (error) {
|
|
387
|
+
fastify.log.error({ error: error.response?.data }, "Plaid identityGet failed");
|
|
388
|
+
throw new Error(error.response?.data?.error_message || "Failed to get identity");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ============================================================================
|
|
393
|
+
// Auth Service
|
|
394
|
+
// ============================================================================
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get account and routing numbers for ACH
|
|
398
|
+
* @param {string} accessToken - Access token
|
|
399
|
+
* @param {Object} [options] - Options
|
|
400
|
+
* @param {string[]} [options.accountIds] - Filter by account IDs
|
|
401
|
+
* @returns {Promise<Object>} Auth response
|
|
402
|
+
*/
|
|
403
|
+
async function getAuth(accessToken, options = {}) {
|
|
404
|
+
const request = { access_token: accessToken };
|
|
405
|
+
|
|
406
|
+
if (options.accountIds) {
|
|
407
|
+
request.options = { account_ids: options.accountIds };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const response = await plaidClient.authGet(request);
|
|
412
|
+
return {
|
|
413
|
+
accounts: response.data.accounts,
|
|
414
|
+
numbers: response.data.numbers,
|
|
415
|
+
item: response.data.item,
|
|
416
|
+
requestId: response.data.request_id,
|
|
417
|
+
};
|
|
418
|
+
} catch (error) {
|
|
419
|
+
fastify.log.error({ error: error.response?.data }, "Plaid authGet failed");
|
|
420
|
+
throw new Error(error.response?.data?.error_message || "Failed to get auth");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// Investments Service
|
|
426
|
+
// ============================================================================
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get investment holdings
|
|
430
|
+
* @param {string} accessToken - Access token
|
|
431
|
+
* @param {Object} [options] - Options
|
|
432
|
+
* @param {string[]} [options.accountIds] - Filter by account IDs
|
|
433
|
+
* @returns {Promise<Object>} Holdings response
|
|
434
|
+
*/
|
|
435
|
+
async function getHoldings(accessToken, options = {}) {
|
|
436
|
+
const request = { access_token: accessToken };
|
|
437
|
+
|
|
438
|
+
if (options.accountIds) {
|
|
439
|
+
request.options = { account_ids: options.accountIds };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const response = await plaidClient.investmentsHoldingsGet(request);
|
|
444
|
+
return {
|
|
445
|
+
accounts: response.data.accounts,
|
|
446
|
+
holdings: response.data.holdings,
|
|
447
|
+
securities: response.data.securities,
|
|
448
|
+
item: response.data.item,
|
|
449
|
+
requestId: response.data.request_id,
|
|
450
|
+
};
|
|
451
|
+
} catch (error) {
|
|
452
|
+
fastify.log.error({ error: error.response?.data }, "Plaid investmentsHoldingsGet failed");
|
|
453
|
+
throw new Error(error.response?.data?.error_message || "Failed to get holdings");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Get investment transactions
|
|
459
|
+
* @param {string} accessToken - Access token
|
|
460
|
+
* @param {string} startDate - Start date (YYYY-MM-DD)
|
|
461
|
+
* @param {string} endDate - End date (YYYY-MM-DD)
|
|
462
|
+
* @param {Object} [options] - Options
|
|
463
|
+
* @param {number} [options.count] - Number of transactions (max 500)
|
|
464
|
+
* @param {number} [options.offset] - Pagination offset
|
|
465
|
+
* @param {string[]} [options.accountIds] - Filter by account IDs
|
|
466
|
+
* @returns {Promise<Object>} Investment transactions response
|
|
467
|
+
*/
|
|
468
|
+
async function getInvestmentTransactions(accessToken, startDate, endDate, options = {}) {
|
|
469
|
+
const request = {
|
|
470
|
+
access_token: accessToken,
|
|
471
|
+
start_date: startDate,
|
|
472
|
+
end_date: endDate,
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const requestOptions = {};
|
|
476
|
+
if (options.count) requestOptions.count = Math.min(options.count, 500);
|
|
477
|
+
if (options.offset) requestOptions.offset = options.offset;
|
|
478
|
+
if (options.accountIds) requestOptions.account_ids = options.accountIds;
|
|
479
|
+
|
|
480
|
+
if (Object.keys(requestOptions).length > 0) {
|
|
481
|
+
request.options = requestOptions;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const response = await plaidClient.investmentsTransactionsGet(request);
|
|
486
|
+
return {
|
|
487
|
+
accounts: response.data.accounts,
|
|
488
|
+
investmentTransactions: response.data.investment_transactions,
|
|
489
|
+
securities: response.data.securities,
|
|
490
|
+
totalInvestmentTransactions: response.data.total_investment_transactions,
|
|
491
|
+
item: response.data.item,
|
|
492
|
+
requestId: response.data.request_id,
|
|
493
|
+
};
|
|
494
|
+
} catch (error) {
|
|
495
|
+
fastify.log.error({ error: error.response?.data }, "Plaid investmentsTransactionsGet failed");
|
|
496
|
+
throw new Error(
|
|
497
|
+
error.response?.data?.error_message || "Failed to get investment transactions"
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ============================================================================
|
|
503
|
+
// Liabilities Service
|
|
504
|
+
// ============================================================================
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Get liabilities information
|
|
508
|
+
* @param {string} accessToken - Access token
|
|
509
|
+
* @param {Object} [options] - Options
|
|
510
|
+
* @param {string[]} [options.accountIds] - Filter by account IDs
|
|
511
|
+
* @returns {Promise<Object>} Liabilities response
|
|
512
|
+
*/
|
|
513
|
+
async function getLiabilities(accessToken, options = {}) {
|
|
514
|
+
const request = { access_token: accessToken };
|
|
515
|
+
|
|
516
|
+
if (options.accountIds) {
|
|
517
|
+
request.options = { account_ids: options.accountIds };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const response = await plaidClient.liabilitiesGet(request);
|
|
522
|
+
return {
|
|
523
|
+
accounts: response.data.accounts,
|
|
524
|
+
liabilities: response.data.liabilities,
|
|
525
|
+
item: response.data.item,
|
|
526
|
+
requestId: response.data.request_id,
|
|
527
|
+
};
|
|
528
|
+
} catch (error) {
|
|
529
|
+
fastify.log.error({ error: error.response?.data }, "Plaid liabilitiesGet failed");
|
|
530
|
+
throw new Error(error.response?.data?.error_message || "Failed to get liabilities");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ============================================================================
|
|
535
|
+
// Item Management
|
|
536
|
+
// ============================================================================
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get Item information
|
|
540
|
+
* @param {string} accessToken - Access token
|
|
541
|
+
* @returns {Promise<Object>} Item response
|
|
542
|
+
*/
|
|
543
|
+
async function getItem(accessToken) {
|
|
544
|
+
try {
|
|
545
|
+
const response = await plaidClient.itemGet({
|
|
546
|
+
access_token: accessToken,
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
item: response.data.item,
|
|
550
|
+
status: response.data.status,
|
|
551
|
+
requestId: response.data.request_id,
|
|
552
|
+
};
|
|
553
|
+
} catch (error) {
|
|
554
|
+
fastify.log.error({ error: error.response?.data }, "Plaid itemGet failed");
|
|
555
|
+
throw new Error(error.response?.data?.error_message || "Failed to get item");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Remove an Item
|
|
561
|
+
* @param {string} accessToken - Access token
|
|
562
|
+
* @returns {Promise<Object>} Remove response
|
|
563
|
+
*/
|
|
564
|
+
async function removeItem(accessToken) {
|
|
565
|
+
try {
|
|
566
|
+
const response = await plaidClient.itemRemove({
|
|
567
|
+
access_token: accessToken,
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
requestId: response.data.request_id,
|
|
571
|
+
};
|
|
572
|
+
} catch (error) {
|
|
573
|
+
fastify.log.error({ error: error.response?.data }, "Plaid itemRemove failed");
|
|
574
|
+
throw new Error(error.response?.data?.error_message || "Failed to remove item");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get institution by ID
|
|
580
|
+
* @param {string} institutionId - Institution ID
|
|
581
|
+
* @param {string[]} [countryCodes] - Country codes
|
|
582
|
+
* @returns {Promise<Object>} Institution response
|
|
583
|
+
*/
|
|
584
|
+
async function getInstitution(institutionId, countryCodes = [CountryCode.Us]) {
|
|
585
|
+
try {
|
|
586
|
+
const response = await plaidClient.institutionsGetById({
|
|
587
|
+
institution_id: institutionId,
|
|
588
|
+
country_codes: countryCodes,
|
|
589
|
+
});
|
|
590
|
+
return {
|
|
591
|
+
institution: response.data.institution,
|
|
592
|
+
requestId: response.data.request_id,
|
|
593
|
+
};
|
|
594
|
+
} catch (error) {
|
|
595
|
+
fastify.log.error({ error: error.response?.data }, "Plaid institutionsGetById failed");
|
|
596
|
+
throw new Error(error.response?.data?.error_message || "Failed to get institution");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Search institutions
|
|
602
|
+
* @param {string} query - Search query
|
|
603
|
+
* @param {Object} [options] - Options
|
|
604
|
+
* @param {string[]} [options.countryCodes] - Country codes
|
|
605
|
+
* @param {string[]} [options.products] - Filter by products
|
|
606
|
+
* @param {number} [options.count] - Number of results
|
|
607
|
+
* @param {number} [options.offset] - Pagination offset
|
|
608
|
+
* @returns {Promise<Object>} Institutions search response
|
|
609
|
+
*/
|
|
610
|
+
async function searchInstitutions(query, options = {}) {
|
|
611
|
+
const request = {
|
|
612
|
+
query,
|
|
613
|
+
country_codes: options.countryCodes || [CountryCode.Us],
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
if (options.products) request.products = options.products;
|
|
617
|
+
if (options.count) request.options = { ...request.options, count: options.count };
|
|
618
|
+
if (options.offset) request.options = { ...request.options, offset: options.offset };
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const response = await plaidClient.institutionsSearch(request);
|
|
622
|
+
return {
|
|
623
|
+
institutions: response.data.institutions,
|
|
624
|
+
requestId: response.data.request_id,
|
|
625
|
+
};
|
|
626
|
+
} catch (error) {
|
|
627
|
+
fastify.log.error({ error: error.response?.data }, "Plaid institutionsSearch failed");
|
|
628
|
+
throw new Error(error.response?.data?.error_message || "Failed to search institutions");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ============================================================================
|
|
633
|
+
// Webhooks
|
|
634
|
+
// ============================================================================
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Update webhook URL for an Item
|
|
638
|
+
* @param {string} accessToken - Access token
|
|
639
|
+
* @param {string} webhook - New webhook URL
|
|
640
|
+
* @returns {Promise<Object>} Update response
|
|
641
|
+
*/
|
|
642
|
+
async function updateWebhook(accessToken, webhook) {
|
|
643
|
+
try {
|
|
644
|
+
const response = await plaidClient.itemWebhookUpdate({
|
|
645
|
+
access_token: accessToken,
|
|
646
|
+
webhook,
|
|
647
|
+
});
|
|
648
|
+
return {
|
|
649
|
+
item: response.data.item,
|
|
650
|
+
requestId: response.data.request_id,
|
|
651
|
+
};
|
|
652
|
+
} catch (error) {
|
|
653
|
+
fastify.log.error({ error: error.response?.data }, "Plaid itemWebhookUpdate failed");
|
|
654
|
+
throw new Error(error.response?.data?.error_message || "Failed to update webhook");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Get webhook verification key
|
|
660
|
+
* @param {string} keyId - Key ID from webhook header
|
|
661
|
+
* @returns {Promise<Object>} Key response
|
|
662
|
+
*/
|
|
663
|
+
async function getWebhookVerificationKey(keyId) {
|
|
664
|
+
try {
|
|
665
|
+
const response = await plaidClient.webhookVerificationKeyGet({
|
|
666
|
+
key_id: keyId,
|
|
667
|
+
});
|
|
668
|
+
return {
|
|
669
|
+
key: response.data.key,
|
|
670
|
+
requestId: response.data.request_id,
|
|
671
|
+
};
|
|
672
|
+
} catch (error) {
|
|
673
|
+
fastify.log.error({ error: error.response?.data }, "Plaid webhookVerificationKeyGet failed");
|
|
674
|
+
throw new Error(error.response?.data?.error_message || "Failed to get verification key");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Store configuration on fastify instance
|
|
679
|
+
fastify.decorate("xplaid", {
|
|
680
|
+
// Configuration
|
|
681
|
+
config: {
|
|
682
|
+
environment,
|
|
683
|
+
clientId: `***${options.clientId.slice(-4)}`,
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
// Constants
|
|
687
|
+
constants: {
|
|
688
|
+
ENVIRONMENTS,
|
|
689
|
+
PRODUCTS,
|
|
690
|
+
COUNTRY_CODES,
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
// Link token service
|
|
694
|
+
link: {
|
|
695
|
+
createToken: createLinkToken,
|
|
696
|
+
getToken: getLinkToken,
|
|
697
|
+
exchangePublicToken,
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
// Accounts service
|
|
701
|
+
accounts: {
|
|
702
|
+
get: getAccounts,
|
|
703
|
+
getBalance,
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
// Transactions service
|
|
707
|
+
transactions: {
|
|
708
|
+
sync: syncTransactions,
|
|
709
|
+
get: getTransactions,
|
|
710
|
+
refresh: refreshTransactions,
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
// Identity service
|
|
714
|
+
identity: {
|
|
715
|
+
get: getIdentity,
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
// Auth service (ACH)
|
|
719
|
+
auth: {
|
|
720
|
+
get: getAuth,
|
|
721
|
+
},
|
|
722
|
+
|
|
723
|
+
// Investments service
|
|
724
|
+
investments: {
|
|
725
|
+
getHoldings,
|
|
726
|
+
getTransactions: getInvestmentTransactions,
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
// Liabilities service
|
|
730
|
+
liabilities: {
|
|
731
|
+
get: getLiabilities,
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
// Item management service
|
|
735
|
+
items: {
|
|
736
|
+
get: getItem,
|
|
737
|
+
remove: removeItem,
|
|
738
|
+
getInstitution,
|
|
739
|
+
searchInstitutions,
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
// Webhooks service
|
|
743
|
+
webhooks: {
|
|
744
|
+
update: updateWebhook,
|
|
745
|
+
getVerificationKey: getWebhookVerificationKey,
|
|
746
|
+
},
|
|
747
|
+
|
|
748
|
+
// Raw client access
|
|
749
|
+
raw: plaidClient,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
fastify.log.info(`# ✅ xPlaid Enabled (${environment})`);
|
|
753
|
+
fastify.log.info("# • Link Token Management");
|
|
754
|
+
fastify.log.info("# • Accounts & Balance");
|
|
755
|
+
fastify.log.info("# • Transactions");
|
|
756
|
+
fastify.log.info("# • Identity & Auth");
|
|
757
|
+
fastify.log.info("# • Investments & Liabilities");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export default fp(xPlaid, {
|
|
761
|
+
name: "xPlaid",
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
export { ENVIRONMENTS, PRODUCTS, COUNTRY_CODES };
|