mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat: matching uncategorized transactions
This commit is contained in:
@@ -0,0 +1,129 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { NextFunction, Request, Response, Router } from 'express';
|
||||||
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
|
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
|
||||||
|
import { param } from 'express-validator';
|
||||||
|
import {
|
||||||
|
GetMatchedTransactionsFilter,
|
||||||
|
IMatchTransactionDTO,
|
||||||
|
} from '@/services/Banking/Matching/types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class BankTransactionsMatchingController extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
private bankTransactionsMatchingApp: MatchBankTransactionsApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
public router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:transactionId',
|
||||||
|
[param('transactionId').exists()],
|
||||||
|
this.validationResult,
|
||||||
|
this.matchBankTransaction.bind(this)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/unmatch/:transactionId',
|
||||||
|
[param('transactionId').exists()],
|
||||||
|
this.validationResult,
|
||||||
|
this.unmatchMatchedBankTransaction.bind(this)
|
||||||
|
);
|
||||||
|
router.get('/', this.getMatchedTransactions.bind(this));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the given bank transaction.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private async matchBankTransaction(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { transactionId } = req.params;
|
||||||
|
const matchTransactionDTO = this.matchedBodyData(
|
||||||
|
req
|
||||||
|
) as IMatchTransactionDTO;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.bankTransactionsMatchingApp.matchTransaction(
|
||||||
|
tenantId,
|
||||||
|
transactionId,
|
||||||
|
matchTransactionDTO
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'The bank transaction has been matched.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private async unmatchMatchedBankTransaction(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const transactionId = req.params?.transactionId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.bankTransactionsMatchingApp.unmatchMatchedTransaction(
|
||||||
|
tenantId,
|
||||||
|
transactionId
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'The bank matched transaction has been unmatched.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the matched transactions.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private async getMatchedTransactions(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
|
||||||
|
|
||||||
|
console.log('test');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const matchedTransactions =
|
||||||
|
await this.bankTransactionsMatchingApp.getMatchedTransactions(
|
||||||
|
tenantId,
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({ data: matchedTransactions });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Router } from 'express';
|
|||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
import { PlaidBankingController } from './PlaidBankingController';
|
import { PlaidBankingController } from './PlaidBankingController';
|
||||||
import { BankingRulesController } from './BankingRulesController';
|
import { BankingRulesController } from './BankingRulesController';
|
||||||
|
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankingController extends BaseController {
|
export class BankingController extends BaseController {
|
||||||
@@ -14,6 +15,10 @@ export class BankingController extends BaseController {
|
|||||||
|
|
||||||
router.use('/plaid', Container.get(PlaidBankingController).router());
|
router.use('/plaid', Container.get(PlaidBankingController).router());
|
||||||
router.use('/rules', Container.get(BankingRulesController).router());
|
router.use('/rules', Container.get(BankingRulesController).router());
|
||||||
|
router.use(
|
||||||
|
'/matches',
|
||||||
|
Container.get(BankTransactionsMatchingController).router()
|
||||||
|
);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
||||||
|
table.increments('id');
|
||||||
|
table.integer('uncategorized_transaction_id').unsigned();
|
||||||
|
table.string('reference_type');
|
||||||
|
table.integer('reference_id');
|
||||||
|
table.decimal('amount');
|
||||||
|
table.timestamps();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.dropTableIfExists('matched_bank_transactions');
|
||||||
|
};
|
||||||
@@ -67,6 +67,7 @@ import DocumentLink from '@/models/DocumentLink';
|
|||||||
import { BankRule } from '@/models/BankRule';
|
import { BankRule } from '@/models/BankRule';
|
||||||
import { BankRuleCondition } from '@/models/BankRuleCondition';
|
import { BankRuleCondition } from '@/models/BankRuleCondition';
|
||||||
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
|
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
|
||||||
|
import { MatchedBankTransaction } from '@/models/MatchedBankTransaction';
|
||||||
|
|
||||||
export default (knex) => {
|
export default (knex) => {
|
||||||
const models = {
|
const models = {
|
||||||
@@ -137,6 +138,7 @@ export default (knex) => {
|
|||||||
BankRule,
|
BankRule,
|
||||||
BankRuleCondition,
|
BankRuleCondition,
|
||||||
RecognizedBankTransaction,
|
RecognizedBankTransaction,
|
||||||
|
MatchedBankTransaction,
|
||||||
};
|
};
|
||||||
return mapValues(models, (model) => model.bindKnex(knex));
|
return mapValues(models, (model) => model.bindKnex(knex));
|
||||||
};
|
};
|
||||||
|
|||||||
24
packages/server/src/models/MatchedBankTransaction.ts
Normal file
24
packages/server/src/models/MatchedBankTransaction.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
|
export class MatchedBankTransaction extends TenantModel {
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'matched_bank_transactions';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
|
||||||
|
export class GetMatchedTransactionBillsTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale credit note object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['referenceNo'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['*'];
|
||||||
|
};
|
||||||
|
|
||||||
|
protected referenceNo(invoice) {
|
||||||
|
return invoice.referenceNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
amount(invoice) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
amountFormatted() {
|
||||||
|
|
||||||
|
}
|
||||||
|
date() {
|
||||||
|
|
||||||
|
}
|
||||||
|
dateFromatted() {
|
||||||
|
|
||||||
|
}
|
||||||
|
transactionId(invoice) {
|
||||||
|
return invoice.id;
|
||||||
|
}
|
||||||
|
transactionNo() {
|
||||||
|
|
||||||
|
}
|
||||||
|
transactionType() {}
|
||||||
|
transsactionTypeFormatted() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
|
||||||
|
export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale credit note object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['referenceNo'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['*'];
|
||||||
|
};
|
||||||
|
|
||||||
|
protected referenceNo(invoice) {
|
||||||
|
return invoice.referenceNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
amount(invoice) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
amountFormatted() {}
|
||||||
|
date() {}
|
||||||
|
dateFromatted() {}
|
||||||
|
transactionId(invoice) {
|
||||||
|
return invoice.id;
|
||||||
|
}
|
||||||
|
transactionNo() {}
|
||||||
|
transactionType() {}
|
||||||
|
transsactionTypeFormatted() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
|
||||||
|
export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale credit note object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['referenceNo', 'transactionNo'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['*'];
|
||||||
|
};
|
||||||
|
|
||||||
|
protected referenceNo(invoice) {
|
||||||
|
return invoice.referenceNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected transactionNo(invoice) {
|
||||||
|
return invoice.invoiceNo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
|
||||||
|
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale credit note object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['referenceNo'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['*'];
|
||||||
|
};
|
||||||
|
|
||||||
|
protected referenceNo(invoice) {
|
||||||
|
return invoice.referenceNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
amount(invoice) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
amountFormatted() {}
|
||||||
|
date() {}
|
||||||
|
dateFromatted() {}
|
||||||
|
transactionId(invoice) {
|
||||||
|
return invoice.id;
|
||||||
|
}
|
||||||
|
transactionNo() {}
|
||||||
|
transactionType() {}
|
||||||
|
transsactionTypeFormatted() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { GetMatchedTransactionsFilter } from './types';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
||||||
|
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
||||||
|
import { GetMatchedTransactionExpensesTransformer } from './GetMatchedTransactionExpensesTransformer';
|
||||||
|
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GetMatchedTransactions {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the matched transactions.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {GetMatchedTransactionsFilter} filter -
|
||||||
|
*/
|
||||||
|
public async getMatchedTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
filter: GetMatchedTransactionsFilter
|
||||||
|
) {
|
||||||
|
const registered = [
|
||||||
|
{
|
||||||
|
type: 'SaleInvoice',
|
||||||
|
callback: this.getSaleInvoicesMatchedTransactions.bind(this),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Bill',
|
||||||
|
callback: this.getBillsMatchedTransactions.bind(this),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Expense',
|
||||||
|
callback: this.getExpensesMatchedTransactions.bind(this),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'ManualJournal',
|
||||||
|
callback: this.getManualJournalsMatchedTransactions.bind(this),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const filtered = filter.transactionType
|
||||||
|
? registered.filter((item) => item.type === filter.transactionType)
|
||||||
|
: registered;
|
||||||
|
|
||||||
|
const matchedTransactions = await PromisePool.withConcurrency(2)
|
||||||
|
.for(filtered)
|
||||||
|
.process(async ({ type, callback }) => {
|
||||||
|
return callback(tenantId, filter);
|
||||||
|
});
|
||||||
|
return R.compose(R.flatten)(matchedTransactions?.results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {GetMatchedTransactionsFilter} filter -
|
||||||
|
*/
|
||||||
|
async getSaleInvoicesMatchedTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
filter: GetMatchedTransactionsFilter
|
||||||
|
) {
|
||||||
|
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const invoices = await SaleInvoice.query();
|
||||||
|
|
||||||
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
invoices,
|
||||||
|
new GetMatchedTransactionInvoicesTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {GetMatchedTransactionsFilter} filter -
|
||||||
|
*/
|
||||||
|
async getBillsMatchedTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
filter: GetMatchedTransactionsFilter
|
||||||
|
) {
|
||||||
|
const { Bill } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const bills = await Bill.query();
|
||||||
|
|
||||||
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
bills,
|
||||||
|
new GetMatchedTransactionBillsTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {GetMatchedTransactionsFilter} filter
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getExpensesMatchedTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
filter: GetMatchedTransactionsFilter
|
||||||
|
) {
|
||||||
|
const { Expense } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const expenses = await Expense.query();
|
||||||
|
|
||||||
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
expenses,
|
||||||
|
new GetMatchedTransactionManualJournalsTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getManualJournalsMatchedTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
filter: GetMatchedTransactionsFilter
|
||||||
|
) {
|
||||||
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const manualJournals = await ManualJournal.query();
|
||||||
|
|
||||||
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
manualJournals,
|
||||||
|
new GetMatchedTransactionManualJournalsTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MatchedTransaction {
|
||||||
|
amount: number;
|
||||||
|
amountFormatted: string;
|
||||||
|
date: string;
|
||||||
|
dateFormatted: string;
|
||||||
|
referenceNo: string;
|
||||||
|
transactionNo: string;
|
||||||
|
transactionId: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { GetMatchedTransactions } from './GetMatchedTransactions';
|
||||||
|
import { MatchBankTransactions } from './MatchTransactions';
|
||||||
|
import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction';
|
||||||
|
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class MatchBankTransactionsApplication {
|
||||||
|
@Inject()
|
||||||
|
private getMatchedTransactionsService: GetMatchedTransactions;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private matchTransactionService: MatchBankTransactions;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private unmatchMatchedTransactionService: UnmatchMatchedBankTransaction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the matched transactions.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {GetMatchedTransactionsFilter} filter -
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public getMatchedTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
filter: GetMatchedTransactionsFilter
|
||||||
|
) {
|
||||||
|
return this.getMatchedTransactionsService.getMatchedTransactions(
|
||||||
|
tenantId,
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the given uncategorized transaction with the given system transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} uncategorizedTransactionId
|
||||||
|
* @param {IMatchTransactionDTO} matchTransactionsDTO
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public matchTransaction(
|
||||||
|
tenantId: number,
|
||||||
|
uncategorizedTransactionId: number,
|
||||||
|
matchTransactionsDTO: IMatchTransactionDTO
|
||||||
|
): Promise<void> {
|
||||||
|
return this.matchTransactionService.matchTransaction(
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
matchTransactionsDTO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmatch the given matched transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} uncategorizedTransactionId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public unmatchMatchedTransaction(
|
||||||
|
tenantId: number,
|
||||||
|
uncategorizedTransactionId: number
|
||||||
|
) {
|
||||||
|
return this.unmatchMatchedTransactionService.unmatchMatchedTransaction(
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import {
|
||||||
|
IBankTransactionMatchedEventPayload,
|
||||||
|
IBankTransactionMatchingEventPayload,
|
||||||
|
IMatchTransactionDTO,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class MatchBankTransactions {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the given uncategorized transaction to the given references.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} uncategorizedTransactionId
|
||||||
|
*/
|
||||||
|
public matchTransaction(
|
||||||
|
tenantId: number,
|
||||||
|
uncategorizedTransactionId: number,
|
||||||
|
matchTransactionsDTO: IMatchTransactionDTO
|
||||||
|
) {
|
||||||
|
const { matchedTransactions } = matchTransactionsDTO;
|
||||||
|
const { MatchBankTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
//
|
||||||
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers the event `onSaleInvoiceCreated`.
|
||||||
|
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
matchTransactionsDTO,
|
||||||
|
trx,
|
||||||
|
} as IBankTransactionMatchingEventPayload);
|
||||||
|
|
||||||
|
//
|
||||||
|
await PromisePool.withConcurrency(10)
|
||||||
|
.for(matchedTransactions)
|
||||||
|
.process(async (matchedTransaction) => {
|
||||||
|
await MatchBankTransaction.query(trx).insert({
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
referenceType: matchedTransaction.referenceType,
|
||||||
|
referenceId: matchedTransaction.referenceId,
|
||||||
|
amount: matchedTransaction.amount,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Triggers the event `onSaleInvoiceCreated`.
|
||||||
|
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
matchTransactionsDTO,
|
||||||
|
trx,
|
||||||
|
} as IBankTransactionMatchedEventPayload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { IBankTransactionUnmatchingEventPayload } from './types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class UnmatchMatchedBankTransaction {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
public unmatchMatchedTransaction(
|
||||||
|
tenantId: number,
|
||||||
|
uncategorizedTransactionId: number
|
||||||
|
) {
|
||||||
|
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
|
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
||||||
|
tenantId,
|
||||||
|
trx,
|
||||||
|
} as IBankTransactionUnmatchingEventPayload);
|
||||||
|
|
||||||
|
await MatchedBankTransaction.query(trx)
|
||||||
|
.where('uncategorizedTransactionId', uncategorizedTransactionId)
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
||||||
|
tenantId,
|
||||||
|
trx,
|
||||||
|
} as IBankTransactionUnmatchingEventPayload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/server/src/services/Banking/Matching/types.ts
Normal file
39
packages/server/src/services/Banking/Matching/types.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export interface IBankTransactionMatchingEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
matchTransactionsDTO: IMatchTransactionDTO;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBankTransactionMatchedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
matchTransactionsDTO: IMatchTransactionDTO;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBankTransactionUnmatchingEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBankTransactionUnmatchedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMatchTransactionDTO {
|
||||||
|
matchedTransactions: Array<{
|
||||||
|
referenceType: string;
|
||||||
|
referenceId: number;
|
||||||
|
amount: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetMatchedTransactionsFilter {
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
minAmount: number;
|
||||||
|
maxAmount: number;
|
||||||
|
transactionType: string;
|
||||||
|
}
|
||||||
@@ -618,6 +618,7 @@ export default {
|
|||||||
onItemCreated: 'onPlaidItemCreated',
|
onItemCreated: 'onPlaidItemCreated',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Bank rules.
|
||||||
bankRules: {
|
bankRules: {
|
||||||
onCreating: 'onBankRuleCreating',
|
onCreating: 'onBankRuleCreating',
|
||||||
onCreated: 'onBankRuleCreated',
|
onCreated: 'onBankRuleCreated',
|
||||||
@@ -628,4 +629,13 @@ export default {
|
|||||||
onDeleting: 'onBankRuleDeleting',
|
onDeleting: 'onBankRuleDeleting',
|
||||||
onDeleted: 'onBankRuleDeleted',
|
onDeleted: 'onBankRuleDeleted',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Bank matching.
|
||||||
|
bankMatch: {
|
||||||
|
onMatching: 'onBankTransactionMatching',
|
||||||
|
onMatched: 'onBankTransactionMatched',
|
||||||
|
|
||||||
|
onUnmatching: 'onBankTransactionUnmathcing',
|
||||||
|
onUnmatched: 'onBankTransactionUnmathced',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user