refactor: atualizações e remoção de módulos não utilizados

This commit is contained in:
joelson brito
2025-11-10 09:39:44 -03:00
parent ed68b7e865
commit b8630adf92
121 changed files with 3507 additions and 3531 deletions

View File

@@ -1,6 +0,0 @@
export interface ILogger {
log(message: string): void;
warn(message: string): void;
error(message: string, trace?: string): void;
}

View File

@@ -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;
}
}

View File

@@ -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;
};
}

View File

@@ -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 {}

View File

@@ -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');
} }
} }

View File

@@ -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,
}; };
} }

View File

@@ -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');
}); });
}); });
}); });

View File

@@ -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(
async () => {
callOrder.push('refreshToken'); callOrder.push('refreshToken');
return 'mock.refresh.token'; 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(
async () => {
return `mock.refresh.token.${Math.random()}`; 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', () => {
}); });
}); });
}); });

View File

@@ -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);
}); });
}); });
}); });

View File

@@ -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');
}); });
}); });
}); });

View File

@@ -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),
),
}, },
}; };
} }

View File

@@ -42,7 +42,7 @@ import { LoginAuditService } from '../services/login-audit.service';
RefreshTokenService, RefreshTokenService,
SessionManagementService, SessionManagementService,
LoginAuditService, LoginAuditService,
AuthenticateUserHandler AuthenticateUserHandler,
], ],
exports: [AuthService], exports: [AuthService],
}) })

View File

@@ -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,9 +180,10 @@ export class AuthService {
this.validateUserDataForToken(user); this.validateUserDataForToken(user);
if (tokenData.sessionId) { if (tokenData.sessionId) {
const isSessionActive = await this.sessionManagementService.isSessionActive( const isSessionActive =
await this.sessionManagementService.isSessionActive(
user.id, user.id,
tokenData.sessionId 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}`,
);
} }
} }
} }

View File

@@ -3,5 +3,4 @@ export class AuthenticateUserCommand {
public readonly username: string, public readonly username: string,
public readonly password: string, public readonly password: string,
) {} ) {}
} }

View File

@@ -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);
} }
} }

View File

@@ -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)

View File

@@ -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', () => {
}); });
}); });
}); });

View File

@@ -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,

View File

@@ -12,5 +12,4 @@ export class Result<T> {
static fail<U>(message: string): Result<U> { static fail<U>(message: string): Result<U> {
return new Result<U>(false, undefined, message); return new Result<U>(false, undefined, message);
} }
} }

View File

@@ -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;
} }

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
} }
} }

View File

@@ -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,9 +39,10 @@ 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 =
await this.sessionManagementService.isSessionActive(
payload.id, payload.id,
payload.sessionId payload.sessionId,
); );
if (!isSessionActive) { if (!isSessionActive) {
@@ -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 = {

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,
);
} }
} }

View File

@@ -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)),
);
} }
} }

View File

@@ -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) {

View File

@@ -3,19 +3,23 @@ import {
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>>
{
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);
}), }),
); );
} }
} }

View File

@@ -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

View File

@@ -5,6 +5,9 @@ export interface IRedisClient {
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>;
}

View File

@@ -1,4 +1,3 @@
import { RedisClientAdapter } from './redis-client.adapter'; import { RedisClientAdapter } from './redis-client.adapter';
export const RedisClientToken = 'RedisClientInterface'; export const RedisClientToken = 'RedisClientInterface';

View File

@@ -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);
} }
} }

View File

@@ -1,8 +1,8 @@
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({
@@ -18,4 +18,4 @@
return redis; return redis;
}, },
inject: [ConfigService], inject: [ConfigService],
}; };

View File

@@ -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 = {

View File

@@ -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: {

View File

@@ -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 { }

View File

@@ -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 { }

View File

@@ -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 { }

View File

@@ -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 { }

View File

@@ -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 { }

View File

@@ -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 { }

View File

@@ -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;
}
}

View File

@@ -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 { }

View File

@@ -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 { }

View File

@@ -1,13 +1,15 @@
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 = (
methods: Partial<DataConsultRepository> = {},
) =>
({
findStores: jest.fn(), findStores: jest.fn(),
findSellers: jest.fn(), findSellers: jest.fn(),
findBillings: jest.fn(), findBillings: jest.fn(),
@@ -16,35 +18,30 @@ export const createMockRepository = (methods: Partial<DataConsultRepository> = {
findAllCarriers: jest.fn(), findAllCarriers: jest.fn(),
findRegions: jest.fn(), findRegions: jest.fn(),
...methods, ...methods,
} as any); } as any);
export const createMockLogger = () => ({ export const createMockRedisClient = () =>
log: jest.fn(), ({
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
} as any);
export const createMockRedisClient = () => ({
get: jest.fn().mockResolvedValue(null), get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(undefined), set: jest.fn().mockResolvedValue(undefined),
} as any); } 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,
}; };
} }

View File

@@ -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,
);
}); });
}); });
}); });

View File

@@ -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') @Get('clientes/:filter')
async customer(@Param('filter') filter: string) { async customer(@Param('filter') filter: string) {
return this.clientesService.customers(filter); return this.clientesService.customers(filter);
} }
} }

View File

@@ -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';

View File

@@ -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}`);
} }
} }

View File

@@ -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();
} }
} }

View File

@@ -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 {}

View File

@@ -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,
);
} }
} }
} }

View File

@@ -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()

View File

@@ -20,4 +20,3 @@ export class RegionDto {
Object.assign(this, partial); Object.assign(this, partial);
} }
} }

View File

@@ -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}`);
}
}
}

View File

@@ -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(),
]);
}
}

View File

@@ -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 {}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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],
}),
];

View File

@@ -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');
}
}

View File

@@ -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
*/ */

View File

@@ -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';

View File

@@ -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';
@@ -16,7 +23,6 @@ export class LogisticService {
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
@@ -52,7 +58,6 @@ 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"
@@ -86,7 +91,11 @@ export class LogisticService {
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) {
@@ -103,7 +112,6 @@ export class LogisticService {
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"
@@ -138,7 +146,6 @@ export class LogisticService {
await queryRunner.release(); await queryRunner.release();
await dataSource.destroy(); await dataSource.destroy();
} }
} }
async getStatusCar(placa: string) { async getStatusCar(placa: string) {
@@ -147,7 +154,6 @@ export class LogisticService {
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}'
@@ -155,8 +161,7 @@ export class LogisticService {
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 {
@@ -188,14 +193,12 @@ export class LogisticService {
} }
async createCarOut(data: CarOutDelivery) { async createCarOut(data: CarOutDelivery) {
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 sqlSequence = `SELECT ESS_SAIDAVEICULO.NEXTVAL as "id" FROM DUAL`; const sqlSequence = `SELECT ESS_SAIDAVEICULO.NEXTVAL as "id" FROM DUAL`;
const dataSequence = await queryRunner.query(sqlSequence); const dataSequence = await queryRunner.query(sqlSequence);
let i = 0; let i = 0;
@@ -207,7 +210,7 @@ export class LogisticService {
const image3 = ''; const image3 = '';
const image4 = ''; const image4 = '';
data.helpers.forEach(helper => { data.helpers.forEach((helper) => {
switch (i) { switch (i) {
case 0: case 0:
helperId1 = helper.id; helperId1 = helper.id;
@@ -246,13 +249,11 @@ export class LogisticService {
,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!'} return { message: 'Dados da saída de veículo gravada com sucesso!' };
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction(); await queryRunner.rollbackTransaction();
throw e; throw e;
@@ -263,14 +264,12 @@ export class LogisticService {
} }
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
@@ -280,8 +279,11 @@ export class LogisticService {
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;
@@ -332,8 +334,7 @@ export class LogisticService {
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);
@@ -343,5 +344,4 @@ export class LogisticService {
await dataSource.destroy(); await dataSource.destroy();
} }
} }
} }

View File

@@ -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(
helmet({
contentSecurityPolicy: { contentSecurityPolicy: {
directives: { directives: {
defaultSrc: [`'self'`], defaultSrc: [`'self'`],
scriptSrc: [`'self'`, `'unsafe-inline'`, 'cdn.jsdelivr.net', 'cdnjs.cloudflare.com'], scriptSrc: [
`'self'`,
`'unsafe-inline'`,
'cdn.jsdelivr.net',
'cdnjs.cloudflare.com',
],
styleSrc: [`'self'`, `'unsafe-inline'`, 'cdnjs.cloudflare.com'], styleSrc: [`'self'`, `'unsafe-inline'`, 'cdnjs.cloudflare.com'],
imgSrc: [`'self'`, 'data:'], imgSrc: [`'self'`, 'data:'],
connectSrc: [`'self'`], connectSrc: [`'self'`],
fontSrc: [`'self'`, 'cdnjs.cloudflare.com'], 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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,8 +18,7 @@ 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') @Get('orders/:id')
@ApiOperation({ summary: 'Lista todos os pedidos de uma loja' }) @ApiOperation({ summary: 'Lista todos os pedidos de uma loja' })
@@ -21,7 +26,7 @@ export class OrdersPaymentController {
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Lista de pedidos retornada com sucesso', description: 'Lista de pedidos retornada com sucesso',
type: [OrderDto] type: [OrderDto],
}) })
async findOrders(@Param('id') storeId: string): Promise<OrderDto[]> { async findOrders(@Param('id') storeId: string): Promise<OrderDto[]> {
return this.orderPaymentService.findOrders(storeId, 0); return this.orderPaymentService.findOrders(storeId, 0);
@@ -34,7 +39,7 @@ export class OrdersPaymentController {
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Pedido retornado com sucesso', description: 'Pedido retornado com sucesso',
type: OrderDto type: OrderDto,
}) })
async findOrder( async findOrder(
@Param('id') storeId: string, @Param('id') storeId: string,
@@ -50,7 +55,7 @@ export class OrdersPaymentController {
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Lista de pagamentos retornada com sucesso', description: 'Lista de pagamentos retornada com sucesso',
type: [PaymentDto] type: [PaymentDto],
}) })
async findPayments(@Param('id') orderId: number): Promise<PaymentDto[]> { async findPayments(@Param('id') orderId: number): Promise<PaymentDto[]> {
return this.orderPaymentService.findPayments(orderId); return this.orderPaymentService.findPayments(orderId);
@@ -59,7 +64,7 @@ export class OrdersPaymentController {
@ApiOperation({ summary: 'Cria um novo pagamento' }) @ApiOperation({ summary: 'Cria um novo pagamento' })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
description: 'Pagamento criado com sucesso' description: 'Pagamento criado com sucesso',
}) })
async createPayment(@Body() data: CreatePaymentDto): Promise<void> { async createPayment(@Body() data: CreatePaymentDto): Promise<void> {
return this.orderPaymentService.createPayment(data); return this.orderPaymentService.createPayment(data);
@@ -69,9 +74,9 @@ export class OrdersPaymentController {
@ApiOperation({ summary: 'Cria uma nova fatura' }) @ApiOperation({ summary: 'Cria uma nova fatura' })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
description: 'Fatura criada com sucesso' description: 'Fatura criada com sucesso',
}) })
async createInvoice(@Body() data: CreateInvoiceDto): Promise<void> { async createInvoice(@Body() data: CreateInvoiceDto): Promise<void> {
return this.orderPaymentService.createInvoice(data); return this.orderPaymentService.createInvoice(data);
} }
} }

View File

@@ -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

View File

@@ -11,7 +11,7 @@ import { CreateInvoiceDto } from './dto/create-invoice.dto';
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[]> {
@@ -48,7 +48,7 @@ export class OrdersPaymentService {
} }
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();
} }
@@ -73,7 +73,7 @@ export class OrdersPaymentService {
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();
} }

View File

@@ -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,
);
} }
} }

View File

@@ -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,19 +44,10 @@ 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,
key,
this.TTL_ORDERS,
async () => {
const orders = await this.ordersRepository.findOrders(query); const orders = await this.ordersRepository.findOrders(query);
if (!query.includeCompletedDeliveries) { if (!query.includeCompletedDeliveries) {
@@ -65,57 +58,43 @@ export class OrdersService {
const deliveryQuery = { const deliveryQuery = {
orderNumber: order.invoiceNumber, orderNumber: order.invoiceNumber,
limit: 10, limit: 10,
offset: 0 offset: 0,
}; };
try { try {
const deliveries = await this.ordersRepository.getCompletedDeliveries(deliveryQuery); const deliveries = await this.ordersRepository.getCompletedDeliveries(
deliveryQuery,
);
order.completedDeliveries = deliveries; order.completedDeliveries = deliveries;
} catch (error) { } catch (error) {
// Se houver erro, definir como array vazio
order.completedDeliveries = []; order.completedDeliveries = [];
} }
} }
return orders; 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,
key,
this.TTL_ORDERS,
async () => {
// Primeiro obtém a lista de pedidos
const orders = await this.findOrders(query); const orders = await this.findOrders(query);
// Para cada pedido, busca o fechamento de caixa
const results = await Promise.all( const results = await Promise.all(
orders.map(async order => { orders.map(async (order) => {
try { try {
const checkout = await this.ordersRepository.findOrderWithCheckoutByOrder( const checkout =
await this.ordersRepository.findOrderWithCheckoutByOrder(
Number(order.orderId), Number(order.orderId),
); );
return { ...order, checkout }; return { ...order, checkout };
@@ -125,29 +104,25 @@ export class OrdersService {
}), }),
); );
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 () => {
const result = await this.ordersRepository.findOrderWithCheckoutByOrder(orderId);
if (!result) { if (!result) {
throw new HttpException('Nenhum fechamento encontrado', HttpStatus.NOT_FOUND); throw new HttpException(
} 'Nenhum fechamento encontrado',
return result; HttpStatus.NOT_FOUND,
}
); );
} }
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(

View File

@@ -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,

View File

@@ -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,11 +220,13 @@ 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(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderTransferDto[] | null> {
try { try {
return await this.ordersService.getTransfer(orderId); return await this.ordersService.getTransfer(orderId);
} catch (error) { } catch (error) {
@@ -172,11 +237,13 @@ export class OrdersController {
} }
} }
@Get('status/:orderId') @Get('status/:orderId')
@ApiOperation({ summary: 'Consulta status do pedido' }) @ApiOperation({ summary: 'Consulta status do pedido' })
@ApiParam({ name: 'orderId' }) @ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getStatusOrder(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderStatusDto[] | null> { async getStatusOrder(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderStatusDto[] | null> {
try { try {
return await this.ordersService.getStatusOrder(orderId); return await this.ordersService.getStatusOrder(orderId);
} catch (error) { } catch (error) {
@@ -187,20 +254,32 @@ export class OrdersController {
} }
} }
@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,12 +287,13 @@ 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(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<LeadtimeDto[]> {
try { try {
return await this.ordersService.getLeadtime(orderId.toString()); return await this.ordersService.getLeadtime(orderId.toString());
} catch (error) { } catch (error) {
@@ -222,27 +302,31 @@ export class OrdersController {
error.status || HttpStatus.INTERNAL_SERVER_ERROR, error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@Post('invoice/check') @Post('invoice/check')
@ApiOperation({ summary: 'Cria conferência de nota fiscal' }) @ApiOperation({ summary: 'Cria conferência de nota fiscal' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async createInvoiceCheck(@Body() invoice: InvoiceCheckDto): Promise<{ message: string }> { async createInvoiceCheck(
@Body() invoice: InvoiceCheckDto,
): Promise<{ message: string }> {
try { try {
return await this.ordersService.createInvoiceCheck(invoice); return await this.ordersService.createInvoiceCheck(invoice);
} catch (error) { } catch (error) {
throw new HttpException( throw new HttpException(
error.message || 'Erro ao salvar conferência', error.message || 'Erro ao salvar conferência',
error.status || HttpStatus.INTERNAL_SERVER_ERROR error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@Get('carriers/:orderId') @Get('carriers/:orderId')
@ApiOperation({ summary: 'Busca transportadoras do pedido' }) @ApiOperation({ summary: 'Busca transportadoras do pedido' })
@ApiParam({ name: 'orderId', example: 236001388 }) @ApiParam({ name: 'orderId', example: 236001388 })
@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[]> {
try { try {
return await this.ordersService.getOrderCarriers(orderId); return await this.ordersService.getOrderCarriers(orderId);
} catch (error) { } catch (error) {
@@ -251,15 +335,21 @@ async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise
error.status || HttpStatus.INTERNAL_SERVER_ERROR, error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@Get('mark/:orderId') @Get('mark/:orderId')
@ApiOperation({ summary: 'Busca marca por ID do pedido' }) @ApiOperation({ summary: 'Busca marca por ID do pedido' })
@ApiParam({ name: 'orderId', example: 236001388 }) @ApiParam({ name: 'orderId', example: 236001388 })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Marca encontrada com sucesso', type: MarkResponseDto }) @ApiResponse({
@ApiResponse({ status: 404, description: 'Marca não encontrada' }) status: 200,
async findOrderByMark(@Param('orderId', ParseIntPipe) orderId: number): Promise<MarkResponseDto> { 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 { try {
return await this.ordersService.findOrderByMark(orderId); return await this.ordersService.findOrderByMark(orderId);
} catch (error) { } catch (error) {
@@ -268,13 +358,17 @@ async findOrderByMark(@Param('orderId', ParseIntPipe) orderId: number): Promise<
error.status || HttpStatus.INTERNAL_SERVER_ERROR, error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@Get('marks') @Get('marks')
@ApiOperation({ summary: 'Busca todas as marcas disponíveis' }) @ApiOperation({ summary: 'Busca todas as marcas disponíveis' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Lista de marcas retornada com sucesso', type: [MarkResponseDto] }) @ApiResponse({
async getAllMarks(): Promise<MarkResponseDto[]> { status: 200,
description: 'Lista de marcas retornada com sucesso',
type: [MarkResponseDto],
})
async getAllMarks(): Promise<MarkResponseDto[]> {
try { try {
return await this.ordersService.getAllMarks(); return await this.ordersService.getAllMarks();
} catch (error) { } catch (error) {
@@ -283,14 +377,25 @@ async getAllMarks(): Promise<MarkResponseDto[]> {
error.status || HttpStatus.INTERNAL_SERVER_ERROR, error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@Get('marks/search') @Get('marks/search')
@ApiOperation({ summary: 'Busca marcas por nome' }) @ApiOperation({ summary: 'Busca marcas por nome' })
@ApiQuery({ name: 'name', required: true, type: 'string', description: 'Nome da marca para buscar' }) @ApiQuery({
@UsePipes(new ValidationPipe({ transform: true })) name: 'name',
@ApiResponse({ status: 200, description: 'Lista de marcas encontradas', type: [MarkResponseDto] }) required: true,
async getMarksByName(@Query('name') markName: string): Promise<MarkResponseDto[]> { 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 { try {
return await this.ordersService.getMarksByName(markName); return await this.ordersService.getMarksByName(markName);
} catch (error) { } catch (error) {
@@ -299,28 +404,63 @@ async getMarksByName(@Query('name') markName: string): Promise<MarkResponseDto[]
error.status || HttpStatus.INTERNAL_SERVER_ERROR, error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@Get('transfer-log/:orderId') @Get('transfer-log/:orderId')
@ApiOperation({ summary: 'Busca log de transferência por ID do pedido' }) @ApiOperation({ summary: 'Busca log de transferência por ID do pedido' })
@ApiParam({ name: 'orderId', description: 'ID do pedido para buscar log de transferência' }) @ApiParam({
@ApiQuery({ name: 'dttransf', required: false, type: 'string', description: 'Data de transferência (formato YYYY-MM-DD)' }) name: 'orderId',
@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: 'digo da filial de origem' }) description: 'ID do pedido para buscar log de transferência',
@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({
@ApiQuery({ name: 'numpedtransf', required: false, type: 'number', description: 'Número do pedido de transferência' }) name: 'dttransf',
@UsePipes(new ValidationPipe({ transform: true })) required: false,
@ApiResponse({ status: 200, description: 'Log de transferência encontrado com sucesso', type: [EstLogTransferResponseDto] }) type: 'string',
@ApiResponse({ status: 400, description: 'OrderId inválido' }) description: 'Data de transferência (formato YYYY-MM-DD)',
@ApiResponse({ status: 404, description: 'Log de transferência não encontrado' }) })
async getTransferLog( @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, @Param('orderId', ParseIntPipe) orderId: number,
@Query('dttransf') dttransf?: string, @Query('dttransf') dttransf?: string,
@Query('codfilial') codfilial?: number, @Query('codfilial') codfilial?: number,
@Query('codfilialdest') codfilialdest?: number, @Query('codfilialdest') codfilialdest?: number,
@Query('numpedloja') numpedloja?: number, @Query('numpedloja') numpedloja?: number,
@Query('numpedtransf') numpedtransf?: number, @Query('numpedtransf') numpedtransf?: number,
) { ) {
try { try {
const filters = { const filters = {
dttransf, dttransf,
@@ -337,21 +477,60 @@ async getTransferLog(
error.status || HttpStatus.INTERNAL_SERVER_ERROR, error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@Get('transfer-log') @Get('transfer-log')
@ApiOperation({ summary: 'Busca logs de transferência com filtros' }) @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({
@ApiQuery({ name: 'dttransfIni', required: false, type: 'string', description: 'Data de transferência inicial (formato YYYY-MM-DD)' }) name: 'dttransf',
@ApiQuery({ name: 'dttransfEnd', required: false, type: 'string', description: 'Data de transferência final (formato YYYY-MM-DD)' }) required: false,
@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: 'Código da filial de origem' }) type: 'string',
@ApiQuery({ name: 'codfilialdest', required: false, type: 'number', description: 'Código da filial de destino' }) description: 'Data de transferência (formato YYYY-MM-DD)',
@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' }) @ApiQuery({
@UsePipes(new ValidationPipe({ transform: true })) name: 'dttransfIni',
@ApiResponse({ status: 200, description: 'Logs de transferência encontrados com sucesso', type: [EstLogTransferResponseDto] }) required: false,
@ApiResponse({ status: 400, description: 'Filtros inválidos' }) type: 'string',
async getTransferLogs( 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('dttransf') dttransf?: string,
@Query('dttransfIni') dttransfIni?: string, @Query('dttransfIni') dttransfIni?: string,
@Query('dttransfEnd') dttransfEnd?: string, @Query('dttransfEnd') dttransfEnd?: string,
@@ -359,7 +538,7 @@ async getTransferLogs(
@Query('codfilialdest') codfilialdest?: number, @Query('codfilialdest') codfilialdest?: number,
@Query('numpedloja') numpedloja?: number, @Query('numpedloja') numpedloja?: number,
@Query('numpedtransf') numpedtransf?: number, @Query('numpedtransf') numpedtransf?: number,
) { ) {
try { try {
const filters = { const filters = {
dttransf, dttransf,
@@ -378,5 +557,5 @@ async getTransferLogs(
error.status || HttpStatus.INTERNAL_SERVER_ERROR, error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
} }

View File

@@ -6,5 +6,4 @@ export class CutItemDto {
saleQuantity: number; saleQuantity: number;
cutQuantity: number; cutQuantity: number;
separedQuantity: number; separedQuantity: number;
} }

View File

@@ -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;
} }

View File

@@ -27,5 +27,4 @@ export class OrderDeliveryDto {
confName: string; confName: string;
releaseDate: Date; releaseDate: Date;
completedDeliveries?: DeliveryCompleted[]; completedDeliveries?: DeliveryCompleted[];
} }

View File

@@ -11,4 +11,4 @@ export class OrderItemDto {
weight: number; weight: number;
department: string; department: string;
brand: string; brand: string;
} }

View File

@@ -4,5 +4,4 @@ export class OrderStatusDto {
statusDate: Date; statusDate: Date;
userName: string; userName: string;
comments: string | null; comments: string | null;
} }

View File

@@ -9,5 +9,4 @@ export class OrderTransferDto {
cause: string; cause: string;
userName: string; userName: string;
program: string; program: string;
} }

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -1,4 +1,3 @@
export class FindInvoiceDto { export class FindInvoiceDto {
chavenfe: string; chavenfe: string;
} }

View File

@@ -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;
} }

View File

@@ -3,5 +3,4 @@ export class InvoiceCheckItemDto {
seq: number; seq: number;
qt: number; qt: number;
confDate: string; confDate: string;
} }

View File

@@ -6,4 +6,4 @@ export class LeadtimeDto {
codigoFuncionario: number | null; codigoFuncionario: number | null;
nomeFuncionario: string | null; nomeFuncionario: string | null;
numeroPedido: number; numeroPedido: number;
} }

View File

@@ -1,4 +1,3 @@
export class OrderDeliveryDto { export class OrderDeliveryDto {
storeId: number; storeId: number;
createDate: Date; createDate: Date;
@@ -19,5 +18,4 @@ export class OrderDeliveryDto {
carIdentification: string | null; carIdentification: string | null;
observation: string | null; observation: string | null;
deliveryConfirmationDate: Date | null; deliveryConfirmationDate: Date | null;
} }

View File

@@ -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);

View File

@@ -1,6 +1,4 @@
export interface DebQueryParams { export interface DebQueryParams {
cpfCgcent: string; cpfCgcent: string;
matricula?: number; matricula?: number;
} }

View File

@@ -2,4 +2,4 @@ export interface MarkData {
MARCA: string; MARCA: string;
CODMARCA: number; CODMARCA: number;
ATIVO: string; ATIVO: string;
} }

View File

@@ -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 {}

View File

@@ -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],

View File

@@ -1,4 +1,4 @@
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';
@@ -6,7 +6,7 @@ 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,
) {} ) {}
/** /**
@@ -17,7 +17,11 @@ export class DebRepository {
* @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(
cpfCgcent: string,
matricula?: number,
cobranca?: string,
): Promise<DebDto[]> {
const queryRunner = this.oracleDataSource.createQueryRunner(); const queryRunner = this.oracleDataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
try { try {
@@ -46,7 +50,9 @@ export class DebRepository {
.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)', {
excludedCob: ['DESD', 'CANC'],
})
.andWhere('c.cgcent = :cpfCgcent', { cpfCgcent }); .andWhere('c.cgcent = :cpfCgcent', { cpfCgcent });
if (matricula) { if (matricula) {

View File

@@ -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,17 +6,19 @@ 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') @Get('parceiros/:filter')
@ApiOperation({ summary: 'Busca parceiros por filtro (ID, CPF ou nome)' }) @ApiOperation({ summary: 'Busca parceiros por filtro (ID, CPF ou nome)' })
@ApiParam({ name: 'filter', description: 'Filtro de busca (ID, CPF ou nome)' }) @ApiParam({
name: 'filter',
description: 'Filtro de busca (ID, CPF ou nome)',
})
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Lista de parceiros encontrados.', description: 'Lista de parceiros encontrados.',
type: PartnerDto, type: PartnerDto,
isArray: true isArray: true,
}) })
async findPartners(@Param('filter') filter: string): Promise<PartnerDto[]> { async findPartners(@Param('filter') filter: string): Promise<PartnerDto[]> {
return this.partnersService.findPartners(filter); return this.partnersService.findPartners(filter);
@@ -28,7 +30,7 @@ export class PartnersController {
status: 200, status: 200,
description: 'Lista de todos os parceiros.', description: 'Lista de todos os parceiros.',
type: PartnerDto, type: PartnerDto,
isArray: true isArray: true,
}) })
async getAllPartners(): Promise<PartnerDto[]> { async getAllPartners(): Promise<PartnerDto[]> {
return this.partnersService.getAllPartners(); return this.partnersService.getAllPartners();
@@ -40,7 +42,7 @@ export class PartnersController {
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Parceiro encontrado.', description: 'Parceiro encontrado.',
type: PartnerDto type: PartnerDto,
}) })
async getPartnerById(@Param('id') id: string): Promise<PartnerDto | null> { async getPartnerById(@Param('id') id: string): Promise<PartnerDto | null> {
return this.partnersService.getPartnerById(id); return this.partnersService.getPartnerById(id);

View File

@@ -8,7 +8,7 @@ import { PartnerDto } from './dto/partner.dto';
@Injectable() @Injectable()
export class PartnersService { export class PartnersService {
private readonly PARTNERS_TTL = 60 * 60 * 12; // 12 horas private readonly PARTNERS_TTL = 60 * 60 * 12;
private readonly PARTNERS_CACHE_KEY = 'parceiros:search'; private readonly PARTNERS_CACHE_KEY = 'parceiros:search';
constructor( constructor(
@@ -18,11 +18,6 @@ export class PartnersService {
private readonly redisClient: IRedisClient, private readonly redisClient: IRedisClient,
) {} ) {}
/**
* Buscar parceiros com cache otimizado
* @param filter - Filtro de busca (ID, CPF ou nome)
* @returns Array de parceiros encontrados
*/
async findPartners(filter: string): Promise<PartnerDto[]> { async findPartners(filter: string): Promise<PartnerDto[]> {
const cacheKey = `${this.PARTNERS_CACHE_KEY}:${filter}`; const cacheKey = `${this.PARTNERS_CACHE_KEY}:${filter}`;
@@ -35,7 +30,6 @@ export class PartnersService {
await queryRunner.connect(); await queryRunner.connect();
try { try {
// Primeira tentativa: busca por ID do parceiro
let sql = `SELECT ESTPARCEIRO.ID as "id", let sql = `SELECT ESTPARCEIRO.ID as "id",
ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME ||
' ( ' || ESTPARCEIRO.CPF || ' )' as "name", ' ( ' || ESTPARCEIRO.CPF || ' )' as "name",
@@ -45,7 +39,6 @@ export class PartnersService {
ORDER BY ESTPARCEIRO.NOME`; ORDER BY ESTPARCEIRO.NOME`;
let partners = await queryRunner.manager.query(sql); let partners = await queryRunner.manager.query(sql);
// Segunda tentativa: busca por CPF se não encontrou por ID
if (partners.length === 0) { if (partners.length === 0) {
sql = `SELECT ESTPARCEIRO.ID as "id", sql = `SELECT ESTPARCEIRO.ID as "id",
ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME ||
@@ -57,34 +50,34 @@ export class PartnersService {
partners = await queryRunner.manager.query(sql); partners = await queryRunner.manager.query(sql);
} }
// Terceira tentativa: busca por nome do parceiro se não encontrou por ID ou CPF
if (partners.length === 0) { if (partners.length === 0) {
sql = `SELECT ESTPARCEIRO.ID as "id", sql = `SELECT ESTPARCEIRO.ID as "id",
ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME ||
' ( ' || ESTPARCEIRO.CPF || ' )' as "name", ' ( ' || ESTPARCEIRO.CPF || ' )' as "name",
ESTPARCEIRO.CPF as "cpf" ESTPARCEIRO.CPF as "cpf"
FROM ESTPARCEIRO FROM ESTPARCEIRO
WHERE ESTPARCEIRO.NOME LIKE '${filter.toUpperCase().replace('@', '%')}%' WHERE ESTPARCEIRO.NOME LIKE '${filter
.toUpperCase()
.replace('@', '%')}%'
ORDER BY ESTPARCEIRO.NOME`; ORDER BY ESTPARCEIRO.NOME`;
partners = await queryRunner.manager.query(sql); partners = await queryRunner.manager.query(sql);
} }
return partners.map(partner => new PartnerDto({ return partners.map(
(partner) =>
new PartnerDto({
id: partner.id, id: partner.id,
cpf: partner.cpf, cpf: partner.cpf,
nome: partner.name nome: partner.name,
})); }),
);
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
} }
} },
); );
} }
/**
* Buscar todos os parceiros com cache
* @returns Array de todos os parceiros
*/
async getAllPartners(): Promise<PartnerDto[]> { async getAllPartners(): Promise<PartnerDto[]> {
const cacheKey = 'parceiros:all'; const cacheKey = 'parceiros:all';
@@ -105,23 +98,21 @@ export class PartnersService {
ORDER BY ESTPARCEIRO.NOME`; ORDER BY ESTPARCEIRO.NOME`;
const partners = await queryRunner.manager.query(sql); const partners = await queryRunner.manager.query(sql);
return partners.map(partner => new PartnerDto({ return partners.map(
(partner) =>
new PartnerDto({
id: partner.id, id: partner.id,
cpf: partner.cpf, cpf: partner.cpf,
nome: partner.name nome: partner.name,
})); }),
);
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
} }
} },
); );
} }
/**
* Buscar parceiro por ID específico com cache
* @param partnerId - ID do parceiro
* @returns Parceiro encontrado ou null
*/
async getPartnerById(partnerId: string): Promise<PartnerDto | null> { async getPartnerById(partnerId: string): Promise<PartnerDto | null> {
const cacheKey = `parceiros:id:${partnerId}`; const cacheKey = `parceiros:id:${partnerId}`;
@@ -142,27 +133,17 @@ export class PartnersService {
WHERE ESTPARCEIRO.ID = '${partnerId}'`; WHERE ESTPARCEIRO.ID = '${partnerId}'`;
const partners = await queryRunner.manager.query(sql); const partners = await queryRunner.manager.query(sql);
return partners.length > 0 ? new PartnerDto({ return partners.length > 0
? new PartnerDto({
id: partners[0].id, id: partners[0].id,
cpf: partners[0].cpf, cpf: partners[0].cpf,
nome: partners[0].name nome: partners[0].name,
}) : null; })
: null;
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
} }
} },
); );
} }
/**
* Limpar cache de parceiros (útil para invalidação)
* @param pattern - Padrão de chaves para limpar (opcional)
*/
async clearPartnersCache(pattern?: string) {
const cachePattern = pattern || 'parceiros:*';
// Nota: Esta funcionalidade requer implementação específica do Redis
// Por enquanto, mantemos a interface para futuras implementações
console.log(`Cache de parceiros seria limpo para o padrão: ${cachePattern}`);
}
} }

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