diff --git a/packages/server/src/models/UncategorizedCashflowTransaction.ts b/packages/server/src/models/UncategorizedCashflowTransaction.ts index 2f3f0a50e..218c35982 100644 --- a/packages/server/src/models/UncategorizedCashflowTransaction.ts +++ b/packages/server/src/models/UncategorizedCashflowTransaction.ts @@ -20,6 +20,7 @@ export default class UncategorizedCashflowTransaction extends mixin( description!: string; plaidTransactionId!: string; recognizedTransactionId!: number; + excludedAt: Date; /** * Table name. @@ -45,6 +46,7 @@ export default class UncategorizedCashflowTransaction extends mixin( 'isDepositTransaction', 'isWithdrawalTransaction', 'isRecognized', + 'isExcluded' ]; } @@ -89,6 +91,14 @@ export default class UncategorizedCashflowTransaction extends mixin( return !!this.recognizedTransactionId; } + /** + * Detarmines whether the transaction is excluded. + * @returns {boolean} + */ + public get isExcluded(): boolean { + return !!this.excludedAt; + } + /** * Model modifiers. */ diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts index 16a25433c..7b8012ccd 100644 --- a/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts @@ -1,7 +1,11 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import UnitOfWork from '@/services/UnitOfWork'; -import { Inject, Service } from 'typedi'; -import { validateTransactionNotCategorized } from './utils'; +import { + validateTransactionNotCategorized, + validateTransactionNotExcluded, +} from './utils'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; import { @@ -37,9 +41,13 @@ export class ExcludeBankTransaction { .findById(uncategorizedTransactionId) .throwIfNotFound(); + // Validate the transaction shouldn't be excluded. + validateTransactionNotExcluded(oldUncategorizedTransaction); + + // Validate the transaction shouldn't be categorized. validateTransactionNotCategorized(oldUncategorizedTransaction); - return this.uow.withTransaction(tenantId, async (trx) => { + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, { tenantId, uncategorizedTransactionId, diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts index 65d65a7c1..91bdf5320 100644 --- a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactions.ts @@ -1,6 +1,6 @@ import { Inject, Service } from 'typedi'; import PromisePool from '@supercharge/promise-pool'; -import { castArray } from 'lodash'; +import { castArray, uniq } from 'lodash'; import { ExcludeBankTransaction } from './ExcludeBankTransaction'; @Service() @@ -18,7 +18,7 @@ export class ExcludeBankTransactions { tenantId: number, bankTransactionIds: Array | number ) { - const _bankTransactionIds = castArray(bankTransactionIds); + const _bankTransactionIds = uniq(castArray(bankTransactionIds)); await PromisePool.withConcurrency(1) .for(_bankTransactionIds) diff --git a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts index 46bd81862..f6bb1bfad 100644 --- a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts +++ b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts @@ -1,7 +1,11 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import UnitOfWork from '@/services/UnitOfWork'; -import { Inject, Service } from 'typedi'; -import { validateTransactionNotCategorized } from './utils'; +import { + validateTransactionNotCategorized, + validateTransactionShouldBeExcluded, +} from './utils'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; import { @@ -37,9 +41,13 @@ export class UnexcludeBankTransaction { .findById(uncategorizedTransactionId) .throwIfNotFound(); + // Validate the transaction should be excludded. + validateTransactionShouldBeExcluded(oldUncategorizedTransaction); + + // Validate the transaction shouldn't be categorized. validateTransactionNotCategorized(oldUncategorizedTransaction); - return this.uow.withTransaction(tenantId, async (trx) => { + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { await this.eventPublisher.emitAsync( events.bankTransactions.onUnexcluding, { diff --git a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts index 846ea1fd8..d3743aeeb 100644 --- a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts +++ b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransactions.ts @@ -1,7 +1,7 @@ import { Inject, Service } from 'typedi'; import PromisePool from '@supercharge/promise-pool'; import { UnexcludeBankTransaction } from './UnexcludeBankTransaction'; -import { castArray } from 'lodash'; +import { castArray, uniq } from 'lodash'; @Service() export class UnexcludeBankTransactions { @@ -17,7 +17,7 @@ export class UnexcludeBankTransactions { tenantId: number, bankTransactionIds: Array | number ) { - const _bankTransactionIds = castArray(bankTransactionIds); + const _bankTransactionIds = uniq(castArray(bankTransactionIds)); await PromisePool.withConcurrency(1) .for(_bankTransactionIds) diff --git a/packages/server/src/services/Banking/Exclude/utils.ts b/packages/server/src/services/Banking/Exclude/utils.ts index 6d4f02a9a..04b06e8ca 100644 --- a/packages/server/src/services/Banking/Exclude/utils.ts +++ b/packages/server/src/services/Banking/Exclude/utils.ts @@ -3,6 +3,8 @@ import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTran const ERRORS = { TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED', + TRANSACTION_ALREADY_EXCLUDED: 'TRANSACTION_ALREADY_EXCLUDED', + TRANSACTION_NOT_EXCLUDED: 'TRANSACTION_NOT_EXCLUDED', }; export const validateTransactionNotCategorized = ( @@ -12,3 +14,19 @@ export const validateTransactionNotCategorized = ( throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED); } }; + +export const validateTransactionNotExcluded = ( + transaction: UncategorizedCashflowTransaction +) => { + if (transaction.isExcluded) { + throw new ServiceError(ERRORS.TRANSACTION_ALREADY_EXCLUDED); + } +}; + +export const validateTransactionShouldBeExcluded = ( + transaction: UncategorizedCashflowTransaction +) => { + if (!transaction.isExcluded) { + throw new ServiceError(ERRORS.TRANSACTION_NOT_EXCLUDED); + } +}; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx index d3c575898..f2bd264bd 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/UncategorizedTransactions/AccountTransactionsUncategorizedTable.tsx @@ -71,6 +71,11 @@ function AccountTransactionsDataTable({ const handleCategorizeBtnClick = (transaction) => { setUncategorizedTransactionIdForMatching(transaction.id); }; + // handles table selected rows change. + const handleSelectedRowsChange = (selected) => { + const transactionIds = selected.map((r) => r.original.id); + setUncategorizedTransactionsSelected(transactionIds); + }; // Handle exclude transaction. const handleExcludeTransaction = (transaction) => { excludeTransaction(transaction.id) @@ -118,6 +123,7 @@ function AccountTransactionsDataTable({ onExclude: handleExcludeTransaction, onCategorize: handleCategorizeBtnClick, }} + onSelectedRowsChange={handleSelectedRowsChange} className={clsx('table-constrant', styles.table, { [styles.showCategorizeColumn]: enableMultipleCategorization, })} diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts index 39b0c9154..92103bd2f 100644 --- a/packages/webapp/src/hooks/query/bank-rules.ts +++ b/packages/webapp/src/hooks/query/bank-rules.ts @@ -319,6 +319,10 @@ export function useExcludeUncategorizedTransaction( { onSuccess: (res, id) => { onValidateExcludeUncategorizedTransaction(queryClient); + queryClient.invalidateQueries([ + QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, + id, + ]); }, ...options, }, @@ -360,6 +364,10 @@ export function useUnexcludeUncategorizedTransaction( { onSuccess: (res, id) => { onValidateExcludeUncategorizedTransaction(queryClient); + queryClient.invalidateQueries([ + QUERY_KEY.BANK_ACCOUNT_SUMMARY_META, + id, + ]); }, ...options, },