From 0e7efd01a9d0da86122782d0a724a74ee5966ba0 Mon Sep 17 00:00:00 2001 From: ericcater Date: Tue, 8 Nov 2022 20:17:32 +0300 Subject: [PATCH 1/4] migrated to newest api --- src/integrations/plaid/plaidIntegration.ts | 106 ++++++++++++--------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/src/integrations/plaid/plaidIntegration.ts b/src/integrations/plaid/plaidIntegration.ts index a7c57e8c..81fac7c8 100644 --- a/src/integrations/plaid/plaidIntegration.ts +++ b/src/integrations/plaid/plaidIntegration.ts @@ -1,6 +1,17 @@ import path from 'path' import { parseISO, format, subMonths } from 'date-fns' -import plaid, { TransactionsResponse, CreateLinkTokenOptions } from 'plaid' +import { + Configuration, + CountryCode, + ItemPublicTokenExchangeResponse, + LinkTokenCreateRequest, + PlaidApi, + PlaidEnvironments, + Products, + TransactionsGetRequest, + TransactionsGetRequestOptions, + TransactionsGetResponse +} from 'plaid' import { Config, updateConfig } from '../../common/config' import { PlaidConfig, PlaidEnvironmentType } from '../../types/integrations/plaid' import { IntegrationId } from '../../types/integrations' @@ -17,8 +28,8 @@ export class PlaidIntegration { config: Config plaidConfig: PlaidConfig environment: string - client: plaid.Client - user: plaid.User + client: PlaidApi + user: any constructor(config: Config) { this.config = config @@ -26,29 +37,28 @@ export class PlaidIntegration { this.environment = this.plaidConfig.environment === PlaidEnvironmentType.Development - ? plaid.environments.development - : plaid.environments.sandbox + ? PlaidEnvironments.development + : PlaidEnvironments.sandbox - this.client = new plaid.Client({ - clientID: this.plaidConfig.credentials.clientId, - secret: this.plaidConfig.credentials.secret, - env: this.environment, - options: { - version: '2019-05-29' + const configuration = new Configuration({ + basePath: this.environment, + baseOptions: { + headers: { + 'PLAID-CLIENT-ID': this.plaidConfig.credentials.clientId, + 'PLAID-SECRET': this.plaidConfig.credentials.secret + } } }) + this.client = new PlaidApi(configuration) + // In production this is supposed to be a unique identifier but for Mintable we only have one user (you) this.user = { client_user_id: PLAID_USER_ID } } - public exchangeAccessToken = (accessToken: string): Promise => - // Exchange an expired API access_token for a new Link public_token - this.client.createPublicToken(accessToken).then(token => token.public_token) - - public savePublicToken = (tokenResponse: plaid.TokenResponse): void => { + public savePublicToken = (tokenResponse: ItemPublicTokenExchangeResponse): void => { updateConfig(config => { config.accounts[tokenResponse.item_id] = { id: tokenResponse.item_id, @@ -72,11 +82,8 @@ export class PlaidIntegration { app.post('/get_access_token', (req, res) => { if (req.body.public_token !== undefined) { - client.exchangePublicToken(req.body.public_token, (error, tokenResponse) => { - if (error != null) { - reject(logError('Encountered error exchanging Plaid public token.', error)) - } - this.savePublicToken(tokenResponse) + client.itemPublicTokenExchange({ public_token: req.body.public_token }).then(res => { + this.savePublicToken(res.data) resolve(logInfo('Plaid access token saved.', req.body)) }) } else if (req.body.exit !== undefined) { @@ -98,9 +105,9 @@ export class PlaidIntegration { const accountConfig: PlaidAccountConfig = this.config.accounts[accountId] as PlaidAccountConfig if (accountConfig.integration === IntegrationId.Plaid) { try { - await this.client.getAccounts(accountConfig.token).then(resp => { + await this.client.accountsGet({ access_token: accountConfig.token }).then(resp => { accounts.push({ - name: resp.accounts[0].name, + name: resp.data.accounts[0].name, token: accountConfig.token }) }) @@ -116,29 +123,20 @@ export class PlaidIntegration { }) app.post('/create_link_token', async (req, res) => { - const clientUserId = this.user.client_user_id - const options: CreateLinkTokenOptions = { + const options: LinkTokenCreateRequest = { user: { - client_user_id: clientUserId + client_user_id: this.user.client_user_id }, client_name: 'Mintable', - products: ['transactions'], - country_codes: ['US'], // TODO + products: [Products.Transactions], + country_codes: [CountryCode.Us], // TODO language: 'en' // TODO } - if (req.body.access_token) { - options.access_token = req.body.access_token - delete options.products - } - this.client.createLinkToken(options, (err, data) => { - if (err) { - logError('Error creating Plaid link token.', err) - } - logInfo('Successfully created Plaid link token.') - res.json({ link_token: data.link_token }) - }) - }) + const result = await this.client.linkTokenCreate(options) + + res.json({ link_token: result.data.link_token }) + }) app.post('/remove', async (req, res) => { try { await updateConfig(config => { @@ -178,7 +176,7 @@ export class PlaidIntegration { accountConfig: AccountConfig, startDate: Date, endDate: Date - ): Promise => { + ): Promise => { return new Promise(async (resolve, reject) => { accountConfig = accountConfig as PlaidAccountConfig try { @@ -186,13 +184,29 @@ export class PlaidIntegration { const start = format(startDate, dateFormat) const end = format(endDate, dateFormat) - let options: plaid.TransactionsRequestOptions = { count: 500, offset: 0 } - let accounts = await this.client.getTransactions(accountConfig.token, start, end, options) + let transactionsGetRequest: TransactionsGetRequest = { + access_token: accountConfig.token, + start_date: start, + end_date: end + } + let transactionsGetRequestOptions: TransactionsGetRequestOptions = { + account_ids: [accountConfig.id], + count: 500, + offset: 0 + } + let accounts: TransactionsGetResponse = null + + await this.client.transactionsGet(transactionsGetRequest, transactionsGetRequestOptions).then(res => { + accounts = res.data + }) while (accounts.transactions.length < accounts.total_transactions) { - options.offset += options.count - const next_page = await this.client.getTransactions(accountConfig.token, start, end, options) - accounts.transactions = accounts.transactions.concat(next_page.transactions) + transactionsGetRequestOptions.offset += transactionsGetRequestOptions.count + const next_page = await this.client.transactionsGet( + transactionsGetRequest, + transactionsGetRequestOptions + ) + accounts.transactions = accounts.transactions.concat(next_page.data.transactions) } return resolve(accounts) From bb7e0ce780a35b1ed1c8bc209e39241470cfb60b Mon Sep 17 00:00:00 2001 From: ericcater Date: Tue, 8 Nov 2022 20:26:57 +0300 Subject: [PATCH 2/4] change package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 68071652..7c5d092a 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "jsonc": "^2.0.0", "lodash": "^4.17.15", "open": "^7.0.2", - "plaid": "^7.0.0", + "plaid": "^12.0.0", "prompts": "^2.3.1", "typescript": "^3.8.3", "typescript-json-schema": "^0.42.0", From e099683947860c976a5aa73ba7b99d471e92db05 Mon Sep 17 00:00:00 2001 From: ericcater Date: Wed, 9 Nov 2022 09:57:18 +0300 Subject: [PATCH 3/4] fix pagination --- src/integrations/plaid/plaidIntegration.ts | 69 +++++++++++----------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/src/integrations/plaid/plaidIntegration.ts b/src/integrations/plaid/plaidIntegration.ts index 81fac7c8..b038d3cd 100644 --- a/src/integrations/plaid/plaidIntegration.ts +++ b/src/integrations/plaid/plaidIntegration.ts @@ -1,6 +1,7 @@ import path from 'path' import { parseISO, format, subMonths } from 'date-fns' -import { +import plaid, { + AccountBase, Configuration, CountryCode, ItemPublicTokenExchangeResponse, @@ -8,9 +9,7 @@ import { PlaidApi, PlaidEnvironments, Products, - TransactionsGetRequest, - TransactionsGetRequestOptions, - TransactionsGetResponse + TransactionsGetRequest } from 'plaid' import { Config, updateConfig } from '../../common/config' import { PlaidConfig, PlaidEnvironmentType } from '../../types/integrations/plaid' @@ -176,40 +175,40 @@ export class PlaidIntegration { accountConfig: AccountConfig, startDate: Date, endDate: Date - ): Promise => { + ): Promise<{ accounts: AccountBase[]; transactions: plaid.Transaction[] }> => { return new Promise(async (resolve, reject) => { accountConfig = accountConfig as PlaidAccountConfig + + const dateFormat = 'yyyy-MM-dd' + const start = format(startDate, dateFormat) + const end = format(endDate, dateFormat) + + const request: TransactionsGetRequest = { + access_token: accountConfig.token, + start_date: start, + end_date: end + } + try { - const dateFormat = 'yyyy-MM-dd' - const start = format(startDate, dateFormat) - const end = format(endDate, dateFormat) - - let transactionsGetRequest: TransactionsGetRequest = { - access_token: accountConfig.token, - start_date: start, - end_date: end - } - let transactionsGetRequestOptions: TransactionsGetRequestOptions = { - account_ids: [accountConfig.id], - count: 500, - offset: 0 - } - let accounts: TransactionsGetResponse = null - - await this.client.transactionsGet(transactionsGetRequest, transactionsGetRequestOptions).then(res => { - accounts = res.data - }) - - while (accounts.transactions.length < accounts.total_transactions) { - transactionsGetRequestOptions.offset += transactionsGetRequestOptions.count - const next_page = await this.client.transactionsGet( - transactionsGetRequest, - transactionsGetRequestOptions - ) - accounts.transactions = accounts.transactions.concat(next_page.data.transactions) - } + const response = await this.client.transactionsGet(request) + + let transactions = response.data.transactions + const total_transactions = response.data.total_transactions + // Manipulate the offset parameter to paginate + // transactions and retrieve all available data + + while (transactions.length < total_transactions) { + const paginatedRequest: TransactionsGetRequest = { + ...request, + options: { + offset: transactions.length + } + } - return resolve(accounts) + const paginatedResponse = await this.client.transactionsGet(paginatedRequest) + transactions = transactions.concat(paginatedResponse.data.transactions) + } + return resolve({ accounts: response.data.accounts, transactions: transactions }) } catch (e) { return reject(e) } @@ -269,7 +268,7 @@ export class PlaidIntegration { })) logInfo( - `Fetched ${data.accounts.length} sub-accounts and ${data.total_transactions} transactions.`, + `Fetched ${data.accounts.length} sub-accounts and ${data.transactions.length} transactions.`, accounts ) return accounts From 8dbbc12ac04f4f82965b18c261a1b666902edace Mon Sep 17 00:00:00 2001 From: ericcater Date: Wed, 9 Nov 2022 10:08:08 +0300 Subject: [PATCH 4/4] added specifying plaid api version --- src/integrations/plaid/plaidIntegration.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/integrations/plaid/plaidIntegration.ts b/src/integrations/plaid/plaidIntegration.ts index b038d3cd..be2fde5b 100644 --- a/src/integrations/plaid/plaidIntegration.ts +++ b/src/integrations/plaid/plaidIntegration.ts @@ -44,7 +44,8 @@ export class PlaidIntegration { baseOptions: { headers: { 'PLAID-CLIENT-ID': this.plaidConfig.credentials.clientId, - 'PLAID-SECRET': this.plaidConfig.credentials.secret + 'PLAID-SECRET': this.plaidConfig.credentials.secret, + 'Plaid-Version': '2020-09-14', } } }) @@ -80,9 +81,11 @@ export class PlaidIntegration { let server: http.Server app.post('/get_access_token', (req, res) => { + console.log(req.body.public_token) if (req.body.public_token !== undefined) { client.itemPublicTokenExchange({ public_token: req.body.public_token }).then(res => { this.savePublicToken(res.data) + console.log(res.data) resolve(logInfo('Plaid access token saved.', req.body)) }) } else if (req.body.exit !== undefined) { @@ -135,6 +138,7 @@ export class PlaidIntegration { const result = await this.client.linkTokenCreate(options) res.json({ link_token: result.data.link_token }) + console.log(result.data.link_token) }) app.post('/remove', async (req, res) => { try { @@ -210,6 +214,7 @@ export class PlaidIntegration { } return resolve({ accounts: response.data.accounts, transactions: transactions }) } catch (e) { + console.log(e) return reject(e) } })