mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
feat: disconnect bank account
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
@@ -651,6 +651,11 @@ export default {
|
||||
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||
},
|
||||
|
||||
bankAccount: {
|
||||
onDisconnecting: 'onBankAccountDisconnecting',
|
||||
onDisconnected: 'onBankAccountDisconnected',
|
||||
},
|
||||
|
||||
// Import files.
|
||||
import: {
|
||||
onImportCommitted: 'onImportFileCommitted',
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user