feat(server): listen to Plaid webhooks

This commit is contained in:
Ahmed Bouhuolia
2024-02-14 17:11:07 +02:00
parent 706a324121
commit 1fd8a53ed1
8 changed files with 71 additions and 51 deletions

View File

@@ -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:

View File

@@ -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' });
}
}

View File

@@ -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' });
}
}

View File

@@ -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.
// ---------------------------

View File

@@ -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
},
};

View File

@@ -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
);
}

View File

@@ -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);

View File

@@ -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<void> {
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
);
}
}