diff --git a/packages/server/src/api/controllers/Banking/BankingRulesController.ts b/packages/server/src/api/controllers/Banking/BankingRulesController.ts index e4b5ea26c..360dbcad6 100644 --- a/packages/server/src/api/controllers/Banking/BankingRulesController.ts +++ b/packages/server/src/api/controllers/Banking/BankingRulesController.ts @@ -96,9 +96,13 @@ export class BankingRulesController extends BaseController { * Creates a new bank rule. * @param {Request} req * @param {Response} res - * @param next + * @param {NextFunction} next */ - public async createBankRule(req: Request, res: Response, next: NextFunction) { + private async createBankRule( + req: Request, + res: Response, + next: NextFunction + ) { const { tenantId } = req; const createBankRuleDTO = this.matchedBodyData(req) as ICreateBankRuleDTO; @@ -118,11 +122,11 @@ export class BankingRulesController extends BaseController { /** * Edits the given bank rule. - * @param req - * @param res - * @param next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - public async editBankRule(req: Request, res: Response, next: NextFunction) { + private async editBankRule(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: ruleId } = req.params; const editBankRuleDTO = this.matchedBodyData(req) as IEditBankRuleDTO; @@ -144,11 +148,15 @@ export class BankingRulesController extends BaseController { /** * Deletes the given bank rule. - * @param req - * @param res - * @param next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - public async deleteBankRule(req: Request, res: Response, next: NextFunction) { + private async deleteBankRule( + req: Request, + res: Response, + next: NextFunction + ) { const { id: ruleId } = req.params; try { await this.bankRulesApplication.deleteBankRule(tenantId, ruleId); @@ -163,11 +171,11 @@ export class BankingRulesController extends BaseController { /** * Retrieve the given bank rule. - * @param req - * @param res - * @param next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - public async getBankRule(req: Request, res: Response, next: NextFunction) { + private async getBankRule(req: Request, res: Response, next: NextFunction) { const { id: ruleId } = req.params; const { tenantId } = req; @@ -185,11 +193,11 @@ export class BankingRulesController extends BaseController { /** * Retrieves the bank rules. - * @param req - * @param res - * @param next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - public async getBankRules(req: Request, res: Response, next: NextFunction) { + private async getBankRules(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; try { diff --git a/packages/server/src/database/migrations/20240618171553_create_recognized_bank_transactions_table.js b/packages/server/src/database/migrations/20240618171553_create_recognized_bank_transactions_table.js new file mode 100644 index 000000000..6fab718a1 --- /dev/null +++ b/packages/server/src/database/migrations/20240618171553_create_recognized_bank_transactions_table.js @@ -0,0 +1,18 @@ +exports.up = function (knex) { + return knex.schema.createTable('recognized_bank_transactions', (table) => { + table.increments('id'); + table.integer('cashflow_transaction_id').unsigned(); + table.inteegr('bank_rule_id').unsigned(); + + table.string('assigned_category'); + table.integer('assigned_account_id').unsigned(); + table.string('assigned_payee'); + table.string('assigned_memo'); + + table.timestamps(); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTableIfExists('recognized_bank_transactions'); +}; diff --git a/packages/server/src/database/migrations/20240618175241_add_recognized_transaction_id_to_uncategorized_transactins_table.js b/packages/server/src/database/migrations/20240618175241_add_recognized_transaction_id_to_uncategorized_transactins_table.js new file mode 100644 index 000000000..f50a13473 --- /dev/null +++ b/packages/server/src/database/migrations/20240618175241_add_recognized_transaction_id_to_uncategorized_transactins_table.js @@ -0,0 +1,11 @@ +exports.up = function (knex) { + return knex.schema.table('uncategorized_cashflow_transactions', (table) => { + table.integer('recognized_transaction_id').unsigned(); + }); +}; + +exports.down = function (knex) { + return knex.schema.table('uncategorized_cashflow_transactions', (table) => { + table.dropColumn('recognized_transaction_id'); + }); +}; diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index ad83f6dec..22a5e1988 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -102,6 +102,7 @@ import { AttachmentsOnVendorCredits } from '@/services/Attachments/events/Attach import { AttachmentsOnCreditNote } from '@/services/Attachments/events/AttachmentsOnCreditNote'; import { AttachmentsOnBillPayments } from '@/services/Attachments/events/AttachmentsOnPaymentsMade'; import { AttachmentsOnSaleEstimates } from '@/services/Attachments/events/AttachmentsOnSaleEstimates'; +import { TriggerRecognizedTransactions } from '@/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions'; export default () => { return new EventPublisher(); @@ -246,5 +247,8 @@ export const susbcribers = () => { AttachmentsOnBillPayments, AttachmentsOnManualJournals, AttachmentsOnExpenses, + + // Bank Rules + TriggerRecognizedTransactions, ]; }; diff --git a/packages/server/src/loaders/jobs.ts b/packages/server/src/loaders/jobs.ts index 231149f48..c64a8fe72 100644 --- a/packages/server/src/loaders/jobs.ts +++ b/packages/server/src/loaders/jobs.ts @@ -13,6 +13,7 @@ import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentRecei import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob'; import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob'; import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob'; +import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob'; export default ({ agenda }: { agenda: Agenda }) => { new ResetPasswordMailJob(agenda); @@ -29,6 +30,7 @@ export default ({ agenda }: { agenda: Agenda }) => { new PlaidFetchTransactionsJob(agenda); new ImportDeleteExpiredFilesJobs(agenda); new SendVerifyMailJob(agenda); + new RegonizeTransactionsJob(agenda); agenda.start().then(() => { agenda.every('1 hours', 'delete-expired-imported-files', {}); diff --git a/packages/server/src/models/BankRule.ts b/packages/server/src/models/BankRule.ts index 1f93a3415..a65b3bb27 100644 --- a/packages/server/src/models/BankRule.ts +++ b/packages/server/src/models/BankRule.ts @@ -2,6 +2,17 @@ import TenantModel from 'models/TenantModel'; import { Model } from 'objection'; export class BankRule extends TenantModel { + id!: number; + name!: string; + order!: number; + applyIfAccountId!: number; + applyIfTransactionType!: string; + assignCategory!: string; + assignAccountId!: number; + assignPayee!: string; + assignMemo!: string; + conditionsType!: string; + /** * Table name */ diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts b/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts new file mode 100644 index 000000000..1e0bb8c58 --- /dev/null +++ b/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTranasctionsService.ts @@ -0,0 +1,47 @@ +import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { transformToMapBy } from '@/utils'; +import { Inject, Service } from 'typedi'; +import { PromisePool } from '@supercharge/promise-pool'; + +@Service() +export class RecognizeedTranasctionsService { + @Inject() + private tenancy: HasTenancyService; + + /** + * Regonized the uncategorized transactions. + * @param {number} tenantId + */ + public async recognizeTransactions(tenantId: number) { + const { UncategorizedCashflowTransaction, BankRule } = + this.tenancy.models(tenantId); + + const uncategorizedTranasctions = + await UncategorizedCashflowTransaction.query().where( + 'regonized_transaction_id', + null + ); + + const bankRules = await BankRule.query(); + const bankRulesByAccountId = transformToMapBy(bankRules, 'accountId'); + + console.log(bankRulesByAccountId); + + const regonizeTransaction = ( + transaction: UncategorizedCashflowTransaction + ) => {}; + + await PromisePool.withConcurrency(MIGRATION_CONCURRENCY) + .for(uncategorizedTranasctions) + .process((transaction: UncategorizedCashflowTransaction, index, pool) => { + return regonizeTransaction(transaction); + }); + } + + public async regonizeTransaction( + uncategorizedTransaction: UncategorizedCashflowTransaction + ) {} +} + +const MIGRATION_CONCURRENCY = 10; diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob.ts b/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob.ts new file mode 100644 index 000000000..2278b6505 --- /dev/null +++ b/packages/server/src/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob.ts @@ -0,0 +1,32 @@ +import Container, { Service } from 'typedi'; +import { RegonizeTranasctionsService } from './RecognizeTranasctionsService'; + +@Service() +export class RegonizeTransactionsJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'regonize-uncategorized-transactions-job', + { priority: 'high', concurrency: 2 }, + this.handler + ); + } + + /** + * Triggers sending invoice mail. + */ + private handler = async (job, done: Function) => { + const { tenantId } = job.attrs.data; + const regonizeTransactions = Container.get(RegonizeTranasctionsService); + + try { + await regonizeTransactions.regonizeTransactions(tenantId); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts b/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts new file mode 100644 index 000000000..3094cc520 --- /dev/null +++ b/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts @@ -0,0 +1,37 @@ +import { Inject, Service } from 'typedi'; +import events from '@/subscribers/events'; +import { + IBankRuleEventCreatedPayload, + IBankRuleEventEditedPayload, +} from '../../Rules/types'; + +@Service() +export class TriggerRecognizedTransactions { + @Inject('agenda') + private agenda: any; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.bankRules.onCreated, + this.recognizedTransactionsOnRuleCreated.bind(this) + ); + bus.subscribe( + events.bankRules.onEdited, + this.recognizedTransactionsOnRuleCreated.bind(this) + ); + } + + /** + * Triggers the recognize uncategorized transactions job. + * @param {IBankRuleEventEditedPayload | IBankRuleEventCreatedPayload} payload - + */ + private async recognizedTransactionsOnRuleCreated({ + tenantId, + }: IBankRuleEventEditedPayload | IBankRuleEventCreatedPayload) { + const payload = { tenantId }; + await this.agenda.now('recognize-uncategorized-transactions-job', payload); + } +} diff --git a/packages/server/src/services/Banking/Rules/BankRulesApplication.ts b/packages/server/src/services/Banking/Rules/BankRulesApplication.ts index c24f47c32..7beb5e1f4 100644 --- a/packages/server/src/services/Banking/Rules/BankRulesApplication.ts +++ b/packages/server/src/services/Banking/Rules/BankRulesApplication.ts @@ -27,9 +27,12 @@ export class BankRulesApplication { * Creates new bank rule. * @param {number} tenantId * @param {ICreateBankRuleDTO} createRuleDTO - * @returns + * @returns {Promise} */ - public createBankRule(tenantId: number, createRuleDTO: ICreateBankRuleDTO) { + public createBankRule( + tenantId: number, + createRuleDTO: ICreateBankRuleDTO + ): Promise { return this.createBankRuleService.createBankRule(tenantId, createRuleDTO); } @@ -37,13 +40,13 @@ export class BankRulesApplication { * Edits the given bank rule. * @param {number} tenantId * @param {IEditBankRuleDTO} editRuleDTO - * @returns + * @returns {Promise} */ public editBankRule( tenantId: number, ruleId: number, editRuleDTO: IEditBankRuleDTO - ) { + ): Promise { return this.editBankRuleService.editBankRule(tenantId, ruleId, editRuleDTO); } @@ -51,9 +54,9 @@ export class BankRulesApplication { * Deletes the given bank rule. * @param {number} tenantId * @param {number} ruleId - * @returns + * @returns {Promise} */ - public deleteBankRule(tenantId: number, ruleId: number) { + public deleteBankRule(tenantId: number, ruleId: number): Promise { return this.deleteBankRuleService.deleteBankRule(tenantId, ruleId); } @@ -61,9 +64,9 @@ export class BankRulesApplication { * Retrieves the given bank rule. * @param {number} tenantId * @param {number} ruleId - * @returns + * @returns {Promise} */ - public getBankRule(tenantId: number, ruleId: number) { + public getBankRule(tenantId: number, ruleId: number): Promise { return this.getBankRuleService.getBankRule(tenantId, ruleId); } @@ -71,9 +74,9 @@ export class BankRulesApplication { * Retrieves the bank rules of the given account. * @param {number} tenantId * @param {number} accountId - * @returns + * @returns {Promise} */ - public getBankRules(tenantId: number) { + public getBankRules(tenantId: number): Promise { return this.getBankRulesService.getBankRules(tenantId); } } diff --git a/packages/server/src/services/Banking/Rules/CreateBankRule.ts b/packages/server/src/services/Banking/Rules/CreateBankRule.ts index 9385d0a79..ccca113e3 100644 --- a/packages/server/src/services/Banking/Rules/CreateBankRule.ts +++ b/packages/server/src/services/Banking/Rules/CreateBankRule.ts @@ -36,8 +36,12 @@ export class CreateBankRuleService { * Creates a new bank rule. * @param {number} tenantId * @param {ICreateBankRuleDTO} createRuleDTO + * @returns {Promise} */ - public createBankRule(tenantId: number, createRuleDTO: ICreateBankRuleDTO) { + public createBankRule( + tenantId: number, + createRuleDTO: ICreateBankRuleDTO + ): Promise { const { BankRule } = this.tenancy.models(tenantId); const transformDTO = this.transformDTO(createRuleDTO); @@ -45,6 +49,7 @@ export class CreateBankRuleService { return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onBankRuleCreating` event. await this.eventPublisher.emitAsync(events.bankRules.onCreating, { + tenantId, createRuleDTO, trx, } as IBankRuleEventCreatingPayload); @@ -55,6 +60,7 @@ export class CreateBankRuleService { // Triggers `onBankRuleCreated` event. await this.eventPublisher.emitAsync(events.bankRules.onCreated, { + tenantId, createRuleDTO, trx, } as IBankRuleEventCreatedPayload); diff --git a/packages/server/src/services/Banking/Rules/DeleteBankRule.ts b/packages/server/src/services/Banking/Rules/DeleteBankRule.ts index 9045f2a9b..9d6ce0167 100644 --- a/packages/server/src/services/Banking/Rules/DeleteBankRule.ts +++ b/packages/server/src/services/Banking/Rules/DeleteBankRule.ts @@ -1,6 +1,6 @@ import { Knex } from 'knex'; -import UnitOfWork from '@/services/UnitOfWork'; import { Inject, Service } from 'typedi'; +import UnitOfWork from '@/services/UnitOfWork'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; import { @@ -26,7 +26,7 @@ export class DeleteBankRuleSerivce { * @param {number} ruleId * @returns {Promise} */ - public async deleteBankRule(tenantId: number, ruleId: number) { + public async deleteBankRule(tenantId: number, ruleId: number): Promise { const { BankRule } = this.tenancy.models(tenantId); const oldBankRule = await BankRule.query() @@ -36,6 +36,7 @@ export class DeleteBankRuleSerivce { return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onBankRuleDeleting` event. await this.eventPublisher.emitAsync(events.bankRules.onDeleting, { + tenantId, oldBankRule, ruleId, trx, @@ -45,6 +46,7 @@ export class DeleteBankRuleSerivce { // Triggers `onBankRuleDeleted` event. await await this.eventPublisher.emitAsync(events.bankRules.onDeleted, { + tenantId, ruleId, trx, } as IBankRuleEventDeletedPayload); diff --git a/packages/server/src/services/Banking/Rules/EditBankRule.ts b/packages/server/src/services/Banking/Rules/EditBankRule.ts index 346541d62..5073e1c59 100644 --- a/packages/server/src/services/Banking/Rules/EditBankRule.ts +++ b/packages/server/src/services/Banking/Rules/EditBankRule.ts @@ -1,9 +1,9 @@ import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; 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 { IBankRuleEventEditedPayload, IBankRuleEventEditingPayload, @@ -56,6 +56,7 @@ export class EditBankRuleService { async (trx?: Knex.Transaction) => { // Triggers `onBankRuleEditing` event. await this.eventPublisher.emitAsync(events.bankRules.onEditing, { + tenantId, oldBankRule, ruleId, editRuleDTO, @@ -63,12 +64,13 @@ export class EditBankRuleService { } as IBankRuleEventEditingPayload); // Updates the given bank rule. - await BankRule.query() + await BankRule.query(trx) .findById(ruleId) .patch({ ...tranformDTO }); // Triggers `onBankRuleEdited` event. await this.eventPublisher.emitAsync(events.bankRules.onEdited, { + tenantId, oldBankRule, ruleId, editRuleDTO, diff --git a/packages/server/src/services/Banking/Rules/GetBankRule.ts b/packages/server/src/services/Banking/Rules/GetBankRule.ts index 82e141f4f..67a26e5a1 100644 --- a/packages/server/src/services/Banking/Rules/GetBankRule.ts +++ b/packages/server/src/services/Banking/Rules/GetBankRule.ts @@ -16,9 +16,9 @@ export class GetBankRuleService { * Retrieves the bank rule. * @param {number} tenantId * @param {number} ruleId - * @returns + * @returns {Promise} */ - async getBankRule(tenantId: number, ruleId: number) { + async getBankRule(tenantId: number, ruleId: number): Promise { const { BankRule } = this.tenancy.models(tenantId); const bankRule = await BankRule.query() diff --git a/packages/server/src/services/Banking/Rules/GetBankRules.ts b/packages/server/src/services/Banking/Rules/GetBankRules.ts index d543956cb..6eeb2c215 100644 --- a/packages/server/src/services/Banking/Rules/GetBankRules.ts +++ b/packages/server/src/services/Banking/Rules/GetBankRules.ts @@ -15,9 +15,9 @@ export class GetBankRulesService { * Retrieves the bank rules of the given account. * @param {number} tenantId * @param {number} accountId - * @returns + * @returns {Promise} */ - public async getBankRules(tenantId: number) { + public async getBankRules(tenantId: number): Promise { const { BankRule } = this.tenancy.models(tenantId); const bankRule = await BankRule.query(); diff --git a/packages/server/src/services/Banking/Rules/types.ts b/packages/server/src/services/Banking/Rules/types.ts index 4ed5aff21..ae1151254 100644 --- a/packages/server/src/services/Banking/Rules/types.ts +++ b/packages/server/src/services/Banking/Rules/types.ts @@ -33,32 +33,38 @@ export interface ICreateBankRuleDTO extends IBankRuleCommonDTO {} export interface IEditBankRuleDTO extends IBankRuleCommonDTO {} export interface IBankRuleEventCreatingPayload { + tenantId: number; createRuleDTO: ICreateBankRuleDTO; trx?: Knex.Transaction; } export interface IBankRuleEventCreatedPayload { + tenantId: number; createRuleDTO: ICreateBankRuleDTO; trx?: Knex.Transaction; } export interface IBankRuleEventEditingPayload { + tenantId: number; ruleId: number; oldBankRule: any; editRuleDTO: IEditBankRuleDTO; trx?: Knex.Transaction; } export interface IBankRuleEventEditedPayload { + tenantId: number; ruleId: number; editRuleDTO: IEditBankRuleDTO; trx?: Knex.Transaction; } export interface IBankRuleEventDeletingPayload { + tenantId: number; oldBankRule: any; ruleId: number; trx?: Knex.Transaction; } export interface IBankRuleEventDeletedPayload { + tenantId: number; ruleId: number; trx?: Knex.Transaction; } diff --git a/packages/server/src/services/Cashflow/CategorizeRecognizedTransaction.ts b/packages/server/src/services/Cashflow/CategorizeRecognizedTransaction.ts new file mode 100644 index 000000000..68c2e7fac --- /dev/null +++ b/packages/server/src/services/Cashflow/CategorizeRecognizedTransaction.ts @@ -0,0 +1,8 @@ +import { Service } from "typedi"; + + +@Service() +export class CategorizeRecognizedTransactionService { + + +} \ No newline at end of file