feat: disconnect and update bank account

This commit is contained in:
Ahmed Bouhuolia
2024-07-16 17:09:00 +02:00
parent fa7e6b1fca
commit c2815afbe3
11 changed files with 246 additions and 29 deletions

View File

@@ -22,8 +22,9 @@ export class BankAccountsController extends BaseController {
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
router.post(
'/:bankAccountId/disconnect',
this.discountBankAccount.bind(this)
this.disconnectBankAccount.bind(this)
);
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
return router;
}
@@ -81,4 +82,31 @@ export class BankAccountsController extends BaseController {
next(error);
}
}
/**
* Refresh the given bank account.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
async refreshBankAccount(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.refreshBankAccount(tenantId, bankAccountId);
return res.status(200).send({
id: bankAccountId,
message: 'The bank account has been disconnected.',
});
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('accounts', (table) => {
table.string('plaid_item_id').nullable();
});
};
exports.down = function (knex) {
return knex.schema.table('accounts', (table) => {
table.dropColumn('plaid_item_id');
});
};

View File

@@ -52,6 +52,7 @@ const noAccessTokenLogger = async (
// Plaid client methods used in this app, mapped to their appropriate logging functions.
const clientMethodLoggingFns = {
transactionsRefresh: defaultLogger,
accountsGet: defaultLogger,
institutionsGet: noAccessTokenLogger,
institutionsGetById: noAccessTokenLogger,

View File

@@ -197,6 +197,7 @@ export default class Account extends mixin(TenantModel, [
const ExpenseEntry = require('models/ExpenseCategory');
const ItemEntry = require('models/ItemEntry');
const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
const PlaidItem = require('models/PlaidItem');
return {
/**
@@ -321,6 +322,18 @@ export default class Account extends mixin(TenantModel, [
query.where('categorized', false);
},
},
/**
* Account model may belongs to a Plaid item.
*/
plaidItem: {
relation: Model.BelongsToOneRelation,
modelClass: PlaidItem.default,
join: {
from: 'accounts.plaidItemId',
to: 'plaid_items.id',
},
},
};
}

View File

@@ -1,11 +1,15 @@
import { Inject, Service } from 'typedi';
import { DisconnectBankAccount } from './DisconnectBankAccount';
import { RefreshBankAccountService } from './RefreshBankAccount';
@Service()
export class BankAccountsApplication {
@Inject()
private disconnectBankAccountService: DisconnectBankAccount;
@Inject()
private refreshBankAccountService: RefreshBankAccountService;
/**
* Disconnects the given bank account.
* @param {number} tenantId
@@ -18,4 +22,17 @@ export class BankAccountsApplication {
bankAccountId
);
}
/**
* Refresh the bank transactions of the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
async refreshBankAccount(tenantId: number, bankAccountId: number) {
return this.refreshBankAccountService.refreshBankAccount(
tenantId,
bankAccountId
);
}
}

View File

@@ -6,6 +6,7 @@ import { PlaidClientWrapper } from '@/lib/Plaid';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { ERRORS } from './types';
@Service()
export class DisconnectBankAccount {
@@ -20,45 +21,46 @@ export class DisconnectBankAccount {
/**
* Disconnects the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
async disconnectBankAccount(tenantId: number, bankAccountId: number) {
const { Account, PlaidItem } = this.tenancy.models(tenantId);
// Retrieve the bank account or throw not found error.
const account = await Account.query()
.findById(bankAccountId)
.where('type', ['bank'])
.whereIn('account_type', ['bank', 'cash'])
.throwIfNotFound();
const plaidItem = await PlaidItem.query().findById(account.plaidAccountId);
const oldPlaidItem = await PlaidItem.query().findById(account.plaidItemId);
if (!plaidItem) {
if (!oldPlaidItem) {
throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED);
}
const request = {
accessToken: plaidItem.plaidAccessToken,
};
const plaidInstance = new PlaidClientWrapper();
//
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBankAccountDisconnecting` event.
await this.eventPublisher.emitAsync(events.bankAccount.onDisconnecting, {
tenantId,
bankAccountId,
});
// Remove the Plaid item.
const data = await plaidInstance.itemRemove(request);
// Remove the Plaid item from the system.
await PlaidItem.query().findById(account.plaidAccountId).delete();
await PlaidItem.query(trx).findById(account.plaidItemId).delete();
// Remove the plaid item association to the bank account.
await Account.query().findById(bankAccountId).patch({
await Account.query(trx).findById(bankAccountId).patch({
plaidAccountId: null,
plaidItemId: null,
isFeedsActive: false,
});
// Remove the Plaid item.
const data = await plaidInstance.itemRemove({
access_token: oldPlaidItem.plaidAccessToken,
});
// Triggers `onBankAccountDisconnected` event.
await this.eventPublisher.emitAsync(events.bankAccount.onDisconnected, {
tenantId,
bankAccountId,
@@ -66,7 +68,3 @@ export class DisconnectBankAccount {
});
}
}
const ERRORS = {
BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED',
};

View File

@@ -0,0 +1,39 @@
import { ServiceError } from '@/exceptions';
import { PlaidClientWrapper } from '@/lib/Plaid';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { Inject } from 'typedi';
export class RefreshBankAccountService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
*
* @param {number} tenantId
* @param {number} bankAccountId
*/
public async refreshBankAccount(tenantId: number, bankAccountId: number) {
const { Account } = this.tenancy.models(tenantId);
const bankAccount = await Account.query()
.findById(bankAccountId)
.withGraphFetched('plaidItem')
.throwIfNotFound();
if (!bankAccount.plaidItem) {
throw new ServiceError('');
}
const plaidInstance = new PlaidClientWrapper();
const data = await plaidInstance.transactionsRefresh({
access_token: bankAccount.plaidItem.plaidAccessToken,
});
await Account.query().findById(bankAccountId).patch({
isFeedsActive: true,
lastFeedsUpdatedAt: new Date(),
});
}
}

View File

@@ -0,0 +1,17 @@
import { Knex } from 'knex';
export interface IBankAccountDisconnectingEventPayload {
tenantId: number;
bankAccountId: number;
trx: Knex.Transaction;
}
export interface IBankAccountDisconnectedEventPayload {
tenantId: number;
bankAccountId: number;
trx: Knex.Transaction;
}
export const ERRORS = {
BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED',
};