mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat: wip uncategorize bank transaction
This commit is contained in:
@@ -138,13 +138,15 @@ export interface ICashflowTransactionCategorizedPayload {
|
|||||||
}
|
}
|
||||||
export interface ICashflowTransactionUncategorizingPayload {
|
export interface ICashflowTransactionUncategorizingPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
export interface ICashflowTransactionUncategorizedPayload {
|
export interface ICashflowTransactionUncategorizedPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
uncategorizedTransactionId: number;
|
||||||
oldUncategorizedTransaction: IUncategorizedCashflowTransaction;
|
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||||
|
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ICashflowTransactionUncategorizedPayload,
|
ICashflowTransactionUncategorizedPayload,
|
||||||
ICashflowTransactionUncategorizingPayload,
|
ICashflowTransactionUncategorizingPayload,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
|
import { validateTransactionShouldBeCategorized } from './utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class UncategorizeCashflowTransaction {
|
export class UncategorizeCashflowTransaction {
|
||||||
@@ -24,11 +25,12 @@ export class UncategorizeCashflowTransaction {
|
|||||||
* Uncategorizes the given cashflow transaction.
|
* Uncategorizes the given cashflow transaction.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} cashflowTransactionId
|
* @param {number} cashflowTransactionId
|
||||||
|
* @returns {Promise<Array<number>>}
|
||||||
*/
|
*/
|
||||||
public async uncategorize(
|
public async uncategorize(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number
|
uncategorizedTransactionId: number
|
||||||
) {
|
): Promise<Array<number>> {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const oldUncategorizedTransaction =
|
const oldUncategorizedTransaction =
|
||||||
@@ -36,6 +38,22 @@ export class UncategorizeCashflowTransaction {
|
|||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.throwIfNotFound();
|
.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.
|
// Updates the transaction under UOW.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
// Triggers `onTransactionUncategorizing` event.
|
// Triggers `onTransactionUncategorizing` event.
|
||||||
@@ -43,30 +61,36 @@ export class UncategorizeCashflowTransaction {
|
|||||||
events.cashflow.onTransactionUncategorizing,
|
events.cashflow.onTransactionUncategorizing,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
oldUncategorizedTransactions,
|
||||||
trx,
|
trx,
|
||||||
} as ICashflowTransactionUncategorizingPayload
|
} as ICashflowTransactionUncategorizingPayload
|
||||||
);
|
);
|
||||||
// Removes the ref relation with the related transaction.
|
// Removes the ref relation with the related transaction.
|
||||||
const uncategorizedTransaction =
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
await UncategorizedCashflowTransaction.query(trx).updateAndFetchById(
|
.whereIn('id', oldUncategoirzedTransactionsIds)
|
||||||
uncategorizedTransactionId,
|
.patch({
|
||||||
{
|
categorized: false,
|
||||||
categorized: false,
|
categorizeRefId: null,
|
||||||
categorizeRefId: null,
|
categorizeRefType: null,
|
||||||
categorizeRefType: null,
|
});
|
||||||
}
|
const uncategorizedTransactions =
|
||||||
|
await UncategorizedCashflowTransaction.query().whereIn(
|
||||||
|
'id',
|
||||||
|
oldUncategoirzedTransactionsIds
|
||||||
);
|
);
|
||||||
// Triggers `onTransactionUncategorized` event.
|
// Triggers `onTransactionUncategorized` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.cashflow.onTransactionUncategorized,
|
events.cashflow.onTransactionUncategorized,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactionId,
|
||||||
oldUncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
|
oldUncategorizedTransactions,
|
||||||
trx,
|
trx,
|
||||||
} as ICashflowTransactionUncategorizedPayload
|
} 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_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 {
|
export enum CASHFLOW_DIRECTION {
|
||||||
|
|||||||
@@ -50,12 +50,15 @@ export class DecrementUncategorizedTransactionOnCategorize {
|
|||||||
*/
|
*/
|
||||||
public async incrementUnCategorizedTransactionsOnUncategorized({
|
public async incrementUnCategorizedTransactionsOnUncategorized({
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
}: ICashflowTransactionUncategorizedPayload) {
|
}: ICashflowTransactionUncategorizedPayload) {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const uncategorizedTransactionIds = uncategorizedTransactions?.map(
|
||||||
|
(t) => t.id
|
||||||
|
);
|
||||||
await Account.query()
|
await Account.query()
|
||||||
.findById(uncategorizedTransaction.accountId)
|
.whereIn('id', uncategorizedTransactionIds)
|
||||||
.increment('uncategorizedTransactions', 1);
|
.increment('uncategorizedTransactions', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { ICashflowTransactionUncategorizedPayload } from '@/interfaces';
|
import { ICashflowTransactionUncategorizedPayload } from '@/interfaces';
|
||||||
import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService';
|
import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class DeleteCashflowTransactionOnUncategorize {
|
export class DeleteCashflowTransactionOnUncategorize {
|
||||||
@@ -25,18 +27,27 @@ export class DeleteCashflowTransactionOnUncategorize {
|
|||||||
*/
|
*/
|
||||||
public async deleteCashflowTransactionOnUncategorize({
|
public async deleteCashflowTransactionOnUncategorize({
|
||||||
tenantId,
|
tenantId,
|
||||||
oldUncategorizedTransaction,
|
oldUncategorizedTransactions,
|
||||||
trx,
|
trx,
|
||||||
}: ICashflowTransactionUncategorizedPayload) {
|
}: ICashflowTransactionUncategorizedPayload) {
|
||||||
// Deletes the cashflow transaction.
|
const _oldUncategorizedTransactions = oldUncategorizedTransactions.filter(
|
||||||
if (
|
(transaction) => transaction.categorizeRefType === 'CashflowTransaction'
|
||||||
oldUncategorizedTransaction.categorizeRefType === 'CashflowTransaction'
|
);
|
||||||
) {
|
|
||||||
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
|
||||||
tenantId,
|
|
||||||
|
|
||||||
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();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
([id, values]) =>
|
(values) => apiRequest.post(`cashflow/transactions/categorize`, values),
|
||||||
apiRequest.post(`cashflow/transactions/${id}/categorize`, values),
|
|
||||||
{
|
{
|
||||||
onSuccess: (res, id) => {
|
onSuccess: (res, id) => {
|
||||||
// Invalidate queries.
|
// Invalidate queries.
|
||||||
@@ -279,7 +278,6 @@ export function useUncategorizeTransaction(props) {
|
|||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Invalidate bank account summary.
|
// Invalidate bank account summary.
|
||||||
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
|
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';
|
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
interface StorePlaidState {
|
interface StorePlaidState {
|
||||||
@@ -113,7 +113,10 @@ export const PlaidSlice = createSlice({
|
|||||||
state: StorePlaidState,
|
state: StorePlaidState,
|
||||||
action: PayloadAction<{ ids: Array<string | number> }>,
|
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,
|
...state.transactionsToCategorizeSelected,
|
||||||
action.payload.id,
|
action.payload.id,
|
||||||
]);
|
]);
|
||||||
|
state.openMatchingTransactionAside = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,6 +148,12 @@ export const PlaidSlice = createSlice({
|
|||||||
state.transactionsToCategorizeSelected.filter(
|
state.transactionsToCategorizeSelected.filter(
|
||||||
(t) => t !== action.payload.id,
|
(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) => {
|
resetTransactionsToCategorizeSelected: (state: StorePlaidState) => {
|
||||||
state.transactionsToCategorizeSelected = [];
|
state.transactionsToCategorizeSelected = [];
|
||||||
|
state.openMatchingTransactionAside = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user