feat: validate the matched linked transacation on deleting.

This commit is contained in:
Ahmed Bouhuolia
2024-06-23 14:34:40 +02:00
parent ca403872b3
commit 589b29bbdd
18 changed files with 307 additions and 20 deletions

View File

@@ -21,6 +21,7 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
* Retrieves the matched transactions.
* @param {number} tenantId -
* @param {GetMatchedTransactionsFilter} filter -
* @returns {Promise<MatchedTransactionsPOJO>}
*/
public async getMatchedTransactions(
tenantId: number,
@@ -38,10 +39,10 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
}
/**
*
* Retrieves the matched transaction.
* @param {number} tenantId
* @param {number} transactionId
* @returns
* @returns {Promise<MatchedTransactionPOJO>}
*/
public async getMatchedTransaction(
tenantId: number,
@@ -49,8 +50,6 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
): Promise<MatchedTransactionPOJO> {
const { SaleInvoice } = this.tenancy.models(tenantId);
console.log(transactionId);
const invoice = await SaleInvoice.query().findById(transactionId);
return this.transformer.transform(

View File

@@ -1,11 +1,11 @@
import { sumBy } from 'lodash';
import { isEmpty, sumBy } from 'lodash';
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import { PromisePool } from '@supercharge/promise-pool';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import {
ERRORS,
IBankTransactionMatchedEventPayload,
@@ -34,6 +34,7 @@ export class MatchBankTransactions {
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @param {IMatchTransactionsDTO} matchTransactionsDTO
* @returns {Promise<void>}
*/
async validate(
tenantId: number,
@@ -43,11 +44,21 @@ export class MatchBankTransactions {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const { matchedTransactions } = matchTransactionsDTO;
// Validates the uncategorized transaction existance.
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query()
.findById(uncategorizedTransactionId)
.withGraphFetched('matchedBankTransactions')
.throwIfNotFound();
// Validates the uncategorized transaction is not already matched.
if (!isEmpty(uncategorizedTransaction.matchedBankTransactions)) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED);
}
// Validate the uncategorized transaction is not excluded.
if (uncategorizedTransaction.excluded) {
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION);
}
// Validates the given matched transaction.
const validateMatchedTransaction = async (matchedTransaction) => {
const getMatchedTransactionsService =

View File

@@ -1,8 +1,8 @@
import { Inject, Service } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { Inject, Service } from 'typedi';
import { IBankTransactionUnmatchingEventPayload } from './types';
@Service()
@@ -16,10 +16,16 @@ export class UnmatchMatchedBankTransaction {
@Inject()
private eventPublisher: EventPublisher;
/**
* Unmatch the matched the given uncategorized bank transaction.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @returns {Promise<void>}
*/
public unmatchMatchedTransaction(
tenantId: number,
uncategorizedTransactionId: number
) {
): Promise<void> {
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
return this.uow.withTransaction(tenantId, async (trx) => {

View File

@@ -0,0 +1,33 @@
import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ERRORS } from './types';
@Service()
export class ValidateTransactionMatched {
@Inject()
private tenancy: HasTenancyService;
/**
*
* @param {number} tenantId
* @param {string} referenceType
* @param {number} referenceId
*/
public async validateTransactionNoMatchLinking(
tenantId: number,
referenceType: string,
referenceId: number
) {
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
const foundMatchedTransaction =
await MatchedBankTransaction.query().findOne({
referenceType,
referenceId,
});
if (foundMatchedTransaction) {
throw new ServiceError(ERRORS.CANNOT_DELETE_TRANSACTION_MATCHED);
}
}
}

View File

@@ -0,0 +1,36 @@
import { IManualJournalDeletingPayload } from '@/interfaces';
import events from '@/subscribers/events';
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
import { Inject, Service } from 'typedi';
@Service()
export class ValidateMatchingOnCashflowDelete {
@Inject()
private validateNoMatchingLinkedService: ValidateTransactionMatched;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.cashflow.onTransactionDeleting,
this.validateMatchingOnCashflowDelete.bind(this)
);
}
/**
*
* @param {IManualJournalDeletingPayload}
*/
public async validateMatchingOnCashflowDelete({
tenantId,
oldManualJournal,
trx,
}: IManualJournalDeletingPayload) {
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
tenantId,
'ManualJournal',
oldManualJournal.id
);
}
}

View File

@@ -0,0 +1,36 @@
import { Inject, Service } from 'typedi';
import { IExpenseEventDeletePayload } from '@/interfaces';
import events from '@/subscribers/events';
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
@Service()
export class ValidateMatchingOnExpenseDelete {
@Inject()
private validateNoMatchingLinkedService: ValidateTransactionMatched;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.expenses.onDeleting,
this.validateMatchingOnExpenseDelete.bind(this)
);
}
/**
*
* @param {IExpenseEventDeletePayload}
*/
public async validateMatchingOnExpenseDelete({
tenantId,
oldExpense,
trx,
}: IExpenseEventDeletePayload) {
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
tenantId,
'Expense',
oldExpense.id
);
}
}

View File

@@ -0,0 +1,36 @@
import { Inject, Service } from 'typedi';
import { IManualJournalDeletingPayload } from '@/interfaces';
import events from '@/subscribers/events';
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
@Service()
export class ValidateMatchingOnManualJournalDelete {
@Inject()
private validateNoMatchingLinkedService: ValidateTransactionMatched;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.manualJournals.onDeleting,
this.validateMatchingOnManualJournalDelete.bind(this)
);
}
/**
*
* @param {IManualJournalDeletingPayload}
*/
public async validateMatchingOnManualJournalDelete({
tenantId,
oldManualJournal,
trx,
}: IManualJournalDeletingPayload) {
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
tenantId,
'ManualJournal',
oldManualJournal.id
);
}
}

View File

@@ -0,0 +1,39 @@
import { Inject, Service } from 'typedi';
import {
IBillPaymentEventDeletedPayload,
IPaymentReceiveDeletedPayload,
} from '@/interfaces';
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
import events from '@/subscribers/events';
@Service()
export class ValidateMatchingOnPaymentMadeDelete {
@Inject()
private validateNoMatchingLinkedService: ValidateTransactionMatched;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.billPayment.onDeleting,
this.validateMatchingOnPaymentMadeDelete.bind(this)
);
}
/**
*
* @param {IPaymentReceiveDeletedPayload}
*/
public async validateMatchingOnPaymentMadeDelete({
tenantId,
oldBillPayment,
trx,
}: IBillPaymentEventDeletedPayload) {
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
tenantId,
'PaymentMade',
oldBillPayment.id
);
}
}

View File

@@ -0,0 +1,36 @@
import { Inject, Service } from 'typedi';
import { IPaymentReceiveDeletedPayload } from '@/interfaces';
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
import events from '@/subscribers/events';
@Service()
export class ValidateMatchingOnPaymentReceivedDelete {
@Inject()
private validateNoMatchingLinkedService: ValidateTransactionMatched;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.paymentReceive.onDeleting,
this.validateMatchingOnPaymentReceivedDelete.bind(this)
);
}
/**
*
* @param {IPaymentReceiveDeletedPayload}
*/
public async validateMatchingOnPaymentReceivedDelete({
tenantId,
oldPaymentReceive,
trx,
}: IPaymentReceiveDeletedPayload) {
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
tenantId,
'PaymentReceive',
oldPaymentReceive.id
);
}
}

View File

@@ -56,6 +56,8 @@ export const ERRORS = {
'RESOURCE_TYPE_MATCHING_TRANSACTION_INVALID',
RESOURCE_ID_MATCHING_TRANSACTION_INVALID:
'RESOURCE_ID_MATCHING_TRANSACTION_INVALID',
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'
};

View File

@@ -32,7 +32,7 @@ export class RecognizeTranasctionsService {
trx
).insert({
bankRuleId: bankRule.id,
cashflowTransactionId: transaction.id,
uncategorizedTransactionId: transaction.id,
assignedCategory: bankRule.assignCategory,
assignedAccountId: bankRule.assignAccountId,
assignedPayee: bankRule.assignPayee,

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
IBankRuleEventCreatedPayload,
IBankRuleEventDeletedPayload,
IBankRuleEventEditedPayload,
} from '../../Rules/types';
@@ -20,17 +21,55 @@ export class TriggerRecognizedTransactions {
);
bus.subscribe(
events.bankRules.onEdited,
this.recognizedTransactionsOnRuleCreated.bind(this)
this.recognizedTransactionsOnRuleEdited.bind(this)
);
bus.subscribe(
events.bankRules.onDeleted,
this.recognizedTransactionsOnRuleDeleted.bind(this)
);
}
/**
* Triggers the recognize uncategorized transactions job.
* @param {IBankRuleEventEditedPayload | IBankRuleEventCreatedPayload} payload -
* Triggers the recognize uncategorized transactions job on rule created.
* @param {IBankRuleEventCreatedPayload} payload -
*/
private async recognizedTransactionsOnRuleCreated({
tenantId,
}: IBankRuleEventEditedPayload | IBankRuleEventCreatedPayload) {
createRuleDTO,
}: IBankRuleEventCreatedPayload) {
const payload = { tenantId };
// Cannot run recognition if the option is not enabled.
if (createRuleDTO.recognition) {
return;
}
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
}
/**
* Triggers the recognize uncategorized transactions job on rule edited.
* @param {IBankRuleEventEditedPayload} payload -
*/
private async recognizedTransactionsOnRuleEdited({
tenantId,
editRuleDTO,
}: IBankRuleEventEditedPayload) {
const payload = { tenantId };
// Cannot run recognition if the option is not enabled.
if (!editRuleDTO.recognition) {
return;
}
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
}
/**
* Triggers the recognize uncategorized transactions job on rule deleted.
* @param {IBankRuleEventDeletedPayload} payload -
*/
private async recognizedTransactionsOnRuleDeleted({
tenantId,
}: IBankRuleEventDeletedPayload) {
const payload = { tenantId };
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
}

View File

@@ -70,6 +70,8 @@ export interface IBankRuleCommonDTO {
assignAccountId: number;
assignPayee?: string;
assignMemo?: string;
recognition?: boolean;
}
export interface ICreateBankRuleDTO extends IBankRuleCommonDTO {}