diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index f303b4527..5a5b02d03 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -115,6 +115,7 @@ import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/E import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize'; import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted'; import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber'; +import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting'; export default () => { return new EventPublisher(); @@ -277,6 +278,7 @@ export const susbcribers = () => { // Plaid RecognizeSyncedBankTranasctions, DisconnectPlaidItemOnAccountDeleted, + DeleteUncategorizedTransactionsOnAccountDeleting, // Loops LoopsEventsSubscriber diff --git a/packages/server/src/models/Pagination.ts b/packages/server/src/models/Pagination.ts index 7d1b89921..5d308e90e 100644 --- a/packages/server/src/models/Pagination.ts +++ b/packages/server/src/models/Pagination.ts @@ -1,4 +1,5 @@ import { Model } from 'objection'; +import { castArray, omit, pick } from 'lodash'; import { isEmpty } from 'lodash'; import { ServiceError } from '@/exceptions'; @@ -16,7 +17,15 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder { }); } - queryAndThrowIfHasRelations = ({ type, message }) => { + queryAndThrowIfHasRelations = ({ + type, + message, + excludeRelations = [], + includedRelations = [], + }) => { + const _excludeRelations = castArray(excludeRelations); + const _includedRelations = castArray(includedRelations); + const model = this.modelClass(); const modelRelations = Object.keys(model.relationMappings).filter( (relation) => @@ -25,9 +34,20 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder { ) !== -1 ); const relations = model.secureDeleteRelations || modelRelations; + const filteredByIncluded = relations.filter((r) => + _includedRelations.includes(r) + ); + const filteredByExcluded = relations.filter( + (r) => !excludeRelations.includes(r) + ); + const filteredRelations = !isEmpty(_includedRelations) + ? filteredByIncluded + : !isEmpty(_excludeRelations) + ? filteredByExcluded + : relations; this.runAfter((model, query) => { - const nonEmptyRelations = relations.filter( + const nonEmptyRelations = filteredRelations.filter( (relation) => !isEmpty(model[relation]) ); if (nonEmptyRelations.length > 0) { @@ -36,7 +56,7 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder { return model; }); return this.onBuild((query) => { - relations.forEach((relation) => { + filteredRelations.forEach((relation) => { query.withGraphFetched(`${relation}(selectId)`).modifiers({ selectId(builder) { builder.select('id'); diff --git a/packages/server/src/models/UncategorizedCashflowTransaction.ts b/packages/server/src/models/UncategorizedCashflowTransaction.ts index fbb737ccb..2b86c9a4a 100644 --- a/packages/server/src/models/UncategorizedCashflowTransaction.ts +++ b/packages/server/src/models/UncategorizedCashflowTransaction.ts @@ -1,6 +1,5 @@ /* eslint-disable global-require */ -import * as R from 'ramda'; -import { Model, ModelOptions, QueryContext, mixin } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from 'models/TenantModel'; import ModelSettings from './ModelSetting'; import Account from './Account'; diff --git a/packages/server/src/services/Accounts/AccountsApplication.ts b/packages/server/src/services/Accounts/AccountsApplication.ts index b90eb37e9..8a06b410f 100644 --- a/packages/server/src/services/Accounts/AccountsApplication.ts +++ b/packages/server/src/services/Accounts/AccountsApplication.ts @@ -43,8 +43,8 @@ export class AccountsApplication { /** * Creates a new account. - * @param {number} tenantId - * @param {IAccountCreateDTO} accountDTO + * @param {number} tenantId + * @param {IAccountCreateDTO} accountDTO * @returns {Promise} */ public createAccount = ( @@ -108,8 +108,8 @@ export class AccountsApplication { /** * Retrieves the account details. - * @param {number} tenantId - * @param {number} accountId + * @param {number} tenantId + * @param {number} accountId * @returns {Promise} */ public getAccount = (tenantId: number, accountId: number) => { diff --git a/packages/server/src/services/Accounts/DeleteAccount.ts b/packages/server/src/services/Accounts/DeleteAccount.ts index d8d499c58..632c78f62 100644 --- a/packages/server/src/services/Accounts/DeleteAccount.ts +++ b/packages/server/src/services/Accounts/DeleteAccount.ts @@ -73,6 +73,7 @@ export class DeleteAccount { .throwIfNotFound() .queryAndThrowIfHasRelations({ type: ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS, + excludeRelations: ['uncategorizedTransactions', 'plaidItem'] }); // Authorize before delete account. await this.authorize(tenantId, accountId, oldAccount); diff --git a/packages/server/src/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting.ts b/packages/server/src/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting.ts new file mode 100644 index 000000000..f2d9202ee --- /dev/null +++ b/packages/server/src/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting.ts @@ -0,0 +1,78 @@ +import { Inject, Service } from 'typedi'; +import { initialize } from 'objection'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import events from '@/subscribers/events'; +import { IAccountEventDeletePayload } from '@/interfaces'; +import { DeleteBankRulesService } from '../../Rules/DeleteBankRules'; +import { RevertRecognizedTransactions } from '../../RegonizeTranasctions/RevertRecognizedTransactions'; + +@Service() +export class DeleteUncategorizedTransactionsOnAccountDeleting { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private deleteBankRules: DeleteBankRulesService; + + @Inject() + private revertRecognizedTransactins: RevertRecognizedTransactions; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.accounts.onDelete, + this.handleDeleteBankRulesOnAccountDeleting.bind(this) + ); + } + + /** + * Handles revert the recognized transactions and delete all the bank rules + * associated to the deleted bank account. + * @param {IAccountEventDeletePayload} + */ + private async handleDeleteBankRulesOnAccountDeleting({ + tenantId, + oldAccount, + trx, + }: IAccountEventDeletePayload) { + const knex = this.tenancy.knex(tenantId); + const { + BankRule, + UncategorizedCashflowTransaction, + MatchedBankTransaction, + RecognizedBankTransaction, + } = this.tenancy.models(tenantId); + + const foundAssociatedRules = await BankRule.query(trx).where( + 'applyIfAccountId', + oldAccount.id + ); + const foundAssociatedRulesIds = foundAssociatedRules.map((rule) => rule.id); + + await initialize(knex, [ + UncategorizedCashflowTransaction, + RecognizedBankTransaction, + MatchedBankTransaction, + ]); + // Revert the recognized transactions of the given bank rules. + await this.revertRecognizedTransactins.revertRecognizedTransactions( + tenantId, + foundAssociatedRulesIds, + null, + trx + ); + // Delete the associated uncategorized transactions. + await UncategorizedCashflowTransaction.query(trx) + .where('accountId', oldAccount.id) + .delete(); + + // Delete the given bank rules. + await this.deleteBankRules.deleteBankRules( + tenantId, + foundAssociatedRulesIds, + trx + ); + } +} diff --git a/packages/server/src/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted.ts b/packages/server/src/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted.ts index 89f02da57..4ecf71e81 100644 --- a/packages/server/src/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted.ts +++ b/packages/server/src/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted.ts @@ -51,6 +51,7 @@ export class DisconnectPlaidItemOnAccountDeleted { .findOne('plaidItemId', oldAccount.plaidItemId) .delete(); + // Remove Plaid item once the transaction resolve. if (oldPlaidItem) { const plaidInstance = PlaidClientWrapper.getClient(); diff --git a/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts b/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts index 547aac45d..c987dd84f 100644 --- a/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts +++ b/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts @@ -1,10 +1,10 @@ -import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Knex } from 'knex'; import { Inject, Service } from 'typedi'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; import { PlaidClientWrapper } from '@/lib/Plaid/Plaid'; import { PlaidSyncDb } from './PlaidSyncDB'; import { PlaidFetchedTransactionsUpdates } from '@/interfaces'; import UnitOfWork from '@/services/UnitOfWork'; -import { Knex } from 'knex'; @Service() export class PlaidUpdateTransactions { @@ -19,9 +19,9 @@ export class PlaidUpdateTransactions { /** * Handles sync the Plaid item to Bigcaptial under UOW. - * @param {number} tenantId - * @param {number} plaidItemId - * @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>} + * @param {number} tenantId - Tenant id. + * @param {number} plaidItemId - Plaid item id. + * @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>} */ public async updateTransactions(tenantId: number, plaidItemId: string) { return this.uow.withTransaction(tenantId, (trx: Knex.Transaction) => { diff --git a/packages/server/src/services/Banking/Rules/DeleteBankRule.ts b/packages/server/src/services/Banking/Rules/DeleteBankRule.ts index c02ab6686..3981059c9 100644 --- a/packages/server/src/services/Banking/Rules/DeleteBankRule.ts +++ b/packages/server/src/services/Banking/Rules/DeleteBankRule.ts @@ -26,31 +26,39 @@ export class DeleteBankRuleSerivce { * @param {number} ruleId * @returns {Promise} */ - public async deleteBankRule(tenantId: number, ruleId: number): Promise { + public async deleteBankRule( + tenantId: number, + ruleId: number, + trx?: Knex.Transaction + ): Promise { const { BankRule, BankRuleCondition } = this.tenancy.models(tenantId); const oldBankRule = await BankRule.query() .findById(ruleId) .throwIfNotFound(); - return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { - // Triggers `onBankRuleDeleting` event. - await this.eventPublisher.emitAsync(events.bankRules.onDeleting, { - tenantId, - oldBankRule, - ruleId, - trx, - } as IBankRuleEventDeletingPayload); - - await BankRuleCondition.query(trx).where('ruleId', ruleId).delete(); - await BankRule.query(trx).findById(ruleId).delete(); - - // Triggers `onBankRuleDeleted` event. - await await this.eventPublisher.emitAsync(events.bankRules.onDeleted, { - tenantId, - ruleId, - trx, - } as IBankRuleEventDeletedPayload); - }); + return this.uow.withTransaction( + tenantId, + async (trx: Knex.Transaction) => { + // Triggers `onBankRuleDeleting` event. + await this.eventPublisher.emitAsync(events.bankRules.onDeleting, { + tenantId, + oldBankRule, + ruleId, + trx, + } as IBankRuleEventDeletingPayload); + + await BankRuleCondition.query(trx).where('ruleId', ruleId).delete() + await BankRule.query(trx).findById(ruleId).delete(); + + // Triggers `onBankRuleDeleted` event. + await await this.eventPublisher.emitAsync(events.bankRules.onDeleted, { + tenantId, + ruleId, + trx, + } as IBankRuleEventDeletedPayload); + }, + trx + ); } } diff --git a/packages/server/src/services/Banking/Rules/DeleteBankRules.ts b/packages/server/src/services/Banking/Rules/DeleteBankRules.ts new file mode 100644 index 000000000..98d17e9cd --- /dev/null +++ b/packages/server/src/services/Banking/Rules/DeleteBankRules.ts @@ -0,0 +1,34 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import PromisePool from '@supercharge/promise-pool'; +import { castArray, uniq } from 'lodash'; +import { DeleteBankRuleSerivce } from './DeleteBankRule'; + +@Service() +export class DeleteBankRulesService { + @Inject() + private deleteBankRuleService: DeleteBankRuleSerivce; + + /** + * Delete bank rules. + * @param {number} tenantId + * @param {number | Array} bankRuleId + */ + async deleteBankRules( + tenantId: number, + bankRuleId: number | Array, + trx?: Knex.Transaction + ) { + const bankRulesIds = uniq(castArray(bankRuleId)); + + const results = await PromisePool.withConcurrency(1) + .for(bankRulesIds) + .process(async (bankRuleId: number) => { + await this.deleteBankRuleService.deleteBankRule( + tenantId, + bankRuleId, + trx + ); + }); + } +} diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx index 5048d2b54..be05ce22e 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsActionsBar.tsx @@ -208,13 +208,18 @@ function AccountTransactionsActionsBar({ bankAccountId: accountId, }); }; - // Handles uncategorize the categorized transactions in bulk. const handleUncategorizeCategorizedBulkBtnClick = () => { openAlert('uncategorize-transactions-bulk', { uncategorizeTransactionsIds: categorizedTransactionsSelected, }); }; + // Handles the delete account button click. + const handleDeleteAccountClick = () => { + openAlert('account-delete', { + accountId, + }); + }; return ( @@ -364,9 +369,19 @@ function AccountTransactionsActionsBar({ + - + + } > diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.tsx index 51749c778..3c090d69b 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsDataTable.tsx @@ -17,6 +17,7 @@ import { TABLES } from '@/constants/tables'; import withSettings from '@/containers/Settings/withSettings'; import withAlertsActions from '@/containers/Alert/withAlertActions'; import withDrawerActions from '@/containers/Drawer/withDrawerActions'; +import { withBankingActions } from '../withBankingActions'; import { useMemorizedColumnsWidths } from '@/hooks'; import { useAccountTransactionsColumns, ActionsMenu } from './components'; @@ -26,7 +27,6 @@ import { useUncategorizeTransaction } from '@/hooks/query'; import { handleCashFlowTransactionType } from './utils'; import { compose } from '@/utils'; -import { withBankingActions } from '../withBankingActions'; /** * Account transactions data table.