diff --git a/packages/server/src/api/controllers/Banking/BankTransactionsMatchingController.ts b/packages/server/src/api/controllers/Banking/BankTransactionsMatchingController.ts index 437152140..3b2199903 100644 --- a/packages/server/src/api/controllers/Banking/BankTransactionsMatchingController.ts +++ b/packages/server/src/api/controllers/Banking/BankTransactionsMatchingController.ts @@ -36,8 +36,6 @@ export class BankTransactionsMatchingController extends BaseController { this.validationResult, this.unmatchMatchedBankTransaction.bind(this) ); - router.get('/', this.getMatchedTransactions.bind(this)); - return router; } diff --git a/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts index 2625a1cb9..6b0be76e2 100644 --- a/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts +++ b/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts @@ -6,18 +6,27 @@ import { ServiceError } from '@/exceptions'; import CheckPolicies from '@/api/middleware/CheckPolicies'; import { AbilitySubject, CashflowAction } from '@/interfaces'; import { CashflowApplication } from '@/services/Cashflow/CashflowApplication'; +import { GetMatchedTransactionsFilter } from '@/services/Banking/Matching/types'; +import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication'; @Service() export default class GetCashflowAccounts extends BaseController { @Inject() private cashflowApplication: CashflowApplication; + @Inject() + private bankTransactionsMatchingApp: MatchBankTransactionsApplication; + /** * Controller router. */ public router() { const router = Router(); + router.get( + '/transactions/:transactionId/matches', + this.getMatchedTransactions.bind(this) + ); router.get( '/transactions/:transactionId', CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow), @@ -47,7 +56,6 @@ export default class GetCashflowAccounts extends BaseController { tenantId, transactionId ); - return res.status(200).send({ cashflow_transaction: this.transfromToResponse(cashflowTransaction), }); @@ -56,6 +64,34 @@ export default class GetCashflowAccounts extends BaseController { } }; + /** + * Retrieves the matched transactions. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + private async getMatchedTransactions( + req: Request<{ transactionId: number }>, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const { transactionId } = req.params; + const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter; + + try { + const data = + await this.bankTransactionsMatchingApp.getMatchedTransactions( + tenantId, + transactionId, + filter + ); + return res.status(200).send(data); + } catch (error) { + next(error); + } + } + /** * Catches the service errors. * @param {Error} error - Error. diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts index 0dc71e02a..1f73081bf 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts @@ -1,13 +1,18 @@ import { Inject, Service } from 'typedi'; import * as R from 'ramda'; +import moment from 'moment'; import { PromisePool } from '@supercharge/promise-pool'; import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types'; import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses'; import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills'; import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; @Service() export class GetMatchedTransactions { + @Inject() + private tenancy: HasTenancyService; + @Inject() private getMatchedInvoicesService: GetMatchedTransactionsByExpenses; @@ -36,11 +41,20 @@ export class GetMatchedTransactions { * Retrieves the matched transactions. * @param {number} tenantId - * @param {GetMatchedTransactionsFilter} filter - + * @returns {Promise} */ public async getMatchedTransactions( tenantId: number, + uncategorizedTransactionId: number, filter: GetMatchedTransactionsFilter ): Promise { + const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); + + const uncategorizedTransaction = + await UncategorizedCashflowTransaction.query() + .findById(uncategorizedTransactionId) + .throwIfNotFound(); + const filtered = filter.transactionType ? this.registered.filter((item) => item.type === filter.transactionType) : this.registered; @@ -50,6 +64,39 @@ export class GetMatchedTransactions { .process(async ({ type, service }) => { return service.getMatchedTransactions(tenantId, filter); }); - return R.compose(R.flatten)(matchedTransactions?.results); + + const { perfectMatches, possibleMatches } = this.groupMatchedResults( + uncategorizedTransaction, + matchedTransactions + ); + return { + perfectMatches, + possibleMatches, + }; + } + + /** + * Groups the given results for getting perfect and possible matches + * based on the given uncategorized transaction. + * @param uncategorizedTransaction + * @param matchedTransactions + * @returns {MatchedTransactionsPOJO} + */ + private groupMatchedResults( + uncategorizedTransaction, + matchedTransactions + ): MatchedTransactionsPOJO { + const results = R.compose(R.flatten)(matchedTransactions?.results); + + const perfectMatches = R.filter( + (match) => + match.amount === uncategorizedTransaction.amount && + moment(match.date).isSame(uncategorizedTransaction.date, 'day'), + results + ); + + const possibleMatches = R.difference(results, perfectMatches); + + return { perfectMatches, possibleMatches }; } } diff --git a/packages/server/src/services/Banking/Matching/MatchBankTransactionsApplication.ts b/packages/server/src/services/Banking/Matching/MatchBankTransactionsApplication.ts index b065d2bc3..44895067a 100644 --- a/packages/server/src/services/Banking/Matching/MatchBankTransactionsApplication.ts +++ b/packages/server/src/services/Banking/Matching/MatchBankTransactionsApplication.ts @@ -23,10 +23,12 @@ export class MatchBankTransactionsApplication { */ public getMatchedTransactions( tenantId: number, + uncategorizedTransactionId: number, filter: GetMatchedTransactionsFilter ) { return this.getMatchedTransactionsService.getMatchedTransactions( tenantId, + uncategorizedTransactionId, filter ); } diff --git a/packages/server/src/services/Banking/Matching/types.ts b/packages/server/src/services/Banking/Matching/types.ts index 74b64d0a7..b39ce3ed2 100644 --- a/packages/server/src/services/Banking/Matching/types.ts +++ b/packages/server/src/services/Banking/Matching/types.ts @@ -49,7 +49,10 @@ export interface MatchedTransactionPOJO { transactionId: number; } -export type MatchedTransactionsPOJO = Array; +export type MatchedTransactionsPOJO = { + perfectMatches: Array; + possibleMatches: Array; +}; export const ERRORS = { RESOURCE_TYPE_MATCHING_TRANSACTION_INVALID: @@ -59,5 +62,5 @@ export const ERRORS = { TOTAL_MATCHING_TRANSACTIONS_INVALID: 'TOTAL_MATCHING_TRANSACTIONS_INVALID', TRANSACTION_ALREADY_MATCHED: 'TRANSACTION_ALREADY_MATCHED', CANNOT_MATCH_EXCLUDED_TRANSACTION: 'CANNOT_MATCH_EXCLUDED_TRANSACTION', - CANNOT_DELETE_TRANSACTION_MATCHED: 'CANNOT_DELETE_TRANSACTION_MATCHED' + CANNOT_DELETE_TRANSACTION_MATCHED: 'CANNOT_DELETE_TRANSACTION_MATCHED', }; diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx index 35dfb7c52..288ee0289 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingTransaction.tsx @@ -23,7 +23,7 @@ const initialValues = { }; export function MatchingBankTransaction() { - const uncategorizedTransactionId = 1; + const uncategorizedTransactionId = 4; const { mutateAsync: matchTransaction } = useMatchTransaction(); // Handles the form submitting. @@ -37,7 +37,7 @@ export function MatchingBankTransaction() { }); return; } - matchTransaction([uncategorizedTransactionId, _values]) + matchTransaction({ id: uncategorizedTransactionId, values: _values }) .then(() => { AppToaster.show({ intent: Intent.SUCCESS, @@ -53,7 +53,9 @@ export function MatchingBankTransaction() { }; return ( - +
@@ -78,10 +80,10 @@ function MatchingBankTransactionContent() { * @returns {React.ReactNode} */ function PerfectMatchingTransactions() { - const { matchingTransactions } = useMatchingTransactionBoot(); + const { perfectMatches, perfectMatchesCount } = useMatchingTransactionBoot(); // Can't continue if the perfect matches is empty. - if (isEmpty(matchingTransactions)) { + if (isEmpty(perfectMatches)) { return null; } return ( @@ -90,13 +92,13 @@ function PerfectMatchingTransactions() {

Perfect Matchines

- 2 + {perfectMatchesCount}
- {matchingTransactions.map((match, index) => ( + {perfectMatches.map((match, index) => ( - {matchingTransactions.map((match, index) => ( + {possibleMatches.map((match, index) => ( ; + possibleMatches: Array; perfectMatchesCount: number; perfectMatches: Array; matches: Array; @@ -14,21 +14,24 @@ const RuleFormBootContext = createContext( ); interface RuleFormBootProps { + uncategorizedTransactionId: number; children: React.ReactNode; } -function MatchingTransactionBoot({ ...props }: RuleFormBootProps) { +function MatchingTransactionBoot({ + uncategorizedTransactionId, + ...props +}: RuleFormBootProps) { const { data: matchingTransactions, isLoading: isMatchingTransactionsLoading, - } = useMatchingTransactions(); + } = useMatchingTransactions(uncategorizedTransactionId); const provider = { isMatchingTransactionsLoading, - matchingTransactions, + possibleMatches: matchingTransactions?.possibleMatches, perfectMatchesCount: 2, - perfectMatches: [], - matches: [], + perfectMatches: matchingTransactions?.perfectMatches, } as MatchingTransactionBootValues; return ; diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts index e642e6a14..5bdc809a7 100644 --- a/packages/webapp/src/hooks/query/bank-rules.ts +++ b/packages/webapp/src/hooks/query/bank-rules.ts @@ -1,5 +1,7 @@ // @ts-nocheck import { + UseMutateFunction, + UseMutationResult, useInfiniteQuery, useMutation, useQuery, @@ -86,15 +88,18 @@ export function useBankRule(bankRuleId: number, props) { * * @returns */ -export function useMatchingTransactions(props?: any) { +export function useMatchingTransactions( + uncategorizedTransactionId: number, + props?: any, +) { const apiRequest = useApiRequest(); - return useQuery( - ['MATCHING_TRANSACTION'], + return useQuery( + ['MATCHING_TRANSACTION', uncategorizedTransactionId], () => apiRequest - .get(`/banking/matches`) - .then((res) => transformToCamelCase(res.data.data)), + .get(`/cashflow/transactions/${uncategorizedTransactionId}/matches`) + .then((res) => transformToCamelCase(res.data)), props, ); } @@ -135,11 +140,18 @@ export function useUnexcludeUncategorizedTransaction(props) { ); } -export function useMatchTransaction(props?: any) { +interface MatchUncategorizedTransactionValues { + id: number; + value: any; +} + +export function useMatchTransaction( + props?: any, +): UseMutationResult { const queryClient = useQueryClient(); const apiRequest = useApiRequest(); - return useMutation( + return useMutation( ([uncategorizedTransactionId, values]) => apiRequest.post(`/banking/matches/${uncategorizedTransactionId}`, values), {