diff --git a/package.json b/package.json index f5e66406..f52bca95 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,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", diff --git a/src/integrations/plaid/plaidIntegration.ts b/src/integrations/plaid/plaidIntegration.ts index a7c57e8c..be2fde5b 100644 --- a/src/integrations/plaid/plaidIntegration.ts +++ b/src/integrations/plaid/plaidIntegration.ts @@ -1,6 +1,16 @@ import path from 'path' import { parseISO, format, subMonths } from 'date-fns' -import plaid, { TransactionsResponse, CreateLinkTokenOptions } from 'plaid' +import plaid, { + AccountBase, + Configuration, + CountryCode, + ItemPublicTokenExchangeResponse, + LinkTokenCreateRequest, + PlaidApi, + PlaidEnvironments, + Products, + TransactionsGetRequest +} from 'plaid' import { Config, updateConfig } from '../../common/config' import { PlaidConfig, PlaidEnvironmentType } from '../../types/integrations/plaid' import { IntegrationId } from '../../types/integrations' @@ -17,8 +27,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 +36,29 @@ export class PlaidIntegration { this.environment = this.plaidConfig.environment === PlaidEnvironmentType.Development - ? plaid.environments.development - : plaid.environments.sandbox - - this.client = new plaid.Client({ - clientID: this.plaidConfig.credentials.clientId, - secret: this.plaidConfig.credentials.secret, - env: this.environment, - options: { - version: '2019-05-29' + ? PlaidEnvironments.development + : PlaidEnvironments.sandbox + + const configuration = new Configuration({ + basePath: this.environment, + baseOptions: { + headers: { + 'PLAID-CLIENT-ID': this.plaidConfig.credentials.clientId, + 'PLAID-SECRET': this.plaidConfig.credentials.secret, + 'Plaid-Version': '2020-09-14', + } } }) + 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, @@ -71,12 +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.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) + console.log(res.data) resolve(logInfo('Plaid access token saved.', req.body)) }) } else if (req.body.exit !== undefined) { @@ -98,9 +107,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 +125,21 @@ 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 }) + console.log(result.data.link_token) + }) app.post('/remove', async (req, res) => { try { await updateConfig(config => { @@ -178,25 +179,42 @@ 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) + const response = await this.client.transactionsGet(request) - let options: plaid.TransactionsRequestOptions = { count: 500, offset: 0 } - let accounts = await this.client.getTransactions(accountConfig.token, start, end, options) + 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 (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) - } + 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) { + console.log(e) return reject(e) } }) @@ -255,7 +273,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