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 { body, param } from 'express-validator';
import { import {
GetMatchedTransactionsFilter, GetMatchedTransactionsFilter,
IMatchTransactionDTO, IMatchTransactionsDTO,
} from '@/services/Banking/Matching/types'; } from '@/services/Banking/Matching/types';
@Service() @Service()
@@ -44,10 +44,10 @@ export class BankTransactionsMatchingController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
* @returns * @returns {Promise<Response|null>}
*/ */
private async matchBankTransaction( private async matchBankTransaction(
req: Request, req: Request<{ transactionId: number }>,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
@@ -55,7 +55,7 @@ export class BankTransactionsMatchingController extends BaseController {
const { transactionId } = req.params; const { transactionId } = req.params;
const matchTransactionDTO = this.matchedBodyData( const matchTransactionDTO = this.matchedBodyData(
req req
) as IMatchTransactionDTO; ) as IMatchTransactionsDTO;
try { try {
await this.bankTransactionsMatchingApp.matchTransaction( await this.bankTransactionsMatchingApp.matchTransaction(
@@ -64,6 +64,7 @@ export class BankTransactionsMatchingController extends BaseController {
matchTransactionDTO matchTransactionDTO
); );
return res.status(200).send({ return res.status(200).send({
id: transactionId,
message: 'The bank transaction has been matched.', message: 'The bank transaction has been matched.',
}); });
} catch (error) { } catch (error) {
@@ -72,60 +73,31 @@ export class BankTransactionsMatchingController extends BaseController {
} }
/** /**
* * Unmatches the matched bank transaction.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
* @returns * @returns {Promise<Response|null>}
*/ */
private async unmatchMatchedBankTransaction( private async unmatchMatchedBankTransaction(
req: Request, req: Request<{ transactionId: number }>,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
const { tenantId } = req; const { tenantId } = req;
const transactionId = req.params?.transactionId; const { transactionId } = req.params;
try { try {
await this.bankTransactionsMatchingApp.unmatchMatchedTransaction( await this.bankTransactionsMatchingApp.unmatchMatchedTransaction(
tenantId, tenantId,
transactionId transactionId
); );
return res.status(200).send({ return res.status(200).send({
id: transactionId,
message: 'The bank matched transaction has been unmatched.', message: 'The bank matched transaction has been unmatched.',
}); });
} catch (error) { } catch (error) {
next(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.increments('id');
table.integer('uncategorized_transaction_id').unsigned(); table.integer('uncategorized_transaction_id').unsigned();
table.string('reference_type'); table.string('reference_type');
table.integer('reference_id'); table.integer('reference_id').unsigned();
table.decimal('amount'); table.decimal('amount');
table.timestamps(); table.timestamps();
}); });

View File

@@ -54,14 +54,17 @@ export class BankRule extends TenantModel {
}, },
}, },
/**
* Bank rule may associated to the assign account.
*/
assignAccount: { assignAccount: {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: Account.default, modelClass: Account.default,
join: { join: {
from: 'bank_rules.assignAccountId', 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 { GetMatchedTransactions } from './GetMatchedTransactions';
import { MatchBankTransactions } from './MatchTransactions'; import { MatchBankTransactions } from './MatchTransactions';
import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction'; import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction';
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types'; import { GetMatchedTransactionsFilter, IMatchTransactionsDTO } from './types';
@Service() @Service()
export class MatchBankTransactionsApplication { export class MatchBankTransactionsApplication {
@@ -43,7 +43,7 @@ export class MatchBankTransactionsApplication {
public matchTransaction( public matchTransaction(
tenantId: number, tenantId: number,
uncategorizedTransactionId: number, uncategorizedTransactionId: number,
matchTransactionsDTO: IMatchTransactionDTO matchTransactionsDTO: IMatchTransactionsDTO
): Promise<void> { ): Promise<void> {
return this.matchTransactionService.matchTransaction( return this.matchTransactionService.matchTransaction(
tenantId, tenantId,

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,15 +14,15 @@ export class ValidateMatchingOnManualJournalDelete {
public attach(bus) { public attach(bus) {
bus.subscribe( bus.subscribe(
events.manualJournals.onDeleting, 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} * @param {IManualJournalDeletingPayload}
*/ */
public async validateMatchingOnManualJournalDelete({ public async validateMatchingOnManualJournalDeleting({
tenantId, tenantId,
oldManualJournal, oldManualJournal,
trx, trx,

View File

@@ -17,15 +17,15 @@ export class ValidateMatchingOnPaymentMadeDelete {
public attach(bus) { public attach(bus) {
bus.subscribe( bus.subscribe(
events.billPayment.onDeleting, 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} * @param {IPaymentReceiveDeletedPayload}
*/ */
public async validateMatchingOnPaymentMadeDelete({ public async validateMatchingOnPaymentMadeDeleting({
tenantId, tenantId,
oldBillPayment, oldBillPayment,
trx, trx,

View File

@@ -14,15 +14,15 @@ export class ValidateMatchingOnPaymentReceivedDelete {
public attach(bus) { public attach(bus) {
bus.subscribe( bus.subscribe(
events.paymentReceive.onDeleting, 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} * @param {IPaymentReceiveDeletedPayload}
*/ */
public async validateMatchingOnPaymentReceivedDelete({ public async validateMatchingOnPaymentReceivedDeleting({
tenantId, tenantId,
oldPaymentReceive, oldPaymentReceive,
trx, trx,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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