mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
feat: validate the matched linked transacation on deleting.
This commit is contained in:
@@ -23,11 +23,9 @@ export class BankTransactionsMatchingController extends BaseController {
|
||||
'/:transactionId',
|
||||
[
|
||||
param('transactionId').exists(),
|
||||
|
||||
body('matchedTransactions').isArray({ min: 1 }),
|
||||
body('matchedTransactions.*.reference_type').exists(),
|
||||
body('matchedTransactions.*.reference_id').isNumeric().toInt(),
|
||||
body('matchedTransactions.*.amount').exists().isNumeric().toFloat(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.matchBankTransaction.bind(this)
|
||||
|
||||
@@ -50,6 +50,8 @@ export class BankingRulesController extends BaseController {
|
||||
body('assign_account_id').isInt({ min: 0 }),
|
||||
body('assign_payee').isString().optional({ nullable: true }),
|
||||
body('assign_memo').isString().optional({ nullable: true }),
|
||||
|
||||
body('recognition').isBoolean().toBoolean().optional({ nullable: true }),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ exports.up = function (knex) {
|
||||
return knex.schema.createTable('recognized_bank_transactions', (table) => {
|
||||
table.increments('id');
|
||||
table
|
||||
.integer('cashflow_transaction_id')
|
||||
.integer('uncategorized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('uncategorized_cashflow_transactions');
|
||||
|
||||
@@ -103,6 +103,11 @@ import { AttachmentsOnCreditNote } from '@/services/Attachments/events/Attachmen
|
||||
import { AttachmentsOnBillPayments } from '@/services/Attachments/events/AttachmentsOnPaymentsMade';
|
||||
import { AttachmentsOnSaleEstimates } from '@/services/Attachments/events/AttachmentsOnSaleEstimates';
|
||||
import { TriggerRecognizedTransactions } from '@/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions';
|
||||
import { ValidateMatchingOnExpenseDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnExpenseDelete';
|
||||
import { ValidateMatchingOnManualJournalDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnManualJournalDelete';
|
||||
import { ValidateMatchingOnPaymentReceivedDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnPaymentReceivedDelete';
|
||||
import { ValidateMatchingOnPaymentMadeDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnPaymentMadeDelete';
|
||||
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -250,5 +255,12 @@ export const susbcribers = () => {
|
||||
|
||||
// Bank Rules
|
||||
TriggerRecognizedTransactions,
|
||||
|
||||
// Validate matching
|
||||
ValidateMatchingOnCashflowDelete,
|
||||
ValidateMatchingOnExpenseDelete,
|
||||
ValidateMatchingOnManualJournalDelete,
|
||||
ValidateMatchingOnPaymentReceivedDelete,
|
||||
ValidateMatchingOnPaymentMadeDelete,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -127,8 +127,8 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
/**
|
||||
* Uncategorized transaction may has association to matched transaction.
|
||||
*/
|
||||
matchedBankTransaction: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
matchedBankTransactions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: MatchedBankTransaction,
|
||||
join: {
|
||||
from: 'uncategorized_cashflow_transactions.id',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ export interface IBankRuleCommonDTO {
|
||||
assignAccountId: number;
|
||||
assignPayee?: string;
|
||||
assignMemo?: string;
|
||||
|
||||
recognition?: boolean;
|
||||
}
|
||||
|
||||
export interface ICreateBankRuleDTO extends IBankRuleCommonDTO {}
|
||||
|
||||
Reference in New Issue
Block a user