Skip to content

Commit

Permalink
Merge pull request #489 from bigcapitalhq/fix-plaid-syncing
Browse files Browse the repository at this point in the history
fix: Plaid data available syncing
  • Loading branch information
abouolia authored Jun 6, 2024
2 parents fc9995c + 3dadbea commit 8862810
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 91 deletions.
38 changes: 21 additions & 17 deletions packages/server/src/api/controllers/Webhooks/Webhooks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Router } from 'express';
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
import { Request, Response } from 'express';
import { NextFunction, Router, Request, Response } from 'express';
import { Inject, Service } from 'typedi';
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
import BaseController from '../BaseController';
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
Expand Down Expand Up @@ -34,7 +33,7 @@ export class Webhooks extends BaseController {
* @param {Response} res
* @returns {Response}
*/
public async lemonWebhooks(req: Request, res: Response, next: any) {
public async lemonWebhooks(req: Request, res: Response, next: NextFunction) {
const data = req.body;
const signature = req.headers['x-signature'] ?? '';
const rawBody = req.rawBody;
Expand All @@ -57,20 +56,25 @@ export class Webhooks extends BaseController {
* @param {Response} res
* @returns {Response}
*/
public async plaidWebhooks(req: Request, res: Response) {
public async plaidWebhooks(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const {
webhook_type: webhookType,
webhook_code: webhookCode,
item_id: plaidItemId,
} = req.body;

await this.plaidApp.webhooks(
tenantId,
plaidItemId,
webhookType,
webhookCode
);
return res.status(200).send({ code: 200, message: 'ok' });
try {
const {
webhook_type: webhookType,
webhook_code: webhookCode,
item_id: plaidItemId,
} = req.body;

await this.plaidApp.webhooks(
tenantId,
plaidItemId,
webhookType,
webhookCode
);
return res.status(200).send({ code: 200, message: 'ok' });
} catch (error) {
next(error);
}
}
}
4 changes: 4 additions & 0 deletions packages/server/src/interfaces/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,7 @@ export enum TaxRateAction {
DELETE = 'Delete',
VIEW = 'View',
}

export interface CreateAccountParams {
ignoreUniqueName: boolean;
}
32 changes: 21 additions & 11 deletions packages/server/src/services/Accounts/CreateAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
IAccountEventCreatedPayload,
IAccountEventCreatingPayload,
IAccountCreateDTO,
CreateAccountParams,
} from '@/interfaces';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
Expand All @@ -30,19 +31,22 @@ export class CreateAccount {

/**
* Authorize the account creation.
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
*/
private authorize = async (
tenantId: number,
accountDTO: IAccountCreateDTO,
baseCurrency: string
baseCurrency: string,
params?: CreateAccountParams
) => {
// Validate account name uniquiness.
await this.validator.validateAccountNameUniquiness(
tenantId,
accountDTO.name
);
if (!params.ignoreUniqueName) {
await this.validator.validateAccountNameUniquiness(
tenantId,
accountDTO.name
);
}
// Validate the account code uniquiness.
if (accountDTO.code) {
await this.validator.isAccountCodeUniqueOrThrowError(
Expand Down Expand Up @@ -82,7 +86,7 @@ export class CreateAccount {

/**
* Transformes the create account DTO to input model.
* @param {IAccountCreateDTO} createAccountDTO
* @param {IAccountCreateDTO} createAccountDTO
*/
private transformDTOToModel = (
createAccountDTO: IAccountCreateDTO,
Expand All @@ -104,16 +108,21 @@ export class CreateAccount {
public createAccount = async (
tenantId: number,
accountDTO: IAccountCreateDTO,
trx?: Knex.Transaction
trx?: Knex.Transaction,
params: CreateAccountParams = { ignoreUniqueName: false }
): Promise<IAccount> => {
const { Account } = this.tenancy.models(tenantId);

// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });

// Authorize the account creation.
await this.authorize(tenantId, accountDTO, tenantMeta.baseCurrency);

await this.authorize(
tenantId,
accountDTO,
tenantMeta.baseCurrency,
params
);
// Transformes the DTO to model.
const accountInputModel = this.transformDTOToModel(
accountDTO,
Expand Down Expand Up @@ -148,3 +157,4 @@ export class CreateAccount {
);
};
}

92 changes: 70 additions & 22 deletions packages/server/src/services/Banking/Plaid/PlaidSyncDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { Inject, Service } from 'typedi';
import bluebird from 'bluebird';
import { entries, groupBy } from 'lodash';
import { CreateAccount } from '@/services/Accounts/CreateAccount';
import { PlaidAccount, PlaidTransaction } from '@/interfaces';
import {
IAccountCreateDTO,
PlaidAccount,
PlaidTransaction,
} from '@/interfaces';
import {
transformPlaidAccountToCreateAccount,
transformPlaidTrxsToCashflowCreate,
} from './utils';
import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTransactionService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
import { Knex } from 'knex';

const CONCURRENCY_ASYNC = 10;

Expand All @@ -28,6 +33,35 @@ export class PlaidSyncDb {
@Inject()
private deleteCashflowTransactionService: DeleteCashflowTransaction;

/**
* Syncs the Plaid bank account.
* @param {number} tenantId
* @param {IAccountCreateDTO} createBankAccountDTO
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public async syncBankAccount(
tenantId: number,
createBankAccountDTO: IAccountCreateDTO,
trx?: Knex.Transaction
) {
const { Account } = this.tenancy.models(tenantId);
const plaidAccount = await Account.query().findOne(
'plaidAccountId',
createBankAccountDTO.plaidAccountId
);
// Can't continue if the Plaid account is already created.
if (plaidAccount) {
return;
}
await this.createAccountService.createAccount(
tenantId,
createBankAccountDTO,
trx,
{ ignoreUniqueName: true }
);
}

/**
* Syncs the plaid accounts to the system accounts.
* @param {number} tenantId Tenant ID.
Expand All @@ -37,7 +71,8 @@ export class PlaidSyncDb {
public async syncBankAccounts(
tenantId: number,
plaidAccounts: PlaidAccount[],
institution: any
institution: any,
trx?: Knex.Transaction
): Promise<void> {
const transformToPlaidAccounts =
transformPlaidAccountToCreateAccount(institution);
Expand All @@ -47,7 +82,7 @@ export class PlaidSyncDb {
await bluebird.map(
accountCreateDTOs,
(createAccountDTO: any) =>
this.createAccountService.createAccount(tenantId, createAccountDTO),
this.syncBankAccount(tenantId, createAccountDTO, trx),
{ concurrency: CONCURRENCY_ASYNC }
);
}
Expand All @@ -61,15 +96,16 @@ export class PlaidSyncDb {
public async syncAccountTranactions(
tenantId: number,
plaidAccountId: number,
plaidTranasctions: PlaidTransaction[]
plaidTranasctions: PlaidTransaction[],
trx?: Knex.Transaction
): Promise<void> {
const { Account } = this.tenancy.models(tenantId);

const cashflowAccount = await Account.query()
const cashflowAccount = await Account.query(trx)
.findOne({ plaidAccountId })
.throwIfNotFound();

const openingEquityBalance = await Account.query().findOne(
const openingEquityBalance = await Account.query(trx).findOne(
'slug',
'opening-balance-equity'
);
Expand All @@ -87,7 +123,8 @@ export class PlaidSyncDb {
(uncategoriedDTO) =>
this.cashflowApp.createUncategorizedTransaction(
tenantId,
uncategoriedDTO
uncategoriedDTO,
trx
),
{ concurrency: 1 }
);
Expand All @@ -100,7 +137,8 @@ export class PlaidSyncDb {
*/
public async syncAccountsTransactions(
tenantId: number,
plaidAccountsTransactions: PlaidTransaction[]
plaidAccountsTransactions: PlaidTransaction[],
trx?: Knex.Transaction
): Promise<void> {
const groupedTrnsxByAccountId = entries(
groupBy(plaidAccountsTransactions, 'account_id')
Expand All @@ -111,7 +149,8 @@ export class PlaidSyncDb {
return this.syncAccountTranactions(
tenantId,
plaidAccountId,
plaidTransactions
plaidTransactions,
trx
);
},
{ concurrency: CONCURRENCY_ASYNC }
Expand All @@ -124,11 +163,12 @@ export class PlaidSyncDb {
*/
public async syncRemoveTransactions(
tenantId: number,
plaidTransactionsIds: string[]
plaidTransactionsIds: string[],
trx?: Knex.Transaction
) {
const { CashflowTransaction } = this.tenancy.models(tenantId);

const cashflowTransactions = await CashflowTransaction.query().whereIn(
const cashflowTransactions = await CashflowTransaction.query(trx).whereIn(
'plaidTransactionId',
plaidTransactionsIds
);
Expand All @@ -140,7 +180,8 @@ export class PlaidSyncDb {
(transactionId: number) =>
this.deleteCashflowTransactionService.deleteCashflowTransaction(
tenantId,
transactionId
transactionId,
trx
),
{ concurrency: CONCURRENCY_ASYNC }
);
Expand All @@ -155,11 +196,12 @@ export class PlaidSyncDb {
public async syncTransactionsCursor(
tenantId: number,
plaidItemId: string,
lastCursor: string
lastCursor: string,
trx?: Knex.Transaction
) {
const { PlaidItem } = this.tenancy.models(tenantId);

await PlaidItem.query().findOne({ plaidItemId }).patch({ lastCursor });
await PlaidItem.query(trx).findOne({ plaidItemId }).patch({ lastCursor });
}

/**
Expand All @@ -169,13 +211,16 @@ export class PlaidSyncDb {
*/
public async updateLastFeedsUpdatedAt(
tenantId: number,
plaidAccountIds: string[]
plaidAccountIds: string[],
trx?: Knex.Transaction
) {
const { Account } = this.tenancy.models(tenantId);

await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({
lastFeedsUpdatedAt: new Date(),
});
await Account.query(trx)
.whereIn('plaid_account_id', plaidAccountIds)
.patch({
lastFeedsUpdatedAt: new Date(),
});
}

/**
Expand All @@ -187,12 +232,15 @@ export class PlaidSyncDb {
public async updateAccountsFeedsActive(
tenantId: number,
plaidAccountIds: string[],
isFeedsActive: boolean = true
isFeedsActive: boolean = true,
trx?: Knex.Transaction
) {
const { Account } = this.tenancy.models(tenantId);

await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({
isFeedsActive,
});
await Account.query(trx)
.whereIn('plaid_account_id', plaidAccountIds)
.patch({
isFeedsActive,
});
}
}
Loading

0 comments on commit 8862810

Please sign in to comment.