mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
feat: matching uncategorized transactions
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user