Merge pull request #563 from bigcapitalhq/pause-resume-bank-feeds-syncing

feat: pause/resume bank account feeds syncing
This commit is contained in:
Ahmed Bouhuolia
2024-08-04 21:47:32 +02:00
committed by GitHub
21 changed files with 724 additions and 24 deletions

View File

@@ -1,9 +1,9 @@
import { Inject, Service } from 'typedi';
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';
import { param } from 'express-validator';
@Service()
export class BankAccountsController extends BaseController {
@@ -25,6 +25,22 @@ export class BankAccountsController extends BaseController {
this.disconnectBankAccount.bind(this)
);
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
router.post(
'/:bankAccountId/pause_feeds',
[
param('bankAccountId').exists().isNumeric().toInt(),
],
this.validationResult,
this.pauseBankAccountFeeds.bind(this)
);
router.post(
'/:bankAccountId/resume_feeds',
[
param('bankAccountId').exists().isNumeric().toInt(),
],
this.validationResult,
this.resumeBankAccountFeeds.bind(this)
);
return router;
}
@@ -109,4 +125,58 @@ export class BankAccountsController extends BaseController {
next(error);
}
}
/**
* Resumes the bank account feeds sync.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async resumeBankAccountFeeds(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.resumeBankAccount(tenantId, bankAccountId);
return res.status(200).send({
message: 'The bank account feeds syncing has been resumed.',
id: bankAccountId,
});
} catch (error) {
next(error);
}
}
/**
* Pauses the bank account feeds sync.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async pauseBankAccountFeeds(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.pauseBankAccount(tenantId, bankAccountId);
return res.status(200).send({
message: 'The bank account feeds syncing has been paused.',
id: bankAccountId,
});
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('plaid_items', (table) => {
table.datetime('paused_at');
});
};
exports.down = function (knex) {
return knex.schema.table('plaid_items', (table) => {
table.dropColumn('paused_at');
});
};

View File

@@ -1,6 +1,8 @@
import TenantModel from 'models/TenantModel';
export default class PlaidItem extends TenantModel {
pausedAt: Date;
/**
* Table name.
*/
@@ -21,4 +23,19 @@ export default class PlaidItem extends TenantModel {
static get relationMappings() {
return {};
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isPaused'];
}
/**
* Detarmines whether the Plaid item feeds syncing is paused.
* @return {boolean}
*/
get isPaused() {
return !!this.pausedAt;
}
}

View File

@@ -18,9 +18,18 @@ export class AccountTransformer extends Transformer {
'flattenName',
'bankBalanceFormatted',
'lastFeedsUpdatedAtFormatted',
'isFeedsPaused',
];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['plaidItem'];
};
/**
* Retrieves the flatten name with all dependants accounts names.
* @param {IAccount} account -
@@ -66,6 +75,15 @@ export class AccountTransformer extends Transformer {
return this.formatDate(account.lastFeedsUpdatedAt);
};
/**
* Detarmines whether the bank account connection is paused.
* @param account
* @returns {boolean}
*/
protected isFeedsPaused = (account: any): boolean => {
return account.plaidItem?.isPaused || false;
};
/**
* Transformes the accounts collection to flat or nested array.
* @param {IAccount[]}

View File

@@ -25,7 +25,10 @@ export class GetAccount {
const { accountRepository } = this.tenancy.repositories(tenantId);
// Find the given account or throw not found error.
const account = await Account.query().findById(accountId).throwIfNotFound();
const account = await Account.query()
.findById(accountId)
.withGraphFetched('plaidItem')
.throwIfNotFound();
const accountsGraph = await accountRepository.getDependencyGraph();

View File

@@ -1,6 +1,8 @@
import { Inject, Service } from 'typedi';
import { DisconnectBankAccount } from './DisconnectBankAccount';
import { RefreshBankAccountService } from './RefreshBankAccount';
import { PauseBankAccountFeeds } from './PauseBankAccountFeeds';
import { ResumeBankAccountFeeds } from './ResumeBankAccountFeeds';
@Service()
export class BankAccountsApplication {
@@ -10,6 +12,12 @@ export class BankAccountsApplication {
@Inject()
private refreshBankAccountService: RefreshBankAccountService;
@Inject()
private resumeBankAccountFeedsService: ResumeBankAccountFeeds;
@Inject()
private pauseBankAccountFeedsService: PauseBankAccountFeeds;
/**
* Disconnects the given bank account.
* @param {number} tenantId
@@ -27,7 +35,7 @@ export class BankAccountsApplication {
* Refresh the bank transactions of the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
* @returns {Promise<void>}
*/
async refreshBankAccount(tenantId: number, bankAccountId: number) {
return this.refreshBankAccountService.refreshBankAccount(
@@ -35,4 +43,30 @@ export class BankAccountsApplication {
bankAccountId
);
}
/**
* Pauses the feeds sync of the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
async pauseBankAccount(tenantId: number, bankAccountId: number) {
return this.pauseBankAccountFeedsService.pauseBankAccountFeeds(
tenantId,
bankAccountId
);
}
/**
* Resumes the feeds sync of the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
async resumeBankAccount(tenantId: number, bankAccountId: number) {
return this.resumeBankAccountFeedsService.resumeBankAccountFeeds(
tenantId,
bankAccountId
);
}
}

View File

@@ -0,0 +1,44 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './types';
@Service()
export class PauseBankAccountFeeds {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Pauses the bankfeed syncing of the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
public async pauseBankAccountFeeds(tenantId: number, bankAccountId: number) {
const { Account, PlaidItem } = this.tenancy.models(tenantId);
const oldAccount = await Account.query()
.findById(bankAccountId)
.withGraphFetched('plaidItem')
.throwIfNotFound();
// Can't continue if the bank account is not connected.
if (!oldAccount.plaidItem) {
throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED);
}
// Cannot continue if the bank account feeds is already paused.
if (oldAccount.plaidItem.isPaused) {
throw new ServiceError(ERRORS.BANK_ACCOUNT_FEEDS_ALREADY_PAUSED);
}
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await PlaidItem.query(trx).findById(oldAccount.plaidItem.id).patch({
pausedAt: new Date(),
});
});
}
}

View File

@@ -0,0 +1,43 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './types';
@Service()
export class ResumeBankAccountFeeds {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Resumes the bank feeds syncing of the bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
public async resumeBankAccountFeeds(tenantId: number, bankAccountId: number) {
const { Account, PlaidItem } = this.tenancy.models(tenantId);
const oldAccount = await Account.query()
.findById(bankAccountId)
.withGraphFetched('plaidItem');
// Can't continue if the bank account is not connected.
if (!oldAccount.plaidItem) {
throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED);
}
// Cannot continue if the bank account feeds is already paused.
if (!oldAccount.plaidItem.isPaused) {
throw new ServiceError(ERRORS.BANK_ACCOUNT_FEEDS_ALREADY_RESUMED);
}
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await PlaidItem.query(trx).findById(oldAccount.plaidItem.id).patch({
pausedAt: null,
});
});
}
}

View File

@@ -14,4 +14,6 @@ export interface IBankAccountDisconnectedEventPayload {
export const ERRORS = {
BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED',
BANK_ACCOUNT_FEEDS_ALREADY_PAUSED: 'BANK_ACCOUNT_FEEDS_ALREADY_PAUSED',
BANK_ACCOUNT_FEEDS_ALREADY_RESUMED: 'BANK_ACCOUNT_FEEDS_ALREADY_RESUMED',
};

View File

@@ -1,11 +1,15 @@
import { Inject, Service } from 'typedi';
import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class PlaidWebooks {
@Inject()
private updateTransactionsService: PlaidUpdateTransactions;
@Inject()
private tenancy: HasTenancyService;
/**
* Listens to Plaid webhooks
* @param {number} tenantId - Tenant Id.
@@ -61,7 +65,7 @@ export class PlaidWebooks {
plaidItemId: string
): void {
console.log(
`WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}`
`PLAID WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}`
);
}
@@ -78,8 +82,21 @@ export class PlaidWebooks {
plaidItemId: string,
webhookCode: string
): Promise<void> {
const { PlaidItem } = this.tenancy.models(tenantId);
const plaidItem = await PlaidItem.query()
.findById(plaidItemId)
.throwIfNotFound();
switch (webhookCode) {
case 'SYNC_UPDATES_AVAILABLE': {
if (plaidItem.isPaused) {
this.serverLogAndEmitSocket(
'Plaid item syncing is paused.',
webhookCode,
plaidItemId
);
return;
}
// Fired when new transactions data becomes available.
const { addedCount, modifiedCount, removedCount } =
await this.updateTransactionsService.updateTransactions(

View File

@@ -52,6 +52,7 @@ import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/Rec
import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog';
import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
import { RuleFormDialog } from '@/containers/Banking/Rules/RuleFormDialog/RuleFormDialog';
import { DisconnectBankAccountDialog } from '@/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog';
/**
* Dialogs container.
@@ -148,7 +149,10 @@ export default function DialogsContainer() {
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
<ExportDialog dialogName={DialogsName.Export} />
<RuleFormDialog dialogName={DialogsName.BankRuleForm} />
<RuleFormDialog dialogName={DialogsName.BankRuleForm} />
<DisconnectBankAccountDialog
dialogName={DialogsName.DisconnectBankAccountConfirmation}
/>
</div>
);
}

View File

@@ -75,5 +75,6 @@ export enum DialogsName {
GeneralLedgerPdfPreview = 'GeneralLedgerPdfPreview',
SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview',
Export = 'Export',
BankRuleForm = 'BankRuleForm'
BankRuleForm = 'BankRuleForm',
DisconnectBankAccountConfirmation = 'DisconnectBankAccountConfirmation',
}

View File

@@ -28,6 +28,7 @@ import TaxRatesAlerts from '@/containers/TaxRates/alerts';
import { CashflowAlerts } from '../CashFlow/CashflowAlerts';
import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts';
import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts';
import { BankAccountAlerts } from '@/containers/CashFlow/AccountTransactions/alerts';
export default [
...AccountsAlerts,
@@ -58,5 +59,6 @@ export default [
...TaxRatesAlerts,
...CashflowAlerts,
...BankRulesAlerts,
...SubscriptionAlerts
...SubscriptionAlerts,
...BankAccountAlerts,
];

View File

@@ -40,12 +40,13 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { compose } from '@/utils';
import {
useDisconnectBankAccount,
useUpdateBankAccount,
useExcludeUncategorizedTransactions,
useUnexcludeUncategorizedTransactions,
} from '@/hooks/query/bank-rules';
import { withBanking } from '../withBanking';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { DialogsName } from '@/constants/dialogs';
function AccountTransactionsActionsBar({
// #withDialogActions
@@ -60,6 +61,9 @@ function AccountTransactionsActionsBar({
// #withBanking
uncategorizedTransationsIdsSelected,
excludedTransactionsIdsSelected,
// #withAlerts
openAlert,
}) {
const history = useHistory();
const { accountId, currentAccount } = useAccountTransactionsContext();
@@ -67,7 +71,6 @@ function AccountTransactionsActionsBar({
// Refresh cashflow infinity transactions hook.
const { refresh } = useRefreshCashflowTransactionsInfinity();
const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount();
const { mutateAsync: updateBankAccount } = useUpdateBankAccount();
// Retrieves the money in/out buttons options.
@@ -75,6 +78,7 @@ function AccountTransactionsActionsBar({
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
const isFeedsActive = !!currentAccount.is_feeds_active;
const isFeedsPaused = currentAccount.is_feeds_paused;
const isSyncingOwner = currentAccount.is_syncing_owner;
// Handle table row size change.
@@ -108,19 +112,9 @@ function AccountTransactionsActionsBar({
// Handles the bank account disconnect click.
const handleDisconnectClick = () => {
disconnectBankAccount({ bankAccountId: 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,
});
});
openDialog(DialogsName.DisconnectBankAccountConfirmation, {
bankAccountId: accountId,
});
};
// handles the bank update button click.
const handleBankUpdateClick = () => {
@@ -191,6 +185,19 @@ function AccountTransactionsActionsBar({
});
};
// Handle resume bank feeds syncing.
const handleResumeFeedsSyncing = () => {
openAlert('resume-feeds-syncing-bank-accounnt', {
bankAccountId: accountId,
});
};
// Handles pause bank feeds syncing.
const handlePauseFeedsSyncing = () => {
openAlert('pause-feeds-syncing-bank-accounnt', {
bankAccountId: accountId,
});
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -238,7 +245,9 @@ function AccountTransactionsActionsBar({
<Tooltip
content={
isFeedsActive
? 'The bank syncing is active'
? isFeedsPaused
? 'The bank syncing is paused'
: 'The bank syncing is active'
: 'The bank syncing is disconnected'
}
minimal={true}
@@ -247,7 +256,13 @@ function AccountTransactionsActionsBar({
<Button
className={Classes.MINIMAL}
icon={<Icon icon="feed" iconSize={16} />}
intent={isFeedsActive ? Intent.SUCCESS : Intent.DANGER}
intent={
isFeedsActive
? isFeedsPaused
? Intent.WARNING
: Intent.SUCCESS
: Intent.DANGER
}
/>
</Tooltip>
</If>
@@ -288,6 +303,23 @@ function AccountTransactionsActionsBar({
<MenuItem onClick={handleBankUpdateClick} text={'Update'} />
<MenuDivider />
</If>
<If condition={isSyncingOwner && isFeedsActive && !isFeedsPaused}>
<MenuItem
onClick={handlePauseFeedsSyncing}
text={'Pause bank feeds'}
/>
<MenuDivider />
</If>
<If condition={isSyncingOwner && isFeedsActive && isFeedsPaused}>
<MenuItem
onClick={handleResumeFeedsSyncing}
text={'Resume bank feeds'}
/>
<MenuDivider />
</If>
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
<If condition={isSyncingOwner && isFeedsActive}>
@@ -311,6 +343,7 @@ function AccountTransactionsActionsBar({
export default compose(
withDialogActions,
withAlertActions,
withSettingsActions,
withSettings(({ cashflowTransactionsSettings }) => ({
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,

View File

@@ -0,0 +1,68 @@
// @ts-nocheck
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster, FormattedMessage as T } from '@/components';
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { usePauseFeedsBankAccount } from '@/hooks/query/bank-accounts';
import { compose } from '@/utils';
/**
* Pause feeds of the bank account alert.
*/
function PauseFeedsBankAccountAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { bankAccountId },
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: pauseBankAccountFeeds, isLoading } =
usePauseFeedsBankAccount();
// Handle activate item alert cancel.
const handleCancelActivateItem = () => {
closeAlert(name);
};
// Handle confirm item activated.
const handleConfirmItemActivate = () => {
pauseBankAccountFeeds({ bankAccountId })
.then(() => {
AppToaster.show({
message: 'The bank feeds of the bank account has been paused.',
intent: Intent.SUCCESS,
});
})
.catch((error) => {})
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={'Pause bank feeds'}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancelActivateItem}
loading={isLoading}
onConfirm={handleConfirmItemActivate}
>
<p>
Are you sure want to pause bank feeds syncing of this bank account, you
can always resume it again?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(PauseFeedsBankAccountAlert);

View File

@@ -0,0 +1,69 @@
// @ts-nocheck
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster, FormattedMessage as T } from '@/components';
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { useResumeFeedsBankAccount } from '@/hooks/query/bank-accounts';
import { compose } from '@/utils';
/**
* Resume bank account feeds alert.
*/
function ResumeFeedsBankAccountAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { bankAccountId },
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: resumeFeedsBankAccount, isLoading } =
useResumeFeedsBankAccount();
// Handle activate item alert cancel.
const handleCancelActivateItem = () => {
closeAlert(name);
};
// Handle confirm item activated.
const handleConfirmItemActivate = () => {
resumeFeedsBankAccount({ bankAccountId })
.then(() => {
AppToaster.show({
message: 'The bank feeds of the bank account has been resumed.',
intent: Intent.SUCCESS,
});
})
.catch((error) => {})
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={'Resume bank feeds'}
intent={Intent.SUCCESS}
isOpen={isOpen}
onCancel={handleCancelActivateItem}
loading={isLoading}
onConfirm={handleConfirmItemActivate}
>
<p>
Are you sure want to resume bank feeds syncing of this bank account, you
can always pause it again?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ResumeFeedsBankAccountAlert);

View File

@@ -0,0 +1,24 @@
// @ts-nocheck
import React from 'react';
const ResumeFeedsBankAccountAlert = React.lazy(
() => import('./ResumeFeedsBankAccount'),
);
const PauseFeedsBankAccountAlert = React.lazy(
() => import('./PauseFeedsBankAccount'),
);
/**
* Bank account alerts.
*/
export const BankAccountAlerts = [
{
name: 'resume-feeds-syncing-bank-accounnt',
component: ResumeFeedsBankAccountAlert,
},
{
name: 'pause-feeds-syncing-bank-accounnt',
component: PauseFeedsBankAccountAlert,
},
];

View File

@@ -0,0 +1,42 @@
// @ts-nocheck
import React from 'react';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const DisconnectBankAccountDialogContent = React.lazy(
() => import('./DisconnectBankAccountDialogContent'),
);
/**
* Disconnect bank account confirmation dialog.
*/
function DisconnectBankAccountDialogRoot({
dialogName,
payload: { bankAccountId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={'Disconnect Bank Account'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
style={{ width: 400 }}
>
<DialogSuspense>
<DisconnectBankAccountDialogContent
dialogName={dialogName}
bankAccountId={bankAccountId}
/>
</DialogSuspense>
</Dialog>
);
}
export const DisconnectBankAccountDialog = compose(withDialogRedux())(
DisconnectBankAccountDialogRoot,
);
DisconnectBankAccountDialog.displayName = 'DisconnectBankAccountDialog';

View File

@@ -0,0 +1,104 @@
// @ts-nocheck
import * as Yup from 'yup';
import { Button, Intent, Classes } from '@blueprintjs/core';
import * as R from 'ramda';
import { Form, Formik, FormikHelpers } from 'formik';
import { AppToaster, FFormGroup, FInputGroup } from '@/components';
import { useDisconnectBankAccount } from '@/hooks/query/bank-rules';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
interface DisconnectFormValues {
label: string;
}
const initialValues = {
label: '',
};
const Schema = Yup.object().shape({
label: Yup.string().required().label('Confirmation'),
});
interface DisconnectBankAccountDialogContentProps {
bankAccountId: number;
}
function DisconnectBankAccountDialogContent({
bankAccountId,
// #withDialogActions
closeDialog,
}: DisconnectBankAccountDialogContentProps) {
const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount();
const handleSubmit = (
values: DisconnectFormValues,
{ setErrors, setSubmitting }: FormikHelpers<DisconnectFormValues>,
) => {
debugger;
setSubmitting(true);
if (values.label !== 'DISCONNECT ACCOUNT') {
setErrors({
label: 'The entered value is incorrect.',
});
setSubmitting(false);
return;
}
disconnectBankAccount({ bankAccountId })
.then(() => {
setSubmitting(false);
AppToaster.show({
message: 'The bank account has been disconnected.',
intent: Intent.SUCCESS,
});
closeDialog(DialogsName.DisconnectBankAccountConfirmation);
})
.catch((error) => {
setSubmitting(false);
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
};
const handleCancelBtnClick = () => {
closeDialog(DialogsName.DisconnectBankAccountConfirmation);
};
return (
<Formik
onSubmit={handleSubmit}
validationSchema={Schema}
initialValues={initialValues}
>
<Form>
<div className={Classes.DIALOG_BODY}>
<FFormGroup
label={`Type "DISCONNECT ACCOUNT"`}
name={'label'}
fastField
>
<FInputGroup name={'label'} fastField />
</FFormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button type="submit" intent={Intent.DANGER}>
Disconnect Bank Account
</Button>
<Button intent={Intent.NONE} onClick={handleCancelBtnClick}>
Cancel
</Button>
</div>
</div>
</Form>
</Formik>
);
}
export default R.compose(withDialogActions)(DisconnectBankAccountDialogContent);

View File

@@ -0,0 +1,91 @@
// @ts-nocheck
import {
UseMutationOptions,
UseMutationResult,
useQueryClient,
useMutation,
} from 'react-query';
import useApiRequest from '../useRequest';
import t from './types';
type PuaseFeedsBankAccountValues = { bankAccountId: number };
interface PuaseFeedsBankAccountResponse {}
/**
* Resumes the feeds syncing of the bank account.
* @param {UseMutationResult<PuaseFeedsBankAccountResponse, Error, ExcludeBankTransactionValue>} options
* @returns {UseMutationResult<PuaseFeedsBankAccountResponse, Error, ExcludeBankTransactionValue>}
*/
export function usePauseFeedsBankAccount(
options?: UseMutationOptions<
PuaseFeedsBankAccountResponse,
Error,
PuaseFeedsBankAccountValues
>,
): UseMutationResult<
PuaseFeedsBankAccountResponse,
Error,
PuaseFeedsBankAccountValues
> {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<
PuaseFeedsBankAccountResponse,
Error,
PuaseFeedsBankAccountValues
>(
(values) =>
apiRequest.post(
`/banking/bank_accounts/${values.bankAccountId}/pause_feeds`,
),
{
onSuccess: (res, values) => {
queryClient.invalidateQueries([t.ACCOUNT, values.bankAccountId]);
},
...options,
},
);
}
type ResumeFeedsBankAccountValues = { bankAccountId: number };
interface ResumeFeedsBankAccountResponse {}
/**
* Resumes the feeds syncing of the bank account.
* @param {UseMutationResult<ResumeFeedsBankAccountResponse, Error, ResumeFeedsBankAccountValues>} options
* @returns {UseMutationResult<ResumeFeedsBankAccountResponse, Error, ResumeFeedsBankAccountValues>}
*/
export function useResumeFeedsBankAccount(
options?: UseMutationOptions<
ResumeFeedsBankAccountResponse,
Error,
ResumeFeedsBankAccountValues
>,
): UseMutationResult<
ResumeFeedsBankAccountResponse,
Error,
ResumeFeedsBankAccountValues
> {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation<
ResumeFeedsBankAccountResponse,
Error,
ResumeFeedsBankAccountValues
>(
(values) =>
apiRequest.post(
`/banking/bank_accounts/${values.bankAccountId}/resume_feeds`,
),
{
onSuccess: (res, values) => {
queryClient.invalidateQueries([t.ACCOUNT, values.bankAccountId]);
},
...options,
},
);
}

View File

@@ -229,6 +229,9 @@ $dashboard-views-bar-height: 44px;
}
}
&.#{$ns}-minimal.#{$ns}-intent-warning{
color: #cc7e25;
}
&.button--blue-highlight {
background-color: #ebfaff;