feat: wip pause/resume bank feeds syncing

This commit is contained in:
Ahmed Bouhuolia
2024-08-04 11:22:21 +02:00
parent 5e12a4cea4
commit 208800b411
12 changed files with 384 additions and 16 deletions

View File

@@ -3,6 +3,7 @@ import { NextFunction, Request, Response, Router } from 'express';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary'; import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication'; import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
import { param } from 'express-validator';
@Service() @Service()
export class BankAccountsController extends BaseController { export class BankAccountsController extends BaseController {
@@ -26,10 +27,18 @@ export class BankAccountsController extends BaseController {
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this)); router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
router.post( router.post(
'/:bankAccountId/pause_feeds', '/:bankAccountId/pause_feeds',
[
param('bankAccountId').exists().isNumeric().toInt(),
],
this.validationResult,
this.pauseBankAccountFeeds.bind(this) this.pauseBankAccountFeeds.bind(this)
); );
router.post( router.post(
'/:bankAccountId/resume_feeds', '/:bankAccountId/resume_feeds',
[
param('bankAccountId').exists().isNumeric().toInt(),
],
this.validationResult,
this.resumeBankAccountFeeds.bind(this) this.resumeBankAccountFeeds.bind(this)
); );
@@ -117,6 +126,13 @@ export class BankAccountsController extends BaseController {
} }
} }
/**
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async resumeBankAccountFeeds( async resumeBankAccountFeeds(
req: Request<{ bankAccountId: number }>, req: Request<{ bankAccountId: number }>,
res: Response, res: Response,
@@ -129,13 +145,21 @@ export class BankAccountsController extends BaseController {
await this.bankAccountsApp.resumeBankAccount(tenantId, bankAccountId); await this.bankAccountsApp.resumeBankAccount(tenantId, bankAccountId);
return res.status(200).send({ return res.status(200).send({
message: '', message: 'The bank account feeds syncing has been resumed.',
id: bankAccountId,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
} }
} }
/**
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async pauseBankAccountFeeds( async pauseBankAccountFeeds(
req: Request<{ bankAccountId: number }>, req: Request<{ bankAccountId: number }>,
res: Response, res: Response,
@@ -148,7 +172,8 @@ export class BankAccountsController extends BaseController {
await this.bankAccountsApp.pauseBankAccount(tenantId, bankAccountId); await this.bankAccountsApp.pauseBankAccount(tenantId, bankAccountId);
return res.status(200).send({ return res.status(200).send({
message: '', message: 'The bank account feeds syncing has been paused.',
id: bankAccountId,
}); });
} catch (error) { } catch (error) {
next(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'; import TenantModel from 'models/TenantModel';
export default class PlaidItem extends TenantModel { export default class PlaidItem extends TenantModel {
pausedAt: Date;
/** /**
* Table name. * Table name.
*/ */
@@ -21,4 +23,19 @@ export default class PlaidItem extends TenantModel {
static get relationMappings() { static get relationMappings() {
return {}; 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

@@ -1,7 +1,8 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { DisconnectBankAccount } from './DisconnectBankAccount'; import { DisconnectBankAccount } from './DisconnectBankAccount';
import { RefreshBankAccountService } from './RefreshBankAccount'; import { RefreshBankAccountService } from './RefreshBankAccount';
import { ResumeBankAccountFeeds } from './PauseBankAccountFeeds'; import { PauseBankAccountFeeds } from './PauseBankAccountFeeds';
import { ResumeBankAccountFeeds } from './ResumeBankAccountFeeds';
@Service() @Service()
export class BankAccountsApplication { export class BankAccountsApplication {
@@ -15,7 +16,7 @@ export class BankAccountsApplication {
private resumeBankAccountFeedsService: ResumeBankAccountFeeds; private resumeBankAccountFeedsService: ResumeBankAccountFeeds;
@Inject() @Inject()
private pauseBankAccountFeedsService: ResumeBankAccountFeeds; private pauseBankAccountFeedsService: PauseBankAccountFeeds;
/** /**
* Disconnects the given bank account. * Disconnects the given bank account.
@@ -50,7 +51,7 @@ export class BankAccountsApplication {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async pauseBankAccount(tenantId: number, bankAccountId: number) { async pauseBankAccount(tenantId: number, bankAccountId: number) {
return this.pauseBankAccountFeedsService.resumeBankAccountFeeds( return this.pauseBankAccountFeedsService.pauseBankAccountFeeds(
tenantId, tenantId,
bankAccountId bankAccountId
); );

View File

@@ -1,9 +1,33 @@
import { Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
@Service() @Service()
export class ResumeBankAccountFeeds { export class PauseBankAccountFeeds {
public resumeBankAccountFeeds(tenantId: number, bankAccountId: number) { @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');
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await PlaidItem.query().findById(oldAccount.plaidItem.id).patch({
pausedAt: null,
});
});
} }
} }

View File

@@ -1,11 +1,32 @@
import { Service } from "typedi"; import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { Inject, Service } from 'typedi';
@Service() @Service()
export class ResumeBankAccountFeeds { 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} tenantId
* @param {number} bankAccountId * @param {number} bankAccountId
* @returns {Promise<void>}
*/ */
public resumeBankAccountFeeds(tenantId: number, bankAccountId: number) {} public async resumeBankAccountFeeds(tenantId: number, bankAccountId: number) {
const { Account, PlaidItem } = this.tenancy.models(tenantId);
const oldAccount = await Account.query()
.findById(bankAccountId)
.withGraphFetched('plaidItem');
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await PlaidItem.query().findById(oldAccount.plaidItem.id).patch({
pausedAt: new Date(),
});
});
}
} }

View File

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

View File

@@ -46,6 +46,7 @@ import {
useUnexcludeUncategorizedTransactions, useUnexcludeUncategorizedTransactions,
} from '@/hooks/query/bank-rules'; } from '@/hooks/query/bank-rules';
import { withBanking } from '../withBanking'; import { withBanking } from '../withBanking';
import withAlertActions from '@/containers/Alert/withAlertActions';
function AccountTransactionsActionsBar({ function AccountTransactionsActionsBar({
// #withDialogActions // #withDialogActions
@@ -60,6 +61,9 @@ function AccountTransactionsActionsBar({
// #withBanking // #withBanking
uncategorizedTransationsIdsSelected, uncategorizedTransationsIdsSelected,
excludedTransactionsIdsSelected, excludedTransactionsIdsSelected,
// #withAlerts
openAlert,
}) { }) {
const history = useHistory(); const history = useHistory();
const { accountId, currentAccount } = useAccountTransactionsContext(); const { accountId, currentAccount } = useAccountTransactionsContext();
@@ -191,6 +195,16 @@ function AccountTransactionsActionsBar({
}); });
}; };
// Handle resume bank feeds syncing.
const handleResumeFeedsSyncing = () => {
openAlert('resume-feeds-syncing-bank-accounnt');
};
// Handles pause bank feeds syncing.
const handlePauseFeedsSyncing = () => {
openAlert('pause-feeds-syncing-bank-accounnt');
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
@@ -284,6 +298,15 @@ function AccountTransactionsActionsBar({
}} }}
content={ content={
<Menu> <Menu>
<MenuItem
onClick={handlePauseFeedsSyncing}
text={'Pause bankfeeds syncing'}
/>
<MenuItem
onClick={handleResumeFeedsSyncing}
text={'Resume bankfeeds syncing'}
/>
<If condition={isSyncingOwner && isFeedsActive}> <If condition={isSyncingOwner && isFeedsActive}>
<MenuItem onClick={handleBankUpdateClick} text={'Update'} /> <MenuItem onClick={handleBankUpdateClick} text={'Update'} />
<MenuDivider /> <MenuDivider />
@@ -311,6 +334,7 @@ function AccountTransactionsActionsBar({
export default compose( export default compose(
withDialogActions, withDialogActions,
withAlertActions,
withSettingsActions, withSettingsActions,
withSettings(({ cashflowTransactionsSettings }) => ({ withSettings(({ cashflowTransactionsSettings }) => ({
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize, cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,

View File

@@ -0,0 +1,67 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
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';
/**
* Item activate 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={<T id={'activate'} />}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancelActivateItem}
loading={isLoading}
onConfirm={handleConfirmItemActivate}
>
<p>Are you sure.</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(PauseFeedsBankAccountAlert);

View File

@@ -0,0 +1,67 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
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';
/**
*
*/
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={<T id={'activate'} />}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancelActivateItem}
loading={isLoading}
onConfirm={handleConfirmItemActivate}
>
<p>Are you sure.</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,85 @@
import {
UseMutationOptions,
UseMutationResult,
useQueryClient,
useMutation,
} from 'react-query';
import useApiRequest from '../useRequest';
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, id) => {},
...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, id) => {},
...options,
},
);
}