Compare commits

..

1 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
985e1dbc01 refactor(nestjs): users module 2025-05-19 19:21:06 +02:00
365 changed files with 1830 additions and 8285 deletions

View File

@@ -1,7 +0,0 @@
import { registerAs } from '@nestjs/config';
export default registerAs('bankfeed', () => ({
enabled:
process.env.BANK_FEED_ENABLED === 'true' ||
process.env.BANK_FEED_ENABLED === 'yes',
}));

View File

@@ -13,7 +13,6 @@ import signupRestrictions from './signup-restrictions';
import jwt from './jwt'; import jwt from './jwt';
import mail from './mail'; import mail from './mail';
import loops from './loops'; import loops from './loops';
import bankfeed from './bankfeed';
export const config = [ export const config = [
systemDatabase, systemDatabase,
@@ -30,6 +29,5 @@ export const config = [
signupRestrictions, signupRestrictions,
jwt, jwt,
mail, mail,
loops, loops
bankfeed,
]; ];

View File

@@ -1,57 +0,0 @@
/**
* Map to store all models that have been marked to prevent base currency mutation.
* Key is the model name, value is the model class.
*/
export const preventMutateBaseCurrencyModels = new Map<string, any>();
/**
* Decorator that marks an ORM model to prevent base currency mutation.
* When applied to a model class, it adds a static property `preventMutateBaseCurrency` set to true
* and registers the model in the preventMutateBaseCurrencyModels map.
*
* @returns {ClassDecorator} A decorator function that can be applied to a class.
*/
export function PreventMutateBaseCurrency(): ClassDecorator {
return (target: any) => {
// Set the static property on the model class
target.preventMutateBaseCurrency = true;
// Register the model in the map
const modelName = target.name;
preventMutateBaseCurrencyModels.set(modelName, target);
// Return the modified class
return target;
};
}
/**
* Get all registered models that prevent base currency mutation.
*
* @returns {Map<string, any>} Map of model names to model classes
*/
export function getPreventMutateBaseCurrencyModels(): Map<string, any> {
return preventMutateBaseCurrencyModels;
}
/**
* Check if a model is registered to prevent base currency mutation.
*
* @param {string} modelName - The name of the model to check
* @returns {boolean} True if the model is registered, false otherwise
*/
export function isModelPreventMutateBaseCurrency(modelName: string): boolean {
return preventMutateBaseCurrencyModels.has(modelName);
}
/**
* Get a specific model by name that prevents base currency mutation.
*
* @param {string} modelName - The name of the model to retrieve
* @returns {any | undefined} The model class if found, undefined otherwise
*/
export function getPreventMutateBaseCurrencyModel(
modelName: string,
): any | undefined {
return preventMutateBaseCurrencyModels.get(modelName);
}

View File

@@ -1,32 +0,0 @@
import { Transform } from 'class-transformer';
import { ValidateIf, ValidationOptions } from 'class-validator';
/**
* Decorator that converts the property value to a number.
* @returns PropertyDecorator
*/
export function ToNumber() {
return Transform(({ value, key }) => {
const defaultValue = null;
if (typeof value === 'number') {
return value;
}
// If value is an empty string or undefined/null, return it as-is (wont pass validation)
if (value === '' || value === null || value === undefined) {
return defaultValue;
}
const parsed = Number(value);
return !isNaN(parsed) ? parsed : value;
});
}
/**
* Validates if the property is not empty.
* @returns PropertyDecorator
*/
export function IsOptional(validationOptions?: ValidationOptions) {
return ValidateIf((_obj, value) => {
return value !== null && value !== undefined && value !== '';
}, validationOptions);
}

View File

@@ -70,10 +70,7 @@ export class SerializeInterceptor implements NestInterceptor<any, any> {
next: CallHandler<any>, next: CallHandler<any>,
): Observable<any> { ): Observable<any> {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
// Transform both body and query parameters
request.body = this.strategy.in(request.body); request.body = this.strategy.in(request.body);
request.query = this.strategy.in(request.query);
// handle returns stream.. // handle returns stream..
return next.handle().pipe(map(this.strategy.out)); return next.handle().pipe(map(this.strategy.out));

View File

@@ -14,15 +14,12 @@ export class ValidationPipe implements PipeTransform<any> {
return value; return value;
} }
const object = plainToInstance(metatype, value); const object = plainToInstance(metatype, value);
const errors = await validate(object, { const errors = await validate(object);
// Strip validated object of any properties that do not have any decorators.
whitelist: true,
});
if (errors.length > 0) { if (errors.length > 0) {
throw new BadRequestException(errors); throw new BadRequestException(errors);
} }
return object; return value;
} }
private toValidate(metatype: Function): boolean { private toValidate(metatype: Function): boolean {

View File

@@ -1,9 +1,9 @@
{ {
"previoud_period_date": "{date} (PP)", "previoud_period_date": "{{date}} (PP)",
"fianncial_sheet.previous_period_change": "Change (PP)", "fianncial_sheet.previous_period_change": "Change (PP)",
"previous_period_percentage": "% Change (PP)", "previous_period_percentage": "% Change (PP)",
"previous_year_date": "{date} (PY)", "previous_year_date": "{{date}} (PY)",
"previous_year_change": "Change (PY)", "previous_year_change": "Change (PY)",
"previous_year_percentage": "% Change (PY)", "previous_year_percentage": "% Change (PY)",
"total_row": "Total {value}" "total_row": "Total {{value}}"
} }

View File

@@ -1,4 +0,0 @@
{
"decrement": "Decrement",
"increment": "Increment"
}

View File

@@ -1,4 +0,0 @@
{
"primary_warehouse": "Primary Warehouse"
}

View File

@@ -1,4 +1,4 @@
import * as FormData from 'form-data'; import FormData from 'form-data';
import { GotenbergUtils } from './GotenbergUtils'; import { GotenbergUtils } from './GotenbergUtils';
import { PageProperties } from './_types'; import { PageProperties } from './_types';

View File

@@ -1,5 +1,5 @@
import * as FormData from 'form-data'; import FormData from 'form-data';
import { Axios } from 'axios'; import Axios from 'axios';
export class GotenbergUtils { export class GotenbergUtils {
public static assert(condition: boolean, message: string): asserts condition { public static assert(condition: boolean, message: string): asserts condition {
@@ -10,12 +10,12 @@ export class GotenbergUtils {
public static async fetch(endpoint: string, data: FormData): Promise<Buffer> { public static async fetch(endpoint: string, data: FormData): Promise<Buffer> {
try { try {
const response = await new Axios({ const response = await Axios.post(endpoint, data, {
headers: { headers: {
...data.getHeaders(), ...data.getHeaders(),
}, },
responseType: 'arraybuffer', // This ensures you get a Buffer bac responseType: 'arraybuffer', // This ensures you get a Buffer bac
}).post(endpoint, data); });
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@@ -1,5 +1,5 @@
import { constants, createReadStream, PathLike, promises } from 'fs'; import { constants, createReadStream, PathLike, promises } from 'fs';
import * as FormData from 'form-data'; import FormData from 'form-data';
import { GotenbergUtils } from './GotenbergUtils'; import { GotenbergUtils } from './GotenbergUtils';
import { IConverter, PageProperties } from './_types'; import { IConverter, PageProperties } from './_types';
import { PdfFormat, ChromiumRoute } from './_types'; import { PdfFormat, ChromiumRoute } from './_types';

View File

@@ -1,4 +1,4 @@
import * as FormData from 'form-data'; import FormData from 'form-data';
import { IConverter, PageProperties, PdfFormat, ChromiumRoute } from './_types'; import { IConverter, PageProperties, PdfFormat, ChromiumRoute } from './_types';
import { ConverterUtils } from './ConvertUtils'; import { ConverterUtils } from './ConvertUtils';
import { Converter } from './Converter'; import { Converter } from './Converter';

View File

@@ -13,7 +13,9 @@ export class MutateBaseCurrencyAccounts {
* Mutates the all accounts or the organziation. * Mutates the all accounts or the organziation.
* @param {string} currencyCode * @param {string} currencyCode
*/ */
async mutateAllAccountsCurrency(currencyCode: string) { mutateAllAccountsCurrency = async (
await this.accountModel().query().update({ currencyCode }); currencyCode: string,
} ) => {
await Account.query().update({ currencyCode });
};
} }

View File

@@ -88,10 +88,6 @@ import { ViewsModule } from '../Views/Views.module';
import { CurrenciesModule } from '../Currencies/Currencies.module'; import { CurrenciesModule } from '../Currencies/Currencies.module';
import { MiscellaneousModule } from '../Miscellaneous/Miscellaneous.module'; import { MiscellaneousModule } from '../Miscellaneous/Miscellaneous.module';
import { UsersModule } from '../UsersModule/Users.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({ @Module({
imports: [ imports: [
@@ -153,7 +149,6 @@ import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
TenancyDatabaseModule, TenancyDatabaseModule,
TenancyModelsModule, TenancyModelsModule,
TenantModelsInitializeModule,
AuthModule, AuthModule,
TenancyModule, TenancyModule,
ChromiumlyTenancyModule, ChromiumlyTenancyModule,
@@ -186,12 +181,10 @@ import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.
LedgerModule, LedgerModule,
BankAccountsModule, BankAccountsModule,
BankRulesModule, BankRulesModule,
BankingTransactionsModule,
BankingTransactionsExcludeModule, BankingTransactionsExcludeModule,
BankingTransactionsRegonizeModule, BankingTransactionsRegonizeModule,
BankingTransactionsModule,
BankingMatchingModule, BankingMatchingModule,
BankingPlaidModule,
BankingCategorizeModule,
TransactionsLockingModule, TransactionsLockingModule,
SettingsModule, SettingsModule,
FeaturesModule, FeaturesModule,
@@ -217,8 +210,7 @@ import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.
ViewsModule, ViewsModule,
CurrenciesModule, CurrenciesModule,
MiscellaneousModule, MiscellaneousModule,
UsersModule, UsersModule
ContactsModule
], ],
controllers: [AppController], controllers: [AppController],
providers: [ providers: [

View File

@@ -33,7 +33,6 @@ const models = [
@Module({ @Module({
imports: [S3Module, ...models], imports: [S3Module, ...models],
exports: [...models],
controllers: [AttachmentsController], controllers: [AttachmentsController],
providers: [ providers: [
DeleteAttachment, DeleteAttachment,

View File

@@ -1,4 +1,4 @@
import * as path from 'path'; import path from 'path';
// import config from '@/config'; // import config from '@/config';
export const getUploadedObjectUri = (objectKey: string) => { export const getUploadedObjectUri = (objectKey: string) => {

View File

@@ -23,13 +23,13 @@ export class EditBankRuleService {
) {} ) {}
/** /**
* Transforms the given edit bank rule dto to model object. *
* @param editDTO * @param createDTO
* @returns * @returns
*/ */
private transformDTO(editDTO: EditBankRuleDto): ModelObject<BankRule> { private transformDTO(createDTO: EditBankRuleDto): ModelObject<BankRule> {
return { return {
...editDTO, ...createDTO,
} as ModelObject<BankRule>; } as ModelObject<BankRule>;
} }

View File

@@ -12,7 +12,6 @@ import {
} from 'class-validator'; } from 'class-validator';
import { BankRuleComparator } from '../types'; import { BankRuleComparator } from '../types';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { ToNumber } from '@/common/decorators/Validators';
class BankRuleConditionDto { class BankRuleConditionDto {
@IsNotEmpty() @IsNotEmpty()
@@ -45,8 +44,6 @@ export class CommandBankRuleDto {
}) })
name: string; name: string;
@IsNotEmpty()
@ToNumber()
@IsInt() @IsInt()
@Min(0) @Min(0)
@ApiProperty({ @ApiProperty({
@@ -56,7 +53,6 @@ export class CommandBankRuleDto {
order: number; order: number;
@IsOptional() @IsOptional()
@ToNumber()
@IsInt() @IsInt()
@Min(0) @Min(0)
@ApiProperty({ @ApiProperty({
@@ -65,7 +61,6 @@ export class CommandBankRuleDto {
}) })
applyIfAccountId?: number; applyIfAccountId?: number;
@IsNotEmpty()
@IsIn(['deposit', 'withdrawal']) @IsIn(['deposit', 'withdrawal'])
@ApiProperty({ @ApiProperty({
description: 'The transaction type to apply the rule if', description: 'The transaction type to apply the rule if',
@@ -87,14 +82,11 @@ export class CommandBankRuleDto {
@Type(() => BankRuleConditionDto) @Type(() => BankRuleConditionDto)
@ApiProperty({ @ApiProperty({
description: 'The conditions to apply the rule if', description: 'The conditions to apply the rule if',
example: [ example: [{ field: 'description', comparator: 'contains', value: 'Salary' }],
{ field: 'description', comparator: 'contains', value: 'Salary' },
],
}) })
conditions: BankRuleConditionDto[]; conditions: BankRuleConditionDto[];
@IsString() @IsString()
@IsNotEmpty()
@ApiProperty({ @ApiProperty({
description: 'The category to assign the rule if', description: 'The category to assign the rule if',
example: 'Income:Salary', example: 'Income:Salary',
@@ -103,8 +95,6 @@ export class CommandBankRuleDto {
@IsInt() @IsInt()
@Min(0) @Min(0)
@ToNumber()
@IsNotEmpty()
@ApiProperty({ @ApiProperty({
description: 'The account ID to assign the rule if', description: 'The account ID to assign the rule if',
example: 1, example: 1,

View File

@@ -1,13 +1,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Account } from '@/modules/Accounts/models/Account.model'; import { Account } from '@/modules/Accounts/models/Account.model';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction'; import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { BaseModel } from '@/models/Model';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; 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';
import { MatchedBankTransaction } from '@/modules/BankingMatching/models/MatchedBankTransaction';
import { TenantModel } from '@/modules/System/models/TenantModel';
import { RecognizedBankTransaction } from '@/modules/BankingTranasctionsRegonize/models/RecognizedBankTransaction';
@Injectable() @Injectable()
export class GetBankAccountSummary { export class GetBankAccountSummary {
@@ -19,19 +14,6 @@ export class GetBankAccountSummary {
private readonly uncategorizedBankTransactionModel: TenantModelProxy< private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction typeof UncategorizedBankTransaction
>, >,
@Inject(MatchedBankTransaction.name)
private readonly matchedBankTransactionModel: TenantModelProxy<
typeof MatchedBankTransaction
>,
@Inject(RecognizedBankTransaction.name)
private readonly recognizedBankTransaction: TenantModelProxy<
typeof RecognizedBankTransaction
>,
@Inject(TENANCY_DB_CONNECTION)
private readonly tenantDb: () => Knex,
) {} ) {}
/** /**
@@ -45,11 +27,6 @@ export class GetBankAccountSummary {
.findById(bankAccountId) .findById(bankAccountId)
.throwIfNotFound(); .throwIfNotFound();
await initialize(this.tenantDb(), [
this.uncategorizedBankTransactionModel(),
this.matchedBankTransactionModel(),
this.recognizedBankTransaction(),
]);
const commonQuery = (q) => { const commonQuery = (q) => {
// Include just the given account. // Include just the given account.
q.where('accountId', bankAccountId); q.where('accountId', bankAccountId);
@@ -60,6 +37,11 @@ export class GetBankAccountSummary {
// Only the not categorized. // Only the not categorized.
q.modify('notCategorized'); q.modify('notCategorized');
}; };
interface UncategorizedTransactionsCount {
total: number;
}
// Retrieves the uncategorized transactions count of the given bank account. // Retrieves the uncategorized transactions count of the given bank account.
const uncategorizedTranasctionsCount = const uncategorizedTranasctionsCount =
await this.uncategorizedBankTransactionModel() await this.uncategorizedBankTransactionModel()

View File

@@ -1,92 +0,0 @@
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

@@ -1,56 +0,0 @@
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';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('banking/categorize')
@ApiTags('banking-categorization')
export class BankingCategorizeController {
constructor(
private readonly bankingCategorizeApplication: BankingCategorizeApplication,
) {}
@Post()
@ApiOperation({ summary: 'Categorize bank transactions.' })
@ApiResponse({
status: 200,
description: 'The bank transactions have been categorized successfully.',
})
public categorizeTransaction(
@Body() body: CategorizeBankTransactionRouteDto,
) {
return this.bankingCategorizeApplication.categorizeTransaction(
castArray(body.uncategorizedTransactionIds),
omit(body, 'uncategorizedTransactionIds'),
);
}
@Delete('/bulk')
@ApiOperation({ summary: 'Uncategorize bank transactions.' })
@ApiResponse({
status: 200,
description: 'The bank transactions have been uncategorized successfully.',
})
public uncategorizeTransactionsBulk(
@Query() uncategorizedTransactionIds: number[] | number,
) {
return this.bankingCategorizeApplication.uncategorizeTransactionsBulk(
castArray(uncategorizedTransactionIds),
);
}
@Delete('/:id')
@ApiOperation({ summary: 'Uncategorize a bank transaction.' })
@ApiResponse({
status: 200,
description: 'The bank transaction has been uncategorized successfully.',
})
public uncategorizeTransaction(
@Param('id') uncategorizedTransactionId: number,
) {
return this.bankingCategorizeApplication.uncategorizeTransaction(
Number(uncategorizedTransactionId),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,113 +0,0 @@
import { ToNumber } from '@/common/decorators/Validators';
import {
IsArray,
IsDateString,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
/**
* DTO for categorizing bank transactions
*/
export class CategorizeBankTransactionDto {
@ApiProperty({
description: 'The date of the bank transaction',
type: Date,
example: '2023-01-01T00:00:00.000Z',
})
@IsDateString()
@IsNotEmpty()
date: Date;
@ApiProperty({
description: 'ID of the credit account associated with this transaction',
type: Number,
example: 1001,
})
@IsInt()
@ToNumber()
@IsNotEmpty()
creditAccountId: number;
@ApiPropertyOptional({
description: 'Optional external reference number',
type: String,
example: 'REF-001',
})
@IsString()
@IsOptional()
referenceNo: string;
@ApiPropertyOptional({
description: 'Optional transaction number or reference',
type: String,
example: 'TRX-001',
})
@IsString()
@IsOptional()
transactionNumber: string;
@ApiProperty({
description: 'Type of bank transaction (e.g., deposit, withdrawal)',
type: String,
example: 'deposit',
})
@IsString()
@IsNotEmpty()
transactionType: string;
@ApiPropertyOptional({
description: 'Exchange rate for currency conversion',
type: Number,
default: 1,
example: 1.15,
})
@IsNumber()
@ToNumber()
@IsOptional()
exchangeRate: number = 1;
@ApiPropertyOptional({
description: 'Currency code for the transaction',
type: String,
example: 'USD',
})
@IsString()
@IsOptional()
currencyCode: string;
@ApiPropertyOptional({
description: 'Description of the bank transaction',
type: String,
example: 'Monthly rent payment',
})
@IsString()
@IsOptional()
description: string;
@ApiPropertyOptional({
description: 'ID of the branch where the transaction occurred',
type: Number,
example: 101,
})
@IsNumber()
@IsOptional()
branchId: number;
}
/**
* Extended DTO for categorizing bank transactions with IDs of uncategorized transactions
*/
export class CategorizeBankTransactionRouteDto extends CategorizeBankTransactionDto {
@ApiProperty({
description: 'Array of uncategorized transaction IDs to be categorized',
type: [Number],
example: [1001, 1002, 1003],
})
@IsArray()
uncategorizedTransactionIds: Array<number>;
}

View File

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

View File

@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { GetMatchedTransactions } from './queries/GetMatchedTransactions.service'; import { GetMatchedTransactions } from './queries/GetMatchedTransactions.service';
import { MatchBankTransactions } from './commands/MatchTransactions'; import { MatchBankTransactions } from './commands/MatchTransactions';
import { UnmatchMatchedBankTransaction } from './commands/UnmatchMatchedTransaction.service'; import { UnmatchMatchedBankTransaction } from './commands/UnmatchMatchedTransaction.service';
import { GetMatchedTransactionsFilter } from './types'; import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
import { MatchTransactionEntryDto } from './dtos/MatchBankTransaction.dto'; import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
@Injectable() @Injectable()
export class BankingMatchingApplication { export class BankingMatchingApplication {
@@ -31,18 +31,17 @@ export class BankingMatchingApplication {
/** /**
* Matches the given uncategorized transaction with the given system transaction. * Matches the given uncategorized transaction with the given system transaction.
* @param {IMatchBankTransactionDto} matchedTransactionsDTO * @param {number} uncategorizedTransactionId
* @param {IMatchTransactionDTO} matchTransactionsDTO
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public matchTransaction( public matchTransaction(
uncategorizedTransactionId: number | Array<number>, uncategorizedTransactionId: number | Array<number>,
matchedTransactionsDto: matchedTransactions: MatchBankTransactionDto,
| MatchTransactionEntryDto
| Array<MatchTransactionEntryDto>,
): Promise<void> { ): Promise<void> {
return this.matchTransactionService.matchTransaction( return this.matchTransactionService.matchTransaction(
uncategorizedTransactionId, uncategorizedTransactionId,
matchedTransactionsDto, matchedTransactions,
); );
} }

View File

@@ -21,7 +21,7 @@ import { ServiceError } from '@/modules/Items/ServiceError';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction'; import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { MatchTransactionEntryDto } from '../dtos/MatchBankTransaction.dto'; import { MatchBankTransactionDto } from '../dtos/MatchBankTransaction.dto';
@Injectable() @Injectable()
export class MatchBankTransactions { export class MatchBankTransactions {
@@ -107,15 +107,16 @@ export class MatchBankTransactions {
/** /**
* Matches the given uncategorized transaction to the given references. * Matches the given uncategorized transaction to the given references.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId * @param {number} uncategorizedTransactionId
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async matchTransaction( public async matchTransaction(
uncategorizedTransactionId: number | Array<number>, uncategorizedTransactionId: number | Array<number>,
matchedTransactionsDto: MatchTransactionEntryDto | Array<MatchTransactionEntryDto>, matchedTransactionsDto: MatchBankTransactionDto,
): Promise<void> { ): Promise<void> {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId); const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
const matchedTransactions = castArray(matchedTransactionsDto); const matchedTransactions = matchedTransactionsDto.entries;
// Validates the given matching transactions DTO. // Validates the given matching transactions DTO.
await this.validate(uncategorizedTransactionIds, matchedTransactions); await this.validate(uncategorizedTransactionIds, matchedTransactions);
@@ -130,7 +131,7 @@ export class MatchBankTransactions {
// Matches the given transactions under promise pool concurrency controlling. // Matches the given transactions under promise pool concurrency controlling.
await PromisePool.withConcurrency(10) await PromisePool.withConcurrency(10)
.for(matchedTransactions) .for(matchedTransactions)
.process(async (matchedTransaction: MatchTransactionEntryDto) => { .process(async (matchedTransaction) => {
const getMatchedTransactionsService = const getMatchedTransactionsService =
this.matchedBankTransactions.registry.get( this.matchedBankTransactions.registry.get(
matchedTransaction.referenceType, matchedTransaction.referenceType,

View File

@@ -1,5 +1,4 @@
import { import {
ArrayMinSize,
IsArray, IsArray,
IsNotEmpty, IsNotEmpty,
IsNumber, IsNumber,
@@ -28,10 +27,6 @@ export class MatchTransactionEntryDto {
} }
export class MatchBankTransactionDto { export class MatchBankTransactionDto {
@IsArray()
@ArrayMinSize(1)
uncategorizedTransactions: Array<number>
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => MatchTransactionEntryDto) @Type(() => MatchTransactionEntryDto)
@@ -42,5 +37,5 @@ export class MatchBankTransactionDto {
{ referenceType: 'SaleInvoice', referenceId: 2 }, { referenceType: 'SaleInvoice', referenceId: 2 },
], ],
}) })
matchedTransactions: MatchTransactionEntryDto[]; entries: MatchTransactionEntryDto[];
} }

View File

@@ -1,14 +1,14 @@
import PromisePool from '@supercharge/promise-pool';
import { OnEvent } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import { import {
IBankTransactionMatchedEventPayload, IBankTransactionMatchedEventPayload,
IBankTransactionUnmatchedEventPayload, IBankTransactionUnmatchedEventPayload,
} from '../types'; } from '../types';
import PromisePool from '@supercharge/promise-pool';
import { OnEvent } from '@nestjs/event-emitter';
import { Account } from '@/modules/Accounts/models/Account.model'; import { Account } from '@/modules/Accounts/models/Account.model';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { Inject, Injectable } from '@nestjs/common';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction'; import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable() @Injectable()
export class DecrementUncategorizedTransactionOnMatchingSubscriber { export class DecrementUncategorizedTransactionOnMatchingSubscriber {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
import { Body, Controller, Post } from '@nestjs/common';
import { PlaidApplication } from './PlaidApplication';
import { PlaidItemDto } from './dtos/PlaidItem.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
@Controller('banking/plaid')
@ApiTags('banking-plaid')
export class BankingPlaidController {
constructor(private readonly plaidApplication: PlaidApplication) {}
@Post('link-token')
@ApiOperation({ summary: 'Get Plaid link token' })
getLinkToken() {
return this.plaidApplication.getLinkToken();
}
@Post('exchange-token')
@ApiOperation({ summary: 'Exchange Plaid access token' })
exchangeToken(@Body() itemDTO: PlaidItemDto) {
return this.plaidApplication.exchangeToken(itemDTO);
}
}

View File

@@ -1,4 +1,3 @@
import { BullModule } from '@nestjs/bullmq';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PlaidUpdateTransactionsOnItemCreatedSubscriber } from './subscribers/PlaidUpdateTransactionsOnItemCreatedSubscriber'; import { PlaidUpdateTransactionsOnItemCreatedSubscriber } from './subscribers/PlaidUpdateTransactionsOnItemCreatedSubscriber';
import { PlaidUpdateTransactions } from './command/PlaidUpdateTransactions'; import { PlaidUpdateTransactions } from './command/PlaidUpdateTransactions';
@@ -16,11 +15,6 @@ import { PlaidItemService } from './command/PlaidItem';
import { TenancyContext } from '../Tenancy/TenancyContext.service'; import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module'; import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
import { SystemPlaidItem } from './models/SystemPlaidItem'; import { SystemPlaidItem } from './models/SystemPlaidItem';
import { BankingPlaidController } from './BankingPlaid.controller';
import { BankingPlaidWebhooksController } from './BankingPlaidWebhooks.controller';
import { SetupPlaidItemTenantService } from './command/SetupPlaidItemTenant.service';
import { UpdateBankingPlaidTransitionsQueueJob } from './types/BankingPlaid.types';
import { PlaidFetchTransactionsProcessor } from './jobs/PlaidFetchTransactionsJob';
const models = [RegisterTenancyModel(PlaidItem)]; const models = [RegisterTenancyModel(PlaidItem)];
@@ -30,7 +24,6 @@ const models = [RegisterTenancyModel(PlaidItem)];
AccountsModule, AccountsModule,
BankingCategorizeModule, BankingCategorizeModule,
BankingTransactionsModule, BankingTransactionsModule,
BullModule.registerQueue({ name: UpdateBankingPlaidTransitionsQueueJob }),
...models, ...models,
], ],
providers: [ providers: [
@@ -41,12 +34,9 @@ const models = [RegisterTenancyModel(PlaidItem)];
PlaidWebooks, PlaidWebooks,
PlaidLinkTokenService, PlaidLinkTokenService,
PlaidApplication, PlaidApplication,
SetupPlaidItemTenantService,
TenancyContext,
PlaidFetchTransactionsProcessor,
PlaidUpdateTransactionsOnItemCreatedSubscriber, PlaidUpdateTransactionsOnItemCreatedSubscriber,
TenancyContext,
], ],
exports: [...models], exports: [...models],
controllers: [BankingPlaidController, BankingPlaidWebhooksController],
}) })
export class BankingPlaidModule {} export class BankingPlaidModule {}

View File

@@ -1,31 +0,0 @@
import { Body, Controller, Post } from '@nestjs/common';
import { PlaidWebhookDto } from './dtos/PlaidItem.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { PlaidApplication } from './PlaidApplication';
import { PublicRoute } from '../Auth/guards/jwt.guard';
import { SetupPlaidItemTenantService } from './command/SetupPlaidItemTenant.service';
@Controller('banking/plaid')
@ApiTags('banking-plaid')
@PublicRoute()
export class BankingPlaidWebhooksController {
constructor(
private readonly plaidApplication: PlaidApplication,
private readonly setupPlaidItemTenantService: SetupPlaidItemTenantService,
) {}
@Post('webhooks')
@ApiOperation({ summary: 'Listen to Plaid webhooks' })
webhooks(@Body() { itemId, webhookType, webhookCode }: PlaidWebhookDto) {
return this.setupPlaidItemTenantService.setupPlaidTenant(
itemId,
() => {
return this.plaidApplication.webhooks(
itemId,
webhookType,
webhookCode,
);
},
);
}
}

View File

@@ -1,12 +1,8 @@
import { ClsService } from 'nestjs-cls';
import { Inject, Injectable } from '@nestjs/common';
import { PlaidLinkTokenService } from './queries/GetPlaidLinkToken.service'; import { PlaidLinkTokenService } from './queries/GetPlaidLinkToken.service';
import { PlaidItemService } from './command/PlaidItem'; import { PlaidItemService } from './command/PlaidItem';
import { PlaidWebooks } from './command/PlaidWebhooks'; import { PlaidWebooks } from './command/PlaidWebhooks';
import { PlaidItemDto } from './dtos/PlaidItem.dto'; import { Injectable } from '@nestjs/common';
import { SystemPlaidItem } from './models/SystemPlaidItem'; import { PlaidItemDTO } from './types/BankingPlaid.types';
import { TenantModel } from '../System/models/TenantModel';
import { SystemUser } from '../System/models/SystemUser';
@Injectable() @Injectable()
export class PlaidApplication { export class PlaidApplication {
@@ -14,16 +10,6 @@ export class PlaidApplication {
private readonly getLinkTokenService: PlaidLinkTokenService, private readonly getLinkTokenService: PlaidLinkTokenService,
private readonly plaidItemService: PlaidItemService, private readonly plaidItemService: PlaidItemService,
private readonly plaidWebhooks: PlaidWebooks, private readonly plaidWebhooks: PlaidWebooks,
private readonly clsService: ClsService,
@Inject(SystemPlaidItem.name)
private readonly systemPlaidItemModel: typeof SystemPlaidItem,
@Inject(TenantModel.name)
private readonly tenantModel: typeof TenantModel,
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
) {} ) {}
/** /**
@@ -39,7 +25,7 @@ export class PlaidApplication {
* @param {PlaidItemDTO} itemDTO * @param {PlaidItemDTO} itemDTO
* @returns * @returns
*/ */
public exchangeToken(itemDTO: PlaidItemDto): Promise<void> { public exchangeToken(itemDTO: PlaidItemDTO): Promise<void> {
return this.plaidItemService.item(itemDTO); return this.plaidItemService.item(itemDTO);
} }
@@ -55,33 +41,10 @@ export class PlaidApplication {
webhookType: string, webhookType: string,
webhookCode: string, webhookCode: string,
): Promise<void> { ): Promise<void> {
return this.plaidWebhooks.webhooks(plaidItemId, webhookType, webhookCode); return this.plaidWebhooks.webhooks(
} plaidItemId,
webhookType,
public async setupPlaidTenant(plaidItemId: string, callback: () => void) { webhookCode,
const plaidItem = await this.systemPlaidItemModel );
.query()
.findOne({ plaidItemId });
if (!plaidItem) {
throw new Error('Plaid item not found');
}
const tenant = await this.tenantModel
.query()
.findOne({ id: plaidItem.tenantId })
.throwIfNotFound();
const user = await this.systemUserModel
.query()
.findOne({
tenantId: tenant.id,
})
.modify('active')
.throwIfNotFound();
this.clsService.set('organizationId', tenant.organizationId);
this.clsService.set('userId', user.id);
return callback();
} }
} }

View File

@@ -0,0 +1,32 @@
// import { Request, Response, NextFunction } from 'express';
// import { SystemPlaidItem, Tenant } from '@/system/models';
// import tenantDependencyInjection from '@/api/middleware/TenantDependencyInjection';
// export const PlaidWebhookTenantBootMiddleware = async (
// req: Request,
// res: Response,
// next: NextFunction
// ) => {
// const { item_id: plaidItemId } = req.body;
// const plaidItem = await SystemPlaidItem.query().findOne({ plaidItemId });
// const notFoundOrganization = () => {
// return res.boom.unauthorized('Organization identication not found.', {
// errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }],
// });
// };
// // In case the given organization not found.
// if (!plaidItem) {
// return notFoundOrganization();
// }
// const tenant = await Tenant.query()
// .findById(plaidItem.tenantId)
// .withGraphFetched('metadata');
// // When the given organization id not found on the system storage.
// if (!tenant) {
// return notFoundOrganization();
// }
// tenantDependencyInjection(req, tenant);
// next();
// };

View File

@@ -6,9 +6,11 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { SystemPlaidItem } from '../models/SystemPlaidItem'; import { SystemPlaidItem } from '../models/SystemPlaidItem';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { IPlaidItemCreatedEventPayload } from '../types/BankingPlaid.types'; import {
IPlaidItemCreatedEventPayload,
PlaidItemDTO,
} from '../types/BankingPlaid.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { PlaidItemDto } from '../dtos/PlaidItem.dto';
@Injectable() @Injectable()
export class PlaidItemService { export class PlaidItemService {
@@ -17,7 +19,9 @@ export class PlaidItemService {
private readonly tenancyContext: TenancyContext, private readonly tenancyContext: TenancyContext,
@Inject(SystemPlaidItem.name) @Inject(SystemPlaidItem.name)
private readonly systemPlaidItemModel: typeof SystemPlaidItem, private readonly systemPlaidItemModel: TenantModelProxy<
typeof SystemPlaidItem
>,
@Inject(PlaidItem.name) @Inject(PlaidItem.name)
private readonly plaidItemModel: TenantModelProxy<typeof PlaidItem>, private readonly plaidItemModel: TenantModelProxy<typeof PlaidItem>,
@@ -29,10 +33,10 @@ export class PlaidItemService {
/** /**
* Exchanges the public token to get access token and item id and then creates * Exchanges the public token to get access token and item id and then creates
* a new Plaid item. * a new Plaid item.
* @param {PlaidItemDto} itemDTO - Plaid item data transfer object. * @param {PlaidItemDTO} itemDTO - Plaid item data transfer object.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async item(itemDTO: PlaidItemDto): Promise<void> { public async item(itemDTO: PlaidItemDTO): Promise<void> {
const { publicToken, institutionId } = itemDTO; const { publicToken, institutionId } = itemDTO;
const tenant = await this.tenancyContext.getTenant(); const tenant = await this.tenancyContext.getTenant();
@@ -53,7 +57,7 @@ export class PlaidItemService {
plaidInstitutionId: institutionId, plaidInstitutionId: institutionId,
}); });
// Stores the Plaid item id on system scope. // Stores the Plaid item id on system scope.
await this.systemPlaidItemModel.query().insert({ tenantId, plaidItemId }); await this.systemPlaidItemModel().query().insert({ tenantId, plaidItemId });
// Triggers `onPlaidItemCreated` event. // Triggers `onPlaidItemCreated` event.
await this.eventEmitter.emitAsync(events.plaid.onItemCreated, { await this.eventEmitter.emitAsync(events.plaid.onItemCreated, {

View File

@@ -1,6 +1,5 @@
import * as R from 'ramda'; import * as R from 'ramda';
import * as bluebird from 'bluebird'; import bluebird from 'bluebird';
import * as uniqid from 'uniqid';
import { entries, groupBy } from 'lodash'; import { entries, groupBy } from 'lodash';
import { import {
AccountBase as PlaidAccountBase, AccountBase as PlaidAccountBase,
@@ -13,6 +12,7 @@ import {
transformPlaidTrxsToCashflowCreate, transformPlaidTrxsToCashflowCreate,
} from '../utils'; } from '../utils';
import { Knex } from 'knex'; import { Knex } from 'knex';
import uniqid from 'uniqid';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { RemovePendingUncategorizedTransaction } from '../../BankingTransactions/commands/RemovePendingUncategorizedTransaction.service'; import { RemovePendingUncategorizedTransaction } from '../../BankingTransactions/commands/RemovePendingUncategorizedTransaction.service';
import { CreateAccountService } from '../../Accounts/CreateAccount.service'; import { CreateAccountService } from '../../Accounts/CreateAccount.service';

View File

@@ -15,13 +15,6 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable() @Injectable()
export class PlaidUpdateTransactions { export class PlaidUpdateTransactions {
/**
* Constructor method.
* @param {PlaidSyncDb} plaidSync - Plaid sync service.
* @param {UnitOfWork} uow - Unit of work.
* @param {TenantModelProxy<typeof PlaidItem>} plaidItemModel - Plaid item model.
* @param {PlaidApi} plaidClient - Plaid client.
*/
constructor( constructor(
private readonly plaidSync: PlaidSyncDb, private readonly plaidSync: PlaidSyncDb,
private readonly uow: UnitOfWork, private readonly uow: UnitOfWork,
@@ -35,7 +28,8 @@ export class PlaidUpdateTransactions {
/** /**
* Handles sync the Plaid item to Bigcaptial under UOW. * Handles sync the Plaid item to Bigcaptial under UOW.
* @param {string} plaidItemId - Plaid item id. * @param {number} tenantId - Tenant id.
* @param {number} plaidItemId - Plaid item id.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>} * @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/ */
public async updateTransactions(plaidItemId: string) { public async updateTransactions(plaidItemId: string) {
@@ -50,8 +44,8 @@ export class PlaidUpdateTransactions {
* - New bank accounts. * - New bank accounts.
* - Last accounts feeds updated at. * - Last accounts feeds updated at.
* - Turn on the accounts feed flag. * - Turn on the accounts feed flag.
* @param {number} tenantId - Tenant ID.
* @param {string} plaidItemId - The Plaid ID for the item. * @param {string} plaidItemId - The Plaid ID for the item.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>} * @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/ */
public async updateTransactionsWork( public async updateTransactionsWork(
@@ -103,6 +97,7 @@ export class PlaidUpdateTransactions {
/** /**
* Fetches transactions from the `Plaid API` for a given item. * Fetches transactions from the `Plaid API` for a given item.
* @param {number} tenantId - Tenant ID.
* @param {string} plaidItemId - The Plaid ID for the item. * @param {string} plaidItemId - The Plaid ID for the item.
* @returns {Promise<PlaidFetchedTransactionsUpdates>} * @returns {Promise<PlaidFetchedTransactionsUpdates>}
*/ */

View File

@@ -1,4 +1,3 @@
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { PlaidItem } from '../models/PlaidItem'; import { PlaidItem } from '../models/PlaidItem';
import { PlaidUpdateTransactions } from './PlaidUpdateTransactions'; import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
@@ -9,7 +8,7 @@ export class PlaidWebooks {
private readonly updateTransactionsService: PlaidUpdateTransactions, private readonly updateTransactionsService: PlaidUpdateTransactions,
@Inject(PlaidItem.name) @Inject(PlaidItem.name)
private readonly plaidItemModel: TenantModelProxy<typeof PlaidItem>, private readonly plaidItemModel: typeof PlaidItem,
) {} ) {}
/** /**
@@ -77,10 +76,11 @@ export class PlaidWebooks {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async handleTransactionsWebooks( public async handleTransactionsWebooks(
tenantId: number,
plaidItemId: string, plaidItemId: string,
webhookCode: string, webhookCode: string,
): Promise<void> { ): Promise<void> {
const plaidItem = await this.plaidItemModel() const plaidItem = await this.plaidItemModel
.query() .query()
.findOne({ plaidItemId }) .findOne({ plaidItemId })
.throwIfNotFound(); .throwIfNotFound();
@@ -122,8 +122,9 @@ export class PlaidWebooks {
/** /**
* Handles all Item webhook events. * Handles all Item webhook events.
* @param {string} plaidItemId - The Plaid ID for the item * @param {number} tenantId - Tenant ID
* @param {string} webhookCode - The webhook code * @param {string} webhookCode - The webhook code
* @param {string} plaidItemId - The Plaid ID for the item
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async itemsHandler( public async itemsHandler(

View File

@@ -1,54 +0,0 @@
import { ClsService } from 'nestjs-cls';
import { Inject, Injectable } from '@nestjs/common';
import { SystemPlaidItem } from '../models/SystemPlaidItem';
import { TenantModel } from '@/modules/System/models/TenantModel';
import { SystemUser } from '@/modules/System/models/SystemUser';
@Injectable()
export class SetupPlaidItemTenantService {
constructor(
private readonly clsService: ClsService,
@Inject(SystemPlaidItem.name)
private readonly systemPlaidItemModel: typeof SystemPlaidItem,
@Inject(TenantModel.name)
private readonly tenantModel: typeof TenantModel,
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
) {}
/**
* Sets up the Plaid tenant.
* @param {string} plaidItemId - The Plaid item id.
* @param {() => void} callback - The callback function to execute after setting up the Plaid tenant.
* @returns {Promise<void>}
*/
public async setupPlaidTenant(plaidItemId: string, callback: () => void) {
const plaidItem = await this.systemPlaidItemModel
.query()
.findOne({ plaidItemId });
if (!plaidItem) {
throw new Error('Plaid item not found');
}
const tenant = await this.tenantModel
.query()
.findOne({ id: plaidItem.tenantId })
.throwIfNotFound();
const user = await this.systemUserModel
.query()
.findOne({
tenantId: tenant.id,
})
.modify('active')
.throwIfNotFound();
this.clsService.set('organizationId', tenant.organizationId);
this.clsService.set('userId', user.id);
return callback();
}
}

View File

@@ -1,26 +0,0 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class PlaidItemDto {
@IsString()
@IsNotEmpty()
publicToken: string;
@IsString()
@IsNotEmpty()
institutionId: string;
}
export class PlaidWebhookDto {
@IsString()
@IsNotEmpty()
itemId: string;
@IsString()
@IsNotEmpty()
webhookType: string;
@IsString()
@IsNotEmpty()
webhookCode: string;
}

View File

@@ -1,46 +1,43 @@
import { Process } from '@nestjs/bull'; // import Container, { Service } from 'typedi';
import { UseCls } from 'nestjs-cls'; // import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
import { Processor, WorkerHost } from '@nestjs/bullmq'; // import { IPlaidItemCreatedEventPayload } from '@/interfaces';
import { Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import {
PlaidFetchTransitonsEventPayload,
UpdateBankingPlaidTransitionsJob,
UpdateBankingPlaidTransitionsQueueJob,
} from '../types/BankingPlaid.types';
import { PlaidUpdateTransactions } from '../command/PlaidUpdateTransactions';
import { SetupPlaidItemTenantService } from '../command/SetupPlaidItemTenant.service';
@Processor({ // @Service()
name: UpdateBankingPlaidTransitionsQueueJob, // export class PlaidFetchTransactionsJob {
scope: Scope.REQUEST, // /**
}) // * Constructor method.
export class PlaidFetchTransactionsProcessor extends WorkerHost { // */
constructor( // constructor(agenda) {
private readonly plaidFetchTransactionsService: PlaidUpdateTransactions, // agenda.define(
private readonly setupPlaidItemService: SetupPlaidItemTenantService, // 'plaid-update-account-transactions',
) { // { priority: 'high', concurrency: 2 },
super(); // this.handler
} // );
// }
/** // /**
* Triggers the function. // * Triggers the function.
*/ // */
@Process(UpdateBankingPlaidTransitionsJob) // private handler = async (job, done: Function) => {
@UseCls() // const { tenantId, plaidItemId } = job.attrs
async process(job: Job<PlaidFetchTransitonsEventPayload>) { // .data as IPlaidItemCreatedEventPayload;
const { plaidItemId } = job.data;
try { // const plaidFetchTransactionsService = Container.get(
await this.setupPlaidItemService.setupPlaidTenant(plaidItemId, () => { // PlaidUpdateTransactions
return this.plaidFetchTransactionsService.updateTransactions( // );
plaidItemId, // const io = Container.get('socket');
);
}); // try {
// Notify the frontend to reflect the new transactions changes. // await plaidFetchTransactionsService.updateTransactions(
// tenantId,
// plaidItemId
// );
// // Notify the frontend to reflect the new transactions changes.
// io.emit('NEW_TRANSACTIONS_DATA', { plaidItemId }); // io.emit('NEW_TRANSACTIONS_DATA', { plaidItemId });
} catch (error) { // done();
console.log(error); // } catch (error) {
} // console.log(error);
} // done(error);
} // }
// };
// }

View File

@@ -30,7 +30,7 @@ export class SystemPlaidItem extends BaseModel {
* Relationship mapping. * Relationship mapping.
*/ */
static get relationMappings() { static get relationMappings() {
const { TenantModel } = require('../../System/models/TenantModel'); const Tenant = require('system/models/Tenant');
return { return {
/** /**
@@ -38,7 +38,7 @@ export class SystemPlaidItem extends BaseModel {
*/ */
tenant: { tenant: {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: TenantModel, modelClass: Tenant.default,
join: { join: {
from: 'users.tenantId', from: 'users.tenantId',
to: 'tenants.id', to: 'tenants.id',

View File

@@ -1,34 +1,22 @@
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { Queue } from 'bullmq'; import { IPlaidItemCreatedEventPayload } from '../types/BankingPlaid.types';
import { InjectQueue } from '@nestjs/bullmq';
import {
IPlaidItemCreatedEventPayload,
UpdateBankingPlaidTransitionsJob,
UpdateBankingPlaidTransitionsQueueJob,
} from '../types/BankingPlaid.types';
@Injectable() @Injectable()
export class PlaidUpdateTransactionsOnItemCreatedSubscriber { export class PlaidUpdateTransactionsOnItemCreatedSubscriber {
constructor(
@InjectQueue(UpdateBankingPlaidTransitionsQueueJob)
private readonly updateTransitionsQueue: Queue,
) {}
/** /**
* Updates the Plaid item transactions * Updates the Plaid item transactions
* @param {IPlaidItemCreatedEventPayload} payload - Event payload. * @param {IPlaidItemCreatedEventPayload} payload - Event payload.
*/ */
@OnEvent(events.plaid.onItemCreated) @OnEvent(events.plaid.onItemCreated)
public async handleUpdateTransactionsOnItemCreated({ public async handleUpdateTransactionsOnItemCreated({
tenantId,
plaidItemId, plaidItemId,
plaidAccessToken,
plaidInstitutionId,
}: IPlaidItemCreatedEventPayload) { }: IPlaidItemCreatedEventPayload) {
const payload = { plaidItemId }; const payload = { tenantId, plaidItemId };
// await this.agenda.now('plaid-update-account-transactions', payload);
await this.updateTransitionsQueue.add( };
UpdateBankingPlaidTransitionsJob,
payload,
);
}
} }

View File

@@ -1,11 +1,11 @@
import { Knex } from 'knex'; import { Knex } from "knex";
import { RemovedTransaction, Transaction } from 'plaid'; import { RemovedTransaction, Transaction } from "plaid";
export interface IPlaidTransactionsSyncedEventPayload { export interface IPlaidTransactionsSyncedEventPayload {
// tenantId: number; // tenantId: number;
plaidAccountId: number; plaidAccountId: number;
batch: string; batch: string;
trx?: Knex.Transaction; trx?: Knex.Transaction
} }
export interface PlaidItemDTO { export interface PlaidItemDTO {
@@ -13,6 +13,7 @@ export interface PlaidItemDTO {
institutionId: string; institutionId: string;
} }
export interface PlaidFetchedTransactionsUpdates { export interface PlaidFetchedTransactionsUpdates {
added: Transaction[]; added: Transaction[];
modified: Transaction[]; modified: Transaction[];
@@ -21,20 +22,11 @@ export interface PlaidFetchedTransactionsUpdates {
cursor: string; cursor: string;
} }
export interface IPlaidItemCreatedEventPayload { export interface IPlaidItemCreatedEventPayload {
tenantId: number; tenantId: number;
plaidAccessToken: string; plaidAccessToken: string;
plaidItemId: string; plaidItemId: string;
plaidInstitutionId: string; plaidInstitutionId: string;
} }
export const UpdateBankingPlaidTransitionsJob =
'update-banking-plaid-transitions-job';
export const UpdateBankingPlaidTransitionsQueueJob =
'update-banking-plaid-transitions-query';
export interface PlaidFetchTransitonsEventPayload {
plaidItemId: string;
}

View File

@@ -1,25 +0,0 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
@Controller('banking/recognized')
@ApiTags('banking-recognized')
export class BankingRecognizedTransactionsController {
constructor(
private readonly recognizedTransactionsApplication: RecognizedTransactionsApplication,
) {}
@Get(':recognizedTransactionId')
async getRecognizedTransaction(
@Param('recognizedTransactionId') recognizedTransactionId: number,
) {
return this.recognizedTransactionsApplication.getRecognizedTransaction(
Number(recognizedTransactionId),
);
}
@Get()
async getRecognizedTransactions(@Query() query: any) {
return this.recognizedTransactionsApplication.getRecognizedTransactions(query);
}
}

View File

@@ -1,46 +1,32 @@
import { forwardRef, Module } from '@nestjs/common'; import { forwardRef, Module } from '@nestjs/common';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module'; import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { RecognizedBankTransaction } from './models/RecognizedBankTransaction'; import { RecognizedBankTransaction } from './models/RecognizedBankTransaction';
import { GetAutofillCategorizeTransactionService } from './queries/GetAutofillCategorizeTransaction.service';
import { RevertRecognizedTransactionsService } from './commands/RevertRecognizedTransactions.service'; import { RevertRecognizedTransactionsService } from './commands/RevertRecognizedTransactions.service';
import { RecognizeTranasctionsService } from './commands/RecognizeTranasctions.service'; import { RecognizeTranasctionsService } from './commands/RecognizeTranasctions.service';
import { TriggerRecognizedTransactionsSubscriber } from './events/TriggerRecognizedTransactions'; import { TriggerRecognizedTransactionsSubscriber } from './events/TriggerRecognizedTransactions';
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module'; import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
import { BankRulesModule } from '../BankRules/BankRules.module'; import { BankRulesModule } from '../BankRules/BankRules.module';
import { BankingRecognizedTransactionsController } from './BankingRecognizedTransactions.controller';
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
import { GetRecognizedTransactionService } from './queries/GetRecognizedTransaction.service';
import { BullModule } from '@nestjs/bullmq';
import { RecognizeUncategorizedTransactionsQueue } from './_types';
import { RegonizeTransactionsPrcessor } from './jobs/RecognizeTransactionsJob';
import { TenancyModule } from '../Tenancy/Tenancy.module';
const models = [RegisterTenancyModel(RecognizedBankTransaction)]; const models = [RegisterTenancyModel(RecognizedBankTransaction)];
@Module({ @Module({
imports: [ imports: [
BankingTransactionsModule, BankingTransactionsModule,
TenancyModule,
forwardRef(() => BankRulesModule), forwardRef(() => BankRulesModule),
BullModule.registerQueue({
name: RecognizeUncategorizedTransactionsQueue,
}),
...models, ...models,
], ],
providers: [ providers: [
RecognizedTransactionsApplication, GetAutofillCategorizeTransactionService,
GetRecognizedTransactionsService,
RevertRecognizedTransactionsService, RevertRecognizedTransactionsService,
RecognizeTranasctionsService, RecognizeTranasctionsService,
TriggerRecognizedTransactionsSubscriber, TriggerRecognizedTransactionsSubscriber,
GetRecognizedTransactionService,
RegonizeTransactionsPrcessor,
], ],
exports: [ exports: [
...models, ...models,
GetAutofillCategorizeTransactionService,
RevertRecognizedTransactionsService, RevertRecognizedTransactionsService,
RecognizeTranasctionsService, RecognizeTranasctionsService,
], ],
controllers: [BankingRecognizedTransactionsController],
}) })
export class BankingTransactionsRegonizeModule {} export class BankingTransactionsRegonizeModule {}

View File

@@ -1,57 +0,0 @@
import { Knex } from 'knex';
import { Injectable } from '@nestjs/common';
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
import { GetRecognizedTransactionService } from './queries/GetRecognizedTransaction.service';
import { RevertRecognizedTransactionsService } from './commands/RevertRecognizedTransactions.service';
import { IGetRecognizedTransactionsQuery } from '../BankingTransactions/types/BankingTransactions.types';
import { RevertRecognizedTransactionsCriteria } from './_types';
@Injectable()
export class RecognizedTransactionsApplication {
constructor(
private readonly getRecognizedTransactionsService: GetRecognizedTransactionsService,
private readonly getRecognizedTransactionService: GetRecognizedTransactionService,
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
) {}
/**
* Retrieves the recognized transactions based on the provided filter.
* @param {IGetRecognizedTransactionsQuery} filter - Filter criteria.
* @returns {Promise<{ data: any[], pagination: any }>}
*/
public getRecognizedTransactions(filter: IGetRecognizedTransactionsQuery) {
return this.getRecognizedTransactionsService.getRecognizedTranactions(
filter,
);
}
/**
* Retrieves a specific recognized transaction by ID.
* @param {number} recognizedTransactionId - The ID of the recognized transaction.
* @returns {Promise<any>}
*/
public getRecognizedTransaction(recognizedTransactionId: number) {
return this.getRecognizedTransactionService.getRecognizedTransaction(
recognizedTransactionId,
);
}
/**
* Reverts a recognized transaction.
* @param {number} ruleId - The ID of the recognized transaction to revert.
* @param {RevertRecognizedTransactionsCriteria} transactionsCriteria - The criteria for the recognized transaction to revert.
* @param {Knex.Transaction} trx - The transaction to use for the revert operation.
* @returns {Promise<any>}
*/
public revertRecognizedTransactions(
ruleId?: number | Array<number>,
transactionsCriteria?: RevertRecognizedTransactionsCriteria,
trx?: Knex.Transaction,
) {
return this.revertRecognizedTransactionsService.revertRecognizedTransactions(
ruleId,
transactionsCriteria,
trx,
);
}
}

View File

@@ -1,5 +1,3 @@
import { TenantJobPayload } from "@/interfaces/Tenant";
export interface RevertRecognizedTransactionsCriteria { export interface RevertRecognizedTransactionsCriteria {
batch?: string; batch?: string;
accountId?: number; accountId?: number;
@@ -9,14 +7,3 @@ export interface RecognizeTransactionsCriteria {
batch?: string; batch?: string;
accountId?: number; accountId?: number;
} }
export const RecognizeUncategorizedTransactionsJob =
'recognize-uncategorized-transactions-job';
export const RecognizeUncategorizedTransactionsQueue =
'recognize-uncategorized-transactions-queue';
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
ruleId: number,
transactionsCriteria: any;
}

View File

@@ -2,47 +2,21 @@ import { isEqual, omit } from 'lodash';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { import { IBankRuleEventCreatedPayload, IBankRuleEventDeletedPayload, IBankRuleEventEditedPayload } from '@/modules/BankRules/types';
IBankRuleEventCreatedPayload,
IBankRuleEventDeletedPayload,
IBankRuleEventEditedPayload,
} from '@/modules/BankRules/types';
import { Queue } from 'bullmq';
import { InjectQueue } from '@nestjs/bullmq';
import {
RecognizeUncategorizedTransactionsJob,
RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue,
} from '../_types';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable() @Injectable()
export class TriggerRecognizedTransactionsSubscriber { export class TriggerRecognizedTransactionsSubscriber {
constructor(
private readonly tenancyContect: TenancyContext,
@InjectQueue(RecognizeUncategorizedTransactionsQueue)
private readonly recognizeTransactionsQueue: Queue,
) {}
/** /**
* Triggers the recognize uncategorized transactions job on rule created. * Triggers the recognize uncategorized transactions job on rule created.
* @param {IBankRuleEventCreatedPayload} payload - * @param {IBankRuleEventCreatedPayload} payload -
*/ */
@OnEvent(events.bankRules.onCreated) @OnEvent(events.bankRules.onCreated)
async recognizedTransactionsOnRuleCreated({ private async recognizedTransactionsOnRuleCreated({
bankRule, bankRule,
}: IBankRuleEventCreatedPayload) { }: IBankRuleEventCreatedPayload) {
const tenantPayload = await this.tenancyContect.getTenantJobPayload(); const payload = { ruleId: bankRule.id };
const payload = {
ruleId: bankRule.id,
...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload;
await this.recognizeTransactionsQueue.add( // await this.agenda.now('recognize-uncategorized-transactions-job', payload);
RecognizeUncategorizedTransactionsJob,
payload,
);
} }
/** /**
@@ -50,33 +24,27 @@ export class TriggerRecognizedTransactionsSubscriber {
* @param {IBankRuleEventEditedPayload} payload - * @param {IBankRuleEventEditedPayload} payload -
*/ */
@OnEvent(events.bankRules.onEdited) @OnEvent(events.bankRules.onEdited)
async recognizedTransactionsOnRuleEdited({ private async recognizedTransactionsOnRuleEdited({
editRuleDTO, editRuleDTO,
oldBankRule, oldBankRule,
bankRule, bankRule,
}: IBankRuleEventEditedPayload) { }: IBankRuleEventEditedPayload) {
const payload = { ruleId: bankRule.id };
// Cannot continue if the new and old bank rule values are the same, // Cannot continue if the new and old bank rule values are the same,
// after excluding `createdAt` and `updatedAt` dates. // after excluding `createdAt` and `updatedAt` dates.
if ( if (
isEqual( isEqual(
omit(bankRule, ['createdAt', 'updatedAt']), omit(bankRule, ['createdAt', 'updatedAt']),
omit(oldBankRule, ['createdAt', 'updatedAt']), omit(oldBankRule, ['createdAt', 'updatedAt'])
) )
) { ) {
return; return;
} }
const tenantPayload = await this.tenancyContect.getTenantJobPayload(); // await this.agenda.now(
const payload = { // 'rerecognize-uncategorized-transactions-job',
ruleId: bankRule.id, // payload
...tenantPayload, // );
} as RecognizeUncategorizedTransactionsJobPayload;
// Re-recognize the transactions based on the new rules.
await this.recognizeTransactionsQueue.add(
RecognizeUncategorizedTransactionsJob,
payload,
);
} }
/** /**
@@ -84,20 +52,15 @@ export class TriggerRecognizedTransactionsSubscriber {
* @param {IBankRuleEventDeletedPayload} payload - * @param {IBankRuleEventDeletedPayload} payload -
*/ */
@OnEvent(events.bankRules.onDeleted) @OnEvent(events.bankRules.onDeleted)
async recognizedTransactionsOnRuleDeleted({ private async recognizedTransactionsOnRuleDeleted({
ruleId, ruleId,
}: IBankRuleEventDeletedPayload) { }: IBankRuleEventDeletedPayload) {
const tenantPayload = await this.tenancyContect.getTenantJobPayload(); const payload = { ruleId };
const payload = {
ruleId,
...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload;
// Re-recognize the transactions based on the new rules. // await this.agenda.now(
await this.recognizeTransactionsQueue.add( // 'revert-recognized-uncategorized-transactions-job',
RecognizeUncategorizedTransactionsJob, // payload
payload, // );
);
} }
/** /**
@@ -105,7 +68,7 @@ export class TriggerRecognizedTransactionsSubscriber {
* @param {IImportFileCommitedEventPayload} payload - * @param {IImportFileCommitedEventPayload} payload -
*/ */
@OnEvent(events.import.onImportCommitted) @OnEvent(events.import.onImportCommitted)
async triggerRecognizeTransactionsOnImportCommitted({ private async triggerRecognizeTransactionsOnImportCommitted({
importId, importId,
// @ts-ignore // @ts-ignore
@@ -113,8 +76,10 @@ export class TriggerRecognizedTransactionsSubscriber {
// const importFile = await Import.query().findOne({ importId }); // const importFile = await Import.query().findOne({ importId });
// const batch = importFile.paramsParsed.batch; // const batch = importFile.paramsParsed.batch;
// const payload = { transactionsCriteria: { batch } }; // const payload = { transactionsCriteria: { batch } };
// // Cannot continue if the imported resource is not bank account transactions. // // Cannot continue if the imported resource is not bank account transactions.
// if (importFile.resource !== 'UncategorizedCashflowTransaction') return; // if (importFile.resource !== 'UncategorizedCashflowTransaction') return;
// await this.agenda.now('recognize-uncategorized-transactions-job', payload); // await this.agenda.now('recognize-uncategorized-transactions-job', payload);
} }
} }

View File

@@ -1,48 +1,36 @@
import { Job } from 'bullmq'; // import Container, { Service } from 'typedi';
import { Processor, WorkerHost } from '@nestjs/bullmq'; // import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
import {
RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue,
} from '../_types';
import { Process } from '@nestjs/bull';
@Processor({ // @Service()
name: RecognizeUncategorizedTransactionsQueue, // export class RegonizeTransactionsJob {
scope: Scope.REQUEST, // /**
}) // * Constructor method.
export class RegonizeTransactionsPrcessor extends WorkerHost { // */
/** // constructor(agenda) {
* @param {RecognizeTranasctionsService} recognizeTranasctionsService - // agenda.define(
* @param {ClsService} clsService - // 'recognize-uncategorized-transactions-job',
*/ // { priority: 'high', concurrency: 2 },
constructor( // this.handler
private readonly recognizeTranasctionsService: RecognizeTranasctionsService, // );
private readonly clsService: ClsService, // }
) {
super();
}
/** // /**
* Triggers sending invoice mail. // * Triggers sending invoice mail.
*/ // */
@Process(RecognizeUncategorizedTransactionsQueue) // private handler = async (job, done: Function) => {
@UseCls() // const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) { // const regonizeTransactions = Container.get(RecognizeTranasctionsService);
const { ruleId, transactionsCriteria } = job.data;
this.clsService.set('organizationId', job.data.organizationId); // try {
this.clsService.set('userId', job.data.userId); // await regonizeTransactions.recognizeTransactions(
// tenantId,
try { // ruleId,
await this.recognizeTranasctionsService.recognizeTransactions( // transactionsCriteria
ruleId, // );
transactionsCriteria, // done();
); // } catch (error) {
} catch (error) { // console.log(error);
console.log(error); // done(error);
} // }
} // };
} // }

View File

@@ -0,0 +1,54 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { BankingTransactionsApplication } from './BankingTransactionsApplication.service';
import {
IBankAccountsFilter,
ICashflowAccountTransactionsQuery,
} from './types/BankingTransactions.types';
import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto';
@Controller('banking/transactions')
@ApiTags('banking-transactions')
export class BankingTransactionsController {
constructor(
private readonly bankingTransactionsApplication: BankingTransactionsApplication,
) {}
@Get()
async getBankAccountTransactions(
@Query() query: ICashflowAccountTransactionsQuery,
) {
return this.bankingTransactionsApplication.getBankAccountTransactions(
query,
);
}
@Post()
async createTransaction(@Body() transactionDTO: CreateBankTransactionDto) {
return this.bankingTransactionsApplication.createTransaction(
transactionDTO,
);
}
@Delete(':id')
async deleteTransaction(@Param('id') transactionId: string) {
return this.bankingTransactionsApplication.deleteTransaction(
Number(transactionId),
);
}
@Get(':id')
async getTransaction(@Param('id') transactionId: string) {
return this.bankingTransactionsApplication.getTransaction(
Number(transactionId),
);
}
}

View File

@@ -19,19 +19,13 @@ import { CommandBankTransactionValidator } from './commands/CommandCasflowValida
import { BranchTransactionDTOTransformer } from '../Branches/integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransformer } from '../Branches/integrations/BranchTransactionDTOTransform';
import { BranchesModule } from '../Branches/Branches.module'; import { BranchesModule } from '../Branches/Branches.module';
import { RemovePendingUncategorizedTransaction } from './commands/RemovePendingUncategorizedTransaction.service'; import { RemovePendingUncategorizedTransaction } from './commands/RemovePendingUncategorizedTransaction.service';
import { BankingTransactionsController } from './controllers/BankingTransactions.controller'; import { BankingTransactionsController } from './BankingTransactions.controller';
import { GetBankAccountsService } from './queries/GetBankAccounts.service'; import { GetBankAccountsService } from './queries/GetBankAccounts.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module'; import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { BankAccount } from './models/BankAccount'; import { BankAccount } from './models/BankAccount';
import { LedgerModule } from '../Ledger/Ledger.module'; import { LedgerModule } from '../Ledger/Ledger.module';
import { GetBankAccountTransactionsService } from './queries/GetBankAccountTransactions/GetBankAccountTransactions.service'; import { GetBankAccountTransactionsService } from './queries/GetBankAccountTransactions/GetBankAccountTransactions.service';
import { GetBankAccountTransactionsRepository } from './queries/GetBankAccountTransactions/GetBankAccountTransactionsRepo.service'; import { GetBankAccountTransactionsRepository } from './queries/GetBankAccountTransactions/GetBankAccountTransactionsRepo.service';
import { GetUncategorizedTransactions } from './queries/GetUncategorizedTransactions';
import { GetUncategorizedBankTransactionService } from './queries/GetUncategorizedBankTransaction.service';
import { BankingUncategorizedTransactionsController } from './controllers/BankingUncategorizedTransactions.controller';
import { BankingPendingTransactionsController } from './controllers/BankingPendingTransactions.controller';
import { GetPendingBankAccountTransactions } from './queries/GetPendingBankAccountTransaction.service';
import { GetAutofillCategorizeTransactionService } from './queries/GetAutofillCategorizeTransaction/GetAutofillCategorizeTransaction.service';
const models = [ const models = [
RegisterTenancyModel(UncategorizedBankTransaction), RegisterTenancyModel(UncategorizedBankTransaction),
@@ -48,11 +42,7 @@ const models = [
DynamicListModule, DynamicListModule,
...models, ...models,
], ],
controllers: [ controllers: [BankingTransactionsController],
BankingTransactionsController,
BankingUncategorizedTransactionsController,
BankingPendingTransactionsController,
],
providers: [ providers: [
BankTransactionAutoIncrement, BankTransactionAutoIncrement,
BankTransactionGLEntriesService, BankTransactionGLEntriesService,
@@ -71,16 +61,7 @@ const models = [
RemovePendingUncategorizedTransaction, RemovePendingUncategorizedTransaction,
GetBankAccountTransactionsRepository, GetBankAccountTransactionsRepository,
GetBankAccountTransactionsService, GetBankAccountTransactionsService,
GetUncategorizedTransactions,
GetUncategorizedBankTransactionService,
GetPendingBankAccountTransactions,
GetAutofillCategorizeTransactionService,
],
exports: [
...models,
RemovePendingUncategorizedTransaction,
CommandBankTransactionValidator,
CreateBankTransactionService
], ],
exports: [...models, RemovePendingUncategorizedTransaction],
}) })
export class BankingTransactionsModule {} export class BankingTransactionsModule {}

View File

@@ -9,13 +9,6 @@ import {
import { GetBankAccountsService } from './queries/GetBankAccounts.service'; import { GetBankAccountsService } from './queries/GetBankAccounts.service';
import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto'; import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto';
import { GetBankAccountTransactionsService } from './queries/GetBankAccountTransactions/GetBankAccountTransactions.service'; import { GetBankAccountTransactionsService } from './queries/GetBankAccountTransactions/GetBankAccountTransactions.service';
import { GetUncategorizedTransactions } from './queries/GetUncategorizedTransactions';
import { GetUncategorizedBankTransactionService } from './queries/GetUncategorizedBankTransaction.service';
import { GetUncategorizedTransactionsQueryDto } from './dtos/GetUncategorizedTransactionsQuery.dto';
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() @Injectable()
export class BankingTransactionsApplication { export class BankingTransactionsApplication {
@@ -25,10 +18,6 @@ export class BankingTransactionsApplication {
private readonly getCashflowTransactionService: GetBankTransactionService, private readonly getCashflowTransactionService: GetBankTransactionService,
private readonly getBankAccountsService: GetBankAccountsService, private readonly getBankAccountsService: GetBankAccountsService,
private readonly getBankAccountTransactionsService: GetBankAccountTransactionsService, private readonly getBankAccountTransactionsService: GetBankAccountTransactionsService,
private readonly getBankAccountUncategorizedTransitionsService: GetUncategorizedTransactions,
private readonly getBankAccountUncategorizedTransactionService: GetUncategorizedBankTransactionService,
private readonly getPendingBankAccountTransactionsService: GetPendingBankAccountTransactions,
private readonly getAutofillCategorizeTransactionService: GetAutofillCategorizeTransactionService,
) {} ) {}
/** /**
@@ -55,7 +44,7 @@ export class BankingTransactionsApplication {
* Retrieves the bank transactions of the given bank id. * Retrieves the bank transactions of the given bank id.
* @param {ICashflowAccountTransactionsQuery} query * @param {ICashflowAccountTransactionsQuery} query
*/ */
public getBankAccountTransactions(query: GetBankTransactionsQueryDto) { public getBankAccountTransactions(query: ICashflowAccountTransactionsQuery) {
return this.getBankAccountTransactionsService.bankAccountTransactions( return this.getBankAccountTransactionsService.bankAccountTransactions(
query, query,
); );
@@ -79,53 +68,4 @@ export class BankingTransactionsApplication {
public getBankAccounts(filterDTO: IBankAccountsFilter) { public getBankAccounts(filterDTO: IBankAccountsFilter) {
return this.getBankAccountsService.getBankAccounts(filterDTO); return this.getBankAccountsService.getBankAccounts(filterDTO);
} }
/**
* Retrieves the uncategorized cashflow transactions.
* @param {number} accountId - Account id.
* @param {IGetUncategorizedTransactionsQuery} query - Query.
*/
public getBankAccountUncategorizedTransactions(
accountId: number,
query: GetUncategorizedTransactionsQueryDto,
) {
return this.getBankAccountUncategorizedTransitionsService.getTransactions(
accountId,
query,
);
}
/**
* Retrieves specific uncategorized cashflow transaction.
* @param {number} uncategorizedTransactionId - Uncategorized transaction id.
*/
public getUncategorizedTransaction(uncategorizedTransactionId: number) {
return this.getBankAccountUncategorizedTransactionService.getTransaction(
uncategorizedTransactionId,
);
}
/**
* Retrieves the pending bank account transactions.
* @param {GetPendingTransactionsQueryDto} filter - Pending transactions query.
*/
public getPendingBankAccountTransactions(
filter?: GetPendingTransactionsQueryDto,
) {
return this.getPendingBankAccountTransactionsService.getPendingTransactions(
filter,
);
}
/**
* Retrieves the autofill values of categorize transactions form.
* @param {Array<number> | number} uncategorizeTransactionsId - Uncategorized transactions ids.
*/
public getAutofillCategorizeTransaction(
uncategorizeTransactionsId: Array<number> | number,
) {
return this.getAutofillCategorizeTransactionService.getAutofillCategorizeTransaction(
uncategorizeTransactionsId,
);
}
} }

View File

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

View File

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

View File

@@ -1,42 +0,0 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
import { BankingTransactionsApplication } from '../BankingTransactionsApplication.service';
import { GetPendingTransactionsQueryDto } from '../dtos/GetPendingTransactionsQuery.dto';
@Controller('banking/pending')
@ApiTags('banking-pending')
export class BankingPendingTransactionsController {
constructor(
private readonly bankingTransactionsApplication: BankingTransactionsApplication,
) {}
@Get()
@ApiOperation({ summary: 'Get pending bank account transactions' })
@ApiResponse({
status: 200,
description: 'Returns a list of pending bank account transactions',
})
@ApiQuery({
name: 'page',
required: false,
type: Number,
description: 'Page number for pagination',
})
@ApiQuery({
name: 'pageSize',
required: false,
type: Number,
description: 'Number of items per page',
})
@ApiQuery({
name: 'accountId',
required: false,
type: Number,
description: 'Filter by bank account ID',
})
async getPendingTransactions(@Query() query: GetPendingTransactionsQueryDto) {
return this.bankingTransactionsApplication.getPendingBankAccountTransactions(
query,
);
}
}

View File

@@ -1,108 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery, ApiBody } from '@nestjs/swagger';
import { BankingTransactionsApplication } from '../BankingTransactionsApplication.service';
import { CreateBankTransactionDto } from '../dtos/CreateBankTransaction.dto';
import { GetBankTransactionsQueryDto } from '../dtos/GetBankTranasctionsQuery.dto';
@Controller('banking/transactions')
@ApiTags('banking-transactions')
export class BankingTransactionsController {
constructor(
private readonly bankingTransactionsApplication: BankingTransactionsApplication,
) {}
@Get()
@ApiOperation({ summary: 'Get bank account transactions' })
@ApiResponse({
status: 200,
description: 'Returns a list of bank account transactions',
})
@ApiQuery({
name: 'page',
required: false,
type: Number,
description: 'Page number for pagination',
})
@ApiQuery({
name: 'pageSize',
required: false,
type: Number,
description: 'Number of items per page',
})
async getBankAccountTransactions(
@Query() query: GetBankTransactionsQueryDto,
) {
return this.bankingTransactionsApplication.getBankAccountTransactions(
query,
);
}
@Post()
@ApiOperation({ summary: 'Create a new bank transaction' })
@ApiResponse({
status: 201,
description: 'The bank transaction has been successfully created',
})
@ApiResponse({
status: 400,
description: 'Invalid input data',
})
@ApiBody({ type: CreateBankTransactionDto })
async createTransaction(@Body() transactionDTO: CreateBankTransactionDto) {
return this.bankingTransactionsApplication.createTransaction(
transactionDTO,
);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a bank transaction' })
@ApiResponse({
status: 200,
description: 'The bank transaction has been successfully deleted',
})
@ApiResponse({
status: 404,
description: 'Bank transaction not found',
})
@ApiParam({
name: 'id',
required: true,
type: String,
description: 'Bank transaction ID',
})
async deleteTransaction(@Param('id') transactionId: string) {
return this.bankingTransactionsApplication.deleteTransaction(
Number(transactionId),
);
}
@Get(':id')
@ApiOperation({ summary: 'Get a specific bank transaction by ID' })
@ApiResponse({
status: 200,
description: 'Returns the bank transaction details',
})
@ApiResponse({
status: 404,
description: 'Bank transaction not found',
})
@ApiParam({
name: 'id',
required: true,
type: String,
description: 'Bank transaction ID',
})
async getTransaction(@Param('id') transactionId: string) {
return this.bankingTransactionsApplication.getTransaction(
Number(transactionId),
);
}
}

View File

@@ -1,106 +0,0 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';
import { GetUncategorizedTransactionsQueryDto } from '../dtos/GetUncategorizedTransactionsQuery.dto';
import { BankingTransactionsApplication } from '../BankingTransactionsApplication.service';
@Controller('banking/uncategorized')
@ApiTags('banking-uncategorized')
export class BankingUncategorizedTransactionsController {
constructor(
private readonly bankingTransactionsApplication: BankingTransactionsApplication,
) {}
@Get('autofill')
@ApiOperation({ summary: 'Get autofill values for categorize transactions' })
@ApiResponse({
status: 200,
description: 'Returns autofill values for categorize transactions',
})
@ApiParam({
name: 'accountId',
required: true,
type: Number,
description: 'Bank account ID',
})
@ApiQuery({
name: 'uncategorizeTransactionsId',
required: true,
type: Number,
description: 'Uncategorize transactions ID',
})
async getAutofillCategorizeTransaction(
@Query('uncategorizedTransactionIds') uncategorizedTransactionIds: Array<number> | number,
) {
console.log(uncategorizedTransactionIds)
return this.bankingTransactionsApplication.getAutofillCategorizeTransaction(
uncategorizedTransactionIds,
);
}
@Get('accounts/:accountId')
@ApiOperation({
summary: 'Get uncategorized transactions for a specific bank account',
})
@ApiResponse({
status: 200,
description:
'Returns a list of uncategorized transactions for the specified bank account',
})
@ApiParam({
name: 'accountId',
required: true,
type: Number,
description: 'Bank account ID',
})
@ApiQuery({
name: 'page',
required: false,
type: Number,
description: 'Page number for pagination',
})
@ApiQuery({
name: 'pageSize',
required: false,
type: Number,
description: 'Number of items per page',
})
async getBankAccountUncategorizedTransactions(
@Param('accountId') accountId: number,
@Query() query: GetUncategorizedTransactionsQueryDto,
) {
return this.bankingTransactionsApplication.getBankAccountUncategorizedTransactions(
accountId,
query,
);
}
@Get(':uncategorizedTransactionId')
@ApiOperation({ summary: 'Get a specific uncategorized transaction by ID' })
@ApiResponse({
status: 200,
description: 'Returns the uncategorized transaction details',
})
@ApiResponse({
status: 404,
description: 'Uncategorized transaction not found',
})
@ApiParam({
name: 'uncategorizedTransactionId',
required: true,
type: Number,
description: 'Uncategorized transaction ID',
})
async getUncategorizedTransaction(
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number,
) {
return this.bankingTransactionsApplication.getUncategorizedTransaction(
Number(uncategorizedTransactionId),
);
}
}

View File

@@ -1,153 +1,58 @@
import { ToNumber } from '@/common/decorators/Validators';
import { import {
IsBoolean, IsBoolean,
IsDateString, IsDate,
IsInt,
IsNotEmpty,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString, IsString,
} from 'class-validator'; } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateBankTransactionDto { export class CreateBankTransactionDto {
@ApiProperty({ @IsDate()
description: 'The date of the bank transaction',
type: Date,
example: '2023-01-01T00:00:00.000Z',
})
@IsDateString()
@IsNotEmpty()
date: Date; date: Date;
@ApiPropertyOptional({
description: 'Optional transaction number or reference',
type: String,
example: 'TRX-001',
})
@IsString() @IsString()
@IsOptional() transactionNumber: string;
transactionNumber?: string;
@ApiPropertyOptional({
description: 'Optional external reference number',
type: String,
example: 'REF-001',
})
@IsString() @IsString()
@IsOptional() referenceNo: string;
referenceNo?: string;
@ApiProperty({
description: 'Type of bank transaction (e.g., deposit, withdrawal)',
type: String,
example: 'deposit',
})
@IsNotEmpty()
@IsString() @IsString()
transactionType: string; transactionType: string;
@ApiProperty({
description: 'Description of the bank transaction',
type: String,
example: 'Monthly rent payment',
})
@IsString() @IsString()
description: string; description: string;
@ApiProperty({
description: 'Transaction amount',
type: Number,
example: 1000.5,
})
@IsNotEmpty()
@ToNumber()
@IsNumber() @IsNumber()
amount: number; amount: number;
@ApiProperty({
description: 'Exchange rate for currency conversion',
type: Number,
default: 1,
example: 1.15,
})
@ToNumber()
@IsNumber() @IsNumber()
exchangeRate: number = 1; exchangeRate: number;
@ApiPropertyOptional({
description: 'Currency code for the transaction',
type: String,
example: 'USD',
})
@IsString() @IsString()
@IsOptional()
currencyCode: string; currencyCode: string;
@ApiProperty({ @IsNumber()
description: 'ID of the credit account associated with this transaction',
type: Number,
example: 1001,
})
@IsNotEmpty()
@ToNumber()
@IsInt()
creditAccountId: number; creditAccountId: number;
@ApiProperty({ @IsNumber()
description: 'ID of the cashflow account associated with this transaction',
type: Number,
example: 2001,
})
@IsNotEmpty()
@ToNumber()
@IsInt()
cashflowAccountId: number; cashflowAccountId: number;
@ApiProperty({
description: 'Whether the transaction should be published',
type: Boolean,
default: true,
})
@IsBoolean() @IsBoolean()
@IsOptional() publish: boolean;
publish: boolean = true;
@ApiPropertyOptional({
description: 'ID of the branch where the transaction occurred',
type: Number,
example: 101,
})
@IsOptional() @IsOptional()
@ToNumber() @IsNumber()
@IsInt()
branchId?: number; branchId?: number;
@ApiPropertyOptional({
description: 'Plaid transaction ID if imported from Plaid',
type: String,
example: 'plaid_trx_12345',
})
@IsOptional() @IsOptional()
@IsString() @IsString()
plaidTransactionId?: string; plaidTransactionId?: string;
@ApiPropertyOptional({
description: 'Plaid account ID if imported from Plaid',
type: String,
example: 'plaid_acc_67890',
})
@IsOptional() @IsOptional()
@IsString() @IsString()
plaidAccountId?: string; plaidAccountId?: string;
@ApiPropertyOptional({
description:
'ID of the uncategorized transaction if this is categorizing an existing transaction',
type: Number,
example: 5001,
})
@IsOptional() @IsOptional()
@IsInt() @IsNumber()
uncategorizedTransactionId?: number; uncategorizedTransactionId?: number;
} }

View File

@@ -1,52 +0,0 @@
import {
IsNotEmpty,
IsNumber,
IsNumberString,
IsOptional,
} from 'class-validator';
import { NumberFormatQueryDto } from './NumberFormatQuery.dto';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
export class GetBankTransactionsQueryDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
@ApiProperty({
description: 'Page number for pagination',
required: false,
type: Number,
example: 1
})
page: number;
@IsOptional()
@Type(() => Number)
@IsNumber()
@ApiProperty({
description: 'Number of items per page',
required: false,
type: Number,
example: 10
})
pageSize: number;
@IsNotEmpty()
@Type(() => Number)
@IsNumber()
@ApiProperty({
description: 'Bank account ID',
required: true,
type: Number,
example: 1
})
accountId: number;
@IsOptional()
@ApiProperty({
description: 'Number format options',
required: false,
type: NumberFormatQueryDto
})
numberFormat: NumberFormatQueryDto;
}

View File

@@ -1,31 +0,0 @@
import { IsOptional } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class GetPendingTransactionsQueryDto {
@IsOptional()
@ApiProperty({
description: 'Page number for pagination',
required: false,
type: Number,
example: 1
})
page?: number;
@IsOptional()
@ApiProperty({
description: 'Number of items per page',
required: false,
type: Number,
example: 10
})
pageSize?: number;
@IsOptional()
@ApiProperty({
description: 'Filter by bank account ID',
required: false,
type: Number,
example: 1
})
accountId?: number;
}

View File

@@ -1,58 +0,0 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class GetUncategorizedTransactionsQueryDto {
@IsOptional()
@ApiProperty({
description: 'Page number for pagination',
required: false,
type: Number,
example: 1
})
page?: number;
@IsOptional()
@ApiProperty({
description: 'Number of items per page',
required: false,
type: Number,
example: 10
})
pageSize?: number;
@IsOptional()
@ApiProperty({
description: 'Minimum date for filtering transactions',
required: false,
type: Date,
example: '2023-01-01'
})
minDate?: Date;
@IsOptional()
@ApiProperty({
description: 'Maximum date for filtering transactions',
required: false,
type: Date,
example: '2023-12-31'
})
maxDate?: Date;
@IsOptional()
@ApiProperty({
description: 'Minimum amount for filtering transactions',
required: false,
type: Number,
example: 100
})
minAmount?: number;
@IsOptional()
@ApiProperty({
description: 'Maximum amount for filtering transactions',
required: false,
type: Number,
example: 1000
})
maxAmount?: number;
}

View File

@@ -1,26 +0,0 @@
import { Type } from "class-transformer";
import { IsBoolean, IsEnum, IsNumber, IsOptional, IsPositive } from "class-validator";
export class NumberFormatQueryDto {
@Type(() => Number)
@IsNumber()
@IsPositive()
@IsOptional()
readonly precision: number;
@IsBoolean()
@IsOptional()
readonly divideOn1000: boolean;
@IsBoolean()
@IsOptional()
readonly showZero: boolean;
@IsEnum(['total', 'always', 'none'])
@IsOptional()
readonly formatMoney: 'total' | 'always' | 'none';
@IsEnum(['parentheses', 'mines'])
@IsOptional()
readonly negativeFormat: 'parentheses' | 'mines';
}

View File

@@ -1,9 +1,9 @@
/* eslint-disable global-require */ /* eslint-disable global-require */
import * as moment from 'moment'; import * as moment from 'moment';
import { Model } from 'objection'; import { Model } from 'objection';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { BaseModel } from '@/models/Model';
export class UncategorizedBankTransaction extends TenantBaseModel { export class UncategorizedBankTransaction extends BaseModel {
readonly amount!: number; readonly amount!: number;
readonly date!: Date | string; readonly date!: Date | string;
readonly categorized!: boolean; readonly categorized!: boolean;

View File

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

View File

@@ -1,6 +1,6 @@
// @ts-nocheck // @ts-nocheck
import * as R from 'ramda'; import R from 'ramda';
import * as moment from 'moment'; import moment from 'moment';
import { first, isEmpty } from 'lodash'; import { first, isEmpty } from 'lodash';
import { import {
ICashflowAccountTransaction, ICashflowAccountTransaction,

View File

@@ -1,16 +1,13 @@
import { Inject, Injectable, Scope } from '@nestjs/common'; import { Injectable, Scope } from '@nestjs/common';
import { ICashflowAccountTransactionsQuery } from '../../types/BankingTransactions.types'; import { ICashflowAccountTransactionsQuery } from '../../types/BankingTransactions.types';
import { import {
groupMatchedBankTransactions, groupMatchedBankTransactions,
groupUncategorizedTransactions, groupUncategorizedTransactions,
} from './_utils'; } from './_utils';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { UncategorizedBankTransaction } from '../../models/UncategorizedBankTransaction';
import { MatchedBankTransaction } from '@/modules/BankingMatching/models/MatchedBankTransaction';
@Injectable({ scope: Scope.REQUEST }) @Injectable({ scope: Scope.REQUEST })
export class GetBankAccountTransactionsRepository { export class GetBankAccountTransactionsRepository {
private models: any;
public query: ICashflowAccountTransactionsQuery; public query: ICashflowAccountTransactionsQuery;
public transactions: any; public transactions: any;
public uncategorizedTransactions: any; public uncategorizedTransactions: any;
@@ -20,28 +17,6 @@ export class GetBankAccountTransactionsRepository {
public pagination: any; public pagination: any;
public openingBalance: any; public openingBalance: any;
/**
* @param {TenantModelProxy<typeof AccountTransaction>} accountTransactionModel - Account transaction model.
* @param {TenantModelProxy<typeof UncategorizedBankTransaction>} uncategorizedBankTransactionModel - Uncategorized transaction model
* @param {TenantModelProxy<typeof MatchedBankTransaction>} matchedBankTransactionModel - Matched bank transaction model.
*/
constructor(
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: TenantModelProxy<
typeof AccountTransaction
>,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
@Inject(MatchedBankTransaction.name)
private readonly matchedBankTransactionModel: TenantModelProxy<
typeof MatchedBankTransaction
>,
) {}
setQuery(query: ICashflowAccountTransactionsQuery) { setQuery(query: ICashflowAccountTransactionsQuery) {
this.query = query; this.query = query;
} }
@@ -62,8 +37,9 @@ export class GetBankAccountTransactionsRepository {
* @param {ICashflowAccountTransactionsQuery} query - * @param {ICashflowAccountTransactionsQuery} query -
*/ */
async initCashflowAccountTransactions() { async initCashflowAccountTransactions() {
const { results, pagination } = await this.accountTransactionModel() const { AccountTransaction } = this.models;
.query()
const { results, pagination } = await AccountTransaction.query()
.where('account_id', this.query.accountId) .where('account_id', this.query.accountId)
.orderBy([ .orderBy([
{ column: 'date', order: 'desc' }, { column: 'date', order: 'desc' },
@@ -83,9 +59,10 @@ export class GetBankAccountTransactionsRepository {
* @return {Promise<number>} * @return {Promise<number>}
*/ */
async initCashflowAccountOpeningBalance(): Promise<void> { async initCashflowAccountOpeningBalance(): Promise<void> {
const { AccountTransaction } = this.models;
// Retrieve the opening balance of credit and debit balances. // Retrieve the opening balance of credit and debit balances.
const openingBalancesSubquery = this.accountTransactionModel() const openingBalancesSubquery = AccountTransaction.query()
.query()
.where('account_id', this.query.accountId) .where('account_id', this.query.accountId)
.orderBy([ .orderBy([
{ column: 'date', order: 'desc' }, { column: 'date', order: 'desc' },
@@ -95,8 +72,7 @@ export class GetBankAccountTransactionsRepository {
.offset(this.pagination.pageSize * (this.pagination.page - 1)); .offset(this.pagination.pageSize * (this.pagination.page - 1));
// Sumation of credit and debit balance. // Sumation of credit and debit balance.
const openingBalances = await this.accountTransactionModel() const openingBalances = await AccountTransaction.query()
.query()
.sum('credit as credit') .sum('credit as credit')
.sum('debit as debit') .sum('debit as debit')
.from(openingBalancesSubquery.as('T')) .from(openingBalancesSubquery.as('T'))
@@ -111,11 +87,14 @@ export class GetBankAccountTransactionsRepository {
* Initialize the uncategorized transactions of the bank account. * Initialize the uncategorized transactions of the bank account.
*/ */
async initCategorizedTransactions() { async initCategorizedTransactions() {
const { UncategorizedCashflowTransaction } = this.models;
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]); const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
const uncategorizedTransactions = const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel() await UncategorizedCashflowTransaction.query().whereIn(
.query() ['categorizeRefType', 'categorizeRefId'],
.whereIn(['categorizeRefType', 'categorizeRefId'], refs); refs,
);
this.uncategorizedTransactions = uncategorizedTransactions; this.uncategorizedTransactions = uncategorizedTransactions;
this.uncategorizedTransactionsMapByRef = groupUncategorizedTransactions( this.uncategorizedTransactionsMapByRef = groupUncategorizedTransactions(
@@ -127,11 +106,14 @@ export class GetBankAccountTransactionsRepository {
* Initialize the matched bank transactions of the bank account. * Initialize the matched bank transactions of the bank account.
*/ */
async initMatchedTransactions(): Promise<void> { async initMatchedTransactions(): Promise<void> {
const { MatchedBankTransaction } = this.models;
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]); const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
const matchedBankTransactions = await this.matchedBankTransactionModel() const matchedBankTransactions =
.query() await MatchedBankTransaction.query().whereIn(
.whereIn(['referenceType', 'referenceId'], refs); ['referenceType', 'referenceId'],
refs,
);
this.matchedBankTransactions = matchedBankTransactions; this.matchedBankTransactions = matchedBankTransactions;
this.matchedBankTransactionsMapByRef = groupMatchedBankTransactions( this.matchedBankTransactionsMapByRef = groupMatchedBankTransactions(
matchedBankTransactions, matchedBankTransactions,

View File

@@ -2,8 +2,6 @@ import { Inject, Injectable } from '@nestjs/common';
import { GetPendingBankAccountTransactionTransformer } from './GetPendingBankAccountTransactionTransformer'; import { GetPendingBankAccountTransactionTransformer } from './GetPendingBankAccountTransactionTransformer';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction'; import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { GetPendingTransactionsQueryDto } from '../dtos/GetPendingTransactionsQuery.dto';
@Injectable() @Injectable()
export class GetPendingBankAccountTransactions { export class GetPendingBankAccountTransactions {
@@ -11,24 +9,21 @@ export class GetPendingBankAccountTransactions {
private readonly transformerService: TransformerInjectable, private readonly transformerService: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name) @Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy< private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction
typeof UncategorizedBankTransaction
>,
) {} ) {}
/** /**
* Retrieves the given bank accounts pending transaction. * Retrieves the given bank accounts pending transaction.
* @param {GetPendingTransactionsQueryDto} filter - Pending transactions query. * @param {GetPendingTransactionsQuery} filter - Pending transactions query.
*/ */
async getPendingTransactions(filter?: GetPendingTransactionsQueryDto) { async getPendingTransactions(filter?: GetPendingTransactionsQuery) {
const _filter = { const _filter = {
page: 1, page: 1,
pageSize: 20, pageSize: 20,
...filter, ...filter,
}; };
const { results, pagination } = const { results, pagination } =
await this.uncategorizedBankTransactionModel() await this.uncategorizedBankTransactionModel.query()
.query()
.onBuild((q) => { .onBuild((q) => {
q.modify('pending'); q.modify('pending');
@@ -40,8 +35,14 @@ export class GetPendingBankAccountTransactions {
const data = await this.transformerService.transform( const data = await this.transformerService.transform(
results, results,
new GetPendingBankAccountTransactionTransformer(), new GetPendingBankAccountTransactionTransformer()
); );
return { data, pagination }; return { data, pagination };
} }
} }
interface GetPendingTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
}

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer'; import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { UncategorizedBankTransaction } from '../../BankingTransactions/models/UncategorizedBankTransaction'; import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable() @Injectable()

View File

@@ -1,9 +1,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { GetRecognizedTransactionTransformer } from './queries/GetRecognizedTransactionTransformer'; import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer';
import { UncategorizedBankTransaction } from '../BankingTransactions/models/UncategorizedBankTransaction'; import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { IGetRecognizedTransactionsQuery } from '../BankingTransactions/types/BankingTransactions.types'; import { IGetRecognizedTransactionsQuery } from '../types/BankingTransactions.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable() @Injectable()
export class GetRecognizedTransactionsService { export class GetRecognizedTransactionsService {
@@ -11,7 +10,7 @@ export class GetRecognizedTransactionsService {
private readonly transformer: TransformerInjectable, private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name) @Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy<typeof UncategorizedBankTransaction>, private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {} ) {}
/** /**
@@ -26,7 +25,7 @@ export class GetRecognizedTransactionsService {
...filter, ...filter,
}; };
const { results, pagination } = const { results, pagination } =
await this.uncategorizedBankTransactionModel().query() await this.uncategorizedBankTransactionModel.query()
.onBuild((q) => { .onBuild((q) => {
q.withGraphFetched('recognizedTransaction.assignAccount'); q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphFetched('recognizedTransaction.bankRule'); q.withGraphFetched('recognizedTransaction.bankRule');

View File

@@ -1,33 +1,26 @@
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction'; import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { Inject, Injectable } from '@nestjs/common';
import { UncategorizedTransactionTransformer } from '../../BankingCategorize/commands/UncategorizedTransaction.transformer'; import { UncategorizedTransactionTransformer } from '../../BankingCategorize/commands/UncategorizedTransaction.transformer';
import { GetUncategorizedTransactionsQueryDto } from '../dtos/GetUncategorizedTransactionsQuery.dto'; import { IGetUncategorizedTransactionsQuery } from '../types/BankingTransactions.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable() @Injectable()
export class GetUncategorizedTransactions { export class GetUncategorizedTransactions {
/**
* @param {TransformerInjectable} transformer
* @param {UncategorizedBankTransaction.name} uncategorizedBankTransactionModel
*/
constructor( constructor(
private readonly transformer: TransformerInjectable, private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name) @Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy< private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
typeof UncategorizedBankTransaction
>,
) {} ) {}
/** /**
* Retrieves the uncategorized cashflow transactions. * Retrieves the uncategorized cashflow transactions.
* @param {number} tenantId - Tenant id.
* @param {number} accountId - Account Id. * @param {number} accountId - Account Id.
* @param {IGetUncategorizedTransactionsQuery} query - Query.
*/ */
public async getTransactions( public async getTransactions(
accountId: number, accountId: number,
query: GetUncategorizedTransactionsQueryDto, query: IGetUncategorizedTransactionsQuery
) { ) {
// Parsed query with default values. // Parsed query with default values.
const _query = { const _query = {
@@ -35,9 +28,9 @@ export class GetUncategorizedTransactions {
pageSize: 20, pageSize: 20,
...query, ...query,
}; };
const { results, pagination } = const { results, pagination } =
await this.uncategorizedBankTransactionModel() await this.uncategorizedBankTransactionModel.query()
.query()
.onBuild((q) => { .onBuild((q) => {
q.where('accountId', accountId); q.where('accountId', accountId);
q.where('categorized', false); q.where('categorized', false);
@@ -70,7 +63,7 @@ export class GetUncategorizedTransactions {
const data = await this.transformer.transform( const data = await this.transformer.transform(
results, results,
new UncategorizedTransactionTransformer(), new UncategorizedTransactionTransformer()
); );
return { return {
data, data,

View File

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

View File

@@ -8,13 +8,12 @@ import { Inject, Injectable } from '@nestjs/common';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { Account } from '@/modules/Accounts/models/Account.model'; import { Account } from '@/modules/Accounts/models/Account.model';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction'; import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable() @Injectable()
export class DecrementUncategorizedTransactionOnCategorizeSubscriber { export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
constructor( constructor(
@Inject(Account.name) @Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>, private readonly accountModel: typeof Account,
) {} ) {}
/** /**
@@ -34,7 +33,7 @@ export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
if (uncategorizedTransaction.isPending) { if (uncategorizedTransaction.isPending) {
return; return;
} }
await this.accountModel() await this.accountModel
.query(trx) .query(trx)
.findById(uncategorizedTransaction.accountId) .findById(uncategorizedTransaction.accountId)
.decrement('uncategorizedTransactions', 1); .decrement('uncategorizedTransactions', 1);
@@ -59,7 +58,7 @@ export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
if (uncategorizedTransaction.isPending) { if (uncategorizedTransaction.isPending) {
return; return;
} }
await this.accountModel() await this.accountModel
.query(trx) .query(trx)
.findById(uncategorizedTransaction.accountId) .findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1); .increment('uncategorizedTransactions', 1);
@@ -81,7 +80,7 @@ export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
// Cannot continue if the transaction is still pending. // Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) return; if (uncategorizedTransaction.isPending) return;
await this.accountModel() await this.accountModel
.query(trx) .query(trx)
.findById(uncategorizedTransaction.accountId) .findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1); .increment('uncategorizedTransactions', 1);

View File

@@ -5,34 +5,19 @@ import {
Get, Get,
Param, Param,
Post, Post,
Put,
Query, Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication'; import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication';
import { ExcludedBankTransactionsQuery } from './types/BankTransactionsExclude.types'; import { ExcludedBankTransactionsQuery } from './types/BankTransactionsExclude.types';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
@Controller('banking/exclude') @Controller('banking/transactions')
@ApiTags('banking-transactions') @ApiTags('banking-transactions')
export class BankingTransactionsExcludeController { export class BankingTransactionsExcludeController {
constructor( constructor(
private readonly excludeBankTransactionsApplication: ExcludeBankTransactionsApplication, private readonly excludeBankTransactionsApplication: ExcludeBankTransactionsApplication,
) {} ) {}
@Put('bulk')
@ApiOperation({ summary: 'Exclude the given bank transactions.' })
public excludeBankTransactions(@Body('ids') ids: number[]) {
return this.excludeBankTransactionsApplication.excludeBankTransactions(ids);
}
@Delete('bulk')
@ApiOperation({ summary: 'Unexclude the given bank transactions.' })
public unexcludeBankTransactions(@Body('ids') ids: number[]) {
return this.excludeBankTransactionsApplication.unexcludeBankTransactions(
ids,
);
}
@Get() @Get()
@ApiOperation({ summary: 'Retrieves the excluded bank transactions.' }) @ApiOperation({ summary: 'Retrieves the excluded bank transactions.' })
public getExcludedBankTransactions( public getExcludedBankTransactions(
@@ -43,7 +28,7 @@ export class BankingTransactionsExcludeController {
); );
} }
@Put(':id') @Post(':id/exclude')
@ApiOperation({ summary: 'Exclude the given bank transaction.' }) @ApiOperation({ summary: 'Exclude the given bank transaction.' })
public excludeBankTransaction(@Param('id') id: string) { public excludeBankTransaction(@Param('id') id: string) {
return this.excludeBankTransactionsApplication.excludeBankTransaction( return this.excludeBankTransactionsApplication.excludeBankTransaction(
@@ -51,11 +36,25 @@ export class BankingTransactionsExcludeController {
); );
} }
@Delete(':id') @Delete(':id/exclude')
@ApiOperation({ summary: 'Unexclude the given bank transaction.' }) @ApiOperation({ summary: 'Unexclude the given bank transaction.' })
public unexcludeBankTransaction(@Param('id') id: string) { public unexcludeBankTransaction(@Param('id') id: string) {
return this.excludeBankTransactionsApplication.unexcludeBankTransaction( return this.excludeBankTransactionsApplication.unexcludeBankTransaction(
Number(id), Number(id),
); );
} }
@Post('bulk/exclude')
@ApiOperation({ summary: 'Exclude the given bank transactions.' })
public excludeBankTransactions(@Body('ids') ids: number[]) {
return this.excludeBankTransactionsApplication.excludeBankTransactions(ids);
}
@Delete('bulk/exclude')
@ApiOperation({ summary: 'Unexclude the given bank transactions.' })
public unexcludeBankTransactions(@Body('ids') ids: number[]) {
return this.excludeBankTransactionsApplication.unexcludeBankTransactions(
ids,
);
}
} }

View File

@@ -1,4 +1,4 @@
import { forwardRef, Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication'; import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication';
import { ExcludeBankTransactionService } from './commands/ExcludeBankTransaction.service'; import { ExcludeBankTransactionService } from './commands/ExcludeBankTransaction.service';
import { UnexcludeBankTransactionService } from './commands/UnexcludeBankTransaction.service'; import { UnexcludeBankTransactionService } from './commands/UnexcludeBankTransaction.service';
@@ -10,9 +10,7 @@ import { BankingTransactionsExcludeController } from './BankingTransactionsExclu
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module'; import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
@Module({ @Module({
imports: [ imports: [BankingTransactionsModule],
forwardRef(() => BankingTransactionsModule),
],
providers: [ providers: [
ExcludeBankTransactionsApplication, ExcludeBankTransactionsApplication,
ExcludeBankTransactionService, ExcludeBankTransactionService,

View File

@@ -1,191 +0,0 @@
import { Inject } from '@nestjs/common';
import { difference, sumBy } from 'lodash';
import {
ILandedCostItemDTO,
ILandedCostDTO,
IBillLandedCostTransaction,
ILandedCostTransaction,
ILandedCostTransactionEntry,
} from './types/BillLandedCosts.types';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { BillLandedCost } from './models/BillLandedCost';
import { ServiceError } from '../Items/ServiceError';
import { CONFIG, ERRORS } from './utils';
import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry';
import { Bill } from '../Bills/models/Bill';
import { TransactionLandedCost } from './commands/TransctionLandedCost.service';
export class BaseLandedCostService {
@Inject()
public readonly transactionLandedCost: TransactionLandedCost;
@Inject(BillLandedCost.name)
private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>;
/**
* Validates allocate cost items association with the purchase invoice entries.
* @param {IItemEntry[]} purchaseInvoiceEntries
* @param {ILandedCostItemDTO[]} landedCostItems
*/
protected validateAllocateCostItems = (
purchaseInvoiceEntries: ItemEntry[],
landedCostItems: ILandedCostItemDTO[],
): void => {
// Purchase invoice entries items ids.
const purchaseInvoiceItems = purchaseInvoiceEntries.map((e) => e.id);
const landedCostItemsIds = landedCostItems.map((item) => item.entryId);
// Not found items ids.
const notFoundItemsIds = difference(
purchaseInvoiceItems,
landedCostItemsIds,
);
// Throw items ids not found service error.
if (notFoundItemsIds.length > 0) {
throw new ServiceError(ERRORS.LANDED_COST_ITEMS_IDS_NOT_FOUND);
}
};
/**
* Transformes DTO to bill landed cost model object.
* @param {ILandedCostDTO} landedCostDTO
* @param {IBill} bill
* @param {ILandedCostTransaction} costTransaction
* @param {ILandedCostTransactionEntry} costTransactionEntry
* @returns
*/
protected transformToBillLandedCost(
landedCostDTO: ILandedCostDTO,
bill: Bill,
costTransaction: ILandedCostTransaction,
costTransactionEntry: ILandedCostTransactionEntry,
) {
const amount = sumBy(landedCostDTO.items, 'cost');
return {
billId: bill.id,
fromTransactionType: landedCostDTO.transactionType,
fromTransactionId: landedCostDTO.transactionId,
fromTransactionEntryId: landedCostDTO.transactionEntryId,
amount,
currencyCode: costTransaction.currencyCode,
exchangeRate: costTransaction.exchangeRate || 1,
allocationMethod: landedCostDTO.allocationMethod,
allocateEntries: landedCostDTO.items,
description: landedCostDTO.description,
costAccountId: costTransactionEntry.costAccountId,
};
}
/**
* Retrieve the cost transaction or throw not found error.
* @param {number} tenantId
* @param {transactionType} transactionType -
* @param {transactionId} transactionId -
*/
public getLandedCostOrThrowError = async (
transactionType: string,
transactionId: number,
) => {
const Model = this.transactionLandedCost.getModel(
transactionType,
);
const model = await Model.query().findById(transactionId);
if (!model) {
throw new ServiceError(ERRORS.LANDED_COST_TRANSACTION_NOT_FOUND);
}
return this.transactionLandedCost.transformToLandedCost(
transactionType,
model,
);
};
/**
* Retrieve the landed cost entries.
* @param {number} tenantId
* @param {string} transactionType
* @param {number} transactionId
* @returns
*/
public getLandedCostEntry = async (
transactionType: string,
transactionId: number,
transactionEntryId: number,
): Promise<any> => {
const Model = this.transactionLandedCost.getModel(
tenantId,
transactionType,
);
const relation = CONFIG.COST_TYPES[transactionType].entries;
const entry = await Model.relatedQuery(relation)
.for(transactionId)
.findOne('id', transactionEntryId)
.where('landedCost', true)
.onBuild((q) => {
if (transactionType === 'Bill') {
q.withGraphFetched('item');
} else if (transactionType === 'Expense') {
q.withGraphFetched('expenseAccount');
}
});
if (!entry) {
throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND);
}
return this.transactionLandedCost.transformToLandedCostEntry(
transactionType,
entry,
);
};
/**
* Retrieve allocate items cost total.
* @param {ILandedCostDTO} landedCostDTO
* @returns {number}
*/
protected getAllocateItemsCostTotal = (
landedCostDTO: ILandedCostDTO,
): number => {
return sumBy(landedCostDTO.items, 'cost');
};
/**
* Validates the landed cost entry amount.
* @param {number} unallocatedCost -
* @param {number} amount -
*/
protected validateLandedCostEntryAmount = (
unallocatedCost: number,
amount: number,
): void => {
if (unallocatedCost < amount) {
throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT);
}
};
/**
* Retrieve the give bill landed cost or throw not found service error.
* @param {number} tenantId - Tenant id.
* @param {number} landedCostId - Landed cost id.
* @returns {Promise<IBillLandedCost>}
*/
public getBillLandedCostOrThrowError = async (
landedCostId: number,
): Promise<BillLandedCost> => {
// Retrieve the bill landed cost model.
const billLandedCost = await this.billLandedCostModel()
.query()
.findById(landedCostId);
if (!billLandedCost) {
throw new ServiceError(ERRORS.BILL_LANDED_COST_NOT_FOUND);
}
return billLandedCost;
};
}

View File

@@ -1,32 +1,8 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TransactionLandedCostEntriesService } from './TransactionLandedCostEntries.service'; import { TransactionLandedCostEntriesService } from './TransactionLandedCostEntries.service';
import { AllocateLandedCostService } from './commands/AllocateLandedCost.service';
import { LandedCostGLEntriesSubscriber } from './commands/LandedCostGLEntries.subscriber';
import { LandedCostGLEntries } from './commands/LandedCostGLEntries.service';
import { LandedCostSyncCostTransactions } from './commands/LandedCostSyncCostTransactions.service';
import { LandedCostSyncCostTransactionsSubscriber } from './commands/LandedCostSyncCostTransactions.subscriber';
import { BillAllocatedLandedCostTransactions } from './commands/BillAllocatedLandedCostTransactions.service';
import { BillAllocateLandedCostController } from './LandedCost.controller';
import { RevertAllocatedLandedCost } from './commands/RevertAllocatedLandedCost.service';
import LandedCostTranasctions from './commands/LandedCostTransactions.service';
import { LandedCostInventoryTransactions } from './commands/LandedCostInventoryTransactions.service';
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
@Module({ @Module({
imports: [InventoryCostModule], providers: [TransactionLandedCostEntriesService],
providers: [
AllocateLandedCostService,
TransactionLandedCostEntriesService,
BillAllocatedLandedCostTransactions,
LandedCostGLEntriesSubscriber,
LandedCostGLEntries,
LandedCostSyncCostTransactions,
RevertAllocatedLandedCost,
LandedCostInventoryTransactions,
LandedCostTranasctions,
LandedCostSyncCostTransactionsSubscriber,
],
exports: [TransactionLandedCostEntriesService], exports: [TransactionLandedCostEntriesService],
controllers: [BillAllocateLandedCostController],
}) })
export class BillLandedCostsModule {} export class BillLandedCostsModule {}

View File

@@ -1,84 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
} from '@nestjs/common';
import { AllocateBillLandedCostDto } from './dtos/AllocateBillLandedCost.dto';
import { AllocateLandedCostService } from './commands/AllocateLandedCost.service';
import { BillAllocatedLandedCostTransactions } from './commands/BillAllocatedLandedCostTransactions.service';
import { RevertAllocatedLandedCost } from './commands/RevertAllocatedLandedCost.service';
import { LandedCostTranasctions } from './commands/LandedCostTransactions.service';
@Controller('landed-cost')
export class BillAllocateLandedCostController {
constructor(
private allocateLandedCost: AllocateLandedCostService,
private billAllocatedCostTransactions: BillAllocatedLandedCostTransactions,
private revertAllocatedLandedCost: RevertAllocatedLandedCost,
private landedCostTranasctions: LandedCostTranasctions,
) {}
@Get('/transactions')
async getLandedCostTransactions(
@Query('transaction_type') transactionType: string,
) {
const transactions =
await this.landedCostTranasctions.getLandedCostTransactions(transactionType);
return transactions;
}
@Post('/bills/:billId/allocate')
public async calculateLandedCost(
@Param('billId') billId: number,
@Body() landedCostDTO: AllocateBillLandedCostDto,
) {
const billLandedCost = await this.allocateLandedCost.allocateLandedCost(
landedCostDTO,
billId,
);
return {
id: billLandedCost.id,
message: 'The items cost are located successfully.',
};
}
@Delete('/:allocatedLandedCostId')
public async deleteAllocatedLandedCost(
@Param('allocatedLandedCostId') allocatedLandedCostId: number,
) {
await this.revertAllocatedLandedCost.deleteAllocatedLandedCost(
allocatedLandedCostId,
);
return {
id: allocatedLandedCostId,
message: 'The allocated landed cost are delete successfully.',
};
}
public async listLandedCosts(
) {
const transactions =
await this.landedCostTranasctions.getLandedCostTransactions(query);
return transactions;
};
@Get('/bills/:billId/transactions')
async getBillLandedCostTransactions(@Param('billId') billId: number) {
const transactions =
await this.billAllocatedCostTransactions.getBillLandedCostTransactions(
billId,
);
return {
billId,
transactions,
};
}
}

View File

@@ -1,10 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ServiceError } from '../Items/ServiceError'; import { ServiceError } from '../Items/ServiceError';
import { transformToMap } from '@/utils/transform-to-key'; import { transformToMap } from '@/utils/transform-to-key';
import { import { ICommonLandedCostEntry, ICommonLandedCostEntryDTO } from './types/BillLandedCosts.types';
ICommonLandedCostEntry,
ICommonLandedCostEntryDTO,
} from './types/BillLandedCosts.types';
const ERRORS = { const ERRORS = {
ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED: ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED:
@@ -22,7 +19,7 @@ export class TransactionLandedCostEntriesService {
*/ */
public getLandedCostEntriesDeleted( public getLandedCostEntriesDeleted(
oldCommonEntries: ICommonLandedCostEntry[], oldCommonEntries: ICommonLandedCostEntry[],
newCommonEntriesDTO: ICommonLandedCostEntryDTO[], newCommonEntriesDTO: ICommonLandedCostEntryDTO[]
): ICommonLandedCostEntry[] { ): ICommonLandedCostEntry[] {
const newBillEntriesById = transformToMap(newCommonEntriesDTO, 'id'); const newBillEntriesById = transformToMap(newCommonEntriesDTO, 'id');
@@ -43,11 +40,11 @@ export class TransactionLandedCostEntriesService {
*/ */
public validateLandedCostEntriesNotDeleted( public validateLandedCostEntriesNotDeleted(
oldCommonEntries: ICommonLandedCostEntry[], oldCommonEntries: ICommonLandedCostEntry[],
newCommonEntriesDTO: ICommonLandedCostEntryDTO[], newCommonEntriesDTO: ICommonLandedCostEntryDTO[]
): void { ): void {
const entriesDeleted = this.getLandedCostEntriesDeleted( const entriesDeleted = this.getLandedCostEntriesDeleted(
oldCommonEntries, oldCommonEntries,
newCommonEntriesDTO, newCommonEntriesDTO
); );
if (entriesDeleted.length > 0) { if (entriesDeleted.length > 0) {
throw new ServiceError(ERRORS.ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED); throw new ServiceError(ERRORS.ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED);
@@ -61,7 +58,7 @@ export class TransactionLandedCostEntriesService {
*/ */
public validateLocatedCostEntriesSmallerThanNewEntries( public validateLocatedCostEntriesSmallerThanNewEntries(
oldCommonEntries: ICommonLandedCostEntry[], oldCommonEntries: ICommonLandedCostEntry[],
newCommonEntriesDTO: ICommonLandedCostEntryDTO[], newCommonEntriesDTO: ICommonLandedCostEntryDTO[]
): void { ): void {
const oldBillEntriesById = transformToMap(oldCommonEntries, 'id'); const oldBillEntriesById = transformToMap(oldCommonEntries, 'id');
@@ -70,7 +67,7 @@ export class TransactionLandedCostEntriesService {
if (oldEntry && oldEntry.allocatedCostAmount > entry.amount) { if (oldEntry && oldEntry.allocatedCostAmount > entry.amount) {
throw new ServiceError( throw new ServiceError(
ERRORS.LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES, ERRORS.LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES
); );
} }
}); });

View File

@@ -1,107 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import {
IAllocatedLandedCostCreatedPayload,
ILandedCostDTO,
} from '../types/BillLandedCosts.types';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Bill } from '@/modules/Bills/models/Bill';
import { BillLandedCost } from '../models/BillLandedCost';
import { BaseLandedCostService } from '../BaseLandedCost.service';
import { events } from '@/common/events/events';
import { AllocateBillLandedCostDto } from '../dtos/AllocateBillLandedCost.dto';
@Injectable()
export class AllocateLandedCostService extends BaseLandedCostService {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
@Inject(Bill.name)
private readonly billModel: TenantModelProxy<typeof Bill>,
@Inject(BillLandedCost.name)
private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>
) {
super();
}
/**
* =================================
* - Allocate landed cost.
* =================================
* - Validates the allocate cost not the same purchase invoice id.
* - Get the given bill (purchase invoice) or throw not found error.
* - Get the given landed cost transaction or throw not found error.
* - Validate landed cost transaction has enough unallocated cost amount.
* - Validate landed cost transaction entry has enough unallocated cost amount.
* - Validate allocate entries existance and associated with cost bill transaction.
* - Writes inventory landed cost transaction.
* - Increment the allocated landed cost transaction.
* - Increment the allocated landed cost transaction entry.
* --------------------------------
* @param {ILandedCostDTO} landedCostDTO - Landed cost DTO.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Purchase invoice id.
*/
public async allocateLandedCost(
allocateCostDTO: AllocateBillLandedCostDto,
billId: number,
): Promise<BillLandedCost> {
// Retrieve total cost of allocated items.
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
// Retrieve the purchase invoice or throw not found error.
const bill = await Bill.query()
.findById(billId)
.withGraphFetched('entries')
.throwIfNotFound();
// Retrieve landed cost transaction or throw not found service error.
const costTransaction = await this.getLandedCostOrThrowError(
allocateCostDTO.transactionType,
allocateCostDTO.transactionId,
);
// Retrieve landed cost transaction entries.
const costTransactionEntry = await this.getLandedCostEntry(
allocateCostDTO.transactionType,
allocateCostDTO.transactionId,
allocateCostDTO.transactionEntryId,
);
// Validates allocate cost items association with the purchase invoice entries.
this.validateAllocateCostItems(bill.entries, allocateCostDTO.items);
// Validate the amount of cost with unallocated landed cost.
this.validateLandedCostEntryAmount(
costTransactionEntry.unallocatedCostAmount,
amount,
);
// Transformes DTO to bill landed cost model object.
const billLandedCostObj = this.transformToBillLandedCost(
allocateCostDTO,
bill,
costTransaction,
costTransactionEntry,
);
// Saves landed cost transactions with associated tranasctions under
// unit-of-work eniverment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Save the bill landed cost model.
const billLandedCost =
await BillLandedCost.query(trx).insertGraph(billLandedCostObj);
// Triggers `onBillLandedCostCreated` event.
await this.eventPublisher.emitAsync(events.billLandedCost.onCreated, {
bill,
billLandedCostId: billLandedCost.id,
billLandedCost,
costTransaction,
costTransactionEntry,
trx,
} as IAllocatedLandedCostCreatedPayload);
return billLandedCost;
});
};
}

View File

@@ -1,177 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
import { omit } from 'lodash';
import * as R from 'ramda';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Bill } from '@/modules/Bills/models/Bill';
import { BillLandedCost } from '../models/BillLandedCost';
import { IBillLandedCostTransaction } from '../types/BillLandedCosts.types';
@Injectable()
export class BillAllocatedLandedCostTransactions {
constructor(
private readonly i18nService: I18nService,
@Inject(Bill.name)
private readonly billModel: TenantModelProxy<typeof Bill>,
@Inject(BillLandedCost.name)
private readonly billLandedCostModel: TenantModelProxy<
typeof BillLandedCost
>,
) {}
/**
* Retrieve the bill associated landed cost transactions.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<IBillLandedCostTransaction>}
*/
public getBillLandedCostTransactions = async (
billId: number,
): Promise<IBillLandedCostTransaction> => {
// Retrieve the given bill id or throw not found service error.
const bill = await this.billModel()
.query()
.findById(billId)
.throwIfNotFound();
// Retrieve the bill associated allocated landed cost with bill and expense entry.
const landedCostTransactions = await this.billLandedCostModel()
.query()
.where('bill_id', billId)
.withGraphFetched('allocateEntries')
.withGraphFetched('allocatedFromBillEntry.item')
.withGraphFetched('allocatedFromExpenseEntry.expenseAccount')
.withGraphFetched('bill');
const transactionsJson = this.i18nService.i18nApply(
[[qim.$each, 'allocationMethodFormatted']],
landedCostTransactions.map((a) => a.toJSON()),
tenantId,
);
return this.transformBillLandedCostTransactions(transactionsJson);
};
/**
*
* @param {IBillLandedCostTransaction[]} landedCostTransactions
* @returns
*/
private transformBillLandedCostTransactions = (
landedCostTransactions: IBillLandedCostTransaction[],
) => {
return landedCostTransactions.map(this.transformBillLandedCostTransaction);
};
/**
*
* @param {IBillLandedCostTransaction} transaction
* @returns
*/
private transformBillLandedCostTransaction = (
transaction: IBillLandedCostTransaction,
) => {
const getTransactionName = R.curry(this.condBillLandedTransactionName)(
transaction.fromTransactionType,
);
const getTransactionDesc = R.curry(
this.condBillLandedTransactionDescription,
)(transaction.fromTransactionType);
return {
formattedAmount: formatNumber(transaction.amount, {
currencyCode: transaction.currencyCode,
}),
...omit(transaction, [
'allocatedFromBillEntry',
'allocatedFromExpenseEntry',
]),
name: getTransactionName(transaction),
description: getTransactionDesc(transaction),
formattedLocalAmount: formatNumber(transaction.localAmount, {
currencyCode: 'USD',
}),
};
};
/**
* Retrieve bill landed cost tranaction name based on the given transaction type.
* @param transactionType
* @param transaction
* @returns
*/
private condBillLandedTransactionName = (
transactionType: string,
transaction,
) => {
return R.cond([
[
R.always(R.equals(transactionType, 'Bill')),
this.getLandedBillTransactionName,
],
[
R.always(R.equals(transactionType, 'Expense')),
this.getLandedExpenseTransactionName,
],
])(transaction);
};
/**
*
* @param transaction
* @returns
*/
private getLandedBillTransactionName = (transaction): string => {
return transaction.allocatedFromBillEntry.item.name;
};
/**
*
* @param transaction
* @returns
*/
private getLandedExpenseTransactionName = (transaction): string => {
return transaction.allocatedFromExpenseEntry.expenseAccount.name;
};
/**
* Retrieve landed cost.
* @param transaction
* @returns
*/
private getLandedBillTransactionDescription = (transaction): string => {
return transaction.allocatedFromBillEntry.description;
};
/**
*
* @param transaction
* @returns
*/
private getLandedExpenseTransactionDescription = (transaction): string => {
return transaction.allocatedFromExpenseEntry.description;
};
/**
* Retrieve the bill landed cost transaction description based on transaction type.
* @param {string} tranasctionType
* @param transaction
* @returns
*/
private condBillLandedTransactionDescription = (
tranasctionType: string,
transaction,
) => {
return R.cond([
[
R.always(R.equals(tranasctionType, 'Bill')),
this.getLandedBillTransactionDescription,
],
[
R.always(R.equals(tranasctionType, 'Expense')),
this.getLandedExpenseTransactionDescription,
],
])(transaction);
};
}

View File

@@ -1,234 +0,0 @@
import * as R from 'ramda';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { BaseLandedCostService } from '../BaseLandedCost.service';
import { BillLandedCost } from '../models/BillLandedCost';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Bill } from '@/modules/Bills/models/Bill';
import { BillLandedCostEntry } from '../models/BillLandedCostEntry';
import { ILedger, ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
@Injectable()
export class LandedCostGLEntries extends BaseLandedCostService {
constructor(
private readonly journalService: JournalPosterService,
private readonly ledgerRepository: LedgerRepository,
@Inject(BillLandedCost.name)
private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>,
) {
super();
}
/**
* Retrieves the landed cost GL common entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @returns
*/
private getLandedCostGLCommonEntry = (
bill: Bill,
allocatedLandedCost: BillLandedCost
) => {
return {
date: bill.billDate,
currencyCode: allocatedLandedCost.currencyCode,
exchangeRate: allocatedLandedCost.exchangeRate,
transactionType: 'LandedCost',
transactionId: allocatedLandedCost.id,
transactionNumber: bill.billNumber,
referenceNumber: bill.referenceNo,
credit: 0,
debit: 0,
};
};
/**
* Retrieves the landed cost GL inventory entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @param {IBillLandedCostEntry} allocatedEntry
* @returns {ILedgerEntry}
*/
private getLandedCostGLInventoryEntry = (
bill: Bill,
allocatedLandedCost: BillLandedCost,
allocatedEntry: BillLandedCostEntry
): ILedgerEntry => {
const commonEntry = this.getLandedCostGLCommonEntry(
bill,
allocatedLandedCost
);
return {
...commonEntry,
debit: allocatedLandedCost.localAmount,
accountId: allocatedEntry.itemEntry.item.inventoryAccountId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieves the landed cost GL cost entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedgerEntry}
*/
private getLandedCostGLCostEntry = (
bill: Bill,
allocatedLandedCost: BillLandedCost,
fromTransactionEntry: ILandedCostTransactionEntry
): ILedgerEntry => {
const commonEntry = this.getLandedCostGLCommonEntry(
bill,
allocatedLandedCost
);
return {
...commonEntry,
credit: allocatedLandedCost.localAmount,
accountId: fromTransactionEntry.costAccountId,
index: 2,
accountNormal: AccountNormal.CREDIT,
};
};
/**
* Retrieve allocated landed cost entry GL entries.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @param {IBillLandedCostEntry} allocatedEntry
* @returns {ILedgerEntry}
*/
private getLandedCostGLAllocateEntry = R.curry(
(
bill: Bill,
allocatedLandedCost: BillLandedCost,
fromTransactionEntry: LandedCostTransactionEntry,
allocatedEntry: BillLandedCostEntry
): ILedgerEntry[] => {
const inventoryEntry = this.getLandedCostGLInventoryEntry(
bill,
allocatedLandedCost,
allocatedEntry
);
const costEntry = this.getLandedCostGLCostEntry(
bill,
allocatedLandedCost,
fromTransactionEntry
);
return [inventoryEntry, costEntry];
}
);
/**
* Compose the landed cost GL entries.
* @param {BillLandedCost} allocatedLandedCost
* @param {Bill} bill
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedgerEntry[]}
*/
public getLandedCostGLEntries = (
allocatedLandedCost: BillLandedCost,
bill: Bill,
fromTransactionEntry: LandedCostTransactionEntry
): ILedgerEntry[] => {
const getEntry = this.getLandedCostGLAllocateEntry(
bill,
allocatedLandedCost,
fromTransactionEntry
);
return allocatedLandedCost.allocateEntries.map(getEntry).flat();
};
/**
* Retrieves the landed cost GL ledger.
* @param {IBillLandedCost} allocatedLandedCost
* @param {Bill} bill
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedger}
*/
public getLandedCostLedger = (
allocatedLandedCost: BillLandedCost,
bill: Bill,
fromTransactionEntry: LandedCostTransactionEntry
): ILedger => {
const entries = this.getLandedCostGLEntries(
allocatedLandedCost,
bill,
fromTransactionEntry
);
return new Ledger(entries);
};
/**
* Writes landed cost GL entries to the storage layer.
* @param {number} tenantId -
*/
public writeLandedCostGLEntries = async (
allocatedLandedCost: BillLandedCost,
bill: Bill,
fromTransactionEntry: ILandedCostTransactionEntry,
trx?: Knex.Transaction
) => {
const ledgerEntries = this.getLandedCostGLEntries(
allocatedLandedCost,
bill,
fromTransactionEntry
);
await this.ledgerRepository.saveLedgerEntries(ledgerEntries, trx);
};
/**
* Generates and writes GL entries of the given landed cost.
* @param {number} billLandedCostId
* @param {Knex.Transaction} trx
*/
public createLandedCostGLEntries = async (
billLandedCostId: number,
trx?: Knex.Transaction
) => {
// Retrieve the bill landed cost transacion with associated
// allocated entries and items.
const allocatedLandedCost = await this.billLandedCostModel().query(trx)
.findById(billLandedCostId)
.withGraphFetched('bill')
.withGraphFetched('allocateEntries.itemEntry.item');
// Retrieve the allocated from transactione entry.
const transactionEntry = await this.getLandedCostEntry(
allocatedLandedCost.fromTransactionType,
allocatedLandedCost.fromTransactionId,
allocatedLandedCost.fromTransactionEntryId
);
// Writes the given landed cost GL entries to the storage layer.
await this.writeLandedCostGLEntries(
allocatedLandedCost,
allocatedLandedCost.bill,
transactionEntry,
trx
);
};
/**
* Reverts GL entries of the given allocated landed cost transaction.
* @param {number} tenantId
* @param {number} landedCostId
* @param {Knex.Transaction} trx
*/
public revertLandedCostGLEntries = async (
landedCostId: number,
trx: Knex.Transaction
) => {
await this.journalService.revertJournalTransactions(
landedCostId,
'LandedCost',
trx
);
};
}

View File

@@ -1,45 +0,0 @@
import {
IAllocatedLandedCostCreatedPayload,
IAllocatedLandedCostDeletedPayload,
} from '../types/BillLandedCosts.types';
import { OnEvent } from '@nestjs/event-emitter';
import { LandedCostGLEntries } from './LandedCostGLEntries.service';
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
@Injectable()
export class LandedCostGLEntriesSubscriber {
constructor(
private readonly billLandedCostGLEntries: LandedCostGLEntries,
) {}
/**
* Writes GL entries once landed cost transaction created.
* @param {IAllocatedLandedCostCreatedPayload} payload -
*/
@OnEvent(events.billLandedCost.onCreated)
async writeGLEntriesOnceLandedCostCreated({
billLandedCost,
trx,
}: IAllocatedLandedCostCreatedPayload) {
await this.billLandedCostGLEntries.createLandedCostGLEntries(
billLandedCost.id,
trx
);
};
/**
* Reverts GL entries associated to landed cost transaction once deleted.
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
@OnEvent(events.billLandedCost.onDeleted)
async revertGLEnteriesOnceLandedCostDeleted({
oldBillLandedCost,
trx,
}: IAllocatedLandedCostDeletedPayload) {
await this.billLandedCostGLEntries.revertLandedCostGLEntries(
oldBillLandedCost.id,
trx
);
};
}

View File

@@ -1,66 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { IBillLandedCostTransaction } from '../types/BillLandedCosts.types';
import { Bill } from '@/modules/Bills/models/Bill';
import { mergeLocatedWithBillEntries } from '../utils';
import { InventoryTransactionsService } from '@/modules/InventoryCost/commands/InventoryTransactions.service';
@Injectable()
export class LandedCostInventoryTransactions {
constructor(
private readonly inventoryTransactionsService: InventoryTransactionsService,
) {}
/**
* Records inventory transactions.
* @param {number} tenantId
* @param {IBillLandedCostTransaction} billLandedCost
* @param {IBill} bill -
*/
public recordInventoryTransactions = async (
billLandedCost: IBillLandedCostTransaction,
bill: Bill,
trx?: Knex.Transaction,
) => {
// Retrieve the merged allocated entries with bill entries.
const allocateEntries = mergeLocatedWithBillEntries(
billLandedCost.allocateEntries,
bill.entries,
);
// Mappes the allocate cost entries to inventory transactions.
const inventoryTransactions = allocateEntries.map((allocateEntry) => ({
date: bill.billDate,
itemId: allocateEntry.entry.itemId,
direction: 'IN',
quantity: null,
rate: allocateEntry.cost,
transactionType: 'LandedCost',
transactionId: billLandedCost.id,
entryId: allocateEntry.entryId,
}));
// Writes inventory transactions.
return this.inventoryTransactionsService.recordInventoryTransactions(
inventoryTransactions,
false,
trx,
);
};
/**
* Deletes the inventory transaction.
* @param {number} tenantId - Tenant id.
* @param {number} landedCostId - Landed cost id.
* @param {Knex.Transaction} trx - Knex transactions.
* @returns
*/
public removeInventoryTransactions = (
landedCostId: number,
trx?: Knex.Transaction,
) => {
return this.inventoryTransactionsService.deleteInventoryTransactions(
landedCostId,
'LandedCost',
trx,
);
};
}

View File

@@ -1,49 +0,0 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
IAllocatedLandedCostCreatedPayload,
IAllocatedLandedCostDeletedPayload,
} from '../types/BillLandedCosts.types';
import { events } from '@/common/events/events';
import { LandedCostInventoryTransactions } from './LandedCostInventoryTransactions.service';
@Injectable()
export class LandedCostInventoryTransactionsSubscriber {
constructor(
private readonly landedCostInventory: LandedCostInventoryTransactions,
) {}
/**
* Writes inventory transactions of the landed cost transaction once created.
* @param {IAllocatedLandedCostCreatedPayload} payload -
*/
@OnEvent(events.billLandedCost.onCreated)
async writeInventoryTransactionsOnceCreated({
billLandedCost,
trx,
bill,
}: IAllocatedLandedCostCreatedPayload) {
// Records the inventory transactions.
await this.landedCostInventory.recordInventoryTransactions(
billLandedCost,
bill,
trx,
);
}
/**
* Reverts inventory transactions of the landed cost transaction once deleted.
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
@OnEvent(events.billLandedCost.onDeleted)
async revertInventoryTransactionsOnceDeleted({
oldBillLandedCost,
trx,
}: IAllocatedLandedCostDeletedPayload) {
// Removes the inventory transactions.
await this.landedCostInventory.removeInventoryTransactions(
oldBillLandedCost.id,
trx,
);
}
}

View File

@@ -1,73 +0,0 @@
import { Knex } from 'knex';
import { CONFIG } from '../utils';
import { Injectable } from '@nestjs/common';
import { TransactionLandedCost } from './TransctionLandedCost.service';
@Injectable()
export class LandedCostSyncCostTransactions {
constructor(
private readonly transactionLandedCost: TransactionLandedCost,
) {}
/**
* Allocate the landed cost amount to cost transactions.
* @param {number} tenantId -
* @param {string} transactionType
* @param {number} transactionId
*/
public incrementLandedCostAmount = async (
transactionType: string,
transactionId: number,
transactionEntryId: number,
amount: number,
trx?: Knex.Transaction
): Promise<void> => {
const Model = this.transactionLandedCost.getModel(
transactionType
);
const relation = CONFIG.COST_TYPES[transactionType].entries;
// Increment the landed cost transaction amount.
await Model.query(trx)
.where('id', transactionId)
.increment('allocatedCostAmount', amount);
// Increment the landed cost entry.
await Model.relatedQuery(relation, trx)
.for(transactionId)
.where('id', transactionEntryId)
.increment('allocatedCostAmount', amount);
};
/**
* Reverts the landed cost amount to cost transaction.
* @param {number} tenantId - Tenant id.
* @param {string} transactionType - Transaction type.
* @param {number} transactionId - Transaction id.
* @param {number} transactionEntryId - Transaction entry id.
* @param {number} amount - Amount
*/
public revertLandedCostAmount = async (
transactionType: string,
transactionId: number,
transactionEntryId: number,
amount: number,
trx?: Knex.Transaction
) => {
const Model = this.transactionLandedCost.getModel(
transactionType
);
const relation = CONFIG.COST_TYPES[transactionType].entries;
// Decrement the allocate cost amount of cost transaction.
await Model.query(trx)
.where('id', transactionId)
.decrement('allocatedCostAmount', amount);
// Decrement the allocated cost amount cost transaction entry.
await Model.relatedQuery(relation, trx)
.for(transactionId)
.where('id', transactionEntryId)
.decrement('allocatedCostAmount', amount);
};
}

View File

@@ -1,53 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
IAllocatedLandedCostCreatedPayload,
IAllocatedLandedCostDeletedPayload,
} from '../types/BillLandedCosts.types';
import { events } from '@/common/events/events';
import { LandedCostSyncCostTransactions } from './LandedCostSyncCostTransactions.service';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class LandedCostSyncCostTransactionsSubscriber {
constructor(
private landedCostSyncCostTransaction: LandedCostSyncCostTransactions,
) {}
/**
* Increment cost transactions once the landed cost allocated.
* @param {IAllocatedLandedCostCreatedPayload} payload -
*/
@OnEvent(events.billLandedCost.onCreated)
async incrementCostTransactionsOnceCreated({
billLandedCost,
trx,
}: IAllocatedLandedCostCreatedPayload) {
// Increment landed cost amount on transaction and entry.
await this.landedCostSyncCostTransaction.incrementLandedCostAmount(
billLandedCost.fromTransactionType,
billLandedCost.fromTransactionId,
billLandedCost.fromTransactionEntryId,
billLandedCost.amount,
trx,
);
}
/**
* Decrement cost transactions once the allocated landed cost reverted.
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
@OnEvent(events.billLandedCost.onDeleted)
async decrementCostTransactionsOnceDeleted({
oldBillLandedCost,
trx,
}: IAllocatedLandedCostDeletedPayload) {
// Reverts the landed cost amount to the cost transaction.
await this.landedCostSyncCostTransaction.revertLandedCostAmount(
oldBillLandedCost.fromTransactionType,
oldBillLandedCost.fromTransactionId,
oldBillLandedCost.fromTransactionEntryId,
oldBillLandedCost.amount,
trx,
);
}
}

Some files were not shown because too many files have changed in this diff Show More