diff --git a/packages/server/src/api/controllers/Banking/RecognizedTransactionsController.ts b/packages/server/src/api/controllers/Banking/RecognizedTransactionsController.ts index 8d0655756..b87662f14 100644 --- a/packages/server/src/api/controllers/Banking/RecognizedTransactionsController.ts +++ b/packages/server/src/api/controllers/Banking/RecognizedTransactionsController.ts @@ -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} + */ + 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); + } + } } diff --git a/packages/server/src/services/Banking/Matching/_utils.ts b/packages/server/src/services/Banking/Matching/_utils.ts new file mode 100644 index 000000000..89a316a4b --- /dev/null +++ b/packages/server/src/services/Banking/Matching/_utils.ts @@ -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); +}; diff --git a/packages/server/src/services/Cashflow/CashflowApplication.ts b/packages/server/src/services/Cashflow/CashflowApplication.ts index 217542bfc..f534e706e 100644 --- a/packages/server/src/services/Cashflow/CashflowApplication.ts +++ b/packages/server/src/services/Cashflow/CashflowApplication.ts @@ -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 + ); + } } diff --git a/packages/server/src/services/Cashflow/GetRecognizedTransaction.ts b/packages/server/src/services/Cashflow/GetRecognizedTransaction.ts new file mode 100644 index 000000000..5ac725084 --- /dev/null +++ b/packages/server/src/services/Cashflow/GetRecognizedTransaction.ts @@ -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() + ); + } +} diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionBoot.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionBoot.tsx index 6719c208c..e528386b4 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionBoot.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionBoot.tsx @@ -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; diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx index 83ba74c4d..ea8dd7f24 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionForm.tsx @@ -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 ( diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/MoneyIn/CategorizeTransactionOtherIncome.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/MoneyIn/CategorizeTransactionOtherIncome.tsx index 17e701096..b45f00bb9 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/MoneyIn/CategorizeTransactionOtherIncome.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/MoneyIn/CategorizeTransactionOtherIncome.tsx @@ -58,7 +58,7 @@ export default function CategorizeTransactionOtherIncome() { - + diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/_utils.ts b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/_utils.ts index cf13dc829..418febc2f 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/_utils.ts +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/_utils.ts @@ -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) => { return transfromToSnakeCase(formValues); -}; \ No newline at end of file +}; + +/** + * 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, + }; +}; diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts index 008b28b56..096114b4b 100644 --- a/packages/webapp/src/hooks/query/bank-rules.ts +++ b/packages/webapp/src/hooks/query/bank-rules.ts @@ -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} options + * @returns {UseQueryResult} + */ +export function useGetRecognizedBankTransaction( + uncategorizedTransactionId: number, + options?: UseQueryOptions, +): UseQueryResult { + const apiRequest = useApiRequest(); + + return useQuery( + [QUERY_KEY.RECOGNIZED_BANK_TRANSACTION, uncategorizedTransactionId], + () => + apiRequest + .get(`/banking/recognized/transactions/${uncategorizedTransactionId}`) + .then((res) => transformToCamelCase(res.data?.data)), + options, + ); +} + /** * @returns */