diff --git a/.env.example b/.env.example index 4ae0ef539..72882dd40 100644 --- a/.env.example +++ b/.env.example @@ -75,6 +75,8 @@ PLAID_CLIENT_ID= PLAID_SECRET_DEVELOPMENT= PLAID_SECRET_SANDBOX= +PLAID_LINK_WEBHOOK= + # (Optional) Redirect URI settings section # Only required for OAuth redirect URI testing (not common on desktop): # Sandbox Mode: diff --git a/packages/server/src/api/controllers/Banking/PlaidBankingController.ts b/packages/server/src/api/controllers/Banking/PlaidBankingController.ts index 1f6c3f4dc..079fdf8bb 100644 --- a/packages/server/src/api/controllers/Banking/PlaidBankingController.ts +++ b/packages/server/src/api/controllers/Banking/PlaidBankingController.ts @@ -16,7 +16,6 @@ export class PlaidBankingController extends BaseController { router.post('/link-token', this.linkToken.bind(this)); router.post('/exchange-token', this.exchangeToken.bind(this)); - router.post('/webhooks', this.webhooks.bind(this)); return router; } @@ -36,7 +35,7 @@ export class PlaidBankingController extends BaseController { } /** - * + * Exchanges the given public token. * @param {Request} req * @param {response} res * @returns {Response} @@ -51,21 +50,4 @@ export class PlaidBankingController extends BaseController { }); return res.status(200).send({}); } - - public async webhooks(req: Request, res: Response) { - const { tenantId } = req; - const { - webhook_type: webhookType, - webhook_code: webhookCode, - item_id: plaidItemId, - } = req.body; - - await this.plaidApp.webhooks( - tenantId, - webhookType, - plaidItemId, - webhookCode - ); - return res.status(200).send({ code: 200, message: 'ok' }); - } } diff --git a/packages/server/src/api/controllers/Webhooks/Webhooks.ts b/packages/server/src/api/controllers/Webhooks/Webhooks.ts new file mode 100644 index 000000000..dd39ad526 --- /dev/null +++ b/packages/server/src/api/controllers/Webhooks/Webhooks.ts @@ -0,0 +1,47 @@ +import { Router } from 'express'; +import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication'; +import { Request, Response } from 'express'; +import { Inject, Service } from 'typedi'; +import BaseController from '../BaseController'; + +@Service() +export class Webhooks extends BaseController { + @Inject() + private plaidApp: PlaidApplication; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.post('/plaid', this.plaidWebhooks.bind(this)); + + return router; + } + + /** + * Listens to Plaid webhooks. + * @param {Request} req + * @param {Response} res + * @returns {Response} + */ + public async plaidWebhooks(req: Request, res: Response) { + const { tenantId } = req; + const { + webhook_type: webhookType, + webhook_code: webhookCode, + item_id: plaidItemId, + } = req.body; + + console.log(req.body, 'triggered'); + + await this.plaidApp.webhooks( + tenantId, + plaidItemId, + webhookType, + webhookCode + ); + return res.status(200).send({ code: 200, message: 'ok' }); + } +} diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts index fdc7d5e3a..e8ef89c10 100644 --- a/packages/server/src/api/index.ts +++ b/packages/server/src/api/index.ts @@ -57,6 +57,7 @@ import { ProjectTasksController } from './controllers/Projects/Tasks'; import { ProjectTimesController } from './controllers/Projects/Times'; import { TaxRatesController } from './controllers/TaxRates/TaxRates'; import { BankingController } from './controllers/Banking/BankingController'; +import { Webhooks } from './controllers/Webhooks/Webhooks'; export default () => { const app = Router(); @@ -72,6 +73,7 @@ export default () => { app.use('/ping', Container.get(Ping).router()); app.use('/jobs', Container.get(Jobs).router()); app.use('/account', Container.get(Account).router()); + app.use('/webhooks', Container.get(Webhooks).router()); // - Dashboard routes. // --------------------------- diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.ts index c38472b0c..12938038f 100644 --- a/packages/server/src/config/index.ts +++ b/packages/server/src/config/index.ts @@ -190,5 +190,6 @@ module.exports = { secretSandbox: process.env.PLAID_SECRET_SANDBOX, redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI, redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI, + linkWebhook: process.env.PLAID_LINK_WEBHOOK }, }; diff --git a/packages/server/src/services/Banking/Plaid/PlaidApplication.ts b/packages/server/src/services/Banking/Plaid/PlaidApplication.ts index 891b06c45..5b6b5d827 100644 --- a/packages/server/src/services/Banking/Plaid/PlaidApplication.ts +++ b/packages/server/src/services/Banking/Plaid/PlaidApplication.ts @@ -45,14 +45,14 @@ export class PlaidApplication { */ public webhooks( tenantId: number, - webhookType: string, plaidItemId: string, + webhookType: string, webhookCode: string ) { return this.plaidWebhooks.webhooks( tenantId, - webhookType, plaidItemId, + webhookType, webhookCode ); } diff --git a/packages/server/src/services/Banking/Plaid/PlaidLinkToken.ts b/packages/server/src/services/Banking/Plaid/PlaidLinkToken.ts index 7d67ec8af..89203df72 100644 --- a/packages/server/src/services/Banking/Plaid/PlaidLinkToken.ts +++ b/packages/server/src/services/Banking/Plaid/PlaidLinkToken.ts @@ -1,5 +1,6 @@ import { PlaidClientWrapper } from '@/lib/Plaid'; import { Service } from 'typedi'; +import config from '@/config'; @Service() export class PlaidLinkTokenService { @@ -11,7 +12,7 @@ export class PlaidLinkTokenService { async getLinkToken(tenantId: number) { const accessToken = null; - // must include transactions in order to receive transactions webhooks + // Must include transactions in order to receive transactions webhooks const products = ['transactions']; const linkTokenParams = { user: { @@ -22,13 +23,9 @@ export class PlaidLinkTokenService { products, country_codes: ['US'], language: 'en', - // webhook: httpTunnel.public_url + '/services/webhook', + webhook: config.plaid.linkWebhook, access_token: accessToken, }; - // If user has entered a redirect uri in the .env file - // if (redirect_uri.indexOf('http') === 0) { - // linkTokenParams.redirect_uri = redirect_uri; - // } const plaidInstance = new PlaidClientWrapper(); const createResponse = await plaidInstance.linkTokenCreate(linkTokenParams); diff --git a/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts b/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts index a91ac7d82..f0389e893 100644 --- a/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts +++ b/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts @@ -13,22 +13,24 @@ export class PlaidWebooks { * @param {string} plaidItemId * @param {string} webhookCode */ - async webhooks( + public async webhooks( tenantId: number, - webhookType: string, plaidItemId: string, + webhookType: string, webhookCode: string ) { + const _webhookType = webhookType.toLowerCase(); + // There are five types of webhooks: AUTH, TRANSACTIONS, ITEM, INCOME, and ASSETS. // @TODO implement handling for remaining webhook types. const webhookHandlerMap = { - transactions: this.handleTransactionsWebooks, - item: this.itemsHandler, + transactions: this.handleTransactionsWebooks.bind(this), + item: this.itemsHandler.bind(this), }; const webhookHandler = - webhookHandlerMap[webhookType] || this.unhandledWebhook; + webhookHandlerMap[_webhookType] || this.unhandledWebhook; - await webhookHandler(tenantId, webhookCode, plaidItemId); + await webhookHandler(tenantId, plaidItemId, webhookCode); } /** @@ -37,7 +39,7 @@ export class PlaidWebooks { * @param {string} webhookCode * @param {string} plaidItemId */ - async unhandledWebhook( + private async unhandledWebhook( webhookType: string, webhookCode: string, plaidItemId: string @@ -114,37 +116,24 @@ export class PlaidWebooks { */ public async itemsHandler( tenantId: number, - webhookCode: string, - plaidItemId: string + plaidItemId: string, + webhookCode: string ): Promise { switch (webhookCode) { case 'WEBHOOK_UPDATE_ACKNOWLEDGED': this.serverLogAndEmitSocket('is updated', plaidItemId, error); break; case 'ERROR': { - this.serverLogAndEmitSocket( - `ERROR: ${error.error_code}: ${error.error_message}`, - itemId, - error.error_code - ); break; } case 'PENDING_EXPIRATION': { - const { id: itemId } = await retrieveItemByPlaidItemId(plaidItemId); - await updateItemStatus(itemId, 'bad'); - - this.serverLogAndEmitSocket( - `user needs to re-enter login credentials`, - itemId, - error - ); break; } default: this.serverLogAndEmitSocket( 'unhandled webhook type received.', - plaidItemId, - error + webhookCode, + plaidItemId ); } }