mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
fix: group matches to get possible and perfect matches
This commit is contained in:
@@ -36,8 +36,6 @@ export class BankTransactionsMatchingController extends BaseController {
|
||||
this.validationResult,
|
||||
this.unmatchMatchedBankTransaction.bind(this)
|
||||
);
|
||||
router.get('/', this.getMatchedTransactions.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<MatchedTransactionsPOJO>}
|
||||
*/
|
||||
public async getMatchedTransactions(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
): Promise<MatchedTransactionsPOJO> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ export class MatchBankTransactionsApplication {
|
||||
*/
|
||||
public getMatchedTransactions(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
) {
|
||||
return this.getMatchedTransactionsService.getMatchedTransactions(
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,10 @@ export interface MatchedTransactionPOJO {
|
||||
transactionId: number;
|
||||
}
|
||||
|
||||
export type MatchedTransactionsPOJO = Array<MatchedTransactionPOJO[]>;
|
||||
export type MatchedTransactionsPOJO = {
|
||||
perfectMatches: Array<MatchedTransactionPOJO[]>;
|
||||
possibleMatches: Array<MatchedTransactionPOJO[]>;
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<MatchingTransactionBoot>
|
||||
<MatchingTransactionBoot
|
||||
uncategorizedTransactionId={uncategorizedTransactionId}
|
||||
>
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
<Form>
|
||||
<MatchingBankTransactionContent />
|
||||
@@ -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() {
|
||||
<Group spacing={6}>
|
||||
<h2 className={styles.matchBarTitle}>Perfect Matchines</h2>
|
||||
<Tag minimal round intent={Intent.SUCCESS}>
|
||||
2
|
||||
{perfectMatchesCount}
|
||||
</Tag>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Stack spacing={9} style={{ padding: '12px 15px' }}>
|
||||
{matchingTransactions.map((match, index) => (
|
||||
{perfectMatches.map((match, index) => (
|
||||
<MatchTransactionField
|
||||
key={index}
|
||||
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||
@@ -115,10 +117,10 @@ function PerfectMatchingTransactions() {
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function GoodMatchingTransactions() {
|
||||
const { matchingTransactions } = useMatchingTransactionBoot();
|
||||
const { possibleMatches } = useMatchingTransactionBoot();
|
||||
|
||||
// Can't continue if the possible matches is emoty.
|
||||
if (isEmpty(matchingTransactions)) {
|
||||
if (isEmpty(possibleMatches)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@@ -133,7 +135,7 @@ function GoodMatchingTransactions() {
|
||||
</Box>
|
||||
|
||||
<Stack spacing={9} style={{ padding: '12px 15px' }}>
|
||||
{matchingTransactions.map((match, index) => (
|
||||
{possibleMatches.map((match, index) => (
|
||||
<MatchTransaction
|
||||
key={index}
|
||||
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { createContext } from 'react';
|
||||
|
||||
interface MatchingTransactionBootValues {
|
||||
isMatchingTransactionsLoading: boolean;
|
||||
matchingTransactions: Array<any>;
|
||||
possibleMatches: Array<any>;
|
||||
perfectMatchesCount: number;
|
||||
perfectMatches: Array<any>;
|
||||
matches: Array<any>;
|
||||
@@ -14,21 +14,24 @@ const RuleFormBootContext = createContext<MatchingTransactionBootValues>(
|
||||
);
|
||||
|
||||
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 <RuleFormBootContext.Provider value={provider} {...props} />;
|
||||
|
||||
@@ -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<any>(
|
||||
['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<MatchUncategorizedTransactionValues> {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
return useMutation<MatchUncategorizedTransactionValues>(
|
||||
([uncategorizedTransactionId, values]) =>
|
||||
apiRequest.post(`/banking/matches/${uncategorizedTransactionId}`, values),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user