feat: disconnect bank account

This commit is contained in:
Ahmed Bouhuolia
2024-07-15 23:18:39 +02:00
parent 107a6f793b
commit fa7e6b1fca
6 changed files with 182 additions and 1 deletions

View File

@@ -3,12 +3,16 @@ import { NextFunction, Request, Response, Router } from 'express';
import BaseController from '@/api/controllers/BaseController';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
@Service()
export class BankAccountsController extends BaseController {
@Inject()
private getBankAccountSummaryService: GetBankAccountSummary;
@Inject()
private bankAccountsApp: BankAccountsApplication;
/**
* Router constructor.
*/
@@ -16,6 +20,10 @@ export class BankAccountsController extends BaseController {
const router = Router();
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
router.post(
'/:bankAccountId/disconnect',
this.discountBankAccount.bind(this)
);
return router;
}
@@ -46,4 +54,31 @@ export class BankAccountsController extends BaseController {
next(error);
}
}
/**
* Disonnect the given bank account.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
async disconnectBankAccount(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.disconnectBankAccount(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,21 @@
import { Inject, Service } from 'typedi';
import { DisconnectBankAccount } from './DisconnectBankAccount';
@Service()
export class BankAccountsApplication {
@Inject()
private disconnectBankAccountService: DisconnectBankAccount;
/**
* Disconnects the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
async disconnectBankAccount(tenantId: number, bankAccountId: number) {
return this.disconnectBankAccountService.disconnectBankAccount(
tenantId,
bankAccountId
);
}
}

View File

@@ -0,0 +1,72 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { PlaidClientWrapper } from '@/lib/Plaid';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
@Service()
export class DisconnectBankAccount {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Disconnects the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
async disconnectBankAccount(tenantId: number, bankAccountId: number) {
const { Account, PlaidItem } = this.tenancy.models(tenantId);
const account = await Account.query()
.findById(bankAccountId)
.where('type', ['bank'])
.throwIfNotFound();
const plaidItem = await PlaidItem.query().findById(account.plaidAccountId);
if (!plaidItem) {
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) => {
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();
// Remove the plaid item association to the bank account.
await Account.query().findById(bankAccountId).patch({
plaidAccountId: null,
isFeedsActive: false,
});
await this.eventPublisher.emitAsync(events.bankAccount.onDisconnected, {
tenantId,
bankAccountId,
});
});
}
}
const ERRORS = {
BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED',
};

View File

@@ -651,6 +651,11 @@ export default {
onUnexcluded: 'onBankTransactionUnexcluded',
},
bankAccount: {
onDisconnecting: 'onBankAccountDisconnecting',
onDisconnected: 'onBankAccountDisconnected',
},
// Import files.
import: {
onImportCommitted: 'onImportFileCommitted',

View File

@@ -11,6 +11,7 @@ import {
MenuItem,
PopoverInteractionKind,
Position,
Intent,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import {
@@ -18,6 +19,7 @@ import {
DashboardActionsBar,
DashboardRowsHeightButton,
FormattedMessage as T,
AppToaster,
} from '@/components';
import { CashFlowMenuItems } from './utils';
@@ -33,6 +35,7 @@ import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { compose } from '@/utils';
import { useDisconnectBankAccount } from '@/hooks/query/bank-rules';
function AccountTransactionsActionsBar({
// #withDialogActions
@@ -50,6 +53,8 @@ function AccountTransactionsActionsBar({
// Refresh cashflow infinity transactions hook.
const { refresh } = useRefreshCashflowTransactionsInfinity();
const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount();
// Retrieves the money in/out buttons options.
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
@@ -82,6 +87,26 @@ function AccountTransactionsActionsBar({
const handleBankRulesClick = () => {
history.push(`/bank-rules?accountId=${accountId}`);
};
const isConnected = true;
// Handles the bank account disconnect click.
const handleDisconnectClick = () => {
disconnectBankAccount(accountId)
.then(() => {
AppToaster.show({
message: 'The bank account has been disconnected.',
intent: Intent.SUCCESS,
});
})
.catch((error) => {
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
};
// Handle the refresh button click.
const handleRefreshBtnClick = () => {
refresh();
@@ -142,6 +167,10 @@ function AccountTransactionsActionsBar({
content={
<Menu>
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
{isConnected && (
<MenuItem onClick={handleDisconnectClick} text={'Disconnect'} />
)}
</Menu>
}
>

View File

@@ -1,4 +1,3 @@
// @ts-nocheck
import {
QueryClient,
UseMutationOptions,
@@ -61,6 +60,26 @@ export function useCreateBankRule(
);
}
interface DisconnectBankAccountRes {}
export function useDisconnectBankAccount(
options?: UseMutationOptions<number, Error, DisconnectBankAccountRes>,
) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<number, Error, DisconnectBankAccountRes>(
(bankAccountId: number) =>
apiRequest
.post(`/banking/bank_accounts/${bankAccountId}`)
.then((res) => res.data),
{
...options,
onSuccess: () => {},
},
);
}
interface EditBankRuleValues {
id: number;
value: any;