feat: recognize the syncd bank transactions

This commit is contained in:
Ahmed Bouhuolia
2024-06-23 18:49:46 +02:00
parent 589b29bbdd
commit 8dc2b18707
12 changed files with 150 additions and 29 deletions

View File

@@ -1,9 +1,9 @@
import { Inject, Service } from 'typedi';
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
import { GetMatchedTransactionsFilter } from './types';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionExpensesTransformer } from './GetMatchedTransactionExpensesTransformer';
@Service()
export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByType {
@@ -14,7 +14,7 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
protected transformer: TransformerInjectable;
/**
*
* Retrieves the matched transactions of expenses.
* @param {number} tenantId
* @param {GetMatchedTransactionsFilter} filter
* @returns
@@ -25,12 +25,25 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
) {
const { Expense } = this.tenancy.models(tenantId);
const expenses = await Expense.query();
const expenses = await Expense.query().onBuild((query) => {
query.whereNotExists(Expense.relatedQuery('matchedBankTransaction'));
if (filter.fromDate) {
query.where('payment_date', '>=', filter.fromDate);
}
if (filter.toDate) {
query.where('payment_date', '<=', filter.toDate);
}
if (filter.minAmount) {
query.where('total_amount', '>=', filter.minAmount);
}
if (filter.maxAmount) {
query.where('total_amount', '<=', filter.maxAmount);
}
});
return this.transformer.transform(
tenantId,
expenses,
new GetMatchedTransactionManualJournalsTransformer()
new GetMatchedTransactionExpensesTransformer()
);
}
}

View File

@@ -1,8 +1,8 @@
import { Inject, Service } from 'typedi';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
import { Inject, Service } from 'typedi';
import { GetMatchedTransactionsFilter } from './types';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionsFilter } from './types';
@Service()
export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactionsByType {
@@ -17,12 +17,27 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio
*/
async getMatchedTransactions(
tenantId: number,
filter: GetMatchedTransactionsFilter
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
) {
const { ManualJournal } = this.tenancy.models(tenantId);
const manualJournals = await ManualJournal.query();
const manualJournals = await ManualJournal.query().onBuild((query) => {
query.whereNotExists(
ManualJournal.relatedQuery('matchedBankTransaction')
);
if (filter.fromDate) {
query.where('date', '>=', filter.fromDate);
}
if (filter.toDate) {
query.where('date', '<=', filter.toDate);
}
if (filter.minAmount) {
query.where('amount', '>=', filter.minAmount);
}
if (filter.maxAmount) {
query.where('amount', '<=', filter.maxAmount);
}
});
return this.transformer.transform(
tenantId,
manualJournals,
@@ -41,6 +56,7 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio
const manualJournal = await ManualJournal.query()
.findById(transactionId)
.whereNotExists(ManualJournal.relatedQuery('matchedBankTransaction'))
.throwIfNotFound();
return this.transformer.transform(

View File

@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi';
import { IManualJournalDeletingPayload } from '@/interfaces';
import events from '@/subscribers/events';
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
import { Inject, Service } from 'typedi';
@Service()
export class ValidateMatchingOnCashflowDelete {

View File

@@ -5,6 +5,7 @@ import { entries, groupBy } from 'lodash';
import { CreateAccount } from '@/services/Accounts/CreateAccount';
import {
IAccountCreateDTO,
\ IPlaidTransactionsSyncedEventPayload,
PlaidAccount,
PlaidTransaction,
} from '@/interfaces';
@@ -16,6 +17,9 @@ import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTra
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
import { Knex } from 'knex';
import { uniqid } from 'uniqid';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
const CONCURRENCY_ASYNC = 10;
@@ -33,6 +37,9 @@ export class PlaidSyncDb {
@Inject()
private deleteCashflowTransactionService: DeleteCashflowTransaction;
@Inject()
private eventPublisher: EventPublisher;
/**
* Syncs the Plaid bank account.
* @param {number} tenantId
@@ -92,6 +99,7 @@ export class PlaidSyncDb {
* @param {number} tenantId - Tenant ID.
* @param {number} plaidAccountId - Plaid account ID.
* @param {PlaidTransaction[]} plaidTranasctions - Plaid transactions
* @return {Promise<void>}
*/
public async syncAccountTranactions(
tenantId: number,
@@ -101,18 +109,14 @@ export class PlaidSyncDb {
): Promise<void> {
const { Account } = this.tenancy.models(tenantId);
const batch = uniqid();
const cashflowAccount = await Account.query(trx)
.findOne({ plaidAccountId })
.throwIfNotFound();
const openingEquityBalance = await Account.query(trx).findOne(
'slug',
'opening-balance-equity'
);
// Transformes the Plaid transactions to cashflow create DTOs.
const transformTransaction = transformPlaidTrxsToCashflowCreate(
cashflowAccount.id,
openingEquityBalance.id
cashflowAccount.id
);
const uncategorizedTransDTOs =
R.map(transformTransaction)(plaidTranasctions);
@@ -123,20 +127,28 @@ export class PlaidSyncDb {
(uncategoriedDTO) =>
this.cashflowApp.createUncategorizedTransaction(
tenantId,
uncategoriedDTO,
{ ...uncategoriedDTO, batch },
trx
),
{ concurrency: 1 }
);
// Triggers `onPlaidTransactionsSynced` event.
await this.eventPublisher.emitAsync(events.plaid.onTransactionsSynced, {
tenantId,
plaidAccountId,
batch,
} as IPlaidTransactionsSyncedEventPayload);
}
/**
* Syncs the accounts transactions in paraller under controlled concurrency.
* @param {number} tenantId
* @param {PlaidTransaction[]} plaidTransactions
* @return {Promise<void>}
*/
public async syncAccountsTransactions(
tenantId: number,
batchNo: string,
plaidAccountsTransactions: PlaidTransaction[],
trx?: Knex.Transaction
): Promise<void> {
@@ -149,6 +161,7 @@ export class PlaidSyncDb {
return this.syncAccountTranactions(
tenantId,
plaidAccountId,
batchNo,
plaidTransactions,
trx
);
@@ -192,13 +205,14 @@ export class PlaidSyncDb {
* @param {number} tenantId - Tenant ID.
* @param {string} itemId - Plaid item ID.
* @param {string} lastCursor - Last transaction cursor.
* @return {Promise<void>}
*/
public async syncTransactionsCursor(
tenantId: number,
plaidItemId: string,
lastCursor: string,
trx?: Knex.Transaction
) {
): Promise<void> {
const { PlaidItem } = this.tenancy.models(tenantId);
await PlaidItem.query(trx).findOne({ plaidItemId }).patch({ lastCursor });
@@ -208,12 +222,13 @@ export class PlaidSyncDb {
* Updates the last feeds updated at of the given Plaid accounts ids.
* @param {number} tenantId
* @param {string[]} plaidAccountIds
* @return {Promise<void>}
*/
public async updateLastFeedsUpdatedAt(
tenantId: number,
plaidAccountIds: string[],
trx?: Knex.Transaction
) {
): Promise<void> {
const { Account } = this.tenancy.models(tenantId);
await Account.query(trx)
@@ -228,13 +243,14 @@ export class PlaidSyncDb {
* @param {number} tenantId
* @param {number[]} plaidAccountIds
* @param {boolean} isFeedsActive
* @returns {Promise<void>}
*/
public async updateAccountsFeedsActive(
tenantId: number,
plaidAccountIds: string[],
isFeedsActive: boolean = true,
trx?: Knex.Transaction
) {
): Promise<void> {
const { Account } = this.tenancy.models(tenantId);
await Account.query(trx)

View File

@@ -0,0 +1,42 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IPlaidItemCreatedEventPayload,
IPlaidTransactionsSyncedEventPayload,
} from '@/interfaces/Plaid';
import events from '@/subscribers/events';
import { RecognizeTranasctionsService } from '../../RegonizeTranasctions/RecognizeTranasctionsService';
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
@Service()
export class RecognizeSyncedBankTranasctions extends EventSubscriber {
@Inject()
private recognizeTranasctionsService: RecognizeTranasctionsService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.plaid.onTransactionsSynced,
this.handleRecognizeSyncedBankTransactions.bind(this)
);
}
/**
* Updates the Plaid item transactions
* @param {IPlaidItemCreatedEventPayload} payload - Event payload.
*/
private handleRecognizeSyncedBankTransactions = async ({
tenantId,
batch,
trx,
}: IPlaidTransactionsSyncedEventPayload) => {
runAfterTransaction(trx, async () => {
await this.recognizeTranasctionsService.recognizeTransactions(
tenantId,
batch
);
});
};
}

View File

@@ -50,14 +50,21 @@ export class RecognizeTranasctionsService {
* @param {number} tenantId -
* @param {Knex.Transaction} trx -
*/
public async recognizeTransactions(tenantId: number, trx?: Knex.Transaction) {
public async recognizeTransactions(
tenantId: number,
batch: string = '',
trx?: Knex.Transaction
) {
const { UncategorizedCashflowTransaction, BankRule } =
this.tenancy.models(tenantId);
const uncategorizedTranasctions =
await UncategorizedCashflowTransaction.query()
.where('recognized_transaction_id', null)
.where('categorized', false);
await UncategorizedCashflowTransaction.query().onBuild((query) => {
query.where('recognized_transaction_id', null);
query.where('categorized', false);
if (batch) query.where('batch', batch);
});
const bankRules = await BankRule.query().withGraphFetched('conditions');
const bankRulesByAccountId = transformToMapBy(
@@ -93,8 +100,8 @@ export class RecognizeTranasctionsService {
}
/**
*
* @param {number} uncategorizedTransaction
*
* @param {number} uncategorizedTransaction
*/
public async regonizeTransaction(
uncategorizedTransaction: UncategorizedCashflowTransaction

View File

@@ -27,7 +27,7 @@ export class DeleteBankRuleSerivce {
* @returns {Promise<void>}
*/
public async deleteBankRule(tenantId: number, ruleId: number): Promise<void> {
const { BankRule } = this.tenancy.models(tenantId);
const { BankRule, BankRuleCondition } = this.tenancy.models(tenantId);
const oldBankRule = await BankRule.query()
.findById(ruleId)
@@ -42,6 +42,7 @@ export class DeleteBankRuleSerivce {
trx,
} as IBankRuleEventDeletingPayload);
await BankRuleCondition.query(trx).where('ruleId', ruleId).delete();
await BankRule.query(trx).findById(ruleId).delete();
// Triggers `onBankRuleDeleted` event.