mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +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 BaseController from '@/api/controllers/BaseController';
|
||||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||||
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
|
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
|
||||||
|
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankAccountsController extends BaseController {
|
export class BankAccountsController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private getBankAccountSummaryService: GetBankAccountSummary;
|
private getBankAccountSummaryService: GetBankAccountSummary;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private bankAccountsApp: BankAccountsApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
@@ -16,6 +20,10 @@ export class BankAccountsController extends BaseController {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
|
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
|
||||||
|
router.post(
|
||||||
|
'/:bankAccountId/disconnect',
|
||||||
|
this.discountBankAccount.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
@@ -46,4 +54,31 @@ export class BankAccountsController extends BaseController {
|
|||||||
next(error);
|
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',
|
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bankAccount: {
|
||||||
|
onDisconnecting: 'onBankAccountDisconnecting',
|
||||||
|
onDisconnected: 'onBankAccountDisconnected',
|
||||||
|
},
|
||||||
|
|
||||||
// Import files.
|
// Import files.
|
||||||
import: {
|
import: {
|
||||||
onImportCommitted: 'onImportFileCommitted',
|
onImportCommitted: 'onImportFileCommitted',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
PopoverInteractionKind,
|
PopoverInteractionKind,
|
||||||
Position,
|
Position,
|
||||||
|
Intent,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
DashboardActionsBar,
|
DashboardActionsBar,
|
||||||
DashboardRowsHeightButton,
|
DashboardRowsHeightButton,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
|
AppToaster,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
|
|
||||||
import { CashFlowMenuItems } from './utils';
|
import { CashFlowMenuItems } from './utils';
|
||||||
@@ -33,6 +35,7 @@ import withSettings from '@/containers/Settings/withSettings';
|
|||||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
import { useDisconnectBankAccount } from '@/hooks/query/bank-rules';
|
||||||
|
|
||||||
function AccountTransactionsActionsBar({
|
function AccountTransactionsActionsBar({
|
||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
@@ -50,6 +53,8 @@ function AccountTransactionsActionsBar({
|
|||||||
// Refresh cashflow infinity transactions hook.
|
// Refresh cashflow infinity transactions hook.
|
||||||
const { refresh } = useRefreshCashflowTransactionsInfinity();
|
const { refresh } = useRefreshCashflowTransactionsInfinity();
|
||||||
|
|
||||||
|
const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount();
|
||||||
|
|
||||||
// Retrieves the money in/out buttons options.
|
// Retrieves the money in/out buttons options.
|
||||||
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
|
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
|
||||||
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
|
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
|
||||||
@@ -82,6 +87,26 @@ function AccountTransactionsActionsBar({
|
|||||||
const handleBankRulesClick = () => {
|
const handleBankRulesClick = () => {
|
||||||
history.push(`/bank-rules?accountId=${accountId}`);
|
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.
|
// Handle the refresh button click.
|
||||||
const handleRefreshBtnClick = () => {
|
const handleRefreshBtnClick = () => {
|
||||||
refresh();
|
refresh();
|
||||||
@@ -142,6 +167,10 @@ function AccountTransactionsActionsBar({
|
|||||||
content={
|
content={
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
|
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
|
||||||
|
|
||||||
|
{isConnected && (
|
||||||
|
<MenuItem onClick={handleDisconnectClick} text={'Disconnect'} />
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
UseMutationOptions,
|
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 {
|
interface EditBankRuleValues {
|
||||||
id: number;
|
id: number;
|
||||||
value: any;
|
value: any;
|
||||||
|
|||||||
Reference in New Issue
Block a user