mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
feat: match bank transaction
This commit is contained in:
@@ -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.',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user