mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
feat: getting matched transactiosn from multi uncategorized transactions
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { param } from 'express-validator';
|
import { param, query } from 'express-validator';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
@@ -24,7 +24,12 @@ export default class GetCashflowAccounts extends BaseController {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/transactions/:transactionId/matches',
|
'/transactions/matches',
|
||||||
|
[
|
||||||
|
query('uncategorizeTransactionsIds').exists().isArray({ min: 1 }),
|
||||||
|
query('uncategorizeTransactionsIds.*').exists().isNumeric().toInt(),
|
||||||
|
],
|
||||||
|
this.validationResult,
|
||||||
this.getMatchedTransactions.bind(this)
|
this.getMatchedTransactions.bind(this)
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
@@ -76,14 +81,15 @@ export default class GetCashflowAccounts extends BaseController {
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const { transactionId } = req.params;
|
const uncategorizeTransactionsIds: Array<number> =
|
||||||
|
req.query.uncategorizeTransactionsIds;
|
||||||
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
|
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data =
|
const data =
|
||||||
await this.bankTransactionsMatchingApp.getMatchedTransactions(
|
await this.bankTransactionsMatchingApp.getMatchedTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
transactionId,
|
uncategorizeTransactionsIds,
|
||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
return res.status(200).send(data);
|
return res.status(200).send(data);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 moment from 'moment';
|
||||||
|
import { first, sumBy } from 'lodash';
|
||||||
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';
|
||||||
@@ -47,19 +48,20 @@ export class GetMatchedTransactions {
|
|||||||
/**
|
/**
|
||||||
* Retrieves the matched transactions.
|
* Retrieves the matched transactions.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
|
* @param {Array<number>} uncategorizedTransactionIds - Uncategorized transactions ids.
|
||||||
* @param {GetMatchedTransactionsFilter} filter -
|
* @param {GetMatchedTransactionsFilter} filter -
|
||||||
* @returns {Promise<MatchedTransactionsPOJO>}
|
* @returns {Promise<MatchedTransactionsPOJO>}
|
||||||
*/
|
*/
|
||||||
public async getMatchedTransactions(
|
public async getMatchedTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionIds: Array<number>,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
): Promise<MatchedTransactionsPOJO> {
|
): Promise<MatchedTransactionsPOJO> {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const uncategorizedTransaction =
|
const uncategorizedTransactions =
|
||||||
await UncategorizedCashflowTransaction.query()
|
await UncategorizedCashflowTransaction.query()
|
||||||
.findById(uncategorizedTransactionId)
|
.whereIn('id', uncategorizedTransactionIds)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const filtered = filter.transactionType
|
const filtered = filter.transactionType
|
||||||
@@ -71,9 +73,8 @@ export class GetMatchedTransactions {
|
|||||||
.process(async ({ type, service }) => {
|
.process(async ({ type, service }) => {
|
||||||
return service.getMatchedTransactions(tenantId, filter);
|
return service.getMatchedTransactions(tenantId, filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { perfectMatches, possibleMatches } = this.groupMatchedResults(
|
const { perfectMatches, possibleMatches } = this.groupMatchedResults(
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
matchedTransactions
|
matchedTransactions
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -90,20 +91,20 @@ export class GetMatchedTransactions {
|
|||||||
* @returns {MatchedTransactionsPOJO}
|
* @returns {MatchedTransactionsPOJO}
|
||||||
*/
|
*/
|
||||||
private groupMatchedResults(
|
private groupMatchedResults(
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions: Array<any>,
|
||||||
matchedTransactions
|
matchedTransactions
|
||||||
): MatchedTransactionsPOJO {
|
): MatchedTransactionsPOJO {
|
||||||
const results = R.compose(R.flatten)(matchedTransactions?.results);
|
const results = R.compose(R.flatten)(matchedTransactions?.results);
|
||||||
|
|
||||||
|
const firstUncategorized = first(uncategorizedTransactions);
|
||||||
|
const amount = sumBy(uncategorizedTransactions, 'amount');
|
||||||
|
const date = firstUncategorized.date;
|
||||||
|
|
||||||
// Sort the results based on amount, date, and transaction type
|
// Sort the results based on amount, date, and transaction type
|
||||||
const closestResullts = sortClosestMatchTransactions(
|
const closestResullts = sortClosestMatchTransactions(amount, date, results);
|
||||||
uncategorizedTransaction,
|
|
||||||
results
|
|
||||||
);
|
|
||||||
const perfectMatches = R.filter(
|
const perfectMatches = R.filter(
|
||||||
(match) =>
|
(match) =>
|
||||||
match.amount === uncategorizedTransaction.amount &&
|
match.amount === amount && moment(match.date).isSame(date, 'day'),
|
||||||
moment(match.date).isSame(uncategorizedTransaction.date, 'day'),
|
|
||||||
closestResullts
|
closestResullts
|
||||||
);
|
);
|
||||||
const possibleMatches = R.difference(closestResullts, perfectMatches);
|
const possibleMatches = R.difference(closestResullts, perfectMatches);
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ export class MatchBankTransactionsApplication {
|
|||||||
*/
|
*/
|
||||||
public getMatchedTransactions(
|
public getMatchedTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionsIds: Array<number>,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
) {
|
) {
|
||||||
return this.getMatchedTransactionsService.getMatchedTransactions(
|
return this.getMatchedTransactionsService.getMatchedTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionsIds,
|
||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
|||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import {
|
import {
|
||||||
sumMatchTranasctions,
|
sumMatchTranasctions,
|
||||||
|
sumUncategorizedTransactions,
|
||||||
validateUncategorizedTransactionsExcluded,
|
validateUncategorizedTransactionsExcluded,
|
||||||
validateUncategorizedTransactionsNotMatched,
|
validateUncategorizedTransactionsNotMatched,
|
||||||
} from './_utils';
|
} from './_utils';
|
||||||
@@ -95,11 +96,14 @@ export class MatchBankTransactions {
|
|||||||
const totalMatchedTranasctions = sumMatchTranasctions(
|
const totalMatchedTranasctions = sumMatchTranasctions(
|
||||||
validatationResult.results
|
validatationResult.results
|
||||||
);
|
);
|
||||||
|
const totalUncategorizedTransactions = sumUncategorizedTransactions(
|
||||||
|
uncategorizedTransactions
|
||||||
|
);
|
||||||
// Validates the total given matching transcations whether is not equal
|
// Validates the total given matching transcations whether is not equal
|
||||||
// uncategorized transaction amount.
|
// uncategorized transaction amount.
|
||||||
// if (totalMatchedTranasctions !== uncategorizedTransaction.amount) {
|
if (totalUncategorizedTransactions === totalMatchedTranasctions) {
|
||||||
// throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,23 +2,22 @@ import moment from 'moment';
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
||||||
import { ERRORS, MatchedTransactionPOJO } from './types';
|
import { ERRORS, MatchedTransactionPOJO } from './types';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty, sumBy } from 'lodash';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
export const sortClosestMatchTransactions = (
|
export const sortClosestMatchTransactions = (
|
||||||
uncategorizedTransaction: UncategorizedCashflowTransaction,
|
amount: number,
|
||||||
|
date: Date,
|
||||||
matches: MatchedTransactionPOJO[]
|
matches: MatchedTransactionPOJO[]
|
||||||
) => {
|
) => {
|
||||||
return R.sortWith([
|
return R.sortWith([
|
||||||
// Sort by amount difference (closest to uncategorized transaction amount first)
|
// Sort by amount difference (closest to uncategorized transaction amount first)
|
||||||
R.ascend((match: MatchedTransactionPOJO) =>
|
R.ascend((match: MatchedTransactionPOJO) =>
|
||||||
Math.abs(match.amount - uncategorizedTransaction.amount)
|
Math.abs(match.amount - amount)
|
||||||
),
|
),
|
||||||
// Sort by date difference (closest to uncategorized transaction date first)
|
// Sort by date difference (closest to uncategorized transaction date first)
|
||||||
R.ascend((match: MatchedTransactionPOJO) =>
|
R.ascend((match: MatchedTransactionPOJO) =>
|
||||||
Math.abs(
|
Math.abs(moment(match.date).diff(moment(date), 'days'))
|
||||||
moment(match.date).diff(moment(uncategorizedTransaction.date), 'days')
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
])(matches);
|
])(matches);
|
||||||
};
|
};
|
||||||
@@ -32,15 +31,23 @@ export const sumMatchTranasctions = (transactions: Array<any>) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sumUncategorizedTransactions = (
|
||||||
|
uncategorizedTransactions: Array<any>
|
||||||
|
) => {
|
||||||
|
return sumBy(uncategorizedTransactions, 'amount');
|
||||||
|
};
|
||||||
|
|
||||||
export const validateUncategorizedTransactionsNotMatched = (
|
export const validateUncategorizedTransactionsNotMatched = (
|
||||||
uncategorizedTransactions: any
|
uncategorizedTransactions: any
|
||||||
) => {
|
) => {
|
||||||
const isMatchedTransactions = uncategorizedTransactions.filter(
|
const matchedTransactions = uncategorizedTransactions.filter(
|
||||||
(trans) => !isEmpty(trans.matchedBankTransactions)
|
(trans) => !isEmpty(trans.matchedBankTransactions)
|
||||||
);
|
);
|
||||||
//
|
//
|
||||||
if (isMatchedTransactions.length > 0) {
|
if (matchedTransactions.length > 0) {
|
||||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED);
|
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED, '', {
|
||||||
|
matchedTransactionsIds: matchedTransactions?.map((m) => m.id),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,6 +58,8 @@ export const validateUncategorizedTransactionsExcluded = (
|
|||||||
(trans) => trans.excluded
|
(trans) => trans.excluded
|
||||||
);
|
);
|
||||||
if (excludedTransactions.length > 0) {
|
if (excludedTransactions.length > 0) {
|
||||||
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION);
|
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION, '', {
|
||||||
|
excludedTransactionsIds: excludedTransactions.map((e) => e.id),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user