refactor(nestjs): banking modules

This commit is contained in:
Ahmed Bouhuolia
2025-06-03 21:42:09 +02:00
parent 5595478e19
commit f87bd341e9
33 changed files with 516 additions and 138 deletions

View File

@@ -90,6 +90,8 @@ import { MiscellaneousModule } from '../Miscellaneous/Miscellaneous.module';
import { UsersModule } from '../UsersModule/Users.module';
import { ContactsModule } from '../Contacts/Contacts.module';
import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module';
import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.module';
@Module({
imports: [
@@ -151,6 +153,7 @@ import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
ScheduleModule.forRoot(),
TenancyDatabaseModule,
TenancyModelsModule,
TenantModelsInitializeModule,
AuthModule,
TenancyModule,
ChromiumlyTenancyModule,
@@ -188,6 +191,7 @@ import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
BankingTransactionsModule,
BankingMatchingModule,
BankingPlaidModule,
BankingCategorizeModule,
TransactionsLockingModule,
SettingsModule,
FeaturesModule,

View File

@@ -0,0 +1,92 @@
import { Knex } from 'knex';
import { CategorizeBankTransaction } from './commands/CategorizeBankTransaction';
import { UncategorizeBankTransactionService } from './commands/UncategorizeBankTransaction.service';
import { UncategorizeBankTransactionsBulk } from './commands/UncategorizeBankTransactionsBulk.service';
import { UncategorizedBankTransactionDto } from './dtos/CreateUncategorizedBankTransaction.dto';
import { CategorizeBankTransactionDto } from './dtos/CategorizeBankTransaction.dto';
import { CategorizeTransactionAsExpense } from './commands/CategorizeTransactionAsExpense';
import { CreateUncategorizedTransactionService } from './commands/CreateUncategorizedTransaction.service';
import { ICategorizeCashflowTransactioDTO } from './types/BankingCategorize.types';
import { Injectable } from '@nestjs/common';
@Injectable()
export class BankingCategorizeApplication {
constructor(
private readonly categorizeBankTransaction: CategorizeBankTransaction,
private readonly uncategorizeBankTransaction: UncategorizeBankTransactionService,
private readonly uncategorizeBankTransactionsBulk: UncategorizeBankTransactionsBulk,
private readonly categorizeTransactionAsExpense: CategorizeTransactionAsExpense,
private readonly createUncategorizedTransaction: CreateUncategorizedTransactionService,
) {}
/**
* Categorize a bank transaction with the given ID and categorization data.
* @param {number | Array<number>} uncategorizedTransactionId - The ID(s) of the uncategorized transaction(s) to categorize.
* @param {CategorizeBankTransactionDto} categorizeDTO - Data for categorization.
* @returns {Promise<any>} The result of the categorization operation.
*/
public categorizeTransaction(
uncategorizedTransactionId: number | Array<number>,
categorizeDTO: CategorizeBankTransactionDto,
) {
return this.categorizeBankTransaction.categorize(
uncategorizedTransactionId,
categorizeDTO,
);
}
/**
* Uncategorize a bank transaction with the given ID.
* @param {number} uncategorizedTransactionId - The ID of the transaction to uncategorize.
* @returns {Promise<Array<number>>} Array of affected transaction IDs.
*/
public uncategorizeTransaction(
uncategorizedTransactionId: number,
): Promise<Array<number>> {
return this.uncategorizeBankTransaction.uncategorize(
uncategorizedTransactionId,
);
}
/**
* Uncategorize multiple bank transactions in bulk.
* @param {number | Array<number>} uncategorizedTransactionIds - The ID(s) of the transaction(s) to uncategorize.
* @returns {Promise<void>}
*/
public uncategorizeTransactionsBulk(
uncategorizedTransactionIds: number | Array<number>,
) {
return this.uncategorizeBankTransactionsBulk.uncategorizeBulk(
uncategorizedTransactionIds,
);
}
/**
* Categorize a transaction as an expense.
* @param {number} cashflowTransactionId - The ID of the cashflow transaction to categorize.
* @param {ICategorizeCashflowTransactioDTO} transactionDTO - Data for categorization.
* @returns {Promise<any>} The result of the categorization operation.
*/
public categorizeTransactionAsExpenseType(
cashflowTransactionId: number,
transactionDTO: ICategorizeCashflowTransactioDTO,
) {
return this.categorizeTransactionAsExpense.categorize(
cashflowTransactionId,
transactionDTO,
);
}
/**
* Create a new uncategorized bank transaction.
* @param {UncategorizedBankTransactionDto} createDTO - Data for creating the uncategorized transaction.
* @param {Knex.Transaction} [trx] - Optional Knex transaction.
* @returns {Promise<any>} The created uncategorized transaction.
*/
public createUncategorizedBankTransaction(
createDTO: UncategorizedBankTransactionDto,
trx?: Knex.Transaction,
) {
return this.createUncategorizedTransaction.create(createDTO, trx);
}
}

View File

@@ -0,0 +1,39 @@
import { Body, Controller, Delete, Param, Post, Query } from '@nestjs/common';
import { castArray, omit } from 'lodash';
import { BankingCategorizeApplication } from './BankingCategorize.application';
import { CategorizeBankTransactionRouteDto } from './dtos/CategorizeBankTransaction.dto';
@Controller('banking/categorize')
export class BankingCategorizeController {
constructor(
private readonly bankingCategorizeApplication: BankingCategorizeApplication,
) {}
@Post()
public categorizeTransaction(
@Body() body: CategorizeBankTransactionRouteDto,
) {
return this.bankingCategorizeApplication.categorizeTransaction(
castArray(body.uncategorizedTransactionIds),
omit(body, 'uncategorizedTransactionIds'),
);
}
@Delete('/bulk')
public uncategorizeTransactionsBulk(
@Query() uncategorizedTransactionIds: number[] | number,
) {
return this.bankingCategorizeApplication.uncategorizeTransactionsBulk(
castArray(uncategorizedTransactionIds),
);
}
@Delete('/:id')
public uncategorizeTransaction(
@Param('id') uncategorizedTransactionId: number,
) {
return this.bankingCategorizeApplication.uncategorizeTransaction(
Number(uncategorizedTransactionId),
);
}
}

View File

@@ -1,20 +1,38 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { CreateUncategorizedTransactionService } from './commands/CreateUncategorizedTransaction.service';
import { CategorizeTransactionAsExpense } from './commands/CategorizeTransactionAsExpense';
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
import { ExpensesModule } from '../Expenses/Expenses.module';
import { UncategorizedTransactionsImportable } from './commands/UncategorizedTransactionsImportable';
import { BankingCategorizeController } from './BankingCategorize.controller';
import { BankingCategorizeApplication } from './BankingCategorize.application';
import { CategorizeBankTransaction } from './commands/CategorizeBankTransaction';
import { UncategorizeBankTransactionService } from './commands/UncategorizeBankTransaction.service';
import { UncategorizeBankTransactionsBulk } from './commands/UncategorizeBankTransactionsBulk.service';
@Module({
imports: [BankingTransactionsModule, ExpensesModule],
imports: [
BankingTransactionsModule,
ExpensesModule,
forwardRef(() => BankingTransactionsModule),
],
providers: [
CreateUncategorizedTransactionService,
CategorizeTransactionAsExpense,
UncategorizedTransactionsImportable
UncategorizedTransactionsImportable,
BankingCategorizeApplication,
CategorizeBankTransaction,
UncategorizeBankTransactionService,
UncategorizeBankTransactionsBulk,
],
exports: [
CreateUncategorizedTransactionService,
CategorizeTransactionAsExpense,
BankingCategorizeApplication,
CategorizeBankTransaction,
UncategorizeBankTransactionService,
UncategorizeBankTransactionsBulk,
],
controllers: [BankingCategorizeController],
})
export class BankingCategorizeModule {}

View File

@@ -5,7 +5,6 @@ import { Knex } from 'knex';
import {
ICashflowTransactionCategorizedPayload,
ICashflowTransactionUncategorizingPayload,
ICategorizeCashflowTransactioDTO,
} from '../types/BankingCategorize.types';
import {
transformCategorizeTransToCashflow,
@@ -17,9 +16,10 @@ import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { CategorizeBankTransactionDto } from '../dtos/CategorizeBankTransaction.dto';
@Injectable()
export class CategorizeCashflowTransaction {
export class CategorizeBankTransaction {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
@@ -38,7 +38,7 @@ export class CategorizeCashflowTransaction {
*/
public async categorize(
uncategorizedTransactionId: number | Array<number>,
categorizeDTO: ICategorizeCashflowTransactioDTO,
categorizeDTO: CategorizeBankTransactionDto,
) {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
@@ -68,7 +68,6 @@ export class CategorizeCashflowTransaction {
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCategorizing,
{
// tenantId,
oldUncategorizedTransactions,
trx,
} as ICashflowTransactionUncategorizingPayload,

View File

@@ -1,6 +1,5 @@
import { Knex } from 'knex';
import {
CreateUncategorizedTransactionDTO,
IUncategorizedTransactionCreatedEventPayload,
IUncategorizedTransactionCreatingEventPayload,
} from '../types/BankingCategorize.types';
@@ -10,6 +9,7 @@ import { UncategorizedBankTransaction } from '../../BankingTransactions/models/U
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { UncategorizedBankTransactionDto } from '../dtos/CreateUncategorizedBankTransaction.dto';
@Injectable()
export class CreateUncategorizedTransactionService {
@@ -30,7 +30,7 @@ export class CreateUncategorizedTransactionService {
* @returns {Promise<UncategorizedBankTransaction>}
*/
public create(
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
createUncategorizedTransactionDTO: UncategorizedBankTransactionDto,
trx?: Knex.Transaction,
) {
return this.uow.withTransaction(async (trx: Knex.Transaction) => {

View File

@@ -12,7 +12,7 @@ import { UncategorizedBankTransaction } from '../../BankingTransactions/models/U
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class UncategorizeCashflowTransactionService {
export class UncategorizeBankTransactionService {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,

View File

@@ -1,18 +1,17 @@
import { castArray } from 'lodash';
import { PromisePool } from '@supercharge/promise-pool';
import { Injectable } from '@nestjs/common';
import { UncategorizeCashflowTransactionService } from './UncategorizeCashflowTransaction.service';
import { UncategorizeBankTransactionService } from './UncategorizeBankTransaction.service';
@Injectable()
export class UncategorizeCashflowTransactionsBulk {
export class UncategorizeBankTransactionsBulk {
constructor(
private readonly uncategorizeTransactionService: UncategorizeCashflowTransactionService
private readonly uncategorizeTransactionService: UncategorizeBankTransactionService
) {}
/**
* Uncategorize the given bank transactions in bulk.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @param {number | Array<number>} uncategorizedTransactionId
*/
public async uncategorizeBulk(
uncategorizedTransactionId: number | Array<number>

View File

@@ -0,0 +1,55 @@
import { ToNumber } from '@/common/decorators/Validators';
import {
IsArray,
IsDateString,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
export class CategorizeBankTransactionDto {
@IsDateString()
@IsNotEmpty()
date: Date;
@IsInt()
@ToNumber()
@IsNotEmpty()
creditAccountId: number;
@IsString()
@IsOptional()
referenceNo: string;
@IsString()
@IsOptional()
transactionNumber: string;
@IsString()
@IsNotEmpty()
transactionType: string;
@IsNumber()
@ToNumber()
@IsOptional()
exchangeRate: number = 1;
@IsString()
@IsOptional()
currencyCode: string;
@IsString()
@IsOptional()
description: string;
@IsNumber()
@IsOptional()
branchId: number;
}
export class CategorizeBankTransactionRouteDto extends CategorizeBankTransactionDto {
@IsArray()
uncategorizedTransactionIds: Array<number>;
}

View File

@@ -0,0 +1,36 @@
import { IsBoolean, IsDateString, IsNumber, IsString } from 'class-validator';
export class UncategorizedBankTransactionDto {
@IsDateString()
date: Date | string;
@IsNumber()
accountId: number;
@IsNumber()
amount: number;
@IsString()
currencyCode: string;
@IsString()
payee?: string;
@IsString()
description?: string;
@IsString()
referenceNo?: string | null;
@IsString()
plaidTransactionId?: string | null;
@IsBoolean()
pending?: boolean;
@IsString()
pendingPlaidTransactionId?: string | null;
@IsString()
batch?: string;
}

View File

@@ -1,47 +1,48 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { BankingMatchingApplication } from './BankingMatchingApplication';
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
import { GetMatchedTransactionsFilter } from './types';
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
@Controller('banking/matching')
@ApiTags('banking-transactions-matching')
export class BankingMatchingController {
constructor(
private readonly bankingMatchingApplication: BankingMatchingApplication
private readonly bankingMatchingApplication: BankingMatchingApplication,
) {}
@Get('matched')
@ApiOperation({ summary: 'Retrieves the matched transactions.' })
async getMatchedTransactions(
@Query('uncategorizedTransactionIds') uncategorizedTransactionIds: number[],
@Query() filter: GetMatchedTransactionsFilter
@Query() filter: GetMatchedTransactionsFilter,
) {
return this.bankingMatchingApplication.getMatchedTransactions(
uncategorizedTransactionIds,
filter
filter,
);
}
@Post('/match/:uncategorizedTransactionId')
@ApiOperation({ summary: 'Match the given uncategorized transaction.' })
async matchTransaction(
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number | number[],
@Body() matchedTransactions: MatchBankTransactionDto
@Param('uncategorizedTransactionId')
uncategorizedTransactionId: number | number[],
@Body() matchedTransactions: MatchBankTransactionDto,
) {
return this.bankingMatchingApplication.matchTransaction(
uncategorizedTransactionId,
matchedTransactions
matchedTransactions,
);
}
@Post('/unmatch/:uncategorizedTransactionId')
@ApiOperation({ summary: 'Unmatch the given uncategorized transaction.' })
async unmatchMatchedTransaction(
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number,
) {
return this.bankingMatchingApplication.unmatchMatchedTransaction(
uncategorizedTransactionId
uncategorizedTransactionId,
);
}
}

View File

@@ -1,3 +1,4 @@
import { Knex } from 'knex';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
import { GetMatchedTransactionsFilter } from '../types';

View File

@@ -1,10 +1,13 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from '../types';
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO, MatchedTransactionsPOJO } from '../types';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionExpensesTransformer } from './GetMatchedTransactionExpensesTransformer';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { Expense } from '@/modules/Expenses/models/Expense.model';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Knex } from 'knex';
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
import { initialize } from 'objection';
@Injectable()
export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByType {
@@ -13,17 +16,26 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
@Inject(Expense.name)
protected readonly expenseModel: TenantModelProxy<typeof Expense>,
@Inject(TENANCY_DB_CONNECTION)
private readonly tenantDb: () => Knex,
@Inject('TENANT_MODELS_INIT')
private readonly tenantModelsInit: () => Promise<boolean>,
) {
super();
}
/**
* Retrieves the matched transactions of expenses.
* @param {number} tenantId
* @param {GetMatchedTransactionsFilter} filter
* @returns
*/
async getMatchedTransactions(filter: GetMatchedTransactionsFilter) {
async getMatchedTransactions(
filter: GetMatchedTransactionsFilter,
): Promise<MatchedTransactionsPOJO> {
// await this.tenantModelsInit();
// Retrieve the expense matches.
const expenses = await this.expenseModel()
.query()
@@ -49,6 +61,7 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
}
query.orderBy('paymentDate', 'DESC');
});
return this.transformer.transform(
expenses,
new GetMatchedTransactionExpensesTransformer(),

View File

@@ -1,3 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { first } from 'lodash';
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
@@ -9,7 +10,6 @@ import {
} from '../types';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { CreatePaymentReceivedService } from '@/modules/PaymentReceived/commands/CreatePaymentReceived.serivce';
import { Inject, Injectable } from '@nestjs/common';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
@@ -86,7 +86,6 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
/**
* Creates the common matched transaction.
* @param {number} tenantId
* @param {Array<number>} uncategorizedTransactionIds
* @param {IMatchTransactionDTO} matchTransactionDTO
* @param {Knex.Transaction} trx

View File

@@ -1,10 +1,13 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { initialize } from 'objection';
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionsFilter } from '../types';
import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
@Injectable()
export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactionsByType {

View File

@@ -12,7 +12,7 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
export abstract class GetMatchedTransactionsByType {
@Inject(MatchedBankTransaction.name)
private readonly matchedBankTransactionModel: TenantModelProxy<
matchedBankTransactionModel: TenantModelProxy<
typeof MatchedBankTransaction
>;

View File

@@ -76,6 +76,11 @@ const models = [
GetPendingBankAccountTransactions,
GetAutofillCategorizeTransactionService,
],
exports: [...models, RemovePendingUncategorizedTransaction],
exports: [
...models,
RemovePendingUncategorizedTransaction,
CommandBankTransactionValidator,
CreateBankTransactionService
],
})
export class BankingTransactionsModule {}

View File

@@ -15,6 +15,7 @@ import { GetUncategorizedTransactionsQueryDto } from './dtos/GetUncategorizedTra
import { GetPendingBankAccountTransactions } from './queries/GetPendingBankAccountTransaction.service';
import { GetPendingTransactionsQueryDto } from './dtos/GetPendingTransactionsQuery.dto';
import { GetAutofillCategorizeTransactionService } from './queries/GetAutofillCategorizeTransaction/GetAutofillCategorizeTransaction.service';
import { GetBankTransactionsQueryDto } from './dtos/GetBankTranasctionsQuery.dto';
@Injectable()
export class BankingTransactionsApplication {
@@ -54,7 +55,7 @@ export class BankingTransactionsApplication {
* Retrieves the bank transactions of the given bank id.
* @param {ICashflowAccountTransactionsQuery} query
*/
public getBankAccountTransactions(query: ICashflowAccountTransactionsQuery) {
public getBankAccountTransactions(query: GetBankTransactionsQueryDto) {
return this.getBankAccountTransactionsService.bankAccountTransactions(
query,
);

View File

@@ -1,3 +1,4 @@
import { Injectable } from '@nestjs/common';
import { includes, camelCase, upperFirst, sumBy } from 'lodash';
import { getCashflowTransactionType } from '../utils';
import {
@@ -6,7 +7,6 @@ import {
ERRORS,
} from '../constants';
import { Account } from '@/modules/Accounts/models/Account.model';
import { Injectable } from '@nestjs/common';
import { ServiceError } from '@/modules/Items/ServiceError';
import { BankTransaction } from '../models/BankTransaction';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';

View File

@@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { pick } from 'lodash';
import { Knex } from 'knex';
import * as R from 'ramda';
import * as composeAsync from 'async/compose';
import { CASHFLOW_TRANSACTION_TYPE } from '../constants';
import { transformCashflowTransactionType } from '../utils';
@@ -14,12 +13,12 @@ import { events } from '@/common/events/events';
import { Account } from '@/modules/Accounts/models/Account.model';
import { BankTransaction } from '../models/BankTransaction';
import {
ICashflowNewCommandDTO,
ICommandCashflowCreatedPayload,
ICommandCashflowCreatingPayload,
} from '../types/BankingTransactions.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { CreateBankTransactionDto } from '../dtos/CreateBankTransaction.dto';
import { formatDateFields } from '@/utils/format-date-fields';
@Injectable()
export class CreateBankTransactionService {
@@ -42,7 +41,7 @@ export class CreateBankTransactionService {
* @param {ICashflowNewCommandDTO} newCashflowTransactionDTO
*/
public authorize = async (
newCashflowTransactionDTO: ICashflowNewCommandDTO,
newCashflowTransactionDTO: CreateBankTransactionDto,
creditAccount: Account,
) => {
const transactionType = transformCashflowTransactionType(
@@ -60,7 +59,7 @@ export class CreateBankTransactionService {
/**
* Transformes owner contribution DTO to cashflow transaction.
* @param {ICashflowNewCommandDTO} newCashflowTransactionDTO - New transaction DTO.
* @param {CreateBankTransactionDto} newCashflowTransactionDTO - New transaction DTO.
* @returns {ICashflowTransactionInput} - Cashflow transaction object.
*/
private transformCashflowTransactionDTO = async (
@@ -91,7 +90,7 @@ export class CreateBankTransactionService {
const initialDTO = {
amount,
...fromDTO,
...formatDateFields(fromDTO, ['date']),
transactionNumber,
currencyCode: cashflowAccount.currencyCode,
exchangeRate: fromDTO?.exchangeRate || 1,
@@ -117,7 +116,7 @@ export class CreateBankTransactionService {
* @returns {Promise<ICashflowTransaction>}
*/
public newCashflowTransaction = async (
newTransactionDTO: ICashflowNewCommandDTO,
newTransactionDTO: CreateBankTransactionDto,
userId?: number,
): Promise<BankTransaction> => {
// Retrieves the cashflow account or throw not found error.

View File

@@ -1,47 +1,64 @@
import { ToNumber } from '@/common/decorators/Validators';
import {
IsBoolean,
IsDate,
IsDateString,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
export class CreateBankTransactionDto {
@IsDate()
@IsDateString()
@IsNotEmpty()
date: Date;
@IsString()
transactionNumber: string;
@IsOptional()
transactionNumber?: string;
@IsString()
referenceNo: string;
@IsOptional()
referenceNo?: string;
@IsNotEmpty()
@IsString()
transactionType: string;
@IsString()
description: string;
@IsNotEmpty()
@ToNumber()
@IsNumber()
amount: number;
@ToNumber()
@IsNumber()
exchangeRate: number;
exchangeRate: number = 1;
@IsString()
@IsOptional()
currencyCode: string;
@IsNumber()
@IsNotEmpty()
@ToNumber()
@IsInt()
creditAccountId: number;
@IsNumber()
@IsNotEmpty()
@ToNumber()
@IsInt()
cashflowAccountId: number;
@IsBoolean()
publish: boolean;
@IsOptional()
publish: boolean = true;
@IsOptional()
@IsNumber()
@ToNumber()
@IsInt()
branchId?: number;
@IsOptional()
@@ -53,6 +70,6 @@ export class CreateBankTransactionDto {
plaidAccountId?: string;
@IsOptional()
@IsNumber()
@IsInt()
uncategorizedTransactionId?: number;
}

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { getBankAccountTransactionsDefaultQuery } from './_utils';
import { GetBankAccountTransactionsRepository } from './GetBankAccountTransactionsRepo.service';
import { GetBankAccountTransactions } from './GetBankAccountTransactions';
import { ICashflowAccountTransactionsQuery } from '../../types/BankingTransactions.types';
import { GetBankTransactionsQueryDto } from '../../dtos/GetBankTranasctionsQuery.dto';
@Injectable()
export class GetBankAccountTransactionsService {
@@ -16,7 +16,7 @@ export class GetBankAccountTransactionsService {
* @return {Promise<IInvetoryItemDetailDOO>}
*/
public async bankAccountTransactions(
query: ICashflowAccountTransactionsQuery,
query: GetBankTransactionsQueryDto,
) {
const parsedQuery = {
...getBankAccountTransactionsDefaultQuery(),

View File

@@ -1,35 +1,23 @@
import { Inject, Injectable } from '@nestjs/common';
import { initialize } from 'objection';
import { Knex } from 'knex';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { UncategorizedTransactionTransformer } from '../../BankingCategorize/commands/UncategorizedTransaction.transformer';
import { GetUncategorizedTransactionsQueryDto } from '../dtos/GetUncategorizedTransactionsQuery.dto';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
import { Account } from '@/modules/Accounts/models/Account.model';
import { RecognizedBankTransaction } from '@/modules/BankingTranasctionsRegonize/models/RecognizedBankTransaction';
import { MatchedBankTransaction } from '@/modules/BankingMatching/models/MatchedBankTransaction';
@Injectable()
export class GetUncategorizedTransactions {
/**
* @param {TransformerInjectable} transformer
* @param {UncategorizedBankTransaction.name} uncategorizedBankTransactionModel
*/
constructor(
private readonly transformer: TransformerInjectable,
@Inject(TENANCY_DB_CONNECTION)
private readonly tenantDb: () => Knex,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy<typeof UncategorizedBankTransaction>,
@Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>,
@Inject(RecognizedBankTransaction.name)
private readonly recognizedTransactionModel: TenantModelProxy<typeof RecognizedBankTransaction>,
@Inject(MatchedBankTransaction.name)
private readonly matchedTransactionModel: TenantModelProxy<typeof MatchedBankTransaction>,
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
) {}
/**
@@ -39,7 +27,7 @@ export class GetUncategorizedTransactions {
*/
public async getTransactions(
accountId: number,
query: GetUncategorizedTransactionsQueryDto
query: GetUncategorizedTransactionsQueryDto,
) {
// Parsed query with default values.
const _query = {
@@ -47,16 +35,9 @@ export class GetUncategorizedTransactions {
pageSize: 20,
...query,
};
await initialize(this.tenantDb(), [
this.accountModel(),
this.uncategorizedBankTransactionModel(),
this.recognizedTransactionModel(),
this.matchedTransactionModel(),
]);
const { results, pagination } =
await this.uncategorizedBankTransactionModel().query()
await this.uncategorizedBankTransactionModel()
.query()
.onBuild((q) => {
q.where('accountId', accountId);
q.where('categorized', false);
@@ -89,7 +70,7 @@ export class GetUncategorizedTransactions {
const data = await this.transformer.transform(
results,
new UncategorizedTransactionTransformer()
new UncategorizedTransactionTransformer(),
);
return {
data,

View File

@@ -3,7 +3,10 @@ import { OnEvent } from '@nestjs/event-emitter';
import { BankTransactionAutoIncrement } from '../commands/BankTransactionAutoIncrement.service';
import { BankTransactionGLEntriesService } from '../commands/BankTransactionGLEntries';
import { events } from '@/common/events/events';
import { ICommandCashflowCreatedPayload, ICommandCashflowDeletedPayload } from '../types/BankingTransactions.types';
import {
ICommandCashflowCreatedPayload,
ICommandCashflowDeletedPayload,
} from '../types/BankingTransactions.types';
@Injectable()
export class BankingTransactionGLEntriesSubscriber {
@@ -56,5 +59,5 @@ export class BankingTransactionGLEntriesSubscriber {
cashflowTransactionId,
trx,
);
};
}
}

View File

@@ -492,7 +492,7 @@ export class Bill extends TenantBaseModel {
TaxRateTransaction,
} = require('../../TaxRates/models/TaxRateTransaction.model');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
const { MatchedBankTransaction } = require('../../BankingMatching/models/MatchedBankTransaction');
return {
vendor: {
@@ -590,17 +590,17 @@ export class Bill extends TenantBaseModel {
/**
* Bill may belongs to matched bank transaction.
*/
// matchedBankTransaction: {
// relation: Model.HasManyRelation,
// modelClass: MatchedBankTransaction,
// join: {
// from: 'bills.id',
// to: 'matched_bank_transactions.referenceId',
// },
// filter(query) {
// query.where('reference_type', 'Bill');
// },
// },
matchedBankTransaction: {
relation: Model.HasManyRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'bills.id',
to: 'matched_bank_transactions.referenceId',
},
filter(query) {
query.where('reference_type', 'Bill');
},
},
};
}

View File

@@ -1,6 +1,6 @@
import * as R from 'ramda';
import { Injectable } from '@nestjs/common';
import validator from 'is-my-json-valid';
import * as validator from 'is-my-json-valid';
import { IFilterRole } from './DynamicFilter/DynamicFilter.types';
import { DynamicFilterAdvancedFilter } from './DynamicFilter/DynamicFilterAdvancedFilter';
import { DynamicFilterRoleAbstractor } from './DynamicFilter/DynamicFilterRoleAbstractor';
@@ -21,7 +21,7 @@ export class DynamicListFilterRoles extends DynamicFilterRoleAbstractor {
properties: {
condition: { type: 'string' },
fieldKey: { type: 'string' },
value: { type: 'string' },
// value: { type: ['number', 'string'] },
},
});
const invalidFields = filterRoles.filter((filterRole) => {

View File

@@ -202,7 +202,7 @@ export class Expense extends TenantBaseModel {
const { ExpenseCategory } = require('./ExpenseCategory.model');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
const { Branch } = require('../../Branches/models/Branch.model');
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
const { MatchedBankTransaction } = require('../../BankingMatching/models/MatchedBankTransaction');
return {
/**
@@ -263,20 +263,20 @@ export class Expense extends TenantBaseModel {
},
},
// /**
// * Expense may belongs to matched bank transaction.
// */
// matchedBankTransaction: {
// relation: Model.HasManyRelation,
// modelClass: MatchedBankTransaction,
// join: {
// from: 'expenses_transactions.id',
// to: 'matched_bank_transactions.referenceId',
// },
// filter(query) {
// query.where('reference_type', 'Expense');
// },
// },
/**
* Expense may belongs to matched bank transaction.
*/
matchedBankTransaction: {
relation: Model.HasManyRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'expenses_transactions.id',
to: 'matched_bank_transactions.referenceId',
},
filter(query) {
query.where('reference_type', 'Expense');
},
},
};
}

View File

@@ -123,7 +123,7 @@ export class ManualJournal extends TenantBaseModel {
const { AccountTransaction } = require('../../Accounts/models/AccountTransaction.model');
const { ManualJournalEntry } = require('./ManualJournalEntry');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
const { MatchedBankTransaction } = require('../../BankingMatching/models/MatchedBankTransaction');
return {
entries: {
@@ -171,17 +171,17 @@ export class ManualJournal extends TenantBaseModel {
/**
* Manual journal may belongs to matched bank transaction.
*/
// matchedBankTransaction: {
// relation: Model.BelongsToOneRelation,
// modelClass: MatchedBankTransaction,
// join: {
// from: 'manual_journals.id',
// to: 'matched_bank_transactions.referenceId',
// },
// filter(query) {
// query.where('reference_type', 'ManualJournal');
// },
// },
matchedBankTransaction: {
relation: Model.BelongsToOneRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'manual_journals.id',
to: 'matched_bank_transactions.referenceId',
},
filter(query) {
query.where('reference_type', 'ManualJournal');
},
},
};
}

View File

@@ -512,7 +512,7 @@ export class SaleInvoice extends TenantBaseModel{
TaxRateTransaction,
} = require('../../TaxRates/models/TaxRateTransaction.model');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
const { MatchedBankTransaction } = require('../../BankingMatching/models/MatchedBankTransaction');
const {
TransactionPaymentServiceEntry,
} = require('../../PaymentServices/models/TransactionPaymentServiceEntry.model');
@@ -667,17 +667,17 @@ export class SaleInvoice extends TenantBaseModel{
/**
* Sale invocie may belongs to matched bank transaction.
*/
// matchedBankTransaction: {
// relation: Model.HasManyRelation,
// modelClass: MatchedBankTransaction,
// join: {
// from: 'sales_invoices.id',
// to: 'matched_bank_transactions.referenceId',
// },
// filter(query) {
// query.where('reference_type', 'SaleInvoice');
// },
// },
matchedBankTransaction: {
relation: Model.HasManyRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'sales_invoices.id',
to: 'matched_bank_transactions.referenceId',
},
filter(query) {
query.where('reference_type', 'SaleInvoice');
},
},
/**
* Sale invoice may belongs to payment methods entries.

View File

@@ -5,6 +5,7 @@ import { EnsureTenantIsSeededGuard } from "./EnsureTenantIsSeeded.guards";
import { APP_GUARD } from "@nestjs/core";
import { TenancyContext } from "./TenancyContext.service";
import { TenantController } from "./Tenant.controller";
import { TenancyInitializeModelsGuard } from "./TenancyInitializeModels.guard";
@Module({
@@ -23,6 +24,10 @@ import { TenantController } from "./Tenant.controller";
{
provide: APP_GUARD,
useClass: EnsureTenantIsSeededGuard
},
{
provide: APP_GUARD,
useClass: TenancyInitializeModelsGuard
}
]
})

View File

@@ -0,0 +1,54 @@
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
SetMetadata,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
import { TENANT_MODELS_INIT } from './TenantModelsInitialize.module';
export const IGNORE_TENANT_MODELS_INITIALIZE =
'IGNORE_TENANT_MODELS_INITIALIZE';
export const IgnoreTenantModelsInitialize = () =>
SetMetadata(IGNORE_TENANT_MODELS_INITIALIZE, true);
@Injectable()
export class TenancyInitializeModelsGuard implements CanActivate {
constructor(
@Inject(TENANT_MODELS_INIT)
private readonly tenantModelsInit: () => Promise<boolean>,
private reflector: Reflector,
) {}
/**
* Initialize tenant models if the route is decorated with TriggerTenantModelsInitialize.
* @param {ExecutionContext} context
* @returns {Promise<boolean>}
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_ROUTE,
[context.getHandler(), context.getClass()],
);
// Skip initialization for public routes
if (isPublic) {
return true;
}
const shouldIgnoreInitialization =
this.reflector.getAllAndOverride<boolean>(
IGNORE_TENANT_MODELS_INITIALIZE,
[context.getHandler(), context.getClass()],
);
// Initialize models unless the route is decorated with IgnoreTenantModelsInitialize
if (!shouldIgnoreInitialization) {
try {
await this.tenantModelsInit();
} catch (error) {
console.error('Failed to initialize tenant models:', error);
}
}
return true;
}
}

View File

@@ -0,0 +1,54 @@
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { ClsModule } from 'nestjs-cls';
import { Global, Module } from '@nestjs/common';
import { Knex } from 'knex';
import { initialize } from 'objection';
import { TENANCY_DB_CONNECTION } from './TenancyDB/TenancyDB.constants';
const RegisteredModels = [
'SaleInvoice',
'Bill',
'Expense',
'BankTransaction',
'MatchedBankTransaction',
'ManualJournalEntry',
'Account',
'UncategorizedBankTransaction',
'RecognizedBankTransaction',
];
export const TENANT_MODELS_INIT = 'TENANT_MODELS_INIT';
const provider = ClsModule.forFeatureAsync({
provide: TENANT_MODELS_INIT,
inject: [TENANCY_DB_CONNECTION, ModuleRef],
useFactory: (tenantKnex: () => Knex, moduleRef: ModuleRef) => async () => {
const knexInstance = tenantKnex();
const contextId = ContextIdFactory.create();
const models = await Promise.all(
RegisteredModels.map((model) => {
return moduleRef.resolve(model, contextId, { strict: false });
}),
);
const modelsInstances = models.map((model) => model());
if (modelsInstances.length > 0) {
try {
// Initialize all models with the knex instance
await initialize(knexInstance, modelsInstances);
} catch (error) {
console.error('Error initializing models:', error);
throw error;
}
}
return true;
},
strict: true,
type: 'function',
});
@Module({
imports: [provider],
exports: [provider],
})
@Global()
export class TenantModelsInitializeModule {}

View File

@@ -248,7 +248,7 @@ export function useCategorizeTransaction(props) {
const apiRequest = useApiRequest();
return useMutation(
(values) => apiRequest.post(`banking/transactions/categorize`, values),
(values) => apiRequest.post(`banking/categorize`, values),
{
onSuccess: (res, id) => {
// Invalidate queries.
@@ -274,7 +274,7 @@ export function useUncategorizeTransaction(props) {
const apiRequest = useApiRequest();
return useMutation(
(id: number) => apiRequest.post(`banking/transactions/${id}/uncategorize`),
(id: number) => apiRequest.delete(`banking/categorize/${id}`),
{
onSuccess: (res, id) => {
// Invalidate queries.