mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
Merge pull request #614 from bigcapitalhq/delete-bank-account-with-uncategorized-transactions
fix: Delete bank account with uncategorized transactions
This commit is contained in:
@@ -115,6 +115,7 @@ import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/E
|
|||||||
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
||||||
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
|
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
|
||||||
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
|
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
|
||||||
|
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -277,6 +278,7 @@ export const susbcribers = () => {
|
|||||||
// Plaid
|
// Plaid
|
||||||
RecognizeSyncedBankTranasctions,
|
RecognizeSyncedBankTranasctions,
|
||||||
DisconnectPlaidItemOnAccountDeleted,
|
DisconnectPlaidItemOnAccountDeleted,
|
||||||
|
DeleteUncategorizedTransactionsOnAccountDeleting,
|
||||||
|
|
||||||
// Loops
|
// Loops
|
||||||
LoopsEventsSubscriber
|
LoopsEventsSubscriber
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
|
import { castArray, omit, pick } from 'lodash';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { ServiceError } from '@/exceptions';
|
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 model = this.modelClass();
|
||||||
const modelRelations = Object.keys(model.relationMappings).filter(
|
const modelRelations = Object.keys(model.relationMappings).filter(
|
||||||
(relation) =>
|
(relation) =>
|
||||||
@@ -25,9 +34,20 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
|
|||||||
) !== -1
|
) !== -1
|
||||||
);
|
);
|
||||||
const relations = model.secureDeleteRelations || modelRelations;
|
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) => {
|
this.runAfter((model, query) => {
|
||||||
const nonEmptyRelations = relations.filter(
|
const nonEmptyRelations = filteredRelations.filter(
|
||||||
(relation) => !isEmpty(model[relation])
|
(relation) => !isEmpty(model[relation])
|
||||||
);
|
);
|
||||||
if (nonEmptyRelations.length > 0) {
|
if (nonEmptyRelations.length > 0) {
|
||||||
@@ -36,7 +56,7 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
|
|||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
return this.onBuild((query) => {
|
return this.onBuild((query) => {
|
||||||
relations.forEach((relation) => {
|
filteredRelations.forEach((relation) => {
|
||||||
query.withGraphFetched(`${relation}(selectId)`).modifiers({
|
query.withGraphFetched(`${relation}(selectId)`).modifiers({
|
||||||
selectId(builder) {
|
selectId(builder) {
|
||||||
builder.select('id');
|
builder.select('id');
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
import * as R from 'ramda';
|
import { Model, mixin } from 'objection';
|
||||||
import { Model, ModelOptions, QueryContext, mixin } from 'objection';
|
|
||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
import ModelSettings from './ModelSetting';
|
import ModelSettings from './ModelSetting';
|
||||||
import Account from './Account';
|
import Account from './Account';
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export class DeleteAccount {
|
|||||||
.throwIfNotFound()
|
.throwIfNotFound()
|
||||||
.queryAndThrowIfHasRelations({
|
.queryAndThrowIfHasRelations({
|
||||||
type: ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS,
|
type: ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS,
|
||||||
|
excludeRelations: ['uncategorizedTransactions', 'plaidItem']
|
||||||
});
|
});
|
||||||
// Authorize before delete account.
|
// Authorize before delete account.
|
||||||
await this.authorize(tenantId, accountId, oldAccount);
|
await this.authorize(tenantId, accountId, oldAccount);
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ export class DisconnectPlaidItemOnAccountDeleted {
|
|||||||
.findOne('plaidItemId', oldAccount.plaidItemId)
|
.findOne('plaidItemId', oldAccount.plaidItemId)
|
||||||
.delete();
|
.delete();
|
||||||
|
|
||||||
|
// Remove Plaid item once the transaction resolve.
|
||||||
if (oldPlaidItem) {
|
if (oldPlaidItem) {
|
||||||
const plaidInstance = PlaidClientWrapper.getClient();
|
const plaidInstance = PlaidClientWrapper.getClient();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import { Knex } from 'knex';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { PlaidClientWrapper } from '@/lib/Plaid/Plaid';
|
import { PlaidClientWrapper } from '@/lib/Plaid/Plaid';
|
||||||
import { PlaidSyncDb } from './PlaidSyncDB';
|
import { PlaidSyncDb } from './PlaidSyncDB';
|
||||||
import { PlaidFetchedTransactionsUpdates } from '@/interfaces';
|
import { PlaidFetchedTransactionsUpdates } from '@/interfaces';
|
||||||
import UnitOfWork from '@/services/UnitOfWork';
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class PlaidUpdateTransactions {
|
export class PlaidUpdateTransactions {
|
||||||
@@ -19,8 +19,8 @@ export class PlaidUpdateTransactions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles sync the Plaid item to Bigcaptial under UOW.
|
* Handles sync the Plaid item to Bigcaptial under UOW.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param {number} plaidItemId
|
* @param {number} plaidItemId - Plaid item id.
|
||||||
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
|
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
|
||||||
*/
|
*/
|
||||||
public async updateTransactions(tenantId: number, plaidItemId: string) {
|
public async updateTransactions(tenantId: number, plaidItemId: string) {
|
||||||
|
|||||||
@@ -26,14 +26,20 @@ export class DeleteBankRuleSerivce {
|
|||||||
* @param {number} ruleId
|
* @param {number} ruleId
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async deleteBankRule(tenantId: number, ruleId: number): Promise<void> {
|
public async deleteBankRule(
|
||||||
|
tenantId: number,
|
||||||
|
ruleId: number,
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
): Promise<void> {
|
||||||
const { BankRule, BankRuleCondition } = this.tenancy.models(tenantId);
|
const { BankRule, BankRuleCondition } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const oldBankRule = await BankRule.query()
|
const oldBankRule = await BankRule.query()
|
||||||
.findById(ruleId)
|
.findById(ruleId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(
|
||||||
|
tenantId,
|
||||||
|
async (trx: Knex.Transaction) => {
|
||||||
// Triggers `onBankRuleDeleting` event.
|
// Triggers `onBankRuleDeleting` event.
|
||||||
await this.eventPublisher.emitAsync(events.bankRules.onDeleting, {
|
await this.eventPublisher.emitAsync(events.bankRules.onDeleting, {
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -42,7 +48,7 @@ export class DeleteBankRuleSerivce {
|
|||||||
trx,
|
trx,
|
||||||
} as IBankRuleEventDeletingPayload);
|
} as IBankRuleEventDeletingPayload);
|
||||||
|
|
||||||
await BankRuleCondition.query(trx).where('ruleId', ruleId).delete();
|
await BankRuleCondition.query(trx).where('ruleId', ruleId).delete()
|
||||||
await BankRule.query(trx).findById(ruleId).delete();
|
await BankRule.query(trx).findById(ruleId).delete();
|
||||||
|
|
||||||
// Triggers `onBankRuleDeleted` event.
|
// Triggers `onBankRuleDeleted` event.
|
||||||
@@ -51,6 +57,8 @@ export class DeleteBankRuleSerivce {
|
|||||||
ruleId,
|
ruleId,
|
||||||
trx,
|
trx,
|
||||||
} as IBankRuleEventDeletedPayload);
|
} as IBankRuleEventDeletedPayload);
|
||||||
});
|
},
|
||||||
|
trx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<number>} bankRuleId
|
||||||
|
*/
|
||||||
|
async deleteBankRules(
|
||||||
|
tenantId: number,
|
||||||
|
bankRuleId: number | Array<number>,
|
||||||
|
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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -208,13 +208,18 @@ function AccountTransactionsActionsBar({
|
|||||||
bankAccountId: accountId,
|
bankAccountId: accountId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles uncategorize the categorized transactions in bulk.
|
// Handles uncategorize the categorized transactions in bulk.
|
||||||
const handleUncategorizeCategorizedBulkBtnClick = () => {
|
const handleUncategorizeCategorizedBulkBtnClick = () => {
|
||||||
openAlert('uncategorize-transactions-bulk', {
|
openAlert('uncategorize-transactions-bulk', {
|
||||||
uncategorizeTransactionsIds: categorizedTransactionsSelected,
|
uncategorizeTransactionsIds: categorizedTransactionsSelected,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
// Handles the delete account button click.
|
||||||
|
const handleDeleteAccountClick = () => {
|
||||||
|
openAlert('account-delete', {
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
@@ -364,9 +369,19 @@ function AccountTransactionsActionsBar({
|
|||||||
|
|
||||||
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
|
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
|
||||||
|
|
||||||
|
<MenuDivider />
|
||||||
<If condition={isSyncingOwner && isFeedsActive}>
|
<If condition={isSyncingOwner && isFeedsActive}>
|
||||||
<MenuItem onClick={handleDisconnectClick} text={'Disconnect'} />
|
<MenuItem
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClick={handleDisconnectClick}
|
||||||
|
text={'Disconnect'}
|
||||||
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
<MenuItem
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClick={handleDeleteAccountClick}
|
||||||
|
text={'Delete'}
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { TABLES } from '@/constants/tables';
|
|||||||
import withSettings from '@/containers/Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import withAlertsActions from '@/containers/Alert/withAlertActions';
|
import withAlertsActions from '@/containers/Alert/withAlertActions';
|
||||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
|
import { withBankingActions } from '../withBankingActions';
|
||||||
|
|
||||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
import { useAccountTransactionsColumns, ActionsMenu } from './components';
|
import { useAccountTransactionsColumns, ActionsMenu } from './components';
|
||||||
@@ -26,7 +27,6 @@ import { useUncategorizeTransaction } from '@/hooks/query';
|
|||||||
import { handleCashFlowTransactionType } from './utils';
|
import { handleCashFlowTransactionType } from './utils';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { withBankingActions } from '../withBankingActions';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account transactions data table.
|
* Account transactions data table.
|
||||||
|
|||||||
Reference in New Issue
Block a user