mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20: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 { 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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<IAccount>}
|
||||
*/
|
||||
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<IAccount>}
|
||||
*/
|
||||
public getAccount = (tenantId: number, accountId: number) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
.delete();
|
||||
|
||||
// Remove Plaid item once the transaction resolve.
|
||||
if (oldPlaidItem) {
|
||||
const plaidInstance = PlaidClientWrapper.getClient();
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -26,31 +26,39 @@ export class DeleteBankRuleSerivce {
|
||||
* @param {number} ruleId
|
||||
* @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 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);
|
||||
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();
|
||||
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);
|
||||
});
|
||||
// Triggers `onBankRuleDeleted` event.
|
||||
await await this.eventPublisher.emitAsync(events.bankRules.onDeleted, {
|
||||
tenantId,
|
||||
ruleId,
|
||||
trx,
|
||||
} 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,
|
||||
});
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<DashboardActionsBar>
|
||||
@@ -364,9 +369,19 @@ function AccountTransactionsActionsBar({
|
||||
|
||||
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
|
||||
|
||||
<MenuDivider />
|
||||
<If condition={isSyncingOwner && isFeedsActive}>
|
||||
<MenuItem onClick={handleDisconnectClick} text={'Disconnect'} />
|
||||
<MenuItem
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleDisconnectClick}
|
||||
text={'Disconnect'}
|
||||
/>
|
||||
</If>
|
||||
<MenuItem
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleDeleteAccountClick}
|
||||
text={'Delete'}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user