From 706a324121b6f7256427eeeb7ec0f2de9a629901 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 8 Feb 2024 20:18:52 +0200 Subject: [PATCH] feat(server): Plaid webhooks --- .../Banking/PlaidBankingController.ts | 18 +++ .../Banking/Plaid/PlaidApplication.ts | 26 +++ .../services/Banking/Plaid/PlaidWebhooks.ts | 151 ++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts diff --git a/packages/server/src/api/controllers/Banking/PlaidBankingController.ts b/packages/server/src/api/controllers/Banking/PlaidBankingController.ts index 1d1cfe8a0..1f6c3f4dc 100644 --- a/packages/server/src/api/controllers/Banking/PlaidBankingController.ts +++ b/packages/server/src/api/controllers/Banking/PlaidBankingController.ts @@ -16,6 +16,7 @@ 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; } @@ -50,4 +51,21 @@ 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/services/Banking/Plaid/PlaidApplication.ts b/packages/server/src/services/Banking/Plaid/PlaidApplication.ts index 16096207e..891b06c45 100644 --- a/packages/server/src/services/Banking/Plaid/PlaidApplication.ts +++ b/packages/server/src/services/Banking/Plaid/PlaidApplication.ts @@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi'; import { PlaidLinkTokenService } from './PlaidLinkToken'; import { PlaidItemService } from './PlaidItem'; import { PlaidItemDTO } from '@/interfaces'; +import { PlaidWebooks } from './PlaidWebhooks'; @Service() export class PlaidApplication { @@ -11,6 +12,9 @@ export class PlaidApplication { @Inject() private plaidItemService: PlaidItemService; + @Inject() + private plaidWebhooks: PlaidWebooks; + /** * Retrieves the Plaid link token. * @param {number} tenantId @@ -30,4 +34,26 @@ export class PlaidApplication { public exchangeToken(tenantId: number, itemDTO: PlaidItemDTO): Promise { return this.plaidItemService.item(tenantId, itemDTO); } + + /** + * Listens to Plaid webhooks + * @param {number} tenantId + * @param {string} webhookType + * @param {string} plaidItemId + * @param {string} webhookCode + * @returns + */ + public webhooks( + tenantId: number, + webhookType: string, + plaidItemId: string, + webhookCode: string + ) { + return this.plaidWebhooks.webhooks( + tenantId, + webhookType, + plaidItemId, + webhookCode + ); + } } diff --git a/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts b/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts new file mode 100644 index 000000000..a91ac7d82 --- /dev/null +++ b/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts @@ -0,0 +1,151 @@ +import { Inject, Service } from 'typedi'; +import { PlaidUpdateTransactions } from './PlaidUpdateTransactions'; + +@Service() +export class PlaidWebooks { + @Inject() + private updateTransactionsService: PlaidUpdateTransactions; + + /** + * Listens to Plaid webhooks + * @param {number} tenantId + * @param {string} webhookType + * @param {string} plaidItemId + * @param {string} webhookCode + */ + async webhooks( + tenantId: number, + webhookType: string, + plaidItemId: string, + webhookCode: string + ) { + // 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, + }; + const webhookHandler = + webhookHandlerMap[webhookType] || this.unhandledWebhook; + + await webhookHandler(tenantId, webhookCode, plaidItemId); + } + + /** + * Handles all unhandled/not yet implemented webhook events. + * @param {string} webhookType + * @param {string} webhookCode + * @param {string} plaidItemId + */ + async unhandledWebhook( + webhookType: string, + webhookCode: string, + plaidItemId: string + ) { + console.log( + `UNHANDLED ${webhookType} WEBHOOK: ${webhookCode}: Plaid item id ${plaidItemId}: unhandled webhook type received.` + ); + } + + /** + * Logs to console and emits to socket + * @param {string} additionalInfo + * @param {string} webhookCode + * @param {string} plaidItemId + */ + private serverLogAndEmitSocket( + additionalInfo: string, + webhookCode: string, + plaidItemId: string + ) { + console.log( + `WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}` + ); + } + + /** + * Handles all transaction webhook events. The transaction webhook notifies + * you that a single item has new transactions available. + * @param {number} tenantId + * @param {string} plaidItemId + * @param {string} webhookCode + * @returns {Promise} + */ + public async handleTransactionsWebooks( + tenantId: number, + plaidItemId: string, + webhookCode: string + ): Promise { + switch (webhookCode) { + case 'SYNC_UPDATES_AVAILABLE': { + // Fired when new transactions data becomes available. + const { addedCount, modifiedCount, removedCount } = + await this.updateTransactionsService.updateTransactions( + tenantId, + plaidItemId + ); + this.serverLogAndEmitSocket( + `Transactions: ${addedCount} added, ${modifiedCount} modified, ${removedCount} removed`, + webhookCode, + plaidItemId + ); + break; + } + case 'DEFAULT_UPDATE': + case 'INITIAL_UPDATE': + case 'HISTORICAL_UPDATE': + /* ignore - not needed if using sync endpoint + webhook */ + break; + default: + this.serverLogAndEmitSocket( + `unhandled webhook type received.`, + webhookCode, + plaidItemId + ); + } + } + + /** + * Handles all Item webhook events. + * @param {number} tenantId - Tenant ID + * @param {string} webhookCode - The webhook code + * @param {string} plaidItemId - The Plaid ID for the item + * @returns {Promise} + */ + public async itemsHandler( + tenantId: number, + webhookCode: string, + plaidItemId: 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 + ); + } + } +}