refactor: atualizações e remoção de módulos não utilizados
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
export interface ILogger {
|
|
||||||
log(message: string): void;
|
|
||||||
warn(message: string): void;
|
|
||||||
error(message: string, trace?: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
|
||||||
import { ILogger } from './ILogger';
|
|
||||||
|
|
||||||
export class NestLoggerAdapter implements ILogger {
|
|
||||||
private readonly logger: Logger;
|
|
||||||
|
|
||||||
constructor(private readonly context: string) {
|
|
||||||
this.logger = new Logger(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
log(message: string, meta?: Record<string, any>): void {
|
|
||||||
this.logger.log(this.formatMessage(message, meta));
|
|
||||||
}
|
|
||||||
|
|
||||||
warn(message: string, meta?: Record<string, any>): void {
|
|
||||||
this.logger.warn(this.formatMessage(message, meta));
|
|
||||||
}
|
|
||||||
|
|
||||||
error(message: string, trace?: string, meta?: Record<string, any>): void {
|
|
||||||
this.logger.error(this.formatMessage(message, meta), trace);
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatMessage(message: string, meta?: Record<string, any>): string {
|
|
||||||
return meta ? `${message} | ${JSON.stringify(meta)}` : message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { ILogger } from './ILogger';
|
|
||||||
|
|
||||||
export function LogExecution(label?: string) {
|
|
||||||
return function (
|
|
||||||
target: any,
|
|
||||||
propertyKey: string,
|
|
||||||
descriptor: PropertyDescriptor
|
|
||||||
) {
|
|
||||||
const original = descriptor.value;
|
|
||||||
descriptor.value = async function (...args: any[]) {
|
|
||||||
const logger: ILogger = this.logger;
|
|
||||||
const context = label || `${target.constructor.name}.${propertyKey}`;
|
|
||||||
const start = Date.now();
|
|
||||||
logger.log(`Iniciando: ${context} | ${JSON.stringify({ args })}`);
|
|
||||||
const result = await original.apply(this, args);
|
|
||||||
const duration = Date.now() - start;
|
|
||||||
logger.log(`Finalizado: ${context} em ${duration}ms`);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
return descriptor;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { NestLoggerAdapter } from './NestLoggerAdapter';
|
|
||||||
import { ILogger } from './ILogger';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: 'LoggerService',
|
|
||||||
useFactory: () => new NestLoggerAdapter('DataConsultService'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exports: ['LoggerService'],
|
|
||||||
})
|
|
||||||
export class LoggerModule {}
|
|
||||||
@@ -8,32 +8,23 @@ import { OrdersPaymentModule } from './orders-payment/orders-payment.module';
|
|||||||
import { AuthModule } from './auth/auth/auth.module';
|
import { AuthModule } from './auth/auth/auth.module';
|
||||||
import { DataConsultModule } from './data-consult/data-consult.module';
|
import { DataConsultModule } from './data-consult/data-consult.module';
|
||||||
import { OrdersModule } from './orders/modules/orders.module';
|
import { OrdersModule } from './orders/modules/orders.module';
|
||||||
import { OcorrencesController } from './crm/occurrences/ocorrences.controller';
|
|
||||||
import { OccurrencesModule } from './crm/occurrences/occurrences.module';
|
|
||||||
import { ReasonTableModule } from './crm/reason-table/reason-table.module';
|
|
||||||
import { NegotiationsModule } from './crm/negotiations/negotiations.module';
|
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { DebModule } from './orders/modules/deb.module';
|
import { DebModule } from './orders/modules/deb.module';
|
||||||
import { LogisticController } from './logistic/logistic.controller';
|
import { LogisticController } from './logistic/logistic.controller';
|
||||||
import { LogisticService } from './logistic/logistic.service';
|
import { LogisticService } from './logistic/logistic.service';
|
||||||
import { LoggerModule } from './Log/logger.module';
|
|
||||||
import jwtConfig from './auth/jwt.config';
|
import jwtConfig from './auth/jwt.config';
|
||||||
import { UsersModule } from './auth/users/users.module';
|
import { UsersModule } from './auth/users/users.module';
|
||||||
import { ProductsModule } from './products/products.module';
|
import { ProductsModule } from './products/products.module';
|
||||||
import { ThrottlerModule, ThrottlerModuleOptions } from '@nestjs/throttler';
|
import { ThrottlerModule, ThrottlerModuleOptions } from '@nestjs/throttler';
|
||||||
import { RateLimiterMiddleware } from './common/middlewares/rate-limiter.middleware';
|
import { RateLimiterMiddleware } from './common/middlewares/rate-limiter.middleware';
|
||||||
import { RequestSanitizerMiddleware } from './common/middlewares/request-sanitizer.middleware';
|
import { RequestSanitizerMiddleware } from './common/middlewares/request-sanitizer.middleware';
|
||||||
import { HealthModule } from './health/health.module';
|
|
||||||
import { clientes } from './data-consult/clientes.module';
|
import { clientes } from './data-consult/clientes.module';
|
||||||
import { PartnersModule } from './partners/partners.module';
|
import { PartnersModule } from './partners/partners.module';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
UsersModule,
|
UsersModule,
|
||||||
ConfigModule.forRoot({ isGlobal: true,
|
ConfigModule.forRoot({ isGlobal: true, load: [jwtConfig] }),
|
||||||
load: [jwtConfig]
|
|
||||||
}),
|
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
name: 'oracle',
|
name: 'oracle',
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
@@ -62,28 +53,19 @@ import { PartnersModule } from './partners/partners.module';
|
|||||||
OrdersModule,
|
OrdersModule,
|
||||||
clientes,
|
clientes,
|
||||||
ProductsModule,
|
ProductsModule,
|
||||||
NegotiationsModule,
|
|
||||||
OccurrencesModule,
|
|
||||||
ReasonTableModule,
|
|
||||||
LoggerModule,
|
|
||||||
DataConsultModule,
|
DataConsultModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
DebModule,
|
DebModule,
|
||||||
OrdersModule,
|
OrdersModule,
|
||||||
HealthModule,
|
|
||||||
PartnersModule,
|
PartnersModule,
|
||||||
],
|
],
|
||||||
controllers: [OcorrencesController, LogisticController ],
|
controllers: [LogisticController],
|
||||||
providers: [ LogisticService,],
|
providers: [LogisticService],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
configure(consumer: MiddlewareConsumer) {
|
configure(consumer: MiddlewareConsumer) {
|
||||||
consumer
|
consumer.apply(RequestSanitizerMiddleware).forRoutes('*');
|
||||||
.apply(RequestSanitizerMiddleware)
|
|
||||||
.forRoutes('*');
|
|
||||||
|
|
||||||
consumer
|
consumer.apply(RateLimiterMiddleware).forRoutes('auth', 'users');
|
||||||
.apply(RateLimiterMiddleware)
|
|
||||||
.forRoutes('auth', 'users');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ export interface AuthServiceTestContext {
|
|||||||
mockUserRepository: ReturnType<typeof createMockUserRepository>;
|
mockUserRepository: ReturnType<typeof createMockUserRepository>;
|
||||||
mockTokenBlacklistService: ReturnType<typeof createMockTokenBlacklistService>;
|
mockTokenBlacklistService: ReturnType<typeof createMockTokenBlacklistService>;
|
||||||
mockRefreshTokenService: ReturnType<typeof createMockRefreshTokenService>;
|
mockRefreshTokenService: ReturnType<typeof createMockRefreshTokenService>;
|
||||||
mockSessionManagementService: ReturnType<typeof createMockSessionManagementService>;
|
mockSessionManagementService: ReturnType<
|
||||||
|
typeof createMockSessionManagementService
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAuthServiceTestModule(): Promise<AuthServiceTestContext> {
|
export async function createAuthServiceTestModule(): Promise<AuthServiceTestContext> {
|
||||||
@@ -101,4 +103,3 @@ export async function createAuthServiceTestModule(): Promise<AuthServiceTestCont
|
|||||||
mockSessionManagementService,
|
mockSessionManagementService,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ describe('AuthService - createToken', () => {
|
|||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
storeId,
|
storeId,
|
||||||
sessionId
|
sessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
||||||
@@ -41,7 +41,7 @@ describe('AuthService - createToken', () => {
|
|||||||
email: email,
|
email: email,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
},
|
},
|
||||||
{ expiresIn: '8h' }
|
{ expiresIn: '8h' },
|
||||||
);
|
);
|
||||||
expect(result).toBe(mockToken);
|
expect(result).toBe(mockToken);
|
||||||
});
|
});
|
||||||
@@ -61,7 +61,7 @@ describe('AuthService - createToken', () => {
|
|||||||
sellerId,
|
sellerId,
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
storeId
|
storeId,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
||||||
@@ -73,7 +73,7 @@ describe('AuthService - createToken', () => {
|
|||||||
email: email,
|
email: email,
|
||||||
sessionId: undefined,
|
sessionId: undefined,
|
||||||
},
|
},
|
||||||
{ expiresIn: '8h' }
|
{ expiresIn: '8h' },
|
||||||
);
|
);
|
||||||
expect(result).toBe(mockToken);
|
expect(result).toBe(mockToken);
|
||||||
});
|
});
|
||||||
@@ -93,12 +93,12 @@ describe('AuthService - createToken', () => {
|
|||||||
sellerId,
|
sellerId,
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
storeId
|
storeId,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
{ expiresIn: '8h' }
|
{ expiresIn: '8h' },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ describe('AuthService - createToken', () => {
|
|||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
storeId,
|
storeId,
|
||||||
sessionId
|
sessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const signCall = context.mockJwtService.sign.mock.calls[0];
|
const signCall = context.mockJwtService.sign.mock.calls[0];
|
||||||
@@ -150,7 +150,7 @@ describe('AuthService - createToken', () => {
|
|||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
storeId,
|
storeId,
|
||||||
sessionId
|
sessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
||||||
@@ -162,7 +162,7 @@ describe('AuthService - createToken', () => {
|
|||||||
email: email,
|
email: email,
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
},
|
},
|
||||||
{ expiresIn: '8h' }
|
{ expiresIn: '8h' },
|
||||||
);
|
);
|
||||||
expect(result).toBe(mockToken);
|
expect(result).toBe(mockToken);
|
||||||
});
|
});
|
||||||
@@ -171,7 +171,13 @@ describe('AuthService - createToken', () => {
|
|||||||
const mockToken = 'mock.jwt.token.once';
|
const mockToken = 'mock.jwt.token.once';
|
||||||
context.mockJwtService.sign.mockReturnValue(mockToken);
|
context.mockJwtService.sign.mockReturnValue(mockToken);
|
||||||
|
|
||||||
await context.service.createToken(1, 100, 'test', 'test@test.com', 'STORE001');
|
await context.service.createToken(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.sign).toHaveBeenCalledTimes(1);
|
expect(context.mockJwtService.sign).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -199,7 +205,13 @@ describe('AuthService - createToken', () => {
|
|||||||
const negativeId = -1;
|
const negativeId = -1;
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(negativeId, 100, 'test', 'test@test.com', 'STORE001')
|
context.service.createToken(
|
||||||
|
negativeId,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('ID de usuário inválido');
|
).rejects.toThrow('ID de usuário inválido');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -207,7 +219,13 @@ describe('AuthService - createToken', () => {
|
|||||||
const zeroId = 0;
|
const zeroId = 0;
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(zeroId, 100, 'test', 'test@test.com', 'STORE001')
|
context.service.createToken(
|
||||||
|
zeroId,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('ID de usuário inválido');
|
).rejects.toThrow('ID de usuário inválido');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,7 +233,13 @@ describe('AuthService - createToken', () => {
|
|||||||
const negativeSellerId = -1;
|
const negativeSellerId = -1;
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, negativeSellerId, 'test', 'test@test.com', 'STORE001')
|
context.service.createToken(
|
||||||
|
1,
|
||||||
|
negativeSellerId,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('ID de vendedor inválido');
|
).rejects.toThrow('ID de vendedor inválido');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,7 +247,13 @@ describe('AuthService - createToken', () => {
|
|||||||
const emptyUsername = '';
|
const emptyUsername = '';
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, emptyUsername, 'test@test.com', 'STORE001')
|
context.service.createToken(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
emptyUsername,
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Nome de usuário não pode estar vazio');
|
).rejects.toThrow('Nome de usuário não pode estar vazio');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,7 +261,13 @@ describe('AuthService - createToken', () => {
|
|||||||
const whitespaceUsername = ' ';
|
const whitespaceUsername = ' ';
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, whitespaceUsername, 'test@test.com', 'STORE001')
|
context.service.createToken(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
whitespaceUsername,
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Nome de usuário não pode estar vazio');
|
).rejects.toThrow('Nome de usuário não pode estar vazio');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,7 +275,7 @@ describe('AuthService - createToken', () => {
|
|||||||
const emptyEmail = '';
|
const emptyEmail = '';
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, 'test', emptyEmail, 'STORE001')
|
context.service.createToken(1, 100, 'test', emptyEmail, 'STORE001'),
|
||||||
).rejects.toThrow('Email não pode estar vazio');
|
).rejects.toThrow('Email não pode estar vazio');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -247,7 +283,7 @@ describe('AuthService - createToken', () => {
|
|||||||
const invalidEmail = 'not-an-email';
|
const invalidEmail = 'not-an-email';
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001')
|
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001'),
|
||||||
).rejects.toThrow('Formato de email inválido');
|
).rejects.toThrow('Formato de email inválido');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -255,7 +291,7 @@ describe('AuthService - createToken', () => {
|
|||||||
const invalidEmail = 'testemail.com';
|
const invalidEmail = 'testemail.com';
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001')
|
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001'),
|
||||||
).rejects.toThrow('Formato de email inválido');
|
).rejects.toThrow('Formato de email inválido');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,19 +299,37 @@ describe('AuthService - createToken', () => {
|
|||||||
const emptyStoreId = '';
|
const emptyStoreId = '';
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, 'test', 'test@test.com', emptyStoreId)
|
context.service.createToken(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
emptyStoreId,
|
||||||
|
),
|
||||||
).rejects.toThrow('ID da loja não pode estar vazio');
|
).rejects.toThrow('ID da loja não pode estar vazio');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject null username', async () => {
|
it('should reject null username', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, null as any, 'test@test.com', 'STORE001')
|
context.service.createToken(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
null as any,
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Nome de usuário não pode estar vazio');
|
).rejects.toThrow('Nome de usuário não pode estar vazio');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject undefined email', async () => {
|
it('should reject undefined email', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, 'test', undefined as any, 'STORE001')
|
context.service.createToken(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
undefined as any,
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Email não pode estar vazio');
|
).rejects.toThrow('Email não pode estar vazio');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,7 +337,13 @@ describe('AuthService - createToken', () => {
|
|||||||
const specialCharsOnly = '@#$%';
|
const specialCharsOnly = '@#$%';
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, specialCharsOnly, 'test@test.com', 'STORE001')
|
context.service.createToken(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
specialCharsOnly,
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Nome de usuário inválido');
|
).rejects.toThrow('Nome de usuário inválido');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,7 +351,13 @@ describe('AuthService - createToken', () => {
|
|||||||
const longUsername = 'a'.repeat(10000);
|
const longUsername = 'a'.repeat(10000);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, longUsername, 'test@test.com', 'STORE001')
|
context.service.createToken(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
longUsername,
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Nome de usuário muito longo');
|
).rejects.toThrow('Nome de usuário muito longo');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -299,7 +365,7 @@ describe('AuthService - createToken', () => {
|
|||||||
const longEmail = 'a'.repeat(10000) + '@test.com';
|
const longEmail = 'a'.repeat(10000) + '@test.com';
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, 'test', longEmail, 'STORE001')
|
context.service.createToken(1, 100, 'test', longEmail, 'STORE001'),
|
||||||
).rejects.toThrow('Email muito longo');
|
).rejects.toThrow('Email muito longo');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -307,7 +373,13 @@ describe('AuthService - createToken', () => {
|
|||||||
const sqlInjection = "admin'; DROP TABLE users; --";
|
const sqlInjection = "admin'; DROP TABLE users; --";
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, sqlInjection, 'test@test.com', 'STORE001')
|
context.service.createToken(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
sqlInjection,
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Nome de usuário contém caracteres inválidos');
|
).rejects.toThrow('Nome de usuário contém caracteres inválidos');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -315,9 +387,8 @@ describe('AuthService - createToken', () => {
|
|||||||
const invalidEmail = 'test@@example.com';
|
const invalidEmail = 'test@@example.com';
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001')
|
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001'),
|
||||||
).rejects.toThrow('Formato de email inválido');
|
).rejects.toThrow('Formato de email inválido');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
context.mockJwtService.sign.mockReturnValue('mock.access.token');
|
context.mockJwtService.sign.mockReturnValue('mock.access.token');
|
||||||
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue('mock.refresh.token');
|
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
|
||||||
|
'mock.refresh.token',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle error when createToken fails after refresh token is generated', async () => {
|
it('should handle error when createToken fails after refresh token is generated', async () => {
|
||||||
@@ -39,10 +41,19 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-123')
|
context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
'session-123',
|
||||||
|
),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
|
|
||||||
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockRefreshTokenService.generateRefreshToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should rollback access token if refresh token generation fails', async () => {
|
it('should rollback access token if refresh token generation fails', async () => {
|
||||||
@@ -52,11 +63,18 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
* Solução esperada: Invalidar o access token ou garantir atomicidade.
|
* Solução esperada: Invalidar o access token ou garantir atomicidade.
|
||||||
*/
|
*/
|
||||||
context.mockRefreshTokenService.generateRefreshToken.mockRejectedValueOnce(
|
context.mockRefreshTokenService.generateRefreshToken.mockRejectedValueOnce(
|
||||||
new Error('Falha ao gerar refresh token')
|
new Error('Falha ao gerar refresh token'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-123')
|
context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
'session-123',
|
||||||
|
),
|
||||||
).rejects.toThrow('Falha ao gerar refresh token');
|
).rejects.toThrow('Falha ao gerar refresh token');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,7 +87,13 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
context.mockJwtService.sign.mockReturnValue('');
|
context.mockJwtService.sign.mockReturnValue('');
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001')
|
context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Token de acesso inválido gerado');
|
).rejects.toThrow('Token de acesso inválido gerado');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,18 +103,34 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
* Problema: Método não valida o retorno.
|
* Problema: Método não valida o retorno.
|
||||||
* Solução esperada: Lançar exceção se token for inválido.
|
* Solução esperada: Lançar exceção se token for inválido.
|
||||||
*/
|
*/
|
||||||
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue('');
|
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001')
|
context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Refresh token inválido gerado');
|
).rejects.toThrow('Refresh token inválido gerado');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that refresh token is not null', async () => {
|
it('should validate that refresh token is not null', async () => {
|
||||||
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(null);
|
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001')
|
context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('Refresh token inválido gerado');
|
).rejects.toThrow('Refresh token inválido gerado');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,12 +147,20 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
return 'mock.access.token';
|
return 'mock.access.token';
|
||||||
});
|
});
|
||||||
|
|
||||||
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(async () => {
|
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(
|
||||||
callOrder.push('refreshToken');
|
async () => {
|
||||||
return 'mock.refresh.token';
|
callOrder.push('refreshToken');
|
||||||
});
|
return 'mock.refresh.token';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001');
|
await context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
);
|
||||||
|
|
||||||
expect(callOrder).toEqual(['accessToken', 'refreshToken']);
|
expect(callOrder).toEqual(['accessToken', 'refreshToken']);
|
||||||
});
|
});
|
||||||
@@ -123,7 +171,13 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
* Problema: Cliente pode não saber quando renovar o token.
|
* Problema: Cliente pode não saber quando renovar o token.
|
||||||
* Solução esperada: Sempre retornar um número positivo válido.
|
* Solução esperada: Sempre retornar um número positivo válido.
|
||||||
*/
|
*/
|
||||||
const result = await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001');
|
const result = await context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.expiresIn).toBeGreaterThan(0);
|
expect(result.expiresIn).toBeGreaterThan(0);
|
||||||
expect(typeof result.expiresIn).toBe('number');
|
expect(typeof result.expiresIn).toBe('number');
|
||||||
@@ -145,19 +199,42 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
return `mock.access.token.${callCount}`;
|
return `mock.access.token.${callCount}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(async () => {
|
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(
|
||||||
return `mock.refresh.token.${Math.random()}`;
|
async () => {
|
||||||
});
|
return `mock.refresh.token.${Math.random()}`;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const promises = [
|
const promises = [
|
||||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-1'),
|
context.service.createTokenPair(
|
||||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-2'),
|
1,
|
||||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-3'),
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
'session-1',
|
||||||
|
),
|
||||||
|
context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
'session-2',
|
||||||
|
),
|
||||||
|
context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
'session-3',
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
const uniqueTokens = new Set(results.map(r => r.accessToken));
|
const uniqueTokens = new Set(results.map((r) => r.accessToken));
|
||||||
expect(uniqueTokens.size).toBe(3);
|
expect(uniqueTokens.size).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,10 +245,18 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
* Solução esperada: Falhar rápido com mensagem clara.
|
* Solução esperada: Falhar rápido com mensagem clara.
|
||||||
*/
|
*/
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createTokenPair(-1, 100, 'test', 'test@test.com', 'STORE001')
|
context.service.createTokenPair(
|
||||||
|
-1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('ID de usuário inválido');
|
).rejects.toThrow('ID de usuário inválido');
|
||||||
|
|
||||||
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockRefreshTokenService.generateRefreshToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create refresh token if validation fails', async () => {
|
it('should not create refresh token if validation fails', async () => {
|
||||||
@@ -181,11 +266,19 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
* Solução esperada: Validar tudo antes de criar qualquer token.
|
* Solução esperada: Validar tudo antes de criar qualquer token.
|
||||||
*/
|
*/
|
||||||
await expect(
|
await expect(
|
||||||
context.service.createTokenPair(1, -1, 'test', 'test@test.com', 'STORE001')
|
context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
-1,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
),
|
||||||
).rejects.toThrow('ID de vendedor inválido');
|
).rejects.toThrow('ID de vendedor inválido');
|
||||||
|
|
||||||
expect(context.mockJwtService.sign).not.toHaveBeenCalled();
|
expect(context.mockJwtService.sign).not.toHaveBeenCalled();
|
||||||
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockRefreshTokenService.generateRefreshToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle undefined sessionId gracefully', async () => {
|
it('should handle undefined sessionId gracefully', async () => {
|
||||||
@@ -194,11 +287,19 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
* Problema: Pode causar problemas ao gerar tokens sem session.
|
* Problema: Pode causar problemas ao gerar tokens sem session.
|
||||||
* Solução esperada: Aceitar undefined e passar corretamente aos serviços.
|
* Solução esperada: Aceitar undefined e passar corretamente aos serviços.
|
||||||
*/
|
*/
|
||||||
const result = await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001');
|
const result = await context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.accessToken).toBeDefined();
|
expect(result.accessToken).toBeDefined();
|
||||||
expect(result.refreshToken).toBeDefined();
|
expect(result.refreshToken).toBeDefined();
|
||||||
expect(context.mockRefreshTokenService.generateRefreshToken).toHaveBeenCalledWith(1, undefined);
|
expect(
|
||||||
|
context.mockRefreshTokenService.generateRefreshToken,
|
||||||
|
).toHaveBeenCalledWith(1, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include all required fields in return object', async () => {
|
it('should include all required fields in return object', async () => {
|
||||||
@@ -207,7 +308,13 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
* Problema: Pode faltar campos ou ter campos extras.
|
* Problema: Pode faltar campos ou ter campos extras.
|
||||||
* Solução esperada: Sempre retornar accessToken, refreshToken e expiresIn.
|
* Solução esperada: Sempre retornar accessToken, refreshToken e expiresIn.
|
||||||
*/
|
*/
|
||||||
const result = await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001');
|
const result = await context.service.createTokenPair(
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
'test',
|
||||||
|
'test@test.com',
|
||||||
|
'STORE001',
|
||||||
|
);
|
||||||
|
|
||||||
expect(result).toHaveProperty('accessToken');
|
expect(result).toHaveProperty('accessToken');
|
||||||
expect(result).toHaveProperty('refreshToken');
|
expect(result).toHaveProperty('refreshToken');
|
||||||
@@ -216,4 +323,3 @@ describe('AuthService - createTokenPair', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ describe('AuthService - logout', () => {
|
|||||||
storeId: 'STORE001',
|
storeId: 'STORE001',
|
||||||
sessionId: 'session-123',
|
sessionId: 'session-123',
|
||||||
});
|
});
|
||||||
context.mockTokenBlacklistService.addToBlacklist.mockResolvedValue(undefined);
|
context.mockTokenBlacklistService.addToBlacklist.mockResolvedValue(
|
||||||
context.mockSessionManagementService.terminateSession.mockResolvedValue(undefined);
|
undefined,
|
||||||
|
);
|
||||||
|
context.mockSessionManagementService.terminateSession.mockResolvedValue(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -37,66 +41,76 @@ describe('AuthService - logout', () => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
it('should reject empty token', async () => {
|
it('should reject empty token', async () => {
|
||||||
await expect(
|
await expect(context.service.logout('')).rejects.toThrow(
|
||||||
context.service.logout('')
|
'Token não pode estar vazio',
|
||||||
).rejects.toThrow('Token não pode estar vazio');
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject null token', async () => {
|
it('should reject null token', async () => {
|
||||||
await expect(
|
await expect(context.service.logout(null as any)).rejects.toThrow(
|
||||||
context.service.logout(null as any)
|
'Token não pode estar vazio',
|
||||||
).rejects.toThrow('Token não pode estar vazio');
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject undefined token', async () => {
|
it('should reject undefined token', async () => {
|
||||||
await expect(
|
await expect(context.service.logout(undefined as any)).rejects.toThrow(
|
||||||
context.service.logout(undefined as any)
|
'Token não pode estar vazio',
|
||||||
).rejects.toThrow('Token não pode estar vazio');
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject whitespace-only token', async () => {
|
it('should reject whitespace-only token', async () => {
|
||||||
await expect(
|
await expect(context.service.logout(' ')).rejects.toThrow(
|
||||||
context.service.logout(' ')
|
'Token não pode estar vazio',
|
||||||
).rejects.toThrow('Token não pode estar vazio');
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject extremely long tokens (DoS prevention)', async () => {
|
it('should reject extremely long tokens (DoS prevention)', async () => {
|
||||||
const hugeToken = 'a'.repeat(100000);
|
const hugeToken = 'a'.repeat(100000);
|
||||||
|
|
||||||
await expect(
|
await expect(context.service.logout(hugeToken)).rejects.toThrow(
|
||||||
context.service.logout(hugeToken)
|
'Token muito longo',
|
||||||
).rejects.toThrow('Token muito longo');
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate decoded token is not null', async () => {
|
it('should validate decoded token is not null', async () => {
|
||||||
context.mockJwtService.decode.mockReturnValue(null);
|
context.mockJwtService.decode.mockReturnValue(null);
|
||||||
|
|
||||||
await expect(
|
await expect(context.service.logout('invalid.token')).rejects.toThrow(
|
||||||
context.service.logout('invalid.token')
|
'Token inválido ou não pode ser decodificado',
|
||||||
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate decoded token has required fields', async () => {
|
it('should validate decoded token has required fields', async () => {
|
||||||
context.mockJwtService.decode.mockReturnValue({} as any);
|
context.mockJwtService.decode.mockReturnValue({} as any);
|
||||||
|
|
||||||
await expect(
|
await expect(context.service.logout('incomplete.token')).rejects.toThrow(
|
||||||
context.service.logout('incomplete.token')
|
'Token inválido ou não pode ser decodificado',
|
||||||
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not add token to blacklist if already blacklisted', async () => {
|
it('should not add token to blacklist if already blacklisted', async () => {
|
||||||
@@ -104,7 +118,9 @@ describe('AuthService - logout', () => {
|
|||||||
|
|
||||||
await context.service.logout('already.blacklisted.token');
|
await context.service.logout('already.blacklisted.token');
|
||||||
|
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate session exists before terminating', async () => {
|
it('should validate session exists before terminating', async () => {
|
||||||
@@ -114,11 +130,11 @@ describe('AuthService - logout', () => {
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
context.mockSessionManagementService.terminateSession.mockRejectedValue(
|
context.mockSessionManagementService.terminateSession.mockRejectedValue(
|
||||||
new Error('Sessão não encontrada')
|
new Error('Sessão não encontrada'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.logout('token.with.invalid.session')
|
context.service.logout('token.with.invalid.session'),
|
||||||
).rejects.toThrow('Sessão não encontrada');
|
).rejects.toThrow('Sessão não encontrada');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,16 +144,16 @@ describe('AuthService - logout', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.logout('invalid.token.format')
|
context.service.logout('invalid.token.format'),
|
||||||
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sanitize token input', async () => {
|
it('should sanitize token input', async () => {
|
||||||
const maliciousToken = "'; DROP TABLE users; --";
|
const maliciousToken = "'; DROP TABLE users; --";
|
||||||
|
|
||||||
await expect(
|
await expect(context.service.logout(maliciousToken)).rejects.toThrow(
|
||||||
context.service.logout(maliciousToken)
|
'Formato de token inválido',
|
||||||
).rejects.toThrow('Formato de token inválido');
|
);
|
||||||
|
|
||||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -149,7 +165,7 @@ describe('AuthService - logout', () => {
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.logout('token.with.invalid.id')
|
context.service.logout('token.with.invalid.id'),
|
||||||
).rejects.toThrow('ID de usuário inválido no token');
|
).rejects.toThrow('ID de usuário inválido no token');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,7 +177,9 @@ describe('AuthService - logout', () => {
|
|||||||
|
|
||||||
await context.service.logout('token.with.empty.sessionid');
|
await context.service.logout('token.with.empty.sessionid');
|
||||||
|
|
||||||
expect(context.mockSessionManagementService.terminateSession).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockSessionManagementService.terminateSession,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should complete logout even if session termination fails', async () => {
|
it('should complete logout even if session termination fails', async () => {
|
||||||
@@ -172,23 +190,27 @@ describe('AuthService - logout', () => {
|
|||||||
|
|
||||||
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
||||||
context.mockSessionManagementService.terminateSession.mockRejectedValue(
|
context.mockSessionManagementService.terminateSession.mockRejectedValue(
|
||||||
new Error('Falha ao terminar sessão')
|
new Error('Falha ao terminar sessão'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await context.service.logout('valid.token');
|
await context.service.logout('valid.token');
|
||||||
|
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('valid.token');
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).toHaveBeenCalledWith('valid.token');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw if token is already blacklisted', async () => {
|
it('should not throw if token is already blacklisted', async () => {
|
||||||
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true);
|
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true);
|
||||||
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
||||||
new Error('Token já está na blacklist')
|
new Error('Token já está na blacklist'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await context.service.logout('already.blacklisted.token');
|
await context.service.logout('already.blacklisted.token');
|
||||||
|
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate token format before decoding', async () => {
|
it('should validate token format before decoding', async () => {
|
||||||
@@ -214,7 +236,9 @@ describe('AuthService - logout', () => {
|
|||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledTimes(3);
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate decoded payload structure', async () => {
|
it('should validate decoded payload structure', async () => {
|
||||||
@@ -223,11 +247,15 @@ describe('AuthService - logout', () => {
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.logout('token.with.invalid.structure')
|
context.service.logout('token.with.invalid.structure'),
|
||||||
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
||||||
|
|
||||||
expect(context.mockSessionManagementService.terminateSession).not.toHaveBeenCalled();
|
expect(
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
context.mockSessionManagementService.terminateSession,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ensure token is always blacklisted on success', async () => {
|
it('should ensure token is always blacklisted on success', async () => {
|
||||||
@@ -235,8 +263,12 @@ describe('AuthService - logout', () => {
|
|||||||
|
|
||||||
await context.service.logout('valid.token');
|
await context.service.logout('valid.token');
|
||||||
|
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('valid.token');
|
expect(
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledTimes(1);
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).toHaveBeenCalledWith('valid.token');
|
||||||
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle race condition when token becomes blacklisted between check and add', async () => {
|
it('should handle race condition when token becomes blacklisted between check and add', async () => {
|
||||||
@@ -248,13 +280,17 @@ describe('AuthService - logout', () => {
|
|||||||
*/
|
*/
|
||||||
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
||||||
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
||||||
new Error('Token já está na blacklist')
|
new Error('Token já está na blacklist'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await context.service.logout('token.with.race.condition');
|
await context.service.logout('token.with.race.condition');
|
||||||
|
|
||||||
expect(context.mockTokenBlacklistService.isBlacklisted).toHaveBeenCalledWith('token.with.race.condition');
|
expect(
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('token.with.race.condition');
|
context.mockTokenBlacklistService.isBlacklisted,
|
||||||
|
).toHaveBeenCalledWith('token.with.race.condition');
|
||||||
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).toHaveBeenCalledWith('token.with.race.condition');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if addToBlacklist fails with non-blacklist error', async () => {
|
it('should throw error if addToBlacklist fails with non-blacklist error', async () => {
|
||||||
@@ -265,15 +301,21 @@ describe('AuthService - logout', () => {
|
|||||||
*/
|
*/
|
||||||
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
||||||
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
||||||
new Error('Erro de conexão com Redis')
|
new Error('Erro de conexão com Redis'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.logout('token.with.blacklist.error')
|
context.service.logout('token.with.blacklist.error'),
|
||||||
).rejects.toThrow('Falha ao adicionar token à blacklist: Erro de conexão com Redis');
|
).rejects.toThrow(
|
||||||
|
'Falha ao adicionar token à blacklist: Erro de conexão com Redis',
|
||||||
|
);
|
||||||
|
|
||||||
expect(context.mockTokenBlacklistService.isBlacklisted).toHaveBeenCalledWith('token.with.blacklist.error');
|
expect(
|
||||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('token.with.blacklist.error');
|
context.mockTokenBlacklistService.isBlacklisted,
|
||||||
|
).toHaveBeenCalledWith('token.with.blacklist.error');
|
||||||
|
expect(
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist,
|
||||||
|
).toHaveBeenCalledWith('token.with.blacklist.error');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should verify isBlacklisted is called before addToBlacklist', async () => {
|
it('should verify isBlacklisted is called before addToBlacklist', async () => {
|
||||||
@@ -286,11 +328,14 @@ describe('AuthService - logout', () => {
|
|||||||
|
|
||||||
await context.service.logout('valid.token');
|
await context.service.logout('valid.token');
|
||||||
|
|
||||||
const isBlacklistedCallOrder = context.mockTokenBlacklistService.isBlacklisted.mock.invocationCallOrder[0];
|
const isBlacklistedCallOrder =
|
||||||
const addToBlacklistCallOrder = context.mockTokenBlacklistService.addToBlacklist.mock.invocationCallOrder[0];
|
context.mockTokenBlacklistService.isBlacklisted.mock
|
||||||
|
.invocationCallOrder[0];
|
||||||
|
const addToBlacklistCallOrder =
|
||||||
|
context.mockTokenBlacklistService.addToBlacklist.mock
|
||||||
|
.invocationCallOrder[0];
|
||||||
|
|
||||||
expect(isBlacklistedCallOrder).toBeLessThan(addToBlacklistCallOrder);
|
expect(isBlacklistedCallOrder).toBeLessThan(addToBlacklistCallOrder);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
situacao: 'A',
|
situacao: 'A',
|
||||||
dataDesligamento: null,
|
dataDesligamento: null,
|
||||||
});
|
});
|
||||||
context.mockSessionManagementService.isSessionActive.mockResolvedValue(true);
|
context.mockSessionManagementService.isSessionActive.mockResolvedValue(
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -40,35 +42,43 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
it('should reject empty refresh token', async () => {
|
it('should reject empty refresh token', async () => {
|
||||||
await expect(
|
await expect(context.service.refreshAccessToken('')).rejects.toThrow(
|
||||||
context.service.refreshAccessToken('')
|
'Refresh token não pode estar vazio',
|
||||||
).rejects.toThrow('Refresh token não pode estar vazio');
|
);
|
||||||
|
|
||||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockRefreshTokenService.validateRefreshToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject null refresh token', async () => {
|
it('should reject null refresh token', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken(null as any)
|
context.service.refreshAccessToken(null as any),
|
||||||
).rejects.toThrow('Refresh token não pode estar vazio');
|
).rejects.toThrow('Refresh token não pode estar vazio');
|
||||||
|
|
||||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockRefreshTokenService.validateRefreshToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject undefined refresh token', async () => {
|
it('should reject undefined refresh token', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken(undefined as any)
|
context.service.refreshAccessToken(undefined as any),
|
||||||
).rejects.toThrow('Refresh token não pode estar vazio');
|
).rejects.toThrow('Refresh token não pode estar vazio');
|
||||||
|
|
||||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockRefreshTokenService.validateRefreshToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject whitespace-only refresh token', async () => {
|
it('should reject whitespace-only refresh token', async () => {
|
||||||
await expect(
|
await expect(context.service.refreshAccessToken(' ')).rejects.toThrow(
|
||||||
context.service.refreshAccessToken(' ')
|
'Refresh token não pode estar vazio',
|
||||||
).rejects.toThrow('Refresh token não pode estar vazio');
|
);
|
||||||
|
|
||||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockRefreshTokenService.validateRefreshToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate tokenData has required id field', async () => {
|
it('should validate tokenData has required id field', async () => {
|
||||||
@@ -77,15 +87,17 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('Dados do refresh token inválidos');
|
).rejects.toThrow('Dados do refresh token inválidos');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate tokenData is not null', async () => {
|
it('should validate tokenData is not null', async () => {
|
||||||
context.mockRefreshTokenService.validateRefreshToken.mockResolvedValue(null);
|
context.mockRefreshTokenService.validateRefreshToken.mockResolvedValue(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('Dados do refresh token inválidos');
|
).rejects.toThrow('Dados do refresh token inválidos');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,7 +113,7 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('Dados do usuário incompletos');
|
).rejects.toThrow('Dados do usuário incompletos');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,7 +129,7 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('Dados do usuário incompletos');
|
).rejects.toThrow('Dados do usuário incompletos');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,7 +145,7 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('Dados do usuário incompletos');
|
).rejects.toThrow('Dados do usuário incompletos');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,7 +153,7 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
context.mockJwtService.sign.mockReturnValue('');
|
context.mockJwtService.sign.mockReturnValue('');
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('Falha ao gerar novo token de acesso');
|
).rejects.toThrow('Falha ao gerar novo token de acesso');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,7 +161,7 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
context.mockJwtService.sign.mockReturnValue(null as any);
|
context.mockJwtService.sign.mockReturnValue(null as any);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('Falha ao gerar novo token de acesso');
|
).rejects.toThrow('Falha ao gerar novo token de acesso');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,10 +171,12 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
sessionId: 'expired-session',
|
sessionId: 'expired-session',
|
||||||
});
|
});
|
||||||
|
|
||||||
context.mockSessionManagementService.isSessionActive = jest.fn().mockResolvedValue(false);
|
context.mockSessionManagementService.isSessionActive = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(false);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('Sessão não está mais ativa');
|
).rejects.toThrow('Sessão não está mais ativa');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,7 +192,7 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('ID de vendedor inválido');
|
).rejects.toThrow('ID de vendedor inválido');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,24 +200,30 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
const hugeToken = 'a'.repeat(100000);
|
const hugeToken = 'a'.repeat(100000);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken(hugeToken)
|
context.service.refreshAccessToken(hugeToken),
|
||||||
).rejects.toThrow('Refresh token muito longo');
|
).rejects.toThrow('Refresh token muito longo');
|
||||||
|
|
||||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockRefreshTokenService.validateRefreshToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sanitize refresh token input', async () => {
|
it('should sanitize refresh token input', async () => {
|
||||||
const maliciousToken = "'; DROP TABLE users; --";
|
const maliciousToken = "'; DROP TABLE users; --";
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken(maliciousToken)
|
context.service.refreshAccessToken(maliciousToken),
|
||||||
).rejects.toThrow('Formato de refresh token inválido');
|
).rejects.toThrow('Formato de refresh token inválido');
|
||||||
|
|
||||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
expect(
|
||||||
|
context.mockRefreshTokenService.validateRefreshToken,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include only required fields in response', async () => {
|
it('should include only required fields in response', async () => {
|
||||||
const result = await context.service.refreshAccessToken('valid.refresh.token');
|
const result = await context.service.refreshAccessToken(
|
||||||
|
'valid.refresh.token',
|
||||||
|
);
|
||||||
|
|
||||||
expect(result).toHaveProperty('accessToken');
|
expect(result).toHaveProperty('accessToken');
|
||||||
expect(result).toHaveProperty('expiresIn');
|
expect(result).toHaveProperty('expiresIn');
|
||||||
@@ -213,7 +233,9 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should validate expiresIn is correct', async () => {
|
it('should validate expiresIn is correct', async () => {
|
||||||
const result = await context.service.refreshAccessToken('valid.refresh.token');
|
const result = await context.service.refreshAccessToken(
|
||||||
|
'valid.refresh.token',
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.expiresIn).toBe(28800);
|
expect(result.expiresIn).toBe(28800);
|
||||||
expect(result.expiresIn).toBeGreaterThan(0);
|
expect(result.expiresIn).toBeGreaterThan(0);
|
||||||
@@ -231,7 +253,7 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -244,7 +266,7 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
|
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
results.forEach(result => {
|
results.forEach((result) => {
|
||||||
expect(result).toHaveProperty('accessToken');
|
expect(result).toHaveProperty('accessToken');
|
||||||
expect(result.accessToken).toBeTruthy();
|
expect(result.accessToken).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -262,9 +284,8 @@ describe('AuthService - refreshAccessToken', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
context.service.refreshAccessToken('valid.refresh.token')
|
context.service.refreshAccessToken('valid.refresh.token'),
|
||||||
).rejects.toThrow('Usuário inválido ou inativo');
|
).rejects.toThrow('Usuário inválido ou inativo');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,17 @@ import { RateLimitingGuard } from '../guards/rate-limiting.guard';
|
|||||||
import { RateLimitingService } from '../services/rate-limiting.service';
|
import { RateLimitingService } from '../services/rate-limiting.service';
|
||||||
import { RefreshTokenService } from '../services/refresh-token.service';
|
import { RefreshTokenService } from '../services/refresh-token.service';
|
||||||
import { SessionManagementService } from '../services/session-management.service';
|
import { SessionManagementService } from '../services/session-management.service';
|
||||||
import { RefreshTokenDto, RefreshTokenResponseDto } from './dto/refresh-token.dto';
|
import {
|
||||||
|
RefreshTokenDto,
|
||||||
|
RefreshTokenResponseDto,
|
||||||
|
} from './dto/refresh-token.dto';
|
||||||
import { SessionsResponseDto } from './dto/session.dto';
|
import { SessionsResponseDto } from './dto/session.dto';
|
||||||
import { LoginAuditService } from '../services/login-audit.service';
|
import { LoginAuditService } from '../services/login-audit.service';
|
||||||
import {
|
import {
|
||||||
LoginAuditFiltersDto,
|
LoginAuditFiltersDto,
|
||||||
LoginAuditResponseDto,
|
LoginAuditResponseDto,
|
||||||
LoginStatsDto,
|
LoginStatsDto,
|
||||||
LoginStatsFiltersDto
|
LoginStatsFiltersDto,
|
||||||
} from './dto/login-audit.dto';
|
} from './dto/login-audit.dto';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
@@ -66,7 +69,10 @@ export class AuthController {
|
|||||||
})
|
})
|
||||||
@ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' })
|
@ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' })
|
||||||
@ApiTooManyRequestsResponse({ description: 'Muitas tentativas de login' })
|
@ApiTooManyRequestsResponse({ description: 'Muitas tentativas de login' })
|
||||||
async login(@Body() dto: LoginDto, @Request() req): Promise<LoginResponseDto> {
|
async login(
|
||||||
|
@Body() dto: LoginDto,
|
||||||
|
@Request() req,
|
||||||
|
): Promise<LoginResponseDto> {
|
||||||
const ip = this.getClientIp(req);
|
const ip = this.getClientIp(req);
|
||||||
|
|
||||||
const command = new AuthenticateUserCommand(dto.username, dto.password);
|
const command = new AuthenticateUserCommand(dto.username, dto.password);
|
||||||
@@ -98,13 +104,17 @@ export class AuthController {
|
|||||||
/**
|
/**
|
||||||
* Verifica se o usuário já possui uma sessão ativa
|
* Verifica se o usuário já possui uma sessão ativa
|
||||||
*/
|
*/
|
||||||
const existingSession = await this.sessionManagementService.hasActiveSession(user.id);
|
const existingSession =
|
||||||
|
await this.sessionManagementService.hasActiveSession(user.id);
|
||||||
|
|
||||||
if (existingSession) {
|
if (existingSession) {
|
||||||
/**
|
/**
|
||||||
* Encerra a sessão existente antes de criar uma nova
|
* Encerra a sessão existente antes de criar uma nova
|
||||||
*/
|
*/
|
||||||
await this.sessionManagementService.terminateSession(user.id, existingSession.sessionId);
|
await this.sessionManagementService.terminateSession(
|
||||||
|
user.id,
|
||||||
|
existingSession.sessionId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await this.sessionManagementService.createSession(
|
const session = await this.sessionManagementService.createSession(
|
||||||
@@ -161,7 +171,6 @@ export class AuthController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Post('logout')
|
@Post('logout')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -173,7 +182,12 @@ export class AuthController {
|
|||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
new ResultModel(false, 'Token não fornecido', null, 'Token não fornecido'),
|
new ResultModel(
|
||||||
|
false,
|
||||||
|
'Token não fornecido',
|
||||||
|
null,
|
||||||
|
'Token não fornecido',
|
||||||
|
),
|
||||||
HttpStatus.UNAUTHORIZED,
|
HttpStatus.UNAUTHORIZED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,8 +206,12 @@ export class AuthController {
|
|||||||
description: 'Token renovado com sucesso',
|
description: 'Token renovado com sucesso',
|
||||||
type: RefreshTokenResponseDto,
|
type: RefreshTokenResponseDto,
|
||||||
})
|
})
|
||||||
@ApiUnauthorizedResponse({ description: 'Refresh token inválido ou expirado' })
|
@ApiUnauthorizedResponse({
|
||||||
async refreshToken(@Body() dto: RefreshTokenDto): Promise<RefreshTokenResponseDto> {
|
description: 'Refresh token inválido ou expirado',
|
||||||
|
})
|
||||||
|
async refreshToken(
|
||||||
|
@Body() dto: RefreshTokenDto,
|
||||||
|
): Promise<RefreshTokenResponseDto> {
|
||||||
const result = await this.authService.refreshAccessToken(dto.refreshToken);
|
const result = await this.authService.refreshAccessToken(dto.refreshToken);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -210,15 +228,20 @@ export class AuthController {
|
|||||||
async getSessions(@Request() req): Promise<SessionsResponseDto> {
|
async getSessions(@Request() req): Promise<SessionsResponseDto> {
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
const currentSessionId = req.user.sessionId;
|
const currentSessionId = req.user.sessionId;
|
||||||
const sessions = await this.sessionManagementService.getActiveSessions(userId, currentSessionId);
|
const sessions = await this.sessionManagementService.getActiveSessions(
|
||||||
|
userId,
|
||||||
|
currentSessionId,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions: sessions.map(session => ({
|
sessions: sessions.map((session) => ({
|
||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
ipAddress: session.ipAddress,
|
ipAddress: session.ipAddress,
|
||||||
userAgent: session.userAgent,
|
userAgent: session.userAgent,
|
||||||
createdAt: DateUtil.toBrazilISOString(new Date(session.createdAt)),
|
createdAt: DateUtil.toBrazilISOString(new Date(session.createdAt)),
|
||||||
lastActivity: DateUtil.toBrazilISOString(new Date(session.lastActivity)),
|
lastActivity: DateUtil.toBrazilISOString(
|
||||||
|
new Date(session.lastActivity),
|
||||||
|
),
|
||||||
isCurrent: session.sessionId === currentSessionId,
|
isCurrent: session.sessionId === currentSessionId,
|
||||||
})),
|
})),
|
||||||
total: sessions.length,
|
total: sessions.length,
|
||||||
@@ -284,7 +307,7 @@ export class AuthController {
|
|||||||
const logs = await this.loginAuditService.getLoginLogs(auditFilters);
|
const logs = await this.loginAuditService.getLoginLogs(auditFilters);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logs: logs.map(log => ({
|
logs: logs.map((log) => ({
|
||||||
...log,
|
...log,
|
||||||
timestamp: DateUtil.toBrazilISOString(log.timestamp),
|
timestamp: DateUtil.toBrazilISOString(log.timestamp),
|
||||||
})),
|
})),
|
||||||
@@ -333,13 +356,12 @@ export class AuthController {
|
|||||||
ipAddress: { type: 'string' },
|
ipAddress: { type: 'string' },
|
||||||
userAgent: { type: 'string' },
|
userAgent: { type: 'string' },
|
||||||
createdAt: { type: 'string' },
|
createdAt: { type: 'string' },
|
||||||
lastActivity: { type: 'string' }
|
lastActivity: { type: 'string' },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@Get('session/status')
|
@Get('session/status')
|
||||||
async checkSessionStatus(@Query('username') username: string): Promise<{
|
async checkSessionStatus(@Query('username') username: string): Promise<{
|
||||||
hasActiveSession: boolean;
|
hasActiveSession: boolean;
|
||||||
@@ -353,7 +375,12 @@ export class AuthController {
|
|||||||
}> {
|
}> {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
new ResultModel(false, 'Username é obrigatório', null, 'Username é obrigatório'),
|
new ResultModel(
|
||||||
|
false,
|
||||||
|
'Username é obrigatório',
|
||||||
|
null,
|
||||||
|
'Username é obrigatório',
|
||||||
|
),
|
||||||
HttpStatus.BAD_REQUEST,
|
HttpStatus.BAD_REQUEST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -369,7 +396,9 @@ export class AuthController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeSession = await this.sessionManagementService.hasActiveSession(user.id);
|
const activeSession = await this.sessionManagementService.hasActiveSession(
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (!activeSession) {
|
if (!activeSession) {
|
||||||
return {
|
return {
|
||||||
@@ -383,8 +412,12 @@ export class AuthController {
|
|||||||
sessionId: activeSession.sessionId,
|
sessionId: activeSession.sessionId,
|
||||||
ipAddress: activeSession.ipAddress,
|
ipAddress: activeSession.ipAddress,
|
||||||
userAgent: activeSession.userAgent,
|
userAgent: activeSession.userAgent,
|
||||||
createdAt: DateUtil.toBrazilISOString(new Date(activeSession.createdAt)),
|
createdAt: DateUtil.toBrazilISOString(
|
||||||
lastActivity: DateUtil.toBrazilISOString(new Date(activeSession.lastActivity)),
|
new Date(activeSession.createdAt),
|
||||||
|
),
|
||||||
|
lastActivity: DateUtil.toBrazilISOString(
|
||||||
|
new Date(activeSession.lastActivity),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { LoginAuditService } from '../services/login-audit.service';
|
|||||||
RefreshTokenService,
|
RefreshTokenService,
|
||||||
SessionManagementService,
|
SessionManagementService,
|
||||||
LoginAuditService,
|
LoginAuditService,
|
||||||
AuthenticateUserHandler
|
AuthenticateUserHandler,
|
||||||
],
|
],
|
||||||
exports: [AuthService],
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
|
import {
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
BadRequestException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
import { JwtPayload } from '../models/jwt-payload.model';
|
import { JwtPayload } from '../models/jwt-payload.model';
|
||||||
@@ -7,7 +11,6 @@ import { TokenBlacklistService } from '../services/token-blacklist.service';
|
|||||||
import { RefreshTokenService } from '../services/refresh-token.service';
|
import { RefreshTokenService } from '../services/refresh-token.service';
|
||||||
import { SessionManagementService } from '../services/session-management.service';
|
import { SessionManagementService } from '../services/session-management.service';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -23,7 +26,14 @@ export class AuthService {
|
|||||||
* Cria um token JWT com validação de todos os parâmetros de entrada
|
* Cria um token JWT com validação de todos os parâmetros de entrada
|
||||||
* @throws BadRequestException quando os parâmetros são inválidos
|
* @throws BadRequestException quando os parâmetros são inválidos
|
||||||
*/
|
*/
|
||||||
async createToken(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) {
|
async createToken(
|
||||||
|
id: number,
|
||||||
|
sellerId: number,
|
||||||
|
username: string,
|
||||||
|
email: string,
|
||||||
|
storeId: string,
|
||||||
|
sessionId?: string,
|
||||||
|
) {
|
||||||
this.validateTokenParameters(id, sellerId, username, email, storeId);
|
this.validateTokenParameters(id, sellerId, username, email, storeId);
|
||||||
|
|
||||||
const user: JwtPayload = {
|
const user: JwtPayload = {
|
||||||
@@ -42,7 +52,13 @@ export class AuthService {
|
|||||||
* Valida os parâmetros de entrada para criação de token
|
* Valida os parâmetros de entrada para criação de token
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private validateTokenParameters(id: number, sellerId: number, username: string, email: string, storeId: string): void {
|
private validateTokenParameters(
|
||||||
|
id: number,
|
||||||
|
sellerId: number,
|
||||||
|
username: string,
|
||||||
|
email: string,
|
||||||
|
storeId: string,
|
||||||
|
): void {
|
||||||
if (!id || id <= 0) {
|
if (!id || id <= 0) {
|
||||||
throw new BadRequestException('ID de usuário inválido');
|
throw new BadRequestException('ID de usuário inválido');
|
||||||
}
|
}
|
||||||
@@ -64,7 +80,9 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (/['";\\]/.test(username)) {
|
if (/['";\\]/.test(username)) {
|
||||||
throw new BadRequestException('Nome de usuário contém caracteres inválidos');
|
throw new BadRequestException(
|
||||||
|
'Nome de usuário contém caracteres inválidos',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!email || typeof email !== 'string' || !email.trim()) {
|
if (!email || typeof email !== 'string' || !email.trim()) {
|
||||||
@@ -92,16 +110,41 @@ export class AuthService {
|
|||||||
* @throws BadRequestException quando os parâmetros são inválidos
|
* @throws BadRequestException quando os parâmetros são inválidos
|
||||||
* @throws Error quando os tokens gerados são inválidos
|
* @throws Error quando os tokens gerados são inválidos
|
||||||
*/
|
*/
|
||||||
async createTokenPair(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) {
|
async createTokenPair(
|
||||||
const accessToken = await this.createToken(id, sellerId, username, email, storeId, sessionId);
|
id: number,
|
||||||
|
sellerId: number,
|
||||||
|
username: string,
|
||||||
|
email: string,
|
||||||
|
storeId: string,
|
||||||
|
sessionId?: string,
|
||||||
|
) {
|
||||||
|
const accessToken = await this.createToken(
|
||||||
|
id,
|
||||||
|
sellerId,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
storeId,
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!accessToken || typeof accessToken !== 'string' || !accessToken.trim()) {
|
if (
|
||||||
|
!accessToken ||
|
||||||
|
typeof accessToken !== 'string' ||
|
||||||
|
!accessToken.trim()
|
||||||
|
) {
|
||||||
throw new Error('Token de acesso inválido gerado');
|
throw new Error('Token de acesso inválido gerado');
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(id, sessionId);
|
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||||
|
id,
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!refreshToken || typeof refreshToken !== 'string' || !refreshToken.trim()) {
|
if (
|
||||||
|
!refreshToken ||
|
||||||
|
typeof refreshToken !== 'string' ||
|
||||||
|
!refreshToken.trim()
|
||||||
|
) {
|
||||||
throw new Error('Refresh token inválido gerado');
|
throw new Error('Refresh token inválido gerado');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +164,9 @@ export class AuthService {
|
|||||||
async refreshAccessToken(refreshToken: string) {
|
async refreshAccessToken(refreshToken: string) {
|
||||||
this.validateRefreshTokenInput(refreshToken);
|
this.validateRefreshTokenInput(refreshToken);
|
||||||
|
|
||||||
const tokenData = await this.refreshTokenService.validateRefreshToken(refreshToken);
|
const tokenData = await this.refreshTokenService.validateRefreshToken(
|
||||||
|
refreshToken,
|
||||||
|
);
|
||||||
|
|
||||||
if (!tokenData || !tokenData.id) {
|
if (!tokenData || !tokenData.id) {
|
||||||
throw new BadRequestException('Dados do refresh token inválidos');
|
throw new BadRequestException('Dados do refresh token inválidos');
|
||||||
@@ -135,10 +180,11 @@ export class AuthService {
|
|||||||
this.validateUserDataForToken(user);
|
this.validateUserDataForToken(user);
|
||||||
|
|
||||||
if (tokenData.sessionId) {
|
if (tokenData.sessionId) {
|
||||||
const isSessionActive = await this.sessionManagementService.isSessionActive(
|
const isSessionActive =
|
||||||
user.id,
|
await this.sessionManagementService.isSessionActive(
|
||||||
tokenData.sessionId
|
user.id,
|
||||||
);
|
tokenData.sessionId,
|
||||||
|
);
|
||||||
if (!isSessionActive) {
|
if (!isSessionActive) {
|
||||||
throw new UnauthorizedException('Sessão não está mais ativa');
|
throw new UnauthorizedException('Sessão não está mais ativa');
|
||||||
}
|
}
|
||||||
@@ -150,10 +196,14 @@ export class AuthService {
|
|||||||
user.name,
|
user.name,
|
||||||
user.email,
|
user.email,
|
||||||
user.storeId,
|
user.storeId,
|
||||||
tokenData.sessionId
|
tokenData.sessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!newAccessToken || typeof newAccessToken !== 'string' || !newAccessToken.trim()) {
|
if (
|
||||||
|
!newAccessToken ||
|
||||||
|
typeof newAccessToken !== 'string' ||
|
||||||
|
!newAccessToken.trim()
|
||||||
|
) {
|
||||||
throw new Error('Falha ao gerar novo token de acesso');
|
throw new Error('Falha ao gerar novo token de acesso');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +218,11 @@ export class AuthService {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private validateRefreshTokenInput(refreshToken: string): void {
|
private validateRefreshTokenInput(refreshToken: string): void {
|
||||||
if (!refreshToken || typeof refreshToken !== 'string' || !refreshToken.trim()) {
|
if (
|
||||||
|
!refreshToken ||
|
||||||
|
typeof refreshToken !== 'string' ||
|
||||||
|
!refreshToken.trim()
|
||||||
|
) {
|
||||||
throw new BadRequestException('Refresh token não pode estar vazio');
|
throw new BadRequestException('Refresh token não pode estar vazio');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,18 +241,32 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
private validateUserDataForToken(user: any): void {
|
private validateUserDataForToken(user: any): void {
|
||||||
if (!user.name || typeof user.name !== 'string' || !user.name.trim()) {
|
if (!user.name || typeof user.name !== 'string' || !user.name.trim()) {
|
||||||
throw new BadRequestException('Dados do usuário incompletos: nome não encontrado');
|
throw new BadRequestException(
|
||||||
|
'Dados do usuário incompletos: nome não encontrado',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.email || typeof user.email !== 'string' || !user.email.trim()) {
|
if (!user.email || typeof user.email !== 'string' || !user.email.trim()) {
|
||||||
throw new BadRequestException('Dados do usuário incompletos: email não encontrado');
|
throw new BadRequestException(
|
||||||
|
'Dados do usuário incompletos: email não encontrado',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.storeId || typeof user.storeId !== 'string' || !user.storeId.trim()) {
|
if (
|
||||||
throw new BadRequestException('Dados do usuário incompletos: storeId não encontrado');
|
!user.storeId ||
|
||||||
|
typeof user.storeId !== 'string' ||
|
||||||
|
!user.storeId.trim()
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Dados do usuário incompletos: storeId não encontrado',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.sellerId !== null && user.sellerId !== undefined && user.sellerId < 0) {
|
if (
|
||||||
|
user.sellerId !== null &&
|
||||||
|
user.sellerId !== undefined &&
|
||||||
|
user.sellerId < 0
|
||||||
|
) {
|
||||||
throw new BadRequestException('ID de vendedor inválido');
|
throw new BadRequestException('ID de vendedor inválido');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,11 +296,15 @@ export class AuthService {
|
|||||||
try {
|
try {
|
||||||
decoded = this.jwtService.decode(token) as JwtPayload;
|
decoded = this.jwtService.decode(token) as JwtPayload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new BadRequestException('Token inválido ou não pode ser decodificado');
|
throw new BadRequestException(
|
||||||
|
'Token inválido ou não pode ser decodificado',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decoded || !decoded.id) {
|
if (!decoded || !decoded.id) {
|
||||||
throw new BadRequestException('Token inválido ou não pode ser decodificado');
|
throw new BadRequestException(
|
||||||
|
'Token inválido ou não pode ser decodificado',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decoded.id <= 0) {
|
if (decoded.id <= 0) {
|
||||||
@@ -241,25 +313,34 @@ export class AuthService {
|
|||||||
|
|
||||||
if (decoded.sessionId && decoded.id && decoded.sessionId.trim()) {
|
if (decoded.sessionId && decoded.id && decoded.sessionId.trim()) {
|
||||||
try {
|
try {
|
||||||
await this.sessionManagementService.terminateSession(decoded.id, decoded.sessionId);
|
await this.sessionManagementService.terminateSession(
|
||||||
|
decoded.id,
|
||||||
|
decoded.sessionId,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
if (errorMessage.includes('Sessão não encontrada')) {
|
if (errorMessage.includes('Sessão não encontrada')) {
|
||||||
throw new Error('Sessão não encontrada');
|
throw new Error('Sessão não encontrada');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAlreadyBlacklisted = await this.tokenBlacklistService.isBlacklisted(token);
|
const isAlreadyBlacklisted = await this.tokenBlacklistService.isBlacklisted(
|
||||||
|
token,
|
||||||
|
);
|
||||||
if (!isAlreadyBlacklisted) {
|
if (!isAlreadyBlacklisted) {
|
||||||
try {
|
try {
|
||||||
await this.tokenBlacklistService.addToBlacklist(token);
|
await this.tokenBlacklistService.addToBlacklist(token);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
if (errorMessage.includes('já está na blacklist')) {
|
if (errorMessage.includes('já está na blacklist')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error(`Falha ao adicionar token à blacklist: ${errorMessage}`);
|
throw new Error(
|
||||||
|
`Falha ao adicionar token à blacklist: ${errorMessage}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export class AuthenticateUserCommand {
|
export class AuthenticateUserCommand {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly username: string,
|
public readonly username: string,
|
||||||
public readonly password: string,
|
public readonly password: string,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,18 @@ import { UserModel } from 'src/core/models/user.model';
|
|||||||
|
|
||||||
@CommandHandler(AuthenticateUserCommand)
|
@CommandHandler(AuthenticateUserCommand)
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthenticateUserHandler implements ICommandHandler<AuthenticateUserCommand> {
|
export class AuthenticateUserHandler
|
||||||
|
implements ICommandHandler<AuthenticateUserCommand>
|
||||||
|
{
|
||||||
constructor(private readonly userRepository: UserRepository) {}
|
constructor(private readonly userRepository: UserRepository) {}
|
||||||
|
|
||||||
async execute(command: AuthenticateUserCommand): Promise<Result<UserModel>> {
|
async execute(command: AuthenticateUserCommand): Promise<Result<UserModel>> {
|
||||||
const { username, password } = command;
|
const { username, password } = command;
|
||||||
|
|
||||||
const user = await this.userRepository.findByUsernameAndPassword(username, password);
|
const user = await this.userRepository.findByUsernameAndPassword(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return Result.fail('Usuário ou senha inválidos');
|
return Result.fail('Usuário ou senha inválidos');
|
||||||
@@ -31,7 +36,6 @@ export class AuthenticateUserHandler implements ICommandHandler<AuthenticateUser
|
|||||||
return Result.fail('Usuário bloqueado, login não permitido!');
|
return Result.fail('Usuário bloqueado, login não permitido!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return Result.ok(user);
|
return Result.ok(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsOptional, IsNumber, IsString, IsBoolean, IsDateString, Min, Max } from 'class-validator';
|
import {
|
||||||
|
IsOptional,
|
||||||
|
IsNumber,
|
||||||
|
IsString,
|
||||||
|
IsBoolean,
|
||||||
|
IsDateString,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
} from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
export class LoginAuditFiltersDto {
|
export class LoginAuditFiltersDto {
|
||||||
@@ -19,7 +27,10 @@ export class LoginAuditFiltersDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
ipAddress?: string;
|
ipAddress?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Filtrar apenas logins bem-sucedidos', required: false })
|
@ApiProperty({
|
||||||
|
description: 'Filtrar apenas logins bem-sucedidos',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@Type(() => Boolean)
|
@Type(() => Boolean)
|
||||||
@@ -35,7 +46,12 @@ export class LoginAuditFiltersDto {
|
|||||||
@IsDateString()
|
@IsDateString()
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Número de registros por página', required: false, minimum: 1, maximum: 1000 })
|
@ApiProperty({
|
||||||
|
description: 'Número de registros por página',
|
||||||
|
required: false,
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 1000,
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@@ -43,7 +59,11 @@ export class LoginAuditFiltersDto {
|
|||||||
@Max(1000)
|
@Max(1000)
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Offset para paginação', required: false, minimum: 0 })
|
@ApiProperty({
|
||||||
|
description: 'Offset para paginação',
|
||||||
|
required: false,
|
||||||
|
minimum: 0,
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@@ -84,7 +104,10 @@ export class LoginAuditLogDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LoginAuditResponseDto {
|
export class LoginAuditResponseDto {
|
||||||
@ApiProperty({ description: 'Lista de logs de login', type: [LoginAuditLogDto] })
|
@ApiProperty({
|
||||||
|
description: 'Lista de logs de login',
|
||||||
|
type: [LoginAuditLogDto],
|
||||||
|
})
|
||||||
logs: LoginAuditLogDto[];
|
logs: LoginAuditLogDto[];
|
||||||
|
|
||||||
@ApiProperty({ description: 'Total de registros encontrados' })
|
@ApiProperty({ description: 'Total de registros encontrados' })
|
||||||
@@ -123,13 +146,21 @@ export class LoginStatsDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LoginStatsFiltersDto {
|
export class LoginStatsFiltersDto {
|
||||||
@ApiProperty({ description: 'ID do usuário para estatísticas', required: false })
|
@ApiProperty({
|
||||||
|
description: 'ID do usuário para estatísticas',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Número de dias para análise', required: false, minimum: 1, maximum: 365 })
|
@ApiProperty({
|
||||||
|
description: 'Número de dias para análise',
|
||||||
|
required: false,
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 365,
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
|
|
||||||
mockGetRequest.mockReturnValue(request);
|
mockGetRequest.mockReturnValue(request);
|
||||||
mockRateLimitingService.isAllowed.mockRejectedValue(
|
mockRateLimitingService.isAllowed.mockRejectedValue(
|
||||||
new Error('Erro de conexão com Redis')
|
new Error('Erro de conexão com Redis'),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -225,7 +225,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
mockGetRequest.mockReturnValue(request);
|
mockGetRequest.mockReturnValue(request);
|
||||||
mockRateLimitingService.isAllowed.mockResolvedValue(false);
|
mockRateLimitingService.isAllowed.mockResolvedValue(false);
|
||||||
mockRateLimitingService.getAttemptInfo.mockRejectedValue(
|
mockRateLimitingService.getAttemptInfo.mockRejectedValue(
|
||||||
new Error('Erro ao buscar informações')
|
new Error('Erro ao buscar informações'),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -336,7 +336,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
const result = await guard.canActivate(mockExecutionContext);
|
const result = await guard.canActivate(mockExecutionContext);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('192.168.1.1');
|
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
|
||||||
|
'192.168.1.1',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle concurrent requests with same IP', async () => {
|
it('should handle concurrent requests with same IP', async () => {
|
||||||
@@ -363,7 +365,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
|
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
results.forEach(result => {
|
results.forEach((result) => {
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -394,7 +396,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
fail('Deveria ter lançado exceção');
|
fail('Deveria ter lançado exceção');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(HttpException);
|
expect(error).toBeInstanceOf(HttpException);
|
||||||
expect((error as HttpException).getStatus()).toBe(HttpStatus.TOO_MANY_REQUESTS);
|
expect((error as HttpException).getStatus()).toBe(
|
||||||
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -419,7 +423,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
fail('Deveria ter lançado exceção');
|
fail('Deveria ter lançado exceção');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response = (error as HttpException).getResponse() as any;
|
const response = (error as HttpException).getResponse() as any;
|
||||||
expect(response.error).toBe('Muitas tentativas de login. Tente novamente em alguns minutos.');
|
expect(response.error).toBe(
|
||||||
|
'Muitas tentativas de login. Tente novamente em alguns minutos.',
|
||||||
|
);
|
||||||
expect(response.success).toBe(false);
|
expect(response.success).toBe(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -512,7 +518,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
const result = await guard.canActivate(mockExecutionContext);
|
const result = await guard.canActivate(mockExecutionContext);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('2001:0db8:85a3:0000:0000:8a2e:0370:7334');
|
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
|
||||||
|
'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject invalid IPv6 format', async () => {
|
it('should reject invalid IPv6 format', async () => {
|
||||||
@@ -556,7 +564,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
|
|
||||||
await guard.canActivate(mockExecutionContext);
|
await guard.canActivate(mockExecutionContext);
|
||||||
|
|
||||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('192.168.1.1');
|
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
|
||||||
|
'192.168.1.1',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fallback to connection.remoteAddress when x-forwarded-for is missing', async () => {
|
it('should fallback to connection.remoteAddress when x-forwarded-for is missing', async () => {
|
||||||
@@ -572,7 +582,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
|
|
||||||
await guard.canActivate(mockExecutionContext);
|
await guard.canActivate(mockExecutionContext);
|
||||||
|
|
||||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('10.0.0.1');
|
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
|
||||||
|
'10.0.0.1',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use default IP when all sources are missing', async () => {
|
it('should use default IP when all sources are missing', async () => {
|
||||||
@@ -603,4 +615,3 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { RateLimitingService } from '../services/rate-limiting.service';
|
import { RateLimitingService } from '../services/rate-limiting.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -19,7 +25,8 @@ export class RateLimitingGuard implements CanActivate {
|
|||||||
try {
|
try {
|
||||||
isAllowed = await this.rateLimitingService.isAllowed(ip);
|
isAllowed = await this.rateLimitingService.isAllowed(ip);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
@@ -36,7 +43,8 @@ export class RateLimitingGuard implements CanActivate {
|
|||||||
try {
|
try {
|
||||||
attemptInfo = await this.rateLimitingService.getAttemptInfo(ip);
|
attemptInfo = await this.rateLimitingService.getAttemptInfo(ip);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
@@ -53,7 +61,8 @@ export class RateLimitingGuard implements CanActivate {
|
|||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Muitas tentativas de login. Tente novamente em alguns minutos.',
|
error:
|
||||||
|
'Muitas tentativas de login. Tente novamente em alguns minutos.',
|
||||||
data: null,
|
data: null,
|
||||||
details: {
|
details: {
|
||||||
attempts: attemptInfo.attempts,
|
attempts: attemptInfo.attempts,
|
||||||
@@ -73,13 +82,16 @@ export class RateLimitingGuard implements CanActivate {
|
|||||||
* @returns Endereço IP do cliente ou '127.0.0.1' se não encontrado
|
* @returns Endereço IP do cliente ou '127.0.0.1' se não encontrado
|
||||||
*/
|
*/
|
||||||
private getClientIp(request: any): string {
|
private getClientIp(request: any): string {
|
||||||
const forwardedFor = request.headers['x-forwarded-for']?.split(',')[0]?.trim();
|
const forwardedFor = request.headers['x-forwarded-for']
|
||||||
|
?.split(',')[0]
|
||||||
|
?.trim();
|
||||||
const realIp = request.headers['x-real-ip']?.trim();
|
const realIp = request.headers['x-real-ip']?.trim();
|
||||||
const connectionIp = request.connection?.remoteAddress;
|
const connectionIp = request.connection?.remoteAddress;
|
||||||
const socketIp = request.socket?.remoteAddress;
|
const socketIp = request.socket?.remoteAddress;
|
||||||
const requestIp = request.ip;
|
const requestIp = request.ip;
|
||||||
|
|
||||||
const rawIp = forwardedFor || realIp || connectionIp || socketIp || requestIp;
|
const rawIp =
|
||||||
|
forwardedFor || realIp || connectionIp || socketIp || requestIp;
|
||||||
|
|
||||||
if (rawIp === null || rawIp === undefined) {
|
if (rawIp === null || rawIp === undefined) {
|
||||||
return '';
|
return '';
|
||||||
@@ -144,7 +156,11 @@ export class RateLimitingGuard implements CanActivate {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ipv4Regex.test(ip) && !ipv6Regex.test(ip) && !ipv6CompressedRegex.test(ip)) {
|
if (
|
||||||
|
!ipv4Regex.test(ip) &&
|
||||||
|
!ipv6Regex.test(ip) &&
|
||||||
|
!ipv6CompressedRegex.test(ip)
|
||||||
|
) {
|
||||||
if (!this.isValidIpv4(ip) && !this.isValidIpv6(ip)) {
|
if (!this.isValidIpv4(ip) && !this.isValidIpv6(ip)) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{
|
{
|
||||||
@@ -166,7 +182,7 @@ export class RateLimitingGuard implements CanActivate {
|
|||||||
const parts = ip.split('.');
|
const parts = ip.split('.');
|
||||||
if (parts.length !== 4) return false;
|
if (parts.length !== 4) return false;
|
||||||
|
|
||||||
return parts.every(part => {
|
return parts.every((part) => {
|
||||||
const num = parseInt(part, 10);
|
const num = parseInt(part, 10);
|
||||||
return !isNaN(num) && num >= 0 && num <= 255;
|
return !isNaN(num) && num >= 0 && num <= 255;
|
||||||
});
|
});
|
||||||
@@ -184,13 +200,13 @@ export class RateLimitingGuard implements CanActivate {
|
|||||||
const leftParts = parts[0] ? parts[0].split(':') : [];
|
const leftParts = parts[0] ? parts[0].split(':') : [];
|
||||||
const rightParts = parts[1] ? parts[1].split(':') : [];
|
const rightParts = parts[1] ? parts[1].split(':') : [];
|
||||||
|
|
||||||
return (leftParts.length + rightParts.length) <= 8;
|
return leftParts.length + rightParts.length <= 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = ip.split(':');
|
const parts = ip.split(':');
|
||||||
if (parts.length !== 8) return false;
|
if (parts.length !== 8) return false;
|
||||||
|
|
||||||
return parts.every(part => {
|
return parts.every((part) => {
|
||||||
if (!part) return false;
|
if (!part) return false;
|
||||||
return /^[0-9a-fA-F]{1,4}$/.test(part);
|
return /^[0-9a-fA-F]{1,4}$/.test(part);
|
||||||
});
|
});
|
||||||
@@ -223,8 +239,11 @@ export class RateLimitingGuard implements CanActivate {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attemptInfo.remainingTime !== undefined &&
|
if (
|
||||||
(typeof attemptInfo.remainingTime !== 'number' || attemptInfo.remainingTime < 0)) {
|
attemptInfo.remainingTime !== undefined &&
|
||||||
|
(typeof attemptInfo.remainingTime !== 'number' ||
|
||||||
|
attemptInfo.remainingTime < 0)
|
||||||
|
) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
export class Result<T> {
|
export class Result<T> {
|
||||||
private constructor(
|
private constructor(
|
||||||
public readonly success: boolean,
|
public readonly success: boolean,
|
||||||
public readonly data?: T,
|
public readonly data?: T,
|
||||||
public readonly error?: string,
|
public readonly error?: string,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static ok<U>(data: U): Result<U> {
|
static ok<U>(data: U): Result<U> {
|
||||||
return new Result<U>(true, data);
|
return new Result<U>(true, data);
|
||||||
}
|
|
||||||
|
|
||||||
static fail<U>(message: string): Result<U> {
|
|
||||||
return new Result<U>(false, undefined, message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fail<U>(message: string): Result<U> {
|
||||||
|
return new Result<U>(false, undefined, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ export class LoginAuditService {
|
|||||||
private readonly LOG_PREFIX = 'login_audit';
|
private readonly LOG_PREFIX = 'login_audit';
|
||||||
private readonly LOG_EXPIRY = 30 * 24 * 60 * 60;
|
private readonly LOG_EXPIRY = 30 * 24 * 60 * 60;
|
||||||
|
|
||||||
constructor(
|
constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}
|
||||||
@Inject('REDIS_CLIENT') private readonly redis: Redis,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async logLoginAttempt(log: Omit<LoginAuditLog, 'id' | 'timestamp'>): Promise<void> {
|
async logLoginAttempt(
|
||||||
|
log: Omit<LoginAuditLog, 'id' | 'timestamp'>,
|
||||||
|
): Promise<void> {
|
||||||
const logId = this.generateLogId();
|
const logId = this.generateLogId();
|
||||||
const timestamp = DateUtil.now();
|
const timestamp = DateUtil.now();
|
||||||
|
|
||||||
@@ -69,7 +69,9 @@ export class LoginAuditService {
|
|||||||
await this.redis.expire(dateLogsKey, this.LOG_EXPIRY);
|
await this.redis.expire(dateLogsKey, this.LOG_EXPIRY);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLoginLogs(filters: LoginAuditFilters = {}): Promise<LoginAuditLog[]> {
|
async getLoginLogs(
|
||||||
|
filters: LoginAuditFilters = {},
|
||||||
|
): Promise<LoginAuditLog[]> {
|
||||||
const logIds = await this.getLogIds(filters);
|
const logIds = await this.getLogIds(filters);
|
||||||
|
|
||||||
const logs: LoginAuditLog[] = [];
|
const logs: LoginAuditLog[] = [];
|
||||||
@@ -102,13 +104,21 @@ export class LoginAuditService {
|
|||||||
return logs.slice(offset, offset + limit);
|
return logs.slice(offset, offset + limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLoginStats(userId?: number, days: number = 7): Promise<{
|
async getLoginStats(
|
||||||
|
userId?: number,
|
||||||
|
days: number = 7,
|
||||||
|
): Promise<{
|
||||||
totalAttempts: number;
|
totalAttempts: number;
|
||||||
successfulLogins: number;
|
successfulLogins: number;
|
||||||
failedLogins: number;
|
failedLogins: number;
|
||||||
uniqueIps: number;
|
uniqueIps: number;
|
||||||
topIps: Array<{ ip: string; count: number }>;
|
topIps: Array<{ ip: string; count: number }>;
|
||||||
dailyStats: Array<{ date: string; attempts: number; successes: number; failures: number }>;
|
dailyStats: Array<{
|
||||||
|
date: string;
|
||||||
|
attempts: number;
|
||||||
|
successes: number;
|
||||||
|
failures: number;
|
||||||
|
}>;
|
||||||
}> {
|
}> {
|
||||||
const endDate = DateUtil.now();
|
const endDate = DateUtil.now();
|
||||||
const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000);
|
const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000);
|
||||||
@@ -127,15 +137,20 @@ export class LoginAuditService {
|
|||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
totalAttempts: logs.length,
|
totalAttempts: logs.length,
|
||||||
successfulLogins: logs.filter(log => log.success).length,
|
successfulLogins: logs.filter((log) => log.success).length,
|
||||||
failedLogins: logs.filter(log => !log.success).length,
|
failedLogins: logs.filter((log) => !log.success).length,
|
||||||
uniqueIps: new Set(logs.map(log => log.ipAddress)).size,
|
uniqueIps: new Set(logs.map((log) => log.ipAddress)).size,
|
||||||
topIps: [] as Array<{ ip: string; count: number }>,
|
topIps: [] as Array<{ ip: string; count: number }>,
|
||||||
dailyStats: [] as Array<{ date: string; attempts: number; successes: number; failures: number }>,
|
dailyStats: [] as Array<{
|
||||||
|
date: string;
|
||||||
|
attempts: number;
|
||||||
|
successes: number;
|
||||||
|
failures: number;
|
||||||
|
}>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ipCounts = new Map<string, number>();
|
const ipCounts = new Map<string, number>();
|
||||||
logs.forEach(log => {
|
logs.forEach((log) => {
|
||||||
ipCounts.set(log.ipAddress, (ipCounts.get(log.ipAddress) || 0) + 1);
|
ipCounts.set(log.ipAddress, (ipCounts.get(log.ipAddress) || 0) + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,10 +159,17 @@ export class LoginAuditService {
|
|||||||
.sort((a, b) => b.count - a.count)
|
.sort((a, b) => b.count - a.count)
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
const dailyCounts = new Map<string, { attempts: number; successes: number; failures: number }>();
|
const dailyCounts = new Map<
|
||||||
logs.forEach(log => {
|
string,
|
||||||
|
{ attempts: number; successes: number; failures: number }
|
||||||
|
>();
|
||||||
|
logs.forEach((log) => {
|
||||||
const date = DateUtil.toBrazilString(log.timestamp, 'yyyy-MM-dd');
|
const date = DateUtil.toBrazilString(log.timestamp, 'yyyy-MM-dd');
|
||||||
const dayStats = dailyCounts.get(date) || { attempts: 0, successes: 0, failures: 0 };
|
const dayStats = dailyCounts.get(date) || {
|
||||||
|
attempts: 0,
|
||||||
|
successes: 0,
|
||||||
|
failures: 0,
|
||||||
|
};
|
||||||
dayStats.attempts++;
|
dayStats.attempts++;
|
||||||
|
|
||||||
if (log.success) {
|
if (log.success) {
|
||||||
@@ -168,7 +190,9 @@ export class LoginAuditService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async cleanupOldLogs(): Promise<void> {
|
async cleanupOldLogs(): Promise<void> {
|
||||||
const cutoffDate = new Date(DateUtil.nowTimestamp() - 30 * 24 * 60 * 60 * 1000);
|
const cutoffDate = new Date(
|
||||||
|
DateUtil.nowTimestamp() - 30 * 24 * 60 * 60 * 1000,
|
||||||
|
);
|
||||||
const cutoffDateStr = DateUtil.toBrazilString(cutoffDate, 'yyyy-MM-dd');
|
const cutoffDateStr = DateUtil.toBrazilString(cutoffDate, 'yyyy-MM-dd');
|
||||||
|
|
||||||
const oldDates = this.getDateRange(new Date('2020-01-01'), cutoffDate);
|
const oldDates = this.getDateRange(new Date('2020-01-01'), cutoffDate);
|
||||||
@@ -190,7 +214,9 @@ export class LoginAuditService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filters.startDate || filters.endDate) {
|
if (filters.startDate || filters.endDate) {
|
||||||
const startDate = filters.startDate || new Date(DateUtil.nowTimestamp() - 7 * 24 * 60 * 60 * 1000);
|
const startDate =
|
||||||
|
filters.startDate ||
|
||||||
|
new Date(DateUtil.nowTimestamp() - 7 * 24 * 60 * 60 * 1000);
|
||||||
const endDate = filters.endDate || DateUtil.now();
|
const endDate = filters.endDate || DateUtil.now();
|
||||||
|
|
||||||
const dates = this.getDateRange(startDate, endDate);
|
const dates = this.getDateRange(startDate, endDate);
|
||||||
@@ -210,7 +236,9 @@ export class LoginAuditService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private generateLogId(): string {
|
private generateLogId(): string {
|
||||||
return `${DateUtil.nowTimestamp()}_${Math.random().toString(36).substr(2, 9)}`;
|
return `${DateUtil.nowTimestamp()}_${Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2, 9)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildLogKey(logId: string): string {
|
private buildLogKey(logId: string): string {
|
||||||
@@ -233,8 +261,14 @@ export class LoginAuditService {
|
|||||||
return `${this.LOG_PREFIX}:date:${date}`;
|
return `${this.LOG_PREFIX}:date:${date}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private matchesFilters(log: LoginAuditLog, filters: LoginAuditFilters): boolean {
|
private matchesFilters(
|
||||||
if (filters.username && !log.username.toLowerCase().includes(filters.username.toLowerCase())) {
|
log: LoginAuditLog,
|
||||||
|
filters: LoginAuditFilters,
|
||||||
|
): boolean {
|
||||||
|
if (
|
||||||
|
filters.username &&
|
||||||
|
!log.username.toLowerCase().includes(filters.username.toLowerCase())
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ export class RateLimitingService {
|
|||||||
blockDurationMs: 1 * 60 * 1000,
|
blockDurationMs: 1 * 60 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(@Inject(RedisClientToken) private readonly redis: IRedisClient) {}
|
||||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async isAllowed(ip: string, config?: Partial<RateLimitConfig>): Promise<boolean> {
|
async isAllowed(
|
||||||
|
ip: string,
|
||||||
|
config?: Partial<RateLimitConfig>,
|
||||||
|
): Promise<boolean> {
|
||||||
const finalConfig = { ...this.defaultConfig, ...config };
|
const finalConfig = { ...this.defaultConfig, ...config };
|
||||||
const key = this.buildAttemptKey(ip);
|
const key = this.buildAttemptKey(ip);
|
||||||
const blockKey = this.buildBlockKey(ip);
|
const blockKey = this.buildBlockKey(ip);
|
||||||
@@ -51,21 +52,25 @@ export class RateLimitingService {
|
|||||||
return {attempts, 0}
|
return {attempts, 0}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await this.redis.eval(
|
const result = (await this.redis.eval(
|
||||||
luaScript,
|
luaScript,
|
||||||
2,
|
2,
|
||||||
key,
|
key,
|
||||||
blockKey,
|
blockKey,
|
||||||
finalConfig.maxAttempts,
|
finalConfig.maxAttempts,
|
||||||
finalConfig.windowMs,
|
finalConfig.windowMs,
|
||||||
finalConfig.blockDurationMs
|
finalConfig.blockDurationMs,
|
||||||
) as [number, number];
|
)) as [number, number];
|
||||||
|
|
||||||
const [attempts, isBlockedResult] = result;
|
const [attempts, isBlockedResult] = result;
|
||||||
return isBlockedResult === 0;
|
return isBlockedResult === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async recordAttempt(ip: string, success: boolean, config?: Partial<RateLimitConfig>): Promise<void> {
|
async recordAttempt(
|
||||||
|
ip: string,
|
||||||
|
success: boolean,
|
||||||
|
config?: Partial<RateLimitConfig>,
|
||||||
|
): Promise<void> {
|
||||||
const finalConfig = { ...this.defaultConfig, ...config };
|
const finalConfig = { ...this.defaultConfig, ...config };
|
||||||
const key = this.buildAttemptKey(ip);
|
const key = this.buildAttemptKey(ip);
|
||||||
const blockKey = this.buildBlockKey(ip);
|
const blockKey = this.buildBlockKey(ip);
|
||||||
|
|||||||
@@ -24,18 +24,21 @@ export class RefreshTokenService {
|
|||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateRefreshToken(userId: number, sessionId?: string): Promise<string> {
|
async generateRefreshToken(
|
||||||
|
userId: number,
|
||||||
|
sessionId?: string,
|
||||||
|
): Promise<string> {
|
||||||
const tokenId = randomBytes(32).toString('hex');
|
const tokenId = randomBytes(32).toString('hex');
|
||||||
const refreshToken = this.jwtService.sign(
|
const refreshToken = this.jwtService.sign(
|
||||||
{ userId, tokenId, sessionId, type: 'refresh' },
|
{ userId, tokenId, sessionId, type: 'refresh' },
|
||||||
{ expiresIn: '7d' }
|
{ expiresIn: '7d' },
|
||||||
);
|
);
|
||||||
|
|
||||||
const tokenData: RefreshTokenData = {
|
const tokenData: RefreshTokenData = {
|
||||||
userId,
|
userId,
|
||||||
tokenId,
|
tokenId,
|
||||||
sessionId,
|
sessionId,
|
||||||
expiresAt: DateUtil.nowTimestamp() + (this.REFRESH_TOKEN_TTL * 1000),
|
expiresAt: DateUtil.nowTimestamp() + this.REFRESH_TOKEN_TTL * 1000,
|
||||||
createdAt: DateUtil.nowTimestamp(),
|
createdAt: DateUtil.nowTimestamp(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +78,7 @@ export class RefreshTokenService {
|
|||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
sessionId: sessionId || tokenData.sessionId,
|
sessionId: sessionId || tokenData.sessionId,
|
||||||
tokenId
|
tokenId,
|
||||||
} as JwtPayload;
|
} as JwtPayload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new UnauthorizedException('Refresh token inválido');
|
throw new UnauthorizedException('Refresh token inválido');
|
||||||
@@ -118,7 +121,7 @@ export class RefreshTokenService {
|
|||||||
if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) {
|
if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) {
|
||||||
const tokensToRemove = activeTokens
|
const tokensToRemove = activeTokens
|
||||||
.slice(this.MAX_REFRESH_TOKENS_PER_USER)
|
.slice(this.MAX_REFRESH_TOKENS_PER_USER)
|
||||||
.map(token => token.tokenId);
|
.map((token) => token.tokenId);
|
||||||
|
|
||||||
for (const tokenId of tokensToRemove) {
|
for (const tokenId of tokensToRemove) {
|
||||||
await this.revokeRefreshToken(userId, tokenId);
|
await this.revokeRefreshToken(userId, tokenId);
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ export class SessionManagementService {
|
|||||||
private readonly SESSION_TTL = 8 * 60 * 60;
|
private readonly SESSION_TTL = 8 * 60 * 60;
|
||||||
private readonly MAX_SESSIONS_PER_USER = 1;
|
private readonly MAX_SESSIONS_PER_USER = 1;
|
||||||
|
|
||||||
constructor(
|
constructor(@Inject(RedisClientToken) private readonly redis: IRedisClient) {}
|
||||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async createSession(userId: number, ipAddress: string, userAgent: string): Promise<SessionData> {
|
async createSession(
|
||||||
|
userId: number,
|
||||||
|
ipAddress: string,
|
||||||
|
userAgent: string,
|
||||||
|
): Promise<SessionData> {
|
||||||
const sessionId = randomBytes(16).toString('hex');
|
const sessionId = randomBytes(16).toString('hex');
|
||||||
const now = DateUtil.nowTimestamp();
|
const now = DateUtil.nowTimestamp();
|
||||||
|
|
||||||
@@ -45,7 +47,10 @@ export class SessionManagementService {
|
|||||||
return sessionData;
|
return sessionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSessionActivity(userId: number, sessionId: string): Promise<void> {
|
async updateSessionActivity(
|
||||||
|
userId: number,
|
||||||
|
sessionId: string,
|
||||||
|
): Promise<void> {
|
||||||
const key = this.buildSessionKey(userId, sessionId);
|
const key = this.buildSessionKey(userId, sessionId);
|
||||||
const sessionData = await this.redis.get<SessionData>(key);
|
const sessionData = await this.redis.get<SessionData>(key);
|
||||||
|
|
||||||
@@ -55,7 +60,10 @@ export class SessionManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActiveSessions(userId: number, currentSessionId?: string): Promise<SessionData[]> {
|
async getActiveSessions(
|
||||||
|
userId: number,
|
||||||
|
currentSessionId?: string,
|
||||||
|
): Promise<SessionData[]> {
|
||||||
const pattern = this.buildSessionPattern(userId);
|
const pattern = this.buildSessionPattern(userId);
|
||||||
const keys = await this.redis.keys(pattern);
|
const keys = await this.redis.keys(pattern);
|
||||||
|
|
||||||
@@ -99,7 +107,10 @@ export class SessionManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async terminateOtherSessions(userId: number, currentSessionId: string): Promise<void> {
|
async terminateOtherSessions(
|
||||||
|
userId: number,
|
||||||
|
currentSessionId: string,
|
||||||
|
): Promise<void> {
|
||||||
const pattern = this.buildSessionPattern(userId);
|
const pattern = this.buildSessionPattern(userId);
|
||||||
const keys = await this.redis.keys(pattern);
|
const keys = await this.redis.keys(pattern);
|
||||||
|
|
||||||
@@ -130,7 +141,7 @@ export class SessionManagementService {
|
|||||||
if (activeSessions.length > this.MAX_SESSIONS_PER_USER) {
|
if (activeSessions.length > this.MAX_SESSIONS_PER_USER) {
|
||||||
const sessionsToRemove = activeSessions
|
const sessionsToRemove = activeSessions
|
||||||
.slice(this.MAX_SESSIONS_PER_USER)
|
.slice(this.MAX_SESSIONS_PER_USER)
|
||||||
.map(session => session.sessionId);
|
.map((session) => session.sessionId);
|
||||||
|
|
||||||
for (const sessionId of sessionsToRemove) {
|
for (const sessionId of sessionsToRemove) {
|
||||||
await this.terminateSession(userId, sessionId);
|
await this.terminateSession(userId, sessionId);
|
||||||
|
|||||||
@@ -59,12 +59,16 @@ export class TokenBlacklistService {
|
|||||||
|
|
||||||
private calculateTokenTTL(payload: JwtPayload): number {
|
private calculateTokenTTL(payload: JwtPayload): number {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const exp = payload.exp || (now + 8 * 60 * 60);
|
const exp = payload.exp || now + 8 * 60 * 60;
|
||||||
return Math.max(0, exp - now);
|
return Math.max(0, exp - now);
|
||||||
}
|
}
|
||||||
|
|
||||||
private hashToken(token: string): string {
|
private hashToken(token: string): string {
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
return crypto.createHash('sha256').update(token).digest('hex').substring(0, 16);
|
return crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(token)
|
||||||
|
.digest('hex')
|
||||||
|
.substring(0, 16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const token = req.headers?.authorization?.replace('Bearer ', '');
|
const token = req.headers?.authorization?.replace('Bearer ', '');
|
||||||
if (token && await this.tokenBlacklistService.isBlacklisted(token)) {
|
if (token && (await this.tokenBlacklistService.isBlacklisted(token))) {
|
||||||
throw new UnauthorizedException('Token foi invalidado');
|
throw new UnauthorizedException('Token foi invalidado');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +39,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
const cachedUser = await this.redis.get<any>(sessionKey);
|
const cachedUser = await this.redis.get<any>(sessionKey);
|
||||||
|
|
||||||
if (cachedUser) {
|
if (cachedUser) {
|
||||||
const isSessionActive = await this.sessionManagementService.isSessionActive(
|
const isSessionActive =
|
||||||
payload.id,
|
await this.sessionManagementService.isSessionActive(
|
||||||
payload.sessionId
|
payload.id,
|
||||||
);
|
payload.sessionId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!isSessionActive) {
|
if (!isSessionActive) {
|
||||||
throw new UnauthorizedException('Sessão expirada ou inválida');
|
throw new UnauthorizedException('Sessão expirada ou inválida');
|
||||||
@@ -65,7 +66,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (user.situacao === 'B') {
|
if (user.situacao === 'B') {
|
||||||
throw new UnauthorizedException('Usuário bloqueado, acesso não permitido');
|
throw new UnauthorizedException(
|
||||||
|
'Usuário bloqueado, acesso não permitido',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userData = {
|
const userData = {
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ export class ResetPasswordService {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const newPassword = Guid.create().toString().substring(0, 8);
|
const newPassword = Guid.create().toString().substring(0, 8);
|
||||||
await this.userRepository.updatePassword(user.sellerId, md5(newPassword).toUpperCase());
|
await this.userRepository.updatePassword(
|
||||||
|
user.sellerId,
|
||||||
|
md5(newPassword).toUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
await this.emailService.sendPasswordReset(user.email, newPassword);
|
await this.emailService.sendPasswordReset(user.email, newPassword);
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,8 @@ import { EmailService } from './email.service';
|
|||||||
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
|
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
|
||||||
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
|
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [TypeOrmModule.forFeature([])],
|
||||||
TypeOrmModule.forFeature([]),
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
UsersService,
|
UsersService,
|
||||||
UserRepository,
|
UserRepository,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { ResetPasswordService } from './reset-password.service';
|
|||||||
import { ChangePasswordService } from './change-password.service';
|
import { ChangePasswordService } from './change-password.service';
|
||||||
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
|
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -22,7 +20,15 @@ export class UsersService {
|
|||||||
return this.resetPasswordService.execute(user.document, user.email);
|
return this.resetPasswordService.execute(user.document, user.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
async changePassword(user: { id: number; password: string; newPassword: string }) {
|
async changePassword(user: {
|
||||||
return this.changePasswordService.execute(user.id, user.password, user.newPassword);
|
id: number;
|
||||||
|
password: string;
|
||||||
|
newPassword: string;
|
||||||
|
}) {
|
||||||
|
return this.changePasswordService.execute(
|
||||||
|
user.id,
|
||||||
|
user.password,
|
||||||
|
user.newPassword,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
export class RateLimiterMiddleware implements NestMiddleware {
|
export class RateLimiterMiddleware implements NestMiddleware {
|
||||||
private readonly ttl: number;
|
private readonly ttl: number;
|
||||||
private readonly limit: number;
|
private readonly limit: number;
|
||||||
private readonly store: Map<string, { count: number; expiration: number }> = new Map();
|
private readonly store: Map<string, { count: number; expiration: number }> =
|
||||||
|
new Map();
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {
|
constructor(private configService: ConfigService) {
|
||||||
this.ttl = this.configService.get<number>('THROTTLE_TTL', 60);
|
this.ttl = this.configService.get<number>('THROTTLE_TTL', 60);
|
||||||
@@ -42,7 +43,9 @@ export class RateLimiterMiddleware implements NestMiddleware {
|
|||||||
const timeToWait = Math.ceil((record.expiration - now) / 1000);
|
const timeToWait = Math.ceil((record.expiration - now) / 1000);
|
||||||
this.setRateLimitHeaders(res, record.count);
|
this.setRateLimitHeaders(res, record.count);
|
||||||
res.header('Retry-After', String(timeToWait));
|
res.header('Retry-After', String(timeToWait));
|
||||||
throw new ThrottlerException(`Too Many Requests. Retry after ${timeToWait} seconds.`);
|
throw new ThrottlerException(
|
||||||
|
`Too Many Requests. Retry after ${timeToWait} seconds.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
record.count++;
|
record.count++;
|
||||||
@@ -52,13 +55,17 @@ export class RateLimiterMiddleware implements NestMiddleware {
|
|||||||
|
|
||||||
private generateKey(req: Request): string {
|
private generateKey(req: Request): string {
|
||||||
// Combina IP com rota para rate limiting mais preciso
|
// Combina IP com rota para rate limiting mais preciso
|
||||||
const ip = req.ip || req.headers['x-forwarded-for'] as string || 'unknown-ip';
|
const ip =
|
||||||
|
req.ip || (req.headers['x-forwarded-for'] as string) || 'unknown-ip';
|
||||||
const path = req.path || req.originalUrl || '';
|
const path = req.path || req.originalUrl || '';
|
||||||
return `${ip}:${path}`;
|
return `${ip}:${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setRateLimitHeaders(res: Response, count: number): void {
|
private setRateLimitHeaders(res: Response, count: number): void {
|
||||||
res.header('X-RateLimit-Limit', String(this.limit));
|
res.header('X-RateLimit-Limit', String(this.limit));
|
||||||
res.header('X-RateLimit-Remaining', String(Math.max(0, this.limit - count)));
|
res.header(
|
||||||
|
'X-RateLimit-Remaining',
|
||||||
|
String(Math.max(0, this.limit - count)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ export class RequestSanitizerMiddleware implements NestMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeObject(obj: any) {
|
private sanitizeObject(obj: any) {
|
||||||
Object.keys(obj).forEach(key => {
|
Object.keys(obj).forEach((key) => {
|
||||||
if (typeof obj[key] === 'string') {
|
if (typeof obj[key] === 'string') {
|
||||||
obj[key] = this.sanitizeString(obj[key]);
|
obj[key] = this.sanitizeString(obj[key]);
|
||||||
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import {
|
import {
|
||||||
CallHandler,
|
CallHandler,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
Injectable,
|
Injectable,
|
||||||
NestInterceptor,
|
NestInterceptor,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { ResultModel } from '../shared/ResultModel';
|
import { ResultModel } from '../shared/ResultModel';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResponseInterceptor<T> implements NestInterceptor<T, ResultModel<T>> {
|
export class ResponseInterceptor<T>
|
||||||
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<ResultModel<T>> {
|
implements NestInterceptor<T, ResultModel<T>>
|
||||||
return next.handle().pipe(
|
{
|
||||||
map((data) => {
|
intercept(
|
||||||
return ResultModel.success(data);
|
context: ExecutionContext,
|
||||||
}),
|
next: CallHandler<T>,
|
||||||
);
|
): Observable<ResultModel<T>> {
|
||||||
}
|
return next.handle().pipe(
|
||||||
|
map((data) => {
|
||||||
|
return ResultModel.success(data);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';
|
import {
|
||||||
|
registerDecorator,
|
||||||
|
ValidationOptions,
|
||||||
|
ValidationArguments,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
// Decorator para sanitizar strings e prevenir SQL/NoSQL injection
|
// Decorator para sanitizar strings e prevenir SQL/NoSQL injection
|
||||||
export function IsSanitized(validationOptions?: ValidationOptions) {
|
export function IsSanitized(validationOptions?: ValidationOptions) {
|
||||||
return function (object: Object, propertyName: string) {
|
return function (object: object, propertyName: string) {
|
||||||
registerDecorator({
|
registerDecorator({
|
||||||
name: 'isSanitized',
|
name: 'isSanitized',
|
||||||
target: object.constructor,
|
target: object.constructor,
|
||||||
@@ -12,19 +16,22 @@ export function IsSanitized(validationOptions?: ValidationOptions) {
|
|||||||
validate(value: any, args: ValidationArguments) {
|
validate(value: any, args: ValidationArguments) {
|
||||||
if (typeof value !== 'string') return true; // Skip non-string values
|
if (typeof value !== 'string') return true; // Skip non-string values
|
||||||
|
|
||||||
const sqlInjectionRegex = /('|"|;|--|\/\*|\*\/|@@|@|char|nchar|varchar|nvarchar|alter|begin|cast|create|cursor|declare|delete|drop|end|exec|execute|fetch|insert|kill|open|select|sys|sysobjects|syscolumns|table|update|xp_)/i;
|
const sqlInjectionRegex =
|
||||||
|
/('|"|;|--|\/\*|\*\/|@@|@|char|nchar|varchar|nvarchar|alter|begin|cast|create|cursor|declare|delete|drop|end|exec|execute|fetch|insert|kill|open|select|sys|sysobjects|syscolumns|table|update|xp_)/i;
|
||||||
if (sqlInjectionRegex.test(value)) {
|
if (sqlInjectionRegex.test(value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for NoSQL injection patterns (MongoDB)
|
// Check for NoSQL injection patterns (MongoDB)
|
||||||
const noSqlInjectionRegex = /(\$where|\$ne|\$gt|\$lt|\$gte|\$lte|\$in|\$nin|\$or|\$and|\$regex|\$options|\$elemMatch|\{.*\:.*\})/i;
|
const noSqlInjectionRegex =
|
||||||
|
/(\$where|\$ne|\$gt|\$lt|\$gte|\$lte|\$in|\$nin|\$or|\$and|\$regex|\$options|\$elemMatch|\{.*\:.*\})/i;
|
||||||
if (noSqlInjectionRegex.test(value)) {
|
if (noSqlInjectionRegex.test(value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for XSS attempts
|
// Check for XSS attempts
|
||||||
const xssRegex = /(<script|javascript:|on\w+\s*=|<%=|<img|<iframe|alert\(|window\.|document\.)/i;
|
const xssRegex =
|
||||||
|
/(<script|javascript:|on\w+\s*=|<%=|<img|<iframe|alert\(|window\.|document\.)/i;
|
||||||
if (xssRegex.test(value)) {
|
if (xssRegex.test(value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -41,7 +48,7 @@ export function IsSanitized(validationOptions?: ValidationOptions) {
|
|||||||
|
|
||||||
// Decorator para validar IDs seguros (evita injeção em IDs)
|
// Decorator para validar IDs seguros (evita injeção em IDs)
|
||||||
export function IsSecureId(validationOptions?: ValidationOptions) {
|
export function IsSecureId(validationOptions?: ValidationOptions) {
|
||||||
return function (object: Object, propertyName: string) {
|
return function (object: object, propertyName: string) {
|
||||||
registerDecorator({
|
registerDecorator({
|
||||||
name: 'isSecureId',
|
name: 'isSecureId',
|
||||||
target: object.constructor,
|
target: object.constructor,
|
||||||
@@ -49,11 +56,14 @@ export function IsSecureId(validationOptions?: ValidationOptions) {
|
|||||||
options: validationOptions,
|
options: validationOptions,
|
||||||
validator: {
|
validator: {
|
||||||
validate(value: any, args: ValidationArguments) {
|
validate(value: any, args: ValidationArguments) {
|
||||||
if (typeof value !== 'string' && typeof value !== 'number') return false;
|
if (typeof value !== 'string' && typeof value !== 'number')
|
||||||
|
return false;
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
// Permitir apenas: letras, números, hífens, underscores e GUIDs
|
// Permitir apenas: letras, números, hífens, underscores e GUIDs
|
||||||
return /^[a-zA-Z0-9\-_]+$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
return /^[a-zA-Z0-9\-_]+$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||||
|
value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se for número, deve ser positivo
|
// Se for número, deve ser positivo
|
||||||
|
|||||||
21
src/core/configs/cache/IRedisClient.ts
vendored
21
src/core/configs/cache/IRedisClient.ts
vendored
@@ -1,10 +1,13 @@
|
|||||||
export interface IRedisClient {
|
export interface IRedisClient {
|
||||||
get<T>(key: string): Promise<T | null>;
|
get<T>(key: string): Promise<T | null>;
|
||||||
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
||||||
del(key: string): Promise<void>;
|
del(key: string): Promise<void>;
|
||||||
del(...keys: string[]): Promise<void>;
|
del(...keys: string[]): Promise<void>;
|
||||||
keys(pattern: string): Promise<string[]>;
|
keys(pattern: string): Promise<string[]>;
|
||||||
ttl(key: string): Promise<number>;
|
ttl(key: string): Promise<number>;
|
||||||
eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise<any>;
|
eval(
|
||||||
}
|
script: string,
|
||||||
|
numKeys: number,
|
||||||
|
...keysAndArgs: (string | number)[]
|
||||||
|
): Promise<any>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { RedisClientAdapter } from './redis-client.adapter';
|
import { RedisClientAdapter } from './redis-client.adapter';
|
||||||
export const RedisClientToken = 'RedisClientInterface';
|
export const RedisClientToken = 'RedisClientInterface';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { IRedisClient } from './IRedisClient';
|
|||||||
export class RedisClientAdapter implements IRedisClient {
|
export class RedisClientAdapter implements IRedisClient {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('REDIS_CLIENT')
|
@Inject('REDIS_CLIENT')
|
||||||
private readonly redis: Redis
|
private readonly redis: Redis,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async get<T>(key: string): Promise<T | null> {
|
async get<T>(key: string): Promise<T | null> {
|
||||||
@@ -43,7 +43,11 @@ export class RedisClientAdapter implements IRedisClient {
|
|||||||
return this.redis.ttl(key);
|
return this.redis.ttl(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise<any> {
|
async eval(
|
||||||
|
script: string,
|
||||||
|
numKeys: number,
|
||||||
|
...keysAndArgs: (string | number)[]
|
||||||
|
): Promise<any> {
|
||||||
return this.redis.eval(script, numKeys, ...keysAndArgs);
|
return this.redis.eval(script, numKeys, ...keysAndArgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/core/configs/cache/redis.module.ts
vendored
2
src/core/configs/cache/redis.module.ts
vendored
@@ -9,4 +9,4 @@ import { RedisClientAdapterProvider } from './redis-client.adapter.provider';
|
|||||||
providers: [RedisProvider, RedisClientAdapterProvider],
|
providers: [RedisProvider, RedisClientAdapterProvider],
|
||||||
exports: [RedisProvider, RedisClientAdapterProvider],
|
exports: [RedisProvider, RedisClientAdapterProvider],
|
||||||
})
|
})
|
||||||
export class RedisModule {}
|
export class RedisModule {}
|
||||||
|
|||||||
36
src/core/configs/cache/redis.provider.ts
vendored
36
src/core/configs/cache/redis.provider.ts
vendored
@@ -1,21 +1,21 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
import { Provider } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
export const RedisProvider: Provider = {
|
export const RedisProvider: Provider = {
|
||||||
provide: 'REDIS_CLIENT',
|
provide: 'REDIS_CLIENT',
|
||||||
useFactory: (configService: ConfigService) => {
|
useFactory: (configService: ConfigService) => {
|
||||||
const redis = new Redis({
|
const redis = new Redis({
|
||||||
host: configService.get<string>('REDIS_HOST', '10.1.1.109'),
|
host: configService.get<string>('REDIS_HOST', '10.1.1.109'),
|
||||||
port: configService.get<number>('REDIS_PORT', 6379),
|
port: configService.get<number>('REDIS_PORT', 6379),
|
||||||
password: configService.get<string>('REDIS_PASSWORD', '1234'),
|
password: configService.get<string>('REDIS_PASSWORD', '1234'),
|
||||||
});
|
});
|
||||||
|
|
||||||
redis.on('error', (err) => {
|
redis.on('error', (err) => {
|
||||||
console.error('Erro ao conectar ao Redis:', err);
|
console.error('Erro ao conectar ao Redis:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
return redis;
|
return redis;
|
||||||
},
|
},
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { DataSourceOptions } from 'typeorm';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as oracledb from 'oracledb';
|
import * as oracledb from 'oracledb';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
oracledb.initOracleClient({ libDir: process.env.ORACLE_CLIENT_LIB_DIR });
|
oracledb.initOracleClient({ libDir: process.env.ORACLE_CLIENT_LIB_DIR });
|
||||||
|
|
||||||
// Definir a estratégia de pool padrão para Oracle
|
// Definir a estratégia de pool padrão para Oracle
|
||||||
@@ -12,19 +10,22 @@ oracledb.queueTimeout = 60000; // timeout da fila em milissegundos
|
|||||||
oracledb.poolIncrement = 1; // incremental de conexões
|
oracledb.poolIncrement = 1; // incremental de conexões
|
||||||
|
|
||||||
export function createOracleConfig(config: ConfigService): DataSourceOptions {
|
export function createOracleConfig(config: ConfigService): DataSourceOptions {
|
||||||
|
|
||||||
const poolMin = parseInt(config.get('ORACLE_POOL_MIN', '5'));
|
const poolMin = parseInt(config.get('ORACLE_POOL_MIN', '5'));
|
||||||
const poolMax = parseInt(config.get('ORACLE_POOL_MAX', '20'));
|
const poolMax = parseInt(config.get('ORACLE_POOL_MAX', '20'));
|
||||||
const poolIncrement = parseInt(config.get('ORACLE_POOL_INCREMENT', '5'));
|
const poolIncrement = parseInt(config.get('ORACLE_POOL_INCREMENT', '5'));
|
||||||
const poolTimeout = parseInt(config.get('ORACLE_POOL_TIMEOUT', '30000'));
|
const poolTimeout = parseInt(config.get('ORACLE_POOL_TIMEOUT', '30000'));
|
||||||
const idleTimeout = parseInt(config.get('ORACLE_POOL_IDLE_TIMEOUT', '300000'));
|
const idleTimeout = parseInt(
|
||||||
|
config.get('ORACLE_POOL_IDLE_TIMEOUT', '300000'),
|
||||||
|
);
|
||||||
|
|
||||||
const validPoolMin = Math.max(1, poolMin);
|
const validPoolMin = Math.max(1, poolMin);
|
||||||
const validPoolMax = Math.max(validPoolMin + 1, poolMax);
|
const validPoolMax = Math.max(validPoolMin + 1, poolMax);
|
||||||
const validPoolIncrement = Math.max(1, poolIncrement);
|
const validPoolIncrement = Math.max(1, poolIncrement);
|
||||||
|
|
||||||
if (validPoolMax <= validPoolMin) {
|
if (validPoolMax <= validPoolMin) {
|
||||||
console.warn('Warning: poolMax deve ser maior que poolMin. Ajustando poolMax para poolMin + 1');
|
console.warn(
|
||||||
|
'Warning: poolMax deve ser maior que poolMin. Ajustando poolMax para poolMin + 1',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: DataSourceOptions = {
|
const options: DataSourceOptions = {
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ export function createPostgresConfig(config: ConfigService): DataSourceOptions {
|
|||||||
// Obter configurações de ambiente ou usar valores padrão
|
// Obter configurações de ambiente ou usar valores padrão
|
||||||
const poolMin = parseInt(config.get('POSTGRES_POOL_MIN', '5'));
|
const poolMin = parseInt(config.get('POSTGRES_POOL_MIN', '5'));
|
||||||
const poolMax = parseInt(config.get('POSTGRES_POOL_MAX', '20'));
|
const poolMax = parseInt(config.get('POSTGRES_POOL_MAX', '20'));
|
||||||
const idleTimeout = parseInt(config.get('POSTGRES_POOL_IDLE_TIMEOUT', '30000'));
|
const idleTimeout = parseInt(
|
||||||
const connectionTimeout = parseInt(config.get('POSTGRES_POOL_CONNECTION_TIMEOUT', '5000'));
|
config.get('POSTGRES_POOL_IDLE_TIMEOUT', '30000'),
|
||||||
const acquireTimeout = parseInt(config.get('POSTGRES_POOL_ACQUIRE_TIMEOUT', '60000'));
|
);
|
||||||
|
const connectionTimeout = parseInt(
|
||||||
|
config.get('POSTGRES_POOL_CONNECTION_TIMEOUT', '5000'),
|
||||||
|
);
|
||||||
|
const acquireTimeout = parseInt(
|
||||||
|
config.get('POSTGRES_POOL_ACQUIRE_TIMEOUT', '60000'),
|
||||||
|
);
|
||||||
|
|
||||||
// Validação de valores mínimos
|
// Validação de valores mínimos
|
||||||
const validPoolMin = Math.max(1, poolMin);
|
const validPoolMin = Math.max(1, poolMin);
|
||||||
@@ -25,7 +31,10 @@ export function createPostgresConfig(config: ConfigService): DataSourceOptions {
|
|||||||
database: config.get('POSTGRES_DB'),
|
database: config.get('POSTGRES_DB'),
|
||||||
synchronize: config.get('NODE_ENV') === 'development',
|
synchronize: config.get('NODE_ENV') === 'development',
|
||||||
entities: [__dirname + '/../**/*.entity.{ts,js}'],
|
entities: [__dirname + '/../**/*.entity.{ts,js}'],
|
||||||
ssl: config.get('NODE_ENV') === 'production' ? { rejectUnauthorized: false } : false,
|
ssl:
|
||||||
|
config.get('NODE_ENV') === 'production'
|
||||||
|
? { rejectUnauthorized: false }
|
||||||
|
: false,
|
||||||
logging: config.get('NODE_ENV') === 'development',
|
logging: config.get('NODE_ENV') === 'development',
|
||||||
poolSize: validPoolMax, // máximo de conexões no pool
|
poolSize: validPoolMax, // máximo de conexões no pool
|
||||||
extra: {
|
extra: {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
/*
|
|
||||||
https://docs.nestjs.com/controllers#controllers
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Controller } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class NegotiationsController { }
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
/*
|
|
||||||
https://docs.nestjs.com/modules
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { NegotiationsController } from './negotiations.controller';
|
|
||||||
import { NegotiationsService } from './negotiations.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
controllers: [
|
|
||||||
NegotiationsController,],
|
|
||||||
providers: [
|
|
||||||
NegotiationsService,],
|
|
||||||
})
|
|
||||||
export class NegotiationsModule { }
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
/*
|
|
||||||
https://docs.nestjs.com/providers#services
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class NegotiationsService { }
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
/*
|
|
||||||
https://docs.nestjs.com/modules
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { OccurrencesService } from './occurrences.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
controllers: [],
|
|
||||||
providers: [
|
|
||||||
OccurrencesService,],
|
|
||||||
})
|
|
||||||
export class OccurrencesModule { }
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
/*
|
|
||||||
https://docs.nestjs.com/providers#services
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class OccurrencesService { }
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
/*
|
|
||||||
https://docs.nestjs.com/controllers#controllers
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Controller } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class OcorrencesController { }
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
/*
|
|
||||||
https://docs.nestjs.com/controllers#controllers
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
|
||||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
|
||||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
|
||||||
|
|
||||||
@ApiTags('CRM - Reason Table')
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Controller('api/v1/crm/reason')
|
|
||||||
export class ReasonTableController {
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
async getReasons() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
async createReasons() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Put('/:id')
|
|
||||||
async updateReasons(@Param('id') id: number) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('/:id')
|
|
||||||
async deleteReasons(@Param('id') id: number) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
/*
|
|
||||||
https://docs.nestjs.com/modules
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ReasonTableController } from './reason-table.controller';
|
|
||||||
import { ReasonTableService } from './reason-table.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
controllers: [
|
|
||||||
ReasonTableController,],
|
|
||||||
providers: [
|
|
||||||
ReasonTableService,],
|
|
||||||
})
|
|
||||||
export class ReasonTableModule { }
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars *//*
|
|
||||||
|
|
||||||
https://docs.nestjs.com/providers#services
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ReasonTableService { }
|
|
||||||
@@ -1,50 +1,47 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { DataConsultService } from '../data-consult.service';
|
import { DataConsultService } from '../data-consult.service';
|
||||||
import { DataConsultRepository } from '../data-consult.repository';
|
import { DataConsultRepository } from '../data-consult.repository';
|
||||||
import { ILogger } from '../../Log/ILogger';
|
|
||||||
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||||
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { DATA_SOURCE } from '../../core/constants';
|
import { DATA_SOURCE } from '../../core/constants';
|
||||||
|
|
||||||
export const createMockRepository = (methods: Partial<DataConsultRepository> = {}) => ({
|
export const createMockRepository = (
|
||||||
findStores: jest.fn(),
|
methods: Partial<DataConsultRepository> = {},
|
||||||
findSellers: jest.fn(),
|
) =>
|
||||||
findBillings: jest.fn(),
|
({
|
||||||
findCustomers: jest.fn(),
|
findStores: jest.fn(),
|
||||||
findAllProducts: jest.fn(),
|
findSellers: jest.fn(),
|
||||||
findAllCarriers: jest.fn(),
|
findBillings: jest.fn(),
|
||||||
findRegions: jest.fn(),
|
findCustomers: jest.fn(),
|
||||||
...methods,
|
findAllProducts: jest.fn(),
|
||||||
} as any);
|
findAllCarriers: jest.fn(),
|
||||||
|
findRegions: jest.fn(),
|
||||||
|
...methods,
|
||||||
|
} as any);
|
||||||
|
|
||||||
export const createMockLogger = () => ({
|
export const createMockRedisClient = () =>
|
||||||
log: jest.fn(),
|
({
|
||||||
error: jest.fn(),
|
get: jest.fn().mockResolvedValue(null),
|
||||||
warn: jest.fn(),
|
set: jest.fn().mockResolvedValue(undefined),
|
||||||
debug: jest.fn(),
|
} as any);
|
||||||
} as any);
|
|
||||||
|
|
||||||
export const createMockRedisClient = () => ({
|
|
||||||
get: jest.fn().mockResolvedValue(null),
|
|
||||||
set: jest.fn().mockResolvedValue(undefined),
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
export interface DataConsultServiceTestContext {
|
export interface DataConsultServiceTestContext {
|
||||||
service: DataConsultService;
|
service: DataConsultService;
|
||||||
mockRepository: jest.Mocked<DataConsultRepository>;
|
mockRepository: jest.Mocked<DataConsultRepository>;
|
||||||
mockLogger: jest.Mocked<ILogger>;
|
|
||||||
mockRedisClient: jest.Mocked<IRedisClient>;
|
mockRedisClient: jest.Mocked<IRedisClient>;
|
||||||
mockDataSource: jest.Mocked<DataSource>;
|
mockDataSource: jest.Mocked<DataSource>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDataConsultServiceTestModule(
|
export async function createDataConsultServiceTestModule(
|
||||||
repositoryMethods: Partial<DataConsultRepository> = {},
|
repositoryMethods: Partial<DataConsultRepository> = {},
|
||||||
redisClientMethods: Partial<IRedisClient> = {}
|
redisClientMethods: Partial<IRedisClient> = {},
|
||||||
): Promise<DataConsultServiceTestContext> {
|
): Promise<DataConsultServiceTestContext> {
|
||||||
const mockRepository = createMockRepository(repositoryMethods);
|
const mockRepository = createMockRepository(repositoryMethods);
|
||||||
const mockLogger = createMockLogger();
|
const mockRedisClient = {
|
||||||
const mockRedisClient = { ...createMockRedisClient(), ...redisClientMethods } as any;
|
...createMockRedisClient(),
|
||||||
|
...redisClientMethods,
|
||||||
|
} as any;
|
||||||
const mockDataSource = {} as any;
|
const mockDataSource = {} as any;
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@@ -58,10 +55,6 @@ export async function createDataConsultServiceTestModule(
|
|||||||
provide: RedisClientToken,
|
provide: RedisClientToken,
|
||||||
useValue: mockRedisClient,
|
useValue: mockRedisClient,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: 'LoggerService',
|
|
||||||
useValue: mockLogger,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: DATA_SOURCE,
|
provide: DATA_SOURCE,
|
||||||
useValue: mockDataSource,
|
useValue: mockDataSource,
|
||||||
@@ -74,9 +67,7 @@ export async function createDataConsultServiceTestModule(
|
|||||||
return {
|
return {
|
||||||
service,
|
service,
|
||||||
mockRepository,
|
mockRepository,
|
||||||
mockLogger,
|
|
||||||
mockRedisClient,
|
mockRedisClient,
|
||||||
mockDataSource,
|
mockDataSource,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe('DataConsultService', () => {
|
|||||||
|
|
||||||
const result = await context.service.stores();
|
const result = await context.service.stores();
|
||||||
|
|
||||||
result.forEach(store => {
|
result.forEach((store) => {
|
||||||
expect(store.id).toBeDefined();
|
expect(store.id).toBeDefined();
|
||||||
expect(store.name).toBeDefined();
|
expect(store.name).toBeDefined();
|
||||||
expect(store.store).toBeDefined();
|
expect(store.store).toBeDefined();
|
||||||
@@ -36,7 +36,10 @@ describe('DataConsultService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that repository result is an array', async () => {
|
it('should validate that repository result is an array', async () => {
|
||||||
context.mockRepository.findStores.mockResolvedValue({ id: '001', name: 'Loja 1' } as any);
|
context.mockRepository.findStores.mockResolvedValue({
|
||||||
|
id: '001',
|
||||||
|
name: 'Loja 1',
|
||||||
|
} as any);
|
||||||
const result = await context.service.stores();
|
const result = await context.service.stores();
|
||||||
expect(Array.isArray(result)).toBe(true);
|
expect(Array.isArray(result)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -49,7 +52,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.stores();
|
const result = await context.service.stores();
|
||||||
result.forEach(store => {
|
result.forEach((store) => {
|
||||||
expect(store.id).not.toBe('');
|
expect(store.id).not.toBe('');
|
||||||
expect(store.name).not.toBe('');
|
expect(store.name).not.toBe('');
|
||||||
expect(store.store).not.toBe('');
|
expect(store.store).not.toBe('');
|
||||||
@@ -60,7 +63,10 @@ describe('DataConsultService', () => {
|
|||||||
const repositoryError = new Error('Database connection failed');
|
const repositoryError = new Error('Database connection failed');
|
||||||
context.mockRepository.findStores.mockRejectedValue(repositoryError);
|
context.mockRepository.findStores.mockRejectedValue(repositoryError);
|
||||||
await expect(context.service.stores()).rejects.toThrow(HttpException);
|
await expect(context.service.stores()).rejects.toThrow(HttpException);
|
||||||
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar lojas', repositoryError);
|
expect(context.mockLogger.error).toHaveBeenCalledWith(
|
||||||
|
'Erro ao buscar lojas',
|
||||||
|
repositoryError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -85,7 +91,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.sellers();
|
const result = await context.service.sellers();
|
||||||
result.forEach(seller => {
|
result.forEach((seller) => {
|
||||||
expect(seller.id).toBeDefined();
|
expect(seller.id).toBeDefined();
|
||||||
expect(seller.name).toBeDefined();
|
expect(seller.name).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -97,7 +103,10 @@ describe('DataConsultService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that repository result is an array', async () => {
|
it('should validate that repository result is an array', async () => {
|
||||||
context.mockRepository.findSellers.mockResolvedValue({ id: '001', name: 'Vendedor 1' } as any);
|
context.mockRepository.findSellers.mockResolvedValue({
|
||||||
|
id: '001',
|
||||||
|
name: 'Vendedor 1',
|
||||||
|
} as any);
|
||||||
const result = await context.service.sellers();
|
const result = await context.service.sellers();
|
||||||
expect(Array.isArray(result)).toBe(true);
|
expect(Array.isArray(result)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -109,7 +118,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.sellers();
|
const result = await context.service.sellers();
|
||||||
result.forEach(seller => {
|
result.forEach((seller) => {
|
||||||
expect(seller.id).not.toBe('');
|
expect(seller.id).not.toBe('');
|
||||||
expect(seller.name).not.toBe('');
|
expect(seller.name).not.toBe('');
|
||||||
});
|
});
|
||||||
@@ -119,7 +128,10 @@ describe('DataConsultService', () => {
|
|||||||
const repositoryError = new Error('Database connection failed');
|
const repositoryError = new Error('Database connection failed');
|
||||||
context.mockRepository.findSellers.mockRejectedValue(repositoryError);
|
context.mockRepository.findSellers.mockRejectedValue(repositoryError);
|
||||||
await expect(context.service.sellers()).rejects.toThrow(HttpException);
|
await expect(context.service.sellers()).rejects.toThrow(HttpException);
|
||||||
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar vendedores', repositoryError);
|
expect(context.mockLogger.error).toHaveBeenCalledWith(
|
||||||
|
'Erro ao buscar vendedores',
|
||||||
|
repositoryError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -144,7 +156,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.billings();
|
const result = await context.service.billings();
|
||||||
result.forEach(billing => {
|
result.forEach((billing) => {
|
||||||
expect(billing.id).toBeDefined();
|
expect(billing.id).toBeDefined();
|
||||||
expect(billing.date).toBeDefined();
|
expect(billing.date).toBeDefined();
|
||||||
expect(billing.total).toBeDefined();
|
expect(billing.total).toBeDefined();
|
||||||
@@ -157,7 +169,11 @@ describe('DataConsultService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that repository result is an array', async () => {
|
it('should validate that repository result is an array', async () => {
|
||||||
context.mockRepository.findBillings.mockResolvedValue({ id: '001', date: new Date(), total: 1000 } as any);
|
context.mockRepository.findBillings.mockResolvedValue({
|
||||||
|
id: '001',
|
||||||
|
date: new Date(),
|
||||||
|
total: 1000,
|
||||||
|
} as any);
|
||||||
const result = await context.service.billings();
|
const result = await context.service.billings();
|
||||||
expect(Array.isArray(result)).toBe(true);
|
expect(Array.isArray(result)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -170,7 +186,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.billings();
|
const result = await context.service.billings();
|
||||||
result.forEach(billing => {
|
result.forEach((billing) => {
|
||||||
expect(billing.id).not.toBe('');
|
expect(billing.id).not.toBe('');
|
||||||
expect(billing.date).toBeDefined();
|
expect(billing.date).toBeDefined();
|
||||||
expect(billing.total).toBeDefined();
|
expect(billing.total).toBeDefined();
|
||||||
@@ -181,7 +197,10 @@ describe('DataConsultService', () => {
|
|||||||
const repositoryError = new Error('Database connection failed');
|
const repositoryError = new Error('Database connection failed');
|
||||||
context.mockRepository.findBillings.mockRejectedValue(repositoryError);
|
context.mockRepository.findBillings.mockRejectedValue(repositoryError);
|
||||||
await expect(context.service.billings()).rejects.toThrow(HttpException);
|
await expect(context.service.billings()).rejects.toThrow(HttpException);
|
||||||
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar faturamento', repositoryError);
|
expect(context.mockLogger.error).toHaveBeenCalledWith(
|
||||||
|
'Erro ao buscar faturamento',
|
||||||
|
repositoryError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -206,7 +225,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.customers('test');
|
const result = await context.service.customers('test');
|
||||||
result.forEach(customer => {
|
result.forEach((customer) => {
|
||||||
expect(customer.id).toBeDefined();
|
expect(customer.id).toBeDefined();
|
||||||
expect(customer.name).toBeDefined();
|
expect(customer.name).toBeDefined();
|
||||||
expect(customer.document).toBeDefined();
|
expect(customer.document).toBeDefined();
|
||||||
@@ -219,7 +238,11 @@ describe('DataConsultService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that repository result is an array', async () => {
|
it('should validate that repository result is an array', async () => {
|
||||||
context.mockRepository.findCustomers.mockResolvedValue({ id: '001', name: 'Cliente 1', document: '12345678900' } as any);
|
context.mockRepository.findCustomers.mockResolvedValue({
|
||||||
|
id: '001',
|
||||||
|
name: 'Cliente 1',
|
||||||
|
document: '12345678900',
|
||||||
|
} as any);
|
||||||
const result = await context.service.customers('test');
|
const result = await context.service.customers('test');
|
||||||
expect(Array.isArray(result)).toBe(true);
|
expect(Array.isArray(result)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -232,7 +255,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.customers('test');
|
const result = await context.service.customers('test');
|
||||||
result.forEach(customer => {
|
result.forEach((customer) => {
|
||||||
expect(customer.id).not.toBe('');
|
expect(customer.id).not.toBe('');
|
||||||
expect(customer.name).not.toBe('');
|
expect(customer.name).not.toBe('');
|
||||||
expect(customer.document).not.toBe('');
|
expect(customer.document).not.toBe('');
|
||||||
@@ -242,8 +265,13 @@ describe('DataConsultService', () => {
|
|||||||
it('should log error when repository throws exception', async () => {
|
it('should log error when repository throws exception', async () => {
|
||||||
const repositoryError = new Error('Database connection failed');
|
const repositoryError = new Error('Database connection failed');
|
||||||
context.mockRepository.findCustomers.mockRejectedValue(repositoryError);
|
context.mockRepository.findCustomers.mockRejectedValue(repositoryError);
|
||||||
await expect(context.service.customers('test')).rejects.toThrow(HttpException);
|
await expect(context.service.customers('test')).rejects.toThrow(
|
||||||
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar clientes', repositoryError);
|
HttpException,
|
||||||
|
);
|
||||||
|
expect(context.mockLogger.error).toHaveBeenCalledWith(
|
||||||
|
'Erro ao buscar clientes',
|
||||||
|
repositoryError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -268,7 +296,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.getAllProducts();
|
const result = await context.service.getAllProducts();
|
||||||
result.forEach(product => {
|
result.forEach((product) => {
|
||||||
expect(product.id).toBeDefined();
|
expect(product.id).toBeDefined();
|
||||||
expect(product.name).toBeDefined();
|
expect(product.name).toBeDefined();
|
||||||
expect(product.manufacturerCode).toBeDefined();
|
expect(product.manufacturerCode).toBeDefined();
|
||||||
@@ -281,7 +309,11 @@ describe('DataConsultService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that repository result is an array', async () => {
|
it('should validate that repository result is an array', async () => {
|
||||||
context.mockRepository.findAllProducts.mockResolvedValue({ id: '001', name: 'Produto 1', manufacturerCode: 'FAB001' } as any);
|
context.mockRepository.findAllProducts.mockResolvedValue({
|
||||||
|
id: '001',
|
||||||
|
name: 'Produto 1',
|
||||||
|
manufacturerCode: 'FAB001',
|
||||||
|
} as any);
|
||||||
const result = await context.service.getAllProducts();
|
const result = await context.service.getAllProducts();
|
||||||
expect(Array.isArray(result)).toBe(true);
|
expect(Array.isArray(result)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -294,7 +326,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.getAllProducts();
|
const result = await context.service.getAllProducts();
|
||||||
result.forEach(product => {
|
result.forEach((product) => {
|
||||||
expect(product.id).not.toBe('');
|
expect(product.id).not.toBe('');
|
||||||
expect(product.name).not.toBe('');
|
expect(product.name).not.toBe('');
|
||||||
expect(product.manufacturerCode).not.toBe('');
|
expect(product.manufacturerCode).not.toBe('');
|
||||||
@@ -303,9 +335,16 @@ describe('DataConsultService', () => {
|
|||||||
|
|
||||||
it('should log error when repository throws exception', async () => {
|
it('should log error when repository throws exception', async () => {
|
||||||
const repositoryError = new Error('Database connection failed');
|
const repositoryError = new Error('Database connection failed');
|
||||||
context.mockRepository.findAllProducts.mockRejectedValue(repositoryError);
|
context.mockRepository.findAllProducts.mockRejectedValue(
|
||||||
await expect(context.service.getAllProducts()).rejects.toThrow(HttpException);
|
repositoryError,
|
||||||
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar todos os produtos', repositoryError);
|
);
|
||||||
|
await expect(context.service.getAllProducts()).rejects.toThrow(
|
||||||
|
HttpException,
|
||||||
|
);
|
||||||
|
expect(context.mockLogger.error).toHaveBeenCalledWith(
|
||||||
|
'Erro ao buscar todos os produtos',
|
||||||
|
repositoryError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -325,12 +364,19 @@ describe('DataConsultService', () => {
|
|||||||
it('should validate that all carriers have required properties (carrierId, carrierName, carrierDescription)', async () => {
|
it('should validate that all carriers have required properties (carrierId, carrierName, carrierDescription)', async () => {
|
||||||
context.mockRepository.findAllCarriers.mockResolvedValue([
|
context.mockRepository.findAllCarriers.mockResolvedValue([
|
||||||
{ carrierId: '001', carrierName: 'Transportadora 1' },
|
{ carrierId: '001', carrierName: 'Transportadora 1' },
|
||||||
{ carrierName: 'Transportadora 2', carrierDescription: '002 - Transportadora 2' },
|
{
|
||||||
{ carrierId: '003', carrierName: 'Transportadora 3', carrierDescription: '003 - Transportadora 3' },
|
carrierName: 'Transportadora 2',
|
||||||
|
carrierDescription: '002 - Transportadora 2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carrierId: '003',
|
||||||
|
carrierName: 'Transportadora 3',
|
||||||
|
carrierDescription: '003 - Transportadora 3',
|
||||||
|
},
|
||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.getAllCarriers();
|
const result = await context.service.getAllCarriers();
|
||||||
result.forEach(carrier => {
|
result.forEach((carrier) => {
|
||||||
expect(carrier.carrierId).toBeDefined();
|
expect(carrier.carrierId).toBeDefined();
|
||||||
expect(carrier.carrierName).toBeDefined();
|
expect(carrier.carrierName).toBeDefined();
|
||||||
expect(carrier.carrierDescription).toBeDefined();
|
expect(carrier.carrierDescription).toBeDefined();
|
||||||
@@ -343,20 +389,36 @@ describe('DataConsultService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that repository result is an array', async () => {
|
it('should validate that repository result is an array', async () => {
|
||||||
context.mockRepository.findAllCarriers.mockResolvedValue({ carrierId: '001', carrierName: 'Transportadora 1', carrierDescription: '001 - Transportadora 1' } as any);
|
context.mockRepository.findAllCarriers.mockResolvedValue({
|
||||||
|
carrierId: '001',
|
||||||
|
carrierName: 'Transportadora 1',
|
||||||
|
carrierDescription: '001 - Transportadora 1',
|
||||||
|
} as any);
|
||||||
const result = await context.service.getAllCarriers();
|
const result = await context.service.getAllCarriers();
|
||||||
expect(Array.isArray(result)).toBe(true);
|
expect(Array.isArray(result)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that required properties are not empty strings', async () => {
|
it('should validate that required properties are not empty strings', async () => {
|
||||||
context.mockRepository.findAllCarriers.mockResolvedValue([
|
context.mockRepository.findAllCarriers.mockResolvedValue([
|
||||||
{ carrierId: '', carrierName: 'Transportadora 1', carrierDescription: '001 - Transportadora 1' },
|
{
|
||||||
{ carrierId: '002', carrierName: '', carrierDescription: '002 - Transportadora 2' },
|
carrierId: '',
|
||||||
{ carrierId: '003', carrierName: 'Transportadora 3', carrierDescription: '' },
|
carrierName: 'Transportadora 1',
|
||||||
|
carrierDescription: '001 - Transportadora 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carrierId: '002',
|
||||||
|
carrierName: '',
|
||||||
|
carrierDescription: '002 - Transportadora 2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carrierId: '003',
|
||||||
|
carrierName: 'Transportadora 3',
|
||||||
|
carrierDescription: '',
|
||||||
|
},
|
||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.getAllCarriers();
|
const result = await context.service.getAllCarriers();
|
||||||
result.forEach(carrier => {
|
result.forEach((carrier) => {
|
||||||
expect(carrier.carrierId).not.toBe('');
|
expect(carrier.carrierId).not.toBe('');
|
||||||
expect(carrier.carrierName).not.toBe('');
|
expect(carrier.carrierName).not.toBe('');
|
||||||
expect(carrier.carrierDescription).not.toBe('');
|
expect(carrier.carrierDescription).not.toBe('');
|
||||||
@@ -365,9 +427,16 @@ describe('DataConsultService', () => {
|
|||||||
|
|
||||||
it('should log error when repository throws exception', async () => {
|
it('should log error when repository throws exception', async () => {
|
||||||
const repositoryError = new Error('Database connection failed');
|
const repositoryError = new Error('Database connection failed');
|
||||||
context.mockRepository.findAllCarriers.mockRejectedValue(repositoryError);
|
context.mockRepository.findAllCarriers.mockRejectedValue(
|
||||||
await expect(context.service.getAllCarriers()).rejects.toThrow(HttpException);
|
repositoryError,
|
||||||
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar transportadoras', repositoryError);
|
);
|
||||||
|
await expect(context.service.getAllCarriers()).rejects.toThrow(
|
||||||
|
HttpException,
|
||||||
|
);
|
||||||
|
expect(context.mockLogger.error).toHaveBeenCalledWith(
|
||||||
|
'Erro ao buscar transportadoras',
|
||||||
|
repositoryError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -392,7 +461,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.getRegions();
|
const result = await context.service.getRegions();
|
||||||
result.forEach(region => {
|
result.forEach((region) => {
|
||||||
expect(region.numregiao).toBeDefined();
|
expect(region.numregiao).toBeDefined();
|
||||||
expect(region.regiao).toBeDefined();
|
expect(region.regiao).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -404,7 +473,10 @@ describe('DataConsultService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that repository result is an array', async () => {
|
it('should validate that repository result is an array', async () => {
|
||||||
context.mockRepository.findRegions.mockResolvedValue({ numregiao: 1, regiao: 'Região Sul' } as any);
|
context.mockRepository.findRegions.mockResolvedValue({
|
||||||
|
numregiao: 1,
|
||||||
|
regiao: 'Região Sul',
|
||||||
|
} as any);
|
||||||
const result = await context.service.getRegions();
|
const result = await context.service.getRegions();
|
||||||
expect(Array.isArray(result)).toBe(true);
|
expect(Array.isArray(result)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -417,7 +489,7 @@ describe('DataConsultService', () => {
|
|||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
const result = await context.service.getRegions();
|
const result = await context.service.getRegions();
|
||||||
result.forEach(region => {
|
result.forEach((region) => {
|
||||||
expect(region.numregiao).toBeDefined();
|
expect(region.numregiao).toBeDefined();
|
||||||
expect(region.numregiao).not.toBeNull();
|
expect(region.numregiao).not.toBeNull();
|
||||||
expect(region.regiao).toBeDefined();
|
expect(region.regiao).toBeDefined();
|
||||||
@@ -428,8 +500,13 @@ describe('DataConsultService', () => {
|
|||||||
it('should log error when repository throws exception', async () => {
|
it('should log error when repository throws exception', async () => {
|
||||||
const repositoryError = new Error('Database connection failed');
|
const repositoryError = new Error('Database connection failed');
|
||||||
context.mockRepository.findRegions.mockRejectedValue(repositoryError);
|
context.mockRepository.findRegions.mockRejectedValue(repositoryError);
|
||||||
await expect(context.service.getRegions()).rejects.toThrow(HttpException);
|
await expect(context.service.getRegions()).rejects.toThrow(
|
||||||
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar regiões', repositoryError);
|
HttpException,
|
||||||
|
);
|
||||||
|
expect(context.mockLogger.error).toHaveBeenCalledWith(
|
||||||
|
'Erro ao buscar regiões',
|
||||||
|
repositoryError,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
|
import {
|
||||||
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
|
ApiTags,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import { Controller, Get, Param } from '@nestjs/common';
|
import { Controller, Get, Param } from '@nestjs/common';
|
||||||
import { clientesService } from './clientes.service';
|
import { clientesService } from './clientes.service';
|
||||||
|
|
||||||
@ApiTags('clientes')
|
@ApiTags('clientes')
|
||||||
@Controller('api/v1/')
|
@Controller('api/v1/')
|
||||||
export class clientesController {
|
export class clientesController {
|
||||||
|
constructor(private readonly clientesService: clientesService) {}
|
||||||
|
|
||||||
constructor(private readonly clientesService: clientesService) {}
|
@Get('clientes/:filter')
|
||||||
|
async customer(@Param('filter') filter: string) {
|
||||||
|
return this.clientesService.customers(filter);
|
||||||
@Get('clientes/:filter')
|
}
|
||||||
async customer(@Param('filter') filter: string) {
|
|
||||||
return this.clientesService.customers(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
import { clientesService } from './clientes.service';
|
import { clientesService } from './clientes.service';
|
||||||
import { clientesController } from './clientes.controller';
|
import { clientesController } from './clientes.controller';
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ export class clientesService {
|
|||||||
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
|
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
|
||||||
,PCCLIENT.ESTCOB as "estcob"
|
,PCCLIENT.ESTCOB as "estcob"
|
||||||
FROM PCCLIENT
|
FROM PCCLIENT
|
||||||
WHERE PCCLIENT.CLIENTE LIKE '${filter.toUpperCase().replace('@', '%')}%'
|
WHERE PCCLIENT.CLIENTE LIKE '${filter
|
||||||
|
.toUpperCase()
|
||||||
|
.replace('@', '%')}%'
|
||||||
ORDER BY PCCLIENT.CLIENTE`;
|
ORDER BY PCCLIENT.CLIENTE`;
|
||||||
customers = await queryRunner.manager.query(sql);
|
customers = await queryRunner.manager.query(sql);
|
||||||
}
|
}
|
||||||
@@ -72,7 +74,7 @@ export class clientesService {
|
|||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ export class clientesService {
|
|||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,19 +138,13 @@ export class clientesService {
|
|||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Limpar cache de clientes (útil para invalidação)
|
|
||||||
* @param pattern - Padrão de chaves para limpar (opcional)
|
|
||||||
*/
|
|
||||||
async clearCustomersCache(pattern?: string) {
|
async clearCustomersCache(pattern?: string) {
|
||||||
const cachePattern = pattern || 'clientes:*';
|
const cachePattern = pattern || 'clientes:*';
|
||||||
|
|
||||||
// Nota: Esta funcionalidade requer implementação específica do Redis
|
|
||||||
// Por enquanto, mantemos a interface para futuras implementações
|
|
||||||
console.log(`Cache de clientes seria limpo para o padrão: ${cachePattern}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
import { Controller, Get, Param, Query, UseGuards, UsePipes, ValidationPipe, ParseIntPipe } from '@nestjs/common';
|
import {
|
||||||
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
UsePipes,
|
||||||
|
ValidationPipe,
|
||||||
|
ParseIntPipe,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import { DataConsultService } from './data-consult.service';
|
import { DataConsultService } from './data-consult.service';
|
||||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||||
import { ProductDto } from './dto/product.dto';
|
import { ProductDto } from './dto/product.dto';
|
||||||
@@ -19,7 +34,11 @@ export class DataConsultController {
|
|||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Get('stores')
|
@Get('stores')
|
||||||
@ApiOperation({ summary: 'Lista todas as lojas' })
|
@ApiOperation({ summary: 'Lista todas as lojas' })
|
||||||
@ApiResponse({ status: 200, description: 'Lista de lojas retornada com sucesso', type: [StoreDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de lojas retornada com sucesso',
|
||||||
|
type: [StoreDto],
|
||||||
|
})
|
||||||
async stores(): Promise<StoreDto[]> {
|
async stores(): Promise<StoreDto[]> {
|
||||||
return this.dataConsultService.stores();
|
return this.dataConsultService.stores();
|
||||||
}
|
}
|
||||||
@@ -28,14 +47,24 @@ export class DataConsultController {
|
|||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Get('sellers')
|
@Get('sellers')
|
||||||
@ApiOperation({ summary: 'Lista todos os vendedores' })
|
@ApiOperation({ summary: 'Lista todos os vendedores' })
|
||||||
@ApiResponse({ status: 200, description: 'Lista de vendedores retornada com sucesso', type: [SellerDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de vendedores retornada com sucesso',
|
||||||
|
type: [SellerDto],
|
||||||
|
})
|
||||||
async sellers(): Promise<SellerDto[]> {
|
async sellers(): Promise<SellerDto[]> {
|
||||||
return this.dataConsultService.sellers();
|
return this.dataConsultService.sellers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
@Get('billings')
|
@Get('billings')
|
||||||
@ApiOperation({ summary: 'Retorna informações de faturamento' })
|
@ApiOperation({ summary: 'Retorna informações de faturamento' })
|
||||||
@ApiResponse({ status: 200, description: 'Informações de faturamento retornadas com sucesso', type: [BillingDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Informações de faturamento retornadas com sucesso',
|
||||||
|
type: [BillingDto],
|
||||||
|
})
|
||||||
async billings(): Promise<BillingDto[]> {
|
async billings(): Promise<BillingDto[]> {
|
||||||
return this.dataConsultService.billings();
|
return this.dataConsultService.billings();
|
||||||
}
|
}
|
||||||
@@ -45,7 +74,11 @@ export class DataConsultController {
|
|||||||
@Get('customers/:filter')
|
@Get('customers/:filter')
|
||||||
@ApiOperation({ summary: 'Filtra clientes pelo parâmetro fornecido' })
|
@ApiOperation({ summary: 'Filtra clientes pelo parâmetro fornecido' })
|
||||||
@ApiParam({ name: 'filter', description: 'Filtro de busca para clientes' })
|
@ApiParam({ name: 'filter', description: 'Filtro de busca para clientes' })
|
||||||
@ApiResponse({ status: 200, description: 'Lista de clientes filtrados retornada com sucesso', type: [CustomerDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de clientes filtrados retornada com sucesso',
|
||||||
|
type: [CustomerDto],
|
||||||
|
})
|
||||||
async customer(@Param('filter') filter: string): Promise<CustomerDto[]> {
|
async customer(@Param('filter') filter: string): Promise<CustomerDto[]> {
|
||||||
return this.dataConsultService.customers(filter);
|
return this.dataConsultService.customers(filter);
|
||||||
}
|
}
|
||||||
@@ -55,7 +88,11 @@ export class DataConsultController {
|
|||||||
@Get('products/:filter')
|
@Get('products/:filter')
|
||||||
@ApiOperation({ summary: 'Busca produtos filtrados' })
|
@ApiOperation({ summary: 'Busca produtos filtrados' })
|
||||||
@ApiParam({ name: 'filter', description: 'Filtro de busca' })
|
@ApiParam({ name: 'filter', description: 'Filtro de busca' })
|
||||||
@ApiResponse({ status: 200, description: 'Lista de produtos filtrados retornada com sucesso', type: [ProductDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de produtos filtrados retornada com sucesso',
|
||||||
|
type: [ProductDto],
|
||||||
|
})
|
||||||
async products(@Param('filter') filter: string): Promise<ProductDto[]> {
|
async products(@Param('filter') filter: string): Promise<ProductDto[]> {
|
||||||
return this.dataConsultService.products(filter);
|
return this.dataConsultService.products(filter);
|
||||||
}
|
}
|
||||||
@@ -64,7 +101,11 @@ export class DataConsultController {
|
|||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Get('all')
|
@Get('all')
|
||||||
@ApiOperation({ summary: 'Lista 500 produtos' })
|
@ApiOperation({ summary: 'Lista 500 produtos' })
|
||||||
@ApiResponse({ status: 200, description: 'Lista de 500 produtos retornada com sucesso', type: [ProductDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de 500 produtos retornada com sucesso',
|
||||||
|
type: [ProductDto],
|
||||||
|
})
|
||||||
async getAllProducts(): Promise<ProductDto[]> {
|
async getAllProducts(): Promise<ProductDto[]> {
|
||||||
return this.dataConsultService.getAllProducts();
|
return this.dataConsultService.getAllProducts();
|
||||||
}
|
}
|
||||||
@@ -73,7 +114,11 @@ export class DataConsultController {
|
|||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Get('carriers/all')
|
@Get('carriers/all')
|
||||||
@ApiOperation({ summary: 'Lista todas as transportadoras cadastradas' })
|
@ApiOperation({ summary: 'Lista todas as transportadoras cadastradas' })
|
||||||
@ApiResponse({ status: 200, description: 'Lista de transportadoras retornada com sucesso', type: [CarrierDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de transportadoras retornada com sucesso',
|
||||||
|
type: [CarrierDto],
|
||||||
|
})
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getAllCarriers(): Promise<CarrierDto[]> {
|
async getAllCarriers(): Promise<CarrierDto[]> {
|
||||||
return this.dataConsultService.getAllCarriers();
|
return this.dataConsultService.getAllCarriers();
|
||||||
@@ -83,9 +128,15 @@ export class DataConsultController {
|
|||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Get('carriers')
|
@Get('carriers')
|
||||||
@ApiOperation({ summary: 'Busca transportadoras por período de data' })
|
@ApiOperation({ summary: 'Busca transportadoras por período de data' })
|
||||||
@ApiResponse({ status: 200, description: 'Lista de transportadoras por período retornada com sucesso', type: [CarrierDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de transportadoras por período retornada com sucesso',
|
||||||
|
type: [CarrierDto],
|
||||||
|
})
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getCarriersByDate(@Query() query: FindCarriersDto): Promise<CarrierDto[]> {
|
async getCarriersByDate(
|
||||||
|
@Query() query: FindCarriersDto,
|
||||||
|
): Promise<CarrierDto[]> {
|
||||||
return this.dataConsultService.getCarriersByDate(query);
|
return this.dataConsultService.getCarriersByDate(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,17 +145,26 @@ export class DataConsultController {
|
|||||||
@Get('carriers/order/:orderId')
|
@Get('carriers/order/:orderId')
|
||||||
@ApiOperation({ summary: 'Busca transportadoras de um pedido específico' })
|
@ApiOperation({ summary: 'Busca transportadoras de um pedido específico' })
|
||||||
@ApiParam({ name: 'orderId', example: 236001388 })
|
@ApiParam({ name: 'orderId', example: 236001388 })
|
||||||
@ApiResponse({ status: 200, description: 'Lista de transportadoras do pedido retornada com sucesso', type: [CarrierDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de transportadoras do pedido retornada com sucesso',
|
||||||
|
type: [CarrierDto],
|
||||||
|
})
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise<CarrierDto[]> {
|
async getOrderCarriers(
|
||||||
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
|
): Promise<CarrierDto[]> {
|
||||||
return this.dataConsultService.getOrderCarriers(orderId);
|
return this.dataConsultService.getOrderCarriers(orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('regions')
|
@Get('regions')
|
||||||
@ApiOperation({ summary: 'Lista todas as regiões cadastradas' })
|
@ApiOperation({ summary: 'Lista todas as regiões cadastradas' })
|
||||||
@ApiResponse({ status: 200, description: 'Lista de regiões retornada com sucesso', type: [RegionDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de regiões retornada com sucesso',
|
||||||
|
type: [RegionDto],
|
||||||
|
})
|
||||||
async getRegions(): Promise<RegionDto[]> {
|
async getRegions(): Promise<RegionDto[]> {
|
||||||
return this.dataConsultService.getRegions();
|
return this.dataConsultService.getRegions();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,18 +2,13 @@ import { Module } from '@nestjs/common';
|
|||||||
import { DataConsultService } from './data-consult.service';
|
import { DataConsultService } from './data-consult.service';
|
||||||
import { DataConsultController } from './data-consult.controller';
|
import { DataConsultController } from './data-consult.controller';
|
||||||
import { DataConsultRepository } from './data-consult.repository';
|
import { DataConsultRepository } from './data-consult.repository';
|
||||||
import { LoggerModule } from 'src/Log/logger.module';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { RedisModule } from 'src/core/configs/cache/redis.module';
|
import { RedisModule } from 'src/core/configs/cache/redis.module';
|
||||||
import { clientes } from './clientes.module';
|
import { clientes } from './clientes.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [LoggerModule, ConfigModule, RedisModule, clientes],
|
imports: [ConfigModule, RedisModule, clientes],
|
||||||
controllers: [DataConsultController],
|
controllers: [DataConsultController],
|
||||||
providers: [
|
providers: [DataConsultService, DataConsultRepository],
|
||||||
DataConsultService,
|
|
||||||
DataConsultRepository,
|
|
||||||
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class DataConsultModule {}
|
export class DataConsultModule {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, HttpException, HttpStatus, Inject } from '@nestjs/common';
|
import { Injectable, HttpException, HttpStatus, Inject, Logger } from '@nestjs/common';
|
||||||
import { DataConsultRepository } from './data-consult.repository';
|
import { DataConsultRepository } from './data-consult.repository';
|
||||||
import { StoreDto } from './dto/store.dto';
|
import { StoreDto } from './dto/store.dto';
|
||||||
import { SellerDto } from './dto/seller.dto';
|
import { SellerDto } from './dto/seller.dto';
|
||||||
@@ -7,7 +7,6 @@ import { CustomerDto } from './dto/customer.dto';
|
|||||||
import { ProductDto } from './dto/product.dto';
|
import { ProductDto } from './dto/product.dto';
|
||||||
import { RegionDto } from './dto/region.dto';
|
import { RegionDto } from './dto/region.dto';
|
||||||
import { CarrierDto, FindCarriersDto } from './dto/carrier.dto';
|
import { CarrierDto, FindCarriersDto } from './dto/carrier.dto';
|
||||||
import { ILogger } from '../Log/ILogger';
|
|
||||||
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
|
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
|
||||||
import { IRedisClient } from '../core/configs/cache/IRedisClient';
|
import { IRedisClient } from '../core/configs/cache/IRedisClient';
|
||||||
import { getOrSetCache } from '../shared/cache.util';
|
import { getOrSetCache } from '../shared/cache.util';
|
||||||
@@ -16,6 +15,7 @@ import { DATA_SOURCE } from '../core/constants';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataConsultService {
|
export class DataConsultService {
|
||||||
|
private readonly logger = new Logger(DataConsultService.name);
|
||||||
private readonly SELLERS_CACHE_KEY = 'data-consult:sellers';
|
private readonly SELLERS_CACHE_KEY = 'data-consult:sellers';
|
||||||
private readonly SELLERS_TTL = 3600;
|
private readonly SELLERS_TTL = 3600;
|
||||||
private readonly STORES_TTL = 3600;
|
private readonly STORES_TTL = 3600;
|
||||||
@@ -31,8 +31,7 @@ export class DataConsultService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly repository: DataConsultRepository,
|
private readonly repository: DataConsultRepository,
|
||||||
@Inject(RedisClientToken) private readonly redisClient: IRedisClient,
|
@Inject(RedisClientToken) private readonly redisClient: IRedisClient,
|
||||||
@Inject('LoggerService') private readonly logger: ILogger,
|
@Inject(DATA_SOURCE) private readonly dataSource: DataSource,
|
||||||
@Inject(DATA_SOURCE) private readonly dataSource: DataSource
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async stores(): Promise<StoreDto[]> {
|
async stores(): Promise<StoreDto[]> {
|
||||||
@@ -41,25 +40,38 @@ export class DataConsultService {
|
|||||||
const stores = await this.repository.findStores();
|
const stores = await this.repository.findStores();
|
||||||
|
|
||||||
if (stores === null || stores === undefined) {
|
if (stores === null || stores === undefined) {
|
||||||
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Resultado inválido do repositório',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const storesArray = Array.isArray(stores) ? stores : [stores];
|
const storesArray = Array.isArray(stores) ? stores : [stores];
|
||||||
|
|
||||||
return storesArray
|
return storesArray
|
||||||
.filter(store => {
|
.filter((store) => {
|
||||||
if (!store || typeof store !== 'object') {
|
if (!store || typeof store !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasId = store.id !== undefined && store.id !== null && store.id !== '';
|
const hasId =
|
||||||
const hasName = store.name !== undefined && store.name !== null && store.name !== '';
|
store.id !== undefined && store.id !== null && store.id !== '';
|
||||||
const hasStore = store.store !== undefined && store.store !== null && store.store !== '';
|
const hasName =
|
||||||
|
store.name !== undefined &&
|
||||||
|
store.name !== null &&
|
||||||
|
store.name !== '';
|
||||||
|
const hasStore =
|
||||||
|
store.store !== undefined &&
|
||||||
|
store.store !== null &&
|
||||||
|
store.store !== '';
|
||||||
return hasId && hasName && hasStore;
|
return hasId && hasName && hasStore;
|
||||||
})
|
})
|
||||||
.map(store => new StoreDto(store));
|
.map((store) => new StoreDto(store));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar lojas', error);
|
this.logger.error('Erro ao buscar lojas', error);
|
||||||
throw new HttpException('Erro ao buscar lojas', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar lojas',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,30 +87,42 @@ export class DataConsultService {
|
|||||||
const sellers = await this.repository.findSellers();
|
const sellers = await this.repository.findSellers();
|
||||||
|
|
||||||
if (sellers === null || sellers === undefined) {
|
if (sellers === null || sellers === undefined) {
|
||||||
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Resultado inválido do repositório',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sellersArray = Array.isArray(sellers) ? sellers : [sellers];
|
const sellersArray = Array.isArray(sellers) ? sellers : [sellers];
|
||||||
|
|
||||||
return sellersArray
|
return sellersArray
|
||||||
.filter(seller => {
|
.filter((seller) => {
|
||||||
if (!seller || typeof seller !== 'object') {
|
if (!seller || typeof seller !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasId = seller.id !== undefined && seller.id !== null && seller.id !== '';
|
const hasId =
|
||||||
const hasName = seller.name !== undefined && seller.name !== null && seller.name !== '';
|
seller.id !== undefined &&
|
||||||
|
seller.id !== null &&
|
||||||
|
seller.id !== '';
|
||||||
|
const hasName =
|
||||||
|
seller.name !== undefined &&
|
||||||
|
seller.name !== null &&
|
||||||
|
seller.name !== '';
|
||||||
return hasId && hasName;
|
return hasId && hasName;
|
||||||
})
|
})
|
||||||
.map(seller => new SellerDto(seller));
|
.map((seller) => new SellerDto(seller));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar vendedores', error);
|
this.logger.error('Erro ao buscar vendedores', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar vendedores', error);
|
this.logger.error('Erro ao buscar vendedores', error);
|
||||||
throw new HttpException('Erro ao buscar vendedores', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar vendedores',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,25 +132,35 @@ export class DataConsultService {
|
|||||||
const billings = await this.repository.findBillings();
|
const billings = await this.repository.findBillings();
|
||||||
|
|
||||||
if (billings === null || billings === undefined) {
|
if (billings === null || billings === undefined) {
|
||||||
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Resultado inválido do repositório',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const billingsArray = Array.isArray(billings) ? billings : [billings];
|
const billingsArray = Array.isArray(billings) ? billings : [billings];
|
||||||
|
|
||||||
return billingsArray
|
return billingsArray
|
||||||
.filter(billing => {
|
.filter((billing) => {
|
||||||
if (!billing || typeof billing !== 'object') {
|
if (!billing || typeof billing !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasId = billing.id !== undefined && billing.id !== null && billing.id !== '';
|
const hasId =
|
||||||
|
billing.id !== undefined &&
|
||||||
|
billing.id !== null &&
|
||||||
|
billing.id !== '';
|
||||||
const hasDate = billing.date !== undefined && billing.date !== null;
|
const hasDate = billing.date !== undefined && billing.date !== null;
|
||||||
const hasTotal = billing.total !== undefined && billing.total !== null;
|
const hasTotal =
|
||||||
|
billing.total !== undefined && billing.total !== null;
|
||||||
return hasId && hasDate && hasTotal;
|
return hasId && hasDate && hasTotal;
|
||||||
})
|
})
|
||||||
.map(billing => new BillingDto(billing));
|
.map((billing) => new BillingDto(billing));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar faturamento', error);
|
this.logger.error('Erro ao buscar faturamento', error);
|
||||||
throw new HttpException('Erro ao buscar faturamento', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar faturamento',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,25 +173,40 @@ export class DataConsultService {
|
|||||||
const customers = await this.repository.findCustomers(filter);
|
const customers = await this.repository.findCustomers(filter);
|
||||||
|
|
||||||
if (customers === null || customers === undefined) {
|
if (customers === null || customers === undefined) {
|
||||||
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Resultado inválido do repositório',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customersArray = Array.isArray(customers) ? customers : [customers];
|
const customersArray = Array.isArray(customers) ? customers : [customers];
|
||||||
|
|
||||||
return customersArray
|
return customersArray
|
||||||
.filter(customer => {
|
.filter((customer) => {
|
||||||
if (!customer || typeof customer !== 'object') {
|
if (!customer || typeof customer !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasId = customer.id !== undefined && customer.id !== null && customer.id !== '';
|
const hasId =
|
||||||
const hasName = customer.name !== undefined && customer.name !== null && customer.name !== '';
|
customer.id !== undefined &&
|
||||||
const hasDocument = customer.document !== undefined && customer.document !== null && customer.document !== '';
|
customer.id !== null &&
|
||||||
|
customer.id !== '';
|
||||||
|
const hasName =
|
||||||
|
customer.name !== undefined &&
|
||||||
|
customer.name !== null &&
|
||||||
|
customer.name !== '';
|
||||||
|
const hasDocument =
|
||||||
|
customer.document !== undefined &&
|
||||||
|
customer.document !== null &&
|
||||||
|
customer.document !== '';
|
||||||
return hasId && hasName && hasDocument;
|
return hasId && hasName && hasDocument;
|
||||||
})
|
})
|
||||||
.map(customer => new CustomerDto(customer));
|
.map((customer) => new CustomerDto(customer));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar clientes', error);
|
this.logger.error('Erro ao buscar clientes', error);
|
||||||
throw new HttpException('Erro ao buscar clientes', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar clientes',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,10 +217,13 @@ export class DataConsultService {
|
|||||||
throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST);
|
throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
const products = await this.repository.findProducts(filter);
|
const products = await this.repository.findProducts(filter);
|
||||||
return products.map(product => new ProductDto(product));
|
return products.map((product) => new ProductDto(product));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar produtos', error);
|
this.logger.error('Erro ao buscar produtos', error);
|
||||||
throw new HttpException('Erro ao buscar produtos', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar produtos',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,31 +239,48 @@ export class DataConsultService {
|
|||||||
const products = await this.repository.findAllProducts();
|
const products = await this.repository.findAllProducts();
|
||||||
|
|
||||||
if (products === null || products === undefined) {
|
if (products === null || products === undefined) {
|
||||||
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Resultado inválido do repositório',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const productsArray = Array.isArray(products) ? products : [products];
|
const productsArray = Array.isArray(products)
|
||||||
|
? products
|
||||||
|
: [products];
|
||||||
|
|
||||||
return productsArray
|
return productsArray
|
||||||
.filter(product => {
|
.filter((product) => {
|
||||||
if (!product || typeof product !== 'object') {
|
if (!product || typeof product !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasId = product.id !== undefined && product.id !== null && product.id !== '';
|
const hasId =
|
||||||
const hasName = product.name !== undefined && product.name !== null && product.name !== '';
|
product.id !== undefined &&
|
||||||
const hasManufacturerCode = product.manufacturerCode !== undefined && product.manufacturerCode !== null && product.manufacturerCode !== '';
|
product.id !== null &&
|
||||||
|
product.id !== '';
|
||||||
|
const hasName =
|
||||||
|
product.name !== undefined &&
|
||||||
|
product.name !== null &&
|
||||||
|
product.name !== '';
|
||||||
|
const hasManufacturerCode =
|
||||||
|
product.manufacturerCode !== undefined &&
|
||||||
|
product.manufacturerCode !== null &&
|
||||||
|
product.manufacturerCode !== '';
|
||||||
return hasId && hasName && hasManufacturerCode;
|
return hasId && hasName && hasManufacturerCode;
|
||||||
})
|
})
|
||||||
.map(product => new ProductDto(product));
|
.map((product) => new ProductDto(product));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar todos os produtos', error);
|
this.logger.error('Erro ao buscar todos os produtos', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar todos os produtos', error);
|
this.logger.error('Erro ao buscar todos os produtos', error);
|
||||||
throw new HttpException('Erro ao buscar produtos', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar produtos',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,22 +296,36 @@ export class DataConsultService {
|
|||||||
const carriers = await this.repository.findAllCarriers();
|
const carriers = await this.repository.findAllCarriers();
|
||||||
|
|
||||||
if (carriers === null || carriers === undefined) {
|
if (carriers === null || carriers === undefined) {
|
||||||
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Resultado inválido do repositório',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const carriersArray = Array.isArray(carriers) ? carriers : [carriers];
|
const carriersArray = Array.isArray(carriers)
|
||||||
|
? carriers
|
||||||
|
: [carriers];
|
||||||
|
|
||||||
return carriersArray
|
return carriersArray
|
||||||
.filter(carrier => {
|
.filter((carrier) => {
|
||||||
if (!carrier || typeof carrier !== 'object') {
|
if (!carrier || typeof carrier !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasCarrierId = carrier.carrierId !== undefined && carrier.carrierId !== null && carrier.carrierId !== '';
|
const hasCarrierId =
|
||||||
const hasCarrierName = carrier.carrierName !== undefined && carrier.carrierName !== null && carrier.carrierName !== '';
|
carrier.carrierId !== undefined &&
|
||||||
const hasCarrierDescription = carrier.carrierDescription !== undefined && carrier.carrierDescription !== null && carrier.carrierDescription !== '';
|
carrier.carrierId !== null &&
|
||||||
|
carrier.carrierId !== '';
|
||||||
|
const hasCarrierName =
|
||||||
|
carrier.carrierName !== undefined &&
|
||||||
|
carrier.carrierName !== null &&
|
||||||
|
carrier.carrierName !== '';
|
||||||
|
const hasCarrierDescription =
|
||||||
|
carrier.carrierDescription !== undefined &&
|
||||||
|
carrier.carrierDescription !== null &&
|
||||||
|
carrier.carrierDescription !== '';
|
||||||
return hasCarrierId && hasCarrierName && hasCarrierDescription;
|
return hasCarrierId && hasCarrierName && hasCarrierDescription;
|
||||||
})
|
})
|
||||||
.map(carrier => ({
|
.map((carrier) => ({
|
||||||
carrierId: carrier.carrierId?.toString() || '',
|
carrierId: carrier.carrierId?.toString() || '',
|
||||||
carrierName: carrier.carrierName || '',
|
carrierName: carrier.carrierName || '',
|
||||||
carrierDescription: carrier.carrierDescription || '',
|
carrierDescription: carrier.carrierDescription || '',
|
||||||
@@ -251,19 +334,24 @@ export class DataConsultService {
|
|||||||
this.logger.error('Erro ao buscar transportadoras', error);
|
this.logger.error('Erro ao buscar transportadoras', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar transportadoras', error);
|
this.logger.error('Erro ao buscar transportadoras', error);
|
||||||
throw new HttpException('Erro ao buscar transportadoras', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar transportadoras',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCarriersByDate(query: FindCarriersDto): Promise<CarrierDto[]> {
|
async getCarriersByDate(query: FindCarriersDto): Promise<CarrierDto[]> {
|
||||||
this.logger.log(`Buscando transportadoras por período: ${JSON.stringify(query)}`);
|
this.logger.log(
|
||||||
|
`Buscando transportadoras por período: ${JSON.stringify(query)}`,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const carriers = await this.repository.findCarriersByDate(query);
|
const carriers = await this.repository.findCarriersByDate(query);
|
||||||
return carriers.map(carrier => ({
|
return carriers.map((carrier) => ({
|
||||||
carrierId: carrier.carrierId?.toString() || '',
|
carrierId: carrier.carrierId?.toString() || '',
|
||||||
carrierName: carrier.carrierName || '',
|
carrierName: carrier.carrierName || '',
|
||||||
carrierDescription: carrier.carrierDescription || '',
|
carrierDescription: carrier.carrierDescription || '',
|
||||||
@@ -271,7 +359,10 @@ export class DataConsultService {
|
|||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar transportadoras por período', error);
|
this.logger.error('Erro ao buscar transportadoras por período', error);
|
||||||
throw new HttpException('Erro ao buscar transportadoras', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar transportadoras',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,14 +370,17 @@ export class DataConsultService {
|
|||||||
this.logger.log(`Buscando transportadoras do pedido: ${orderId}`);
|
this.logger.log(`Buscando transportadoras do pedido: ${orderId}`);
|
||||||
try {
|
try {
|
||||||
const carriers = await this.repository.findOrderCarriers(orderId);
|
const carriers = await this.repository.findOrderCarriers(orderId);
|
||||||
return carriers.map(carrier => ({
|
return carriers.map((carrier) => ({
|
||||||
carrierId: carrier.carrierId?.toString() || '',
|
carrierId: carrier.carrierId?.toString() || '',
|
||||||
carrierName: carrier.carrierName || '',
|
carrierName: carrier.carrierName || '',
|
||||||
carrierDescription: carrier.carrierDescription || '',
|
carrierDescription: carrier.carrierDescription || '',
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar transportadoras do pedido', error);
|
this.logger.error('Erro ao buscar transportadoras do pedido', error);
|
||||||
throw new HttpException('Erro ao buscar transportadoras do pedido', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar transportadoras do pedido',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,30 +396,40 @@ export class DataConsultService {
|
|||||||
const regions = await this.repository.findRegions();
|
const regions = await this.repository.findRegions();
|
||||||
|
|
||||||
if (regions === null || regions === undefined) {
|
if (regions === null || regions === undefined) {
|
||||||
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Resultado inválido do repositório',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const regionsArray = Array.isArray(regions) ? regions : [regions];
|
const regionsArray = Array.isArray(regions) ? regions : [regions];
|
||||||
|
|
||||||
return regionsArray
|
return regionsArray
|
||||||
.filter(region => {
|
.filter((region) => {
|
||||||
if (!region || typeof region !== 'object') {
|
if (!region || typeof region !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const hasNumregiao = region.numregiao !== undefined && region.numregiao !== null;
|
const hasNumregiao =
|
||||||
const hasRegiao = region.regiao !== undefined && region.regiao !== null && region.regiao !== '';
|
region.numregiao !== undefined && region.numregiao !== null;
|
||||||
|
const hasRegiao =
|
||||||
|
region.regiao !== undefined &&
|
||||||
|
region.regiao !== null &&
|
||||||
|
region.regiao !== '';
|
||||||
return hasNumregiao && hasRegiao;
|
return hasNumregiao && hasRegiao;
|
||||||
})
|
})
|
||||||
.map(region => new RegionDto(region));
|
.map((region) => new RegionDto(region));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar regiões', error);
|
this.logger.error('Erro ao buscar regiões', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Erro ao buscar regiões', error);
|
this.logger.error('Erro ao buscar regiões', error);
|
||||||
throw new HttpException('Erro ao buscar regiões', HttpStatus.INTERNAL_SERVER_ERROR);
|
throw new HttpException(
|
||||||
|
'Erro ao buscar regiões',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,26 +4,26 @@ import { IsOptional, IsString, IsDateString } from 'class-validator';
|
|||||||
export class CarrierDto {
|
export class CarrierDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'ID da transportadora',
|
description: 'ID da transportadora',
|
||||||
example: '123'
|
example: '123',
|
||||||
})
|
})
|
||||||
carrierId: string;
|
carrierId: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Nome da transportadora',
|
description: 'Nome da transportadora',
|
||||||
example: 'TRANSPORTADORA ABC LTDA'
|
example: 'TRANSPORTADORA ABC LTDA',
|
||||||
})
|
})
|
||||||
carrierName: string;
|
carrierName: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Descrição completa da transportadora (ID - Nome)',
|
description: 'Descrição completa da transportadora (ID - Nome)',
|
||||||
example: '123 - TRANSPORTADORA ABC LTDA'
|
example: '123 - TRANSPORTADORA ABC LTDA',
|
||||||
})
|
})
|
||||||
carrierDescription: string;
|
carrierDescription: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Quantidade de pedidos da transportadora no período',
|
description: 'Quantidade de pedidos da transportadora no período',
|
||||||
example: 15,
|
example: 15,
|
||||||
required: false
|
required: false,
|
||||||
})
|
})
|
||||||
ordersCount?: number;
|
ordersCount?: number;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ export class FindCarriersDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Data inicial para filtro (formato YYYY-MM-DD)',
|
description: 'Data inicial para filtro (formato YYYY-MM-DD)',
|
||||||
example: '2024-01-01',
|
example: '2024-01-01',
|
||||||
required: false
|
required: false,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
@@ -41,7 +41,7 @@ export class FindCarriersDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Data final para filtro (formato YYYY-MM-DD)',
|
description: 'Data final para filtro (formato YYYY-MM-DD)',
|
||||||
example: '2024-12-31',
|
example: '2024-12-31',
|
||||||
required: false
|
required: false,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
@@ -50,7 +50,7 @@ export class FindCarriersDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'ID da filial',
|
description: 'ID da filial',
|
||||||
example: '1',
|
example: '1',
|
||||||
required: false
|
required: false,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -20,4 +20,3 @@ export class RegionDto {
|
|||||||
Object.assign(this, partial);
|
Object.assign(this, partial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,258 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { HttpService } from '@nestjs/axios';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
import { HealthCheckResult } from '@nestjs/terminus';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HealthAlertService {
|
|
||||||
private readonly logger = new Logger(HealthAlertService.name);
|
|
||||||
private readonly webhookUrls: Record<string, string>;
|
|
||||||
private readonly alertThresholds: Record<string, any>;
|
|
||||||
private readonly alertCooldowns: Map<string, number> = new Map();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly httpService: HttpService,
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {
|
|
||||||
// Configurações de webhooks para diferentes canais de alerta
|
|
||||||
this.webhookUrls = {
|
|
||||||
slack: this.configService.get<string>('ALERT_WEBHOOK_SLACK'),
|
|
||||||
teams: this.configService.get<string>('ALERT_WEBHOOK_TEAMS'),
|
|
||||||
email: this.configService.get<string>('ALERT_WEBHOOK_EMAIL'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Thresholds para diferentes tipos de alerta
|
|
||||||
this.alertThresholds = {
|
|
||||||
disk: {
|
|
||||||
criticalPercent: this.configService.get<number>('ALERT_DISK_CRITICAL_PERCENT', 90),
|
|
||||||
warningPercent: this.configService.get<number>('ALERT_DISK_WARNING_PERCENT', 80),
|
|
||||||
},
|
|
||||||
memory: {
|
|
||||||
criticalPercent: this.configService.get<number>('ALERT_MEMORY_CRITICAL_PERCENT', 90),
|
|
||||||
warningPercent: this.configService.get<number>('ALERT_MEMORY_WARNING_PERCENT', 80),
|
|
||||||
},
|
|
||||||
db: {
|
|
||||||
cooldownMinutes: this.configService.get<number>('ALERT_DB_COOLDOWN_MINUTES', 15),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async processHealthCheckResult(result: HealthCheckResult): Promise<void> {
|
|
||||||
try {
|
|
||||||
const { status, info, error, details } = result;
|
|
||||||
|
|
||||||
// Se o status geral não for 'ok', envie um alerta
|
|
||||||
if (status !== 'ok') {
|
|
||||||
// Verificar quais componentes estão com problema
|
|
||||||
const failedComponents = Object.entries(error)
|
|
||||||
.map(([key, value]) => ({ key, value }));
|
|
||||||
|
|
||||||
if (failedComponents.length > 0) {
|
|
||||||
await this.sendAlert('critical', 'Health Check Falhou',
|
|
||||||
`Os seguintes componentes estão com problemas: ${failedComponents.map(c => c.key).join(', ')}`,
|
|
||||||
{ result, failedComponents }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar alertas específicos para cada tipo de componente
|
|
||||||
if (details.disk) {
|
|
||||||
await this.checkDiskAlerts(details.disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (details.memory_heap) {
|
|
||||||
await this.checkMemoryAlerts(details.memory_heap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar alertas de banco de dados
|
|
||||||
['oracle', 'postgres'].forEach(db => {
|
|
||||||
if (details[db] && details[db].status !== 'up') {
|
|
||||||
this.checkDatabaseAlerts(db, details[db]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao processar health check result: ${error.message}`, error.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkDiskAlerts(diskDetails: any): Promise<void> {
|
|
||||||
try {
|
|
||||||
if (!diskDetails.freeBytes || !diskDetails.totalBytes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const usedPercent = ((diskDetails.totalBytes - diskDetails.freeBytes) / diskDetails.totalBytes) * 100;
|
|
||||||
|
|
||||||
if (usedPercent >= this.alertThresholds.disk.criticalPercent) {
|
|
||||||
await this.sendAlert('critical', 'Espaço em Disco Crítico',
|
|
||||||
`O uso de disco está em ${usedPercent.toFixed(1)}%, acima do limite crítico de ${this.alertThresholds.disk.criticalPercent}%`,
|
|
||||||
{ diskDetails, usedPercent }
|
|
||||||
);
|
|
||||||
} else if (usedPercent >= this.alertThresholds.disk.warningPercent) {
|
|
||||||
await this.sendAlert('warning', 'Alerta de Espaço em Disco',
|
|
||||||
`O uso de disco está em ${usedPercent.toFixed(1)}%, acima do limite de alerta de ${this.alertThresholds.disk.warningPercent}%`,
|
|
||||||
{ diskDetails, usedPercent }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao verificar alertas de disco: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkMemoryAlerts(memoryDetails: any): Promise<void> {
|
|
||||||
try {
|
|
||||||
if (!memoryDetails.usedBytes || !memoryDetails.thresholdBytes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const usedPercent = (memoryDetails.usedBytes / memoryDetails.thresholdBytes) * 100;
|
|
||||||
|
|
||||||
if (usedPercent >= this.alertThresholds.memory.criticalPercent) {
|
|
||||||
await this.sendAlert('critical', 'Uso de Memória Crítico',
|
|
||||||
`O uso de memória heap está em ${usedPercent.toFixed(1)}%, acima do limite crítico de ${this.alertThresholds.memory.criticalPercent}%`,
|
|
||||||
{ memoryDetails, usedPercent }
|
|
||||||
);
|
|
||||||
} else if (usedPercent >= this.alertThresholds.memory.warningPercent) {
|
|
||||||
await this.sendAlert('warning', 'Alerta de Uso de Memória',
|
|
||||||
`O uso de memória heap está em ${usedPercent.toFixed(1)}%, acima do limite de alerta de ${this.alertThresholds.memory.warningPercent}%`,
|
|
||||||
{ memoryDetails, usedPercent }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao verificar alertas de memória: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkDatabaseAlerts(dbName: string, dbDetails: any): Promise<void> {
|
|
||||||
try {
|
|
||||||
const now = Date.now();
|
|
||||||
const lastAlertTime = this.alertCooldowns.get(dbName) || 0;
|
|
||||||
const cooldownMs = this.alertThresholds.db.cooldownMinutes * 60 * 1000;
|
|
||||||
|
|
||||||
// Verifica se já passou o período de cooldown para este banco
|
|
||||||
if (now - lastAlertTime >= cooldownMs) {
|
|
||||||
await this.sendAlert('critical', `Problema de Conexão com Banco de Dados ${dbName}`,
|
|
||||||
`A conexão com o banco de dados ${dbName} está com problemas: ${dbDetails.message || 'Erro não especificado'}`,
|
|
||||||
{ dbName, dbDetails }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Atualiza o timestamp do último alerta
|
|
||||||
this.alertCooldowns.set(dbName, now);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao verificar alertas de banco de dados: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendAlert(
|
|
||||||
severity: 'critical' | 'warning' | 'info',
|
|
||||||
title: string,
|
|
||||||
message: string,
|
|
||||||
details?: any,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const environment = this.configService.get<string>('NODE_ENV', 'development');
|
|
||||||
const appName = this.configService.get<string>('APP_NAME', 'Portal Jurunense API');
|
|
||||||
|
|
||||||
this.logger.warn(`[${severity.toUpperCase()}] ${title}: ${message}`);
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
severity,
|
|
||||||
title: `[${environment.toUpperCase()}] [${appName}] ${title}`,
|
|
||||||
message,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
details: details || {},
|
|
||||||
environment,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enviar para Slack, se configurado
|
|
||||||
if (this.webhookUrls.slack) {
|
|
||||||
await this.sendSlackAlert(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enviar para Microsoft Teams, se configurado
|
|
||||||
if (this.webhookUrls.teams) {
|
|
||||||
await this.sendTeamsAlert(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enviar para serviço de email, se configurado
|
|
||||||
if (this.webhookUrls.email) {
|
|
||||||
await this.sendEmailAlert(payload);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao enviar alerta: ${error.message}`, error.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendSlackAlert(payload: any): Promise<void> {
|
|
||||||
try {
|
|
||||||
const slackPayload = {
|
|
||||||
text: `${payload.title}`,
|
|
||||||
blocks: [
|
|
||||||
{
|
|
||||||
type: 'header',
|
|
||||||
text: {
|
|
||||||
type: 'plain_text',
|
|
||||||
text: payload.title,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'section',
|
|
||||||
text: {
|
|
||||||
type: 'mrkdwn',
|
|
||||||
text: `*Mensagem:* ${payload.message}\n*Severidade:* ${payload.severity}\n*Ambiente:* ${payload.environment}\n*Timestamp:* ${payload.timestamp}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await firstValueFrom(this.httpService.post(this.webhookUrls.slack, slackPayload));
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao enviar alerta para Slack: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendTeamsAlert(payload: any): Promise<void> {
|
|
||||||
try {
|
|
||||||
const teamsPayload = {
|
|
||||||
"@type": "MessageCard",
|
|
||||||
"@context": "http://schema.org/extensions",
|
|
||||||
"themeColor": payload.severity === 'critical' ? "FF0000" : (payload.severity === 'warning' ? "FFA500" : "0078D7"),
|
|
||||||
"summary": payload.title,
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"activityTitle": payload.title,
|
|
||||||
"activitySubtitle": `Severidade: ${payload.severity} | Ambiente: ${payload.environment}`,
|
|
||||||
"text": payload.message,
|
|
||||||
"facts": [
|
|
||||||
{
|
|
||||||
"name": "Timestamp",
|
|
||||||
"value": payload.timestamp
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await firstValueFrom(this.httpService.post(this.webhookUrls.teams, teamsPayload));
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao enviar alerta para Microsoft Teams: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendEmailAlert(payload: any): Promise<void> {
|
|
||||||
try {
|
|
||||||
const emailPayload = {
|
|
||||||
subject: payload.title,
|
|
||||||
text: `${payload.message}\n\nSeveridade: ${payload.severity}\nAmbiente: ${payload.environment}\nTimestamp: ${payload.timestamp}`,
|
|
||||||
html: `<h2>${payload.title}</h2><p>${payload.message}</p><p><strong>Severidade:</strong> ${payload.severity}<br><strong>Ambiente:</strong> ${payload.environment}<br><strong>Timestamp:</strong> ${payload.timestamp}</p>`,
|
|
||||||
};
|
|
||||||
|
|
||||||
await firstValueFrom(this.httpService.post(this.webhookUrls.email, emailPayload));
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao enviar alerta por email: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
HealthCheck,
|
|
||||||
HealthCheckService,
|
|
||||||
HttpHealthIndicator,
|
|
||||||
DiskHealthIndicator,
|
|
||||||
MemoryHealthIndicator,
|
|
||||||
} from '@nestjs/terminus';
|
|
||||||
import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
|
|
||||||
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
||||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
|
||||||
import * as os from 'os';
|
|
||||||
|
|
||||||
@ApiTags('Health Check')
|
|
||||||
@Controller('health')
|
|
||||||
export class HealthController {
|
|
||||||
private readonly diskPath: string;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private health: HealthCheckService,
|
|
||||||
private http: HttpHealthIndicator,
|
|
||||||
private disk: DiskHealthIndicator,
|
|
||||||
private memory: MemoryHealthIndicator,
|
|
||||||
private typeOrmHealth: TypeOrmHealthIndicator,
|
|
||||||
private dbPoolStats: DbPoolStatsIndicator,
|
|
||||||
private configService: ConfigService,
|
|
||||||
) {
|
|
||||||
this.diskPath = os.platform() === 'win32' ? 'C:\\' : '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get()
|
|
||||||
@HealthCheck()
|
|
||||||
@ApiOperation({ summary: 'Verificar saúde geral da aplicação' })
|
|
||||||
check() {
|
|
||||||
return this.health.check([
|
|
||||||
// Verifica o status da própria aplicação
|
|
||||||
() => this.http.pingCheck('api', 'http://localhost:8066/docs'),
|
|
||||||
|
|
||||||
// Verifica espaço em disco (espaço livre < 80%)
|
|
||||||
() => this.disk.checkStorage('disk_percent', {
|
|
||||||
path: this.diskPath,
|
|
||||||
thresholdPercent: 0.8, // 80%
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Verifica espaço em disco (pelo menos 500MB livres)
|
|
||||||
() => this.disk.checkStorage('disk_space', {
|
|
||||||
path: this.diskPath,
|
|
||||||
threshold: 500 * 1024 * 1024, // 500MB em bytes
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Verifica uso de memória (heap <150MB)
|
|
||||||
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024), // 150MB
|
|
||||||
|
|
||||||
// Verifica as conexões de banco de dados
|
|
||||||
() => this.typeOrmHealth.checkOracle(),
|
|
||||||
() => this.typeOrmHealth.checkPostgres(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('db')
|
|
||||||
@HealthCheck()
|
|
||||||
@ApiOperation({ summary: 'Verificar saúde das conexões de banco de dados' })
|
|
||||||
checkDatabase() {
|
|
||||||
return this.health.check([
|
|
||||||
() => this.typeOrmHealth.checkOracle(),
|
|
||||||
() => this.typeOrmHealth.checkPostgres(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('memory')
|
|
||||||
@HealthCheck()
|
|
||||||
@ApiOperation({ summary: 'Verificar uso de memória' })
|
|
||||||
checkMemory() {
|
|
||||||
return this.health.check([
|
|
||||||
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
|
|
||||||
() => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('disk')
|
|
||||||
@HealthCheck()
|
|
||||||
@ApiOperation({ summary: 'Verificar espaço em disco' })
|
|
||||||
checkDisk() {
|
|
||||||
return this.health.check([
|
|
||||||
// Verificar espaço em disco usando porcentagem
|
|
||||||
() => this.disk.checkStorage('disk_percent', {
|
|
||||||
path: this.diskPath,
|
|
||||||
thresholdPercent: 0.8,
|
|
||||||
}),
|
|
||||||
// Verificar espaço em disco usando valor absoluto
|
|
||||||
() => this.disk.checkStorage('disk_space', {
|
|
||||||
path: this.diskPath,
|
|
||||||
threshold: 500 * 1024 * 1024,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Get('pool')
|
|
||||||
@HealthCheck()
|
|
||||||
@ApiOperation({ summary: 'Verificar estatísticas do pool de conexões' })
|
|
||||||
checkPoolStats() {
|
|
||||||
return this.health.check([
|
|
||||||
() => this.dbPoolStats.checkOraclePoolStats(),
|
|
||||||
() => this.dbPoolStats.checkPostgresPoolStats(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TerminusModule } from '@nestjs/terminus';
|
|
||||||
import { HttpModule } from '@nestjs/axios';
|
|
||||||
import { HealthController } from './health.controller';
|
|
||||||
import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
|
|
||||||
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
|
|
||||||
import { metricProviders } from './metrics/metrics.config';
|
|
||||||
import { CustomMetricsService } from './metrics/custom.metrics';
|
|
||||||
import { MetricsInterceptor } from './metrics/metrics.interceptor';
|
|
||||||
import { HealthAlertService } from './alert/health-alert.service';
|
|
||||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TerminusModule,
|
|
||||||
HttpModule,
|
|
||||||
ConfigModule,
|
|
||||||
PrometheusModule.register({
|
|
||||||
path: '/metrics',
|
|
||||||
defaultMetrics: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
controllers: [HealthController],
|
|
||||||
providers: [
|
|
||||||
TypeOrmHealthIndicator,
|
|
||||||
DbPoolStatsIndicator,
|
|
||||||
CustomMetricsService,
|
|
||||||
HealthAlertService,
|
|
||||||
{
|
|
||||||
provide: APP_INTERCEPTOR,
|
|
||||||
useClass: MetricsInterceptor,
|
|
||||||
},
|
|
||||||
...metricProviders,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
CustomMetricsService,
|
|
||||||
HealthAlertService,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class HealthModule {}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
HealthIndicator,
|
|
||||||
HealthIndicatorResult,
|
|
||||||
HealthCheckError, // Import HealthCheckError for better terminus integration
|
|
||||||
} from '@nestjs/terminus';
|
|
||||||
import { InjectConnection } from '@nestjs/typeorm';
|
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
|
|
||||||
const ORACLE_HEALTH_KEY = 'oracle_pool_stats';
|
|
||||||
const POSTGRES_HEALTH_KEY = 'postgres_pool_stats';
|
|
||||||
const ORACLE_PROGRAM_PATTERN = 'node%'; // Default pattern for Oracle
|
|
||||||
const POSTGRES_APP_NAME_PATTERN = 'nodejs%'; // Default pattern for PostgreSQL
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DbPoolStatsIndicator extends HealthIndicator {
|
|
||||||
private readonly logger = new Logger(DbPoolStatsIndicator.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
|
|
||||||
@InjectConnection('oracle') private readonly oracleDataSource: DataSource,
|
|
||||||
@InjectConnection('postgres') private readonly postgresDataSource: DataSource,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifica a integridade do pool de conexões Oracle consultando V$SESSION.
|
|
||||||
* Observações: Requer privilégios SELECT em V$SESSION e depende da coluna PROGRAM.
|
|
||||||
* Isso verifica principalmente a acessibilidade do banco de dados e o sucesso da execução da consulta.
|
|
||||||
* Considere estatísticas de pool em nível de driver para obter uma integridade de pool mais precisa, se disponível.
|
|
||||||
*
|
|
||||||
* @param key Custom key for the health indicator component.
|
|
||||||
* @param programLike Optional pattern to match the PROGRAM column in V$SESSION.
|
|
||||||
*/
|
|
||||||
async checkOraclePoolStats(
|
|
||||||
key: string = ORACLE_HEALTH_KEY,
|
|
||||||
programLike: string = ORACLE_PROGRAM_PATTERN,
|
|
||||||
): Promise<HealthIndicatorResult> {
|
|
||||||
try {
|
|
||||||
// Usar parâmetros de consulta é uma boa prática, embora menos crítica para LIKE com um padrão fixo.
|
|
||||||
// Oracle usa a sintaxe :paramName
|
|
||||||
const query = `
|
|
||||||
SELECT
|
|
||||||
COUNT(*) AS "totalConnections" -- Use quoted identifiers if needed, or match case below
|
|
||||||
FROM
|
|
||||||
V$SESSION
|
|
||||||
WHERE
|
|
||||||
TYPE = 'USER'
|
|
||||||
AND PROGRAM LIKE :pattern
|
|
||||||
`;
|
|
||||||
const params = { pattern: programLike };
|
|
||||||
|
|
||||||
const results: { totalConnections: number | string }[] =
|
|
||||||
await this.oracleDataSource.query(query, [params.pattern]); // Pass parameters as an array for Oracle usually
|
|
||||||
|
|
||||||
if (!results || results.length === 0) {
|
|
||||||
this.logger.warn(`Oracle V$SESSION query returned no results for pattern '${programLike}'`);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalConnections = parseInt(String(results?.[0]?.totalConnections ?? 0), 10);
|
|
||||||
|
|
||||||
if (isNaN(totalConnections)) {
|
|
||||||
throw new Error('Failed to parse totalConnections from Oracle V$SESSION query result.');
|
|
||||||
}
|
|
||||||
// isHealthy é verdadeiro se a consulta for executada sem gerar um erro.
|
|
||||||
// Adicione lógica aqui se contagens de conexão específicas indicarem estado não íntegro (por exemplo, > poolMax)
|
|
||||||
const isHealthy = true;
|
|
||||||
const details = {
|
|
||||||
totalConnections: totalConnections,
|
|
||||||
programPattern: programLike,
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getStatus(key, isHealthy, details);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Oracle pool stats check failed for key "${key}": ${error.message}`, error.stack);
|
|
||||||
throw new HealthCheckError(
|
|
||||||
`${key} check failed`,
|
|
||||||
this.getStatus(key, false, { message: error.message }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifica a integridade do pool de conexões do PostgreSQL consultando pg_stat_activity.
|
|
||||||
* Observações: Depende de o application_name estar definido corretamente na string de conexão ou nas opções.
|
|
||||||
* Isso verifica principalmente a acessibilidade do banco de dados e o sucesso da execução da consulta.
|
|
||||||
* Considere estatísticas de pool em nível de driver para obter uma integridade de pool mais precisa, se disponível.
|
|
||||||
*
|
|
||||||
* @param key Custom key for the health indicator component.
|
|
||||||
* @param appNameLike Optional pattern to match the application_name column.
|
|
||||||
*/
|
|
||||||
async checkPostgresPoolStats(
|
|
||||||
key: string = POSTGRES_HEALTH_KEY,
|
|
||||||
appNameLike: string = POSTGRES_APP_NAME_PATTERN,
|
|
||||||
): Promise<HealthIndicatorResult> {
|
|
||||||
try {
|
|
||||||
const query = `
|
|
||||||
SELECT
|
|
||||||
count(*) AS "totalConnections",
|
|
||||||
sum(CASE WHEN state = 'active' THEN 1 ELSE 0 END) AS "activeConnections",
|
|
||||||
sum(CASE WHEN state = 'idle' THEN 1 ELSE 0 END) AS "idleConnections",
|
|
||||||
sum(CASE WHEN state = 'idle in transaction' THEN 1 ELSE 0 END) AS "idleInTransactionConnections"
|
|
||||||
FROM
|
|
||||||
pg_stat_activity
|
|
||||||
WHERE
|
|
||||||
datname = current_database()
|
|
||||||
AND application_name LIKE $1
|
|
||||||
`;
|
|
||||||
const params = [appNameLike];
|
|
||||||
|
|
||||||
const results: {
|
|
||||||
totalConnections: string | number;
|
|
||||||
activeConnections: string | number;
|
|
||||||
idleConnections: string | number;
|
|
||||||
idleInTransactionConnections: string | number;
|
|
||||||
}[] = await this.postgresDataSource.query(query, params);
|
|
||||||
|
|
||||||
|
|
||||||
if (!results || results.length === 0) {
|
|
||||||
|
|
||||||
throw new Error('PostgreSQL pg_stat_activity query returned no results unexpectedly.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = results[0];
|
|
||||||
|
|
||||||
const totalConnections = parseInt(String(result.totalConnections ?? 0), 10);
|
|
||||||
const activeConnections = parseInt(String(result.activeConnections ?? 0), 10);
|
|
||||||
const idleConnections = parseInt(String(result.idleConnections ?? 0), 10);
|
|
||||||
const idleInTransactionConnections = parseInt(String(result.idleInTransactionConnections ?? 0), 10);
|
|
||||||
|
|
||||||
// Validate parsing
|
|
||||||
if (isNaN(totalConnections) || isNaN(activeConnections) || isNaN(idleConnections) || isNaN(idleInTransactionConnections)) {
|
|
||||||
throw new Error('Failed to parse connection counts from PostgreSQL pg_stat_activity query result.');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const isHealthy = true;
|
|
||||||
const details = {
|
|
||||||
totalConnections,
|
|
||||||
activeConnections,
|
|
||||||
idleConnections,
|
|
||||||
idleInTransactionConnections,
|
|
||||||
applicationNamePattern: appNameLike,
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.getStatus(key, isHealthy, details);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`PostgreSQL pool stats check failed for key "${key}": ${error.message}`, error.stack);
|
|
||||||
|
|
||||||
throw new HealthCheckError(
|
|
||||||
`${key} check failed`,
|
|
||||||
this.getStatus(key, false, { message: error.message }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience method to run all pool checks defined in this indicator.
|
|
||||||
* You would typically call this from your main HealthController.
|
|
||||||
*/
|
|
||||||
async checkAllPools() : Promise<HealthIndicatorResult[]> {
|
|
||||||
const results = await Promise.allSettled([
|
|
||||||
this.checkOraclePoolStats(),
|
|
||||||
this.checkPostgresPoolStats()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Processa os resultados para se ajustar à estrutura do Terminus, se necessário, ou retorna diretamente
|
|
||||||
// Observações: Métodos individuais já retornam HealthIndicatorResult ou lançam HealthCheckError
|
|
||||||
// Este método pode não ser estritamente necessário se você chamar verificações individuais no controlador.
|
|
||||||
// Para simplificar, vamos supor que o controlador chama as verificações individuais.
|
|
||||||
// Se você quisesse que esse método retornasse um único status, precisaria de mais lógica.
|
|
||||||
// Relançar erros ou agregar status.
|
|
||||||
|
|
||||||
// Example: Log results (individual methods handle the Terminus return/error)
|
|
||||||
results.forEach(result => {
|
|
||||||
if (result.status === 'rejected') {
|
|
||||||
// Already logged and thrown as HealthCheckError inside the check methods
|
|
||||||
} else {
|
|
||||||
// Optionally log success details
|
|
||||||
this.logger.log(`Pool check successful: ${JSON.stringify(result.value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return results
|
|
||||||
.filter((r): r is PromiseFulfilledResult<HealthIndicatorResult> => r.status === 'fulfilled')
|
|
||||||
.map(r => r.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
|
|
||||||
import { InjectConnection } from '@nestjs/typeorm';
|
|
||||||
import { Connection, DataSource } from 'typeorm';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TypeOrmHealthIndicator extends HealthIndicator {
|
|
||||||
constructor(
|
|
||||||
@InjectConnection('oracle') private oracleConnection: DataSource,
|
|
||||||
@InjectConnection('postgres') private postgresConnection: DataSource,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkOracle(): Promise<HealthIndicatorResult> {
|
|
||||||
const key = 'oracle';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isHealthy = this.oracleConnection.isInitialized;
|
|
||||||
|
|
||||||
const result = this.getStatus(key, isHealthy);
|
|
||||||
|
|
||||||
if (isHealthy) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HealthCheckError('Oracle healthcheck failed', result);
|
|
||||||
} catch (error) {
|
|
||||||
const result = this.getStatus(key, false, { message: error.message });
|
|
||||||
throw new HealthCheckError('Oracle healthcheck failed', result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkPostgres(): Promise<HealthIndicatorResult> {
|
|
||||||
const key = 'postgres';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isHealthy = this.postgresConnection.isInitialized;
|
|
||||||
|
|
||||||
const result = this.getStatus(key, isHealthy);
|
|
||||||
|
|
||||||
if (isHealthy) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HealthCheckError('Postgres healthcheck failed', result);
|
|
||||||
} catch (error) {
|
|
||||||
const result = this.getStatus(key, false, { message: error.message });
|
|
||||||
throw new HealthCheckError('Postgres healthcheck failed', result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectMetric } from '@willsoto/nestjs-prometheus';
|
|
||||||
import { Counter, Gauge, Histogram } from 'prom-client';
|
|
||||||
import { InjectConnection } from '@nestjs/typeorm';
|
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CustomMetricsService {
|
|
||||||
constructor(
|
|
||||||
@InjectMetric('http_request_total')
|
|
||||||
private readonly requestCounter: Counter<string>,
|
|
||||||
|
|
||||||
@InjectMetric('http_request_duration_seconds')
|
|
||||||
private readonly requestDuration: Histogram<string>,
|
|
||||||
|
|
||||||
@InjectMetric('api_memory_usage_bytes')
|
|
||||||
private readonly memoryGauge: Gauge<string>,
|
|
||||||
|
|
||||||
@InjectMetric('api_db_connection_pool_used')
|
|
||||||
private readonly dbPoolUsedGauge: Gauge<string>,
|
|
||||||
|
|
||||||
@InjectMetric('api_db_connection_pool_total')
|
|
||||||
private readonly dbPoolTotalGauge: Gauge<string>,
|
|
||||||
|
|
||||||
@InjectMetric('api_db_query_duration_seconds')
|
|
||||||
private readonly dbQueryDuration: Histogram<string>,
|
|
||||||
|
|
||||||
@InjectConnection('oracle')
|
|
||||||
private oracleConnection: DataSource,
|
|
||||||
|
|
||||||
@InjectConnection('postgres')
|
|
||||||
private postgresConnection: DataSource,
|
|
||||||
) {
|
|
||||||
// Iniciar coleta de métricas de memória
|
|
||||||
this.startMemoryMetrics();
|
|
||||||
|
|
||||||
// Iniciar coleta de métricas do pool de conexões
|
|
||||||
this.startDbPoolMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
recordHttpRequest(method: string, route: string, statusCode: number): void {
|
|
||||||
this.requestCounter.inc({ method, route, statusCode: statusCode.toString() });
|
|
||||||
}
|
|
||||||
|
|
||||||
startTimingRequest(): (labels?: Record<string, string>) => void {
|
|
||||||
const end = this.requestDuration.startTimer();
|
|
||||||
return (labels?: Record<string, string>) => end(labels);
|
|
||||||
}
|
|
||||||
|
|
||||||
recordDbQueryDuration(db: 'oracle' | 'postgres', operation: string, durationMs: number): void {
|
|
||||||
this.dbQueryDuration.observe({ db, operation }, durationMs / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private startMemoryMetrics(): void {
|
|
||||||
// Coletar métricas de memória a cada 15 segundos
|
|
||||||
setInterval(() => {
|
|
||||||
const memoryUsage = process.memoryUsage();
|
|
||||||
this.memoryGauge.set({ type: 'rss' }, memoryUsage.rss);
|
|
||||||
this.memoryGauge.set({ type: 'heapTotal' }, memoryUsage.heapTotal);
|
|
||||||
this.memoryGauge.set({ type: 'heapUsed' }, memoryUsage.heapUsed);
|
|
||||||
this.memoryGauge.set({ type: 'external' }, memoryUsage.external);
|
|
||||||
}, 15000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private startDbPoolMetrics(): void {
|
|
||||||
// Coletar métricas do pool de conexões a cada 15 segundos
|
|
||||||
setInterval(async () => {
|
|
||||||
try {
|
|
||||||
// Tente obter estatísticas do pool do Oracle
|
|
||||||
// Nota: depende da implementação específica do OracleDB
|
|
||||||
if (this.oracleConnection && this.oracleConnection.driver) {
|
|
||||||
const oraclePoolStats = (this.oracleConnection.driver as any).pool?.getStatistics?.();
|
|
||||||
if (oraclePoolStats) {
|
|
||||||
this.dbPoolUsedGauge.set({ db: 'oracle' }, oraclePoolStats.busy || 0);
|
|
||||||
this.dbPoolTotalGauge.set({ db: 'oracle' }, oraclePoolStats.poolMax || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tente obter estatísticas do pool do Postgres
|
|
||||||
// Nota: depende da implementação específica do TypeORM
|
|
||||||
if (this.postgresConnection && this.postgresConnection.driver) {
|
|
||||||
const pgPoolStats = (this.postgresConnection.driver as any).pool;
|
|
||||||
if (pgPoolStats) {
|
|
||||||
this.dbPoolUsedGauge.set({ db: 'postgres' }, pgPoolStats.totalCount - pgPoolStats.idleCount || 0);
|
|
||||||
this.dbPoolTotalGauge.set({ db: 'postgres' }, pgPoolStats.totalCount || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erro ao coletar métricas do pool de conexões:', error);
|
|
||||||
}
|
|
||||||
}, 15000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import {
|
|
||||||
makeCounterProvider,
|
|
||||||
makeGaugeProvider,
|
|
||||||
makeHistogramProvider
|
|
||||||
} from '@willsoto/nestjs-prometheus';
|
|
||||||
|
|
||||||
export const metricProviders = [
|
|
||||||
// Contador de requisições HTTP
|
|
||||||
makeCounterProvider({
|
|
||||||
name: 'http_request_total',
|
|
||||||
help: 'Total de requisições HTTP',
|
|
||||||
labelNames: ['method', 'route', 'statusCode'],
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Histograma de duração de requisições HTTP
|
|
||||||
makeHistogramProvider({
|
|
||||||
name: 'http_request_duration_seconds',
|
|
||||||
help: 'Duração das requisições HTTP em segundos',
|
|
||||||
labelNames: ['method', 'route', 'error'], // 👈 adicionado "error"
|
|
||||||
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Gauge para uso de memória
|
|
||||||
makeGaugeProvider({
|
|
||||||
name: 'api_memory_usage_bytes',
|
|
||||||
help: 'Uso de memória da aplicação em bytes',
|
|
||||||
labelNames: ['type'],
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Gauge para conexões de banco de dados usadas
|
|
||||||
makeGaugeProvider({
|
|
||||||
name: 'api_db_connection_pool_used',
|
|
||||||
help: 'Número de conexões de banco de dados em uso',
|
|
||||||
labelNames: ['db'],
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Gauge para total de conexões no pool de banco de dados
|
|
||||||
makeGaugeProvider({
|
|
||||||
name: 'api_db_connection_pool_total',
|
|
||||||
help: 'Número total de conexões no pool de banco de dados',
|
|
||||||
labelNames: ['db'],
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Histograma para duração de consultas de banco de dados
|
|
||||||
makeHistogramProvider({
|
|
||||||
name: 'api_db_query_duration_seconds',
|
|
||||||
help: 'Duração das consultas de banco de dados em segundos',
|
|
||||||
labelNames: ['db', 'operation'],
|
|
||||||
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2],
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
NestInterceptor,
|
|
||||||
ExecutionContext,
|
|
||||||
CallHandler,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { tap } from 'rxjs/operators';
|
|
||||||
import { CustomMetricsService } from './custom.metrics';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class MetricsInterceptor implements NestInterceptor {
|
|
||||||
constructor(private metricsService: CustomMetricsService) {}
|
|
||||||
|
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
||||||
if (context.getType() !== 'http') {
|
|
||||||
return next.handle();
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
|
||||||
const { method, url } = request;
|
|
||||||
|
|
||||||
// Simplificar a rota para evitar cardinalidade alta no Prometheus
|
|
||||||
// Ex: /users/123 -> /users/:id
|
|
||||||
const route = this.normalizeRoute(url);
|
|
||||||
|
|
||||||
// Inicia o timer para medir a duração da requisição
|
|
||||||
const endTimer = this.metricsService.startTimingRequest();
|
|
||||||
|
|
||||||
return next.handle().pipe(
|
|
||||||
tap({
|
|
||||||
next: (data) => {
|
|
||||||
const response = context.switchToHttp().getResponse();
|
|
||||||
const statusCode = response.statusCode;
|
|
||||||
|
|
||||||
// Registra a requisição concluída
|
|
||||||
this.metricsService.recordHttpRequest(method, route, statusCode);
|
|
||||||
|
|
||||||
// Finaliza o timer com labels adicionais
|
|
||||||
endTimer({ method, route });
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
// Determina o código de status do erro
|
|
||||||
const statusCode = error.status || 500;
|
|
||||||
|
|
||||||
// Registra a requisição com erro
|
|
||||||
this.metricsService.recordHttpRequest(method, route, statusCode);
|
|
||||||
|
|
||||||
// Finaliza o timer com labels adicionais
|
|
||||||
endTimer({ method, route, error: 'true' });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeRoute(url: string): string {
|
|
||||||
// Remove query parameters
|
|
||||||
const path = url.split('?')[0];
|
|
||||||
|
|
||||||
// Normaliza rotas com IDs e outros parâmetros dinâmicos
|
|
||||||
// Por exemplo, /users/123 -> /users/:id
|
|
||||||
return path.replace(/\/[0-9a-f]{8,}|\/[0-9]+/g, '/:id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
/*
|
/*
|
||||||
https://docs.nestjs.com/controllers#controllers
|
https://docs.nestjs.com/controllers#controllers
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
import { LogisticController } from './logistic.controller';
|
import { LogisticController } from './logistic.controller';
|
||||||
import { LogisticService } from './logistic.service';
|
import { LogisticService } from './logistic.service';
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Get, HttpException, HttpStatus, Injectable, Query, UseGuards } from '@nestjs/common';
|
import {
|
||||||
|
Get,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { createOracleConfig } from '../core/configs/typeorm.oracle.config';
|
import { createOracleConfig } from '../core/configs/typeorm.oracle.config';
|
||||||
import { createPostgresConfig } from '../core/configs/typeorm.postgres.config';
|
import { createPostgresConfig } from '../core/configs/typeorm.postgres.config';
|
||||||
import { CarOutDelivery } from '../core/models/car-out-delivery.model';
|
import { CarOutDelivery } from '../core/models/car-out-delivery.model';
|
||||||
@@ -8,16 +15,15 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LogisticService {
|
export class LogisticService {
|
||||||
constructor(private readonly configService: ConfigService) {}
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
async getExpedicao() {
|
async getExpedicao() {
|
||||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
try {
|
try {
|
||||||
|
const sqlWMS = `select dados.*,
|
||||||
const sqlWMS = `select dados.*,
|
|
||||||
( select count(distinct v.numero_carga) quantidade_cargas_embarcadas
|
( select count(distinct v.numero_carga) quantidade_cargas_embarcadas
|
||||||
from volume v, carga c2
|
from volume v, carga c2
|
||||||
where v.numero_carga = c2.numero
|
where v.numero_carga = c2.numero
|
||||||
@@ -52,8 +58,7 @@ export class LogisticService {
|
|||||||
where dados.data_saida >= current_date
|
where dados.data_saida >= current_date
|
||||||
ORDER BY dados.data_saida desc `;
|
ORDER BY dados.data_saida desc `;
|
||||||
|
|
||||||
|
const sql = `SELECT COUNT(DISTINCT PCCARREG.NUMCAR) as "qtde"
|
||||||
const sql = `SELECT COUNT(DISTINCT PCCARREG.NUMCAR) as "qtde"
|
|
||||||
,SUM(PCPEDI.QT * PCPRODUT.PESOBRUTO) as "totalKG"
|
,SUM(PCPEDI.QT * PCPRODUT.PESOBRUTO) as "totalKG"
|
||||||
,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_nao_iniciado"
|
,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_nao_iniciado"
|
||||||
,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NOT NULL
|
,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NOT NULL
|
||||||
@@ -74,37 +79,40 @@ export class LogisticService {
|
|||||||
AND PCPEDI.TIPOENTREGA IN ('EN', 'EF')
|
AND PCPEDI.TIPOENTREGA IN ('EN', 'EF')
|
||||||
AND PCCARREG.DTSAIDA = TRUNC(SYSDATE)`;
|
AND PCCARREG.DTSAIDA = TRUNC(SYSDATE)`;
|
||||||
|
|
||||||
const mov = await queryRunner.manager.query(sqlWMS);
|
const mov = await queryRunner.manager.query(sqlWMS);
|
||||||
|
|
||||||
const hoje = new Date();
|
const hoje = new Date();
|
||||||
|
|
||||||
let amanha = new Date(hoje);
|
let amanha = new Date(hoje);
|
||||||
amanha.setDate(hoje.getDate() + 1);
|
amanha.setDate(hoje.getDate() + 1);
|
||||||
const amanhaString = amanha.toISOString().split('T')[0];
|
const amanhaString = amanha.toISOString().split('T')[0];
|
||||||
amanha = new Date(amanhaString);
|
amanha = new Date(amanhaString);
|
||||||
|
|
||||||
console.log(amanha);
|
console.log(amanha);
|
||||||
console.log(JSON.stringify(mov));
|
console.log(JSON.stringify(mov));
|
||||||
|
|
||||||
const movFiltered = mov.filter((m) => m.data_saida.toISOString().split('T')[0] == amanha.toISOString().split('T')[0]);
|
const movFiltered = mov.filter(
|
||||||
|
(m) =>
|
||||||
|
m.data_saida.toISOString().split('T')[0] ==
|
||||||
|
amanha.toISOString().split('T')[0],
|
||||||
|
);
|
||||||
|
|
||||||
return movFiltered;
|
return movFiltered;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
await dataSource.destroy();
|
await dataSource.destroy();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getDeliveries(placa: string) {
|
async getDeliveries(placa: string) {
|
||||||
const dataSource = new DataSource(createOracleConfig(this.configService));
|
const dataSource = new DataSource(createOracleConfig(this.configService));
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
try {
|
try {
|
||||||
|
const sql = `SELECT PCCARREG.NUMCAR as "id"
|
||||||
const sql = `SELECT PCCARREG.NUMCAR as "id"
|
|
||||||
,PCCARREG.DTSAIDA as "createDate"
|
,PCCARREG.DTSAIDA as "createDate"
|
||||||
,PCCARREG.DESTINO as "comment"
|
,PCCARREG.DESTINO as "comment"
|
||||||
,PCCARREG.TOTPESO as "weight"
|
,PCCARREG.TOTPESO as "weight"
|
||||||
@@ -129,190 +137,184 @@ export class LogisticService {
|
|||||||
AND PCCARREG.DTFECHA IS NULL
|
AND PCCARREG.DTFECHA IS NULL
|
||||||
AND PCCARREG.DTSAIDA >= TRUNC(SYSDATE)`;
|
AND PCCARREG.DTSAIDA >= TRUNC(SYSDATE)`;
|
||||||
|
|
||||||
const deliveries = await queryRunner.manager.query(sql);
|
const deliveries = await queryRunner.manager.query(sql);
|
||||||
|
|
||||||
return deliveries;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
await dataSource.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return deliveries;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
await dataSource.destroy();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getStatusCar(placa: string) {
|
async getStatusCar(placa: string) {
|
||||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
try {
|
try {
|
||||||
|
const sql = `SELECT ESTSAIDAVEICULO.CODSAIDA FROM ESTSAIDAVEICULO, PCVEICUL
|
||||||
const sql = `SELECT ESTSAIDAVEICULO.CODSAIDA FROM ESTSAIDAVEICULO, PCVEICUL
|
|
||||||
WHERE ESTSAIDAVEICULO.CODVEICULO = PCVEICUL.CODVEICULO
|
WHERE ESTSAIDAVEICULO.CODVEICULO = PCVEICUL.CODVEICULO
|
||||||
AND PCVEICUL.PLACA = '${placa}'
|
AND PCVEICUL.PLACA = '${placa}'
|
||||||
AND ESTSAIDAVEICULO.DTRETORNO IS NULL`;
|
AND ESTSAIDAVEICULO.DTRETORNO IS NULL`;
|
||||||
|
|
||||||
const outCar = await queryRunner.manager.query(sql);
|
const outCar = await queryRunner.manager.query(sql);
|
||||||
|
|
||||||
return { veiculoEmViagem: ( outCar.length > 0 ) ? true : false };
|
return { veiculoEmViagem: outCar.length > 0 ? true : false };
|
||||||
|
} catch (e) {
|
||||||
} catch (e) {
|
console.log(e);
|
||||||
console.log(e);
|
} finally {
|
||||||
} finally {
|
await queryRunner.release();
|
||||||
await queryRunner.release();
|
await dataSource.destroy();
|
||||||
await dataSource.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getEmployee() {
|
async getEmployee() {
|
||||||
const dataSource = new DataSource(createOracleConfig(this.configService));
|
const dataSource = new DataSource(createOracleConfig(this.configService));
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
try {
|
try {
|
||||||
const sql = `SELECT PCEMPR.MATRICULA as "id"
|
const sql = `SELECT PCEMPR.MATRICULA as "id"
|
||||||
,PCEMPR.NOME as "name"
|
,PCEMPR.NOME as "name"
|
||||||
,PCEMPR.FUNCAO as "fuctionName"
|
,PCEMPR.FUNCAO as "fuctionName"
|
||||||
FROM PCEMPR, PCCONSUM
|
FROM PCEMPR, PCCONSUM
|
||||||
WHERE PCEMPR.DTDEMISSAO IS NULL
|
WHERE PCEMPR.DTDEMISSAO IS NULL
|
||||||
AND PCEMPR.CODSETOR = PCCONSUM.CODSETOREXPED
|
AND PCEMPR.CODSETOR = PCCONSUM.CODSETOREXPED
|
||||||
ORDER BY PCEMPR.NOME `;
|
ORDER BY PCEMPR.NOME `;
|
||||||
const dataEmployee = await queryRunner.query(sql);
|
const dataEmployee = await queryRunner.query(sql);
|
||||||
|
|
||||||
return dataEmployee;
|
return dataEmployee;
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
await dataSource.destroy();
|
await dataSource.destroy();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createCarOut(data: CarOutDelivery) {
|
async createCarOut(data: CarOutDelivery) {
|
||||||
|
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
||||||
|
await dataSource.initialize();
|
||||||
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
try {
|
||||||
|
const sqlSequence = `SELECT ESS_SAIDAVEICULO.NEXTVAL as "id" FROM DUAL`;
|
||||||
|
const dataSequence = await queryRunner.query(sqlSequence);
|
||||||
|
let i = 0;
|
||||||
|
let helperId1 = 0;
|
||||||
|
let helperId2 = 0;
|
||||||
|
let helperId3 = 0;
|
||||||
|
const image1 = '';
|
||||||
|
const image2 = '';
|
||||||
|
const image3 = '';
|
||||||
|
const image4 = '';
|
||||||
|
|
||||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
data.helpers.forEach((helper) => {
|
||||||
await dataSource.initialize();
|
switch (i) {
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
case 0:
|
||||||
await queryRunner.connect();
|
helperId1 = helper.id;
|
||||||
await queryRunner.startTransaction();
|
break;
|
||||||
try {
|
case 1:
|
||||||
|
helperId2 = helper.id;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
helperId3 = helper.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
});
|
||||||
|
|
||||||
const sqlSequence = `SELECT ESS_SAIDAVEICULO.NEXTVAL as "id" FROM DUAL`;
|
for (let y = 0; y < data.photos.length; y++) {
|
||||||
const dataSequence = await queryRunner.query(sqlSequence);
|
const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
|
||||||
let i = 0;
|
|
||||||
let helperId1 = 0;
|
|
||||||
let helperId2 = 0;
|
|
||||||
let helperId3 = 0;
|
|
||||||
const image1 = '';
|
|
||||||
const image2 = '';
|
|
||||||
const image3 = '';
|
|
||||||
const image4 = '';
|
|
||||||
|
|
||||||
data.helpers.forEach(helper => {
|
|
||||||
switch (i) {
|
|
||||||
case 0:
|
|
||||||
helperId1 = helper.id;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
helperId2 = helper.id;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
helperId3 = helper.id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let y = 0; y < data.photos.length; y++) {
|
|
||||||
const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
|
|
||||||
VALUES (${dataSequence[0].id}, 'SA', '${data.photos[y]}' )`;
|
VALUES (${dataSequence[0].id}, 'SA', '${data.photos[y]}' )`;
|
||||||
await queryRunner.query(sqlImage);
|
await queryRunner.query(sqlImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sqlSaidaVeiculo = `INSERT INTO ESTSAIDAVEICULO ( CODSAIDA, CODVEICULO, DTSAIDA, QTAJUDANTES, CODFUNCSAIDA )
|
const sqlSaidaVeiculo = `INSERT INTO ESTSAIDAVEICULO ( CODSAIDA, CODVEICULO, DTSAIDA, QTAJUDANTES, CODFUNCSAIDA )
|
||||||
VALUES ( ${dataSequence[0].id}, ${data.vehicleCode}, SYSDATE, ${data.helpers.length},
|
VALUES ( ${dataSequence[0].id}, ${data.vehicleCode}, SYSDATE, ${data.helpers.length},
|
||||||
${data.userCode} )`;
|
${data.userCode} )`;
|
||||||
await queryRunner.query(sqlSaidaVeiculo);
|
await queryRunner.query(sqlSaidaVeiculo);
|
||||||
|
|
||||||
for (let y = 0; y < data.numberLoading.length; y++) {
|
for (let y = 0; y < data.numberLoading.length; y++) {
|
||||||
const sqlLoading = `INSERT INTO ESTSAIDAVEICULOCARREG ( CODSAIDA, NUMCAR )
|
const sqlLoading = `INSERT INTO ESTSAIDAVEICULOCARREG ( CODSAIDA, NUMCAR )
|
||||||
VALUES ( ${dataSequence[0].id}, ${data.numberLoading[y]})`;
|
VALUES ( ${dataSequence[0].id}, ${data.numberLoading[y]})`;
|
||||||
await queryRunner.query(sqlLoading);
|
await queryRunner.query(sqlLoading);
|
||||||
|
|
||||||
const sql = `UPDATE PCCARREG SET
|
const sql = `UPDATE PCCARREG SET
|
||||||
DTSAIDAVEICULO = SYSDATE
|
DTSAIDAVEICULO = SYSDATE
|
||||||
,CODFUNCAJUD = ${helperId1}
|
,CODFUNCAJUD = ${helperId1}
|
||||||
,CODFUNCAJUD2 = ${helperId2}
|
,CODFUNCAJUD2 = ${helperId2}
|
||||||
,CODFUNCAJUD3 = ${helperId3}
|
,CODFUNCAJUD3 = ${helperId3}
|
||||||
,KMINICIAL = ${data.startKm}
|
,KMINICIAL = ${data.startKm}
|
||||||
WHERE NUMCAR = ${data.numberLoading[y]}`;
|
WHERE NUMCAR = ${data.numberLoading[y]}`;
|
||||||
await queryRunner.query(sql);
|
await queryRunner.query(sql);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
return { message: 'Dados da saída de veículo gravada com sucesso!' };
|
||||||
|
} catch (e) {
|
||||||
return { message: 'Dados da saída de veículo gravada com sucesso!'}
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw e;
|
||||||
} catch (e) {
|
} finally {
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.release();
|
||||||
throw e;
|
await dataSource.destroy();
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
await dataSource.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createCarIn(data: CarInDelivery) {
|
async createCarIn(data: CarInDelivery) {
|
||||||
|
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
||||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
await dataSource.initialize();
|
||||||
await dataSource.initialize();
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
await queryRunner.connect();
|
||||||
await queryRunner.connect();
|
await queryRunner.startTransaction();
|
||||||
await queryRunner.startTransaction();
|
try {
|
||||||
try {
|
const sqlOutCar = `SELECT ESTSAIDAVEICULO.CODSAIDA as "id"
|
||||||
|
|
||||||
const sqlOutCar = `SELECT ESTSAIDAVEICULO.CODSAIDA as "id"
|
|
||||||
FROM PCCARREG, PCVEICUL, ESTSAIDAVEICULO, ESTSAIDAVEICULOCARREG
|
FROM PCCARREG, PCVEICUL, ESTSAIDAVEICULO, ESTSAIDAVEICULOCARREG
|
||||||
WHERE PCCARREG.CODVEICULO = PCVEICUL.CODVEICULO
|
WHERE PCCARREG.CODVEICULO = PCVEICUL.CODVEICULO
|
||||||
AND PCCARREG.NUMCAR = ESTSAIDAVEICULOCARREG.NUMCAR
|
AND PCCARREG.NUMCAR = ESTSAIDAVEICULOCARREG.NUMCAR
|
||||||
AND ESTSAIDAVEICULOCARREG.CODSAIDA = ESTSAIDAVEICULO.CODSAIDA
|
AND ESTSAIDAVEICULOCARREG.CODSAIDA = ESTSAIDAVEICULO.CODSAIDA
|
||||||
-- AND ESTSAIDAVEICULO.DTRETORNO IS NULL
|
-- AND ESTSAIDAVEICULO.DTRETORNO IS NULL
|
||||||
AND PCVEICUL.PLACA = '${data.licensePlate}'`;
|
AND PCVEICUL.PLACA = '${data.licensePlate}'`;
|
||||||
const dataOutCar = await queryRunner.query(sqlOutCar);
|
const dataOutCar = await queryRunner.query(sqlOutCar);
|
||||||
|
|
||||||
if ( dataOutCar.length == 0 ) {
|
if (dataOutCar.length == 0) {
|
||||||
throw new HttpException('Não foi localiza viagens em aberto para este veículo.', HttpStatus.BAD_REQUEST );
|
throw new HttpException(
|
||||||
}
|
'Não foi localiza viagens em aberto para este veículo.',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const i = 0;
|
const i = 0;
|
||||||
const image1 = '';
|
const image1 = '';
|
||||||
const image2 = '';
|
const image2 = '';
|
||||||
const image3 = '';
|
const image3 = '';
|
||||||
const image4 = '';
|
const image4 = '';
|
||||||
|
|
||||||
for (let y = 0; y < data.invoices.length; y++) {
|
for (let y = 0; y < data.invoices.length; y++) {
|
||||||
const invoice = data.invoices[y];
|
const invoice = data.invoices[y];
|
||||||
const sqlInvoice = `INSERT INTO ESTRETORNONF ( CODSAIDA, NUMCAR, NUMNOTA, SITUACAO, MOTIVO )
|
const sqlInvoice = `INSERT INTO ESTRETORNONF ( CODSAIDA, NUMCAR, NUMNOTA, SITUACAO, MOTIVO )
|
||||||
VALUES ( ${dataOutCar[0].id}, ${invoice.loadingNumber}, ${invoice.invoiceNumber},
|
VALUES ( ${dataOutCar[0].id}, ${invoice.loadingNumber}, ${invoice.invoiceNumber},
|
||||||
'${invoice.status}', '${invoice.reasonText}')`;
|
'${invoice.status}', '${invoice.reasonText}')`;
|
||||||
await queryRunner.query(sqlInvoice);
|
await queryRunner.query(sqlInvoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCarreg = `UPDATE PCCARREG SET
|
const updateCarreg = `UPDATE PCCARREG SET
|
||||||
PCCARREG.DTRETORNO = SYSDATE
|
PCCARREG.DTRETORNO = SYSDATE
|
||||||
,PCCARREG.KMFINAL = ${data.finalKm}
|
,PCCARREG.KMFINAL = ${data.finalKm}
|
||||||
WHERE PCCARREG.NUMCAR IN ( SELECT SC.NUMCAR
|
WHERE PCCARREG.NUMCAR IN ( SELECT SC.NUMCAR
|
||||||
FROM ESTSAIDAVEICULOCARREG SC
|
FROM ESTSAIDAVEICULOCARREG SC
|
||||||
WHERE SC.CODSAIDA = ${dataOutCar[0].id} )`;
|
WHERE SC.CODSAIDA = ${dataOutCar[0].id} )`;
|
||||||
await queryRunner.query(updateCarreg);
|
await queryRunner.query(updateCarreg);
|
||||||
|
|
||||||
for (let i = 0; i < data.images.length; i++) {
|
for (let i = 0; i < data.images.length; i++) {
|
||||||
const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
|
const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
|
||||||
VALUES (${dataOutCar[0].id}, 'RE', '${data.images[i]}' )`;
|
VALUES (${dataOutCar[0].id}, 'RE', '${data.images[i]}' )`;
|
||||||
await queryRunner.query(sqlImage);
|
await queryRunner.query(sqlImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sqlInCar = `UPDATE ESTSAIDAVEICULO SET
|
const sqlInCar = `UPDATE ESTSAIDAVEICULO SET
|
||||||
ESTSAIDAVEICULO.DTRETORNO = SYSDATE
|
ESTSAIDAVEICULO.DTRETORNO = SYSDATE
|
||||||
,ESTSAIDAVEICULO.QTPALETES_PBR = ${data.qtdPaletesPbr}
|
,ESTSAIDAVEICULO.QTPALETES_PBR = ${data.qtdPaletesPbr}
|
||||||
,ESTSAIDAVEICULO.QTPALETES_CIM = ${data.qtdPaletesCim}
|
,ESTSAIDAVEICULO.QTPALETES_CIM = ${data.qtdPaletesCim}
|
||||||
@@ -323,25 +325,23 @@ export class LogisticService {
|
|||||||
,ESTSAIDAVEICULO.OBSSOBRA = '${data.observationRemnant}'
|
,ESTSAIDAVEICULO.OBSSOBRA = '${data.observationRemnant}'
|
||||||
WHERE ESTSAIDAVEICULO.CODSAIDA = ${dataOutCar[0].id}`;
|
WHERE ESTSAIDAVEICULO.CODSAIDA = ${dataOutCar[0].id}`;
|
||||||
|
|
||||||
await queryRunner.query(sqlInCar);
|
await queryRunner.query(sqlInCar);
|
||||||
for (let i = 0; i < data.imagesRemnant.length; i++) {
|
for (let i = 0; i < data.imagesRemnant.length; i++) {
|
||||||
const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
|
const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
|
||||||
VALUES (${dataOutCar[0].id}, 'SO', '${data.imagesRemnant[i]}' )`;
|
VALUES (${dataOutCar[0].id}, 'SO', '${data.imagesRemnant[i]}' )`;
|
||||||
await queryRunner.query(sqlImage);
|
await queryRunner.query(sqlImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
return { message: 'Dados de retorno do veículo gravada com sucesso!'}
|
return { message: 'Dados de retorno do veículo gravada com sucesso!' };
|
||||||
|
} catch (e) {
|
||||||
} catch (e) {
|
await queryRunner.rollbackTransaction();
|
||||||
await queryRunner.rollbackTransaction();
|
console.log(e);
|
||||||
console.log(e);
|
throw e;
|
||||||
throw e;
|
} finally {
|
||||||
} finally {
|
await queryRunner.release();
|
||||||
await queryRunner.release();
|
await dataSource.destroy();
|
||||||
await dataSource.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/main.ts
32
src/main.ts
@@ -15,18 +15,25 @@ async function bootstrap() {
|
|||||||
|
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
|
|
||||||
app.use(helmet({
|
app.use(
|
||||||
contentSecurityPolicy: {
|
helmet({
|
||||||
directives: {
|
contentSecurityPolicy: {
|
||||||
defaultSrc: [`'self'`],
|
directives: {
|
||||||
scriptSrc: [`'self'`, `'unsafe-inline'`, 'cdn.jsdelivr.net', 'cdnjs.cloudflare.com'],
|
defaultSrc: [`'self'`],
|
||||||
styleSrc: [`'self'`, `'unsafe-inline'`, 'cdnjs.cloudflare.com'],
|
scriptSrc: [
|
||||||
imgSrc: [`'self'`, 'data:'],
|
`'self'`,
|
||||||
connectSrc: [`'self'`],
|
`'unsafe-inline'`,
|
||||||
fontSrc: [`'self'`, 'cdnjs.cloudflare.com'],
|
'cdn.jsdelivr.net',
|
||||||
|
'cdnjs.cloudflare.com',
|
||||||
|
],
|
||||||
|
styleSrc: [`'self'`, `'unsafe-inline'`, 'cdnjs.cloudflare.com'],
|
||||||
|
imgSrc: [`'self'`, 'data:'],
|
||||||
|
connectSrc: [`'self'`],
|
||||||
|
fontSrc: [`'self'`, 'cdnjs.cloudflare.com'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
}));
|
);
|
||||||
|
|
||||||
// Configurar pasta de arquivos estáticos
|
// Configurar pasta de arquivos estáticos
|
||||||
app.useStaticAssets(join(__dirname, '..', 'public'), {
|
app.useStaticAssets(join(__dirname, '..', 'public'), {
|
||||||
@@ -56,7 +63,6 @@ async function bootstrap() {
|
|||||||
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
|
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
.setTitle('Portal Jurunense API')
|
.setTitle('Portal Jurunense API')
|
||||||
.setDescription('Documentação da API do Portal Jurunense')
|
.setDescription('Documentação da API do Portal Jurunense')
|
||||||
@@ -68,7 +74,5 @@ async function bootstrap() {
|
|||||||
SwaggerModule.setup('docs', app, document);
|
SwaggerModule.setup('docs', app, document);
|
||||||
|
|
||||||
await app.listen(8066);
|
await app.listen(8066);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class CreatePaymentDto {
|
|||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Valor do pagamento',
|
description: 'Valor do pagamento',
|
||||||
example: 1000.00,
|
example: 1000.0,
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class OrderDto {
|
|||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Valor total do pedido',
|
description: 'Valor total do pedido',
|
||||||
example: 1000.00,
|
example: 1000.0,
|
||||||
})
|
})
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export class OrderDto {
|
|||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Valor total pago',
|
description: 'Valor total pago',
|
||||||
example: 1000.00,
|
example: 1000.0,
|
||||||
})
|
})
|
||||||
amountPaid: number;
|
amountPaid: number;
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class PaymentDto {
|
|||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Valor do pagamento',
|
description: 'Valor do pagamento',
|
||||||
example: 1000.00,
|
example: 1000.0,
|
||||||
})
|
})
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiParam, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiResponse,
|
||||||
|
ApiBearerAuth,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import { OrdersPaymentService } from './orders-payment.service';
|
import { OrdersPaymentService } from './orders-payment.service';
|
||||||
import { OrderDto } from './dto/order.dto';
|
import { OrderDto } from './dto/order.dto';
|
||||||
import { PaymentDto } from './dto/payment.dto';
|
import { PaymentDto } from './dto/payment.dto';
|
||||||
@@ -12,66 +18,65 @@ import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
|||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('api/v1/orders-payment')
|
@Controller('api/v1/orders-payment')
|
||||||
export class OrdersPaymentController {
|
export class OrdersPaymentController {
|
||||||
|
constructor(private readonly orderPaymentService: OrdersPaymentService) {}
|
||||||
|
|
||||||
constructor(private readonly orderPaymentService: OrdersPaymentService){}
|
@Get('orders/:id')
|
||||||
|
@ApiOperation({ summary: 'Lista todos os pedidos de uma loja' })
|
||||||
|
@ApiParam({ name: 'id', description: 'ID da loja' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de pedidos retornada com sucesso',
|
||||||
|
type: [OrderDto],
|
||||||
|
})
|
||||||
|
async findOrders(@Param('id') storeId: string): Promise<OrderDto[]> {
|
||||||
|
return this.orderPaymentService.findOrders(storeId, 0);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('orders/:id')
|
@Get('orders/:id/:orderId')
|
||||||
@ApiOperation({ summary: 'Lista todos os pedidos de uma loja' })
|
@ApiOperation({ summary: 'Busca um pedido específico' })
|
||||||
@ApiParam({ name: 'id', description: 'ID da loja' })
|
@ApiParam({ name: 'id', description: 'ID da loja' })
|
||||||
@ApiResponse({
|
@ApiParam({ name: 'orderId', description: 'ID do pedido' })
|
||||||
status: 200,
|
@ApiResponse({
|
||||||
description: 'Lista de pedidos retornada com sucesso',
|
status: 200,
|
||||||
type: [OrderDto]
|
description: 'Pedido retornado com sucesso',
|
||||||
})
|
type: OrderDto,
|
||||||
async findOrders(@Param('id') storeId: string): Promise<OrderDto[]> {
|
})
|
||||||
return this.orderPaymentService.findOrders(storeId, 0);
|
async findOrder(
|
||||||
}
|
@Param('id') storeId: string,
|
||||||
|
@Param('orderId') orderId: number,
|
||||||
|
): Promise<OrderDto> {
|
||||||
|
const orders = await this.orderPaymentService.findOrders(storeId, orderId);
|
||||||
|
return orders[0];
|
||||||
|
}
|
||||||
|
|
||||||
@Get('orders/:id/:orderId')
|
@Get('payments/:id')
|
||||||
@ApiOperation({ summary: 'Busca um pedido específico' })
|
@ApiOperation({ summary: 'Lista todos os pagamentos de um pedido' })
|
||||||
@ApiParam({ name: 'id', description: 'ID da loja' })
|
@ApiParam({ name: 'id', description: 'ID do pedido' })
|
||||||
@ApiParam({ name: 'orderId', description: 'ID do pedido' })
|
@ApiResponse({
|
||||||
@ApiResponse({
|
status: 200,
|
||||||
status: 200,
|
description: 'Lista de pagamentos retornada com sucesso',
|
||||||
description: 'Pedido retornado com sucesso',
|
type: [PaymentDto],
|
||||||
type: OrderDto
|
})
|
||||||
})
|
async findPayments(@Param('id') orderId: number): Promise<PaymentDto[]> {
|
||||||
async findOrder(
|
return this.orderPaymentService.findPayments(orderId);
|
||||||
@Param('id') storeId: string,
|
}
|
||||||
@Param('orderId') orderId: number,
|
@Post('payments/create')
|
||||||
): Promise<OrderDto> {
|
@ApiOperation({ summary: 'Cria um novo pagamento' })
|
||||||
const orders = await this.orderPaymentService.findOrders(storeId, orderId);
|
@ApiResponse({
|
||||||
return orders[0];
|
status: 201,
|
||||||
}
|
description: 'Pagamento criado com sucesso',
|
||||||
|
})
|
||||||
|
async createPayment(@Body() data: CreatePaymentDto): Promise<void> {
|
||||||
|
return this.orderPaymentService.createPayment(data);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('payments/:id')
|
@Post('invoice/create')
|
||||||
@ApiOperation({ summary: 'Lista todos os pagamentos de um pedido' })
|
@ApiOperation({ summary: 'Cria uma nova fatura' })
|
||||||
@ApiParam({ name: 'id', description: 'ID do pedido' })
|
@ApiResponse({
|
||||||
@ApiResponse({
|
status: 201,
|
||||||
status: 200,
|
description: 'Fatura criada com sucesso',
|
||||||
description: 'Lista de pagamentos retornada com sucesso',
|
})
|
||||||
type: [PaymentDto]
|
async createInvoice(@Body() data: CreateInvoiceDto): Promise<void> {
|
||||||
})
|
return this.orderPaymentService.createInvoice(data);
|
||||||
async findPayments(@Param('id') orderId: number): Promise<PaymentDto[]> {
|
}
|
||||||
return this.orderPaymentService.findPayments(orderId);
|
}
|
||||||
}
|
|
||||||
@Post('payments/create')
|
|
||||||
@ApiOperation({ summary: 'Cria um novo pagamento' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 201,
|
|
||||||
description: 'Pagamento criado com sucesso'
|
|
||||||
})
|
|
||||||
async createPayment(@Body() data: CreatePaymentDto): Promise<void> {
|
|
||||||
return this.orderPaymentService.createPayment(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('invoice/create')
|
|
||||||
@ApiOperation({ summary: 'Cria uma nova fatura' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 201,
|
|
||||||
description: 'Fatura criada com sucesso'
|
|
||||||
})
|
|
||||||
async createInvoice(@Body() data: CreateInvoiceDto): Promise<void> {
|
|
||||||
return this.orderPaymentService.createInvoice(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
https://docs.nestjs.com/modules
|
https://docs.nestjs.com/modules
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ import { CreateInvoiceDto } from './dto/create-invoice.dto';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrdersPaymentService {
|
export class OrdersPaymentService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
@Inject(DATA_SOURCE) private readonly dataSource: DataSource
|
@Inject(DATA_SOURCE) private readonly dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findOrders(storeId: string, orderId: number): Promise<OrderDto[]> {
|
async findOrders(storeId: string, orderId: number): Promise<OrderDto[]> {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
try {
|
try {
|
||||||
const sql = `SELECT PCPEDC.DATA as "createDate"
|
const sql = `SELECT PCPEDC.DATA as "createDate"
|
||||||
,PCPEDC.CODFILIAL as "storeId"
|
,PCPEDC.CODFILIAL as "storeId"
|
||||||
,PCPEDC.NUMPED as "orderId"
|
,PCPEDC.NUMPED as "orderId"
|
||||||
,PCPEDC.CODCLI as "customerId"
|
,PCPEDC.CODCLI as "customerId"
|
||||||
@@ -42,23 +42,23 @@ export class OrdersPaymentService {
|
|||||||
AND PCPEDC.POSICAO IN ('L')
|
AND PCPEDC.POSICAO IN ('L')
|
||||||
AND PCPEDC.DATA >= TRUNC(SYSDATE) - 5
|
AND PCPEDC.DATA >= TRUNC(SYSDATE) - 5
|
||||||
AND PCPEDC.CODFILIAL = ${storeId} `;
|
AND PCPEDC.CODFILIAL = ${storeId} `;
|
||||||
let sqlWhere = '';
|
let sqlWhere = '';
|
||||||
if (orderId > 0) {
|
if (orderId > 0) {
|
||||||
sqlWhere += ` AND PCPEDC.NUMPED = ${orderId}`;
|
sqlWhere += ` AND PCPEDC.NUMPED = ${orderId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const orders = await queryRunner.manager.query(sql + sqlWhere);
|
const orders = await queryRunner.manager.query(sql + sqlWhere);
|
||||||
return orders.map(order => new OrderDto(order));
|
return orders.map((order) => new OrderDto(order));
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async findPayments(orderId: number): Promise<PaymentDto[]> {
|
async findPayments(orderId: number): Promise<PaymentDto[]> {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
try {
|
try {
|
||||||
const sql = `SELECT
|
const sql = `SELECT
|
||||||
ESTPAGAMENTO.NUMORCA as "orderId"
|
ESTPAGAMENTO.NUMORCA as "orderId"
|
||||||
,ESTPAGAMENTO.DTPAGAMENTO as "payDate"
|
,ESTPAGAMENTO.DTPAGAMENTO as "payDate"
|
||||||
,ESTPAGAMENTO.CARTAO as "card"
|
,ESTPAGAMENTO.CARTAO as "card"
|
||||||
@@ -72,49 +72,49 @@ export class OrdersPaymentService {
|
|||||||
FROM ESTPAGAMENTO
|
FROM ESTPAGAMENTO
|
||||||
WHERE ESTPAGAMENTO.NUMORCA = ${orderId}`;
|
WHERE ESTPAGAMENTO.NUMORCA = ${orderId}`;
|
||||||
|
|
||||||
const payments = await queryRunner.manager.query(sql);
|
const payments = await queryRunner.manager.query(sql);
|
||||||
return payments.map(payment => new PaymentDto(payment));
|
return payments.map((payment) => new PaymentDto(payment));
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createPayment(payment: CreatePaymentDto): Promise<void> {
|
async createPayment(payment: CreatePaymentDto): Promise<void> {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
await queryRunner.startTransaction();
|
await queryRunner.startTransaction();
|
||||||
try {
|
try {
|
||||||
const sql = `INSERT INTO ESTPAGAMENTO ( NUMORCA, DTPAGAMENTO, CARTAO, CODAUTORIZACAO, CODRESPOSTA, DTREQUISICAO, DTSERVIDOR, IDTRANSACAO,
|
const sql = `INSERT INTO ESTPAGAMENTO ( NUMORCA, DTPAGAMENTO, CARTAO, CODAUTORIZACAO, CODRESPOSTA, DTREQUISICAO, DTSERVIDOR, IDTRANSACAO,
|
||||||
NSU, PARCELAS, VALOR, NOMEBANDEIRA, FORMAPAGTO, DTPROCESSAMENTO, CODFUNC )
|
NSU, PARCELAS, VALOR, NOMEBANDEIRA, FORMAPAGTO, DTPROCESSAMENTO, CODFUNC )
|
||||||
VALUES ( ${payment.orderId}, TRUNC(SYSDATE), '${payment.card}', '${payment.auth}', '00', SYSDATE, SYSDATE, NULL,
|
VALUES ( ${payment.orderId}, TRUNC(SYSDATE), '${payment.card}', '${payment.auth}', '00', SYSDATE, SYSDATE, NULL,
|
||||||
'${payment.nsu}', ${payment.installments}, ${payment.amount}, '${payment.flagName}',
|
'${payment.nsu}', ${payment.installments}, ${payment.amount}, '${payment.flagName}',
|
||||||
'${payment.paymentType}', SYSDATE, ${payment.userId} ) `;
|
'${payment.paymentType}', SYSDATE, ${payment.userId} ) `;
|
||||||
|
|
||||||
await queryRunner.manager.query(sql);
|
await queryRunner.manager.query(sql);
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createInvoice(data: CreateInvoiceDto): Promise<void> {
|
async createInvoice(data: CreateInvoiceDto): Promise<void> {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
await queryRunner.startTransaction();
|
await queryRunner.startTransaction();
|
||||||
try {
|
try {
|
||||||
const sql = `BEGIN
|
const sql = `BEGIN
|
||||||
ESK_FATURAMENTO.FATURAMENTO_VENDA_ASSISTIDA(${data.orderId}, ${data.userId});
|
ESK_FATURAMENTO.FATURAMENTO_VENDA_ASSISTIDA(${data.orderId}, ${data.userId});
|
||||||
END;`;
|
END;`;
|
||||||
await queryRunner.manager.query(sql);
|
await queryRunner.manager.query(sql);
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { DebDto } from '../dto/DebDto';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DebService {
|
export class DebService {
|
||||||
constructor(
|
constructor(private readonly debRepository: DebRepository) {}
|
||||||
private readonly debRepository: DebRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca débitos por CPF ou CGCENT
|
* Busca débitos por CPF ou CGCENT
|
||||||
@@ -21,6 +19,10 @@ export class DebService {
|
|||||||
matricula?: number,
|
matricula?: number,
|
||||||
cobranca?: string,
|
cobranca?: string,
|
||||||
): Promise<DebDto[]> {
|
): Promise<DebDto[]> {
|
||||||
return await this.debRepository.findByCpfCgcent(cpfCgcent, matricula, cobranca);
|
return await this.debRepository.findByCpfCgcent(
|
||||||
|
cpfCgcent,
|
||||||
|
matricula,
|
||||||
|
cobranca,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,14 +17,16 @@ import { LeadtimeDto } from '../dto/leadtime.dto';
|
|||||||
import { HttpException } from '@nestjs/common/exceptions/http.exception';
|
import { HttpException } from '@nestjs/common/exceptions/http.exception';
|
||||||
import { CarrierDto } from '../../data-consult/dto/carrier.dto';
|
import { CarrierDto } from '../../data-consult/dto/carrier.dto';
|
||||||
import { MarkData } from '../interface/markdata';
|
import { MarkData } from '../interface/markdata';
|
||||||
import { EstLogTransferFilterDto, EstLogTransferResponseDto } from '../dto/estlogtransfer.dto';
|
import {
|
||||||
|
EstLogTransferFilterDto,
|
||||||
|
EstLogTransferResponseDto,
|
||||||
|
} from '../dto/estlogtransfer.dto';
|
||||||
import { DeliveryCompletedQuery } from '../dto/delivery-completed-query.dto';
|
import { DeliveryCompletedQuery } from '../dto/delivery-completed-query.dto';
|
||||||
import { DeliveryCompleted } from '../dto/delivery-completed.dto';
|
import { DeliveryCompleted } from '../dto/delivery-completed.dto';
|
||||||
import { OrderResponseDto } from '../dto/order-response.dto';
|
import { OrderResponseDto } from '../dto/order-response.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrdersService {
|
export class OrdersService {
|
||||||
// Cache TTL em segundos
|
|
||||||
private static readonly DEFAULT_TTL = 60;
|
private static readonly DEFAULT_TTL = 60;
|
||||||
private readonly TTL_ORDERS = OrdersService.DEFAULT_TTL;
|
private readonly TTL_ORDERS = OrdersService.DEFAULT_TTL;
|
||||||
private readonly TTL_INVOICE = OrdersService.DEFAULT_TTL;
|
private readonly TTL_INVOICE = OrdersService.DEFAULT_TTL;
|
||||||
@@ -42,112 +44,85 @@ export class OrdersService {
|
|||||||
@Inject(RedisClientToken) private readonly redisClient: IRedisClient,
|
@Inject(RedisClientToken) private readonly redisClient: IRedisClient,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar pedidos com cache baseado nos filtros
|
|
||||||
* @param query - Filtros para busca de pedidos
|
|
||||||
* @returns Lista de pedidos
|
|
||||||
*/
|
|
||||||
async findOrders(query: FindOrdersDto): Promise<OrderResponseDto[]> {
|
async findOrders(query: FindOrdersDto): Promise<OrderResponseDto[]> {
|
||||||
const key = `orders:query:${this.hashObject(query)}`;
|
const key = `orders:query:${this.hashObject(query)}`;
|
||||||
|
|
||||||
return getOrSetCache(
|
return getOrSetCache(this.redisClient, key, this.TTL_ORDERS, async () => {
|
||||||
this.redisClient,
|
const orders = await this.ordersRepository.findOrders(query);
|
||||||
key,
|
|
||||||
this.TTL_ORDERS,
|
|
||||||
async () => {
|
|
||||||
const orders = await this.ordersRepository.findOrders(query);
|
|
||||||
|
|
||||||
if (!query.includeCompletedDeliveries) {
|
|
||||||
return orders;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const order of orders) {
|
|
||||||
const deliveryQuery = {
|
|
||||||
orderNumber: order.invoiceNumber,
|
|
||||||
limit: 10,
|
|
||||||
offset: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const deliveries = await this.ordersRepository.getCompletedDeliveries(deliveryQuery);
|
|
||||||
order.completedDeliveries = deliveries;
|
|
||||||
} catch (error) {
|
|
||||||
// Se houver erro, definir como array vazio
|
|
||||||
order.completedDeliveries = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!query.includeCompletedDeliveries) {
|
||||||
return orders;
|
return orders;
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
for (const order of orders) {
|
||||||
|
const deliveryQuery = {
|
||||||
|
orderNumber: order.invoiceNumber,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deliveries = await this.ordersRepository.getCompletedDeliveries(
|
||||||
|
deliveryQuery,
|
||||||
|
);
|
||||||
|
order.completedDeliveries = deliveries;
|
||||||
|
} catch (error) {
|
||||||
|
order.completedDeliveries = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async findOrdersByDeliveryDate(
|
||||||
* Buscar pedidos por data de entrega com cache
|
query: FindOrdersByDeliveryDateDto,
|
||||||
* @param query - Filtros para busca por data de entrega
|
): Promise<OrderResponseDto[]> {
|
||||||
* @returns Lista de pedidos
|
|
||||||
*/
|
|
||||||
async findOrdersByDeliveryDate(query: FindOrdersByDeliveryDateDto): Promise<OrderResponseDto[]> {
|
|
||||||
const key = `orders:delivery:${this.hashObject(query)}`;
|
const key = `orders:delivery:${this.hashObject(query)}`;
|
||||||
return getOrSetCache(
|
return getOrSetCache(this.redisClient, key, this.TTL_ORDERS, () =>
|
||||||
this.redisClient,
|
this.ordersRepository.findOrdersByDeliveryDate(query),
|
||||||
key,
|
|
||||||
this.TTL_ORDERS,
|
|
||||||
() => this.ordersRepository.findOrdersByDeliveryDate(query),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async findOrdersWithCheckout(
|
||||||
* Buscar pedidos com resultados de fechamento de caixa
|
query: FindOrdersDto,
|
||||||
* @param query - Filtros para busca de pedidos
|
): Promise<(OrderResponseDto & { checkout: any })[]> {
|
||||||
* @returns Lista de pedidos com dados de fechamento de caixa
|
|
||||||
*/
|
|
||||||
async findOrdersWithCheckout(query: FindOrdersDto): Promise<(OrderResponseDto & { checkout: any })[]> {
|
|
||||||
const key = `orders:checkout:${this.hashObject(query)}`;
|
const key = `orders:checkout:${this.hashObject(query)}`;
|
||||||
return getOrSetCache(
|
return getOrSetCache(this.redisClient, key, this.TTL_ORDERS, async () => {
|
||||||
this.redisClient,
|
const orders = await this.findOrders(query);
|
||||||
key,
|
const results = await Promise.all(
|
||||||
this.TTL_ORDERS,
|
orders.map(async (order) => {
|
||||||
async () => {
|
try {
|
||||||
// Primeiro obtém a lista de pedidos
|
const checkout =
|
||||||
const orders = await this.findOrders(query);
|
await this.ordersRepository.findOrderWithCheckoutByOrder(
|
||||||
// Para cada pedido, busca o fechamento de caixa
|
|
||||||
const results = await Promise.all(
|
|
||||||
orders.map(async order => {
|
|
||||||
try {
|
|
||||||
const checkout = await this.ordersRepository.findOrderWithCheckoutByOrder(
|
|
||||||
Number(order.orderId),
|
Number(order.orderId),
|
||||||
);
|
);
|
||||||
return { ...order, checkout };
|
return { ...order, checkout };
|
||||||
} catch {
|
} catch {
|
||||||
return { ...order, checkout: null };
|
return { ...order, checkout: null };
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return results;
|
return results;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderCheckout(orderId: number) {
|
async getOrderCheckout(orderId: number) {
|
||||||
const key = `orders:checkout:${orderId}`;
|
const key = `orders:checkout:${orderId}`;
|
||||||
return getOrSetCache(
|
return getOrSetCache(this.redisClient, key, this.TTL_ORDERS, async () => {
|
||||||
this.redisClient,
|
const result = await this.ordersRepository.findOrderWithCheckoutByOrder(
|
||||||
key,
|
orderId,
|
||||||
this.TTL_ORDERS,
|
);
|
||||||
async () => {
|
if (!result) {
|
||||||
const result = await this.ordersRepository.findOrderWithCheckoutByOrder(orderId);
|
throw new HttpException(
|
||||||
if (!result) {
|
'Nenhum fechamento encontrado',
|
||||||
throw new HttpException('Nenhum fechamento encontrado', HttpStatus.NOT_FOUND);
|
HttpStatus.NOT_FOUND,
|
||||||
}
|
);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
);
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar nota fiscal por chave NFe com cache
|
|
||||||
*/
|
|
||||||
async findInvoice(chavenfe: string): Promise<InvoiceDto> {
|
async findInvoice(chavenfe: string): Promise<InvoiceDto> {
|
||||||
const key = `orders:invoice:${chavenfe}`;
|
const key = `orders:invoice:${chavenfe}`;
|
||||||
|
|
||||||
@@ -172,16 +147,13 @@ export class OrdersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar itens de pedido com cache
|
|
||||||
*/
|
|
||||||
async getItens(orderId: string): Promise<OrderItemDto[]> {
|
async getItens(orderId: string): Promise<OrderItemDto[]> {
|
||||||
const key = `orders:itens:${orderId}`;
|
const key = `orders:itens:${orderId}`;
|
||||||
|
|
||||||
return getOrSetCache(this.redisClient, key, this.TTL_ITENS, async () => {
|
return getOrSetCache(this.redisClient, key, this.TTL_ITENS, async () => {
|
||||||
const itens = await this.ordersRepository.getItens(orderId);
|
const itens = await this.ordersRepository.getItens(orderId);
|
||||||
|
|
||||||
return itens.map(item => ({
|
return itens.map((item) => ({
|
||||||
productId: Number(item.productId),
|
productId: Number(item.productId),
|
||||||
description: item.description,
|
description: item.description,
|
||||||
pacth: item.pacth,
|
pacth: item.pacth,
|
||||||
@@ -198,20 +170,14 @@ export class OrdersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar entregas do pedido com cache
|
|
||||||
*/
|
|
||||||
async getOrderDeliveries(
|
async getOrderDeliveries(
|
||||||
orderId: string,
|
orderId: string,
|
||||||
query: { createDateIni: string; createDateEnd: string },
|
query: { createDateIni: string; createDateEnd: string },
|
||||||
): Promise<OrderDeliveryDto[]> {
|
): Promise<OrderDeliveryDto[]> {
|
||||||
const key = `orders:deliveries:${orderId}:${query.createDateIni}:${query.createDateEnd}`;
|
const key = `orders:deliveries:${orderId}:${query.createDateIni}:${query.createDateEnd}`;
|
||||||
|
|
||||||
return getOrSetCache(
|
return getOrSetCache(this.redisClient, key, this.TTL_DELIVERIES, () =>
|
||||||
this.redisClient,
|
this.ordersRepository.getOrderDeliveries(orderId),
|
||||||
key,
|
|
||||||
this.TTL_DELIVERIES,
|
|
||||||
() => this.ordersRepository.getOrderDeliveries(orderId, query),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +187,7 @@ export class OrdersService {
|
|||||||
return getOrSetCache(this.redisClient, key, this.TTL_ITENS, async () => {
|
return getOrSetCache(this.redisClient, key, this.TTL_ITENS, async () => {
|
||||||
const itens = await this.ordersRepository.getCutItens(orderId);
|
const itens = await this.ordersRepository.getCutItens(orderId);
|
||||||
|
|
||||||
return itens.map(item => ({
|
return itens.map((item) => ({
|
||||||
productId: Number(item.productId),
|
productId: Number(item.productId),
|
||||||
description: item.description,
|
description: item.description,
|
||||||
pacth: item.pacth,
|
pacth: item.pacth,
|
||||||
@@ -233,7 +199,10 @@ export class OrdersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderDelivery(orderId: string, includeCompletedDeliveries: boolean = false): Promise<OrderDeliveryDto> {
|
async getOrderDelivery(
|
||||||
|
orderId: string,
|
||||||
|
includeCompletedDeliveries: boolean = false,
|
||||||
|
): Promise<OrderDeliveryDto> {
|
||||||
const key = `orders:delivery:${orderId}:${includeCompletedDeliveries}`;
|
const key = `orders:delivery:${orderId}:${includeCompletedDeliveries}`;
|
||||||
|
|
||||||
return getOrSetCache(
|
return getOrSetCache(
|
||||||
@@ -241,7 +210,9 @@ export class OrdersService {
|
|||||||
key,
|
key,
|
||||||
this.TTL_DELIVERIES,
|
this.TTL_DELIVERIES,
|
||||||
async () => {
|
async () => {
|
||||||
const orderDelivery = await this.ordersRepository.getOrderDelivery(orderId);
|
const orderDelivery = await this.ordersRepository.getOrderDelivery(
|
||||||
|
orderId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!orderDelivery) {
|
if (!orderDelivery) {
|
||||||
return null;
|
return null;
|
||||||
@@ -252,8 +223,8 @@ export class OrdersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Buscar entregas realizadas usando o transactionId do pedido
|
const transactionId =
|
||||||
const transactionId = await this.ordersRepository.getOrderTransactionId(orderId);
|
await this.ordersRepository.getOrderTransactionId(orderId);
|
||||||
|
|
||||||
if (!transactionId) {
|
if (!transactionId) {
|
||||||
orderDelivery.completedDeliveries = [];
|
orderDelivery.completedDeliveries = [];
|
||||||
@@ -263,31 +234,27 @@ export class OrdersService {
|
|||||||
const deliveryQuery = {
|
const deliveryQuery = {
|
||||||
transactionId: transactionId,
|
transactionId: transactionId,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offset: 0
|
offset: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const deliveries = await this.ordersRepository.getCompletedDeliveriesByTransactionId(deliveryQuery);
|
const deliveries =
|
||||||
|
await this.ordersRepository.getCompletedDeliveriesByTransactionId(
|
||||||
|
deliveryQuery,
|
||||||
|
);
|
||||||
orderDelivery.completedDeliveries = deliveries;
|
orderDelivery.completedDeliveries = deliveries;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Se houver erro, definir como array vazio
|
|
||||||
orderDelivery.completedDeliveries = [];
|
orderDelivery.completedDeliveries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return orderDelivery;
|
return orderDelivery;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar leadtime do pedido com cache
|
|
||||||
*/
|
|
||||||
async getLeadtime(orderId: string): Promise<LeadtimeDto[]> {
|
async getLeadtime(orderId: string): Promise<LeadtimeDto[]> {
|
||||||
const key = `orders:leadtime:${orderId}`;
|
const key = `orders:leadtime:${orderId}`;
|
||||||
return getOrSetCache(
|
return getOrSetCache(this.redisClient, key, this.TTL_LEADTIME, () =>
|
||||||
this.redisClient,
|
this.ordersRepository.getLeadtimeWMS(orderId),
|
||||||
key,
|
|
||||||
this.TTL_LEADTIME,
|
|
||||||
() => this.ordersRepository.getLeadtimeWMS(orderId)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,25 +266,21 @@ export class OrdersService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar log de transferência por ID do pedido com cache
|
|
||||||
*/
|
|
||||||
async getTransferLog(
|
async getTransferLog(
|
||||||
orderId: number,
|
orderId: number,
|
||||||
filters?: EstLogTransferFilterDto
|
filters?: EstLogTransferFilterDto,
|
||||||
): Promise<EstLogTransferResponseDto[] | null> {
|
): Promise<EstLogTransferResponseDto[] | null> {
|
||||||
const key = `orders:transfer-log:${orderId}:${this.hashObject(filters || {})}`;
|
const key = `orders:transfer-log:${orderId}:${this.hashObject(
|
||||||
|
filters || {},
|
||||||
|
)}`;
|
||||||
|
|
||||||
return getOrSetCache(this.redisClient, key, this.TTL_TRANSFER, () =>
|
return getOrSetCache(this.redisClient, key, this.TTL_TRANSFER, () =>
|
||||||
this.ordersRepository.estlogtransfer(orderId, filters),
|
this.ordersRepository.estlogtransfer(orderId, filters),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar logs de transferência com filtros (sem especificar pedido específico)
|
|
||||||
*/
|
|
||||||
async getTransferLogs(
|
async getTransferLogs(
|
||||||
filters?: EstLogTransferFilterDto
|
filters?: EstLogTransferFilterDto,
|
||||||
): Promise<EstLogTransferResponseDto[] | null> {
|
): Promise<EstLogTransferResponseDto[] | null> {
|
||||||
const key = `orders:transfer-logs:${this.hashObject(filters || {})}`;
|
const key = `orders:transfer-logs:${this.hashObject(filters || {})}`;
|
||||||
|
|
||||||
@@ -334,11 +297,6 @@ export class OrdersService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Utilitário para gerar hash MD5 de objetos para chaves de cache
|
|
||||||
* @param obj - Objeto a ser serializado e hasheado
|
|
||||||
* @returns Hash MD5 do objeto serializado
|
|
||||||
*/
|
|
||||||
private hashObject(obj: unknown): string {
|
private hashObject(obj: unknown): string {
|
||||||
const objRecord = obj as Record<string, unknown>;
|
const objRecord = obj as Record<string, unknown>;
|
||||||
const sortedKeys = Object.keys(objRecord).sort();
|
const sortedKeys = Object.keys(objRecord).sort();
|
||||||
@@ -346,21 +304,19 @@ export class OrdersService {
|
|||||||
return createHash('md5').update(str).digest('hex');
|
return createHash('md5').update(str).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
async createInvoiceCheck(invoice: InvoiceCheckDto): Promise<{ message: string }> {
|
async createInvoiceCheck(
|
||||||
// Não usa cache para operações de escrita
|
invoice: InvoiceCheckDto,
|
||||||
|
): Promise<{ message: string }> {
|
||||||
return this.ordersRepository.createInvoiceCheck(invoice);
|
return this.ordersRepository.createInvoiceCheck(invoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar transportadoras do pedido com cache
|
|
||||||
*/
|
|
||||||
async getOrderCarriers(orderId: number): Promise<CarrierDto[]> {
|
async getOrderCarriers(orderId: number): Promise<CarrierDto[]> {
|
||||||
const key = `orders:carriers:${orderId}`;
|
const key = `orders:carriers:${orderId}`;
|
||||||
|
|
||||||
return getOrSetCache(this.redisClient, key, this.TTL_CARRIERS, async () => {
|
return getOrSetCache(this.redisClient, key, this.TTL_CARRIERS, async () => {
|
||||||
const carriers = await this.ordersRepository.getOrderCarriers(orderId);
|
const carriers = await this.ordersRepository.getOrderCarriers(orderId);
|
||||||
|
|
||||||
return carriers.map(carrier => ({
|
return carriers.map((carrier) => ({
|
||||||
carrierId: carrier.carrierId?.toString() || '',
|
carrierId: carrier.carrierId?.toString() || '',
|
||||||
carrierName: carrier.carrierName || '',
|
carrierName: carrier.carrierName || '',
|
||||||
carrierDescription: carrier.carrierDescription || '',
|
carrierDescription: carrier.carrierDescription || '',
|
||||||
@@ -368,9 +324,6 @@ export class OrdersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar marca por ID com cache
|
|
||||||
*/
|
|
||||||
async findOrderByMark(orderId: number): Promise<MarkData> {
|
async findOrderByMark(orderId: number): Promise<MarkData> {
|
||||||
const key = `orders:mark:${orderId}`;
|
const key = `orders:mark:${orderId}`;
|
||||||
|
|
||||||
@@ -383,9 +336,6 @@ export class OrdersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar todas as marcas disponíveis com cache
|
|
||||||
*/
|
|
||||||
async getAllMarks(): Promise<MarkData[]> {
|
async getAllMarks(): Promise<MarkData[]> {
|
||||||
const key = 'orders:marks:all';
|
const key = 'orders:marks:all';
|
||||||
|
|
||||||
@@ -394,9 +344,6 @@ export class OrdersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar marcas por nome com cache
|
|
||||||
*/
|
|
||||||
async getMarksByName(markName: string): Promise<MarkData[]> {
|
async getMarksByName(markName: string): Promise<MarkData[]> {
|
||||||
const key = `orders:marks:name:${markName}`;
|
const key = `orders:marks:name:${markName}`;
|
||||||
|
|
||||||
@@ -405,10 +352,9 @@ export class OrdersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async getCompletedDeliveries(
|
||||||
* Buscar entregas realizadas com cache baseado nos filtros
|
query: DeliveryCompletedQuery,
|
||||||
*/
|
): Promise<DeliveryCompleted[]> {
|
||||||
async getCompletedDeliveries(query: DeliveryCompletedQuery): Promise<DeliveryCompleted[]> {
|
|
||||||
const key = `orders:completed-deliveries:${this.hashObject(query)}`;
|
const key = `orders:completed-deliveries:${this.hashObject(query)}`;
|
||||||
|
|
||||||
return getOrSetCache(
|
return getOrSetCache(
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export class DebController {
|
|||||||
@Get('find-by-cpf')
|
@Get('find-by-cpf')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Busca débitos por CPF/CGCENT',
|
summary: 'Busca débitos por CPF/CGCENT',
|
||||||
description: 'Busca débitos de um cliente usando CPF ou CGCENT. Opcionalmente pode filtrar por matrícula do funcionário ou código de cobrança.',
|
description:
|
||||||
|
'Busca débitos de um cliente usando CPF ou CGCENT. Opcionalmente pode filtrar por matrícula do funcionário ou código de cobrança.',
|
||||||
})
|
})
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -34,9 +35,7 @@ export class DebController {
|
|||||||
description: 'Erro interno do servidor',
|
description: 'Erro interno do servidor',
|
||||||
})
|
})
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async findByCpfCgcent(
|
async findByCpfCgcent(@Query() query: FindDebDto): Promise<DebDto[]> {
|
||||||
@Query() query: FindDebDto,
|
|
||||||
): Promise<DebDto[]> {
|
|
||||||
return await this.debService.findByCpfCgcent(
|
return await this.debService.findByCpfCgcent(
|
||||||
query.cpfCgcent,
|
query.cpfCgcent,
|
||||||
query.matricula,
|
query.matricula,
|
||||||
|
|||||||
@@ -7,21 +7,26 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UsePipes,
|
UsePipes,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors,
|
|
||||||
ValidationPipe,
|
ValidationPipe,
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
DefaultValuePipe,
|
DefaultValuePipe,
|
||||||
ParseBoolPipe,
|
ParseBoolPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiTags, ApiQuery, ApiParam, ApiResponse } from '@nestjs/swagger';
|
import {
|
||||||
import { ResponseInterceptor } from '../../common/response.interceptor';
|
ApiBearerAuth,
|
||||||
|
ApiOperation,
|
||||||
|
ApiTags,
|
||||||
|
ApiQuery,
|
||||||
|
ApiParam,
|
||||||
|
ApiResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import { OrdersService } from '../application/orders.service';
|
import { OrdersService } from '../application/orders.service';
|
||||||
import { FindOrdersDto } from '../dto/find-orders.dto';
|
import { FindOrdersDto } from '../dto/find-orders.dto';
|
||||||
import { FindOrdersByDeliveryDateDto } from '../dto/find-orders-by-delivery-date.dto';
|
import { FindOrdersByDeliveryDateDto } from '../dto/find-orders-by-delivery-date.dto';
|
||||||
import { JwtAuthGuard, } from 'src/auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||||
import { InvoiceDto } from '../dto/find-invoice.dto';
|
import { InvoiceDto } from '../dto/find-invoice.dto';
|
||||||
import { OrderItemDto } from "../dto/OrderItemDto";
|
import { OrderItemDto } from '../dto/OrderItemDto';
|
||||||
import { LeadtimeDto } from '../dto/leadtime.dto';
|
import { LeadtimeDto } from '../dto/leadtime.dto';
|
||||||
import { CutItemDto } from '../dto/CutItemDto';
|
import { CutItemDto } from '../dto/CutItemDto';
|
||||||
import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
|
import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
|
||||||
@@ -34,7 +39,6 @@ import { OrderResponseDto } from '../dto/order-response.dto';
|
|||||||
import { MarkResponseDto } from '../dto/mark-response.dto';
|
import { MarkResponseDto } from '../dto/mark-response.dto';
|
||||||
import { EstLogTransferResponseDto } from '../dto/estlogtransfer.dto';
|
import { EstLogTransferResponseDto } from '../dto/estlogtransfer.dto';
|
||||||
|
|
||||||
|
|
||||||
@ApiTags('Orders')
|
@ApiTags('Orders')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@@ -45,15 +49,47 @@ export class OrdersController {
|
|||||||
@Get('find')
|
@Get('find')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Busca pedidos',
|
summary: 'Busca pedidos',
|
||||||
description: 'Busca pedidos com filtros avançados. Suporta filtros por data, cliente, vendedor, status, tipo de entrega e status de transferência.'
|
description:
|
||||||
|
'Busca pedidos com filtros avançados. Suporta filtros por data, cliente, vendedor, status, tipo de entrega e status de transferência.',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'includeCheckout',
|
||||||
|
required: false,
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Incluir dados de checkout',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'statusTransfer',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'markId',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'ID da marca para filtrar pedidos',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'markName',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
description: 'Nome da marca para filtrar pedidos (busca parcial)',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'hasPreBox',
|
||||||
|
required: false,
|
||||||
|
type: 'boolean',
|
||||||
|
description:
|
||||||
|
'Filtrar pedidos que tenham registros na tabela de transfer log',
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'includeCheckout', required: false, type: 'boolean', description: 'Incluir dados de checkout' })
|
|
||||||
@ApiQuery({ name: 'statusTransfer', required: false, type: 'string', description: 'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)' })
|
|
||||||
@ApiQuery({ name: 'markId', required: false, type: 'number', description: 'ID da marca para filtrar pedidos' })
|
|
||||||
@ApiQuery({ name: 'markName', required: false, type: 'string', description: 'Nome da marca para filtrar pedidos (busca parcial)' })
|
|
||||||
@ApiQuery({ name: 'hasPreBox', required: false, type: 'boolean', description: 'Filtrar pedidos que tenham registros na tabela de transfer log' })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
@ApiResponse({ status: 200, description: 'Lista de pedidos retornada com sucesso', type: [OrderResponseDto] })
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de pedidos retornada com sucesso',
|
||||||
|
type: [OrderResponseDto],
|
||||||
|
})
|
||||||
findOrders(
|
findOrders(
|
||||||
@Query() query: FindOrdersDto,
|
@Query() query: FindOrdersDto,
|
||||||
@Query('includeCheckout', new DefaultValuePipe(false), ParseBoolPipe)
|
@Query('includeCheckout', new DefaultValuePipe(false), ParseBoolPipe)
|
||||||
@@ -68,17 +104,42 @@ export class OrdersController {
|
|||||||
@Get('find-by-delivery-date')
|
@Get('find-by-delivery-date')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Busca pedidos por data de entrega',
|
summary: 'Busca pedidos por data de entrega',
|
||||||
description: 'Busca pedidos filtrados por data de entrega. Suporta filtros adicionais como status de transferência, cliente, vendedor, etc.'
|
description:
|
||||||
|
'Busca pedidos filtrados por data de entrega. Suporta filtros adicionais como status de transferência, cliente, vendedor, etc.',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'statusTransfer',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'markId',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'ID da marca para filtrar pedidos',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'markName',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
description: 'Nome da marca para filtrar pedidos (busca parcial)',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'hasPreBox',
|
||||||
|
required: false,
|
||||||
|
type: 'boolean',
|
||||||
|
description:
|
||||||
|
'Filtrar pedidos que tenham registros na tabela de transfer log',
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'statusTransfer', required: false, type: 'string', description: 'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)' })
|
|
||||||
@ApiQuery({ name: 'markId', required: false, type: 'number', description: 'ID da marca para filtrar pedidos' })
|
|
||||||
@ApiQuery({ name: 'markName', required: false, type: 'string', description: 'Nome da marca para filtrar pedidos (busca parcial)' })
|
|
||||||
@ApiQuery({ name: 'hasPreBox', required: false, type: 'boolean', description: 'Filtrar pedidos que tenham registros na tabela de transfer log' })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
@ApiResponse({ status: 200, description: 'Lista de pedidos por data de entrega retornada com sucesso', type: [OrderResponseDto] })
|
@ApiResponse({
|
||||||
findOrdersByDeliveryDate(
|
status: 200,
|
||||||
@Query() query: FindOrdersByDeliveryDateDto,
|
description: 'Lista de pedidos por data de entrega retornada com sucesso',
|
||||||
) {
|
type: [OrderResponseDto],
|
||||||
|
})
|
||||||
|
findOrdersByDeliveryDate(@Query() query: FindOrdersByDeliveryDateDto) {
|
||||||
return this.ordersService.findOrdersByDeliveryDate(query);
|
return this.ordersService.findOrdersByDeliveryDate(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,20 +147,16 @@ export class OrdersController {
|
|||||||
@ApiOperation({ summary: 'Busca fechamento de caixa para um pedido' })
|
@ApiOperation({ summary: 'Busca fechamento de caixa para um pedido' })
|
||||||
@ApiParam({ name: 'orderId' })
|
@ApiParam({ name: 'orderId' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
getOrderCheckout(
|
getOrderCheckout(@Param('orderId', ParseIntPipe) orderId: number) {
|
||||||
@Param('orderId', ParseIntPipe) orderId: number,
|
|
||||||
) {
|
|
||||||
return this.ordersService.getOrderCheckout(orderId);
|
return this.ordersService.getOrderCheckout(orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Get('invoice/:chavenfe')
|
@Get('invoice/:chavenfe')
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'chavenfe',
|
name: 'chavenfe',
|
||||||
required: true,
|
required: true,
|
||||||
description: 'Chave da Nota Fiscal (44 dígitos)',
|
description: 'Chave da Nota Fiscal (44 dígitos)',
|
||||||
})
|
})
|
||||||
|
|
||||||
@ApiOperation({ summary: 'Busca NF pela chave' })
|
@ApiOperation({ summary: 'Busca NF pela chave' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getInvoice(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> {
|
async getInvoice(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> {
|
||||||
@@ -117,7 +174,9 @@ export class OrdersController {
|
|||||||
@ApiOperation({ summary: 'Busca PELO numero do pedido' })
|
@ApiOperation({ summary: 'Busca PELO numero do pedido' })
|
||||||
@ApiParam({ name: 'orderId' })
|
@ApiParam({ name: 'orderId' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderItemDto[]> {
|
async getItens(
|
||||||
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
|
): Promise<OrderItemDto[]> {
|
||||||
try {
|
try {
|
||||||
return await this.ordersService.getItens(orderId.toString());
|
return await this.ordersService.getItens(orderId.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -131,7 +190,9 @@ export class OrdersController {
|
|||||||
@ApiOperation({ summary: 'Busca itens cortados do pedido' })
|
@ApiOperation({ summary: 'Busca itens cortados do pedido' })
|
||||||
@ApiParam({ name: 'orderId' })
|
@ApiParam({ name: 'orderId' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getCutItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<CutItemDto[]> {
|
async getCutItens(
|
||||||
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
|
): Promise<CutItemDto[]> {
|
||||||
try {
|
try {
|
||||||
return await this.ordersService.getCutItens(orderId.toString());
|
return await this.ordersService.getCutItens(orderId.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -146,7 +207,9 @@ export class OrdersController {
|
|||||||
@ApiOperation({ summary: 'Busca dados de entrega do pedido' })
|
@ApiOperation({ summary: 'Busca dados de entrega do pedido' })
|
||||||
@ApiParam({ name: 'orderId' })
|
@ApiParam({ name: 'orderId' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getOrderDelivery(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderDeliveryDto | null> {
|
async getOrderDelivery(
|
||||||
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
|
): Promise<OrderDeliveryDto | null> {
|
||||||
try {
|
try {
|
||||||
return await this.ordersService.getOrderDelivery(orderId.toString());
|
return await this.ordersService.getOrderDelivery(orderId.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -157,50 +220,66 @@ export class OrdersController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('transfer/:orderId')
|
@Get('transfer/:orderId')
|
||||||
@ApiOperation({ summary: 'Consulta pedidos de transferência' })
|
@ApiOperation({ summary: 'Consulta pedidos de transferência' })
|
||||||
@ApiParam({ name: 'orderId' })
|
@ApiParam({ name: 'orderId' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getTransfer(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderTransferDto[] | null> {
|
async getTransfer(
|
||||||
try {
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
return await this.ordersService.getTransfer(orderId);
|
): Promise<OrderTransferDto[] | null> {
|
||||||
} catch (error) {
|
try {
|
||||||
throw new HttpException(
|
return await this.ordersService.getTransfer(orderId);
|
||||||
error.message || 'Erro ao buscar transferências do pedido',
|
} catch (error) {
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
throw new HttpException(
|
||||||
);
|
error.message || 'Erro ao buscar transferências do pedido',
|
||||||
}
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
}
|
);
|
||||||
|
|
||||||
@Get('status/:orderId')
|
|
||||||
@ApiOperation({ summary: 'Consulta status do pedido' })
|
|
||||||
@ApiParam({ name: 'orderId' })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
|
||||||
async getStatusOrder(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderStatusDto[] | null> {
|
|
||||||
try {
|
|
||||||
return await this.ordersService.getStatusOrder(orderId);
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.message || 'Erro ao buscar status do pedido',
|
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('status/:orderId')
|
||||||
|
@ApiOperation({ summary: 'Consulta status do pedido' })
|
||||||
|
@ApiParam({ name: 'orderId' })
|
||||||
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
|
async getStatusOrder(
|
||||||
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
|
): Promise<OrderStatusDto[] | null> {
|
||||||
|
try {
|
||||||
|
return await this.ordersService.getStatusOrder(orderId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Erro ao buscar status do pedido',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':orderId/deliveries')
|
@Get(':orderId/deliveries')
|
||||||
@ApiOperation({ summary: 'Consulta entregas do pedido' })
|
@ApiOperation({ summary: 'Consulta entregas do pedido' })
|
||||||
@ApiParam({ name: 'orderId' })
|
@ApiParam({ name: 'orderId' })
|
||||||
@ApiQuery({ name: 'createDateIni', required: false, description: 'Data inicial para filtro (formato YYYY-MM-DD)' })
|
@ApiQuery({
|
||||||
@ApiQuery({ name: 'createDateEnd', required: false, description: 'Data final para filtro (formato YYYY-MM-DD)' })
|
name: 'createDateIni',
|
||||||
|
required: false,
|
||||||
|
description: 'Data inicial para filtro (formato YYYY-MM-DD)',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'createDateEnd',
|
||||||
|
required: false,
|
||||||
|
description: 'Data final para filtro (formato YYYY-MM-DD)',
|
||||||
|
})
|
||||||
async getOrderDeliveries(
|
async getOrderDeliveries(
|
||||||
@Param('orderId', ParseIntPipe) orderId: number,
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
@Query('createDateIni') createDateIni?: string,
|
@Query('createDateIni') createDateIni?: string,
|
||||||
@Query('createDateEnd') createDateEnd?: string,
|
@Query('createDateEnd') createDateEnd?: string,
|
||||||
): Promise<OrderDeliveryDto[]> {
|
): Promise<OrderDeliveryDto[]> {
|
||||||
// Definir datas padrão caso não sejam fornecidas
|
// Definir datas padrão caso não sejam fornecidas
|
||||||
const defaultDateIni = createDateIni || new Date(new Date().setDate(new Date().getDate() - 30)).toISOString().split('T')[0];
|
const defaultDateIni =
|
||||||
const defaultDateEnd = createDateEnd || new Date().toISOString().split('T')[0];
|
createDateIni ||
|
||||||
|
new Date(new Date().setDate(new Date().getDate() - 30))
|
||||||
|
.toISOString()
|
||||||
|
.split('T')[0];
|
||||||
|
const defaultDateEnd =
|
||||||
|
createDateEnd || new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
return this.ordersService.getOrderDeliveries(orderId.toString(), {
|
return this.ordersService.getOrderDeliveries(orderId.toString(), {
|
||||||
createDateIni: defaultDateIni,
|
createDateIni: defaultDateIni,
|
||||||
@@ -208,175 +287,275 @@ export class OrdersController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('leadtime/:orderId')
|
||||||
@Get('leadtime/:orderId')
|
@ApiOperation({ summary: 'Consulta leadtime do pedido' })
|
||||||
@ApiOperation({ summary: 'Consulta leadtime do pedido' })
|
|
||||||
@ApiParam({ name: 'orderId' })
|
@ApiParam({ name: 'orderId' })
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async getLeadtime(@Param('orderId', ParseIntPipe) orderId: number): Promise<LeadtimeDto[]> {
|
async getLeadtime(
|
||||||
try {
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
|
): Promise<LeadtimeDto[]> {
|
||||||
|
try {
|
||||||
return await this.ordersService.getLeadtime(orderId.toString());
|
return await this.ordersService.getLeadtime(orderId.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
error.message || 'Erro ao buscar leadtime do pedido',
|
error.message || 'Erro ao buscar leadtime do pedido',
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('invoice/check')
|
||||||
|
@ApiOperation({ summary: 'Cria conferência de nota fiscal' })
|
||||||
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
|
async createInvoiceCheck(
|
||||||
|
@Body() invoice: InvoiceCheckDto,
|
||||||
|
): Promise<{ message: string }> {
|
||||||
|
try {
|
||||||
|
return await this.ordersService.createInvoiceCheck(invoice);
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Erro ao salvar conferência',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('carriers/:orderId')
|
||||||
|
@ApiOperation({ summary: 'Busca transportadoras do pedido' })
|
||||||
|
@ApiParam({ name: 'orderId', example: 236001388 })
|
||||||
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
|
async getOrderCarriers(
|
||||||
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
|
): Promise<CarrierDto[]> {
|
||||||
|
try {
|
||||||
|
return await this.ordersService.getOrderCarriers(orderId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Erro ao buscar transportadoras do pedido',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('mark/:orderId')
|
||||||
|
@ApiOperation({ summary: 'Busca marca por ID do pedido' })
|
||||||
|
@ApiParam({ name: 'orderId', example: 236001388 })
|
||||||
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Marca encontrada com sucesso',
|
||||||
|
type: MarkResponseDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Marca não encontrada' })
|
||||||
|
async findOrderByMark(
|
||||||
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
|
): Promise<MarkResponseDto> {
|
||||||
|
try {
|
||||||
|
return await this.ordersService.findOrderByMark(orderId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Erro ao buscar marca do pedido',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('marks')
|
||||||
|
@ApiOperation({ summary: 'Busca todas as marcas disponíveis' })
|
||||||
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de marcas retornada com sucesso',
|
||||||
|
type: [MarkResponseDto],
|
||||||
|
})
|
||||||
|
async getAllMarks(): Promise<MarkResponseDto[]> {
|
||||||
|
try {
|
||||||
|
return await this.ordersService.getAllMarks();
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Erro ao buscar marcas',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('marks/search')
|
||||||
|
@ApiOperation({ summary: 'Busca marcas por nome' })
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'name',
|
||||||
|
required: true,
|
||||||
|
type: 'string',
|
||||||
|
description: 'Nome da marca para buscar',
|
||||||
|
})
|
||||||
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de marcas encontradas',
|
||||||
|
type: [MarkResponseDto],
|
||||||
|
})
|
||||||
|
async getMarksByName(
|
||||||
|
@Query('name') markName: string,
|
||||||
|
): Promise<MarkResponseDto[]> {
|
||||||
|
try {
|
||||||
|
return await this.ordersService.getMarksByName(markName);
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Erro ao buscar marcas',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('transfer-log/:orderId')
|
||||||
|
@ApiOperation({ summary: 'Busca log de transferência por ID do pedido' })
|
||||||
|
@ApiParam({
|
||||||
|
name: 'orderId',
|
||||||
|
description: 'ID do pedido para buscar log de transferência',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'dttransf',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
description: 'Data de transferência (formato YYYY-MM-DD)',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'codfilial',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'Código da filial de origem',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'codfilialdest',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'Código da filial de destino',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'numpedloja',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'Número do pedido da loja',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'numpedtransf',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'Número do pedido de transferência',
|
||||||
|
})
|
||||||
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Log de transferência encontrado com sucesso',
|
||||||
|
type: [EstLogTransferResponseDto],
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: 'OrderId inválido' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: 'Log de transferência não encontrado',
|
||||||
|
})
|
||||||
|
async getTransferLog(
|
||||||
|
@Param('orderId', ParseIntPipe) orderId: number,
|
||||||
|
@Query('dttransf') dttransf?: string,
|
||||||
|
@Query('codfilial') codfilial?: number,
|
||||||
|
@Query('codfilialdest') codfilialdest?: number,
|
||||||
|
@Query('numpedloja') numpedloja?: number,
|
||||||
|
@Query('numpedtransf') numpedtransf?: number,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const filters = {
|
||||||
|
dttransf,
|
||||||
|
codfilial,
|
||||||
|
codfilialdest,
|
||||||
|
numpedloja,
|
||||||
|
numpedtransf,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.ordersService.getTransferLog(orderId, filters);
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Erro ao buscar log de transferência',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('transfer-log')
|
||||||
|
@ApiOperation({ summary: 'Busca logs de transferência com filtros' })
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'dttransf',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
description: 'Data de transferência (formato YYYY-MM-DD)',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'dttransfIni',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
description: 'Data de transferência inicial (formato YYYY-MM-DD)',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'dttransfEnd',
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
description: 'Data de transferência final (formato YYYY-MM-DD)',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'codfilial',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'Código da filial de origem',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'codfilialdest',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'Código da filial de destino',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'numpedloja',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'Número do pedido da loja',
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'numpedtransf',
|
||||||
|
required: false,
|
||||||
|
type: 'number',
|
||||||
|
description: 'Número do pedido de transferência',
|
||||||
|
})
|
||||||
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Logs de transferência encontrados com sucesso',
|
||||||
|
type: [EstLogTransferResponseDto],
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: 'Filtros inválidos' })
|
||||||
|
async getTransferLogs(
|
||||||
|
@Query('dttransf') dttransf?: string,
|
||||||
|
@Query('dttransfIni') dttransfIni?: string,
|
||||||
|
@Query('dttransfEnd') dttransfEnd?: string,
|
||||||
|
@Query('codfilial') codfilial?: number,
|
||||||
|
@Query('codfilialdest') codfilialdest?: number,
|
||||||
|
@Query('numpedloja') numpedloja?: number,
|
||||||
|
@Query('numpedtransf') numpedtransf?: number,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const filters = {
|
||||||
|
dttransf,
|
||||||
|
dttransfIni,
|
||||||
|
dttransfEnd,
|
||||||
|
codfilial,
|
||||||
|
codfilialdest,
|
||||||
|
numpedloja,
|
||||||
|
numpedtransf,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.ordersService.getTransferLogs(filters);
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.message || 'Erro ao buscar logs de transferência',
|
||||||
|
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('invoice/check')
|
|
||||||
@ApiOperation({ summary: 'Cria conferência de nota fiscal' })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
|
||||||
async createInvoiceCheck(@Body() invoice: InvoiceCheckDto): Promise<{ message: string }> {
|
|
||||||
try {
|
|
||||||
return await this.ordersService.createInvoiceCheck(invoice);
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.message || 'Erro ao salvar conferência',
|
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('carriers/:orderId')
|
|
||||||
@ApiOperation({ summary: 'Busca transportadoras do pedido' })
|
|
||||||
@ApiParam({ name: 'orderId', example: 236001388 })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
|
||||||
async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise<CarrierDto[]> {
|
|
||||||
try {
|
|
||||||
return await this.ordersService.getOrderCarriers(orderId);
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.message || 'Erro ao buscar transportadoras do pedido',
|
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('mark/:orderId')
|
|
||||||
@ApiOperation({ summary: 'Busca marca por ID do pedido' })
|
|
||||||
@ApiParam({ name: 'orderId', example: 236001388 })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
|
||||||
@ApiResponse({ status: 200, description: 'Marca encontrada com sucesso', type: MarkResponseDto })
|
|
||||||
@ApiResponse({ status: 404, description: 'Marca não encontrada' })
|
|
||||||
async findOrderByMark(@Param('orderId', ParseIntPipe) orderId: number): Promise<MarkResponseDto> {
|
|
||||||
try {
|
|
||||||
return await this.ordersService.findOrderByMark(orderId);
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.message || 'Erro ao buscar marca do pedido',
|
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('marks')
|
|
||||||
@ApiOperation({ summary: 'Busca todas as marcas disponíveis' })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
|
||||||
@ApiResponse({ status: 200, description: 'Lista de marcas retornada com sucesso', type: [MarkResponseDto] })
|
|
||||||
async getAllMarks(): Promise<MarkResponseDto[]> {
|
|
||||||
try {
|
|
||||||
return await this.ordersService.getAllMarks();
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.message || 'Erro ao buscar marcas',
|
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('marks/search')
|
|
||||||
@ApiOperation({ summary: 'Busca marcas por nome' })
|
|
||||||
@ApiQuery({ name: 'name', required: true, type: 'string', description: 'Nome da marca para buscar' })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
|
||||||
@ApiResponse({ status: 200, description: 'Lista de marcas encontradas', type: [MarkResponseDto] })
|
|
||||||
async getMarksByName(@Query('name') markName: string): Promise<MarkResponseDto[]> {
|
|
||||||
try {
|
|
||||||
return await this.ordersService.getMarksByName(markName);
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.message || 'Erro ao buscar marcas',
|
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('transfer-log/:orderId')
|
|
||||||
@ApiOperation({ summary: 'Busca log de transferência por ID do pedido' })
|
|
||||||
@ApiParam({ name: 'orderId', description: 'ID do pedido para buscar log de transferência' })
|
|
||||||
@ApiQuery({ name: 'dttransf', required: false, type: 'string', description: 'Data de transferência (formato YYYY-MM-DD)' })
|
|
||||||
@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: 'Código da filial de origem' })
|
|
||||||
@ApiQuery({ name: 'codfilialdest', required: false, type: 'number', description: 'Código da filial de destino' })
|
|
||||||
@ApiQuery({ name: 'numpedloja', required: false, type: 'number', description: 'Número do pedido da loja' })
|
|
||||||
@ApiQuery({ name: 'numpedtransf', required: false, type: 'number', description: 'Número do pedido de transferência' })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
|
||||||
@ApiResponse({ status: 200, description: 'Log de transferência encontrado com sucesso', type: [EstLogTransferResponseDto] })
|
|
||||||
@ApiResponse({ status: 400, description: 'OrderId inválido' })
|
|
||||||
@ApiResponse({ status: 404, description: 'Log de transferência não encontrado' })
|
|
||||||
async getTransferLog(
|
|
||||||
@Param('orderId', ParseIntPipe) orderId: number,
|
|
||||||
@Query('dttransf') dttransf?: string,
|
|
||||||
@Query('codfilial') codfilial?: number,
|
|
||||||
@Query('codfilialdest') codfilialdest?: number,
|
|
||||||
@Query('numpedloja') numpedloja?: number,
|
|
||||||
@Query('numpedtransf') numpedtransf?: number,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const filters = {
|
|
||||||
dttransf,
|
|
||||||
codfilial,
|
|
||||||
codfilialdest,
|
|
||||||
numpedloja,
|
|
||||||
numpedtransf,
|
|
||||||
};
|
|
||||||
|
|
||||||
return await this.ordersService.getTransferLog(orderId, filters);
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.message || 'Erro ao buscar log de transferência',
|
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('transfer-log')
|
|
||||||
@ApiOperation({ summary: 'Busca logs de transferência com filtros' })
|
|
||||||
@ApiQuery({ name: 'dttransf', required: false, type: 'string', description: 'Data de transferência (formato YYYY-MM-DD)' })
|
|
||||||
@ApiQuery({ name: 'dttransfIni', required: false, type: 'string', description: 'Data de transferência inicial (formato YYYY-MM-DD)' })
|
|
||||||
@ApiQuery({ name: 'dttransfEnd', required: false, type: 'string', description: 'Data de transferência final (formato YYYY-MM-DD)' })
|
|
||||||
@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: 'Código da filial de origem' })
|
|
||||||
@ApiQuery({ name: 'codfilialdest', required: false, type: 'number', description: 'Código da filial de destino' })
|
|
||||||
@ApiQuery({ name: 'numpedloja', required: false, type: 'number', description: 'Número do pedido da loja' })
|
|
||||||
@ApiQuery({ name: 'numpedtransf', required: false, type: 'number', description: 'Número do pedido de transferência' })
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
|
||||||
@ApiResponse({ status: 200, description: 'Logs de transferência encontrados com sucesso', type: [EstLogTransferResponseDto] })
|
|
||||||
@ApiResponse({ status: 400, description: 'Filtros inválidos' })
|
|
||||||
async getTransferLogs(
|
|
||||||
@Query('dttransf') dttransf?: string,
|
|
||||||
@Query('dttransfIni') dttransfIni?: string,
|
|
||||||
@Query('dttransfEnd') dttransfEnd?: string,
|
|
||||||
@Query('codfilial') codfilial?: number,
|
|
||||||
@Query('codfilialdest') codfilialdest?: number,
|
|
||||||
@Query('numpedloja') numpedloja?: number,
|
|
||||||
@Query('numpedtransf') numpedtransf?: number,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const filters = {
|
|
||||||
dttransf,
|
|
||||||
dttransfIni,
|
|
||||||
dttransfEnd,
|
|
||||||
codfilial,
|
|
||||||
codfilialdest,
|
|
||||||
numpedloja,
|
|
||||||
numpedtransf,
|
|
||||||
};
|
|
||||||
|
|
||||||
return await this.ordersService.getTransferLogs(filters);
|
|
||||||
} catch (error) {
|
|
||||||
throw new HttpException(
|
|
||||||
error.message || 'Erro ao buscar logs de transferência',
|
|
||||||
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
export class CutItemDto {
|
export class CutItemDto {
|
||||||
productId: number;
|
productId: number;
|
||||||
description: string;
|
description: string;
|
||||||
pacth: string;
|
pacth: string;
|
||||||
stockId: number;
|
stockId: number;
|
||||||
saleQuantity: number;
|
saleQuantity: number;
|
||||||
cutQuantity: number;
|
cutQuantity: number;
|
||||||
separedQuantity: number;
|
separedQuantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export class DebDto {
|
|||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Valor da prestação',
|
description: 'Valor da prestação',
|
||||||
example: 150.50,
|
example: 150.5,
|
||||||
})
|
})
|
||||||
valor: number;
|
valor: number;
|
||||||
|
|
||||||
@@ -81,4 +81,3 @@ export class DebDto {
|
|||||||
})
|
})
|
||||||
situacao: string;
|
situacao: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import { DeliveryCompleted } from './delivery-completed.dto';
|
import { DeliveryCompleted } from './delivery-completed.dto';
|
||||||
|
|
||||||
export class OrderDeliveryDto {
|
export class OrderDeliveryDto {
|
||||||
placeId: number;
|
placeId: number;
|
||||||
placeName: string;
|
placeName: string;
|
||||||
street: string;
|
street: string;
|
||||||
addressNumber: string;
|
addressNumber: string;
|
||||||
bairro: string;
|
bairro: string;
|
||||||
city: string;
|
city: string;
|
||||||
state: string;
|
state: string;
|
||||||
addressComplement: string;
|
addressComplement: string;
|
||||||
cep: string;
|
cep: string;
|
||||||
commentOrder1: string;
|
commentOrder1: string;
|
||||||
commentOrder2: string;
|
commentOrder2: string;
|
||||||
commentDelivery1: string;
|
commentDelivery1: string;
|
||||||
commentDelivery2: string;
|
commentDelivery2: string;
|
||||||
commentDelivery3: string;
|
commentDelivery3: string;
|
||||||
commentDelivery4: string;
|
commentDelivery4: string;
|
||||||
shippimentId: number;
|
shippimentId: number;
|
||||||
shippimentDate: Date;
|
shippimentDate: Date;
|
||||||
shippimentComment: string;
|
shippimentComment: string;
|
||||||
place: string;
|
place: string;
|
||||||
driver: string;
|
driver: string;
|
||||||
car: string;
|
car: string;
|
||||||
closeDate: Date;
|
closeDate: Date;
|
||||||
separatorName: string;
|
separatorName: string;
|
||||||
confName: string;
|
confName: string;
|
||||||
releaseDate: Date;
|
releaseDate: Date;
|
||||||
completedDeliveries?: DeliveryCompleted[];
|
completedDeliveries?: DeliveryCompleted[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export class OrderItemDto {
|
export class OrderItemDto {
|
||||||
productId: number;
|
productId: number;
|
||||||
description: string;
|
description: string;
|
||||||
pacth: string;
|
pacth: string;
|
||||||
color: string;
|
color: string;
|
||||||
stockId: number;
|
stockId: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
salePrice: number;
|
salePrice: number;
|
||||||
deliveryType: string;
|
deliveryType: string;
|
||||||
total: number;
|
total: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
department: string;
|
department: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
export class OrderStatusDto {
|
export class OrderStatusDto {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
status: string;
|
status: string;
|
||||||
statusDate: Date;
|
statusDate: Date;
|
||||||
userName: string;
|
userName: string;
|
||||||
comments: string | null;
|
comments: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
export class OrderTransferDto {
|
export class OrderTransferDto {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
transferDate: Date;
|
transferDate: Date;
|
||||||
invoiceId: number;
|
invoiceId: number;
|
||||||
transactionId: number;
|
transactionId: number;
|
||||||
oldShipment: number;
|
oldShipment: number;
|
||||||
newShipment: number;
|
newShipment: number;
|
||||||
transferText: string;
|
transferText: string;
|
||||||
cause: string;
|
cause: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
program: string;
|
program: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import { IsOptional, IsString, IsNumber, IsDateString } from 'class-validator';
|
|||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class DeliveryCompletedQuery {
|
export class DeliveryCompletedQuery {
|
||||||
@ApiPropertyOptional({ description: 'Data de início para filtro (formato YYYY-MM-DD)' })
|
@ApiPropertyOptional({
|
||||||
|
description: 'Data de início para filtro (formato YYYY-MM-DD)',
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Data de fim para filtro (formato YYYY-MM-DD)' })
|
@ApiPropertyOptional({
|
||||||
|
description: 'Data de fim para filtro (formato YYYY-MM-DD)',
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
@@ -42,7 +46,10 @@ export class DeliveryCompletedQuery {
|
|||||||
@IsString()
|
@IsString()
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Limite de registros por página', default: 100 })
|
@ApiPropertyOptional({
|
||||||
|
description: 'Limite de registros por página',
|
||||||
|
default: 100,
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsString, IsNumber, IsNotEmpty, IsOptional, MinLength } from 'class-validator';
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNumber,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
export class FindDebDto {
|
export class FindDebDto {
|
||||||
@@ -31,4 +37,3 @@ export class FindDebDto {
|
|||||||
})
|
})
|
||||||
cobranca?: string;
|
cobranca?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export class FindInvoiceDto {
|
export class FindInvoiceDto {
|
||||||
chavenfe: string;
|
chavenfe: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import {
|
|||||||
IsDateString,
|
IsDateString,
|
||||||
IsString,
|
IsString,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsIn,
|
IsBoolean,
|
||||||
IsBoolean
|
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,7 +16,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsDateString()
|
@IsDateString()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Data de entrega inicial (formato: YYYY-MM-DD)',
|
description: 'Data de entrega inicial (formato: YYYY-MM-DD)',
|
||||||
example: '2024-01-01'
|
example: '2024-01-01',
|
||||||
})
|
})
|
||||||
deliveryDateIni?: string;
|
deliveryDateIni?: string;
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsDateString()
|
@IsDateString()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Data de entrega final (formato: YYYY-MM-DD)',
|
description: 'Data de entrega final (formato: YYYY-MM-DD)',
|
||||||
example: '2024-12-31'
|
example: '2024-12-31',
|
||||||
})
|
})
|
||||||
deliveryDateEnd?: string;
|
deliveryDateEnd?: string;
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Código da filial',
|
description: 'Código da filial',
|
||||||
example: '01'
|
example: '01',
|
||||||
})
|
})
|
||||||
codfilial?: string;
|
codfilial?: string;
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'ID do vendedor (separado por vírgula para múltiplos valores)',
|
description: 'ID do vendedor (separado por vírgula para múltiplos valores)',
|
||||||
example: '270,431'
|
example: '270,431',
|
||||||
})
|
})
|
||||||
sellerId?: string;
|
sellerId?: string;
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'ID do cliente',
|
description: 'ID do cliente',
|
||||||
example: 456
|
example: 456,
|
||||||
})
|
})
|
||||||
customerId?: number;
|
customerId?: number;
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Tipo de entrega (EN, EF, RP, RI)',
|
description: 'Tipo de entrega (EN, EF, RP, RI)',
|
||||||
example: 'EN'
|
example: 'EN',
|
||||||
})
|
})
|
||||||
deliveryType?: string;
|
deliveryType?: string;
|
||||||
|
|
||||||
@@ -65,7 +64,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Status do pedido (L, P, B, M, F)',
|
description: 'Status do pedido (L, P, B, M, F)',
|
||||||
example: 'L'
|
example: 'L',
|
||||||
})
|
})
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'ID do pedido específico',
|
description: 'ID do pedido específico',
|
||||||
example: 236001388
|
example: 236001388,
|
||||||
})
|
})
|
||||||
orderId?: number;
|
orderId?: number;
|
||||||
|
|
||||||
@@ -82,7 +81,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Filtrar por status de transferência',
|
description: 'Filtrar por status de transferência',
|
||||||
example: 'Em Trânsito,Em Separação,Aguardando Separação,Concluída',
|
example: 'Em Trânsito,Em Separação,Aguardando Separação,Concluída',
|
||||||
enum: ['Em Trânsito', 'Em Separação', 'Aguardando Separação', 'Concluída']
|
enum: ['Em Trânsito', 'Em Separação', 'Aguardando Separação', 'Concluída'],
|
||||||
})
|
})
|
||||||
statusTransfer?: string;
|
statusTransfer?: string;
|
||||||
|
|
||||||
@@ -90,7 +89,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'ID da marca para filtrar pedidos',
|
description: 'ID da marca para filtrar pedidos',
|
||||||
example: 1
|
example: 1,
|
||||||
})
|
})
|
||||||
markId?: number;
|
markId?: number;
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Nome da marca para filtrar pedidos',
|
description: 'Nome da marca para filtrar pedidos',
|
||||||
example: 'Nike'
|
example: 'Nike',
|
||||||
})
|
})
|
||||||
markName?: string;
|
markName?: string;
|
||||||
|
|
||||||
@@ -106,8 +105,9 @@ export class FindOrdersByDeliveryDateDto {
|
|||||||
@Type(() => Boolean)
|
@Type(() => Boolean)
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Filtrar pedidos que tenham registros na tabela de transfer log',
|
description:
|
||||||
example: true
|
'Filtrar pedidos que tenham registros na tabela de transfer log',
|
||||||
|
example: true,
|
||||||
})
|
})
|
||||||
hasPreBox?: boolean;
|
hasPreBox?: boolean;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
export class InvoiceCheckItemDto {
|
export class InvoiceCheckItemDto {
|
||||||
productId: number;
|
productId: number;
|
||||||
seq: number;
|
seq: number;
|
||||||
qt: number;
|
qt: number;
|
||||||
confDate: string;
|
confDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export class LeadtimeDto {
|
export class LeadtimeDto {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
etapa: number;
|
etapa: number;
|
||||||
descricaoEtapa: string;
|
descricaoEtapa: string;
|
||||||
data: Date | string;
|
data: Date | string;
|
||||||
codigoFuncionario: number | null;
|
codigoFuncionario: number | null;
|
||||||
nomeFuncionario: string | null;
|
nomeFuncionario: string | null;
|
||||||
numeroPedido: number;
|
numeroPedido: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
|
|
||||||
export class OrderDeliveryDto {
|
export class OrderDeliveryDto {
|
||||||
storeId: number;
|
storeId: number;
|
||||||
createDate: Date;
|
createDate: Date;
|
||||||
orderId: number;
|
orderId: number;
|
||||||
orderIdSale: number | null;
|
orderIdSale: number | null;
|
||||||
deliveryDate: Date | null;
|
deliveryDate: Date | null;
|
||||||
cnpj: string | null;
|
cnpj: string | null;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
customer: string;
|
customer: string;
|
||||||
deliveryType: string | null;
|
deliveryType: string | null;
|
||||||
quantityItens: number;
|
quantityItens: number;
|
||||||
status: string;
|
status: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
shipmentId: number;
|
shipmentId: number;
|
||||||
driverId: number | null;
|
driverId: number | null;
|
||||||
driverName: string | null;
|
driverName: string | null;
|
||||||
carPlate: string | null;
|
carPlate: string | null;
|
||||||
carIdentification: string | null;
|
carIdentification: string | null;
|
||||||
observation: string | null;
|
observation: string | null;
|
||||||
deliveryConfirmationDate: Date | null;
|
deliveryConfirmationDate: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,13 @@ import { map } from 'rxjs/operators';
|
|||||||
import { ResultModel } from '../../shared/ResultModel';
|
import { ResultModel } from '../../shared/ResultModel';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrdersResponseInterceptor<T> implements NestInterceptor<T, ResultModel<T>> {
|
export class OrdersResponseInterceptor<T>
|
||||||
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<ResultModel<T>> {
|
implements NestInterceptor<T, ResultModel<T>>
|
||||||
|
{
|
||||||
|
intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler<T>,
|
||||||
|
): Observable<ResultModel<T>> {
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
map((data) => {
|
map((data) => {
|
||||||
return ResultModel.success(data);
|
return ResultModel.success(data);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export interface DebQueryParams {
|
export interface DebQueryParams {
|
||||||
cpfCgcent: string;
|
cpfCgcent: string;
|
||||||
matricula?: number;
|
matricula?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface MarkData {
|
export interface MarkData {
|
||||||
MARCA: string;
|
MARCA: string;
|
||||||
CODMARCA: number;
|
CODMARCA: number;
|
||||||
ATIVO: string;
|
ATIVO: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,9 @@ import { DatabaseModule } from '../../core/database/database.module';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [ConfigModule, DatabaseModule],
|
||||||
ConfigModule,
|
|
||||||
DatabaseModule,
|
|
||||||
],
|
|
||||||
controllers: [DebController],
|
controllers: [DebController],
|
||||||
providers: [DebService, DebRepository],
|
providers: [DebService, DebRepository],
|
||||||
exports: [DebService],
|
exports: [DebService],
|
||||||
})
|
})
|
||||||
export class DebModule {}
|
export class DebModule {}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import { DatabaseModule } from '../../core/database/database.module';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [ConfigModule, DatabaseModule],
|
||||||
ConfigModule,
|
|
||||||
DatabaseModule,
|
|
||||||
],
|
|
||||||
controllers: [OrdersController],
|
controllers: [OrdersController],
|
||||||
providers: [OrdersService, OrdersRepository],
|
providers: [OrdersService, OrdersRepository],
|
||||||
exports: [OrdersService],
|
exports: [OrdersService],
|
||||||
|
|||||||
@@ -1,68 +1,74 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { InjectDataSource } from '@nestjs/typeorm';
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
import { DebDto } from '../dto/DebDto';
|
import { DebDto } from '../dto/DebDto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DebRepository {
|
export class DebRepository {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectDataSource("oracle") private readonly oracleDataSource: DataSource,
|
@InjectDataSource('oracle') private readonly oracleDataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca débitos por CPF/CGCENT
|
* Busca débitos por CPF/CGCENT
|
||||||
* @param cpfCgcent - CPF ou CGCENT do cliente
|
* @param cpfCgcent - CPF ou CGCENT do cliente
|
||||||
* @param matricula - Matrícula do funcionário (opcional)
|
* @param matricula - Matrícula do funcionário (opcional)
|
||||||
* @param cobranca - Código de cobrança (opcional)
|
* @param cobranca - Código de cobrança (opcional)
|
||||||
* @returns Lista de débitos do cliente
|
* @returns Lista de débitos do cliente
|
||||||
* @throws {Error} Erro ao executar a query no banco de dados
|
* @throws {Error} Erro ao executar a query no banco de dados
|
||||||
*/
|
*/
|
||||||
async findByCpfCgcent(cpfCgcent: string, matricula?: number, cobranca?: string): Promise<DebDto[]> {
|
async findByCpfCgcent(
|
||||||
const queryRunner = this.oracleDataSource.createQueryRunner();
|
cpfCgcent: string,
|
||||||
await queryRunner.connect();
|
matricula?: number,
|
||||||
try {
|
cobranca?: string,
|
||||||
const queryBuilder = queryRunner.manager
|
): Promise<DebDto[]> {
|
||||||
.createQueryBuilder()
|
const queryRunner = this.oracleDataSource.createQueryRunner();
|
||||||
.select([
|
await queryRunner.connect();
|
||||||
'p.dtemissao AS "dtemissao"',
|
try {
|
||||||
'p.codfilial AS "codfilial"',
|
const queryBuilder = queryRunner.manager
|
||||||
'p.duplic AS "duplic"',
|
.createQueryBuilder()
|
||||||
'p.prest AS "prest"',
|
.select([
|
||||||
'p.codcli AS "codcli"',
|
'p.dtemissao AS "dtemissao"',
|
||||||
'c.cliente AS "cliente"',
|
'p.codfilial AS "codfilial"',
|
||||||
'p.codcob AS "codcob"',
|
'p.duplic AS "duplic"',
|
||||||
'cb.cobranca AS "cobranca"',
|
'p.prest AS "prest"',
|
||||||
'p.dtvenc AS "dtvenc"',
|
'p.codcli AS "codcli"',
|
||||||
'p.dtpag AS "dtpag"',
|
'c.cliente AS "cliente"',
|
||||||
'p.valor AS "valor"',
|
'p.codcob AS "codcob"',
|
||||||
`CASE
|
'cb.cobranca AS "cobranca"',
|
||||||
|
'p.dtvenc AS "dtvenc"',
|
||||||
|
'p.dtpag AS "dtpag"',
|
||||||
|
'p.valor AS "valor"',
|
||||||
|
`CASE
|
||||||
WHEN p.dtpag IS NOT NULL THEN 'PAGO'
|
WHEN p.dtpag IS NOT NULL THEN 'PAGO'
|
||||||
WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO'
|
WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO'
|
||||||
WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER'
|
WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER'
|
||||||
ELSE 'NENHUM'
|
ELSE 'NENHUM'
|
||||||
END AS "situacao"`,
|
END AS "situacao"`,
|
||||||
])
|
])
|
||||||
.from('pcprest', 'p')
|
.from('pcprest', 'p')
|
||||||
.innerJoin('pcclient', 'c', 'p.codcli = c.codcli')
|
.innerJoin('pcclient', 'c', 'p.codcli = c.codcli')
|
||||||
.innerJoin('pccob', 'cb', 'p.codcob = cb.codcob')
|
.innerJoin('pccob', 'cb', 'p.codcob = cb.codcob')
|
||||||
.innerJoin('pcempr', 'e', 'c.cgcent = e.cpf')
|
.innerJoin('pcempr', 'e', 'c.cgcent = e.cpf')
|
||||||
.where('p.codcob NOT IN (:...excludedCob)', { excludedCob: ['DESD', 'CANC'] })
|
.where('p.codcob NOT IN (:...excludedCob)', {
|
||||||
.andWhere('c.cgcent = :cpfCgcent', { cpfCgcent });
|
excludedCob: ['DESD', 'CANC'],
|
||||||
|
})
|
||||||
|
.andWhere('c.cgcent = :cpfCgcent', { cpfCgcent });
|
||||||
|
|
||||||
if (matricula) {
|
if (matricula) {
|
||||||
queryBuilder.andWhere('e.matricula = :matricula', { matricula });
|
queryBuilder.andWhere('e.matricula = :matricula', { matricula });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cobranca) {
|
if (cobranca) {
|
||||||
queryBuilder.andWhere('p.codcob = :cobranca', { cobranca });
|
queryBuilder.andWhere('p.codcob = :cobranca', { cobranca });
|
||||||
}
|
}
|
||||||
|
|
||||||
queryBuilder.orderBy('p.dtvenc', 'ASC');
|
queryBuilder.orderBy('p.dtvenc', 'ASC');
|
||||||
|
|
||||||
const result = await queryBuilder.getRawMany();
|
const result = await queryBuilder.getRawMany();
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||||
import { Controller, Get, Param } from '@nestjs/common';
|
import { Controller, Get, Param } from '@nestjs/common';
|
||||||
import { PartnersService } from './partners.service';
|
import { PartnersService } from './partners.service';
|
||||||
import { PartnerDto } from './dto/partner.dto';
|
import { PartnerDto } from './dto/partner.dto';
|
||||||
@@ -6,43 +6,45 @@ import { PartnerDto } from './dto/partner.dto';
|
|||||||
@ApiTags('Parceiros')
|
@ApiTags('Parceiros')
|
||||||
@Controller('api/v1/')
|
@Controller('api/v1/')
|
||||||
export class PartnersController {
|
export class PartnersController {
|
||||||
|
constructor(private readonly partnersService: PartnersService) {}
|
||||||
|
|
||||||
constructor(private readonly partnersService: PartnersService) {}
|
@Get('parceiros/:filter')
|
||||||
|
@ApiOperation({ summary: 'Busca parceiros por filtro (ID, CPF ou nome)' })
|
||||||
|
@ApiParam({
|
||||||
|
name: 'filter',
|
||||||
|
description: 'Filtro de busca (ID, CPF ou nome)',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de parceiros encontrados.',
|
||||||
|
type: PartnerDto,
|
||||||
|
isArray: true,
|
||||||
|
})
|
||||||
|
async findPartners(@Param('filter') filter: string): Promise<PartnerDto[]> {
|
||||||
|
return this.partnersService.findPartners(filter);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('parceiros/:filter')
|
@Get('parceiros')
|
||||||
@ApiOperation({ summary: 'Busca parceiros por filtro (ID, CPF ou nome)' })
|
@ApiOperation({ summary: 'Lista todos os parceiros' })
|
||||||
@ApiParam({ name: 'filter', description: 'Filtro de busca (ID, CPF ou nome)' })
|
@ApiResponse({
|
||||||
@ApiResponse({
|
status: 200,
|
||||||
status: 200,
|
description: 'Lista de todos os parceiros.',
|
||||||
description: 'Lista de parceiros encontrados.',
|
type: PartnerDto,
|
||||||
type: PartnerDto,
|
isArray: true,
|
||||||
isArray: true
|
})
|
||||||
})
|
async getAllPartners(): Promise<PartnerDto[]> {
|
||||||
async findPartners(@Param('filter') filter: string): Promise<PartnerDto[]> {
|
return this.partnersService.getAllPartners();
|
||||||
return this.partnersService.findPartners(filter);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Get('parceiros')
|
@Get('parceiros/id/:id')
|
||||||
@ApiOperation({ summary: 'Lista todos os parceiros' })
|
@ApiOperation({ summary: 'Busca parceiro por ID específico' })
|
||||||
@ApiResponse({
|
@ApiParam({ name: 'id', description: 'ID do parceiro' })
|
||||||
status: 200,
|
@ApiResponse({
|
||||||
description: 'Lista de todos os parceiros.',
|
status: 200,
|
||||||
type: PartnerDto,
|
description: 'Parceiro encontrado.',
|
||||||
isArray: true
|
type: PartnerDto,
|
||||||
})
|
})
|
||||||
async getAllPartners(): Promise<PartnerDto[]> {
|
async getPartnerById(@Param('id') id: string): Promise<PartnerDto | null> {
|
||||||
return this.partnersService.getAllPartners();
|
return this.partnersService.getPartnerById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('parceiros/id/:id')
|
|
||||||
@ApiOperation({ summary: 'Busca parceiro por ID específico' })
|
|
||||||
@ApiParam({ name: 'id', description: 'ID do parceiro' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Parceiro encontrado.',
|
|
||||||
type: PartnerDto
|
|
||||||
})
|
|
||||||
async getPartnerById(@Param('id') id: string): Promise<PartnerDto | null> {
|
|
||||||
return this.partnersService.getPartnerById(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user