mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat: auto fill categorize form from recognized transaction
This commit is contained in:
@@ -15,6 +15,10 @@ export class RecognizedTransactionsController extends BaseController {
|
||||
const router = Router();
|
||||
|
||||
router.get('/', this.getRecognizedTransactions.bind(this));
|
||||
router.get(
|
||||
'/transactions/:uncategorizedTransactionId',
|
||||
this.getRecognizedTransaction.bind(this)
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -44,4 +48,30 @@ export class RecognizedTransactionsController extends BaseController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the recognized transaction of the ginen uncategorized transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
async getRecognizedTransaction(
|
||||
req: Request<{ uncategorizedTransactionId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { uncategorizedTransactionId } = req.params;
|
||||
|
||||
try {
|
||||
const data = await this.cashflowApplication.getRecognizedTransaction(
|
||||
tenantId,
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
packages/server/src/services/Banking/Matching/_utils.ts
Normal file
22
packages/server/src/services/Banking/Matching/_utils.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
||||
import { MatchedTransactionPOJO } from './types';
|
||||
|
||||
export const sortClosestMatchTransactions = (
|
||||
uncategorizedTransaction: UncategorizedCashflowTransaction,
|
||||
matches: MatchedTransactionPOJO[]
|
||||
) => {
|
||||
return R.sortWith([
|
||||
// Sort by amount difference (closest to uncategorized transaction amount first)
|
||||
R.ascend((match: MatchedTransactionPOJO) =>
|
||||
Math.abs(match.amount - uncategorizedTransaction.amount)
|
||||
),
|
||||
// Sort by date difference (closest to uncategorized transaction date first)
|
||||
R.ascend((match: MatchedTransactionPOJO) =>
|
||||
Math.abs(
|
||||
moment(match.date).diff(moment(uncategorizedTransaction.date), 'days')
|
||||
)
|
||||
),
|
||||
])(matches);
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import NewCashflowTransactionService from './NewCashflowTransactionService';
|
||||
import GetCashflowAccountsService from './GetCashflowAccountsService';
|
||||
import { GetCashflowTransactionService } from './GetCashflowTransactionsService';
|
||||
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
|
||||
import { GetRecognizedTransactionService } from './GetRecognizedTransaction';
|
||||
|
||||
@Service()
|
||||
export class CashflowApplication {
|
||||
@@ -56,6 +57,9 @@ export class CashflowApplication {
|
||||
@Inject()
|
||||
private getRecognizedTranasctionsService: GetRecognizedTransactionsService;
|
||||
|
||||
@Inject()
|
||||
private getRecognizedTransactionService: GetRecognizedTransactionService;
|
||||
|
||||
/**
|
||||
* Creates a new cashflow transaction.
|
||||
* @param {number} tenantId
|
||||
@@ -234,4 +238,20 @@ export class CashflowApplication {
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the recognized transaction of the given uncategorized transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} uncategorizedTransactionId
|
||||
* @returns
|
||||
*/
|
||||
public getRecognizedTransaction(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number
|
||||
) {
|
||||
return this.getRecognizedTransactionService.getRecognizedTransaction(
|
||||
tenantId,
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
@Service()
|
||||
export class GetRecognizedTransactionService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves the recognized transaction of the given uncategorized transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} uncategorizedTransactionId
|
||||
*/
|
||||
public async getRecognizedTransaction(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number
|
||||
) {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const uncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query()
|
||||
.findById(uncategorizedTransactionId)
|
||||
.withGraphFetched('matchedBankTransactions')
|
||||
.withGraphFetched('recognizedTransaction.assignAccount')
|
||||
.withGraphFetched('recognizedTransaction.bankRule')
|
||||
.withGraphFetched('account')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
uncategorizedTransaction,
|
||||
new GetRecognizedTransactionTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { useAccounts, useBranches } from '@/hooks/query';
|
||||
import { useFeatureCan } from '@/hooks/state';
|
||||
import { Features } from '@/constants';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
import { useGetRecognizedBankTransaction } from '@/hooks/query/bank-rules';
|
||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
||||
|
||||
interface CategorizeTransactionBootProps {
|
||||
children: React.ReactNode;
|
||||
@@ -17,6 +19,8 @@ interface CategorizeTransactionBootValue {
|
||||
isBranchesLoading: boolean;
|
||||
isAccountsLoading: boolean;
|
||||
primaryBranch: any;
|
||||
recognizedTranasction: any;
|
||||
isRecognizedTransactionLoading: boolean;
|
||||
}
|
||||
|
||||
const CategorizeTransactionBootContext =
|
||||
@@ -30,6 +34,9 @@ const CategorizeTransactionBootContext =
|
||||
function CategorizeTransactionBoot({
|
||||
...props
|
||||
}: CategorizeTransactionBootProps) {
|
||||
const { uncategorizedTransaction, uncategorizedTransactionId } =
|
||||
useCategorizeTransactionTabsBoot();
|
||||
|
||||
// Detarmines whether the feature is enabled.
|
||||
const { featureCan } = useFeatureCan();
|
||||
const isBranchFeatureCan = featureCan(Features.Branches);
|
||||
@@ -42,6 +49,14 @@ function CategorizeTransactionBoot({
|
||||
{},
|
||||
{ enabled: isBranchFeatureCan },
|
||||
);
|
||||
// Fetches the recognized transaction.
|
||||
const {
|
||||
data: recognizedTranasction,
|
||||
isLoading: isRecognizedTransactionLoading,
|
||||
} = useGetRecognizedBankTransaction(uncategorizedTransactionId, {
|
||||
enabled: !!uncategorizedTransaction.is_recognized,
|
||||
});
|
||||
|
||||
// Retrieves the primary branch.
|
||||
const primaryBranch = useMemo(
|
||||
() => branches?.find((b) => b.primary) || first(branches),
|
||||
@@ -54,6 +69,8 @@ function CategorizeTransactionBoot({
|
||||
isBranchesLoading,
|
||||
isAccountsLoading,
|
||||
primaryBranch,
|
||||
recognizedTranasction,
|
||||
isRecognizedTransactionLoading,
|
||||
};
|
||||
const isLoading = isBranchesLoading || isAccountsLoading;
|
||||
|
||||
|
||||
@@ -6,16 +6,14 @@ import { CreateCategorizeTransactionSchema } from './CategorizeTransactionForm.s
|
||||
import { CategorizeTransactionFormContent } from './CategorizeTransactionFormContent';
|
||||
import { CategorizeTransactionFormFooter } from './CategorizeTransactionFormFooter';
|
||||
import { useCategorizeTransaction } from '@/hooks/query';
|
||||
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
||||
import {
|
||||
transformToCategorizeForm,
|
||||
defaultInitialValues,
|
||||
tranformToRequest,
|
||||
useCategorizeTransactionFormInitialValues,
|
||||
} from './_utils';
|
||||
import { compose } from '@/utils';
|
||||
import { withBankingActions } from '@/containers/CashFlow/withBankingActions';
|
||||
import { AppToaster } from '@/components';
|
||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Categorize cashflow transaction form dialog content.
|
||||
@@ -24,11 +22,12 @@ function CategorizeTransactionFormRoot({
|
||||
// #withBankingActions
|
||||
closeMatchingTransactionAside,
|
||||
}) {
|
||||
const { uncategorizedTransactionId, uncategorizedTransaction } =
|
||||
useCategorizeTransactionTabsBoot();
|
||||
const { primaryBranch } = useCategorizeTransactionBoot();
|
||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
||||
const { mutateAsync: categorizeTransaction } = useCategorizeTransaction();
|
||||
|
||||
// Form initial values in create and edit mode.
|
||||
const initialValues = useCategorizeTransactionFormInitialValues();
|
||||
|
||||
// Callbacks handles form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
const transformedValues = tranformToRequest(values);
|
||||
@@ -62,19 +61,6 @@ function CategorizeTransactionFormRoot({
|
||||
}
|
||||
});
|
||||
};
|
||||
// Form initial values in create and edit mode.
|
||||
const initialValues = {
|
||||
...defaultInitialValues,
|
||||
/**
|
||||
* We only care about the fields in the form. Previously unfilled optional
|
||||
* values such as `notes` come back from the API as null, so remove those
|
||||
* as well.
|
||||
*/
|
||||
...transformToCategorizeForm(uncategorizedTransaction),
|
||||
|
||||
/** Assign the primary branch id as default value. */
|
||||
branchId: primaryBranch?.id || null,
|
||||
};
|
||||
|
||||
return (
|
||||
<DivRoot>
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function CategorizeTransactionOtherIncome() {
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup name={'referenceNo'} label={'Reference No.'} fastField inline>
|
||||
<FInputGroup name={'reference_no'} fill />
|
||||
<FInputGroup name={'referenceNo'} fill />
|
||||
</FFormGroup>
|
||||
|
||||
<FFormGroup name={'description'} label={'Description'} fastField inline>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { transformToForm, transfromToSnakeCase } from '@/utils';
|
||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
||||
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
||||
|
||||
// Default initial form values.
|
||||
export const defaultInitialValues = {
|
||||
@@ -14,8 +16,11 @@ export const defaultInitialValues = {
|
||||
branchId: '',
|
||||
};
|
||||
|
||||
export const transformToCategorizeForm = (uncategorizedTransaction) => {
|
||||
const defaultValues = {
|
||||
export const transformToCategorizeForm = (
|
||||
uncategorizedTransaction: any,
|
||||
recognizedTransaction?: any,
|
||||
) => {
|
||||
let defaultValues = {
|
||||
debitAccountId: uncategorizedTransaction.account_id,
|
||||
transactionType: uncategorizedTransaction.is_deposit_transaction
|
||||
? 'other_income'
|
||||
@@ -23,10 +28,51 @@ export const transformToCategorizeForm = (uncategorizedTransaction) => {
|
||||
amount: uncategorizedTransaction.amount,
|
||||
date: uncategorizedTransaction.date,
|
||||
};
|
||||
if (recognizedTransaction) {
|
||||
const recognizedDefaults = getRecognizedTransactionDefaultValues(
|
||||
recognizedTransaction,
|
||||
);
|
||||
defaultValues = R.merge(defaultValues, recognizedDefaults);
|
||||
}
|
||||
return transformToForm(defaultValues, defaultInitialValues);
|
||||
};
|
||||
|
||||
export const getRecognizedTransactionDefaultValues = (
|
||||
recognizedTransaction: any,
|
||||
) => {
|
||||
return {
|
||||
creditAccountId: recognizedTransaction.assignedAccountId || '',
|
||||
// transactionType: recognizedTransaction.assignCategory,
|
||||
referenceNo: recognizedTransaction.referenceNo || '',
|
||||
};
|
||||
};
|
||||
|
||||
export const tranformToRequest = (formValues) => {
|
||||
export const tranformToRequest = (formValues: Record<string, any>) => {
|
||||
return transfromToSnakeCase(formValues);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Categorize transaction form initial values.
|
||||
* @returns
|
||||
*/
|
||||
export const useCategorizeTransactionFormInitialValues = () => {
|
||||
const { primaryBranch, recognizedTranasction } =
|
||||
useCategorizeTransactionBoot();
|
||||
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
||||
|
||||
return {
|
||||
...defaultInitialValues,
|
||||
/**
|
||||
* We only care about the fields in the form. Previously unfilled optional
|
||||
* values such as `notes` come back from the API as null, so remove those
|
||||
* as well.
|
||||
*/
|
||||
...transformToCategorizeForm(
|
||||
uncategorizedTransaction,
|
||||
recognizedTranasction,
|
||||
),
|
||||
|
||||
/** Assign the primary branch id as default value. */
|
||||
branchId: primaryBranch?.id || null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import t from './types';
|
||||
const QUERY_KEY = {
|
||||
BANK_RULES: 'BANK_RULE',
|
||||
BANK_TRANSACTION_MATCHES: 'BANK_TRANSACTION_MATCHES',
|
||||
RECOGNIZED_BANK_TRANSACTION: 'RECOGNIZED_BANK_TRANSACTION',
|
||||
EXCLUDED_BANK_TRANSACTIONS_INFINITY: 'EXCLUDED_BANK_TRANSACTIONS_INFINITY',
|
||||
RECOGNIZED_BANK_TRANSACTIONS_INFINITY:
|
||||
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
|
||||
@@ -318,6 +319,30 @@ export function useMatchUncategorizedTransaction(
|
||||
});
|
||||
}
|
||||
|
||||
interface GetRecognizedBankTransactionRes {}
|
||||
|
||||
/**
|
||||
* REtrieves the given recognized bank transaction.
|
||||
* @param {number} uncategorizedTransactionId
|
||||
* @param {UseQueryOptions<GetRecognizedBankTransactionRes, Error>} options
|
||||
* @returns {UseQueryResult<GetRecognizedBankTransactionRes, Error>}
|
||||
*/
|
||||
export function useGetRecognizedBankTransaction(
|
||||
uncategorizedTransactionId: number,
|
||||
options?: UseQueryOptions<GetRecognizedBankTransactionRes, Error>,
|
||||
): UseQueryResult<GetRecognizedBankTransactionRes, Error> {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useQuery<GetRecognizedBankTransactionRes, Error>(
|
||||
[QUERY_KEY.RECOGNIZED_BANK_TRANSACTION, uncategorizedTransactionId],
|
||||
() =>
|
||||
apiRequest
|
||||
.get(`/banking/recognized/transactions/${uncategorizedTransactionId}`)
|
||||
.then((res) => transformToCamelCase(res.data?.data)),
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user