Compare commits
20 Commits
users-modu
...
refactor-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1130975efd | ||
|
|
fa180b3ac5 | ||
|
|
90d6bea9b9 | ||
|
|
4366bf478a | ||
|
|
0a57b6e20e | ||
|
|
9a685ffe5d | ||
|
|
51988dba3b | ||
|
|
f87bd341e9 | ||
|
|
5595478e19 | ||
|
|
7247b52fe5 | ||
|
|
deadd5ac80 | ||
|
|
66a2261e50 | ||
|
|
c51347d3ec | ||
|
|
b7a3c42074 | ||
|
|
83c9392b74 | ||
|
|
24bf3dd06d | ||
|
|
2b3f98d8fe | ||
|
|
4e64a9eadb | ||
|
|
0823bfc4e9 | ||
|
|
99fe5a6b0d |
7
packages/server/src/common/config/bankfeed.ts
Normal file
7
packages/server/src/common/config/bankfeed.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('bankfeed', () => ({
|
||||
enabled:
|
||||
process.env.BANK_FEED_ENABLED === 'true' ||
|
||||
process.env.BANK_FEED_ENABLED === 'yes',
|
||||
}));
|
||||
@@ -13,6 +13,7 @@ import signupRestrictions from './signup-restrictions';
|
||||
import jwt from './jwt';
|
||||
import mail from './mail';
|
||||
import loops from './loops';
|
||||
import bankfeed from './bankfeed';
|
||||
|
||||
export const config = [
|
||||
systemDatabase,
|
||||
@@ -29,5 +30,6 @@ export const config = [
|
||||
signupRestrictions,
|
||||
jwt,
|
||||
mail,
|
||||
loops
|
||||
loops,
|
||||
bankfeed,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
32
packages/server/src/common/decorators/Validators.ts
Normal file
32
packages/server/src/common/decorators/Validators.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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 (won’t 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);
|
||||
}
|
||||
@@ -70,7 +70,10 @@ export class SerializeInterceptor implements NestInterceptor<any, any> {
|
||||
next: CallHandler<any>,
|
||||
): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
// Transform both body and query parameters
|
||||
request.body = this.strategy.in(request.body);
|
||||
request.query = this.strategy.in(request.query);
|
||||
|
||||
// handle returns stream..
|
||||
return next.handle().pipe(map(this.strategy.out));
|
||||
|
||||
@@ -14,12 +14,15 @@ export class ValidationPipe implements PipeTransform<any> {
|
||||
return 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) {
|
||||
throw new BadRequestException(errors);
|
||||
}
|
||||
return value;
|
||||
return object;
|
||||
}
|
||||
|
||||
private toValidate(metatype: Function): boolean {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"previoud_period_date": "{{date}} (PP)",
|
||||
"previoud_period_date": "{date} (PP)",
|
||||
"fianncial_sheet.previous_period_change": "Change (PP)",
|
||||
"previous_period_percentage": "% Change (PP)",
|
||||
"previous_year_date": "{{date}} (PY)",
|
||||
"previous_year_date": "{date} (PY)",
|
||||
"previous_year_change": "Change (PY)",
|
||||
"previous_year_percentage": "% Change (PY)",
|
||||
"total_row": "Total {{value}}"
|
||||
"total_row": "Total {value}"
|
||||
}
|
||||
|
||||
4
packages/server/src/i18n/en/inventory_adjustment.json
Normal file
4
packages/server/src/i18n/en/inventory_adjustment.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"decrement": "Decrement",
|
||||
"increment": "Increment"
|
||||
}
|
||||
4
packages/server/src/i18n/en/warehouses.json
Normal file
4
packages/server/src/i18n/en/warehouses.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"primary_warehouse": "Primary Warehouse"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import FormData from 'form-data';
|
||||
import * as FormData from 'form-data';
|
||||
import { GotenbergUtils } from './GotenbergUtils';
|
||||
import { PageProperties } from './_types';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import FormData from 'form-data';
|
||||
import Axios from 'axios';
|
||||
import * as FormData from 'form-data';
|
||||
import { Axios } from 'axios';
|
||||
|
||||
export class GotenbergUtils {
|
||||
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> {
|
||||
try {
|
||||
const response = await Axios.post(endpoint, data, {
|
||||
const response = await new Axios({
|
||||
headers: {
|
||||
...data.getHeaders(),
|
||||
},
|
||||
responseType: 'arraybuffer', // This ensures you get a Buffer bac
|
||||
});
|
||||
}).post(endpoint, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { constants, createReadStream, PathLike, promises } from 'fs';
|
||||
import FormData from 'form-data';
|
||||
import * as FormData from 'form-data';
|
||||
import { GotenbergUtils } from './GotenbergUtils';
|
||||
import { IConverter, PageProperties } from './_types';
|
||||
import { PdfFormat, ChromiumRoute } from './_types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import FormData from 'form-data';
|
||||
import * as FormData from 'form-data';
|
||||
import { IConverter, PageProperties, PdfFormat, ChromiumRoute } from './_types';
|
||||
import { ConverterUtils } from './ConvertUtils';
|
||||
import { Converter } from './Converter';
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Account } from './models/Account.model';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
// @Service()
|
||||
// export class MutateBaseCurrencyAccounts {
|
||||
// @Inject()
|
||||
// tenancy: HasTenancyService;
|
||||
@Injectable()
|
||||
export class MutateBaseCurrencyAccounts {
|
||||
constructor(
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) {}
|
||||
|
||||
// /**
|
||||
// * Mutates the all accounts or the organziation.
|
||||
// * @param {number} tenantId
|
||||
// * @param {string} currencyCode
|
||||
// */
|
||||
// public mutateAllAccountsCurrency = async (
|
||||
// tenantId: number,
|
||||
// currencyCode: string
|
||||
// ) => {
|
||||
// const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
// await Account.query().update({ currencyCode });
|
||||
// };
|
||||
// }
|
||||
/**
|
||||
* Mutates the all accounts or the organziation.
|
||||
* @param {string} currencyCode
|
||||
*/
|
||||
async mutateAllAccountsCurrency(currencyCode: string) {
|
||||
await this.accountModel().query().update({ currencyCode });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { events } from '@/common/events/events';
|
||||
import { MutateBaseCurrencyAccounts } from '../MutateBaseCurrencyAccounts';
|
||||
|
||||
@Injectable()
|
||||
export class MutateBaseCurrencyAccountsSubscriber {
|
||||
constructor(
|
||||
public readonly mutateBaseCurrencyAccounts: MutateBaseCurrencyAccounts,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Updates the all accounts currency once the base currency
|
||||
* of the organization is mutated.
|
||||
*/
|
||||
@OnEvent(events.organization.baseCurrencyUpdated)
|
||||
async updateAccountsCurrencyOnBaseCurrencyMutated({
|
||||
organizationDTO,
|
||||
}) {
|
||||
await this.mutateBaseCurrencyAccounts.mutateAllAccountsCurrency(
|
||||
organizationDTO.baseCurrency
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// import { Service, Inject } from 'typedi';
|
||||
// import events from '@/subscribers/events';
|
||||
// import { MutateBaseCurrencyAccounts } from '../MutateBaseCurrencyAccounts';
|
||||
|
||||
// @Service()
|
||||
// export class MutateBaseCurrencyAccountsSubscriber {
|
||||
// @Inject()
|
||||
// public mutateBaseCurrencyAccounts: MutateBaseCurrencyAccounts;
|
||||
|
||||
// /**
|
||||
// * Attaches the events with handles.
|
||||
// * @param bus
|
||||
// */
|
||||
// attach(bus) {
|
||||
// bus.subscribe(
|
||||
// events.organization.baseCurrencyUpdated,
|
||||
// this.updateAccountsCurrencyOnBaseCurrencyMutated
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Updates the all accounts currency once the base currency
|
||||
// * of the organization is mutated.
|
||||
// */
|
||||
// private updateAccountsCurrencyOnBaseCurrencyMutated = async ({
|
||||
// tenantId,
|
||||
// organizationDTO,
|
||||
// }) => {
|
||||
// await this.mutateBaseCurrencyAccounts.mutateAllAccountsCurrency(
|
||||
// tenantId,
|
||||
// organizationDTO.baseCurrency
|
||||
// );
|
||||
// };
|
||||
// }
|
||||
@@ -86,6 +86,12 @@ import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/Credit
|
||||
import { ResourceModule } from '../Resource/Resource.module';
|
||||
import { ViewsModule } from '../Views/Views.module';
|
||||
import { CurrenciesModule } from '../Currencies/Currencies.module';
|
||||
import { MiscellaneousModule } from '../Miscellaneous/Miscellaneous.module';
|
||||
import { UsersModule } from '../UsersModule/Users.module';
|
||||
import { ContactsModule } from '../Contacts/Contacts.module';
|
||||
import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
|
||||
import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module';
|
||||
import { TenantModelsInitializeModule } from '../Tenancy/TenantModelsInitialize.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -147,6 +153,7 @@ import { CurrenciesModule } from '../Currencies/Currencies.module';
|
||||
ScheduleModule.forRoot(),
|
||||
TenancyDatabaseModule,
|
||||
TenancyModelsModule,
|
||||
TenantModelsInitializeModule,
|
||||
AuthModule,
|
||||
TenancyModule,
|
||||
ChromiumlyTenancyModule,
|
||||
@@ -179,10 +186,12 @@ import { CurrenciesModule } from '../Currencies/Currencies.module';
|
||||
LedgerModule,
|
||||
BankAccountsModule,
|
||||
BankRulesModule,
|
||||
BankingTransactionsModule,
|
||||
BankingTransactionsExcludeModule,
|
||||
BankingTransactionsRegonizeModule,
|
||||
BankingTransactionsModule,
|
||||
BankingMatchingModule,
|
||||
BankingPlaidModule,
|
||||
BankingCategorizeModule,
|
||||
TransactionsLockingModule,
|
||||
SettingsModule,
|
||||
FeaturesModule,
|
||||
@@ -206,7 +215,10 @@ import { CurrenciesModule } from '../Currencies/Currencies.module';
|
||||
ImportModule,
|
||||
ResourceModule,
|
||||
ViewsModule,
|
||||
CurrenciesModule
|
||||
CurrenciesModule,
|
||||
MiscellaneousModule,
|
||||
UsersModule,
|
||||
ContactsModule
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -33,6 +33,7 @@ const models = [
|
||||
|
||||
@Module({
|
||||
imports: [S3Module, ...models],
|
||||
exports: [...models],
|
||||
controllers: [AttachmentsController],
|
||||
providers: [
|
||||
DeleteAttachment,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import path from 'path';
|
||||
import * as path from 'path';
|
||||
// import config from '@/config';
|
||||
|
||||
export const getUploadedObjectUri = (objectKey: string) => {
|
||||
|
||||
@@ -23,13 +23,13 @@ export class EditBankRuleService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param createDTO
|
||||
* Transforms the given edit bank rule dto to model object.
|
||||
* @param editDTO
|
||||
* @returns
|
||||
*/
|
||||
private transformDTO(createDTO: EditBankRuleDto): ModelObject<BankRule> {
|
||||
private transformDTO(editDTO: EditBankRuleDto): ModelObject<BankRule> {
|
||||
return {
|
||||
...createDTO,
|
||||
...editDTO,
|
||||
} as ModelObject<BankRule>;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from 'class-validator';
|
||||
import { BankRuleComparator } from '../types';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ToNumber } from '@/common/decorators/Validators';
|
||||
|
||||
class BankRuleConditionDto {
|
||||
@IsNotEmpty()
|
||||
@@ -44,6 +45,8 @@ export class CommandBankRuleDto {
|
||||
})
|
||||
name: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ApiProperty({
|
||||
@@ -53,6 +56,7 @@ export class CommandBankRuleDto {
|
||||
order: number;
|
||||
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ApiProperty({
|
||||
@@ -61,6 +65,7 @@ export class CommandBankRuleDto {
|
||||
})
|
||||
applyIfAccountId?: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsIn(['deposit', 'withdrawal'])
|
||||
@ApiProperty({
|
||||
description: 'The transaction type to apply the rule if',
|
||||
@@ -82,11 +87,14 @@ export class CommandBankRuleDto {
|
||||
@Type(() => BankRuleConditionDto)
|
||||
@ApiProperty({
|
||||
description: 'The conditions to apply the rule if',
|
||||
example: [{ field: 'description', comparator: 'contains', value: 'Salary' }],
|
||||
example: [
|
||||
{ field: 'description', comparator: 'contains', value: 'Salary' },
|
||||
],
|
||||
})
|
||||
conditions: BankRuleConditionDto[];
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
description: 'The category to assign the rule if',
|
||||
example: 'Income:Salary',
|
||||
@@ -95,6 +103,8 @@ export class CommandBankRuleDto {
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ToNumber()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
description: 'The account ID to assign the rule if',
|
||||
example: 1,
|
||||
|
||||
@@ -15,6 +15,8 @@ import { BankingTransactionsModule } from '../BankingTransactions/BankingTransac
|
||||
import { GetBankAccountsService } from './queries/GetBankAccounts';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
|
||||
import { MutateBaseCurrencyAccountsSubscriber } from '../Accounts/susbcribers/MutateBaseCurrencyAccounts.subscriber';
|
||||
import { MutateBaseCurrencyAccounts } from '../Accounts/MutateBaseCurrencyAccounts';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -23,7 +25,7 @@ import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
|
||||
BankRulesModule,
|
||||
BankingTransactionsRegonizeModule,
|
||||
BankingTransactionsModule,
|
||||
DynamicListModule
|
||||
DynamicListModule,
|
||||
],
|
||||
providers: [
|
||||
DisconnectBankAccountService,
|
||||
@@ -34,7 +36,9 @@ import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
|
||||
DisconnectPlaidItemOnAccountDeleted,
|
||||
BankAccountsApplication,
|
||||
GetBankAccountsService,
|
||||
GetBankAccountSummary
|
||||
GetBankAccountSummary,
|
||||
MutateBaseCurrencyAccounts,
|
||||
MutateBaseCurrencyAccountsSubscriber,
|
||||
],
|
||||
exports: [BankAccountsApplication],
|
||||
controllers: [BankAccountsController],
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Knex } from 'knex';
|
||||
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { initialize } from 'objection';
|
||||
import { MatchedBankTransaction } from '@/modules/BankingMatching/models/MatchedBankTransaction';
|
||||
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||
import { RecognizedBankTransaction } from '@/modules/BankingTranasctionsRegonize/models/RecognizedBankTransaction';
|
||||
|
||||
@Injectable()
|
||||
export class GetBankAccountSummary {
|
||||
@@ -14,6 +19,19 @@ export class GetBankAccountSummary {
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
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,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -27,6 +45,11 @@ export class GetBankAccountSummary {
|
||||
.findById(bankAccountId)
|
||||
.throwIfNotFound();
|
||||
|
||||
await initialize(this.tenantDb(), [
|
||||
this.uncategorizedBankTransactionModel(),
|
||||
this.matchedBankTransactionModel(),
|
||||
this.recognizedBankTransaction(),
|
||||
]);
|
||||
const commonQuery = (q) => {
|
||||
// Include just the given account.
|
||||
q.where('accountId', bankAccountId);
|
||||
@@ -37,11 +60,6 @@ export class GetBankAccountSummary {
|
||||
// Only the not categorized.
|
||||
q.modify('notCategorized');
|
||||
};
|
||||
|
||||
interface UncategorizedTransactionsCount {
|
||||
total: number;
|
||||
}
|
||||
|
||||
// Retrieves the uncategorized transactions count of the given bank account.
|
||||
const uncategorizedTranasctionsCount =
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Knex } from 'knex';
|
||||
import { CategorizeBankTransaction } from './commands/CategorizeBankTransaction';
|
||||
import { UncategorizeBankTransactionService } from './commands/UncategorizeBankTransaction.service';
|
||||
import { UncategorizeBankTransactionsBulk } from './commands/UncategorizeBankTransactionsBulk.service';
|
||||
import { UncategorizedBankTransactionDto } from './dtos/CreateUncategorizedBankTransaction.dto';
|
||||
import { CategorizeBankTransactionDto } from './dtos/CategorizeBankTransaction.dto';
|
||||
import { CategorizeTransactionAsExpense } from './commands/CategorizeTransactionAsExpense';
|
||||
import { CreateUncategorizedTransactionService } from './commands/CreateUncategorizedTransaction.service';
|
||||
import { ICategorizeCashflowTransactioDTO } from './types/BankingCategorize.types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class BankingCategorizeApplication {
|
||||
constructor(
|
||||
private readonly categorizeBankTransaction: CategorizeBankTransaction,
|
||||
private readonly uncategorizeBankTransaction: UncategorizeBankTransactionService,
|
||||
private readonly uncategorizeBankTransactionsBulk: UncategorizeBankTransactionsBulk,
|
||||
private readonly categorizeTransactionAsExpense: CategorizeTransactionAsExpense,
|
||||
private readonly createUncategorizedTransaction: CreateUncategorizedTransactionService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Categorize a bank transaction with the given ID and categorization data.
|
||||
* @param {number | Array<number>} uncategorizedTransactionId - The ID(s) of the uncategorized transaction(s) to categorize.
|
||||
* @param {CategorizeBankTransactionDto} categorizeDTO - Data for categorization.
|
||||
* @returns {Promise<any>} The result of the categorization operation.
|
||||
*/
|
||||
public categorizeTransaction(
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
categorizeDTO: CategorizeBankTransactionDto,
|
||||
) {
|
||||
return this.categorizeBankTransaction.categorize(
|
||||
uncategorizedTransactionId,
|
||||
categorizeDTO,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uncategorize a bank transaction with the given ID.
|
||||
* @param {number} uncategorizedTransactionId - The ID of the transaction to uncategorize.
|
||||
* @returns {Promise<Array<number>>} Array of affected transaction IDs.
|
||||
*/
|
||||
public uncategorizeTransaction(
|
||||
uncategorizedTransactionId: number,
|
||||
): Promise<Array<number>> {
|
||||
return this.uncategorizeBankTransaction.uncategorize(
|
||||
uncategorizedTransactionId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uncategorize multiple bank transactions in bulk.
|
||||
* @param {number | Array<number>} uncategorizedTransactionIds - The ID(s) of the transaction(s) to uncategorize.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public uncategorizeTransactionsBulk(
|
||||
uncategorizedTransactionIds: number | Array<number>,
|
||||
) {
|
||||
return this.uncategorizeBankTransactionsBulk.uncategorizeBulk(
|
||||
uncategorizedTransactionIds,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize a transaction as an expense.
|
||||
* @param {number} cashflowTransactionId - The ID of the cashflow transaction to categorize.
|
||||
* @param {ICategorizeCashflowTransactioDTO} transactionDTO - Data for categorization.
|
||||
* @returns {Promise<any>} The result of the categorization operation.
|
||||
*/
|
||||
public categorizeTransactionAsExpenseType(
|
||||
cashflowTransactionId: number,
|
||||
transactionDTO: ICategorizeCashflowTransactioDTO,
|
||||
) {
|
||||
return this.categorizeTransactionAsExpense.categorize(
|
||||
cashflowTransactionId,
|
||||
transactionDTO,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new uncategorized bank transaction.
|
||||
* @param {UncategorizedBankTransactionDto} createDTO - Data for creating the uncategorized transaction.
|
||||
* @param {Knex.Transaction} [trx] - Optional Knex transaction.
|
||||
* @returns {Promise<any>} The created uncategorized transaction.
|
||||
*/
|
||||
public createUncategorizedBankTransaction(
|
||||
createDTO: UncategorizedBankTransactionDto,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
return this.createUncategorizedTransaction.create(createDTO, trx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,38 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { CreateUncategorizedTransactionService } from './commands/CreateUncategorizedTransaction.service';
|
||||
import { CategorizeTransactionAsExpense } from './commands/CategorizeTransactionAsExpense';
|
||||
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
|
||||
import { ExpensesModule } from '../Expenses/Expenses.module';
|
||||
import { UncategorizedTransactionsImportable } from './commands/UncategorizedTransactionsImportable';
|
||||
import { BankingCategorizeController } from './BankingCategorize.controller';
|
||||
import { BankingCategorizeApplication } from './BankingCategorize.application';
|
||||
import { CategorizeBankTransaction } from './commands/CategorizeBankTransaction';
|
||||
import { UncategorizeBankTransactionService } from './commands/UncategorizeBankTransaction.service';
|
||||
import { UncategorizeBankTransactionsBulk } from './commands/UncategorizeBankTransactionsBulk.service';
|
||||
|
||||
@Module({
|
||||
imports: [BankingTransactionsModule, ExpensesModule],
|
||||
imports: [
|
||||
BankingTransactionsModule,
|
||||
ExpensesModule,
|
||||
forwardRef(() => BankingTransactionsModule),
|
||||
],
|
||||
providers: [
|
||||
CreateUncategorizedTransactionService,
|
||||
CategorizeTransactionAsExpense,
|
||||
UncategorizedTransactionsImportable
|
||||
UncategorizedTransactionsImportable,
|
||||
BankingCategorizeApplication,
|
||||
CategorizeBankTransaction,
|
||||
UncategorizeBankTransactionService,
|
||||
UncategorizeBankTransactionsBulk,
|
||||
],
|
||||
exports: [
|
||||
CreateUncategorizedTransactionService,
|
||||
CategorizeTransactionAsExpense,
|
||||
BankingCategorizeApplication,
|
||||
CategorizeBankTransaction,
|
||||
UncategorizeBankTransactionService,
|
||||
UncategorizeBankTransactionsBulk,
|
||||
],
|
||||
controllers: [BankingCategorizeController],
|
||||
})
|
||||
export class BankingCategorizeModule {}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Knex } from 'knex';
|
||||
import {
|
||||
ICashflowTransactionCategorizedPayload,
|
||||
ICashflowTransactionUncategorizingPayload,
|
||||
ICategorizeCashflowTransactioDTO,
|
||||
} from '../types/BankingCategorize.types';
|
||||
import {
|
||||
transformCategorizeTransToCashflow,
|
||||
@@ -17,9 +16,10 @@ import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { CategorizeBankTransactionDto } from '../dtos/CategorizeBankTransaction.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CategorizeCashflowTransaction {
|
||||
export class CategorizeBankTransaction {
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
@@ -38,7 +38,7 @@ export class CategorizeCashflowTransaction {
|
||||
*/
|
||||
public async categorize(
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
categorizeDTO: ICategorizeCashflowTransactioDTO,
|
||||
categorizeDTO: CategorizeBankTransactionDto,
|
||||
) {
|
||||
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
||||
|
||||
@@ -68,7 +68,6 @@ export class CategorizeCashflowTransaction {
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionCategorizing,
|
||||
{
|
||||
// tenantId,
|
||||
oldUncategorizedTransactions,
|
||||
trx,
|
||||
} as ICashflowTransactionUncategorizingPayload,
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
CreateUncategorizedTransactionDTO,
|
||||
IUncategorizedTransactionCreatedEventPayload,
|
||||
IUncategorizedTransactionCreatingEventPayload,
|
||||
} from '../types/BankingCategorize.types';
|
||||
@@ -10,6 +9,7 @@ import { UncategorizedBankTransaction } from '../../BankingTransactions/models/U
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { UncategorizedBankTransactionDto } from '../dtos/CreateUncategorizedBankTransaction.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CreateUncategorizedTransactionService {
|
||||
@@ -30,7 +30,7 @@ export class CreateUncategorizedTransactionService {
|
||||
* @returns {Promise<UncategorizedBankTransaction>}
|
||||
*/
|
||||
public create(
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
|
||||
createUncategorizedTransactionDTO: UncategorizedBankTransactionDto,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { UncategorizedBankTransaction } from '../../BankingTransactions/models/U
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class UncategorizeCashflowTransactionService {
|
||||
export class UncategorizeBankTransactionService {
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
@@ -1,18 +1,17 @@
|
||||
import { castArray } from 'lodash';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UncategorizeCashflowTransactionService } from './UncategorizeCashflowTransaction.service';
|
||||
import { UncategorizeBankTransactionService } from './UncategorizeBankTransaction.service';
|
||||
|
||||
@Injectable()
|
||||
export class UncategorizeCashflowTransactionsBulk {
|
||||
export class UncategorizeBankTransactionsBulk {
|
||||
constructor(
|
||||
private readonly uncategorizeTransactionService: UncategorizeCashflowTransactionService
|
||||
private readonly uncategorizeTransactionService: UncategorizeBankTransactionService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Uncategorize the given bank transactions in bulk.
|
||||
* @param {number} tenantId
|
||||
* @param {number} uncategorizedTransactionId
|
||||
* @param {number | Array<number>} uncategorizedTransactionId
|
||||
*/
|
||||
public async uncategorizeBulk(
|
||||
uncategorizedTransactionId: number | Array<number>
|
||||
@@ -0,0 +1,113 @@
|
||||
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>;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { IsBoolean, IsDateString, IsNumber, IsString } from 'class-validator';
|
||||
|
||||
export class UncategorizedBankTransactionDto {
|
||||
@IsDateString()
|
||||
date: Date | string;
|
||||
|
||||
@IsNumber()
|
||||
accountId: number;
|
||||
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@IsString()
|
||||
currencyCode: string;
|
||||
|
||||
@IsString()
|
||||
payee?: string;
|
||||
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
referenceNo?: string | null;
|
||||
|
||||
@IsString()
|
||||
plaidTransactionId?: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
pending?: boolean;
|
||||
|
||||
@IsString()
|
||||
pendingPlaidTransactionId?: string | null;
|
||||
|
||||
@IsString()
|
||||
batch?: string;
|
||||
}
|
||||
@@ -1,47 +1,44 @@
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { BankingMatchingApplication } from './BankingMatchingApplication';
|
||||
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
|
||||
import { GetMatchedTransactionsFilter } from './types';
|
||||
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
|
||||
|
||||
@Controller('banking/matching')
|
||||
@ApiTags('banking-transactions-matching')
|
||||
export class BankingMatchingController {
|
||||
constructor(
|
||||
private readonly bankingMatchingApplication: BankingMatchingApplication
|
||||
private readonly bankingMatchingApplication: BankingMatchingApplication,
|
||||
) {}
|
||||
|
||||
@Get('matched/transactions')
|
||||
@Get('matched')
|
||||
@ApiOperation({ summary: 'Retrieves the matched transactions.' })
|
||||
async getMatchedTransactions(
|
||||
@Query('uncategorizedTransactionIds') uncategorizedTransactionIds: number[],
|
||||
@Query() filter: GetMatchedTransactionsFilter
|
||||
@Query() filter: GetMatchedTransactionsFilter,
|
||||
) {
|
||||
return this.bankingMatchingApplication.getMatchedTransactions(
|
||||
uncategorizedTransactionIds,
|
||||
filter
|
||||
filter,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/match/:uncategorizedTransactionId')
|
||||
@Post('/match')
|
||||
@ApiOperation({ summary: 'Match the given uncategorized transaction.' })
|
||||
async matchTransaction(
|
||||
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number | number[],
|
||||
@Body() matchedTransactions: MatchBankTransactionDto
|
||||
) {
|
||||
async matchTransaction(@Body() matchedTransactions: MatchBankTransactionDto) {
|
||||
return this.bankingMatchingApplication.matchTransaction(
|
||||
uncategorizedTransactionId,
|
||||
matchedTransactions
|
||||
matchedTransactions.uncategorizedTransactions,
|
||||
matchedTransactions.matchedTransactions,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/unmatch/:uncategorizedTransactionId')
|
||||
@ApiOperation({ summary: 'Unmatch the given uncategorized transaction.' })
|
||||
async unmatchMatchedTransaction(
|
||||
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number
|
||||
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number,
|
||||
) {
|
||||
return this.bankingMatchingApplication.unmatchMatchedTransaction(
|
||||
uncategorizedTransactionId
|
||||
uncategorizedTransactionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { GetMatchedTransactions } from './queries/GetMatchedTransactions.service';
|
||||
import { MatchBankTransactions } from './commands/MatchTransactions';
|
||||
import { UnmatchMatchedBankTransaction } from './commands/UnmatchMatchedTransaction.service';
|
||||
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
|
||||
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
|
||||
import { GetMatchedTransactionsFilter } from './types';
|
||||
import { MatchTransactionEntryDto } from './dtos/MatchBankTransaction.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BankingMatchingApplication {
|
||||
@@ -31,17 +31,18 @@ export class BankingMatchingApplication {
|
||||
|
||||
/**
|
||||
* Matches the given uncategorized transaction with the given system transaction.
|
||||
* @param {number} uncategorizedTransactionId
|
||||
* @param {IMatchTransactionDTO} matchTransactionsDTO
|
||||
* @param {IMatchBankTransactionDto} matchedTransactionsDTO
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public matchTransaction(
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
matchedTransactions: MatchBankTransactionDto,
|
||||
matchedTransactionsDto:
|
||||
| MatchTransactionEntryDto
|
||||
| Array<MatchTransactionEntryDto>,
|
||||
): Promise<void> {
|
||||
return this.matchTransactionService.matchTransaction(
|
||||
uncategorizedTransactionId,
|
||||
matchedTransactions,
|
||||
matchedTransactionsDto,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { MatchBankTransactionDto } from '../dtos/MatchBankTransaction.dto';
|
||||
import { MatchTransactionEntryDto } from '../dtos/MatchBankTransaction.dto';
|
||||
|
||||
@Injectable()
|
||||
export class MatchBankTransactions {
|
||||
@@ -107,16 +107,15 @@ export class MatchBankTransactions {
|
||||
|
||||
/**
|
||||
* Matches the given uncategorized transaction to the given references.
|
||||
* @param {number} tenantId
|
||||
* @param {number} uncategorizedTransactionId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async matchTransaction(
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
matchedTransactionsDto: MatchBankTransactionDto,
|
||||
matchedTransactionsDto: MatchTransactionEntryDto | Array<MatchTransactionEntryDto>,
|
||||
): Promise<void> {
|
||||
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
||||
const matchedTransactions = matchedTransactionsDto.entries;
|
||||
const matchedTransactions = castArray(matchedTransactionsDto);
|
||||
|
||||
// Validates the given matching transactions DTO.
|
||||
await this.validate(uncategorizedTransactionIds, matchedTransactions);
|
||||
@@ -131,7 +130,7 @@ export class MatchBankTransactions {
|
||||
// Matches the given transactions under promise pool concurrency controlling.
|
||||
await PromisePool.withConcurrency(10)
|
||||
.for(matchedTransactions)
|
||||
.process(async (matchedTransaction) => {
|
||||
.process(async (matchedTransaction: MatchTransactionEntryDto) => {
|
||||
const getMatchedTransactionsService =
|
||||
this.matchedBankTransactions.registry.get(
|
||||
matchedTransaction.referenceType,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
@@ -27,6 +28,10 @@ export class MatchTransactionEntryDto {
|
||||
}
|
||||
|
||||
export class MatchBankTransactionDto {
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
uncategorizedTransactions: Array<number>
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MatchTransactionEntryDto)
|
||||
@@ -37,5 +42,5 @@ export class MatchBankTransactionDto {
|
||||
{ referenceType: 'SaleInvoice', referenceId: 2 },
|
||||
],
|
||||
})
|
||||
entries: MatchTransactionEntryDto[];
|
||||
matchedTransactions: MatchTransactionEntryDto[];
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IBankTransactionMatchedEventPayload,
|
||||
IBankTransactionUnmatchedEventPayload,
|
||||
} from '../types';
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DecrementUncategorizedTransactionOnMatchingSubscriber {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Knex } from 'knex';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
||||
import { GetMatchedTransactionsFilter } from '../types';
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from '../types';
|
||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO, MatchedTransactionsPOJO } from '../types';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { GetMatchedTransactionExpensesTransformer } from './GetMatchedTransactionExpensesTransformer';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { Expense } from '@/modules/Expenses/models/Expense.model';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Knex } from 'knex';
|
||||
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
|
||||
import { initialize } from 'objection';
|
||||
|
||||
@Injectable()
|
||||
export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByType {
|
||||
@@ -13,17 +16,26 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
|
||||
|
||||
@Inject(Expense.name)
|
||||
protected readonly expenseModel: TenantModelProxy<typeof Expense>,
|
||||
|
||||
@Inject(TENANCY_DB_CONNECTION)
|
||||
private readonly tenantDb: () => Knex,
|
||||
|
||||
@Inject('TENANT_MODELS_INIT')
|
||||
private readonly tenantModelsInit: () => Promise<boolean>,
|
||||
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions of expenses.
|
||||
* @param {number} tenantId
|
||||
* @param {GetMatchedTransactionsFilter} filter
|
||||
* @returns
|
||||
*/
|
||||
async getMatchedTransactions(filter: GetMatchedTransactionsFilter) {
|
||||
async getMatchedTransactions(
|
||||
filter: GetMatchedTransactionsFilter,
|
||||
): Promise<MatchedTransactionsPOJO> {
|
||||
// await this.tenantModelsInit();
|
||||
// Retrieve the expense matches.
|
||||
const expenses = await this.expenseModel()
|
||||
.query()
|
||||
@@ -49,6 +61,7 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
|
||||
}
|
||||
query.orderBy('paymentDate', 'DESC');
|
||||
});
|
||||
|
||||
return this.transformer.transform(
|
||||
expenses,
|
||||
new GetMatchedTransactionExpensesTransformer(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { first } from 'lodash';
|
||||
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
} from '../types';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { CreatePaymentReceivedService } from '@/modules/PaymentReceived/commands/CreatePaymentReceived.serivce';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
@@ -86,7 +86,6 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
|
||||
|
||||
/**
|
||||
* Creates the common matched transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {Array<number>} uncategorizedTransactionIds
|
||||
* @param {IMatchTransactionDTO} matchTransactionDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { initialize } from 'objection';
|
||||
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { GetMatchedTransactionsFilter } from '../types';
|
||||
import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
|
||||
|
||||
@Injectable()
|
||||
export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactionsByType {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
export abstract class GetMatchedTransactionsByType {
|
||||
@Inject(MatchedBankTransaction.name)
|
||||
private readonly matchedBankTransactionModel: TenantModelProxy<
|
||||
matchedBankTransactionModel: TenantModelProxy<
|
||||
typeof MatchedBankTransaction
|
||||
>;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PlaidUpdateTransactionsOnItemCreatedSubscriber } from './subscribers/PlaidUpdateTransactionsOnItemCreatedSubscriber';
|
||||
import { PlaidUpdateTransactions } from './command/PlaidUpdateTransactions';
|
||||
@@ -15,6 +16,11 @@ import { PlaidItemService } from './command/PlaidItem';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
|
||||
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)];
|
||||
|
||||
@@ -24,6 +30,7 @@ const models = [RegisterTenancyModel(PlaidItem)];
|
||||
AccountsModule,
|
||||
BankingCategorizeModule,
|
||||
BankingTransactionsModule,
|
||||
BullModule.registerQueue({ name: UpdateBankingPlaidTransitionsQueueJob }),
|
||||
...models,
|
||||
],
|
||||
providers: [
|
||||
@@ -34,9 +41,12 @@ const models = [RegisterTenancyModel(PlaidItem)];
|
||||
PlaidWebooks,
|
||||
PlaidLinkTokenService,
|
||||
PlaidApplication,
|
||||
PlaidUpdateTransactionsOnItemCreatedSubscriber,
|
||||
SetupPlaidItemTenantService,
|
||||
TenancyContext,
|
||||
PlaidFetchTransactionsProcessor,
|
||||
PlaidUpdateTransactionsOnItemCreatedSubscriber,
|
||||
],
|
||||
exports: [...models],
|
||||
controllers: [BankingPlaidController, BankingPlaidWebhooksController],
|
||||
})
|
||||
export class BankingPlaidModule {}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { PlaidLinkTokenService } from './queries/GetPlaidLinkToken.service';
|
||||
import { PlaidItemService } from './command/PlaidItem';
|
||||
import { PlaidWebooks } from './command/PlaidWebhooks';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PlaidItemDTO } from './types/BankingPlaid.types';
|
||||
import { PlaidItemDto } from './dtos/PlaidItem.dto';
|
||||
import { SystemPlaidItem } from './models/SystemPlaidItem';
|
||||
import { TenantModel } from '../System/models/TenantModel';
|
||||
import { SystemUser } from '../System/models/SystemUser';
|
||||
|
||||
@Injectable()
|
||||
export class PlaidApplication {
|
||||
@@ -10,6 +14,16 @@ export class PlaidApplication {
|
||||
private readonly getLinkTokenService: PlaidLinkTokenService,
|
||||
private readonly plaidItemService: PlaidItemService,
|
||||
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,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -25,7 +39,7 @@ export class PlaidApplication {
|
||||
* @param {PlaidItemDTO} itemDTO
|
||||
* @returns
|
||||
*/
|
||||
public exchangeToken(itemDTO: PlaidItemDTO): Promise<void> {
|
||||
public exchangeToken(itemDTO: PlaidItemDto): Promise<void> {
|
||||
return this.plaidItemService.item(itemDTO);
|
||||
}
|
||||
|
||||
@@ -41,10 +55,33 @@ export class PlaidApplication {
|
||||
webhookType: string,
|
||||
webhookCode: string,
|
||||
): Promise<void> {
|
||||
return this.plaidWebhooks.webhooks(
|
||||
plaidItemId,
|
||||
webhookType,
|
||||
webhookCode,
|
||||
);
|
||||
return this.plaidWebhooks.webhooks(plaidItemId, webhookType, webhookCode);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// 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();
|
||||
// };
|
||||
@@ -6,11 +6,9 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { SystemPlaidItem } from '../models/SystemPlaidItem';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import {
|
||||
IPlaidItemCreatedEventPayload,
|
||||
PlaidItemDTO,
|
||||
} from '../types/BankingPlaid.types';
|
||||
import { IPlaidItemCreatedEventPayload } from '../types/BankingPlaid.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { PlaidItemDto } from '../dtos/PlaidItem.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PlaidItemService {
|
||||
@@ -19,9 +17,7 @@ export class PlaidItemService {
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
|
||||
@Inject(SystemPlaidItem.name)
|
||||
private readonly systemPlaidItemModel: TenantModelProxy<
|
||||
typeof SystemPlaidItem
|
||||
>,
|
||||
private readonly systemPlaidItemModel: typeof SystemPlaidItem,
|
||||
|
||||
@Inject(PlaidItem.name)
|
||||
private readonly plaidItemModel: TenantModelProxy<typeof PlaidItem>,
|
||||
@@ -33,10 +29,10 @@ export class PlaidItemService {
|
||||
/**
|
||||
* Exchanges the public token to get access token and item id and then creates
|
||||
* a new Plaid item.
|
||||
* @param {PlaidItemDTO} itemDTO - Plaid item data transfer object.
|
||||
* @param {PlaidItemDto} itemDTO - Plaid item data transfer object.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async item(itemDTO: PlaidItemDTO): Promise<void> {
|
||||
public async item(itemDTO: PlaidItemDto): Promise<void> {
|
||||
const { publicToken, institutionId } = itemDTO;
|
||||
|
||||
const tenant = await this.tenancyContext.getTenant();
|
||||
@@ -57,7 +53,7 @@ export class PlaidItemService {
|
||||
plaidInstitutionId: institutionId,
|
||||
});
|
||||
// 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.
|
||||
await this.eventEmitter.emitAsync(events.plaid.onItemCreated, {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as R from 'ramda';
|
||||
import bluebird from 'bluebird';
|
||||
import * as bluebird from 'bluebird';
|
||||
import * as uniqid from 'uniqid';
|
||||
import { entries, groupBy } from 'lodash';
|
||||
import {
|
||||
AccountBase as PlaidAccountBase,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
transformPlaidTrxsToCashflowCreate,
|
||||
} from '../utils';
|
||||
import { Knex } from 'knex';
|
||||
import uniqid from 'uniqid';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { RemovePendingUncategorizedTransaction } from '../../BankingTransactions/commands/RemovePendingUncategorizedTransaction.service';
|
||||
import { CreateAccountService } from '../../Accounts/CreateAccount.service';
|
||||
|
||||
@@ -15,6 +15,13 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
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(
|
||||
private readonly plaidSync: PlaidSyncDb,
|
||||
private readonly uow: UnitOfWork,
|
||||
@@ -28,8 +35,7 @@ export class PlaidUpdateTransactions {
|
||||
|
||||
/**
|
||||
* Handles sync the Plaid item to Bigcaptial under UOW.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} plaidItemId - Plaid item id.
|
||||
* @param {string} plaidItemId - Plaid item id.
|
||||
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
|
||||
*/
|
||||
public async updateTransactions(plaidItemId: string) {
|
||||
@@ -44,9 +50,9 @@ export class PlaidUpdateTransactions {
|
||||
* - New bank accounts.
|
||||
* - Last accounts feeds updated at.
|
||||
* - Turn on the accounts feed flag.
|
||||
* @param {number} tenantId - Tenant ID.
|
||||
* @param {string} plaidItemId - The Plaid ID for the item.
|
||||
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
|
||||
*/
|
||||
public async updateTransactionsWork(
|
||||
plaidItemId: string,
|
||||
@@ -97,7 +103,6 @@ export class PlaidUpdateTransactions {
|
||||
|
||||
/**
|
||||
* Fetches transactions from the `Plaid API` for a given item.
|
||||
* @param {number} tenantId - Tenant ID.
|
||||
* @param {string} plaidItemId - The Plaid ID for the item.
|
||||
* @returns {Promise<PlaidFetchedTransactionsUpdates>}
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { PlaidItem } from '../models/PlaidItem';
|
||||
import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
@@ -8,7 +9,7 @@ export class PlaidWebooks {
|
||||
private readonly updateTransactionsService: PlaidUpdateTransactions,
|
||||
|
||||
@Inject(PlaidItem.name)
|
||||
private readonly plaidItemModel: typeof PlaidItem,
|
||||
private readonly plaidItemModel: TenantModelProxy<typeof PlaidItem>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -76,11 +77,10 @@ export class PlaidWebooks {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async handleTransactionsWebooks(
|
||||
tenantId: number,
|
||||
plaidItemId: string,
|
||||
webhookCode: string,
|
||||
): Promise<void> {
|
||||
const plaidItem = await this.plaidItemModel
|
||||
const plaidItem = await this.plaidItemModel()
|
||||
.query()
|
||||
.findOne({ plaidItemId })
|
||||
.throwIfNotFound();
|
||||
@@ -122,9 +122,8 @@ export class PlaidWebooks {
|
||||
|
||||
/**
|
||||
* Handles all Item webhook events.
|
||||
* @param {number} tenantId - Tenant ID
|
||||
* @param {string} webhookCode - The webhook code
|
||||
* @param {string} plaidItemId - The Plaid ID for the item
|
||||
* @param {string} webhookCode - The webhook code
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async itemsHandler(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,43 +1,46 @@
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
|
||||
// import { IPlaidItemCreatedEventPayload } from '@/interfaces';
|
||||
import { Process } from '@nestjs/bull';
|
||||
import { UseCls } from 'nestjs-cls';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
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';
|
||||
|
||||
// @Service()
|
||||
// export class PlaidFetchTransactionsJob {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define(
|
||||
// 'plaid-update-account-transactions',
|
||||
// { priority: 'high', concurrency: 2 },
|
||||
// this.handler
|
||||
// );
|
||||
// }
|
||||
@Processor({
|
||||
name: UpdateBankingPlaidTransitionsQueueJob,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class PlaidFetchTransactionsProcessor extends WorkerHost {
|
||||
constructor(
|
||||
private readonly plaidFetchTransactionsService: PlaidUpdateTransactions,
|
||||
private readonly setupPlaidItemService: SetupPlaidItemTenantService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Triggers the function.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const { tenantId, plaidItemId } = job.attrs
|
||||
// .data as IPlaidItemCreatedEventPayload;
|
||||
/**
|
||||
* Triggers the function.
|
||||
*/
|
||||
@Process(UpdateBankingPlaidTransitionsJob)
|
||||
@UseCls()
|
||||
async process(job: Job<PlaidFetchTransitonsEventPayload>) {
|
||||
const { plaidItemId } = job.data;
|
||||
|
||||
// const plaidFetchTransactionsService = Container.get(
|
||||
// PlaidUpdateTransactions
|
||||
// );
|
||||
// const io = Container.get('socket');
|
||||
|
||||
// try {
|
||||
// await plaidFetchTransactionsService.updateTransactions(
|
||||
// tenantId,
|
||||
// plaidItemId
|
||||
// );
|
||||
// // Notify the frontend to reflect the new transactions changes.
|
||||
// io.emit('NEW_TRANSACTIONS_DATA', { plaidItemId });
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
try {
|
||||
await this.setupPlaidItemService.setupPlaidTenant(plaidItemId, () => {
|
||||
return this.plaidFetchTransactionsService.updateTransactions(
|
||||
plaidItemId,
|
||||
);
|
||||
});
|
||||
// Notify the frontend to reflect the new transactions changes.
|
||||
// io.emit('NEW_TRANSACTIONS_DATA', { plaidItemId });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class SystemPlaidItem extends BaseModel {
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Tenant = require('system/models/Tenant');
|
||||
const { TenantModel } = require('../../System/models/TenantModel');
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -38,7 +38,7 @@ export class SystemPlaidItem extends BaseModel {
|
||||
*/
|
||||
tenant: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Tenant.default,
|
||||
modelClass: TenantModel,
|
||||
join: {
|
||||
from: 'users.tenantId',
|
||||
to: 'tenants.id',
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { events } from '@/common/events/events';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { IPlaidItemCreatedEventPayload } from '../types/BankingPlaid.types';
|
||||
import { Queue } from 'bullmq';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import {
|
||||
IPlaidItemCreatedEventPayload,
|
||||
UpdateBankingPlaidTransitionsJob,
|
||||
UpdateBankingPlaidTransitionsQueueJob,
|
||||
} from '../types/BankingPlaid.types';
|
||||
|
||||
@Injectable()
|
||||
export class PlaidUpdateTransactionsOnItemCreatedSubscriber {
|
||||
constructor(
|
||||
@InjectQueue(UpdateBankingPlaidTransitionsQueueJob)
|
||||
private readonly updateTransitionsQueue: Queue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Updates the Plaid item transactions
|
||||
* @param {IPlaidItemCreatedEventPayload} payload - Event payload.
|
||||
*/
|
||||
@OnEvent(events.plaid.onItemCreated)
|
||||
public async handleUpdateTransactionsOnItemCreated({
|
||||
tenantId,
|
||||
plaidItemId,
|
||||
plaidAccessToken,
|
||||
plaidInstitutionId,
|
||||
}: IPlaidItemCreatedEventPayload) {
|
||||
const payload = { tenantId, plaidItemId };
|
||||
// await this.agenda.now('plaid-update-account-transactions', payload);
|
||||
};
|
||||
const payload = { plaidItemId };
|
||||
|
||||
await this.updateTransitionsQueue.add(
|
||||
UpdateBankingPlaidTransitionsJob,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Knex } from "knex";
|
||||
import { RemovedTransaction, Transaction } from "plaid";
|
||||
import { Knex } from 'knex';
|
||||
import { RemovedTransaction, Transaction } from 'plaid';
|
||||
|
||||
export interface IPlaidTransactionsSyncedEventPayload {
|
||||
// tenantId: number;
|
||||
plaidAccountId: number;
|
||||
batch: string;
|
||||
trx?: Knex.Transaction
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface PlaidItemDTO {
|
||||
@@ -13,7 +13,6 @@ export interface PlaidItemDTO {
|
||||
institutionId: string;
|
||||
}
|
||||
|
||||
|
||||
export interface PlaidFetchedTransactionsUpdates {
|
||||
added: Transaction[];
|
||||
modified: Transaction[];
|
||||
@@ -22,11 +21,20 @@ export interface PlaidFetchedTransactionsUpdates {
|
||||
cursor: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface IPlaidItemCreatedEventPayload {
|
||||
tenantId: number;
|
||||
plaidAccessToken: string;
|
||||
plaidItemId: string;
|
||||
plaidInstitutionId: string;
|
||||
}
|
||||
|
||||
export const UpdateBankingPlaidTransitionsJob =
|
||||
'update-banking-plaid-transitions-job';
|
||||
|
||||
export const UpdateBankingPlaidTransitionsQueueJob =
|
||||
'update-banking-plaid-transitions-query';
|
||||
|
||||
|
||||
export interface PlaidFetchTransitonsEventPayload {
|
||||
plaidItemId: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,46 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
|
||||
import { RecognizedBankTransaction } from './models/RecognizedBankTransaction';
|
||||
import { GetAutofillCategorizeTransactionService } from './queries/GetAutofillCategorizeTransaction.service';
|
||||
import { RevertRecognizedTransactionsService } from './commands/RevertRecognizedTransactions.service';
|
||||
import { RecognizeTranasctionsService } from './commands/RecognizeTranasctions.service';
|
||||
import { TriggerRecognizedTransactionsSubscriber } from './events/TriggerRecognizedTransactions';
|
||||
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.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)];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BankingTransactionsModule,
|
||||
TenancyModule,
|
||||
forwardRef(() => BankRulesModule),
|
||||
BullModule.registerQueue({
|
||||
name: RecognizeUncategorizedTransactionsQueue,
|
||||
}),
|
||||
...models,
|
||||
],
|
||||
providers: [
|
||||
GetAutofillCategorizeTransactionService,
|
||||
RecognizedTransactionsApplication,
|
||||
GetRecognizedTransactionsService,
|
||||
RevertRecognizedTransactionsService,
|
||||
RecognizeTranasctionsService,
|
||||
TriggerRecognizedTransactionsSubscriber,
|
||||
GetRecognizedTransactionService,
|
||||
RegonizeTransactionsPrcessor,
|
||||
],
|
||||
exports: [
|
||||
...models,
|
||||
GetAutofillCategorizeTransactionService,
|
||||
RevertRecognizedTransactionsService,
|
||||
RecognizeTranasctionsService,
|
||||
],
|
||||
controllers: [BankingRecognizedTransactionsController],
|
||||
})
|
||||
export class BankingTransactionsRegonizeModule {}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer';
|
||||
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
|
||||
import { GetRecognizedTransactionTransformer } from './queries/GetRecognizedTransactionTransformer';
|
||||
import { UncategorizedBankTransaction } from '../BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { IGetRecognizedTransactionsQuery } from '../types/BankingTransactions.types';
|
||||
import { IGetRecognizedTransactionsQuery } from '../BankingTransactions/types/BankingTransactions.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetRecognizedTransactionsService {
|
||||
@@ -10,7 +11,7 @@ export class GetRecognizedTransactionsService {
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<typeof UncategorizedBankTransaction>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -25,7 +26,7 @@ export class GetRecognizedTransactionsService {
|
||||
...filter,
|
||||
};
|
||||
const { results, pagination } =
|
||||
await this.uncategorizedBankTransactionModel.query()
|
||||
await this.uncategorizedBankTransactionModel().query()
|
||||
.onBuild((q) => {
|
||||
q.withGraphFetched('recognizedTransaction.assignAccount');
|
||||
q.withGraphFetched('recognizedTransaction.bankRule');
|
||||
@@ -0,0 +1,57 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TenantJobPayload } from "@/interfaces/Tenant";
|
||||
|
||||
export interface RevertRecognizedTransactionsCriteria {
|
||||
batch?: string;
|
||||
accountId?: number;
|
||||
@@ -7,3 +9,14 @@ export interface RecognizeTransactionsCriteria {
|
||||
batch?: string;
|
||||
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;
|
||||
}
|
||||
@@ -2,21 +2,47 @@ import { isEqual, omit } from 'lodash';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { IBankRuleEventCreatedPayload, IBankRuleEventDeletedPayload, IBankRuleEventEditedPayload } from '@/modules/BankRules/types';
|
||||
import {
|
||||
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()
|
||||
export class TriggerRecognizedTransactionsSubscriber {
|
||||
constructor(
|
||||
private readonly tenancyContect: TenancyContext,
|
||||
|
||||
@InjectQueue(RecognizeUncategorizedTransactionsQueue)
|
||||
private readonly recognizeTransactionsQueue: Queue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Triggers the recognize uncategorized transactions job on rule created.
|
||||
* @param {IBankRuleEventCreatedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.bankRules.onCreated)
|
||||
private async recognizedTransactionsOnRuleCreated({
|
||||
async recognizedTransactionsOnRuleCreated({
|
||||
bankRule,
|
||||
}: IBankRuleEventCreatedPayload) {
|
||||
const payload = { ruleId: bankRule.id };
|
||||
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
|
||||
const payload = {
|
||||
ruleId: bankRule.id,
|
||||
...tenantPayload,
|
||||
} as RecognizeUncategorizedTransactionsJobPayload;
|
||||
|
||||
// await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
await this.recognizeTransactionsQueue.add(
|
||||
RecognizeUncategorizedTransactionsJob,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,27 +50,33 @@ export class TriggerRecognizedTransactionsSubscriber {
|
||||
* @param {IBankRuleEventEditedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.bankRules.onEdited)
|
||||
private async recognizedTransactionsOnRuleEdited({
|
||||
async recognizedTransactionsOnRuleEdited({
|
||||
editRuleDTO,
|
||||
oldBankRule,
|
||||
bankRule,
|
||||
}: IBankRuleEventEditedPayload) {
|
||||
const payload = { ruleId: bankRule.id };
|
||||
|
||||
|
||||
// Cannot continue if the new and old bank rule values are the same,
|
||||
// after excluding `createdAt` and `updatedAt` dates.
|
||||
if (
|
||||
isEqual(
|
||||
omit(bankRule, ['createdAt', 'updatedAt']),
|
||||
omit(oldBankRule, ['createdAt', 'updatedAt'])
|
||||
omit(oldBankRule, ['createdAt', 'updatedAt']),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// await this.agenda.now(
|
||||
// 'rerecognize-uncategorized-transactions-job',
|
||||
// payload
|
||||
// );
|
||||
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
|
||||
const payload = {
|
||||
ruleId: bankRule.id,
|
||||
...tenantPayload,
|
||||
} as RecognizeUncategorizedTransactionsJobPayload;
|
||||
|
||||
// Re-recognize the transactions based on the new rules.
|
||||
await this.recognizeTransactionsQueue.add(
|
||||
RecognizeUncategorizedTransactionsJob,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,15 +84,20 @@ export class TriggerRecognizedTransactionsSubscriber {
|
||||
* @param {IBankRuleEventDeletedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.bankRules.onDeleted)
|
||||
private async recognizedTransactionsOnRuleDeleted({
|
||||
async recognizedTransactionsOnRuleDeleted({
|
||||
ruleId,
|
||||
}: IBankRuleEventDeletedPayload) {
|
||||
const payload = { ruleId };
|
||||
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
|
||||
const payload = {
|
||||
ruleId,
|
||||
...tenantPayload,
|
||||
} as RecognizeUncategorizedTransactionsJobPayload;
|
||||
|
||||
// await this.agenda.now(
|
||||
// 'revert-recognized-uncategorized-transactions-job',
|
||||
// payload
|
||||
// );
|
||||
// Re-recognize the transactions based on the new rules.
|
||||
await this.recognizeTransactionsQueue.add(
|
||||
RecognizeUncategorizedTransactionsJob,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +105,7 @@ export class TriggerRecognizedTransactionsSubscriber {
|
||||
* @param {IImportFileCommitedEventPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.import.onImportCommitted)
|
||||
private async triggerRecognizeTransactionsOnImportCommitted({
|
||||
async triggerRecognizeTransactionsOnImportCommitted({
|
||||
importId,
|
||||
|
||||
// @ts-ignore
|
||||
@@ -76,10 +113,8 @@ export class TriggerRecognizedTransactionsSubscriber {
|
||||
// const importFile = await Import.query().findOne({ importId });
|
||||
// const batch = importFile.paramsParsed.batch;
|
||||
// const payload = { transactionsCriteria: { batch } };
|
||||
|
||||
// // Cannot continue if the imported resource is not bank account transactions.
|
||||
// if (importFile.resource !== 'UncategorizedCashflowTransaction') return;
|
||||
|
||||
// await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,48 @@
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
|
||||
import { Job } from 'bullmq';
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
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';
|
||||
|
||||
// @Service()
|
||||
// export class RegonizeTransactionsJob {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define(
|
||||
// 'recognize-uncategorized-transactions-job',
|
||||
// { priority: 'high', concurrency: 2 },
|
||||
// this.handler
|
||||
// );
|
||||
// }
|
||||
@Processor({
|
||||
name: RecognizeUncategorizedTransactionsQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class RegonizeTransactionsPrcessor extends WorkerHost {
|
||||
/**
|
||||
* @param {RecognizeTranasctionsService} recognizeTranasctionsService -
|
||||
* @param {ClsService} clsService -
|
||||
*/
|
||||
constructor(
|
||||
private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
|
||||
private readonly clsService: ClsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Triggers sending invoice mail.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
|
||||
// const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
@Process(RecognizeUncategorizedTransactionsQueue)
|
||||
@UseCls()
|
||||
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
|
||||
const { ruleId, transactionsCriteria } = job.data;
|
||||
|
||||
// try {
|
||||
// await regonizeTransactions.recognizeTransactions(
|
||||
// tenantId,
|
||||
// ruleId,
|
||||
// transactionsCriteria
|
||||
// );
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
this.clsService.set('organizationId', job.data.organizationId);
|
||||
this.clsService.set('userId', job.data.userId);
|
||||
|
||||
try {
|
||||
await this.recognizeTranasctionsService.recognizeTransactions(
|
||||
ruleId,
|
||||
transactionsCriteria,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
|
||||
import { UncategorizedBankTransaction } from '../../BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
@@ -1,54 +0,0 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,19 @@ import { CommandBankTransactionValidator } from './commands/CommandCasflowValida
|
||||
import { BranchTransactionDTOTransformer } from '../Branches/integrations/BranchTransactionDTOTransform';
|
||||
import { BranchesModule } from '../Branches/Branches.module';
|
||||
import { RemovePendingUncategorizedTransaction } from './commands/RemovePendingUncategorizedTransaction.service';
|
||||
import { BankingTransactionsController } from './BankingTransactions.controller';
|
||||
import { BankingTransactionsController } from './controllers/BankingTransactions.controller';
|
||||
import { GetBankAccountsService } from './queries/GetBankAccounts.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { BankAccount } from './models/BankAccount';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { GetBankAccountTransactionsService } from './queries/GetBankAccountTransactions/GetBankAccountTransactions.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 = [
|
||||
RegisterTenancyModel(UncategorizedBankTransaction),
|
||||
@@ -42,7 +48,11 @@ const models = [
|
||||
DynamicListModule,
|
||||
...models,
|
||||
],
|
||||
controllers: [BankingTransactionsController],
|
||||
controllers: [
|
||||
BankingTransactionsController,
|
||||
BankingUncategorizedTransactionsController,
|
||||
BankingPendingTransactionsController,
|
||||
],
|
||||
providers: [
|
||||
BankTransactionAutoIncrement,
|
||||
BankTransactionGLEntriesService,
|
||||
@@ -61,7 +71,16 @@ const models = [
|
||||
RemovePendingUncategorizedTransaction,
|
||||
GetBankAccountTransactionsRepository,
|
||||
GetBankAccountTransactionsService,
|
||||
GetUncategorizedTransactions,
|
||||
GetUncategorizedBankTransactionService,
|
||||
GetPendingBankAccountTransactions,
|
||||
GetAutofillCategorizeTransactionService,
|
||||
],
|
||||
exports: [
|
||||
...models,
|
||||
RemovePendingUncategorizedTransaction,
|
||||
CommandBankTransactionValidator,
|
||||
CreateBankTransactionService
|
||||
],
|
||||
exports: [...models, RemovePendingUncategorizedTransaction],
|
||||
})
|
||||
export class BankingTransactionsModule {}
|
||||
|
||||
@@ -9,6 +9,13 @@ import {
|
||||
import { GetBankAccountsService } from './queries/GetBankAccounts.service';
|
||||
import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto';
|
||||
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()
|
||||
export class BankingTransactionsApplication {
|
||||
@@ -18,6 +25,10 @@ export class BankingTransactionsApplication {
|
||||
private readonly getCashflowTransactionService: GetBankTransactionService,
|
||||
private readonly getBankAccountsService: GetBankAccountsService,
|
||||
private readonly getBankAccountTransactionsService: GetBankAccountTransactionsService,
|
||||
private readonly getBankAccountUncategorizedTransitionsService: GetUncategorizedTransactions,
|
||||
private readonly getBankAccountUncategorizedTransactionService: GetUncategorizedBankTransactionService,
|
||||
private readonly getPendingBankAccountTransactionsService: GetPendingBankAccountTransactions,
|
||||
private readonly getAutofillCategorizeTransactionService: GetAutofillCategorizeTransactionService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -44,7 +55,7 @@ export class BankingTransactionsApplication {
|
||||
* Retrieves the bank transactions of the given bank id.
|
||||
* @param {ICashflowAccountTransactionsQuery} query
|
||||
*/
|
||||
public getBankAccountTransactions(query: ICashflowAccountTransactionsQuery) {
|
||||
public getBankAccountTransactions(query: GetBankTransactionsQueryDto) {
|
||||
return this.getBankAccountTransactionsService.bankAccountTransactions(
|
||||
query,
|
||||
);
|
||||
@@ -68,4 +79,53 @@ export class BankingTransactionsApplication {
|
||||
public getBankAccounts(filterDTO: IBankAccountsFilter) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { includes, camelCase, upperFirst, sumBy } from 'lodash';
|
||||
import { getCashflowTransactionType } from '../utils';
|
||||
import {
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
ERRORS,
|
||||
} from '../constants';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { BankTransaction } from '../models/BankTransaction';
|
||||
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { pick } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import * as R from 'ramda';
|
||||
import * as composeAsync from 'async/compose';
|
||||
import { CASHFLOW_TRANSACTION_TYPE } from '../constants';
|
||||
import { transformCashflowTransactionType } from '../utils';
|
||||
@@ -14,12 +13,12 @@ import { events } from '@/common/events/events';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { BankTransaction } from '../models/BankTransaction';
|
||||
import {
|
||||
ICashflowNewCommandDTO,
|
||||
ICommandCashflowCreatedPayload,
|
||||
ICommandCashflowCreatingPayload,
|
||||
} from '../types/BankingTransactions.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { CreateBankTransactionDto } from '../dtos/CreateBankTransaction.dto';
|
||||
import { formatDateFields } from '@/utils/format-date-fields';
|
||||
|
||||
@Injectable()
|
||||
export class CreateBankTransactionService {
|
||||
@@ -42,7 +41,7 @@ export class CreateBankTransactionService {
|
||||
* @param {ICashflowNewCommandDTO} newCashflowTransactionDTO
|
||||
*/
|
||||
public authorize = async (
|
||||
newCashflowTransactionDTO: ICashflowNewCommandDTO,
|
||||
newCashflowTransactionDTO: CreateBankTransactionDto,
|
||||
creditAccount: Account,
|
||||
) => {
|
||||
const transactionType = transformCashflowTransactionType(
|
||||
@@ -60,7 +59,7 @@ export class CreateBankTransactionService {
|
||||
|
||||
/**
|
||||
* Transformes owner contribution DTO to cashflow transaction.
|
||||
* @param {ICashflowNewCommandDTO} newCashflowTransactionDTO - New transaction DTO.
|
||||
* @param {CreateBankTransactionDto} newCashflowTransactionDTO - New transaction DTO.
|
||||
* @returns {ICashflowTransactionInput} - Cashflow transaction object.
|
||||
*/
|
||||
private transformCashflowTransactionDTO = async (
|
||||
@@ -91,7 +90,7 @@ export class CreateBankTransactionService {
|
||||
|
||||
const initialDTO = {
|
||||
amount,
|
||||
...fromDTO,
|
||||
...formatDateFields(fromDTO, ['date']),
|
||||
transactionNumber,
|
||||
currencyCode: cashflowAccount.currencyCode,
|
||||
exchangeRate: fromDTO?.exchangeRate || 1,
|
||||
@@ -117,7 +116,7 @@ export class CreateBankTransactionService {
|
||||
* @returns {Promise<ICashflowTransaction>}
|
||||
*/
|
||||
public newCashflowTransaction = async (
|
||||
newTransactionDTO: ICashflowNewCommandDTO,
|
||||
newTransactionDTO: CreateBankTransactionDto,
|
||||
userId?: number,
|
||||
): Promise<BankTransaction> => {
|
||||
// Retrieves the cashflow account or throw not found error.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,153 @@
|
||||
import { ToNumber } from '@/common/decorators/Validators';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsDateString,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateBankTransactionDto {
|
||||
@IsDate()
|
||||
@ApiProperty({
|
||||
description: 'The date of the bank transaction',
|
||||
type: Date,
|
||||
example: '2023-01-01T00:00:00.000Z',
|
||||
})
|
||||
@IsDateString()
|
||||
@IsNotEmpty()
|
||||
date: Date;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Optional transaction number or reference',
|
||||
type: String,
|
||||
example: 'TRX-001',
|
||||
})
|
||||
@IsString()
|
||||
transactionNumber: string;
|
||||
@IsOptional()
|
||||
transactionNumber?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Optional external reference number',
|
||||
type: String,
|
||||
example: 'REF-001',
|
||||
})
|
||||
@IsString()
|
||||
referenceNo: string;
|
||||
@IsOptional()
|
||||
referenceNo?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Type of bank transaction (e.g., deposit, withdrawal)',
|
||||
type: String,
|
||||
example: 'deposit',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
transactionType: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Description of the bank transaction',
|
||||
type: String,
|
||||
example: 'Monthly rent payment',
|
||||
})
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Transaction amount',
|
||||
type: Number,
|
||||
example: 1000.5,
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@ToNumber()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Exchange rate for currency conversion',
|
||||
type: Number,
|
||||
default: 1,
|
||||
example: 1.15,
|
||||
})
|
||||
@ToNumber()
|
||||
@IsNumber()
|
||||
exchangeRate: number;
|
||||
exchangeRate: number = 1;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Currency code for the transaction',
|
||||
type: String,
|
||||
example: 'USD',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
currencyCode: string;
|
||||
|
||||
@IsNumber()
|
||||
@ApiProperty({
|
||||
description: 'ID of the credit account associated with this transaction',
|
||||
type: Number,
|
||||
example: 1001,
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
creditAccountId: number;
|
||||
|
||||
@IsNumber()
|
||||
@ApiProperty({
|
||||
description: 'ID of the cashflow account associated with this transaction',
|
||||
type: Number,
|
||||
example: 2001,
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
cashflowAccountId: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the transaction should be published',
|
||||
type: Boolean,
|
||||
default: true,
|
||||
})
|
||||
@IsBoolean()
|
||||
publish: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
publish: boolean = true;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'ID of the branch where the transaction occurred',
|
||||
type: Number,
|
||||
example: 101,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
branchId?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Plaid transaction ID if imported from Plaid',
|
||||
type: String,
|
||||
example: 'plaid_trx_12345',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
plaidTransactionId?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Plaid account ID if imported from Plaid',
|
||||
type: String,
|
||||
example: 'plaid_acc_67890',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
plaidAccountId?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'ID of the uncategorized transaction if this is categorizing an existing transaction',
|
||||
type: Number,
|
||||
example: 5001,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@IsInt()
|
||||
uncategorizedTransactionId?: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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';
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable global-require */
|
||||
import * as moment from 'moment';
|
||||
import { Model } from 'objection';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
export class UncategorizedBankTransaction extends BaseModel {
|
||||
export class UncategorizedBankTransaction extends TenantBaseModel {
|
||||
readonly amount!: number;
|
||||
readonly date!: Date | string;
|
||||
readonly categorized!: boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { getBankAccountTransactionsDefaultQuery } from './_utils';
|
||||
import { GetBankAccountTransactionsRepository } from './GetBankAccountTransactionsRepo.service';
|
||||
import { GetBankAccountTransactions } from './GetBankAccountTransactions';
|
||||
import { ICashflowAccountTransactionsQuery } from '../../types/BankingTransactions.types';
|
||||
import { GetBankTransactionsQueryDto } from '../../dtos/GetBankTranasctionsQuery.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GetBankAccountTransactionsService {
|
||||
@@ -16,7 +16,7 @@ export class GetBankAccountTransactionsService {
|
||||
* @return {Promise<IInvetoryItemDetailDOO>}
|
||||
*/
|
||||
public async bankAccountTransactions(
|
||||
query: ICashflowAccountTransactionsQuery,
|
||||
query: GetBankTransactionsQueryDto,
|
||||
) {
|
||||
const parsedQuery = {
|
||||
...getBankAccountTransactionsDefaultQuery(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import * as moment from 'moment';
|
||||
import { first, isEmpty } from 'lodash';
|
||||
import {
|
||||
ICashflowAccountTransaction,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Injectable, Scope } from '@nestjs/common';
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { ICashflowAccountTransactionsQuery } from '../../types/BankingTransactions.types';
|
||||
import {
|
||||
groupMatchedBankTransactions,
|
||||
groupUncategorizedTransactions,
|
||||
} 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 })
|
||||
export class GetBankAccountTransactionsRepository {
|
||||
private models: any;
|
||||
public query: ICashflowAccountTransactionsQuery;
|
||||
public transactions: any;
|
||||
public uncategorizedTransactions: any;
|
||||
@@ -17,6 +20,28 @@ export class GetBankAccountTransactionsRepository {
|
||||
public pagination: 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) {
|
||||
this.query = query;
|
||||
}
|
||||
@@ -37,9 +62,8 @@ export class GetBankAccountTransactionsRepository {
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
*/
|
||||
async initCashflowAccountTransactions() {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
const { results, pagination } = await AccountTransaction.query()
|
||||
const { results, pagination } = await this.accountTransactionModel()
|
||||
.query()
|
||||
.where('account_id', this.query.accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
@@ -59,10 +83,9 @@ export class GetBankAccountTransactionsRepository {
|
||||
* @return {Promise<number>}
|
||||
*/
|
||||
async initCashflowAccountOpeningBalance(): Promise<void> {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
// Retrieve the opening balance of credit and debit balances.
|
||||
const openingBalancesSubquery = AccountTransaction.query()
|
||||
const openingBalancesSubquery = this.accountTransactionModel()
|
||||
.query()
|
||||
.where('account_id', this.query.accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
@@ -72,7 +95,8 @@ export class GetBankAccountTransactionsRepository {
|
||||
.offset(this.pagination.pageSize * (this.pagination.page - 1));
|
||||
|
||||
// Sumation of credit and debit balance.
|
||||
const openingBalances = await AccountTransaction.query()
|
||||
const openingBalances = await this.accountTransactionModel()
|
||||
.query()
|
||||
.sum('credit as credit')
|
||||
.sum('debit as debit')
|
||||
.from(openingBalancesSubquery.as('T'))
|
||||
@@ -87,14 +111,11 @@ export class GetBankAccountTransactionsRepository {
|
||||
* Initialize the uncategorized transactions of the bank account.
|
||||
*/
|
||||
async initCategorizedTransactions() {
|
||||
const { UncategorizedCashflowTransaction } = this.models;
|
||||
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||
|
||||
const uncategorizedTransactions =
|
||||
await UncategorizedCashflowTransaction.query().whereIn(
|
||||
['categorizeRefType', 'categorizeRefId'],
|
||||
refs,
|
||||
);
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query()
|
||||
.whereIn(['categorizeRefType', 'categorizeRefId'], refs);
|
||||
|
||||
this.uncategorizedTransactions = uncategorizedTransactions;
|
||||
this.uncategorizedTransactionsMapByRef = groupUncategorizedTransactions(
|
||||
@@ -106,14 +127,11 @@ export class GetBankAccountTransactionsRepository {
|
||||
* Initialize the matched bank transactions of the bank account.
|
||||
*/
|
||||
async initMatchedTransactions(): Promise<void> {
|
||||
const { MatchedBankTransaction } = this.models;
|
||||
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||
|
||||
const matchedBankTransactions =
|
||||
await MatchedBankTransaction.query().whereIn(
|
||||
['referenceType', 'referenceId'],
|
||||
refs,
|
||||
);
|
||||
const matchedBankTransactions = await this.matchedBankTransactionModel()
|
||||
.query()
|
||||
.whereIn(['referenceType', 'referenceId'], refs);
|
||||
this.matchedBankTransactions = matchedBankTransactions;
|
||||
this.matchedBankTransactionsMapByRef = groupMatchedBankTransactions(
|
||||
matchedBankTransactions,
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { GetPendingBankAccountTransactionTransformer } from './GetPendingBankAccountTransactionTransformer';
|
||||
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { GetPendingTransactionsQueryDto } from '../dtos/GetPendingTransactionsQuery.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GetPendingBankAccountTransactions {
|
||||
@@ -9,40 +11,37 @@ export class GetPendingBankAccountTransactions {
|
||||
private readonly transformerService: TransformerInjectable,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the given bank accounts pending transaction.
|
||||
* @param {GetPendingTransactionsQuery} filter - Pending transactions query.
|
||||
* @param {GetPendingTransactionsQueryDto} filter - Pending transactions query.
|
||||
*/
|
||||
async getPendingTransactions(filter?: GetPendingTransactionsQuery) {
|
||||
async getPendingTransactions(filter?: GetPendingTransactionsQueryDto) {
|
||||
const _filter = {
|
||||
page: 1,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
...filter,
|
||||
};
|
||||
const { results, pagination } =
|
||||
await this.uncategorizedBankTransactionModel.query()
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query()
|
||||
.onBuild((q) => {
|
||||
q.modify('pending');
|
||||
|
||||
if (_filter?.accountId) {
|
||||
q.where('accountId', _filter.accountId);
|
||||
}
|
||||
})
|
||||
.pagination(_filter.page - 1, _filter.pageSize);
|
||||
if (_filter?.accountId) {
|
||||
q.where('accountId', _filter.accountId);
|
||||
}
|
||||
})
|
||||
.pagination(_filter.page - 1, _filter.pageSize);
|
||||
|
||||
const data = await this.transformerService.transform(
|
||||
results,
|
||||
new GetPendingBankAccountTransactionTransformer()
|
||||
new GetPendingBankAccountTransactionTransformer(),
|
||||
);
|
||||
return { data, pagination };
|
||||
}
|
||||
}
|
||||
|
||||
interface GetPendingTransactionsQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
accountId?: number;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { UncategorizedTransactionTransformer } from '../../BankingCategorize/commands/UncategorizedTransaction.transformer';
|
||||
import { IGetUncategorizedTransactionsQuery } from '../types/BankingTransactions.types';
|
||||
import { GetUncategorizedTransactionsQueryDto } from '../dtos/GetUncategorizedTransactionsQuery.dto';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetUncategorizedTransactions {
|
||||
/**
|
||||
* @param {TransformerInjectable} transformer
|
||||
* @param {UncategorizedBankTransaction.name} uncategorizedBankTransactionModel
|
||||
*/
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the uncategorized cashflow transactions.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} accountId - Account Id.
|
||||
* @param {IGetUncategorizedTransactionsQuery} query - Query.
|
||||
*/
|
||||
public async getTransactions(
|
||||
accountId: number,
|
||||
query: IGetUncategorizedTransactionsQuery
|
||||
query: GetUncategorizedTransactionsQueryDto,
|
||||
) {
|
||||
// Parsed query with default values.
|
||||
const _query = {
|
||||
@@ -28,9 +35,9 @@ export class GetUncategorizedTransactions {
|
||||
pageSize: 20,
|
||||
...query,
|
||||
};
|
||||
|
||||
const { results, pagination } =
|
||||
await this.uncategorizedBankTransactionModel.query()
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query()
|
||||
.onBuild((q) => {
|
||||
q.where('accountId', accountId);
|
||||
q.where('categorized', false);
|
||||
@@ -63,7 +70,7 @@ export class GetUncategorizedTransactions {
|
||||
|
||||
const data = await this.transformer.transform(
|
||||
results,
|
||||
new UncategorizedTransactionTransformer()
|
||||
new UncategorizedTransactionTransformer(),
|
||||
);
|
||||
return {
|
||||
data,
|
||||
|
||||
@@ -3,7 +3,10 @@ import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { BankTransactionAutoIncrement } from '../commands/BankTransactionAutoIncrement.service';
|
||||
import { BankTransactionGLEntriesService } from '../commands/BankTransactionGLEntries';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ICommandCashflowCreatedPayload, ICommandCashflowDeletedPayload } from '../types/BankingTransactions.types';
|
||||
import {
|
||||
ICommandCashflowCreatedPayload,
|
||||
ICommandCashflowDeletedPayload,
|
||||
} from '../types/BankingTransactions.types';
|
||||
|
||||
@Injectable()
|
||||
export class BankingTransactionGLEntriesSubscriber {
|
||||
@@ -56,5 +59,5 @@ export class BankingTransactionGLEntriesSubscriber {
|
||||
cashflowTransactionId,
|
||||
trx,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { events } from '@/common/events/events';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
|
||||
constructor(
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: typeof Account,
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -33,7 +34,7 @@ export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
|
||||
if (uncategorizedTransaction.isPending) {
|
||||
return;
|
||||
}
|
||||
await this.accountModel
|
||||
await this.accountModel()
|
||||
.query(trx)
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
@@ -58,7 +59,7 @@ export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
|
||||
if (uncategorizedTransaction.isPending) {
|
||||
return;
|
||||
}
|
||||
await this.accountModel
|
||||
await this.accountModel()
|
||||
.query(trx)
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
@@ -80,7 +81,7 @@ export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
|
||||
// Cannot continue if the transaction is still pending.
|
||||
if (uncategorizedTransaction.isPending) return;
|
||||
|
||||
await this.accountModel
|
||||
await this.accountModel()
|
||||
.query(trx)
|
||||
.findById(uncategorizedTransaction.accountId)
|
||||
.increment('uncategorizedTransactions', 1);
|
||||
|
||||
@@ -5,19 +5,34 @@ import {
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication';
|
||||
import { ExcludedBankTransactionsQuery } from './types/BankTransactionsExclude.types';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@Controller('banking/transactions')
|
||||
@Controller('banking/exclude')
|
||||
@ApiTags('banking-transactions')
|
||||
export class BankingTransactionsExcludeController {
|
||||
constructor(
|
||||
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()
|
||||
@ApiOperation({ summary: 'Retrieves the excluded bank transactions.' })
|
||||
public getExcludedBankTransactions(
|
||||
@@ -28,7 +43,7 @@ export class BankingTransactionsExcludeController {
|
||||
);
|
||||
}
|
||||
|
||||
@Post(':id/exclude')
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Exclude the given bank transaction.' })
|
||||
public excludeBankTransaction(@Param('id') id: string) {
|
||||
return this.excludeBankTransactionsApplication.excludeBankTransaction(
|
||||
@@ -36,25 +51,11 @@ export class BankingTransactionsExcludeController {
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':id/exclude')
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Unexclude the given bank transaction.' })
|
||||
public unexcludeBankTransaction(@Param('id') id: string) {
|
||||
return this.excludeBankTransactionsApplication.unexcludeBankTransaction(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication';
|
||||
import { ExcludeBankTransactionService } from './commands/ExcludeBankTransaction.service';
|
||||
import { UnexcludeBankTransactionService } from './commands/UnexcludeBankTransaction.service';
|
||||
@@ -10,7 +10,9 @@ import { BankingTransactionsExcludeController } from './BankingTransactionsExclu
|
||||
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
|
||||
|
||||
@Module({
|
||||
imports: [BankingTransactionsModule],
|
||||
imports: [
|
||||
forwardRef(() => BankingTransactionsModule),
|
||||
],
|
||||
providers: [
|
||||
ExcludeBankTransactionsApplication,
|
||||
ExcludeBankTransactionService,
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,32 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
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({
|
||||
providers: [TransactionLandedCostEntriesService],
|
||||
imports: [InventoryCostModule],
|
||||
providers: [
|
||||
AllocateLandedCostService,
|
||||
TransactionLandedCostEntriesService,
|
||||
BillAllocatedLandedCostTransactions,
|
||||
LandedCostGLEntriesSubscriber,
|
||||
LandedCostGLEntries,
|
||||
LandedCostSyncCostTransactions,
|
||||
RevertAllocatedLandedCost,
|
||||
LandedCostInventoryTransactions,
|
||||
LandedCostTranasctions,
|
||||
LandedCostSyncCostTransactionsSubscriber,
|
||||
],
|
||||
exports: [TransactionLandedCostEntriesService],
|
||||
controllers: [BillAllocateLandedCostController],
|
||||
})
|
||||
export class BillLandedCostsModule {}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
import { transformToMap } from '@/utils/transform-to-key';
|
||||
import { ICommonLandedCostEntry, ICommonLandedCostEntryDTO } from './types/BillLandedCosts.types';
|
||||
import {
|
||||
ICommonLandedCostEntry,
|
||||
ICommonLandedCostEntryDTO,
|
||||
} from './types/BillLandedCosts.types';
|
||||
|
||||
const ERRORS = {
|
||||
ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED:
|
||||
@@ -19,7 +22,7 @@ export class TransactionLandedCostEntriesService {
|
||||
*/
|
||||
public getLandedCostEntriesDeleted(
|
||||
oldCommonEntries: ICommonLandedCostEntry[],
|
||||
newCommonEntriesDTO: ICommonLandedCostEntryDTO[]
|
||||
newCommonEntriesDTO: ICommonLandedCostEntryDTO[],
|
||||
): ICommonLandedCostEntry[] {
|
||||
const newBillEntriesById = transformToMap(newCommonEntriesDTO, 'id');
|
||||
|
||||
@@ -40,11 +43,11 @@ export class TransactionLandedCostEntriesService {
|
||||
*/
|
||||
public validateLandedCostEntriesNotDeleted(
|
||||
oldCommonEntries: ICommonLandedCostEntry[],
|
||||
newCommonEntriesDTO: ICommonLandedCostEntryDTO[]
|
||||
newCommonEntriesDTO: ICommonLandedCostEntryDTO[],
|
||||
): void {
|
||||
const entriesDeleted = this.getLandedCostEntriesDeleted(
|
||||
oldCommonEntries,
|
||||
newCommonEntriesDTO
|
||||
newCommonEntriesDTO,
|
||||
);
|
||||
if (entriesDeleted.length > 0) {
|
||||
throw new ServiceError(ERRORS.ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED);
|
||||
@@ -58,7 +61,7 @@ export class TransactionLandedCostEntriesService {
|
||||
*/
|
||||
public validateLocatedCostEntriesSmallerThanNewEntries(
|
||||
oldCommonEntries: ICommonLandedCostEntry[],
|
||||
newCommonEntriesDTO: ICommonLandedCostEntryDTO[]
|
||||
newCommonEntriesDTO: ICommonLandedCostEntryDTO[],
|
||||
): void {
|
||||
const oldBillEntriesById = transformToMap(oldCommonEntries, 'id');
|
||||
|
||||
@@ -67,7 +70,7 @@ export class TransactionLandedCostEntriesService {
|
||||
|
||||
if (oldEntry && oldEntry.allocatedCostAmount > entry.amount) {
|
||||
throw new ServiceError(
|
||||
ERRORS.LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES
|
||||
ERRORS.LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
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;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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,
|
||||
);
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user