fix: filter the uncategorized transactions out of matched transactions

This commit is contained in:
Ahmed Bouhuolia
2024-07-03 17:23:12 +02:00
parent 91730d204e
commit 67b519db61
19 changed files with 98 additions and 91 deletions

View File

@@ -5,7 +5,7 @@ import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/Ma
import { body, param } from 'express-validator';
import {
GetMatchedTransactionsFilter,
IMatchTransactionDTO,
IMatchTransactionsDTO,
} from '@/services/Banking/Matching/types';
@Service()
@@ -44,10 +44,10 @@ export class BankTransactionsMatchingController extends BaseController {
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
* @returns {Promise<Response|null>}
*/
private async matchBankTransaction(
req: Request,
req: Request<{ transactionId: number }>,
res: Response,
next: NextFunction
) {
@@ -55,7 +55,7 @@ export class BankTransactionsMatchingController extends BaseController {
const { transactionId } = req.params;
const matchTransactionDTO = this.matchedBodyData(
req
) as IMatchTransactionDTO;
) as IMatchTransactionsDTO;
try {
await this.bankTransactionsMatchingApp.matchTransaction(
@@ -64,6 +64,7 @@ export class BankTransactionsMatchingController extends BaseController {
matchTransactionDTO
);
return res.status(200).send({
id: transactionId,
message: 'The bank transaction has been matched.',
});
} catch (error) {
@@ -72,60 +73,31 @@ export class BankTransactionsMatchingController extends BaseController {
}
/**
*
* Unmatches the matched bank transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
* @returns {Promise<Response|null>}
*/
private async unmatchMatchedBankTransaction(
req: Request,
req: Request<{ transactionId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const transactionId = req.params?.transactionId;
const { transactionId } = req.params;
try {
await this.bankTransactionsMatchingApp.unmatchMatchedTransaction(
tenantId,
transactionId
);
return res.status(200).send({
id: transactionId,
message: 'The bank matched transaction has been unmatched.',
});
} catch (error) {
next(error);
}
}
/**
* Retrieves the matched transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async getMatchedTransactions(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
console.log('test');
try {
const matchedTransactions =
await this.bankTransactionsMatchingApp.getMatchedTransactions(
tenantId,
filter
);
return res.status(200).send({ data: matchedTransactions });
} catch (error) {
next(error);
}
}
}

View File

@@ -3,7 +3,7 @@ exports.up = function (knex) {
table.increments('id');
table.integer('uncategorized_transaction_id').unsigned();
table.string('reference_type');
table.integer('reference_id');
table.integer('reference_id').unsigned();
table.decimal('amount');
table.timestamps();
});

View File

@@ -54,14 +54,17 @@ export class BankRule extends TenantModel {
},
},
/**
* Bank rule may associated to the assign account.
*/
assignAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'bank_rules.assignAccountId',
to: 'accounts.id'
}
}
to: 'accounts.id',
},
},
};
}
}

View File

@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
import { GetMatchedTransactions } from './GetMatchedTransactions';
import { MatchBankTransactions } from './MatchTransactions';
import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction';
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
import { GetMatchedTransactionsFilter, IMatchTransactionsDTO } from './types';
@Service()
export class MatchBankTransactionsApplication {
@@ -43,7 +43,7 @@ export class MatchBankTransactionsApplication {
public matchTransaction(
tenantId: number,
uncategorizedTransactionId: number,
matchTransactionsDTO: IMatchTransactionDTO
matchTransactionsDTO: IMatchTransactionsDTO
): Promise<void> {
return this.matchTransactionService.matchTransaction(
tenantId,

View File

@@ -105,12 +105,13 @@ export class MatchBankTransactions {
* Matches the given uncategorized transaction to the given references.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @returns {Promise<void>}
*/
public async matchTransaction(
tenantId: number,
uncategorizedTransactionId: number,
matchTransactionsDTO: IMatchTransactionsDTO
) {
): Promise<void> {
const { matchedTransactions } = matchTransactionsDTO;
// Validates the given matching transactions DTO.

View File

@@ -9,10 +9,11 @@ export class ValidateTransactionMatched {
private tenancy: HasTenancyService;
/**
*
* Validate the given transaction whether is matched with bank transactions.
* @param {number} tenantId
* @param {string} referenceType
* @param {number} referenceId
* @param {string} referenceType - Transaction reference type.
* @param {number} referenceId - Transaction reference id.
* @returns {Promise<void>}
*/
public async validateTransactionNoMatchLinking(
tenantId: number,

View File

@@ -14,15 +14,15 @@ export class ValidateMatchingOnCashflowDelete {
public attach(bus) {
bus.subscribe(
events.cashflow.onTransactionDeleting,
this.validateMatchingOnCashflowDelete.bind(this)
this.validateMatchingOnCashflowDeleting.bind(this)
);
}
/**
*
* Validates the cashflow transaction whether matched with bank transaction on deleting.
* @param {IManualJournalDeletingPayload}
*/
public async validateMatchingOnCashflowDelete({
public async validateMatchingOnCashflowDeleting({
tenantId,
oldManualJournal,
trx,

View File

@@ -14,15 +14,15 @@ export class ValidateMatchingOnExpenseDelete {
public attach(bus) {
bus.subscribe(
events.expenses.onDeleting,
this.validateMatchingOnExpenseDelete.bind(this)
this.validateMatchingOnExpenseDeleting.bind(this)
);
}
/**
*
* Validates the expense transaction whether matched with bank transaction on deleting.
* @param {IExpenseEventDeletePayload}
*/
public async validateMatchingOnExpenseDelete({
public async validateMatchingOnExpenseDeleting({
tenantId,
oldExpense,
trx,

View File

@@ -14,15 +14,15 @@ export class ValidateMatchingOnManualJournalDelete {
public attach(bus) {
bus.subscribe(
events.manualJournals.onDeleting,
this.validateMatchingOnManualJournalDelete.bind(this)
this.validateMatchingOnManualJournalDeleting.bind(this)
);
}
/**
*
* Validates the manual journal transaction whether matched with bank transaction on deleting.
* @param {IManualJournalDeletingPayload}
*/
public async validateMatchingOnManualJournalDelete({
public async validateMatchingOnManualJournalDeleting({
tenantId,
oldManualJournal,
trx,

View File

@@ -17,15 +17,15 @@ export class ValidateMatchingOnPaymentMadeDelete {
public attach(bus) {
bus.subscribe(
events.billPayment.onDeleting,
this.validateMatchingOnPaymentMadeDelete.bind(this)
this.validateMatchingOnPaymentMadeDeleting.bind(this)
);
}
/**
*
* Validates the payment made transaction whether matched with bank transaction on deleting.
* @param {IPaymentReceiveDeletedPayload}
*/
public async validateMatchingOnPaymentMadeDelete({
public async validateMatchingOnPaymentMadeDeleting({
tenantId,
oldBillPayment,
trx,

View File

@@ -14,15 +14,15 @@ export class ValidateMatchingOnPaymentReceivedDelete {
public attach(bus) {
bus.subscribe(
events.paymentReceive.onDeleting,
this.validateMatchingOnPaymentReceivedDelete.bind(this)
this.validateMatchingOnPaymentReceivedDeleting.bind(this)
);
}
/**
*
* Validates the payment received transaction whether matched with bank transaction on deleting.
* @param {IPaymentReceiveDeletedPayload}
*/
public async validateMatchingOnPaymentReceivedDelete({
public async validateMatchingOnPaymentReceivedDeleting({
tenantId,
oldPaymentReceive,
trx,

View File

@@ -3,6 +3,7 @@ import { Knex } from 'knex';
export enum BankRuleConditionField {
Amount = 'Amount',
Description = 'Description',
Payee = 'Payee'
}
export enum BankRuleConditionComparator {

View File

@@ -1,4 +1,5 @@
import { Inject, Service } from 'typedi';
import { initialize } from 'objection';
import HasTenancyService from '../Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { UncategorizedTransactionTransformer } from './UncategorizedTransactionTransformer';
@@ -22,7 +23,13 @@ export class GetUncategorizedTransactions {
accountId: number,
query: IGetUncategorizedTransactionsQuery
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const {
UncategorizedCashflowTransaction,
RecognizedBankTransaction,
MatchedBankTransaction,
Account,
} = this.tenancy.models(tenantId);
const knex = this.tenancy.knex(tenantId);
// Parsed query with default values.
const _query = {
@@ -30,6 +37,15 @@ export class GetUncategorizedTransactions {
pageSize: 20,
...query,
};
// Initialize the ORM models metadata.
await initialize(knex, [
UncategorizedCashflowTransaction,
MatchedBankTransaction,
RecognizedBankTransaction,
Account,
]);
const { results, pagination } =
await UncategorizedCashflowTransaction.query()
.onBuild((q) => {
@@ -40,6 +56,9 @@ export class GetUncategorizedTransactions {
q.withGraphFetched('account');
q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphJoined('matchedBankTransactions');
q.whereNull('matchedBankTransactions.id');
q.orderBy('date', 'DESC');
})
.pagination(_query.page - 1, _query.pageSize);

View File

@@ -34,7 +34,6 @@ import { DialogsName } from '@/constants/dialogs';
function RuleFormContentFormRoot({
// #withDialogActions
openDialog,
closeDialog,
}) {
const { accounts, bankRule, isEditMode, bankRuleId } =
@@ -180,6 +179,10 @@ export const RuleFormContentForm = R.compose(withDialogActions)(
RuleFormContentFormRoot,
);
/**
* Rule form conditions stack.
* @returns {React.ReactNode}
*/
function RuleFormConditions() {
const { values, setFieldValue } = useFormikContext<RuleFormValues>();
@@ -245,6 +248,10 @@ function RuleFormConditions() {
);
}
/**
* Rule form actions buttons.
* @returns {React.ReactNode}
*/
function RuleFormActionsRoot({
// #withDialogActions
closeDialog,

View File

@@ -135,7 +135,7 @@ function AccountTransactionsActionsBar({
<Popover
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
position={Position.BOTTOM_RIGHT}
modifiers={{
offset: { offset: '0, 4' },
}}

View File

@@ -2,7 +2,7 @@
import { isEmpty } from 'lodash';
import * as R from 'ramda';
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
import { FastField, FastFieldProps, Formik } from 'formik';
import { FastField, FastFieldProps, Formik, useFormikContext } from 'formik';
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
import {
MatchingTransactionBoot,
@@ -27,12 +27,18 @@ const initialValues = {
matched: {},
};
export function MatchingBankTransaction() {
function MatchingBankTransactionRoot({
// #withBankingActions
closeMatchingTransactionAside,
}) {
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
// Handles the form submitting.
const handleSubmit = (values: MatchingTransactionFormValues) => {
const handleSubmit = (
values: MatchingTransactionFormValues,
{ setSubmitting }: FormikHelpers<MatchingTransactionFormValues>,
) => {
const _values = transformToReq(values);
if (_values.matchedTransactions?.length === 0) {
@@ -42,18 +48,22 @@ export function MatchingBankTransaction() {
});
return;
}
setSubmitting(true);
matchTransaction({ id: uncategorizedTransactionId, value: _values })
.then(() => {
AppToaster.show({
intent: Intent.SUCCESS,
message: 'The bank transaction has been matched successfully.',
});
setSubmitting(false);
closeMatchingTransactionAside();
})
.catch((err) => {
AppToaster.show({
intent: Intent.DANGER,
message: 'Something went wrong.',
});
setSubmitting(false);
});
};
@@ -71,6 +81,10 @@ export function MatchingBankTransaction() {
);
}
export const MatchingBankTransaction = R.compose(withBankingActions)(
MatchingBankTransactionRoot,
);
function MatchingBankTransactionContent() {
return (
<Box className={styles.root}>
@@ -193,12 +207,16 @@ interface MatchTransctionFooterProps extends WithBankingActionsProps {}
*/
const MatchTransactionFooter = R.compose(withBankingActions)(
({ closeMatchingTransactionAside }: MatchTransctionFooterProps) => {
const { submitForm, isSubmitting } = useFormikContext();
const totalPending = useGetPendingAmountMatched();
const showReconcileLink = useIsShowReconcileTransactionLink();
const handleCancelBtnClick = () => {
closeMatchingTransactionAside();
};
const handleSubmitBtnClick = () => {
submitForm();
};
return (
<Box className={styles.footer}>
@@ -223,7 +241,8 @@ const MatchTransactionFooter = R.compose(withBankingActions)(
<Button
intent={Intent.PRIMARY}
style={{ minWidth: 85 }}
type="submit"
onClick={handleSubmitBtnClick}
loading={isSubmitting}
>
Match
</Button>

View File

@@ -116,6 +116,9 @@ export function useDeleteBankRule(
queryClient.invalidateQueries(
QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY,
);
queryClient.invalidateQueries([
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
]);
},
...options,
},

View File

@@ -1228,6 +1228,8 @@ export const getDashboardRoutes = () => [
() => import('@/containers/Banking/Rules/RulesList/RulesLandingPage'),
),
pageTitle: 'Bank Rules',
breadcrumb: 'Bank Rules',
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Homepage
{

View File

@@ -1,21 +0,0 @@
// @ts-nocheck
import t from '@/store/types';
/**
* Sets global table state of the table.
* @param {object} queries
*/
export const setUncategorizedTransactionIdForMatching = (
uncategorizedTransactionId: number,
) => {
return {
type: 'setUncategorizedTransactionIdForMatching',
payload: uncategorizedTransactionId,
};
};
export const closeMatchingTransactionAside = () => {
return {
type: 'closeMatchingTransactionAside',
};
};