mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
feat: wip uncategorize bank transaction
This commit is contained in:
@@ -138,13 +138,15 @@ export interface ICashflowTransactionCategorizedPayload {
|
||||
}
|
||||
export interface ICashflowTransactionUncategorizingPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ICashflowTransactionUncategorizedPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||
oldUncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||
uncategorizedTransactionId: number;
|
||||
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Array<number>>}
|
||||
*/
|
||||
public async uncategorize(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number
|
||||
) {
|
||||
): Promise<Array<number>> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,3 +85,12 @@ export const validateUncategorizedTransactionsNotExcluded = (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const validateTransactionShouldBeCategorized = (
|
||||
uncategorizedTransaction: any
|
||||
) => {
|
||||
if (!uncategorizedTransaction.categorized) {
|
||||
throw new ServiceError(ERRORS.TRANSACTION_NOT_CATEGORIZED);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
@@ -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<string | number> }>,
|
||||
) => {
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user