feat: match bank transaction

This commit is contained in:
Ahmed Bouhuolia
2024-06-20 23:31:46 +02:00
parent b37002bea6
commit 738a84bb4b
20 changed files with 492 additions and 55 deletions

View File

@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express'; import { NextFunction, Request, Response, Router } from 'express';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication'; import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
import { param } from 'express-validator'; import { body, param } from 'express-validator';
import { import {
GetMatchedTransactionsFilter, GetMatchedTransactionsFilter,
IMatchTransactionDTO, IMatchTransactionDTO,
@@ -21,7 +21,14 @@ export class BankTransactionsMatchingController extends BaseController {
router.post( router.post(
'/:transactionId', '/:transactionId',
[param('transactionId').exists()], [
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.validationResult,
this.matchBankTransaction.bind(this) this.matchBankTransaction.bind(this)
); );
@@ -60,7 +67,6 @@ export class BankTransactionsMatchingController extends BaseController {
transactionId, transactionId,
matchTransactionDTO matchTransactionDTO
); );
return res.status(200).send({ return res.status(200).send({
message: 'The bank transaction has been matched.', message: 'The bank transaction has been matched.',
}); });

View File

@@ -7,7 +7,7 @@ import { ExcludeBankTransactionsApplication } from '@/services/Banking/Exclude/E
@Service() @Service()
export class ExcludeBankTransactionsController extends BaseController { export class ExcludeBankTransactionsController extends BaseController {
@Inject() @Inject()
prviate excludeBankTransactionApp: ExcludeBankTransactionsApplication; private excludeBankTransactionApp: ExcludeBankTransactionsApplication;
/** /**
* Router constructor. * Router constructor.

View File

@@ -0,0 +1,30 @@
import { body } from 'express-validator';
import BaseController from '../BaseController';
import { Router } from 'express';
import { Service } from 'typedi';
@Service()
export class BankReconcileController extends BaseController {
/**
* Router constructor.
*/
public router() {
const router = Router();
router.post(
'/',
[
body('amount').exists(),
body('date').exists(),
body('fromAccountId').exists(),
body('toAccountId').exists(),
body('reference').optional({ nullable: true }),
],
this.validationResult,
this.createBankReconcileTransaction.bind(this)
);
return router;
}
createBankReconcileTransaction() {}
}

View File

@@ -404,6 +404,7 @@ export default class Bill extends mixin(TenantModel, [
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const TaxRateTransaction = require('models/TaxRateTransaction'); const TaxRateTransaction = require('models/TaxRateTransaction');
const Document = require('models/Document'); const Document = require('models/Document');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
return { return {
vendor: { vendor: {
@@ -485,6 +486,21 @@ export default class Bill extends mixin(TenantModel, [
query.where('model_ref', 'Bill'); query.where('model_ref', 'Bill');
}, },
}, },
/**
* Bill may belongs to matched bank transaction.
*/
matchedBankTransaction: {
relation: Model.HasManyRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'bills.id',
to: 'matched_bank_transactions.referenceId',
},
filter(query) {
query.where('reference_type', 'Bill');
},
},
}; };
} }

View File

@@ -102,6 +102,7 @@ export default class CashflowTransaction extends TenantModel {
const CashflowTransactionLine = require('models/CashflowTransactionLine'); const CashflowTransactionLine = require('models/CashflowTransactionLine');
const AccountTransaction = require('models/AccountTransaction'); const AccountTransaction = require('models/AccountTransaction');
const Account = require('models/Account'); const Account = require('models/Account');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
return { return {
/** /**
@@ -158,6 +159,22 @@ export default class CashflowTransaction extends TenantModel {
to: 'accounts.id', to: 'accounts.id',
}, },
}, },
/**
* Cashflow transaction may belongs to matched bank transaction.
*/
matchedBankTransaction: {
relation: Model.HasManyRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'cashflow_transactions.id',
to: 'matched_bank_transactions.referenceId',
},
filter: (query) => {
const referenceTypes = getCashflowAccountTransactionsTypes();
query.whereIn('reference_type', referenceTypes);
},
},
}; };
} }
} }

View File

@@ -182,6 +182,7 @@ export default class Expense extends mixin(TenantModel, [
const ExpenseCategory = require('models/ExpenseCategory'); const ExpenseCategory = require('models/ExpenseCategory');
const Document = require('models/Document'); const Document = require('models/Document');
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
return { return {
paymentAccount: { paymentAccount: {
@@ -234,6 +235,21 @@ export default class Expense extends mixin(TenantModel, [
query.where('model_ref', 'Expense'); query.where('model_ref', 'Expense');
}, },
}, },
/**
* Expense may belongs to matched bank transaction.
*/
matchedBankTransaction: {
relation: Model.HasManyRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'expenses_transactions.id',
to: 'matched_bank_transactions.referenceId',
},
filter(query) {
query.where('reference_type', 'Expense');
},
},
}; };
} }

View File

@@ -1,4 +1,5 @@
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { Model } from 'objection';
export class MatchedBankTransaction extends TenantModel { export class MatchedBankTransaction extends TenantModel {
/** /**
@@ -12,7 +13,7 @@ export class MatchedBankTransaction extends TenantModel {
* Timestamps columns. * Timestamps columns.
*/ */
get timestamps() { get timestamps() {
return []; return ['createdAt', 'updatedAt'];
} }
/** /**
@@ -21,4 +22,11 @@ export class MatchedBankTransaction extends TenantModel {
static get virtualAttributes() { static get virtualAttributes() {
return []; return [];
} }
/**
* Relationship mapping.
*/
static get relationMappings() {
return {};
}
} }

View File

@@ -411,6 +411,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
const Account = require('models/Account'); const Account = require('models/Account');
const TaxRateTransaction = require('models/TaxRateTransaction'); const TaxRateTransaction = require('models/TaxRateTransaction');
const Document = require('models/Document'); const Document = require('models/Document');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
return { return {
/** /**
@@ -543,6 +544,21 @@ export default class SaleInvoice extends mixin(TenantModel, [
query.where('model_ref', 'SaleInvoice'); query.where('model_ref', 'SaleInvoice');
}, },
}, },
/**
* Sale invocie may belongs to matched bank transaction.
*/
matchedBankTransaction: {
relation: Model.HasManyRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'sales_invoices.id',
to: "matched_bank_transactions.referenceId",
},
filter(query) {
query.where('reference_type', 'SaleInvoice');
},
},
}; };
} }

View File

@@ -97,6 +97,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
const { const {
RecognizedBankTransaction, RecognizedBankTransaction,
} = require('models/RecognizedBankTransaction'); } = require('models/RecognizedBankTransaction');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
return { return {
/** /**
@@ -122,6 +123,18 @@ export default class UncategorizedCashflowTransaction extends mixin(
to: 'recognized_bank_transactions.id', to: 'recognized_bank_transactions.id',
}, },
}, },
/**
* Uncategorized transaction may has association to matched transaction.
*/
matchedBankTransaction: {
relation: Model.BelongsToOneRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'uncategorized_cashflow_transactions.id',
to: 'matched_bank_transactions.uncategorizedTransactionId',
},
},
}; };
} }

View File

@@ -43,7 +43,7 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
* @returns {number} * @returns {number}
*/ */
protected amount(manualJournal) { protected amount(manualJournal) {
return manualJournal.totalAmount; return manualJournal.amount;
} }
/** /**
@@ -52,7 +52,7 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
* @returns {string} * @returns {string}
*/ */
protected amountFormatted(manualJournal) { protected amountFormatted(manualJournal) {
return this.formatNumber(manualJournal.totalAmount, { return this.formatNumber(manualJournal.amount, {
currencyCode: manualJournal.currencyCode, currencyCode: manualJournal.currencyCode,
}); });
} }

View File

@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import { PromisePool } from '@supercharge/promise-pool'; import { PromisePool } from '@supercharge/promise-pool';
import { GetMatchedTransactionsFilter } from './types'; import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types';
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses'; import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills'; import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals'; import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
@@ -20,6 +20,9 @@ export class GetMatchedTransactions {
@Inject() @Inject()
private getMatchedExpensesService: GetMatchedTransactionsByExpenses; private getMatchedExpensesService: GetMatchedTransactionsByExpenses;
/**
* Registered matched transactions types.
*/
get registered() { get registered() {
return [ return [
{ type: 'SaleInvoice', service: this.getMatchedInvoicesService }, { type: 'SaleInvoice', service: this.getMatchedInvoicesService },
@@ -37,7 +40,7 @@ export class GetMatchedTransactions {
public async getMatchedTransactions( public async getMatchedTransactions(
tenantId: number, tenantId: number,
filter: GetMatchedTransactionsFilter filter: GetMatchedTransactionsFilter
) { ): Promise<MatchedTransactionsPOJO> {
const filtered = filter.transactionType const filtered = filter.transactionType
? this.registered.filter((item) => item.type === filter.transactionType) ? this.registered.filter((item) => item.type === filter.transactionType)
: this.registered; : this.registered;
@@ -50,13 +53,3 @@ export class GetMatchedTransactions {
return R.compose(R.flatten)(matchedTransactions?.results); return R.compose(R.flatten)(matchedTransactions?.results);
} }
} }
interface MatchedTransaction {
amount: number;
amountFormatted: string;
date: string;
dateFormatted: string;
referenceNo: string;
transactionNo: string;
transactionId: number;
}

View File

@@ -1,12 +1,12 @@
import { Inject, Service } from 'typedi';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer'; import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
import { GetMatchedTransactionsFilter } from './types'; import { GetMatchedTransactionsFilter } from './types';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi'; import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
@Service() @Service()
export class GetMatchedTransactionsByBills { export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType {
@Inject() @Inject()
private tenancy: HasTenancyService; private tenancy: HasTenancyService;

View File

@@ -3,14 +3,15 @@ import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTran
import { GetMatchedTransactionsFilter } from './types'; import { GetMatchedTransactionsFilter } from './types';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
@Service() @Service()
export class GetMatchedTransactionsByExpenses { export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByType {
@Inject() @Inject()
private tenancy: HasTenancyService; protected tenancy: HasTenancyService;
@Inject() @Inject()
private transformer: TransformerInjectable; protected transformer: TransformerInjectable;
/** /**
* *

View File

@@ -1,16 +1,21 @@
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer'; import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
import { GetMatchedTransactionsFilter } from './types'; import {
GetMatchedTransactionsFilter,
MatchedTransactionPOJO,
MatchedTransactionsPOJO,
} from './types';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
@Service() @Service()
export class GetMatchedTransactionsByInvoices { export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByType {
@Inject() @Inject()
private tenancy: HasTenancyService; protected tenancy: HasTenancyService;
@Inject() @Inject()
private transformer: TransformerInjectable; protected transformer: TransformerInjectable;
/** /**
* Retrieves the matched transactions. * Retrieves the matched transactions.
@@ -20,7 +25,7 @@ export class GetMatchedTransactionsByInvoices {
public async getMatchedTransactions( public async getMatchedTransactions(
tenantId: number, tenantId: number,
filter: GetMatchedTransactionsFilter filter: GetMatchedTransactionsFilter
) { ): Promise<MatchedTransactionsPOJO> {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
const invoices = await SaleInvoice.query(); const invoices = await SaleInvoice.query();
@@ -31,4 +36,27 @@ export class GetMatchedTransactionsByInvoices {
new GetMatchedTransactionInvoicesTransformer() new GetMatchedTransactionInvoicesTransformer()
); );
} }
/**
*
* @param {number} tenantId
* @param {number} transactionId
* @returns
*/
public async getMatchedTransaction(
tenantId: number,
transactionId: number
): Promise<MatchedTransactionPOJO> {
const { SaleInvoice } = this.tenancy.models(tenantId);
console.log(transactionId);
const invoice = await SaleInvoice.query().findById(transactionId);
return this.transformer.transform(
tenantId,
invoice,
new GetMatchedTransactionInvoicesTransformer()
);
}
} }

View File

@@ -1,17 +1,20 @@
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer'; import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetMatchedTransactionsFilter } from './types'; import { GetMatchedTransactionsFilter } from './types';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
@Service() @Service()
export class GetMatchedTransactionsByManualJournals { export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactionsByType {
@Inject()
private tenancy: HasTenancyService;
@Inject() @Inject()
private transformer: TransformerInjectable; private transformer: TransformerInjectable;
/**
* Retrieve the matched transactions of manual journals.
* @param {number} tenantId
* @param {GetMatchedTransactionsFilter} filter
* @returns
*/
async getMatchedTransactions( async getMatchedTransactions(
tenantId: number, tenantId: number,
filter: GetMatchedTransactionsFilter filter: GetMatchedTransactionsFilter
@@ -26,4 +29,24 @@ export class GetMatchedTransactionsByManualJournals {
new GetMatchedTransactionManualJournalsTransformer() new GetMatchedTransactionManualJournalsTransformer()
); );
} }
/**
* Retrieves the matched transaction of manual journals.
* @param {number} tenantId
* @param {number} transactionId
* @returns
*/
async getMatchedTransaction(tenantId: number, transactionId: number) {
const { ManualJournal } = this.tenancy.models(tenantId);
const manualJournal = await ManualJournal.query()
.findById(transactionId)
.throwIfNotFound();
return this.transformer.transform(
tenantId,
manualJournal,
new GetMatchedTransactionManualJournalsTransformer()
);
}
} }

View File

@@ -0,0 +1,65 @@
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import {
GetMatchedTransactionsFilter,
IMatchTransactionDTO,
MatchedTransactionPOJO,
MatchedTransactionsPOJO,
} from './types';
import { Inject, Service } from 'typedi';
// @Service()
export abstract class GetMatchedTransactionsByType {
@Inject()
protected tenancy: HasTenancyService;
/**
* Retrieves the matched transactions.
* @param {number} tenantId -
* @param {GetMatchedTransactionsFilter} filter -
*/
public async getMatchedTransactions(
tenantId: number,
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 -
*/
public async getMatchedTransaction(
tenantId: number,
transactionId: number
): Promise<MatchedTransactionPOJO> {
throw new Error(
'The `getMatchedTransaction` method is not defined for the transaction type.'
);
}
/**
*
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @param {IMatchTransactionDTO} matchTransactionDTO
* @param {Knex.Transaction} trx
*/
public async createMatchedTransaction(
tenantId: number,
uncategorizedTransactionId: number,
matchTransactionDTO: IMatchTransactionDTO,
trx?: Knex.Transaction
) {
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
await MatchedBankTransaction.query(trx).insert({
uncategorizedTransactionId,
referenceType: matchTransactionDTO.referenceType,
referenceId: matchTransactionDTO.referenceId,
});
}
}

View File

@@ -1,3 +1,4 @@
import { sumBy } from 'lodash';
import { PromisePool } from '@supercharge/promise-pool'; import { PromisePool } from '@supercharge/promise-pool';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
@@ -6,10 +7,13 @@ import events from '@/subscribers/events';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { import {
ERRORS,
IBankTransactionMatchedEventPayload, IBankTransactionMatchedEventPayload,
IBankTransactionMatchingEventPayload, IBankTransactionMatchingEventPayload,
IMatchTransactionDTO, IMatchTransactionsDTO,
} from './types'; } from './types';
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
import { ServiceError } from '@/exceptions';
@Service() @Service()
export class MatchBankTransactions { export class MatchBankTransactions {
@@ -22,22 +26,90 @@ export class MatchBankTransactions {
@Inject() @Inject()
private eventPublisher: EventPublisher; private eventPublisher: EventPublisher;
@Inject()
private matchedBankTransactions: MatchTransactionsTypes;
/**
* Validates the match bank transactions DTO.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @param {IMatchTransactionsDTO} matchTransactionsDTO
*/
async validate(
tenantId: number,
uncategorizedTransactionId: number,
matchTransactionsDTO: IMatchTransactionsDTO
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const { matchedTransactions } = matchTransactionsDTO;
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query()
.findById(uncategorizedTransactionId)
.throwIfNotFound();
// 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(
tenantId,
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 = sumBy(
validatationResult.results,
'amount'
);
// Validates the total given matching transcations whether is not equal
// uncategorized transaction amount.
if (totalMatchedTranasctions !== uncategorizedTransaction.amount) {
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
}
}
/** /**
* Matches the given uncategorized transaction to the given references. * Matches the given uncategorized transaction to the given references.
* @param {number} tenantId * @param {number} tenantId
* @param {number} uncategorizedTransactionId * @param {number} uncategorizedTransactionId
*/ */
public matchTransaction( public async matchTransaction(
tenantId: number, tenantId: number,
uncategorizedTransactionId: number, uncategorizedTransactionId: number,
matchTransactionsDTO: IMatchTransactionDTO matchTransactionsDTO: IMatchTransactionsDTO
) { ) {
const { matchedTransactions } = matchTransactionsDTO; const { matchedTransactions } = matchTransactionsDTO;
const { MatchBankTransaction } = this.tenancy.models(tenantId);
// // Validates the given matching transactions DTO.
await this.validate(
tenantId,
uncategorizedTransactionId,
matchTransactionsDTO
);
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers the event `onSaleInvoiceCreated`. // Triggers the event `onBankTransactionMatching`.
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, { await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
tenantId, tenantId,
uncategorizedTransactionId, uncategorizedTransactionId,
@@ -45,19 +117,23 @@ export class MatchBankTransactions {
trx, trx,
} as IBankTransactionMatchingEventPayload); } as IBankTransactionMatchingEventPayload);
// Matches the given transactions under promise pool concurrency controlling. // Matches the given transactions under promise pool concurrency controlling.
await PromisePool.withConcurrency(10) await PromisePool.withConcurrency(10)
.for(matchedTransactions) .for(matchedTransactions)
.process(async (matchedTransaction) => { .process(async (matchedTransaction) => {
await MatchBankTransaction.query(trx).insert({ const getMatchedTransactionsService =
this.matchedBankTransactions.registry.get(
matchedTransaction.referenceType
);
await getMatchedTransactionsService.createMatchedTransaction(
tenantId,
uncategorizedTransactionId, uncategorizedTransactionId,
referenceType: matchedTransaction.referenceType, matchedTransaction,
referenceId: matchedTransaction.referenceId, trx
amount: matchedTransaction.amount, );
});
}); });
// Triggers the event `onSaleInvoiceCreated`. // Triggers the event `onBankTransactionMatched`.
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, { await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
tenantId, tenantId,
uncategorizedTransactionId, uncategorizedTransactionId,

View File

@@ -0,0 +1,57 @@
import Container, { Service } from 'typedi';
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
@Service()
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,
},
];
}
/**
* 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;
}
}
}

View File

@@ -0,0 +1,50 @@
import { camelCase, upperFirst } from 'lodash';
import { GetMatchedTransactionsByType } from './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));
}
}

View File

@@ -3,14 +3,14 @@ import { Knex } from 'knex';
export interface IBankTransactionMatchingEventPayload { export interface IBankTransactionMatchingEventPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionId: number; uncategorizedTransactionId: number;
matchTransactionsDTO: IMatchTransactionDTO; matchTransactionsDTO: IMatchTransactionsDTO;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }
export interface IBankTransactionMatchedEventPayload { export interface IBankTransactionMatchedEventPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionId: number; uncategorizedTransactionId: number;
matchTransactionsDTO: IMatchTransactionDTO; matchTransactionsDTO: IMatchTransactionsDTO;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }
@@ -23,11 +23,12 @@ export interface IBankTransactionUnmatchedEventPayload {
} }
export interface IMatchTransactionDTO { export interface IMatchTransactionDTO {
matchedTransactions: Array<{ referenceType: string;
referenceType: string; referenceId: number;
referenceId: number; }
amount: number;
}>; export interface IMatchTransactionsDTO {
matchedTransactions: Array<IMatchTransactionDTO>;
} }
export interface GetMatchedTransactionsFilter { export interface GetMatchedTransactionsFilter {
@@ -37,3 +38,24 @@ export interface GetMatchedTransactionsFilter {
maxAmount: number; maxAmount: number;
transactionType: string; transactionType: string;
} }
export interface MatchedTransactionPOJO {
amount: number;
amountFormatted: string;
date: string;
dateFormatted: string;
referenceNo: string;
transactionNo: string;
transactionId: number;
}
export type MatchedTransactionsPOJO = Array<MatchedTransactionPOJO[]>;
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',
};