From 208800b411e07fc24085006aaeac8b16e5bdf277 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 4 Aug 2024 11:22:21 +0200 Subject: [PATCH] feat: wip pause/resume bank feeds syncing --- .../Banking/BankAccountsController.ts | 29 ++++++- ...e_paused_at_column_to_plaid_items_table.js | 11 +++ packages/server/src/models/PlaidItem.ts | 17 ++++ .../BankAccounts/BankAccountsApplication.tsx | 15 ++-- .../BankAccounts/PauseBankAccountFeeds.tsx | 30 ++++++- .../BankAccounts/ResumeBankAccountFeeds.tsx | 27 +++++- .../containers/AlertsContainer/registered.tsx | 4 +- .../AccountTransactionsActionsBar.tsx | 24 ++++++ .../alerts/PauseFeedsBankAccount.tsx | 67 +++++++++++++++ .../alerts/ResumeFeedsBankAccount.tsx | 67 +++++++++++++++ .../AccountTransactions/alerts/index.ts | 24 ++++++ .../webapp/src/hooks/query/bank-accounts.ts | 85 +++++++++++++++++++ 12 files changed, 384 insertions(+), 16 deletions(-) create mode 100644 packages/server/src/database/migrations/20240804084709_create_paused_at_column_to_plaid_items_table.js create mode 100644 packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/PauseFeedsBankAccount.tsx create mode 100644 packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/ResumeFeedsBankAccount.tsx create mode 100644 packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts create mode 100644 packages/webapp/src/hooks/query/bank-accounts.ts diff --git a/packages/server/src/api/controllers/Banking/BankAccountsController.ts b/packages/server/src/api/controllers/Banking/BankAccountsController.ts index 2a2661216..942c6b890 100644 --- a/packages/server/src/api/controllers/Banking/BankAccountsController.ts +++ b/packages/server/src/api/controllers/Banking/BankAccountsController.ts @@ -3,6 +3,7 @@ import { NextFunction, Request, Response, Router } from 'express'; import BaseController from '@/api/controllers/BaseController'; 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 { @@ -26,10 +27,18 @@ export class BankAccountsController extends BaseController { 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) ); @@ -117,6 +126,13 @@ export class BankAccountsController extends BaseController { } } + /** + * + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ async resumeBankAccountFeeds( req: Request<{ bankAccountId: number }>, res: Response, @@ -129,13 +145,21 @@ export class BankAccountsController extends BaseController { await this.bankAccountsApp.resumeBankAccount(tenantId, bankAccountId); return res.status(200).send({ - message: '', + message: 'The bank account feeds syncing has been resumed.', + id: bankAccountId, }); } catch (error) { next(error); } } + /** + * + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ async pauseBankAccountFeeds( req: Request<{ bankAccountId: number }>, res: Response, @@ -148,7 +172,8 @@ export class BankAccountsController extends BaseController { await this.bankAccountsApp.pauseBankAccount(tenantId, bankAccountId); return res.status(200).send({ - message: '', + message: 'The bank account feeds syncing has been paused.', + id: bankAccountId, }); } catch (error) { next(error); diff --git a/packages/server/src/database/migrations/20240804084709_create_paused_at_column_to_plaid_items_table.js b/packages/server/src/database/migrations/20240804084709_create_paused_at_column_to_plaid_items_table.js new file mode 100644 index 000000000..2947eb30b --- /dev/null +++ b/packages/server/src/database/migrations/20240804084709_create_paused_at_column_to_plaid_items_table.js @@ -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'); + }); +}; diff --git a/packages/server/src/models/PlaidItem.ts b/packages/server/src/models/PlaidItem.ts index 6dc515394..aca189038 100644 --- a/packages/server/src/models/PlaidItem.ts +++ b/packages/server/src/models/PlaidItem.ts @@ -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; + } } diff --git a/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx b/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx index faef21ac5..b2fc48775 100644 --- a/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx +++ b/packages/server/src/services/Banking/BankAccounts/BankAccountsApplication.tsx @@ -1,7 +1,8 @@ import { Inject, Service } from 'typedi'; import { DisconnectBankAccount } from './DisconnectBankAccount'; import { RefreshBankAccountService } from './RefreshBankAccount'; -import { ResumeBankAccountFeeds } from './PauseBankAccountFeeds'; +import { PauseBankAccountFeeds } from './PauseBankAccountFeeds'; +import { ResumeBankAccountFeeds } from './ResumeBankAccountFeeds'; @Service() export class BankAccountsApplication { @@ -15,7 +16,7 @@ export class BankAccountsApplication { private resumeBankAccountFeedsService: ResumeBankAccountFeeds; @Inject() - private pauseBankAccountFeedsService: ResumeBankAccountFeeds; + private pauseBankAccountFeedsService: PauseBankAccountFeeds; /** * Disconnects the given bank account. @@ -45,12 +46,12 @@ export class BankAccountsApplication { /** * Pauses the feeds sync of the given bank account. - * @param {number} tenantId - * @param {number} bankAccountId + * @param {number} tenantId + * @param {number} bankAccountId * @returns {Promise} */ async pauseBankAccount(tenantId: number, bankAccountId: number) { - return this.pauseBankAccountFeedsService.resumeBankAccountFeeds( + return this.pauseBankAccountFeedsService.pauseBankAccountFeeds( tenantId, bankAccountId ); @@ -58,8 +59,8 @@ export class BankAccountsApplication { /** * Resumes the feeds sync of the given bank account. - * @param {number} tenantId - * @param {number} bankAccountId + * @param {number} tenantId + * @param {number} bankAccountId * @returns {Promise} */ async resumeBankAccount(tenantId: number, bankAccountId: number) { diff --git a/packages/server/src/services/Banking/BankAccounts/PauseBankAccountFeeds.tsx b/packages/server/src/services/Banking/BankAccounts/PauseBankAccountFeeds.tsx index 34cf2664b..3108bebf5 100644 --- a/packages/server/src/services/Banking/BankAccounts/PauseBankAccountFeeds.tsx +++ b/packages/server/src/services/Banking/BankAccounts/PauseBankAccountFeeds.tsx @@ -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() -export class ResumeBankAccountFeeds { - public resumeBankAccountFeeds(tenantId: number, bankAccountId: number) { +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} + */ + 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, + }); + }); } } diff --git a/packages/server/src/services/Banking/BankAccounts/ResumeBankAccountFeeds.tsx b/packages/server/src/services/Banking/BankAccounts/ResumeBankAccountFeeds.tsx index 7851e0e16..680568470 100644 --- a/packages/server/src/services/Banking/BankAccounts/ResumeBankAccountFeeds.tsx +++ b/packages/server/src/services/Banking/BankAccounts/ResumeBankAccountFeeds.tsx @@ -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() 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} */ - 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(), + }); + }); + } } diff --git a/packages/webapp/src/containers/AlertsContainer/registered.tsx b/packages/webapp/src/containers/AlertsContainer/registered.tsx index 40724c66c..f585c398f 100644 --- a/packages/webapp/src/containers/AlertsContainer/registered.tsx +++ b/packages/webapp/src/containers/AlertsContainer/registered.tsx @@ -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, ]; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx index d6ff1b075..f01427c43 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx @@ -46,6 +46,7 @@ import { useUnexcludeUncategorizedTransactions, } from '@/hooks/query/bank-rules'; import { withBanking } from '../withBanking'; +import withAlertActions from '@/containers/Alert/withAlertActions'; function AccountTransactionsActionsBar({ // #withDialogActions @@ -60,6 +61,9 @@ function AccountTransactionsActionsBar({ // #withBanking uncategorizedTransationsIdsSelected, excludedTransactionsIdsSelected, + + // #withAlerts + openAlert, }) { const history = useHistory(); 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 ( @@ -284,6 +298,15 @@ function AccountTransactionsActionsBar({ }} content={ + + + @@ -311,6 +334,7 @@ function AccountTransactionsActionsBar({ export default compose( withDialogActions, + withAlertActions, withSettingsActions, withSettings(({ cashflowTransactionsSettings }) => ({ cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize, diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/PauseFeedsBankAccount.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/PauseFeedsBankAccount.tsx new file mode 100644 index 000000000..5c4dbaf60 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/PauseFeedsBankAccount.tsx @@ -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 ( + } + confirmButtonText={} + intent={Intent.WARNING} + isOpen={isOpen} + onCancel={handleCancelActivateItem} + loading={isLoading} + onConfirm={handleConfirmItemActivate} + > +

Are you sure.

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(PauseFeedsBankAccountAlert); diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/ResumeFeedsBankAccount.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/ResumeFeedsBankAccount.tsx new file mode 100644 index 000000000..23a867349 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/ResumeFeedsBankAccount.tsx @@ -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 ( + } + confirmButtonText={} + intent={Intent.WARNING} + isOpen={isOpen} + onCancel={handleCancelActivateItem} + loading={isLoading} + onConfirm={handleConfirmItemActivate} + > +

Are you sure.

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(ResumeFeedsBankAccountAlert); diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts new file mode 100644 index 000000000..c8b6e0fcb --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/alerts/index.ts @@ -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, + }, +]; diff --git a/packages/webapp/src/hooks/query/bank-accounts.ts b/packages/webapp/src/hooks/query/bank-accounts.ts new file mode 100644 index 000000000..a55863dda --- /dev/null +++ b/packages/webapp/src/hooks/query/bank-accounts.ts @@ -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} options + * @returns {UseMutationResult} + */ +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} options + * @returns {UseMutationResult} + */ +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, + }, + ); +}