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

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
table.string('batch');
});
};
exports.down = function (knex) {
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
table.dropColumn('batch');
});
};

View File

@@ -267,4 +267,5 @@ export interface CreateUncategorizedTransactionDTO {
description?: string; description?: string;
referenceNo?: string | null; referenceNo?: string | null;
plaidTransactionId?: string | null; plaidTransactionId?: string | null;
batch?: string;
} }

View File

@@ -1,3 +1,5 @@
import { Knex } from "knex";
export interface IPlaidItemCreatedEventPayload { export interface IPlaidItemCreatedEventPayload {
tenantId: number; tenantId: number;
plaidAccessToken: string; plaidAccessToken: string;
@@ -54,3 +56,10 @@ export interface SyncAccountsTransactionsTask {
plaidAccountId: number; plaidAccountId: number;
plaidTransactions: PlaidTransaction[]; plaidTransactions: PlaidTransaction[];
} }
export interface IPlaidTransactionsSyncedEventPayload {
tenantId: number;
plaidAccountId: number;
batch: string;
trx?: Knex.Transaction
}

View File

@@ -108,6 +108,7 @@ import { ValidateMatchingOnManualJournalDelete } from '@/services/Banking/Matchi
import { ValidateMatchingOnPaymentReceivedDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnPaymentReceivedDelete'; import { ValidateMatchingOnPaymentReceivedDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnPaymentReceivedDelete';
import { ValidateMatchingOnPaymentMadeDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnPaymentMadeDelete'; import { ValidateMatchingOnPaymentMadeDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnPaymentMadeDelete';
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete'; import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions';
export default () => { export default () => {
return new EventPublisher(); return new EventPublisher();
@@ -262,5 +263,8 @@ export const susbcribers = () => {
ValidateMatchingOnManualJournalDelete, ValidateMatchingOnManualJournalDelete,
ValidateMatchingOnPaymentReceivedDelete, ValidateMatchingOnPaymentReceivedDelete,
ValidateMatchingOnPaymentMadeDelete, ValidateMatchingOnPaymentMadeDelete,
// Plaid
RecognizeSyncedBankTranasctions,
]; ];
}; };

View File

@@ -1,9 +1,9 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
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'; import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionExpensesTransformer } from './GetMatchedTransactionExpensesTransformer';
@Service() @Service()
export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByType { export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByType {
@@ -14,7 +14,7 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
protected transformer: TransformerInjectable; protected transformer: TransformerInjectable;
/** /**
* * Retrieves the matched transactions of expenses.
* @param {number} tenantId * @param {number} tenantId
* @param {GetMatchedTransactionsFilter} filter * @param {GetMatchedTransactionsFilter} filter
* @returns * @returns
@@ -25,12 +25,25 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
) { ) {
const { Expense } = this.tenancy.models(tenantId); 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( return this.transformer.transform(
tenantId, tenantId,
expenses, expenses,
new GetMatchedTransactionManualJournalsTransformer() new GetMatchedTransactionExpensesTransformer()
); );
} }
} }

View File

@@ -1,8 +1,8 @@
import { Inject, Service } from 'typedi';
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 { GetMatchedTransactionsFilter } from './types';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType'; import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionsFilter } from './types';
@Service() @Service()
export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactionsByType { export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactionsByType {
@@ -17,12 +17,27 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio
*/ */
async getMatchedTransactions( async getMatchedTransactions(
tenantId: number, tenantId: number,
filter: GetMatchedTransactionsFilter filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
) { ) {
const { ManualJournal } = this.tenancy.models(tenantId); 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( return this.transformer.transform(
tenantId, tenantId,
manualJournals, manualJournals,
@@ -41,6 +56,7 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio
const manualJournal = await ManualJournal.query() const manualJournal = await ManualJournal.query()
.findById(transactionId) .findById(transactionId)
.whereNotExists(ManualJournal.relatedQuery('matchedBankTransaction'))
.throwIfNotFound(); .throwIfNotFound();
return this.transformer.transform( return this.transformer.transform(

View File

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

View File

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

View File

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

View File

@@ -616,6 +616,7 @@ export default {
plaid: { plaid: {
onItemCreated: 'onPlaidItemCreated', onItemCreated: 'onPlaidItemCreated',
onTransactionsSynced: 'onPlaidTransactionsSynced',
}, },
// Bank rules. // Bank rules.
@@ -637,5 +638,5 @@ export default {
onUnmatching: 'onBankTransactionUnmathcing', onUnmatching: 'onBankTransactionUnmathcing',
onUnmatched: 'onBankTransactionUnmathced', onUnmatched: 'onBankTransactionUnmathced',
} },
}; };