feat: cashflow transaction matching

This commit is contained in:
Ahmed Bouhuolia
2024-07-04 22:44:20 +02:00
parent 202179ec0b
commit 87f60f7461
10 changed files with 333 additions and 45 deletions

View File

@@ -25,11 +25,18 @@ import {
import { useCreateCashflowTransaction } from '@/hooks/query';
import { useAccountTransactionsContext } from '../../AccountTransactions/AccountTransactionsProvider';
import { MatchingReconcileFormSchema } from './MatchingReconcileTransactionForm.schema';
import { initialValues } from './_utils';
import { initialValues, transformToReq } from './_utils';
interface MatchingReconcileTransactionFormProps {
onSubmitSuccess?: (values: any) => void;
}
function MatchingReconcileTransactionFormRoot({
closeReconcileMatchingTransaction,
}) {
// #props¿
onSubmitSuccess,
}: MatchingReconcileTransactionFormProps) {
// Mutation create cashflow transaction.
const { mutateAsync: createCashflowTransactionMutate } =
useCreateCashflowTransaction();
@@ -47,7 +54,7 @@ function MatchingReconcileTransactionFormRoot({
const _values = transformToReq(values, accountId);
createCashflowTransactionMutate(_values)
.then(() => {
.then((res) => {
setSubmitting(false);
AppToaster.show({
@@ -55,6 +62,8 @@ function MatchingReconcileTransactionFormRoot({
intent: Intent.SUCCESS,
});
closeReconcileMatchingTransaction();
onSubmitSuccess &&
onSubmitSuccess({ id: res.data.id, type: 'CashflowTransaction' });
})
.catch((error) => {
setSubmitting(false);
@@ -97,25 +106,6 @@ export const MatchingReconcileTransactionForm = R.compose(withBankingActions)(
MatchingReconcileTransactionFormRoot,
);
export function MatchingReconcileTransactionFooter() {
const { isSubmitting } = useFormikContext();
return (
<Box className={styles.footer}>
<Group>
<Button
fill
type={'submit'}
intent={Intent.PRIMARY}
loading={isSubmitting}
>
Submit
</Button>
</Group>
</Box>
);
}
function ReconcileMatchingType() {
const { setFieldValue, values } =
useFormikContext<MatchingReconcileFormValues>();
@@ -135,7 +125,7 @@ function ReconcileMatchingType() {
);
}
export function CreateReconcileTransactionContent() {
function CreateReconcileTransactionContent() {
const { accounts, branches } = useMatchingReconcileTransactionBoot();
return (
@@ -197,3 +187,22 @@ export function CreateReconcileTransactionContent() {
</Box>
);
}
function MatchingReconcileTransactionFooter() {
const { isSubmitting } = useFormikContext();
return (
<Box className={styles.footer}>
<Group>
<Button
fill
type={'submit'}
intent={Intent.PRIMARY}
loading={isSubmitting}
>
Submit
</Button>
</Group>
</Box>
);
}

View File

@@ -1,6 +1,7 @@
// @ts-nocheck
import { isEmpty } from 'lodash';
import * as R from 'ramda';
import { useEffect, useState } from 'react';
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
import { FastField, FastFieldProps, Formik, useFormikContext } from 'formik';
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
@@ -39,9 +40,6 @@ const initialValues = {
function MatchingBankTransactionRoot({
// #withBankingActions
closeMatchingTransactionAside,
// #withBanking
openReconcileMatchingTransaction,
}) {
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
@@ -84,25 +82,89 @@ function MatchingBankTransactionRoot({
uncategorizedTransactionId={uncategorizedTransactionId}
>
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
<>
<MatchingBankTransactionContent />
{openReconcileMatchingTransaction && (
<MatchingReconcileTransactionForm />
)}
{!openReconcileMatchingTransaction && <MatchTransactionFooter />}
</>
<MatchingBankTransactionFormContent />
</Formik>
</MatchingTransactionBoot>
);
}
export const MatchingBankTransaction = R.compose(
export const MatchingBankTransaction = R.compose(withBankingActions)(
MatchingBankTransactionRoot,
);
/**
* Matching bank transaction form content.
* @returns {React.ReactNode}
*/
const MatchingBankTransactionFormContent = R.compose(
withBankingActions,
withBanking(({ openReconcileMatchingTransaction }) => ({
openReconcileMatchingTransaction,
})),
)(MatchingBankTransactionRoot);
)(
({
// #withBankingActions
closeMatchingTransactionAside,
// #withBanking
openReconcileMatchingTransaction,
}) => {
const {
isMatchingTransactionsFetching,
isMatchingTransactionsSuccess,
matches,
} = useMatchingTransactionBoot();
const [pending, setPending] = useState<null | {
refId: number;
refType: string;
}>(null);
const { setFieldValue } = useFormikContext();
// This effect is responsible for automatically marking a transaction as matched
// when the matching process is successful and not currently fetching.
useEffect(() => {
if (
pending &&
isMatchingTransactionsSuccess &&
!isMatchingTransactionsFetching
) {
const foundMatch = matches?.find(
(m) =>
m.referenceType === pending?.refType &&
m.referenceId === pending?.refId,
);
if (foundMatch) {
setFieldValue(`matched.${pending.refType}-${pending.refId}`, true);
}
setPending(null);
}
}, [
isMatchingTransactionsFetching,
isMatchingTransactionsSuccess,
matches,
pending,
setFieldValue,
]);
const handleReconcileFormSubmitSuccess = (payload) => {
setPending({ refId: payload.id, refType: payload.type });
};
return (
<>
<MatchingBankTransactionContent />
{openReconcileMatchingTransaction && (
<MatchingReconcileTransactionForm
onSubmitSuccess={handleReconcileFormSubmitSuccess}
/>
)}
{!openReconcileMatchingTransaction && <MatchTransactionFooter />}
</>
);
},
);
function MatchingBankTransactionContent() {
return (
@@ -178,8 +240,8 @@ function PossibleMatchingTransactions() {
key={index}
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
date={match.dateFormatted}
transactionId={match.transactionId}
transactionType={match.transactionType}
transactionId={match.referenceId}
transactionType={match.referenceType}
/>
))}
</Stack>

View File

@@ -1,9 +1,12 @@
import { defaultTo } from 'lodash';
import React, { createContext } from 'react';
import { defaultTo } from 'lodash';
import * as R from 'ramda';
import { useGetBankTransactionsMatches } from '@/hooks/query/bank-rules';
interface MatchingTransactionBootValues {
isMatchingTransactionsLoading: boolean;
isMatchingTransactionsFetching: boolean;
isMatchingTransactionsSuccess: boolean;
possibleMatches: Array<any>;
perfectMatchesCount: number;
perfectMatches: Array<any>;
@@ -26,13 +29,24 @@ function MatchingTransactionBoot({
const {
data: matchingTransactions,
isLoading: isMatchingTransactionsLoading,
isFetching: isMatchingTransactionsFetching,
isSuccess: isMatchingTransactionsSuccess,
} = useGetBankTransactionsMatches(uncategorizedTransactionId);
const possibleMatches = defaultTo(matchingTransactions?.possibleMatches, []);
const perfectMatchesCount = matchingTransactions?.perfectMatches?.length || 0;
const perfectMatches = defaultTo(matchingTransactions?.perfectMatches, []);
const matches = R.concat(perfectMatches, possibleMatches);
const provider = {
isMatchingTransactionsLoading,
possibleMatches: defaultTo(matchingTransactions?.possibleMatches, []),
perfectMatchesCount: matchingTransactions?.perfectMatches?.length || 0,
perfectMatches: defaultTo(matchingTransactions?.perfectMatches, []),
isMatchingTransactionsFetching,
isMatchingTransactionsSuccess,
possibleMatches,
perfectMatchesCount,
perfectMatches,
matches,
} as MatchingTransactionBootValues;
return <RuleFormBootContext.Provider value={provider} {...props} />;

View File

@@ -24,7 +24,7 @@ export const useGetPendingAmountMatched = () => {
return useMemo(() => {
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
(match) => {
const key = `${match.transactionType}-${match.transactionId}`;
const key = `${match.referenceType}-${match.referenceId}`;
return values.matched[key];
},
);