From fdf3e34f1c422d1e1b6152452876ee1a72f345d0 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 3 Aug 2024 23:30:23 +0200 Subject: [PATCH] feat: wip uncategorize bank transaction --- .../server/src/interfaces/CashflowService.ts | 6 ++- .../UncategorizeCashflowTransaction.ts | 48 ++++++++++++++----- .../server/src/services/Cashflow/constants.ts | 4 +- ...entUncategorizedTransactionOnCategorize.ts | 7 ++- ...DeleteCashflowTransactionOnUncategorize.ts | 29 +++++++---- .../server/src/services/Cashflow/utils.ts | 9 ++++ .../src/hooks/query/cashflowAccounts.tsx | 4 +- .../src/store/banking/banking.reducer.ts | 15 +++++- 8 files changed, 91 insertions(+), 31 deletions(-) diff --git a/packages/server/src/interfaces/CashflowService.ts b/packages/server/src/interfaces/CashflowService.ts index 66b258406..2174c2390 100644 --- a/packages/server/src/interfaces/CashflowService.ts +++ b/packages/server/src/interfaces/CashflowService.ts @@ -138,13 +138,15 @@ export interface ICashflowTransactionCategorizedPayload { } export interface ICashflowTransactionUncategorizingPayload { tenantId: number; + uncategorizedTransactionId: number; oldUncategorizedTransactions: Array; trx: Knex.Transaction; } export interface ICashflowTransactionUncategorizedPayload { tenantId: number; - uncategorizedTransaction: IUncategorizedCashflowTransaction; - oldUncategorizedTransaction: IUncategorizedCashflowTransaction; + uncategorizedTransactionId: number; + uncategorizedTransactions: Array; + oldUncategorizedTransactions: Array; trx: Knex.Transaction; } diff --git a/packages/server/src/services/Cashflow/UncategorizeCashflowTransaction.ts b/packages/server/src/services/Cashflow/UncategorizeCashflowTransaction.ts index ba6740685..9d9c9b359 100644 --- a/packages/server/src/services/Cashflow/UncategorizeCashflowTransaction.ts +++ b/packages/server/src/services/Cashflow/UncategorizeCashflowTransaction.ts @@ -8,6 +8,7 @@ import { ICashflowTransactionUncategorizedPayload, ICashflowTransactionUncategorizingPayload, } from '@/interfaces'; +import { validateTransactionShouldBeCategorized } from './utils'; @Service() export class UncategorizeCashflowTransaction { @@ -24,11 +25,12 @@ export class UncategorizeCashflowTransaction { * Uncategorizes the given cashflow transaction. * @param {number} tenantId * @param {number} cashflowTransactionId + * @returns {Promise>} */ public async uncategorize( tenantId: number, uncategorizedTransactionId: number - ) { + ): Promise> { const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); const oldUncategorizedTransaction = @@ -36,6 +38,22 @@ export class UncategorizeCashflowTransaction { .findById(uncategorizedTransactionId) .throwIfNotFound(); + validateTransactionShouldBeCategorized(oldUncategorizedTransaction); + + const associatedUncategorizedTransactions = + await UncategorizedCashflowTransaction.query() + .where('categorizeRefId', oldUncategorizedTransaction.categorizeRefId) + .where( + 'categorizeRefType', + oldUncategorizedTransaction.categorizeRefType + ); + const oldUncategorizedTransactions = [ + oldUncategorizedTransaction, + ...associatedUncategorizedTransactions, + ]; + const oldUncategoirzedTransactionsIds = oldUncategorizedTransactions.map( + (t) => t.id + ); // Updates the transaction under UOW. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onTransactionUncategorizing` event. @@ -43,30 +61,36 @@ export class UncategorizeCashflowTransaction { events.cashflow.onTransactionUncategorizing, { tenantId, + uncategorizedTransactionId, + oldUncategorizedTransactions, trx, } as ICashflowTransactionUncategorizingPayload ); // Removes the ref relation with the related transaction. - const uncategorizedTransaction = - await UncategorizedCashflowTransaction.query(trx).updateAndFetchById( - uncategorizedTransactionId, - { - categorized: false, - categorizeRefId: null, - categorizeRefType: null, - } + await UncategorizedCashflowTransaction.query(trx) + .whereIn('id', oldUncategoirzedTransactionsIds) + .patch({ + categorized: false, + categorizeRefId: null, + categorizeRefType: null, + }); + const uncategorizedTransactions = + await UncategorizedCashflowTransaction.query().whereIn( + 'id', + oldUncategoirzedTransactionsIds ); // Triggers `onTransactionUncategorized` event. await this.eventPublisher.emitAsync( events.cashflow.onTransactionUncategorized, { tenantId, - uncategorizedTransaction, - oldUncategorizedTransaction, + uncategorizedTransactionId, + uncategorizedTransactions, + oldUncategorizedTransactions, trx, } as ICashflowTransactionUncategorizedPayload ); - return uncategorizedTransaction; + return oldUncategoirzedTransactionsIds; }); } } diff --git a/packages/server/src/services/Cashflow/constants.ts b/packages/server/src/services/Cashflow/constants.ts index 28768c85f..f9b8e24e8 100644 --- a/packages/server/src/services/Cashflow/constants.ts +++ b/packages/server/src/services/Cashflow/constants.ts @@ -16,7 +16,9 @@ export const ERRORS = { CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED: 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED', - CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION: 'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION' + CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION: 'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION', + TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED' + }; export enum CASHFLOW_DIRECTION { diff --git a/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts b/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts index 5dd8047fd..cf610b582 100644 --- a/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts +++ b/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts @@ -50,12 +50,15 @@ export class DecrementUncategorizedTransactionOnCategorize { */ public async incrementUnCategorizedTransactionsOnUncategorized({ tenantId, - uncategorizedTransaction, + uncategorizedTransactions, }: ICashflowTransactionUncategorizedPayload) { const { Account } = this.tenancy.models(tenantId); + const uncategorizedTransactionIds = uncategorizedTransactions?.map( + (t) => t.id + ); await Account.query() - .findById(uncategorizedTransaction.accountId) + .whereIn('id', uncategorizedTransactionIds) .increment('uncategorizedTransactions', 1); } diff --git a/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts b/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts index 9460cc739..95a41e88a 100644 --- a/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts +++ b/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts @@ -1,8 +1,10 @@ import { Inject, Service } from 'typedi'; +import { PromisePool } from '@supercharge/promise-pool'; import events from '@/subscribers/events'; import { ICashflowTransactionUncategorizedPayload } from '@/interfaces'; import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService'; import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { ServiceError } from '@/exceptions'; @Service() export class DeleteCashflowTransactionOnUncategorize { @@ -25,18 +27,27 @@ export class DeleteCashflowTransactionOnUncategorize { */ public async deleteCashflowTransactionOnUncategorize({ tenantId, - oldUncategorizedTransaction, + oldUncategorizedTransactions, trx, }: ICashflowTransactionUncategorizedPayload) { - // Deletes the cashflow transaction. - if ( - oldUncategorizedTransaction.categorizeRefType === 'CashflowTransaction' - ) { - await this.deleteCashflowTransactionService.deleteCashflowTransaction( - tenantId, + const _oldUncategorizedTransactions = oldUncategorizedTransactions.filter( + (transaction) => transaction.categorizeRefType === 'CashflowTransaction' + ); - oldUncategorizedTransaction.categorizeRefId - ); + // Deletes the cashflow transaction. + if (_oldUncategorizedTransactions.length > 0) { + const result = await PromisePool.withConcurrency(1) + .for(_oldUncategorizedTransactions) + .process(async (oldUncategorizedTransaction) => { + await this.deleteCashflowTransactionService.deleteCashflowTransaction( + tenantId, + oldUncategorizedTransaction.categorizeRefId, + trx + ); + }); + if (result.errors) { + throw new ServiceError('SOMETHING_WRONG'); + } } } } diff --git a/packages/server/src/services/Cashflow/utils.ts b/packages/server/src/services/Cashflow/utils.ts index a47d0bca3..98918de9b 100644 --- a/packages/server/src/services/Cashflow/utils.ts +++ b/packages/server/src/services/Cashflow/utils.ts @@ -85,3 +85,12 @@ export const validateUncategorizedTransactionsNotExcluded = ( }); } }; + + +export const validateTransactionShouldBeCategorized = ( + uncategorizedTransaction: any +) => { + if (!uncategorizedTransaction.categorized) { + throw new ServiceError(ERRORS.TRANSACTION_NOT_CATEGORIZED); + } +}; diff --git a/packages/webapp/src/hooks/query/cashflowAccounts.tsx b/packages/webapp/src/hooks/query/cashflowAccounts.tsx index 0681210b7..089fc6a34 100644 --- a/packages/webapp/src/hooks/query/cashflowAccounts.tsx +++ b/packages/webapp/src/hooks/query/cashflowAccounts.tsx @@ -243,8 +243,7 @@ export function useCategorizeTransaction(props) { const apiRequest = useApiRequest(); return useMutation( - ([id, values]) => - apiRequest.post(`cashflow/transactions/${id}/categorize`, values), + (values) => apiRequest.post(`cashflow/transactions/categorize`, values), { onSuccess: (res, id) => { // Invalidate queries. @@ -279,7 +278,6 @@ export function useUncategorizeTransaction(props) { queryClient.invalidateQueries( t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, ); - // Invalidate bank account summary. queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META'); }, diff --git a/packages/webapp/src/store/banking/banking.reducer.ts b/packages/webapp/src/store/banking/banking.reducer.ts index 0d57b8acc..ff75372dd 100644 --- a/packages/webapp/src/store/banking/banking.reducer.ts +++ b/packages/webapp/src/store/banking/banking.reducer.ts @@ -1,4 +1,4 @@ -import { uniq } from 'lodash'; +import { castArray, uniq } from 'lodash'; import { PayloadAction, createSlice } from '@reduxjs/toolkit'; interface StorePlaidState { @@ -113,7 +113,10 @@ export const PlaidSlice = createSlice({ state: StorePlaidState, action: PayloadAction<{ ids: Array }>, ) => { - state.transactionsToCategorizeSelected = action.payload.ids; + const ids = castArray(action.payload.ids); + + state.transactionsToCategorizeSelected = ids; + state.openMatchingTransactionAside = true; }, /** @@ -129,6 +132,7 @@ export const PlaidSlice = createSlice({ ...state.transactionsToCategorizeSelected, action.payload.id, ]); + state.openMatchingTransactionAside = true; }, /** @@ -144,6 +148,12 @@ export const PlaidSlice = createSlice({ state.transactionsToCategorizeSelected.filter( (t) => t !== action.payload.id, ); + + if (state.transactionsToCategorizeSelected.length === 0) { + state.openMatchingTransactionAside = false; + } else { + state.openMatchingTransactionAside = true; + } }, /** @@ -152,6 +162,7 @@ export const PlaidSlice = createSlice({ */ resetTransactionsToCategorizeSelected: (state: StorePlaidState) => { state.transactionsToCategorizeSelected = []; + state.openMatchingTransactionAside = false; }, /**