mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50: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.validationResult,
|
||||||
this.unmatchMatchedBankTransaction.bind(this)
|
this.unmatchMatchedBankTransaction.bind(this)
|
||||||
);
|
);
|
||||||
router.get('/', this.getMatchedTransactions.bind(this));
|
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,27 @@ import { ServiceError } from '@/exceptions';
|
|||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||||
|
import { GetMatchedTransactionsFilter } from '@/services/Banking/Matching/types';
|
||||||
|
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class GetCashflowAccounts extends BaseController {
|
export default class GetCashflowAccounts extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private cashflowApplication: CashflowApplication;
|
private cashflowApplication: CashflowApplication;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private bankTransactionsMatchingApp: MatchBankTransactionsApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller router.
|
* Controller router.
|
||||||
*/
|
*/
|
||||||
public router() {
|
public router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/transactions/:transactionId/matches',
|
||||||
|
this.getMatchedTransactions.bind(this)
|
||||||
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/transactions/:transactionId',
|
'/transactions/:transactionId',
|
||||||
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
|
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
|
||||||
@@ -47,7 +56,6 @@ export default class GetCashflowAccounts extends BaseController {
|
|||||||
tenantId,
|
tenantId,
|
||||||
transactionId
|
transactionId
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
cashflow_transaction: this.transfromToResponse(cashflowTransaction),
|
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.
|
* Catches the service errors.
|
||||||
* @param {Error} error - Error.
|
* @param {Error} error - Error.
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
|
import moment from 'moment';
|
||||||
import { PromisePool } from '@supercharge/promise-pool';
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types';
|
import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types';
|
||||||
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
|
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
|
||||||
import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
||||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetMatchedTransactions {
|
export class GetMatchedTransactions {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedInvoicesService: GetMatchedTransactionsByExpenses;
|
private getMatchedInvoicesService: GetMatchedTransactionsByExpenses;
|
||||||
|
|
||||||
@@ -36,11 +41,20 @@ export class GetMatchedTransactions {
|
|||||||
* Retrieves the matched transactions.
|
* Retrieves the matched transactions.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {GetMatchedTransactionsFilter} filter -
|
* @param {GetMatchedTransactionsFilter} filter -
|
||||||
|
* @returns {Promise<MatchedTransactionsPOJO>}
|
||||||
*/
|
*/
|
||||||
public async getMatchedTransactions(
|
public async getMatchedTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
|
uncategorizedTransactionId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
): Promise<MatchedTransactionsPOJO> {
|
): Promise<MatchedTransactionsPOJO> {
|
||||||
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const uncategorizedTransaction =
|
||||||
|
await UncategorizedCashflowTransaction.query()
|
||||||
|
.findById(uncategorizedTransactionId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
const filtered = filter.transactionType
|
const filtered = filter.transactionType
|
||||||
? this.registered.filter((item) => item.type === filter.transactionType)
|
? this.registered.filter((item) => item.type === filter.transactionType)
|
||||||
: this.registered;
|
: this.registered;
|
||||||
@@ -50,6 +64,39 @@ export class GetMatchedTransactions {
|
|||||||
.process(async ({ type, service }) => {
|
.process(async ({ type, service }) => {
|
||||||
return service.getMatchedTransactions(tenantId, filter);
|
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(
|
public getMatchedTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
|
uncategorizedTransactionId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
) {
|
) {
|
||||||
return this.getMatchedTransactionsService.getMatchedTransactions(
|
return this.getMatchedTransactionsService.getMatchedTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ export interface MatchedTransactionPOJO {
|
|||||||
transactionId: number;
|
transactionId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MatchedTransactionsPOJO = Array<MatchedTransactionPOJO[]>;
|
export type MatchedTransactionsPOJO = {
|
||||||
|
perfectMatches: Array<MatchedTransactionPOJO[]>;
|
||||||
|
possibleMatches: Array<MatchedTransactionPOJO[]>;
|
||||||
|
};
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
RESOURCE_TYPE_MATCHING_TRANSACTION_INVALID:
|
RESOURCE_TYPE_MATCHING_TRANSACTION_INVALID:
|
||||||
@@ -59,5 +62,5 @@ export const ERRORS = {
|
|||||||
TOTAL_MATCHING_TRANSACTIONS_INVALID: 'TOTAL_MATCHING_TRANSACTIONS_INVALID',
|
TOTAL_MATCHING_TRANSACTIONS_INVALID: 'TOTAL_MATCHING_TRANSACTIONS_INVALID',
|
||||||
TRANSACTION_ALREADY_MATCHED: 'TRANSACTION_ALREADY_MATCHED',
|
TRANSACTION_ALREADY_MATCHED: 'TRANSACTION_ALREADY_MATCHED',
|
||||||
CANNOT_MATCH_EXCLUDED_TRANSACTION: 'CANNOT_MATCH_EXCLUDED_TRANSACTION',
|
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() {
|
export function MatchingBankTransaction() {
|
||||||
const uncategorizedTransactionId = 1;
|
const uncategorizedTransactionId = 4;
|
||||||
const { mutateAsync: matchTransaction } = useMatchTransaction();
|
const { mutateAsync: matchTransaction } = useMatchTransaction();
|
||||||
|
|
||||||
// Handles the form submitting.
|
// Handles the form submitting.
|
||||||
@@ -37,7 +37,7 @@ export function MatchingBankTransaction() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
matchTransaction([uncategorizedTransactionId, _values])
|
matchTransaction({ id: uncategorizedTransactionId, values: _values })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
intent: Intent.SUCCESS,
|
intent: Intent.SUCCESS,
|
||||||
@@ -53,7 +53,9 @@ export function MatchingBankTransaction() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MatchingTransactionBoot>
|
<MatchingTransactionBoot
|
||||||
|
uncategorizedTransactionId={uncategorizedTransactionId}
|
||||||
|
>
|
||||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||||
<Form>
|
<Form>
|
||||||
<MatchingBankTransactionContent />
|
<MatchingBankTransactionContent />
|
||||||
@@ -78,10 +80,10 @@ function MatchingBankTransactionContent() {
|
|||||||
* @returns {React.ReactNode}
|
* @returns {React.ReactNode}
|
||||||
*/
|
*/
|
||||||
function PerfectMatchingTransactions() {
|
function PerfectMatchingTransactions() {
|
||||||
const { matchingTransactions } = useMatchingTransactionBoot();
|
const { perfectMatches, perfectMatchesCount } = useMatchingTransactionBoot();
|
||||||
|
|
||||||
// Can't continue if the perfect matches is empty.
|
// Can't continue if the perfect matches is empty.
|
||||||
if (isEmpty(matchingTransactions)) {
|
if (isEmpty(perfectMatches)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -90,13 +92,13 @@ function PerfectMatchingTransactions() {
|
|||||||
<Group spacing={6}>
|
<Group spacing={6}>
|
||||||
<h2 className={styles.matchBarTitle}>Perfect Matchines</h2>
|
<h2 className={styles.matchBarTitle}>Perfect Matchines</h2>
|
||||||
<Tag minimal round intent={Intent.SUCCESS}>
|
<Tag minimal round intent={Intent.SUCCESS}>
|
||||||
2
|
{perfectMatchesCount}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack spacing={9} style={{ padding: '12px 15px' }}>
|
<Stack spacing={9} style={{ padding: '12px 15px' }}>
|
||||||
{matchingTransactions.map((match, index) => (
|
{perfectMatches.map((match, index) => (
|
||||||
<MatchTransactionField
|
<MatchTransactionField
|
||||||
key={index}
|
key={index}
|
||||||
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||||
@@ -115,10 +117,10 @@ function PerfectMatchingTransactions() {
|
|||||||
* @returns {React.ReactNode}
|
* @returns {React.ReactNode}
|
||||||
*/
|
*/
|
||||||
function GoodMatchingTransactions() {
|
function GoodMatchingTransactions() {
|
||||||
const { matchingTransactions } = useMatchingTransactionBoot();
|
const { possibleMatches } = useMatchingTransactionBoot();
|
||||||
|
|
||||||
// Can't continue if the possible matches is emoty.
|
// Can't continue if the possible matches is emoty.
|
||||||
if (isEmpty(matchingTransactions)) {
|
if (isEmpty(possibleMatches)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -133,7 +135,7 @@ function GoodMatchingTransactions() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack spacing={9} style={{ padding: '12px 15px' }}>
|
<Stack spacing={9} style={{ padding: '12px 15px' }}>
|
||||||
{matchingTransactions.map((match, index) => (
|
{possibleMatches.map((match, index) => (
|
||||||
<MatchTransaction
|
<MatchTransaction
|
||||||
key={index}
|
key={index}
|
||||||
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { createContext } from 'react';
|
|||||||
|
|
||||||
interface MatchingTransactionBootValues {
|
interface MatchingTransactionBootValues {
|
||||||
isMatchingTransactionsLoading: boolean;
|
isMatchingTransactionsLoading: boolean;
|
||||||
matchingTransactions: Array<any>;
|
possibleMatches: Array<any>;
|
||||||
perfectMatchesCount: number;
|
perfectMatchesCount: number;
|
||||||
perfectMatches: Array<any>;
|
perfectMatches: Array<any>;
|
||||||
matches: Array<any>;
|
matches: Array<any>;
|
||||||
@@ -14,21 +14,24 @@ const RuleFormBootContext = createContext<MatchingTransactionBootValues>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface RuleFormBootProps {
|
interface RuleFormBootProps {
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MatchingTransactionBoot({ ...props }: RuleFormBootProps) {
|
function MatchingTransactionBoot({
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
...props
|
||||||
|
}: RuleFormBootProps) {
|
||||||
const {
|
const {
|
||||||
data: matchingTransactions,
|
data: matchingTransactions,
|
||||||
isLoading: isMatchingTransactionsLoading,
|
isLoading: isMatchingTransactionsLoading,
|
||||||
} = useMatchingTransactions();
|
} = useMatchingTransactions(uncategorizedTransactionId);
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
isMatchingTransactionsLoading,
|
isMatchingTransactionsLoading,
|
||||||
matchingTransactions,
|
possibleMatches: matchingTransactions?.possibleMatches,
|
||||||
perfectMatchesCount: 2,
|
perfectMatchesCount: 2,
|
||||||
perfectMatches: [],
|
perfectMatches: matchingTransactions?.perfectMatches,
|
||||||
matches: [],
|
|
||||||
} as MatchingTransactionBootValues;
|
} as MatchingTransactionBootValues;
|
||||||
|
|
||||||
return <RuleFormBootContext.Provider value={provider} {...props} />;
|
return <RuleFormBootContext.Provider value={provider} {...props} />;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import {
|
import {
|
||||||
|
UseMutateFunction,
|
||||||
|
UseMutationResult,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
@@ -86,15 +88,18 @@ export function useBankRule(bankRuleId: number, props) {
|
|||||||
*
|
*
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useMatchingTransactions(props?: any) {
|
export function useMatchingTransactions(
|
||||||
|
uncategorizedTransactionId: number,
|
||||||
|
props?: any,
|
||||||
|
) {
|
||||||
const apiRequest = useApiRequest();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useQuery(
|
return useQuery<any>(
|
||||||
['MATCHING_TRANSACTION'],
|
['MATCHING_TRANSACTION', uncategorizedTransactionId],
|
||||||
() =>
|
() =>
|
||||||
apiRequest
|
apiRequest
|
||||||
.get(`/banking/matches`)
|
.get(`/cashflow/transactions/${uncategorizedTransactionId}/matches`)
|
||||||
.then((res) => transformToCamelCase(res.data.data)),
|
.then((res) => transformToCamelCase(res.data)),
|
||||||
props,
|
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 queryClient = useQueryClient();
|
||||||
const apiRequest = useApiRequest();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation<MatchUncategorizedTransactionValues>(
|
||||||
([uncategorizedTransactionId, values]) =>
|
([uncategorizedTransactionId, values]) =>
|
||||||
apiRequest.post(`/banking/matches/${uncategorizedTransactionId}`, values),
|
apiRequest.post(`/banking/matches/${uncategorizedTransactionId}`, values),
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user