mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20:31 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { BankingMatchingApplication } from './BankingMatchingApplication';
|
||||
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
|
||||
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
|
||||
|
||||
@Controller('banking/matching')
|
||||
@ApiTags('banking-transactions-matching')
|
||||
export class BankingMatchingController {
|
||||
constructor(
|
||||
private readonly bankingMatchingApplication: BankingMatchingApplication
|
||||
) {}
|
||||
|
||||
@Get('matched/transactions')
|
||||
@ApiOperation({ summary: 'Retrieves the matched transactions.' })
|
||||
async getMatchedTransactions(
|
||||
@Query('uncategorizedTransactionIds') uncategorizedTransactionIds: number[],
|
||||
@Query() filter: GetMatchedTransactionsFilter
|
||||
) {
|
||||
return this.bankingMatchingApplication.getMatchedTransactions(
|
||||
uncategorizedTransactionIds,
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/match/:uncategorizedTransactionId')
|
||||
@ApiOperation({ summary: 'Match the given uncategorized transaction.' })
|
||||
async matchTransaction(
|
||||
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number | number[],
|
||||
@Body() matchedTransactions: MatchBankTransactionDto
|
||||
) {
|
||||
return this.bankingMatchingApplication.matchTransaction(
|
||||
uncategorizedTransactionId,
|
||||
matchedTransactions
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/unmatch/:uncategorizedTransactionId')
|
||||
@ApiOperation({ summary: 'Unmatch the given uncategorized transaction.' })
|
||||
async unmatchMatchedTransaction(
|
||||
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number
|
||||
) {
|
||||
return this.bankingMatchingApplication.unmatchMatchedTransaction(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MatchedBankTransaction } from './models/MatchedBankTransaction';
|
||||
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
|
||||
import { BankingMatchingApplication } from './BankingMatchingApplication';
|
||||
import { GetMatchedTransactions } from './queries/GetMatchedTransactions.service';
|
||||
import { UnmatchMatchedBankTransaction } from './commands/UnmatchMatchedTransaction.service';
|
||||
import { GetMatchedTransactionsByBills } from './queries/GetMatchedTransactionsByBills.service';
|
||||
import { GetMatchedTransactionsByCashflow } from './queries/GetMatchedTransactionsByCashflow';
|
||||
import { GetMatchedTransactionsByExpenses } from './queries/GetMatchedTransactionsByExpenses';
|
||||
import { GetMatchedTransactionsByInvoices } from './queries/GetMatchedTransactionsByInvoices.service';
|
||||
import { ValidateMatchingOnExpenseDeleteSubscriber } from './events/ValidateMatchingOnExpenseDelete';
|
||||
import { ValidateMatchingOnPaymentReceivedDeleteSubscriber } from './events/ValidateMatchingOnPaymentReceivedDelete';
|
||||
import { DecrementUncategorizedTransactionOnMatchingSubscriber } from './events/DecrementUncategorizedTransactionsOnMatch';
|
||||
import { ValidateMatchingOnPaymentMadeDeleteSubscriber } from './events/ValidateMatchingOnPaymentMadeDelete';
|
||||
import { ValidateMatchingOnManualJournalDeleteSubscriber } from './events/ValidateMatchingOnManualJournalDelete';
|
||||
import { ValidateMatchingOnCashflowDeleteSubscriber } from './events/ValidateMatchingOnCashflowDelete';
|
||||
import { BillPaymentsModule } from '../BillPayments/BillPayments.module';
|
||||
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
|
||||
import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.module';
|
||||
import { MatchBankTransactions } from './commands/MatchTransactions';
|
||||
import { MatchTransactionsTypes } from './commands/MatchTransactionsTypes';
|
||||
import { GetMatchedTransactionsByManualJournals } from './queries/GetMatchedTransactionsByManualJournals.service';
|
||||
import { ValidateTransactionMatched } from './commands/ValidateTransactionsMatched.service';
|
||||
import { BankingMatchingController } from './BankingMatching.controller';
|
||||
|
||||
const models = [RegisterTenancyModel(MatchedBankTransaction)];
|
||||
|
||||
@Module({
|
||||
controllers: [BankingMatchingController],
|
||||
imports: [
|
||||
...models,
|
||||
BillPaymentsModule,
|
||||
BankingTransactionsModule,
|
||||
PaymentsReceivedModule,
|
||||
],
|
||||
providers: [
|
||||
ValidateTransactionMatched,
|
||||
MatchBankTransactions,
|
||||
MatchTransactionsTypes,
|
||||
GetMatchedTransactionsByBills,
|
||||
GetMatchedTransactionsByCashflow,
|
||||
GetMatchedTransactionsByExpenses,
|
||||
GetMatchedTransactionsByInvoices,
|
||||
GetMatchedTransactionsByManualJournals,
|
||||
BankingMatchingApplication,
|
||||
GetMatchedTransactions,
|
||||
UnmatchMatchedBankTransaction,
|
||||
ValidateMatchingOnExpenseDeleteSubscriber,
|
||||
ValidateMatchingOnPaymentReceivedDeleteSubscriber,
|
||||
DecrementUncategorizedTransactionOnMatchingSubscriber,
|
||||
ValidateMatchingOnPaymentMadeDeleteSubscriber,
|
||||
ValidateMatchingOnManualJournalDeleteSubscriber,
|
||||
ValidateMatchingOnCashflowDeleteSubscriber,
|
||||
],
|
||||
exports: [...models],
|
||||
})
|
||||
export class BankingMatchingModule {}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GetMatchedTransactions } from './queries/GetMatchedTransactions.service';
|
||||
import { MatchBankTransactions } from './commands/MatchTransactions';
|
||||
import { UnmatchMatchedBankTransaction } from './commands/UnmatchMatchedTransaction.service';
|
||||
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
|
||||
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BankingMatchingApplication {
|
||||
constructor(
|
||||
private readonly getMatchedTransactionsService: GetMatchedTransactions,
|
||||
private readonly matchTransactionService: MatchBankTransactions,
|
||||
private readonly unmatchMatchedTransactionService: UnmatchMatchedBankTransaction,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions.
|
||||
* @param {Array<number>} uncategorizedTransactionsIds - Uncategorized transactions ids.
|
||||
* @param {GetMatchedTransactionsFilter} filter -
|
||||
* @returns {Promise<MatchedTransactionsPOJO>}
|
||||
*/
|
||||
public getMatchedTransactions(
|
||||
uncategorizedTransactionsIds: Array<number>,
|
||||
filter: GetMatchedTransactionsFilter,
|
||||
) {
|
||||
return this.getMatchedTransactionsService.getMatchedTransactions(
|
||||
uncategorizedTransactionsIds,
|
||||
filter,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the given uncategorized transaction with the given system transaction.
|
||||
* @param {number} uncategorizedTransactionId
|
||||
* @param {IMatchTransactionDTO} matchTransactionsDTO
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public matchTransaction(
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
matchedTransactions: MatchBankTransactionDto,
|
||||
): Promise<void> {
|
||||
return this.matchTransactionService.matchTransaction(
|
||||
uncategorizedTransactionId,
|
||||
matchedTransactions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmatch the given matched transaction.
|
||||
* @param {number} uncategorizedTransactionId - Uncategorized transaction id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public unmatchMatchedTransaction(uncategorizedTransactionId: number) {
|
||||
return this.unmatchMatchedTransactionService.unmatchMatchedTransaction(
|
||||
uncategorizedTransactionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
64
packages/server/src/modules/BankingMatching/_utils.ts
Normal file
64
packages/server/src/modules/BankingMatching/_utils.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { isEmpty, sumBy } from 'lodash';
|
||||
import { ERRORS, MatchedTransactionPOJO } from './types';
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
|
||||
export const sortClosestMatchTransactions = (
|
||||
amount: number,
|
||||
date: Date,
|
||||
matches: MatchedTransactionPOJO[]
|
||||
) => {
|
||||
return R.sortWith([
|
||||
// Sort by amount difference (closest to uncategorized transaction amount first)
|
||||
R.ascend((match: MatchedTransactionPOJO) =>
|
||||
Math.abs(match.amount - amount)
|
||||
),
|
||||
// Sort by date difference (closest to uncategorized transaction date first)
|
||||
R.ascend((match: MatchedTransactionPOJO) =>
|
||||
Math.abs(moment(match.date).diff(moment(date), 'days'))
|
||||
),
|
||||
])(matches);
|
||||
};
|
||||
|
||||
export const sumMatchTranasctions = (transactions: Array<any>) => {
|
||||
return transactions.reduce(
|
||||
(total, item) =>
|
||||
total +
|
||||
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
export const sumUncategorizedTransactions = (
|
||||
uncategorizedTransactions: Array<any>
|
||||
) => {
|
||||
return sumBy(uncategorizedTransactions, 'amount');
|
||||
};
|
||||
|
||||
export const validateUncategorizedTransactionsNotMatched = (
|
||||
uncategorizedTransactions: any
|
||||
) => {
|
||||
const matchedTransactions = uncategorizedTransactions.filter(
|
||||
(trans) => !isEmpty(trans.matchedBankTransactions)
|
||||
);
|
||||
//
|
||||
if (matchedTransactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED, '', {
|
||||
matchedTransactionsIds: matchedTransactions?.map((m) => m.id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateUncategorizedTransactionsExcluded = (
|
||||
uncategorizedTransactions: any
|
||||
) => {
|
||||
const excludedTransactions = uncategorizedTransactions.filter(
|
||||
(trans) => trans.excluded
|
||||
);
|
||||
if (excludedTransactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION, '', {
|
||||
excludedTransactionsIds: excludedTransactions.map((e) => e.id),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import { castArray } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import {
|
||||
ERRORS,
|
||||
IBankTransactionMatchedEventPayload,
|
||||
IBankTransactionMatchingEventPayload,
|
||||
IMatchTransactionDTO,
|
||||
} from '../types';
|
||||
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
||||
import {
|
||||
sumMatchTranasctions,
|
||||
sumUncategorizedTransactions,
|
||||
validateUncategorizedTransactionsExcluded,
|
||||
validateUncategorizedTransactionsNotMatched,
|
||||
} from '../_utils';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { MatchBankTransactionDto } from '../dtos/MatchBankTransaction.dto';
|
||||
|
||||
@Injectable()
|
||||
export class MatchBankTransactions {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly matchedBankTransactions: MatchTransactionsTypes,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the match bank transactions DTO.
|
||||
* @param {number} uncategorizedTransactionId - Uncategorized transaction id.
|
||||
* @param {IMatchTransactionsDTO} matchTransactionsDTO - Match transactions DTO.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async validate(
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
matchedTransactions: Array<IMatchTransactionDTO>,
|
||||
) {
|
||||
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
||||
|
||||
// Validates the uncategorized transaction existance.
|
||||
const uncategorizedTransactions =
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query()
|
||||
.whereIn('id', uncategorizedTransactionIds)
|
||||
.withGraphFetched('matchedBankTransactions')
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validates the uncategorized transaction is not already matched.
|
||||
validateUncategorizedTransactionsNotMatched(uncategorizedTransactions);
|
||||
|
||||
// Validate the uncategorized transaction is not excluded.
|
||||
validateUncategorizedTransactionsExcluded(uncategorizedTransactions);
|
||||
|
||||
// Validates the given matched transaction.
|
||||
const validateMatchedTransaction = async (matchedTransaction) => {
|
||||
const getMatchedTransactionsService =
|
||||
this.matchedBankTransactions.registry.get(
|
||||
matchedTransaction.referenceType,
|
||||
);
|
||||
if (!getMatchedTransactionsService) {
|
||||
throw new ServiceError(
|
||||
ERRORS.RESOURCE_TYPE_MATCHING_TRANSACTION_INVALID,
|
||||
);
|
||||
}
|
||||
const foundMatchedTransaction =
|
||||
await getMatchedTransactionsService.getMatchedTransaction(
|
||||
matchedTransaction.referenceId,
|
||||
);
|
||||
if (!foundMatchedTransaction) {
|
||||
throw new ServiceError(ERRORS.RESOURCE_ID_MATCHING_TRANSACTION_INVALID);
|
||||
}
|
||||
return foundMatchedTransaction;
|
||||
};
|
||||
// Matches the given transactions under promise pool concurrency controlling.
|
||||
const validatationResult = await PromisePool.withConcurrency(10)
|
||||
.for(matchedTransactions)
|
||||
.process(validateMatchedTransaction);
|
||||
|
||||
if (validatationResult.errors?.length > 0) {
|
||||
const error = validatationResult.errors.map((er) => er.raw)[0];
|
||||
throw new ServiceError(error);
|
||||
}
|
||||
// Calculate the total given matching transactions.
|
||||
const totalMatchedTranasctions = sumMatchTranasctions(
|
||||
validatationResult.results,
|
||||
);
|
||||
const totalUncategorizedTransactions = sumUncategorizedTransactions(
|
||||
uncategorizedTransactions,
|
||||
);
|
||||
// Validates the total given matching transcations whether is not equal
|
||||
// uncategorized transaction amount.
|
||||
if (totalUncategorizedTransactions !== totalMatchedTranasctions) {
|
||||
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the given uncategorized transaction to the given references.
|
||||
* @param {number} tenantId
|
||||
* @param {number} uncategorizedTransactionId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async matchTransaction(
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
matchedTransactionsDto: MatchBankTransactionDto,
|
||||
): Promise<void> {
|
||||
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
||||
const matchedTransactions = matchedTransactionsDto.entries;
|
||||
|
||||
// Validates the given matching transactions DTO.
|
||||
await this.validate(uncategorizedTransactionIds, matchedTransactions);
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers the event `onBankTransactionMatching`.
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
|
||||
uncategorizedTransactionIds,
|
||||
matchedTransactions,
|
||||
trx,
|
||||
} as IBankTransactionMatchingEventPayload);
|
||||
|
||||
// Matches the given transactions under promise pool concurrency controlling.
|
||||
await PromisePool.withConcurrency(10)
|
||||
.for(matchedTransactions)
|
||||
.process(async (matchedTransaction) => {
|
||||
const getMatchedTransactionsService =
|
||||
this.matchedBankTransactions.registry.get(
|
||||
matchedTransaction.referenceType,
|
||||
);
|
||||
await getMatchedTransactionsService.createMatchedTransaction(
|
||||
uncategorizedTransactionIds,
|
||||
matchedTransaction,
|
||||
trx,
|
||||
);
|
||||
});
|
||||
// Triggers the event `onBankTransactionMatched`.
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
|
||||
uncategorizedTransactionIds,
|
||||
matchedTransactions,
|
||||
trx,
|
||||
} as IBankTransactionMatchedEventPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { GetMatchedTransactionsByExpenses } from '../queries/GetMatchedTransactionsByExpenses';
|
||||
import { GetMatchedTransactionsByBills } from '../queries/GetMatchedTransactionsByBills.service';
|
||||
import { GetMatchedTransactionsByManualJournals } from '../queries/GetMatchedTransactionsByManualJournals.service';
|
||||
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
|
||||
import { GetMatchedTransactionsByInvoices } from '../queries/GetMatchedTransactionsByInvoices.service';
|
||||
import { GetMatchedTransactionCashflowTransformer } from '../queries/GetMatchedTransactionCashflowTransformer';
|
||||
import { GetMatchedTransactionsByCashflow } from '../queries/GetMatchedTransactionsByCashflow';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class MatchTransactionsTypes {
|
||||
private static registry: MatchTransactionsTypesRegistry;
|
||||
|
||||
/**
|
||||
* Consttuctor method.
|
||||
*/
|
||||
constructor() {
|
||||
this.boot();
|
||||
}
|
||||
|
||||
get registered() {
|
||||
return [
|
||||
{ type: 'SaleInvoice', service: GetMatchedTransactionsByInvoices },
|
||||
{ type: 'Bill', service: GetMatchedTransactionsByBills },
|
||||
{ type: 'Expense', service: GetMatchedTransactionsByExpenses },
|
||||
{
|
||||
type: 'ManualJournal',
|
||||
service: GetMatchedTransactionsByManualJournals,
|
||||
},
|
||||
{
|
||||
type: 'CashflowTransaction',
|
||||
service: GetMatchedTransactionsByCashflow,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Importable instances.
|
||||
*/
|
||||
private types = [];
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public get registry() {
|
||||
return MatchTransactionsTypes.registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boots all the registered importables.
|
||||
*/
|
||||
public boot() {
|
||||
if (!MatchTransactionsTypes.registry) {
|
||||
const instance = MatchTransactionsTypesRegistry.getInstance();
|
||||
|
||||
this.registered.forEach((registered) => {
|
||||
// const serviceInstanace = Container.get(registered.service);
|
||||
// instance.register(registered.type, serviceInstanace);
|
||||
});
|
||||
MatchTransactionsTypes.registry = instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { camelCase, upperFirst } from 'lodash';
|
||||
import { GetMatchedTransactionsByType } from '../queries/GetMatchedTransactionsByType';
|
||||
|
||||
export class MatchTransactionsTypesRegistry {
|
||||
private static instance: MatchTransactionsTypesRegistry;
|
||||
private importables: Record<string, GetMatchedTransactionsByType>;
|
||||
|
||||
constructor() {
|
||||
this.importables = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets singleton instance of registry.
|
||||
* @returns {MatchTransactionsTypesRegistry}
|
||||
*/
|
||||
public static getInstance(): MatchTransactionsTypesRegistry {
|
||||
if (!MatchTransactionsTypesRegistry.instance) {
|
||||
MatchTransactionsTypesRegistry.instance =
|
||||
new MatchTransactionsTypesRegistry();
|
||||
}
|
||||
return MatchTransactionsTypesRegistry.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given importable service.
|
||||
* @param {string} resource
|
||||
* @param {GetMatchedTransactionsByType} importable
|
||||
*/
|
||||
public register(
|
||||
resource: string,
|
||||
importable: GetMatchedTransactionsByType
|
||||
): void {
|
||||
const _resource = this.sanitizeResourceName(resource);
|
||||
this.importables[_resource] = importable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the importable service instance of the given resource name.
|
||||
* @param {string} name
|
||||
* @returns {GetMatchedTransactionsByType}
|
||||
*/
|
||||
public get(name: string): GetMatchedTransactionsByType {
|
||||
const _name = this.sanitizeResourceName(name);
|
||||
return this.importables[_name];
|
||||
}
|
||||
|
||||
private sanitizeResourceName(resource: string) {
|
||||
return upperFirst(camelCase(resource));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IBankTransactionUnmatchingEventPayload } from '../types';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { MatchedBankTransaction } from '../models/MatchedBankTransaction';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class UnmatchMatchedBankTransaction {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(MatchedBankTransaction.name)
|
||||
private readonly matchedBankTransactionModel: TenantModelProxy<
|
||||
typeof MatchedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Unmatch the matched the given uncategorized bank transaction.
|
||||
* @param {number} uncategorizedTransactionId - Uncategorized transaction id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public unmatchMatchedTransaction(
|
||||
uncategorizedTransactionId: number,
|
||||
): Promise<void> {
|
||||
return this.uow.withTransaction(async (trx) => {
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
} as IBankTransactionUnmatchingEventPayload);
|
||||
|
||||
await this.matchedBankTransactionModel()
|
||||
.query(trx)
|
||||
.where('uncategorizedTransactionId', uncategorizedTransactionId)
|
||||
.delete();
|
||||
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
} as IBankTransactionUnmatchingEventPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Knex } from 'knex';
|
||||
import { ERRORS } from '../types';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { MatchedBankTransaction } from '../models/MatchedBankTransaction';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateTransactionMatched {
|
||||
constructor(
|
||||
@Inject(MatchedBankTransaction.name)
|
||||
private readonly matchedBankTransactionModel: TenantModelProxy<
|
||||
typeof MatchedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validate the given transaction whether is matched with bank transactions.
|
||||
* @param {string} referenceType - Transaction reference type.
|
||||
* @param {number} referenceId - Transaction reference id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async validateTransactionNoMatchLinking(
|
||||
referenceType: string,
|
||||
referenceId: number,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const foundMatchedTransaction = await this.matchedBankTransactionModel()
|
||||
.query(trx)
|
||||
.findOne({
|
||||
referenceType,
|
||||
referenceId,
|
||||
});
|
||||
|
||||
if (foundMatchedTransaction) {
|
||||
throw new ServiceError(ERRORS.CANNOT_DELETE_TRANSACTION_MATCHED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class MatchTransactionEntryDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
description: 'The type of the reference',
|
||||
example: 'SaleInvoice',
|
||||
})
|
||||
referenceType: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
description: 'The ID of the reference',
|
||||
example: 1,
|
||||
})
|
||||
referenceId: number;
|
||||
}
|
||||
|
||||
export class MatchBankTransactionDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MatchTransactionEntryDto)
|
||||
@ApiProperty({
|
||||
description: 'The entries to match',
|
||||
example: [
|
||||
{ referenceType: 'SaleInvoice', referenceId: 1 },
|
||||
{ referenceType: 'SaleInvoice', referenceId: 2 },
|
||||
],
|
||||
})
|
||||
entries: MatchTransactionEntryDto[];
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
IBankTransactionMatchedEventPayload,
|
||||
IBankTransactionUnmatchedEventPayload,
|
||||
} from '../types';
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DecrementUncategorizedTransactionOnMatchingSubscriber {
|
||||
constructor(
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
@OnEvent(events.bankMatch.onMatched)
|
||||
public async decrementUnCategorizedTransactionsOnMatching({
|
||||
uncategorizedTransactionIds,
|
||||
trx,
|
||||
}: IBankTransactionMatchedEventPayload) {
|
||||
const uncategorizedTransactions =
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query()
|
||||
.whereIn('id', uncategorizedTransactionIds);
|
||||
|
||||
await PromisePool.withConcurrency(1)
|
||||
.for(uncategorizedTransactions)
|
||||
.process(async (transaction) => {
|
||||
await this.accountModel()
|
||||
.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
@OnEvent(events.bankMatch.onUnmatched)
|
||||
public async incrementUnCategorizedTransactionsOnUnmatching({
|
||||
uncategorizedTransactionId,
|
||||
trx,
|
||||
}: IBankTransactionUnmatchedEventPayload) {
|
||||
const transaction = await this.uncategorizedBankTransactionModel()
|
||||
.query()
|
||||
.findById(uncategorizedTransactionId);
|
||||
|
||||
await this.accountModel()
|
||||
.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ValidateTransactionMatched } from '../commands/ValidateTransactionsMatched.service';
|
||||
import { ICommandCashflowDeletingPayload } from '@/modules/BankingTransactions/types/BankingTransactions.types';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateMatchingOnCashflowDeleteSubscriber {
|
||||
constructor(
|
||||
private readonly validateNoMatchingLinkedService: ValidateTransactionMatched,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
@OnEvent(events.cashflow.onTransactionDeleting)
|
||||
public async validateMatchingOnCashflowDeleting({
|
||||
oldCashflowTransaction,
|
||||
trx,
|
||||
}: ICommandCashflowDeletingPayload) {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
'CashflowTransaction',
|
||||
oldCashflowTransaction.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { ValidateTransactionMatched } from '../commands/ValidateTransactionsMatched.service';
|
||||
import { IExpenseEventDeletePayload } from '@/modules/Expenses/interfaces/Expenses.interface';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateMatchingOnExpenseDeleteSubscriber {
|
||||
constructor(
|
||||
private readonly validateNoMatchingLinkedService: ValidateTransactionMatched,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the expense transaction whether matched with bank transaction on deleting.
|
||||
* @param {IExpenseEventDeletePayload}
|
||||
*/
|
||||
@OnEvent(events.expenses.onDeleting)
|
||||
public async validateMatchingOnExpenseDeleting({
|
||||
oldExpense,
|
||||
trx,
|
||||
}: IExpenseEventDeletePayload) {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
'Expense',
|
||||
oldExpense.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ValidateTransactionMatched } from '../commands/ValidateTransactionsMatched.service';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { IManualJournalDeletingPayload } from '@/modules/ManualJournals/types/ManualJournals.types';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateMatchingOnManualJournalDeleteSubscriber {
|
||||
constructor(
|
||||
private readonly validateNoMatchingLinkedService: ValidateTransactionMatched,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the manual journal transaction whether matched with bank transaction on deleting.
|
||||
* @param {IManualJournalDeletingPayload}
|
||||
*/
|
||||
@OnEvent(events.manualJournals.onDeleting)
|
||||
public async validateMatchingOnManualJournalDeleting({
|
||||
oldManualJournal,
|
||||
trx,
|
||||
}: IManualJournalDeletingPayload) {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
'ManualJournal',
|
||||
oldManualJournal.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { ValidateTransactionMatched } from '../commands/ValidateTransactionsMatched.service';
|
||||
import { IBillPaymentEventDeletedPayload } from '@/modules/BillPayments/types/BillPayments.types';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateMatchingOnPaymentMadeDeleteSubscriber {
|
||||
constructor(
|
||||
private readonly validateNoMatchingLinkedService: ValidateTransactionMatched,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the payment made transaction whether matched with bank transaction on deleting.
|
||||
* @param {IPaymentReceivedDeletedPayload}
|
||||
*/
|
||||
@OnEvent(events.billPayment.onDeleting)
|
||||
public async validateMatchingOnPaymentMadeDeleting({
|
||||
oldBillPayment,
|
||||
trx,
|
||||
}: IBillPaymentEventDeletedPayload) {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
'PaymentMade',
|
||||
oldBillPayment.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ValidateTransactionMatched } from '../commands/ValidateTransactionsMatched.service';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { IPaymentReceivedDeletedPayload } from '@/modules/PaymentReceived/types/PaymentReceived.types';
|
||||
|
||||
@Injectable()
|
||||
export class ValidateMatchingOnPaymentReceivedDeleteSubscriber {
|
||||
constructor(
|
||||
private readonly validateNoMatchingLinkedService: ValidateTransactionMatched,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validates the payment received transaction whether matched with bank transaction on deleting.
|
||||
* @param {IPaymentReceivedDeletedPayload}
|
||||
*/
|
||||
@OnEvent(events.paymentReceive.onDeleting)
|
||||
public async validateMatchingOnPaymentReceivedDeleting({
|
||||
oldPaymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceivedDeletedPayload) {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
'PaymentReceive',
|
||||
oldPaymentReceive.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { BaseModel } from '@/models/Model';
|
||||
|
||||
export class MatchedBankTransaction extends BaseModel {
|
||||
public referenceId!: number;
|
||||
public referenceType!: string;
|
||||
public uncategorizedTransactionId!: number;
|
||||
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'matched_bank_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Transformer } from "@/modules/Transformer/Transformer";
|
||||
|
||||
export class GetMatchedTransactionBillsTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'referenceNo',
|
||||
'amount',
|
||||
'amountFormatted',
|
||||
'transactionNo',
|
||||
'date',
|
||||
'dateFormatted',
|
||||
'transactionId',
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceId',
|
||||
'referenceType',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the reference number of the bill.
|
||||
* @param {Object} bill - The bill object.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceNo(bill) {
|
||||
return bill.referenceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the amount of the bill.
|
||||
* @param {Object} bill - The bill object.
|
||||
* @returns {number}
|
||||
*/
|
||||
protected amount(bill) {
|
||||
return bill.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the formatted amount of the bill.
|
||||
* @param {Object} bill - The bill object.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected amountFormatted(bill) {
|
||||
return this.formatNumber(bill.amount, {
|
||||
currencyCode: bill.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the date of the bill.
|
||||
* @param {Object} bill - The bill object.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected date(bill) {
|
||||
return bill.billDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the formatted date of the bill.
|
||||
* @param {Object} bill - The bill object.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected dateFormatted(bill) {
|
||||
return this.formatDate(bill.billDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transcation id of the bill.
|
||||
* @param {Object} bill - The bill object.
|
||||
* @returns {number}
|
||||
*/
|
||||
protected transactionId(bill) {
|
||||
return bill.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal transaction type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionType() {
|
||||
return 'Bill';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal formatted transaction type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transsactionTypeFormatted() {
|
||||
return 'Bill';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bill transaction normal (debit or credit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal() {
|
||||
return 'credit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the match transaction reference id.
|
||||
* @param bill
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(bill) {
|
||||
return bill.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the match transaction referenece type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'Bill';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Transformer } from "@/modules/Transformer/Transformer";
|
||||
|
||||
export class GetMatchedTransactionCashflowTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'referenceNo',
|
||||
'amount',
|
||||
'amountFormatted',
|
||||
'transactionNo',
|
||||
'date',
|
||||
'dateFormatted',
|
||||
'transactionId',
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceId',
|
||||
'referenceType',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the invoice reference number.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceNo(invoice) {
|
||||
return invoice.referenceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction amount.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected amount(transaction) {
|
||||
return transaction.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction formatted amount.
|
||||
* @param transaction
|
||||
* @returns {string}
|
||||
*/
|
||||
protected amountFormatted(transaction) {
|
||||
return this.formatNumber(transaction.amount, {
|
||||
currencyCode: transaction.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the date of the invoice.
|
||||
* @param invoice
|
||||
* @returns {Date}
|
||||
*/
|
||||
protected date(transaction) {
|
||||
return transaction.date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the date of the invoice.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected dateFormatted(transaction) {
|
||||
return this.formatDate(transaction.date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction ID of the invoice.
|
||||
* @param invoice
|
||||
* @returns {number}
|
||||
*/
|
||||
protected transactionId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice transaction number.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNo(transaction) {
|
||||
return transaction.transactionNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice transaction type.
|
||||
* @param invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected transactionType(transaction) {
|
||||
return transaction.transactionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice formatted transaction type.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transsactionTypeFormatted(transaction) {
|
||||
return transaction.transactionTypeFormatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow transaction normal (credit or debit).
|
||||
* @param transaction
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal(transaction) {
|
||||
return transaction.isCashCredit ? 'credit' : 'debit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow transaction reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow transaction reference type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'CashflowTransaction';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Transformer } from "@/modules/Transformer/Transformer";
|
||||
|
||||
export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'referenceNo',
|
||||
'amount',
|
||||
'amountFormatted',
|
||||
'transactionNo',
|
||||
'date',
|
||||
'dateFormatted',
|
||||
'transactionId',
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceType',
|
||||
'referenceId',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the expense reference number.
|
||||
* @param expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceNo(expense) {
|
||||
return expense.referenceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expense amount.
|
||||
* @param expense
|
||||
* @returns {number}
|
||||
*/
|
||||
protected amount(expense) {
|
||||
return expense.totalAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the amount of the expense.
|
||||
* @param expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected amountFormatted(expense) {
|
||||
return this.formatNumber(expense.totalAmount, {
|
||||
currencyCode: expense.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the date of the expense.
|
||||
* @param expense
|
||||
* @returns {Date}
|
||||
*/
|
||||
protected date(expense) {
|
||||
return expense.paymentDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the date of the expense.
|
||||
* @param expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected dateFormatted(expense) {
|
||||
return this.formatDate(expense.paymentDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the transaction ID of the expense.
|
||||
* @param expense
|
||||
* @returns {number}
|
||||
*/
|
||||
protected transactionId(expense) {
|
||||
return expense.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expense transaction number.
|
||||
* @param expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNo(expense) {
|
||||
return expense.expenseNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expense transaction type.
|
||||
* @param expense
|
||||
* @returns {String}
|
||||
*/
|
||||
protected transactionType() {
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted transaction type of the expense.
|
||||
* @param expense
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transsactionTypeFormatted() {
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the expense transaction normal (credit or debit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal() {
|
||||
return 'credit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Transformer } from "@/modules/Transformer/Transformer";
|
||||
|
||||
export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'referenceNo',
|
||||
'amount',
|
||||
'amountFormatted',
|
||||
'transactionNo',
|
||||
'date',
|
||||
'dateFormatted',
|
||||
'transactionId',
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceType',
|
||||
'referenceId'
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the invoice reference number.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceNo(invoice) {
|
||||
return invoice.referenceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice amount.
|
||||
* @param invoice
|
||||
* @returns {number}
|
||||
*/
|
||||
protected amount(invoice) {
|
||||
return invoice.dueAmount;
|
||||
}
|
||||
/**
|
||||
* Format the amount of the invoice.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected amountFormatted(invoice) {
|
||||
return this.formatNumber(invoice.dueAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the date of the invoice.
|
||||
* @param invoice
|
||||
* @returns {Date}
|
||||
*/
|
||||
protected date(invoice) {
|
||||
return invoice.invoiceDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the date of the invoice.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected dateFormatted(invoice) {
|
||||
return this.formatDate(invoice.invoiceDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction ID of the invoice.
|
||||
* @param invoice
|
||||
* @returns {number}
|
||||
*/
|
||||
protected transactionId(invoice) {
|
||||
return invoice.id;
|
||||
}
|
||||
/**
|
||||
* Retrieve the invoice transaction number.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNo(invoice) {
|
||||
return invoice.invoiceNo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice transaction type.
|
||||
* @param invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected transactionType(invoice) {
|
||||
return 'SaleInvoice';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the invoice formatted transaction type.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transsactionTypeFormatted(invoice) {
|
||||
return 'Sale invoice';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction normal of invoice (credit or debit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal() {
|
||||
return 'debit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference type.
|
||||
* @returns {string}
|
||||
*/ protected referenceType() {
|
||||
return 'SaleInvoice';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { sumBy } from 'lodash';
|
||||
import { AccountNormal } from '@/interfaces/Account';
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
|
||||
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'referenceNo',
|
||||
'amount',
|
||||
'amountFormatted',
|
||||
'transactionNo',
|
||||
'date',
|
||||
'dateFormatted',
|
||||
'transactionId',
|
||||
'transactionNo',
|
||||
'transactionType',
|
||||
'transsactionTypeFormatted',
|
||||
'transactionNormal',
|
||||
'referenceType',
|
||||
'referenceId',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude all attributes.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal reference no.
|
||||
* @param manualJournal
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceNo(manualJournal) {
|
||||
return manualJournal.referenceNo;
|
||||
}
|
||||
|
||||
protected total(manualJournal) {
|
||||
const credit = sumBy(manualJournal?.entries, 'credit');
|
||||
const debit = sumBy(manualJournal?.entries, 'debit');
|
||||
|
||||
return debit - credit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal amount.
|
||||
* @param manualJournal
|
||||
* @returns {number}
|
||||
*/
|
||||
protected amount(manualJournal) {
|
||||
return Math.abs(this.total(manualJournal));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal formatted amount.
|
||||
* @param manualJournal
|
||||
* @returns {string}
|
||||
*/
|
||||
protected amountFormatted(manualJournal) {
|
||||
return this.formatNumber(manualJournal.amount, {
|
||||
currencyCode: manualJournal.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreives the manual journal date.
|
||||
* @param manualJournal
|
||||
* @returns {Date}
|
||||
*/
|
||||
protected date(manualJournal) {
|
||||
return manualJournal.date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal formatted date.
|
||||
* @param manualJournal
|
||||
* @returns {string}
|
||||
*/
|
||||
protected dateFormatted(manualJournal) {
|
||||
return this.formatDate(manualJournal.date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal transaction id.
|
||||
* @returns {number}
|
||||
*/
|
||||
protected transactionId(manualJournal) {
|
||||
return manualJournal.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal transaction number.
|
||||
* @param manualJournal
|
||||
*/
|
||||
protected transactionNo(manualJournal) {
|
||||
return manualJournal.journalNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal transaction type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionType() {
|
||||
return 'ManualJournal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal formatted transaction type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transsactionTypeFormatted() {
|
||||
return 'Manual Journal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal transaction normal (credit or debit).
|
||||
* @returns {string}
|
||||
*/
|
||||
protected transactionNormal(transaction) {
|
||||
const amount = this.total(transaction);
|
||||
|
||||
return amount >= 0 ? AccountNormal.DEBIT : AccountNormal.CREDIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the manual journal reference type.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected referenceType() {
|
||||
return 'ManualJournal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the manual journal reference id.
|
||||
* @param transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
protected referenceId(transaction) {
|
||||
return transaction.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as R from 'ramda';
|
||||
import * as moment from 'moment';
|
||||
import { first, sumBy } from 'lodash';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
GetMatchedTransactionsFilter,
|
||||
MatchedTransactionsPOJO,
|
||||
} from '../types';
|
||||
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
|
||||
import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills.service';
|
||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals.service';
|
||||
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
||||
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices.service';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { sortClosestMatchTransactions } from '../_utils';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetMatchedTransactions {
|
||||
constructor(
|
||||
private readonly getMatchedInvoicesService: GetMatchedTransactionsByInvoices,
|
||||
private readonly getMatchedBillsService: GetMatchedTransactionsByBills,
|
||||
private readonly getMatchedManualJournalService: GetMatchedTransactionsByManualJournals,
|
||||
private readonly getMatchedExpensesService: GetMatchedTransactionsByExpenses,
|
||||
private readonly getMatchedCashflowService: GetMatchedTransactionsByCashflow,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registered matched transactions types.
|
||||
*/
|
||||
get registered() {
|
||||
return [
|
||||
{ type: 'SaleInvoice', service: this.getMatchedInvoicesService },
|
||||
{ type: 'Bill', service: this.getMatchedBillsService },
|
||||
{ type: 'Expense', service: this.getMatchedExpensesService },
|
||||
{ type: 'ManualJournal', service: this.getMatchedManualJournalService },
|
||||
{ type: 'Cashflow', service: this.getMatchedCashflowService },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions.
|
||||
* @param {Array<number>} uncategorizedTransactionIds - Uncategorized transactions ids.
|
||||
* @param {GetMatchedTransactionsFilter} filter -
|
||||
* @returns {Promise<MatchedTransactionsPOJO>}
|
||||
*/
|
||||
public async getMatchedTransactions(
|
||||
uncategorizedTransactionIds: Array<number>,
|
||||
filter: GetMatchedTransactionsFilter,
|
||||
): Promise<MatchedTransactionsPOJO> {
|
||||
const uncategorizedTransactions =
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query()
|
||||
.whereIn('id', uncategorizedTransactionIds)
|
||||
.throwIfNotFound();
|
||||
|
||||
const totalPending = sumBy(uncategorizedTransactions, 'amount');
|
||||
|
||||
const filtered = filter.transactionType
|
||||
? this.registered.filter((item) => item.type === filter.transactionType)
|
||||
: this.registered;
|
||||
|
||||
const matchedTransactions = await PromisePool.withConcurrency(2)
|
||||
.for(filtered)
|
||||
.process(async ({ type, service }) => {
|
||||
return service.getMatchedTransactions(filter);
|
||||
});
|
||||
const { perfectMatches, possibleMatches } = this.groupMatchedResults(
|
||||
uncategorizedTransactions,
|
||||
matchedTransactions,
|
||||
);
|
||||
return {
|
||||
perfectMatches,
|
||||
possibleMatches,
|
||||
totalPending,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the given results for getting perfect and possible matches
|
||||
* based on the given uncategorized transaction.
|
||||
* @param uncategorizedTransaction
|
||||
* @param matchedTransactions
|
||||
* @returns {MatchedTransactionsPOJO}
|
||||
*/
|
||||
private groupMatchedResults(
|
||||
uncategorizedTransactions: Array<any>,
|
||||
matchedTransactions,
|
||||
): MatchedTransactionsPOJO {
|
||||
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
|
||||
const closestResullts = sortClosestMatchTransactions(amount, date, results);
|
||||
const perfectMatches = R.filter(
|
||||
(match) =>
|
||||
match.amount === amount && moment(match.date).isSame(date, 'day'),
|
||||
closestResullts,
|
||||
);
|
||||
const possibleMatches = R.difference(closestResullts, perfectMatches);
|
||||
const totalPending = sumBy(uncategorizedTransactions, 'amount');
|
||||
|
||||
return { perfectMatches, possibleMatches, totalPending };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { first } from 'lodash';
|
||||
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
||||
import {
|
||||
GetMatchedTransactionsFilter,
|
||||
IMatchTransactionDTO,
|
||||
MatchedTransactionPOJO,
|
||||
} from '../types';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { CreateBillPaymentService } from '@/modules/BillPayments/commands/CreateBillPayment.service';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { Bill } from '@/modules/Bills/models/Bill';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { CreateBillPaymentDto } from '@/modules/BillPayments/dtos/BillPayment.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType {
|
||||
constructor(
|
||||
private readonly createPaymentMadeService: CreateBillPaymentService,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(Bill.name)
|
||||
private readonly billModel: TenantModelProxy<typeof Bill>,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {GetMatchedTransactionsFilter} filter -
|
||||
*/
|
||||
public async getMatchedTransactions(filter: GetMatchedTransactionsFilter) {
|
||||
// Retrieves the bill matches.
|
||||
const bills = await this.billModel()
|
||||
.query()
|
||||
.onBuild((q) => {
|
||||
q.withGraphJoined('matchedBankTransaction');
|
||||
q.whereNull('matchedBankTransaction.id');
|
||||
q.modify('published');
|
||||
|
||||
if (filter.fromDate) {
|
||||
q.where('billDate', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
q.where('billDate', '<=', filter.toDate);
|
||||
}
|
||||
q.orderBy('billDate', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
bills,
|
||||
new GetMatchedTransactionBillsTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given bill matched transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} transactionId
|
||||
* @returns {Promise<MatchedTransactionPOJO>}
|
||||
*/
|
||||
public async getMatchedTransaction(
|
||||
transactionId: number,
|
||||
): Promise<MatchedTransactionPOJO> {
|
||||
const bill = await this.billModel()
|
||||
.query()
|
||||
.findById(transactionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
bill,
|
||||
new GetMatchedTransactionBillsTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the common matched transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {Array<number>} uncategorizedTransactionIds
|
||||
* @param {IMatchTransactionDTO} matchTransactionDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async createMatchedTransaction(
|
||||
uncategorizedTransactionIds: Array<number>,
|
||||
matchTransactionDTO: IMatchTransactionDTO,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
await super.createMatchedTransaction(
|
||||
uncategorizedTransactionIds,
|
||||
matchTransactionDTO,
|
||||
trx,
|
||||
);
|
||||
const uncategorizedTransactionId = first(uncategorizedTransactionIds);
|
||||
const uncategorizedTransaction =
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query(trx)
|
||||
.findById(uncategorizedTransactionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const bill = await this.billModel()
|
||||
.query(trx)
|
||||
.findById(matchTransactionDTO.referenceId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const createPaymentMadeDTO: CreateBillPaymentDto = {
|
||||
vendorId: bill.vendorId,
|
||||
paymentAccountId: uncategorizedTransaction.accountId,
|
||||
paymentDate: uncategorizedTransaction.date,
|
||||
exchangeRate: 1,
|
||||
entries: [
|
||||
{
|
||||
paymentAmount: bill.dueAmount,
|
||||
billId: bill.id,
|
||||
},
|
||||
],
|
||||
branchId: bill.branchId,
|
||||
};
|
||||
// Create a new bill payment associated to the matched bill.
|
||||
const billPayment = await this.createPaymentMadeService.createBillPayment(
|
||||
createPaymentMadeDTO,
|
||||
trx,
|
||||
);
|
||||
// Link the create bill payment with matched transaction.
|
||||
await super.createMatchedTransaction(
|
||||
uncategorizedTransactionIds,
|
||||
{
|
||||
referenceType: 'BillPayment',
|
||||
referenceId: billPayment.id,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
||||
import { GetMatchedTransactionsFilter } from '../types';
|
||||
import { BankTransaction } from '@/modules/BankingTransactions/models/BankTransaction';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetMatchedTransactionsByCashflow extends GetMatchedTransactionsByType {
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(BankTransaction.name)
|
||||
private readonly bankTransactionModel: TenantModelProxy<
|
||||
typeof BankTransaction
|
||||
>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the matched transactions of cash flow.
|
||||
* @param {number} tenantId
|
||||
* @param {GetMatchedTransactionsFilter} filter
|
||||
* @returns
|
||||
*/
|
||||
async getMatchedTransactions(
|
||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>,
|
||||
) {
|
||||
const transactions = await this.bankTransactionModel()
|
||||
.query()
|
||||
.onBuild((q) => {
|
||||
// Not matched to bank transaction.
|
||||
q.withGraphJoined('matchedBankTransaction');
|
||||
q.whereNull('matchedBankTransaction.id');
|
||||
|
||||
// Not categorized.
|
||||
q.modify('notCategorized');
|
||||
|
||||
// Published.
|
||||
q.modify('published');
|
||||
|
||||
if (filter.fromDate) {
|
||||
q.where('date', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
q.where('date', '<=', filter.toDate);
|
||||
}
|
||||
q.orderBy('date', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
transactions,
|
||||
new GetMatchedTransactionCashflowTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transaction of cash flow.
|
||||
* @param {number} tenantId
|
||||
* @param {number} transactionId
|
||||
* @returns
|
||||
*/
|
||||
async getMatchedTransaction(transactionId: number) {
|
||||
const transactions = await this.bankTransactionModel()
|
||||
.query()
|
||||
.findById(transactionId)
|
||||
.withGraphJoined('matchedBankTransaction')
|
||||
.whereNull('matchedBankTransaction.id')
|
||||
.modify('notCategorized')
|
||||
.modify('published')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
transactions,
|
||||
new GetMatchedTransactionCashflowTransformer(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from '../types';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { GetMatchedTransactionExpensesTransformer } from './GetMatchedTransactionExpensesTransformer';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { Expense } from '@/modules/Expenses/models/Expense.model';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByType {
|
||||
constructor(
|
||||
protected readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(Expense.name)
|
||||
protected readonly expenseModel: TenantModelProxy<typeof Expense>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions of expenses.
|
||||
* @param {number} tenantId
|
||||
* @param {GetMatchedTransactionsFilter} filter
|
||||
* @returns
|
||||
*/
|
||||
async getMatchedTransactions(filter: GetMatchedTransactionsFilter) {
|
||||
// Retrieve the expense matches.
|
||||
const expenses = await this.expenseModel()
|
||||
.query()
|
||||
.onBuild((query) => {
|
||||
// Filter out the not matched to bank transactions.
|
||||
query.withGraphJoined('matchedBankTransaction');
|
||||
query.whereNull('matchedBankTransaction.id');
|
||||
|
||||
// Filter the published onyl
|
||||
query.modify('filterByPublished');
|
||||
|
||||
if (filter.fromDate) {
|
||||
query.where('paymentDate', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
query.where('paymentDate', '<=', filter.toDate);
|
||||
}
|
||||
if (filter.minAmount) {
|
||||
query.where('totalAmount', '>=', filter.minAmount);
|
||||
}
|
||||
if (filter.maxAmount) {
|
||||
query.where('totalAmount', '<=', filter.maxAmount);
|
||||
}
|
||||
query.orderBy('paymentDate', 'DESC');
|
||||
});
|
||||
return this.transformer.transform(
|
||||
expenses,
|
||||
new GetMatchedTransactionExpensesTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given matched expense transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} transactionId
|
||||
* @returns {GetMatchedTransactionExpensesTransformer-}
|
||||
*/
|
||||
public async getMatchedTransaction(
|
||||
transactionId: number,
|
||||
): Promise<MatchedTransactionPOJO> {
|
||||
const expense = await this.expenseModel()
|
||||
.query()
|
||||
.findById(transactionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
expense,
|
||||
new GetMatchedTransactionExpensesTransformer(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Knex } from 'knex';
|
||||
import { first } from 'lodash';
|
||||
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
||||
import {
|
||||
GetMatchedTransactionsFilter,
|
||||
IMatchTransactionDTO,
|
||||
MatchedTransactionPOJO,
|
||||
MatchedTransactionsPOJO,
|
||||
} from '../types';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { CreatePaymentReceivedService } from '@/modules/PaymentReceived/commands/CreatePaymentReceived.serivce';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { IPaymentReceivedCreateDTO } from '@/modules/PaymentReceived/types/PaymentReceived.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByType {
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
private readonly createPaymentReceivedService: CreatePaymentReceivedService,
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions.
|
||||
* @param {GetMatchedTransactionsFilter} filter -
|
||||
* @returns {Promise<MatchedTransactionsPOJO>}
|
||||
*/
|
||||
public async getMatchedTransactions(
|
||||
filter: GetMatchedTransactionsFilter,
|
||||
): Promise<MatchedTransactionsPOJO> {
|
||||
// Retrieve the invoices that not matched, unpaid.
|
||||
const invoices = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.onBuild((q) => {
|
||||
q.withGraphJoined('matchedBankTransaction');
|
||||
q.whereNull('matchedBankTransaction.id');
|
||||
q.modify('unpaid');
|
||||
q.modify('published');
|
||||
|
||||
if (filter.fromDate) {
|
||||
q.where('invoiceDate', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
q.where('invoiceDate', '<=', filter.toDate);
|
||||
}
|
||||
q.orderBy('invoiceDate', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
invoices,
|
||||
new GetMatchedTransactionInvoicesTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} transactionId
|
||||
* @returns {Promise<MatchedTransactionPOJO>}
|
||||
*/
|
||||
public async getMatchedTransaction(
|
||||
transactionId: number,
|
||||
): Promise<MatchedTransactionPOJO> {
|
||||
const invoice = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.findById(transactionId);
|
||||
|
||||
return this.transformer.transform(
|
||||
invoice,
|
||||
new GetMatchedTransactionInvoicesTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the common matched transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {Array<number>} uncategorizedTransactionIds
|
||||
* @param {IMatchTransactionDTO} matchTransactionDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async createMatchedTransaction(
|
||||
uncategorizedTransactionIds: Array<number>,
|
||||
matchTransactionDTO: IMatchTransactionDTO,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
await super.createMatchedTransaction(
|
||||
uncategorizedTransactionIds,
|
||||
matchTransactionDTO,
|
||||
trx,
|
||||
);
|
||||
const uncategorizedTransactionId = first(uncategorizedTransactionIds);
|
||||
const uncategorizedTransaction =
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query(trx)
|
||||
.findById(uncategorizedTransactionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const invoice = await SaleInvoice.query(trx)
|
||||
.findById(matchTransactionDTO.referenceId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const createPaymentReceivedDTO: IPaymentReceivedCreateDTO = {
|
||||
customerId: invoice.customerId,
|
||||
paymentDate: uncategorizedTransaction.date,
|
||||
amount: invoice.dueAmount,
|
||||
depositAccountId: uncategorizedTransaction.accountId,
|
||||
entries: [
|
||||
{
|
||||
index: 1,
|
||||
invoiceId: invoice.id,
|
||||
paymentAmount: invoice.dueAmount,
|
||||
},
|
||||
],
|
||||
branchId: invoice.branchId,
|
||||
};
|
||||
// Create a payment received associated to the matched invoice.
|
||||
const paymentReceived =
|
||||
await this.createPaymentReceivedService.createPaymentReceived(
|
||||
createPaymentReceivedDTO,
|
||||
trx,
|
||||
);
|
||||
// Link the create payment received with matched invoice transaction.
|
||||
await super.createMatchedTransaction(
|
||||
uncategorizedTransactionIds,
|
||||
{
|
||||
referenceType: 'PaymentReceive',
|
||||
referenceId: paymentReceived.id,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { GetMatchedTransactionsFilter } from '../types';
|
||||
import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactionsByType {
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(ManualJournal.name)
|
||||
private readonly manualJournalModel: TenantModelProxy<typeof ManualJournal>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the matched transactions of manual journals.
|
||||
* @param {GetMatchedTransactionsFilter} filter
|
||||
* @returns
|
||||
*/
|
||||
async getMatchedTransactions(
|
||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>,
|
||||
) {
|
||||
// @todo: get the account id from the filter
|
||||
const accountId = 1000;
|
||||
|
||||
const manualJournals = await this.manualJournalModel()
|
||||
.query()
|
||||
.onBuild((query) => {
|
||||
query.withGraphJoined('matchedBankTransaction');
|
||||
query.whereNull('matchedBankTransaction.id');
|
||||
|
||||
query.withGraphJoined('entries');
|
||||
query.where('entries.accountId', accountId);
|
||||
query.modify('filterByPublished');
|
||||
|
||||
if (filter.fromDate) {
|
||||
query.where('date', '>=', filter.fromDate);
|
||||
}
|
||||
if (filter.toDate) {
|
||||
query.where('date', '<=', filter.toDate);
|
||||
}
|
||||
if (filter.minAmount) {
|
||||
query.where('amount', '>=', filter.minAmount);
|
||||
}
|
||||
if (filter.maxAmount) {
|
||||
query.where('amount', '<=', filter.maxAmount);
|
||||
}
|
||||
});
|
||||
return this.transformer.transform(
|
||||
manualJournals,
|
||||
new GetMatchedTransactionManualJournalsTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transaction of manual journals.
|
||||
* @param {number} tenantId
|
||||
* @param {number} transactionId
|
||||
* @returns
|
||||
*/
|
||||
public async getMatchedTransaction(transactionId: number) {
|
||||
const manualJournal = await this.manualJournalModel()
|
||||
.query()
|
||||
.findById(transactionId)
|
||||
.whereNotExists(ManualJournal.relatedQuery('matchedBankTransaction'))
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
manualJournal,
|
||||
new GetMatchedTransactionManualJournalsTransformer(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
GetMatchedTransactionsFilter,
|
||||
IMatchTransactionDTO,
|
||||
MatchedTransactionPOJO,
|
||||
MatchedTransactionsPOJO,
|
||||
} from '../types';
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
import { MatchedBankTransaction } from '../models/MatchedBankTransaction';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
export abstract class GetMatchedTransactionsByType {
|
||||
@Inject(MatchedBankTransaction.name)
|
||||
private readonly matchedBankTransactionModel: TenantModelProxy<
|
||||
typeof MatchedBankTransaction
|
||||
>;
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {GetMatchedTransactionsFilter} filter -
|
||||
* @returns {Promise<MatchedTransactionsPOJO>}
|
||||
*/
|
||||
public async getMatchedTransactions(
|
||||
filter: GetMatchedTransactionsFilter,
|
||||
): Promise<MatchedTransactionsPOJO> {
|
||||
throw new Error(
|
||||
'The `getMatchedTransactions` method is not defined for the transaction type.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transaction details.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} transactionId -
|
||||
* @returns {Promise<MatchedTransactionPOJO>}
|
||||
*/
|
||||
public async getMatchedTransaction(
|
||||
transactionId: number,
|
||||
): Promise<MatchedTransactionPOJO> {
|
||||
throw new Error(
|
||||
'The `getMatchedTransaction` method is not defined for the transaction type.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the common matched transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {Array<number>} uncategorizedTransactionIds
|
||||
* @param {IMatchTransactionDTO} matchTransactionDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async createMatchedTransaction(
|
||||
uncategorizedTransactionIds: Array<number>,
|
||||
matchTransactionDTO: IMatchTransactionDTO,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
await PromisePool.withConcurrency(2)
|
||||
.for(uncategorizedTransactionIds)
|
||||
.process(async (uncategorizedTransactionId) => {
|
||||
await this.matchedBankTransactionModel().query(trx).insert({
|
||||
uncategorizedTransactionId,
|
||||
referenceType: matchTransactionDTO.referenceType,
|
||||
referenceId: matchTransactionDTO.referenceId,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
73
packages/server/src/modules/BankingMatching/types.ts
Normal file
73
packages/server/src/modules/BankingMatching/types.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export interface IBankTransactionMatchingEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionIds: Array<number>;
|
||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IBankTransactionMatchedEventPayload {
|
||||
// tenantId: number;
|
||||
uncategorizedTransactionIds: Array<number>;
|
||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IBankTransactionUnmatchingEventPayload {
|
||||
// tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IBankTransactionUnmatchedEventPayload {
|
||||
// tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IMatchTransactionDTO {
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
}
|
||||
|
||||
export interface IMatchTransactionsDTO {
|
||||
uncategorizedTransactionIds: Array<number>;
|
||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||
}
|
||||
|
||||
export interface GetMatchedTransactionsFilter {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
minAmount: number;
|
||||
maxAmount: number;
|
||||
transactionType: string;
|
||||
}
|
||||
|
||||
export interface MatchedTransactionPOJO {
|
||||
amount: number;
|
||||
amountFormatted: string;
|
||||
date: string;
|
||||
dateFormatted: string;
|
||||
referenceNo: string;
|
||||
transactionNo: string;
|
||||
transactionId: number;
|
||||
transactionType: string;
|
||||
}
|
||||
|
||||
export type MatchedTransactionsPOJO = {
|
||||
perfectMatches: Array<MatchedTransactionPOJO>;
|
||||
possibleMatches: Array<MatchedTransactionPOJO>;
|
||||
totalPending: number;
|
||||
};
|
||||
|
||||
export const ERRORS = {
|
||||
RESOURCE_TYPE_MATCHING_TRANSACTION_INVALID:
|
||||
'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',
|
||||
};
|
||||
Reference in New Issue
Block a user