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 { DataConsultModule } from './data-consult/data-consult.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 { DebModule } from './orders/modules/deb.module';
import { LogisticController } from './logistic/logistic.controller';
import { LogisticService } from './logistic/logistic.service';
import { LoggerModule } from './Log/logger.module';
import jwtConfig from './auth/jwt.config';
import { UsersModule } from './auth/users/users.module';
import { ProductsModule } from './products/products.module';
import { ThrottlerModule, ThrottlerModuleOptions } from '@nestjs/throttler';
import { RateLimiterMiddleware } from './common/middlewares/rate-limiter.middleware';
import { RequestSanitizerMiddleware } from './common/middlewares/request-sanitizer.middleware';
import { HealthModule } from './health/health.module';
import { clientes } from './data-consult/clientes.module';
import { PartnersModule } from './partners/partners.module';
@Module({
imports: [
UsersModule,
ConfigModule.forRoot({ isGlobal: true,
load: [jwtConfig]
}),
ConfigModule.forRoot({ isGlobal: true, load: [jwtConfig] }),
TypeOrmModule.forRootAsync({
name: 'oracle',
inject: [ConfigService],
@@ -62,28 +53,19 @@ import { PartnersModule } from './partners/partners.module';
OrdersModule,
clientes,
ProductsModule,
NegotiationsModule,
OccurrencesModule,
ReasonTableModule,
LoggerModule,
DataConsultModule,
AuthModule,
DebModule,
OrdersModule,
HealthModule,
PartnersModule,
],
controllers: [OcorrencesController, LogisticController ],
providers: [ LogisticService,],
controllers: [LogisticController],
providers: [LogisticService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(RequestSanitizerMiddleware)
.forRoutes('*');
consumer.apply(RequestSanitizerMiddleware).forRoutes('*');
consumer
.apply(RateLimiterMiddleware)
.forRoutes('auth', 'users');
consumer.apply(RateLimiterMiddleware).forRoutes('auth', 'users');
}
}

View File

@@ -46,7 +46,9 @@ export interface AuthServiceTestContext {
mockUserRepository: ReturnType<typeof createMockUserRepository>;
mockTokenBlacklistService: ReturnType<typeof createMockTokenBlacklistService>;
mockRefreshTokenService: ReturnType<typeof createMockRefreshTokenService>;
mockSessionManagementService: ReturnType<typeof createMockSessionManagementService>;
mockSessionManagementService: ReturnType<
typeof createMockSessionManagementService
>;
}
export async function createAuthServiceTestModule(): Promise<AuthServiceTestContext> {
@@ -101,4 +103,3 @@ export async function createAuthServiceTestModule(): Promise<AuthServiceTestCont
mockSessionManagementService,
};
}

View File

@@ -29,7 +29,7 @@ describe('AuthService - createToken', () => {
username,
email,
storeId,
sessionId
sessionId,
);
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
@@ -41,7 +41,7 @@ describe('AuthService - createToken', () => {
email: email,
sessionId: sessionId,
},
{ expiresIn: '8h' }
{ expiresIn: '8h' },
);
expect(result).toBe(mockToken);
});
@@ -61,7 +61,7 @@ describe('AuthService - createToken', () => {
sellerId,
username,
email,
storeId
storeId,
);
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
@@ -73,7 +73,7 @@ describe('AuthService - createToken', () => {
email: email,
sessionId: undefined,
},
{ expiresIn: '8h' }
{ expiresIn: '8h' },
);
expect(result).toBe(mockToken);
});
@@ -93,12 +93,12 @@ describe('AuthService - createToken', () => {
sellerId,
username,
email,
storeId
storeId,
);
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
expect.any(Object),
{ expiresIn: '8h' }
{ expiresIn: '8h' },
);
});
@@ -119,7 +119,7 @@ describe('AuthService - createToken', () => {
username,
email,
storeId,
sessionId
sessionId,
);
const signCall = context.mockJwtService.sign.mock.calls[0];
@@ -150,7 +150,7 @@ describe('AuthService - createToken', () => {
username,
email,
storeId,
sessionId
sessionId,
);
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
@@ -162,7 +162,7 @@ describe('AuthService - createToken', () => {
email: email,
sessionId: sessionId,
},
{ expiresIn: '8h' }
{ expiresIn: '8h' },
);
expect(result).toBe(mockToken);
});
@@ -171,7 +171,13 @@ describe('AuthService - createToken', () => {
const mockToken = 'mock.jwt.token.once';
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);
});
@@ -199,7 +205,13 @@ describe('AuthService - createToken', () => {
const negativeId = -1;
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');
});
@@ -207,7 +219,13 @@ describe('AuthService - createToken', () => {
const zeroId = 0;
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');
});
@@ -215,7 +233,13 @@ describe('AuthService - createToken', () => {
const negativeSellerId = -1;
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');
});
@@ -223,7 +247,13 @@ describe('AuthService - createToken', () => {
const emptyUsername = '';
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');
});
@@ -231,7 +261,13 @@ describe('AuthService - createToken', () => {
const whitespaceUsername = ' ';
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');
});
@@ -239,7 +275,7 @@ describe('AuthService - createToken', () => {
const emptyEmail = '';
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');
});
@@ -247,7 +283,7 @@ describe('AuthService - createToken', () => {
const invalidEmail = 'not-an-email';
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');
});
@@ -255,7 +291,7 @@ describe('AuthService - createToken', () => {
const invalidEmail = 'testemail.com';
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');
});
@@ -263,19 +299,37 @@ describe('AuthService - createToken', () => {
const emptyStoreId = '';
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');
});
it('should reject null username', async () => {
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');
});
it('should reject undefined email', async () => {
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');
});
@@ -283,7 +337,13 @@ describe('AuthService - createToken', () => {
const specialCharsOnly = '@#$%';
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');
});
@@ -291,7 +351,13 @@ describe('AuthService - createToken', () => {
const longUsername = 'a'.repeat(10000);
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');
});
@@ -299,7 +365,7 @@ describe('AuthService - createToken', () => {
const longEmail = 'a'.repeat(10000) + '@test.com';
await expect(
context.service.createToken(1, 100, 'test', longEmail, 'STORE001')
context.service.createToken(1, 100, 'test', longEmail, 'STORE001'),
).rejects.toThrow('Email muito longo');
});
@@ -307,7 +373,13 @@ describe('AuthService - createToken', () => {
const sqlInjection = "admin'; DROP TABLE users; --";
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');
});
@@ -315,9 +387,8 @@ describe('AuthService - createToken', () => {
const invalidEmail = 'test@@example.com';
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');
});
});
});

View File

@@ -25,7 +25,9 @@ describe('AuthService - createTokenPair', () => {
beforeEach(() => {
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 () => {
@@ -39,10 +41,19 @@ describe('AuthService - createTokenPair', () => {
});
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();
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled();
expect(
context.mockRefreshTokenService.generateRefreshToken,
).not.toHaveBeenCalled();
});
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.
*/
context.mockRefreshTokenService.generateRefreshToken.mockRejectedValueOnce(
new Error('Falha ao gerar refresh token')
new Error('Falha ao gerar refresh token'),
);
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');
});
@@ -69,7 +87,13 @@ describe('AuthService - createTokenPair', () => {
context.mockJwtService.sign.mockReturnValue('');
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');
});
@@ -79,18 +103,34 @@ describe('AuthService - createTokenPair', () => {
* Problema: Método não valida o retorno.
* Solução esperada: Lançar exceção se token for inválido.
*/
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue('');
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
'',
);
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');
});
it('should validate that refresh token is not null', async () => {
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(null);
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
null,
);
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');
});
@@ -107,12 +147,20 @@ describe('AuthService - createTokenPair', () => {
return 'mock.access.token';
});
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(async () => {
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(
async () => {
callOrder.push('refreshToken');
return 'mock.refresh.token';
});
},
);
await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001');
await context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
);
expect(callOrder).toEqual(['accessToken', 'refreshToken']);
});
@@ -123,7 +171,13 @@ describe('AuthService - createTokenPair', () => {
* Problema: Cliente pode não saber quando renovar o token.
* 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(typeof result.expiresIn).toBe('number');
@@ -145,19 +199,42 @@ describe('AuthService - createTokenPair', () => {
return `mock.access.token.${callCount}`;
});
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(async () => {
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(
async () => {
return `mock.refresh.token.${Math.random()}`;
});
},
);
const promises = [
context.service.createTokenPair(1, 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'),
context.service.createTokenPair(
1,
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 uniqueTokens = new Set(results.map(r => r.accessToken));
const uniqueTokens = new Set(results.map((r) => r.accessToken));
expect(uniqueTokens.size).toBe(3);
});
@@ -168,10 +245,18 @@ describe('AuthService - createTokenPair', () => {
* Solução esperada: Falhar rápido com mensagem clara.
*/
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');
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled();
expect(
context.mockRefreshTokenService.generateRefreshToken,
).not.toHaveBeenCalled();
});
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.
*/
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');
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 () => {
@@ -194,11 +287,19 @@ describe('AuthService - createTokenPair', () => {
* Problema: Pode causar problemas ao gerar tokens sem session.
* 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.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 () => {
@@ -207,7 +308,13 @@ describe('AuthService - createTokenPair', () => {
* Problema: Pode faltar campos ou ter campos extras.
* 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('refreshToken');
@@ -216,4 +323,3 @@ describe('AuthService - createTokenPair', () => {
});
});
});

View File

@@ -13,8 +13,12 @@ describe('AuthService - logout', () => {
storeId: 'STORE001',
sessionId: 'session-123',
});
context.mockTokenBlacklistService.addToBlacklist.mockResolvedValue(undefined);
context.mockSessionManagementService.terminateSession.mockResolvedValue(undefined);
context.mockTokenBlacklistService.addToBlacklist.mockResolvedValue(
undefined,
);
context.mockSessionManagementService.terminateSession.mockResolvedValue(
undefined,
);
});
afterEach(() => {
@@ -37,66 +41,76 @@ describe('AuthService - logout', () => {
*/
it('should reject empty token', async () => {
await expect(
context.service.logout('')
).rejects.toThrow('Token não pode estar vazio');
await expect(context.service.logout('')).rejects.toThrow(
'Token não pode estar vazio',
);
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
expect(
context.mockTokenBlacklistService.addToBlacklist,
).not.toHaveBeenCalled();
});
it('should reject null token', async () => {
await expect(
context.service.logout(null as any)
).rejects.toThrow('Token não pode estar vazio');
await expect(context.service.logout(null as any)).rejects.toThrow(
'Token não pode estar vazio',
);
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
expect(
context.mockTokenBlacklistService.addToBlacklist,
).not.toHaveBeenCalled();
});
it('should reject undefined token', async () => {
await expect(
context.service.logout(undefined as any)
).rejects.toThrow('Token não pode estar vazio');
await expect(context.service.logout(undefined as any)).rejects.toThrow(
'Token não pode estar vazio',
);
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 () => {
await expect(
context.service.logout(' ')
).rejects.toThrow('Token não pode estar vazio');
await expect(context.service.logout(' ')).rejects.toThrow(
'Token não pode estar vazio',
);
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 () => {
const hugeToken = 'a'.repeat(100000);
await expect(
context.service.logout(hugeToken)
).rejects.toThrow('Token muito longo');
await expect(context.service.logout(hugeToken)).rejects.toThrow(
'Token muito longo',
);
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 () => {
context.mockJwtService.decode.mockReturnValue(null);
await expect(
context.service.logout('invalid.token')
).rejects.toThrow('Token inválido ou não pode ser decodificado');
await expect(context.service.logout('invalid.token')).rejects.toThrow(
'Token inválido ou não pode ser decodificado',
);
});
it('should validate decoded token has required fields', async () => {
context.mockJwtService.decode.mockReturnValue({} as any);
await expect(
context.service.logout('incomplete.token')
).rejects.toThrow('Token inválido ou não pode ser decodificado');
await expect(context.service.logout('incomplete.token')).rejects.toThrow(
'Token inválido ou não pode ser decodificado',
);
});
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');
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
expect(
context.mockTokenBlacklistService.addToBlacklist,
).not.toHaveBeenCalled();
});
it('should validate session exists before terminating', async () => {
@@ -114,11 +130,11 @@ describe('AuthService - logout', () => {
} as any);
context.mockSessionManagementService.terminateSession.mockRejectedValue(
new Error('Sessão não encontrada')
new Error('Sessão não encontrada'),
);
await expect(
context.service.logout('token.with.invalid.session')
context.service.logout('token.with.invalid.session'),
).rejects.toThrow('Sessão não encontrada');
});
@@ -128,16 +144,16 @@ describe('AuthService - logout', () => {
});
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');
});
it('should sanitize token input', async () => {
const maliciousToken = "'; DROP TABLE users; --";
await expect(
context.service.logout(maliciousToken)
).rejects.toThrow('Formato de token inválido');
await expect(context.service.logout(maliciousToken)).rejects.toThrow(
'Formato de token inválido',
);
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
});
@@ -149,7 +165,7 @@ describe('AuthService - logout', () => {
} as any);
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');
});
@@ -161,7 +177,9 @@ describe('AuthService - logout', () => {
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 () => {
@@ -172,23 +190,27 @@ describe('AuthService - logout', () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
context.mockSessionManagementService.terminateSession.mockRejectedValue(
new Error('Falha ao terminar sessão')
new Error('Falha ao terminar sessão'),
);
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 () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true);
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');
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
expect(
context.mockTokenBlacklistService.addToBlacklist,
).not.toHaveBeenCalled();
});
it('should validate token format before decoding', async () => {
@@ -214,7 +236,9 @@ describe('AuthService - logout', () => {
await Promise.all(promises);
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledTimes(3);
expect(
context.mockTokenBlacklistService.addToBlacklist,
).toHaveBeenCalledTimes(3);
});
it('should validate decoded payload structure', async () => {
@@ -223,11 +247,15 @@ describe('AuthService - logout', () => {
} as any);
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');
expect(context.mockSessionManagementService.terminateSession).not.toHaveBeenCalled();
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
expect(
context.mockSessionManagementService.terminateSession,
).not.toHaveBeenCalled();
expect(
context.mockTokenBlacklistService.addToBlacklist,
).not.toHaveBeenCalled();
});
it('should ensure token is always blacklisted on success', async () => {
@@ -235,8 +263,12 @@ describe('AuthService - logout', () => {
await context.service.logout('valid.token');
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('valid.token');
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledTimes(1);
expect(
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 () => {
@@ -248,13 +280,17 @@ describe('AuthService - logout', () => {
*/
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
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');
expect(context.mockTokenBlacklistService.isBlacklisted).toHaveBeenCalledWith('token.with.race.condition');
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('token.with.race.condition');
expect(
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 () => {
@@ -265,15 +301,21 @@ describe('AuthService - logout', () => {
*/
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
new Error('Erro de conexão com Redis')
new Error('Erro de conexão com Redis'),
);
await expect(
context.service.logout('token.with.blacklist.error')
).rejects.toThrow('Falha ao adicionar token à blacklist: Erro de conexão com Redis');
context.service.logout('token.with.blacklist.error'),
).rejects.toThrow(
'Falha ao adicionar token à blacklist: Erro de conexão com Redis',
);
expect(context.mockTokenBlacklistService.isBlacklisted).toHaveBeenCalledWith('token.with.blacklist.error');
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('token.with.blacklist.error');
expect(
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 () => {
@@ -286,11 +328,14 @@ describe('AuthService - logout', () => {
await context.service.logout('valid.token');
const isBlacklistedCallOrder = context.mockTokenBlacklistService.isBlacklisted.mock.invocationCallOrder[0];
const addToBlacklistCallOrder = context.mockTokenBlacklistService.addToBlacklist.mock.invocationCallOrder[0];
const isBlacklistedCallOrder =
context.mockTokenBlacklistService.isBlacklisted.mock
.invocationCallOrder[0];
const addToBlacklistCallOrder =
context.mockTokenBlacklistService.addToBlacklist.mock
.invocationCallOrder[0];
expect(isBlacklistedCallOrder).toBeLessThan(addToBlacklistCallOrder);
});
});
});

View File

@@ -19,7 +19,9 @@ describe('AuthService - refreshAccessToken', () => {
situacao: 'A',
dataDesligamento: null,
});
context.mockSessionManagementService.isSessionActive.mockResolvedValue(true);
context.mockSessionManagementService.isSessionActive.mockResolvedValue(
true,
);
});
afterEach(() => {
@@ -40,35 +42,43 @@ describe('AuthService - refreshAccessToken', () => {
*/
it('should reject empty refresh token', async () => {
await expect(
context.service.refreshAccessToken('')
).rejects.toThrow('Refresh token não pode estar vazio');
await expect(context.service.refreshAccessToken('')).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 () => {
await expect(
context.service.refreshAccessToken(null as any)
context.service.refreshAccessToken(null as any),
).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 () => {
await expect(
context.service.refreshAccessToken(undefined as any)
context.service.refreshAccessToken(undefined as any),
).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 () => {
await expect(
context.service.refreshAccessToken(' ')
).rejects.toThrow('Refresh token não pode estar vazio');
await expect(context.service.refreshAccessToken(' ')).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 () => {
@@ -77,15 +87,17 @@ describe('AuthService - refreshAccessToken', () => {
} as any);
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do refresh token inválidos');
});
it('should validate tokenData is not null', async () => {
context.mockRefreshTokenService.validateRefreshToken.mockResolvedValue(null);
context.mockRefreshTokenService.validateRefreshToken.mockResolvedValue(
null,
);
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do refresh token inválidos');
});
@@ -101,7 +113,7 @@ describe('AuthService - refreshAccessToken', () => {
});
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do usuário incompletos');
});
@@ -117,7 +129,7 @@ describe('AuthService - refreshAccessToken', () => {
});
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do usuário incompletos');
});
@@ -133,7 +145,7 @@ describe('AuthService - refreshAccessToken', () => {
});
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do usuário incompletos');
});
@@ -141,7 +153,7 @@ describe('AuthService - refreshAccessToken', () => {
context.mockJwtService.sign.mockReturnValue('');
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Falha ao gerar novo token de acesso');
});
@@ -149,7 +161,7 @@ describe('AuthService - refreshAccessToken', () => {
context.mockJwtService.sign.mockReturnValue(null as any);
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Falha ao gerar novo token de acesso');
});
@@ -159,10 +171,12 @@ describe('AuthService - refreshAccessToken', () => {
sessionId: 'expired-session',
});
context.mockSessionManagementService.isSessionActive = jest.fn().mockResolvedValue(false);
context.mockSessionManagementService.isSessionActive = jest
.fn()
.mockResolvedValue(false);
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Sessão não está mais ativa');
});
@@ -178,7 +192,7 @@ describe('AuthService - refreshAccessToken', () => {
});
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('ID de vendedor inválido');
});
@@ -186,24 +200,30 @@ describe('AuthService - refreshAccessToken', () => {
const hugeToken = 'a'.repeat(100000);
await expect(
context.service.refreshAccessToken(hugeToken)
context.service.refreshAccessToken(hugeToken),
).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 () => {
const maliciousToken = "'; DROP TABLE users; --";
await expect(
context.service.refreshAccessToken(maliciousToken)
context.service.refreshAccessToken(maliciousToken),
).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 () => {
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('expiresIn');
@@ -213,7 +233,9 @@ describe('AuthService - refreshAccessToken', () => {
});
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).toBeGreaterThan(0);
@@ -231,7 +253,7 @@ describe('AuthService - refreshAccessToken', () => {
});
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow();
});
@@ -244,7 +266,7 @@ describe('AuthService - refreshAccessToken', () => {
const results = await Promise.all(promises);
results.forEach(result => {
results.forEach((result) => {
expect(result).toHaveProperty('accessToken');
expect(result.accessToken).toBeTruthy();
});
@@ -262,9 +284,8 @@ describe('AuthService - refreshAccessToken', () => {
});
await expect(
context.service.refreshAccessToken('valid.refresh.token')
context.service.refreshAccessToken('valid.refresh.token'),
).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 { RefreshTokenService } from '../services/refresh-token.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 { LoginAuditService } from '../services/login-audit.service';
import {
LoginAuditFiltersDto,
LoginAuditResponseDto,
LoginStatsDto,
LoginStatsFiltersDto
LoginStatsFiltersDto,
} from './dto/login-audit.dto';
import {
ApiTags,
@@ -66,7 +69,10 @@ export class AuthController {
})
@ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' })
@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 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
*/
const existingSession = await this.sessionManagementService.hasActiveSession(user.id);
const existingSession =
await this.sessionManagementService.hasActiveSession(user.id);
if (existingSession) {
/**
* 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(
@@ -161,7 +171,6 @@ export class AuthController {
);
}
@Post('logout')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@@ -173,7 +182,12 @@ export class AuthController {
if (!token) {
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,
);
}
@@ -192,8 +206,12 @@ export class AuthController {
description: 'Token renovado com sucesso',
type: RefreshTokenResponseDto,
})
@ApiUnauthorizedResponse({ description: 'Refresh token inválido ou expirado' })
async refreshToken(@Body() dto: RefreshTokenDto): Promise<RefreshTokenResponseDto> {
@ApiUnauthorizedResponse({
description: 'Refresh token inválido ou expirado',
})
async refreshToken(
@Body() dto: RefreshTokenDto,
): Promise<RefreshTokenResponseDto> {
const result = await this.authService.refreshAccessToken(dto.refreshToken);
return result;
}
@@ -210,15 +228,20 @@ export class AuthController {
async getSessions(@Request() req): Promise<SessionsResponseDto> {
const userId = req.user.id;
const currentSessionId = req.user.sessionId;
const sessions = await this.sessionManagementService.getActiveSessions(userId, currentSessionId);
const sessions = await this.sessionManagementService.getActiveSessions(
userId,
currentSessionId,
);
return {
sessions: sessions.map(session => ({
sessions: sessions.map((session) => ({
sessionId: session.sessionId,
ipAddress: session.ipAddress,
userAgent: session.userAgent,
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,
})),
total: sessions.length,
@@ -284,7 +307,7 @@ export class AuthController {
const logs = await this.loginAuditService.getLoginLogs(auditFilters);
return {
logs: logs.map(log => ({
logs: logs.map((log) => ({
...log,
timestamp: DateUtil.toBrazilISOString(log.timestamp),
})),
@@ -333,13 +356,12 @@ export class AuthController {
ipAddress: { type: 'string' },
userAgent: { type: 'string' },
createdAt: { type: 'string' },
lastActivity: { type: 'string' }
}
}
}
}
lastActivity: { type: 'string' },
},
},
},
},
})
@Get('session/status')
async checkSessionStatus(@Query('username') username: string): Promise<{
hasActiveSession: boolean;
@@ -353,7 +375,12 @@ export class AuthController {
}> {
if (!username) {
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,
);
}
@@ -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) {
return {
@@ -383,8 +412,12 @@ export class AuthController {
sessionId: activeSession.sessionId,
ipAddress: activeSession.ipAddress,
userAgent: activeSession.userAgent,
createdAt: DateUtil.toBrazilISOString(new Date(activeSession.createdAt)),
lastActivity: DateUtil.toBrazilISOString(new Date(activeSession.lastActivity)),
createdAt: DateUtil.toBrazilISOString(
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,
SessionManagementService,
LoginAuditService,
AuthenticateUserHandler
AuthenticateUserHandler,
],
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 { UsersService } from '../users/users.service';
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 { SessionManagementService } from '../services/session-management.service';
@Injectable()
export class AuthService {
constructor(
@@ -23,7 +26,14 @@ export class AuthService {
* Cria um token JWT com validação de todos os parâmetros de entrada
* @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);
const user: JwtPayload = {
@@ -42,7 +52,13 @@ export class AuthService {
* Valida os parâmetros de entrada para criação de token
* @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) {
throw new BadRequestException('ID de usuário inválido');
}
@@ -64,7 +80,9 @@ export class AuthService {
}
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()) {
@@ -92,16 +110,41 @@ export class AuthService {
* @throws BadRequestException quando os parâmetros 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) {
const accessToken = await this.createToken(id, sellerId, username, email, storeId, sessionId);
async createTokenPair(
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');
}
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');
}
@@ -121,7 +164,9 @@ export class AuthService {
async refreshAccessToken(refreshToken: string) {
this.validateRefreshTokenInput(refreshToken);
const tokenData = await this.refreshTokenService.validateRefreshToken(refreshToken);
const tokenData = await this.refreshTokenService.validateRefreshToken(
refreshToken,
);
if (!tokenData || !tokenData.id) {
throw new BadRequestException('Dados do refresh token inválidos');
@@ -135,9 +180,10 @@ export class AuthService {
this.validateUserDataForToken(user);
if (tokenData.sessionId) {
const isSessionActive = await this.sessionManagementService.isSessionActive(
const isSessionActive =
await this.sessionManagementService.isSessionActive(
user.id,
tokenData.sessionId
tokenData.sessionId,
);
if (!isSessionActive) {
throw new UnauthorizedException('Sessão não está mais ativa');
@@ -150,10 +196,14 @@ export class AuthService {
user.name,
user.email,
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');
}
@@ -168,7 +218,11 @@ export class AuthService {
* @private
*/
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');
}
@@ -187,18 +241,32 @@ export class AuthService {
*/
private validateUserDataForToken(user: any): void {
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()) {
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()) {
throw new BadRequestException('Dados do usuário incompletos: storeId não encontrado');
if (
!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');
}
}
@@ -228,11 +296,15 @@ export class AuthService {
try {
decoded = this.jwtService.decode(token) as JwtPayload;
} 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) {
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) {
@@ -241,25 +313,34 @@ export class AuthService {
if (decoded.sessionId && decoded.id && decoded.sessionId.trim()) {
try {
await this.sessionManagementService.terminateSession(decoded.id, decoded.sessionId);
await this.sessionManagementService.terminateSession(
decoded.id,
decoded.sessionId,
);
} 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')) {
throw new Error('Sessão não encontrada');
}
}
}
const isAlreadyBlacklisted = await this.tokenBlacklistService.isBlacklisted(token);
const isAlreadyBlacklisted = await this.tokenBlacklistService.isBlacklisted(
token,
);
if (!isAlreadyBlacklisted) {
try {
await this.tokenBlacklistService.addToBlacklist(token);
} 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')) {
return;
}
throw new Error(`Falha ao adicionar token à blacklist: ${errorMessage}`);
throw new Error(
`Falha ao adicionar token à blacklist: ${errorMessage}`,
);
}
}
}

View File

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

View File

@@ -7,13 +7,18 @@ import { UserModel } from 'src/core/models/user.model';
@CommandHandler(AuthenticateUserCommand)
@Injectable()
export class AuthenticateUserHandler implements ICommandHandler<AuthenticateUserCommand> {
export class AuthenticateUserHandler
implements ICommandHandler<AuthenticateUserCommand>
{
constructor(private readonly userRepository: UserRepository) {}
async execute(command: AuthenticateUserCommand): Promise<Result<UserModel>> {
const { username, password } = command;
const user = await this.userRepository.findByUsernameAndPassword(username, password);
const user = await this.userRepository.findByUsernameAndPassword(
username,
password,
);
if (!user) {
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.ok(user);
}
}

View File

@@ -1,5 +1,13 @@
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';
export class LoginAuditFiltersDto {
@@ -19,7 +27,10 @@ export class LoginAuditFiltersDto {
@IsString()
ipAddress?: string;
@ApiProperty({ description: 'Filtrar apenas logins bem-sucedidos', required: false })
@ApiProperty({
description: 'Filtrar apenas logins bem-sucedidos',
required: false,
})
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
@@ -35,7 +46,12 @@ export class LoginAuditFiltersDto {
@IsDateString()
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()
@IsNumber()
@Type(() => Number)
@@ -43,7 +59,11 @@ export class LoginAuditFiltersDto {
@Max(1000)
limit?: number;
@ApiProperty({ description: 'Offset para paginação', required: false, minimum: 0 })
@ApiProperty({
description: 'Offset para paginação',
required: false,
minimum: 0,
})
@IsOptional()
@IsNumber()
@Type(() => Number)
@@ -84,7 +104,10 @@ export class LoginAuditLogDto {
}
export class LoginAuditResponseDto {
@ApiProperty({ description: 'Lista de logs de login', type: [LoginAuditLogDto] })
@ApiProperty({
description: 'Lista de logs de login',
type: [LoginAuditLogDto],
})
logs: LoginAuditLogDto[];
@ApiProperty({ description: 'Total de registros encontrados' })
@@ -123,13 +146,21 @@ export class LoginStatsDto {
}
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()
@IsNumber()
@Type(() => 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()
@IsNumber()
@Type(() => Number)

View File

@@ -196,7 +196,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
mockGetRequest.mockReturnValue(request);
mockRateLimitingService.isAllowed.mockRejectedValue(
new Error('Erro de conexão com Redis')
new Error('Erro de conexão com Redis'),
);
try {
@@ -225,7 +225,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
mockGetRequest.mockReturnValue(request);
mockRateLimitingService.isAllowed.mockResolvedValue(false);
mockRateLimitingService.getAttemptInfo.mockRejectedValue(
new Error('Erro ao buscar informações')
new Error('Erro ao buscar informações'),
);
try {
@@ -336,7 +336,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
const result = await guard.canActivate(mockExecutionContext);
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 () => {
@@ -363,7 +365,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
const results = await Promise.all(promises);
results.forEach(result => {
results.forEach((result) => {
expect(result).toBe(true);
});
});
@@ -394,7 +396,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
fail('Deveria ter lançado exceção');
} catch (error) {
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');
} catch (error) {
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);
}
});
@@ -512,7 +518,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
const result = await guard.canActivate(mockExecutionContext);
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 () => {
@@ -556,7 +564,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
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 () => {
@@ -572,7 +582,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
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 () => {
@@ -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';
@Injectable()
@@ -19,7 +25,8 @@ export class RateLimitingGuard implements CanActivate {
try {
isAllowed = await this.rateLimitingService.isAllowed(ip);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new HttpException(
{
success: false,
@@ -36,7 +43,8 @@ export class RateLimitingGuard implements CanActivate {
try {
attemptInfo = await this.rateLimitingService.getAttemptInfo(ip);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new HttpException(
{
success: false,
@@ -53,7 +61,8 @@ export class RateLimitingGuard implements CanActivate {
throw new HttpException(
{
success: false,
error: 'Muitas tentativas de login. Tente novamente em alguns minutos.',
error:
'Muitas tentativas de login. Tente novamente em alguns minutos.',
data: null,
details: {
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
*/
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 connectionIp = request.connection?.remoteAddress;
const socketIp = request.socket?.remoteAddress;
const requestIp = request.ip;
const rawIp = forwardedFor || realIp || connectionIp || socketIp || requestIp;
const rawIp =
forwardedFor || realIp || connectionIp || socketIp || requestIp;
if (rawIp === null || rawIp === undefined) {
return '';
@@ -144,7 +156,11 @@ export class RateLimitingGuard implements CanActivate {
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)) {
throw new HttpException(
{
@@ -166,7 +182,7 @@ export class RateLimitingGuard implements CanActivate {
const parts = ip.split('.');
if (parts.length !== 4) return false;
return parts.every(part => {
return parts.every((part) => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255;
});
@@ -184,13 +200,13 @@ export class RateLimitingGuard implements CanActivate {
const leftParts = parts[0] ? parts[0].split(':') : [];
const rightParts = parts[1] ? parts[1].split(':') : [];
return (leftParts.length + rightParts.length) <= 8;
return leftParts.length + rightParts.length <= 8;
}
const parts = ip.split(':');
if (parts.length !== 8) return false;
return parts.every(part => {
return parts.every((part) => {
if (!part) return false;
return /^[0-9a-fA-F]{1,4}$/.test(part);
});
@@ -223,8 +239,11 @@ export class RateLimitingGuard implements CanActivate {
);
}
if (attemptInfo.remainingTime !== undefined &&
(typeof attemptInfo.remainingTime !== 'number' || attemptInfo.remainingTime < 0)) {
if (
attemptInfo.remainingTime !== undefined &&
(typeof attemptInfo.remainingTime !== 'number' ||
attemptInfo.remainingTime < 0)
) {
throw new HttpException(
{
success: false,

View File

@@ -13,4 +13,3 @@ export class Result<T> {
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_EXPIRY = 30 * 24 * 60 * 60;
constructor(
@Inject('REDIS_CLIENT') private readonly redis: Redis,
) {}
constructor(@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 timestamp = DateUtil.now();
@@ -69,7 +69,9 @@ export class LoginAuditService {
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 logs: LoginAuditLog[] = [];
@@ -102,13 +104,21 @@ export class LoginAuditService {
return logs.slice(offset, offset + limit);
}
async getLoginStats(userId?: number, days: number = 7): Promise<{
async getLoginStats(
userId?: number,
days: number = 7,
): Promise<{
totalAttempts: number;
successfulLogins: number;
failedLogins: number;
uniqueIps: 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 startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000);
@@ -127,15 +137,20 @@ export class LoginAuditService {
const stats = {
totalAttempts: logs.length,
successfulLogins: logs.filter(log => log.success).length,
failedLogins: logs.filter(log => !log.success).length,
uniqueIps: new Set(logs.map(log => log.ipAddress)).size,
successfulLogins: logs.filter((log) => log.success).length,
failedLogins: logs.filter((log) => !log.success).length,
uniqueIps: new Set(logs.map((log) => log.ipAddress)).size,
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>();
logs.forEach(log => {
logs.forEach((log) => {
ipCounts.set(log.ipAddress, (ipCounts.get(log.ipAddress) || 0) + 1);
});
@@ -144,10 +159,17 @@ export class LoginAuditService {
.sort((a, b) => b.count - a.count)
.slice(0, 10);
const dailyCounts = new Map<string, { attempts: number; successes: number; failures: number }>();
logs.forEach(log => {
const dailyCounts = new Map<
string,
{ attempts: number; successes: number; failures: number }
>();
logs.forEach((log) => {
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++;
if (log.success) {
@@ -168,7 +190,9 @@ export class LoginAuditService {
}
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 oldDates = this.getDateRange(new Date('2020-01-01'), cutoffDate);
@@ -190,7 +214,9 @@ export class LoginAuditService {
}
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 dates = this.getDateRange(startDate, endDate);
@@ -210,7 +236,9 @@ export class LoginAuditService {
}
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 {
@@ -233,8 +261,14 @@ export class LoginAuditService {
return `${this.LOG_PREFIX}:date:${date}`;
}
private matchesFilters(log: LoginAuditLog, filters: LoginAuditFilters): boolean {
if (filters.username && !log.username.toLowerCase().includes(filters.username.toLowerCase())) {
private matchesFilters(
log: LoginAuditLog,
filters: LoginAuditFilters,
): boolean {
if (
filters.username &&
!log.username.toLowerCase().includes(filters.username.toLowerCase())
) {
return false;
}

View File

@@ -16,11 +16,12 @@ export class RateLimitingService {
blockDurationMs: 1 * 60 * 1000,
};
constructor(
@Inject(RedisClientToken) private readonly redis: IRedisClient,
) {}
constructor(@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 key = this.buildAttemptKey(ip);
const blockKey = this.buildBlockKey(ip);
@@ -51,21 +52,25 @@ export class RateLimitingService {
return {attempts, 0}
`;
const result = await this.redis.eval(
const result = (await this.redis.eval(
luaScript,
2,
key,
blockKey,
finalConfig.maxAttempts,
finalConfig.windowMs,
finalConfig.blockDurationMs
) as [number, number];
finalConfig.blockDurationMs,
)) as [number, number];
const [attempts, isBlockedResult] = result;
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 key = this.buildAttemptKey(ip);
const blockKey = this.buildBlockKey(ip);

View File

@@ -24,18 +24,21 @@ export class RefreshTokenService {
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 refreshToken = this.jwtService.sign(
{ userId, tokenId, sessionId, type: 'refresh' },
{ expiresIn: '7d' }
{ expiresIn: '7d' },
);
const tokenData: RefreshTokenData = {
userId,
tokenId,
sessionId,
expiresAt: DateUtil.nowTimestamp() + (this.REFRESH_TOKEN_TTL * 1000),
expiresAt: DateUtil.nowTimestamp() + this.REFRESH_TOKEN_TTL * 1000,
createdAt: DateUtil.nowTimestamp(),
};
@@ -75,7 +78,7 @@ export class RefreshTokenService {
username: '',
email: '',
sessionId: sessionId || tokenData.sessionId,
tokenId
tokenId,
} as JwtPayload;
} catch (error) {
throw new UnauthorizedException('Refresh token inválido');
@@ -118,7 +121,7 @@ export class RefreshTokenService {
if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) {
const tokensToRemove = activeTokens
.slice(this.MAX_REFRESH_TOKENS_PER_USER)
.map(token => token.tokenId);
.map((token) => token.tokenId);
for (const tokenId of tokensToRemove) {
await this.revokeRefreshToken(userId, tokenId);

View File

@@ -19,11 +19,13 @@ export class SessionManagementService {
private readonly SESSION_TTL = 8 * 60 * 60;
private readonly MAX_SESSIONS_PER_USER = 1;
constructor(
@Inject(RedisClientToken) private readonly redis: IRedisClient,
) {}
constructor(@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 now = DateUtil.nowTimestamp();
@@ -45,7 +47,10 @@ export class SessionManagementService {
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 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 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 keys = await this.redis.keys(pattern);
@@ -130,7 +141,7 @@ export class SessionManagementService {
if (activeSessions.length > this.MAX_SESSIONS_PER_USER) {
const sessionsToRemove = activeSessions
.slice(this.MAX_SESSIONS_PER_USER)
.map(session => session.sessionId);
.map((session) => session.sessionId);
for (const sessionId of sessionsToRemove) {
await this.terminateSession(userId, sessionId);

View File

@@ -59,12 +59,16 @@ export class TokenBlacklistService {
private calculateTokenTTL(payload: JwtPayload): number {
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);
}
private hashToken(token: string): string {
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 ', '');
if (token && await this.tokenBlacklistService.isBlacklisted(token)) {
if (token && (await this.tokenBlacklistService.isBlacklisted(token))) {
throw new UnauthorizedException('Token foi invalidado');
}
@@ -39,9 +39,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
const cachedUser = await this.redis.get<any>(sessionKey);
if (cachedUser) {
const isSessionActive = await this.sessionManagementService.isSessionActive(
const isSessionActive =
await this.sessionManagementService.isSessionActive(
payload.id,
payload.sessionId
payload.sessionId,
);
if (!isSessionActive) {
@@ -65,7 +66,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}
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 = {

View File

@@ -19,7 +19,10 @@ export class ResetPasswordService {
if (!user) return null;
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);

View File

@@ -8,11 +8,8 @@ import { EmailService } from './email.service';
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
@Module({
imports: [
TypeOrmModule.forFeature([]),
],
imports: [TypeOrmModule.forFeature([])],
providers: [
UsersService,
UserRepository,

View File

@@ -4,8 +4,6 @@ import { ResetPasswordService } from './reset-password.service';
import { ChangePasswordService } from './change-password.service';
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
@Injectable()
export class UsersService {
constructor(
@@ -22,7 +20,15 @@ export class UsersService {
return this.resetPasswordService.execute(user.document, user.email);
}
async changePassword(user: { id: number; password: string; newPassword: string }) {
return this.changePasswordService.execute(user.id, user.password, user.newPassword);
async changePassword(user: {
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 {
private readonly ttl: 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) {
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);
this.setRateLimitHeaders(res, record.count);
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++;
@@ -52,13 +55,17 @@ export class RateLimiterMiddleware implements NestMiddleware {
private generateKey(req: Request): string {
// 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 || '';
return `${ip}:${path}`;
}
private setRateLimitHeaders(res: Response, count: number): void {
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) {
Object.keys(obj).forEach(key => {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
obj[key] = this.sanitizeString(obj[key]);
} else if (typeof obj[key] === 'object' && obj[key] !== null) {

View File

@@ -9,8 +9,13 @@ import {
import { ResultModel } from '../shared/ResultModel';
@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, ResultModel<T>> {
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<ResultModel<T>> {
export class ResponseInterceptor<T>
implements NestInterceptor<T, ResultModel<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<ResultModel<T>> {
return next.handle().pipe(
map((data) => {
return ResultModel.success(data);
@@ -18,4 +23,3 @@ import {
);
}
}

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
export function IsSanitized(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isSanitized',
target: object.constructor,
@@ -12,19 +16,22 @@ export function IsSanitized(validationOptions?: ValidationOptions) {
validate(value: any, args: ValidationArguments) {
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)) {
return false;
}
// 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)) {
return false;
}
// 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)) {
return false;
}
@@ -41,7 +48,7 @@ export function IsSanitized(validationOptions?: ValidationOptions) {
// Decorator para validar IDs seguros (evita injeção em IDs)
export function IsSecureId(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isSecureId',
target: object.constructor,
@@ -49,11 +56,14 @@ export function IsSecureId(validationOptions?: ValidationOptions) {
options: validationOptions,
validator: {
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') {
// 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

View File

@@ -5,6 +5,9 @@ export interface IRedisClient {
del(...keys: string[]): Promise<void>;
keys(pattern: string): Promise<string[]>;
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';
export const RedisClientToken = 'RedisClientInterface';

View File

@@ -6,7 +6,7 @@ import { IRedisClient } from './IRedisClient';
export class RedisClientAdapter implements IRedisClient {
constructor(
@Inject('REDIS_CLIENT')
private readonly redis: Redis
private readonly redis: Redis,
) {}
async get<T>(key: string): Promise<T | null> {
@@ -43,7 +43,11 @@ export class RedisClientAdapter implements IRedisClient {
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);
}
}

View File

@@ -2,8 +2,6 @@ import { DataSourceOptions } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import * as oracledb from 'oracledb';
oracledb.initOracleClient({ libDir: process.env.ORACLE_CLIENT_LIB_DIR });
// 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
export function createOracleConfig(config: ConfigService): DataSourceOptions {
const poolMin = parseInt(config.get('ORACLE_POOL_MIN', '5'));
const poolMax = parseInt(config.get('ORACLE_POOL_MAX', '20'));
const poolIncrement = parseInt(config.get('ORACLE_POOL_INCREMENT', '5'));
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 validPoolMax = Math.max(validPoolMin + 1, poolMax);
const validPoolIncrement = Math.max(1, poolIncrement);
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 = {

View File

@@ -5,9 +5,15 @@ export function createPostgresConfig(config: ConfigService): DataSourceOptions {
// Obter configurações de ambiente ou usar valores padrão
const poolMin = parseInt(config.get('POSTGRES_POOL_MIN', '5'));
const poolMax = parseInt(config.get('POSTGRES_POOL_MAX', '20'));
const idleTimeout = parseInt(config.get('POSTGRES_POOL_IDLE_TIMEOUT', '30000'));
const connectionTimeout = parseInt(config.get('POSTGRES_POOL_CONNECTION_TIMEOUT', '5000'));
const acquireTimeout = parseInt(config.get('POSTGRES_POOL_ACQUIRE_TIMEOUT', '60000'));
const idleTimeout = parseInt(
config.get('POSTGRES_POOL_IDLE_TIMEOUT', '30000'),
);
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
const validPoolMin = Math.max(1, poolMin);
@@ -25,7 +31,10 @@ export function createPostgresConfig(config: ConfigService): DataSourceOptions {
database: config.get('POSTGRES_DB'),
synchronize: config.get('NODE_ENV') === 'development',
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',
poolSize: validPoolMax, // máximo de conexões no pool
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 { DataConsultService } from '../data-consult.service';
import { DataConsultRepository } from '../data-consult.repository';
import { ILogger } from '../../Log/ILogger';
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
import { DataSource } from 'typeorm';
import { DATA_SOURCE } from '../../core/constants';
export const createMockRepository = (methods: Partial<DataConsultRepository> = {}) => ({
export const createMockRepository = (
methods: Partial<DataConsultRepository> = {},
) =>
({
findStores: jest.fn(),
findSellers: jest.fn(),
findBillings: jest.fn(),
@@ -18,14 +20,8 @@ export const createMockRepository = (methods: Partial<DataConsultRepository> = {
...methods,
} as any);
export const createMockLogger = () => ({
log: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
} as any);
export const createMockRedisClient = () => ({
export const createMockRedisClient = () =>
({
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(undefined),
} as any);
@@ -33,18 +29,19 @@ export const createMockRedisClient = () => ({
export interface DataConsultServiceTestContext {
service: DataConsultService;
mockRepository: jest.Mocked<DataConsultRepository>;
mockLogger: jest.Mocked<ILogger>;
mockRedisClient: jest.Mocked<IRedisClient>;
mockDataSource: jest.Mocked<DataSource>;
}
export async function createDataConsultServiceTestModule(
repositoryMethods: Partial<DataConsultRepository> = {},
redisClientMethods: Partial<IRedisClient> = {}
redisClientMethods: Partial<IRedisClient> = {},
): Promise<DataConsultServiceTestContext> {
const mockRepository = createMockRepository(repositoryMethods);
const mockLogger = createMockLogger();
const mockRedisClient = { ...createMockRedisClient(), ...redisClientMethods } as any;
const mockRedisClient = {
...createMockRedisClient(),
...redisClientMethods,
} as any;
const mockDataSource = {} as any;
const module: TestingModule = await Test.createTestingModule({
@@ -58,10 +55,6 @@ export async function createDataConsultServiceTestModule(
provide: RedisClientToken,
useValue: mockRedisClient,
},
{
provide: 'LoggerService',
useValue: mockLogger,
},
{
provide: DATA_SOURCE,
useValue: mockDataSource,
@@ -74,9 +67,7 @@ export async function createDataConsultServiceTestModule(
return {
service,
mockRepository,
mockLogger,
mockRedisClient,
mockDataSource,
};
}

View File

@@ -23,7 +23,7 @@ describe('DataConsultService', () => {
const result = await context.service.stores();
result.forEach(store => {
result.forEach((store) => {
expect(store.id).toBeDefined();
expect(store.name).toBeDefined();
expect(store.store).toBeDefined();
@@ -36,7 +36,10 @@ describe('DataConsultService', () => {
});
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();
expect(Array.isArray(result)).toBe(true);
});
@@ -49,7 +52,7 @@ describe('DataConsultService', () => {
] as any);
const result = await context.service.stores();
result.forEach(store => {
result.forEach((store) => {
expect(store.id).not.toBe('');
expect(store.name).not.toBe('');
expect(store.store).not.toBe('');
@@ -60,7 +63,10 @@ describe('DataConsultService', () => {
const repositoryError = new Error('Database connection failed');
context.mockRepository.findStores.mockRejectedValue(repositoryError);
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);
const result = await context.service.sellers();
result.forEach(seller => {
result.forEach((seller) => {
expect(seller.id).toBeDefined();
expect(seller.name).toBeDefined();
});
@@ -97,7 +103,10 @@ describe('DataConsultService', () => {
});
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();
expect(Array.isArray(result)).toBe(true);
});
@@ -109,7 +118,7 @@ describe('DataConsultService', () => {
] as any);
const result = await context.service.sellers();
result.forEach(seller => {
result.forEach((seller) => {
expect(seller.id).not.toBe('');
expect(seller.name).not.toBe('');
});
@@ -119,7 +128,10 @@ describe('DataConsultService', () => {
const repositoryError = new Error('Database connection failed');
context.mockRepository.findSellers.mockRejectedValue(repositoryError);
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);
const result = await context.service.billings();
result.forEach(billing => {
result.forEach((billing) => {
expect(billing.id).toBeDefined();
expect(billing.date).toBeDefined();
expect(billing.total).toBeDefined();
@@ -157,7 +169,11 @@ describe('DataConsultService', () => {
});
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();
expect(Array.isArray(result)).toBe(true);
});
@@ -170,7 +186,7 @@ describe('DataConsultService', () => {
] as any);
const result = await context.service.billings();
result.forEach(billing => {
result.forEach((billing) => {
expect(billing.id).not.toBe('');
expect(billing.date).toBeDefined();
expect(billing.total).toBeDefined();
@@ -181,7 +197,10 @@ describe('DataConsultService', () => {
const repositoryError = new Error('Database connection failed');
context.mockRepository.findBillings.mockRejectedValue(repositoryError);
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);
const result = await context.service.customers('test');
result.forEach(customer => {
result.forEach((customer) => {
expect(customer.id).toBeDefined();
expect(customer.name).toBeDefined();
expect(customer.document).toBeDefined();
@@ -219,7 +238,11 @@ describe('DataConsultService', () => {
});
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');
expect(Array.isArray(result)).toBe(true);
});
@@ -232,7 +255,7 @@ describe('DataConsultService', () => {
] as any);
const result = await context.service.customers('test');
result.forEach(customer => {
result.forEach((customer) => {
expect(customer.id).not.toBe('');
expect(customer.name).not.toBe('');
expect(customer.document).not.toBe('');
@@ -242,8 +265,13 @@ describe('DataConsultService', () => {
it('should log error when repository throws exception', async () => {
const repositoryError = new Error('Database connection failed');
context.mockRepository.findCustomers.mockRejectedValue(repositoryError);
await expect(context.service.customers('test')).rejects.toThrow(HttpException);
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar clientes', repositoryError);
await expect(context.service.customers('test')).rejects.toThrow(
HttpException,
);
expect(context.mockLogger.error).toHaveBeenCalledWith(
'Erro ao buscar clientes',
repositoryError,
);
});
});
});
@@ -268,7 +296,7 @@ describe('DataConsultService', () => {
] as any);
const result = await context.service.getAllProducts();
result.forEach(product => {
result.forEach((product) => {
expect(product.id).toBeDefined();
expect(product.name).toBeDefined();
expect(product.manufacturerCode).toBeDefined();
@@ -281,7 +309,11 @@ describe('DataConsultService', () => {
});
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();
expect(Array.isArray(result)).toBe(true);
});
@@ -294,7 +326,7 @@ describe('DataConsultService', () => {
] as any);
const result = await context.service.getAllProducts();
result.forEach(product => {
result.forEach((product) => {
expect(product.id).not.toBe('');
expect(product.name).not.toBe('');
expect(product.manufacturerCode).not.toBe('');
@@ -303,9 +335,16 @@ describe('DataConsultService', () => {
it('should log error when repository throws exception', async () => {
const repositoryError = new Error('Database connection failed');
context.mockRepository.findAllProducts.mockRejectedValue(repositoryError);
await expect(context.service.getAllProducts()).rejects.toThrow(HttpException);
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar todos os produtos', repositoryError);
context.mockRepository.findAllProducts.mockRejectedValue(
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 () => {
context.mockRepository.findAllCarriers.mockResolvedValue([
{ 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);
const result = await context.service.getAllCarriers();
result.forEach(carrier => {
result.forEach((carrier) => {
expect(carrier.carrierId).toBeDefined();
expect(carrier.carrierName).toBeDefined();
expect(carrier.carrierDescription).toBeDefined();
@@ -343,20 +389,36 @@ describe('DataConsultService', () => {
});
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();
expect(Array.isArray(result)).toBe(true);
});
it('should validate that required properties are not empty strings', async () => {
context.mockRepository.findAllCarriers.mockResolvedValue([
{ carrierId: '', carrierName: 'Transportadora 1', carrierDescription: '001 - Transportadora 1' },
{ carrierId: '002', carrierName: '', carrierDescription: '002 - Transportadora 2' },
{ carrierId: '003', carrierName: 'Transportadora 3', carrierDescription: '' },
{
carrierId: '',
carrierName: 'Transportadora 1',
carrierDescription: '001 - Transportadora 1',
},
{
carrierId: '002',
carrierName: '',
carrierDescription: '002 - Transportadora 2',
},
{
carrierId: '003',
carrierName: 'Transportadora 3',
carrierDescription: '',
},
] as any);
const result = await context.service.getAllCarriers();
result.forEach(carrier => {
result.forEach((carrier) => {
expect(carrier.carrierId).not.toBe('');
expect(carrier.carrierName).not.toBe('');
expect(carrier.carrierDescription).not.toBe('');
@@ -365,9 +427,16 @@ describe('DataConsultService', () => {
it('should log error when repository throws exception', async () => {
const repositoryError = new Error('Database connection failed');
context.mockRepository.findAllCarriers.mockRejectedValue(repositoryError);
await expect(context.service.getAllCarriers()).rejects.toThrow(HttpException);
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar transportadoras', repositoryError);
context.mockRepository.findAllCarriers.mockRejectedValue(
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);
const result = await context.service.getRegions();
result.forEach(region => {
result.forEach((region) => {
expect(region.numregiao).toBeDefined();
expect(region.regiao).toBeDefined();
});
@@ -404,7 +473,10 @@ describe('DataConsultService', () => {
});
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();
expect(Array.isArray(result)).toBe(true);
});
@@ -417,7 +489,7 @@ describe('DataConsultService', () => {
] as any);
const result = await context.service.getRegions();
result.forEach(region => {
result.forEach((region) => {
expect(region.numregiao).toBeDefined();
expect(region.numregiao).not.toBeNull();
expect(region.regiao).toBeDefined();
@@ -428,8 +500,13 @@ describe('DataConsultService', () => {
it('should log error when repository throws exception', async () => {
const repositoryError = new Error('Database connection failed');
context.mockRepository.findRegions.mockRejectedValue(repositoryError);
await expect(context.service.getRegions()).rejects.toThrow(HttpException);
expect(context.mockLogger.error).toHaveBeenCalledWith('Erro ao buscar regiões', repositoryError);
await expect(context.service.getRegions()).rejects.toThrow(
HttpException,
);
expect(context.mockLogger.error).toHaveBeenCalledWith(
'Erro ao buscar regiões',
repositoryError,
);
});
});
});

View File

@@ -1,21 +1,16 @@
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
import {
ApiTags,
} from '@nestjs/swagger';
import { Controller, Get, Param } from '@nestjs/common';
import { clientesService } from './clientes.service';
@ApiTags('clientes')
@Controller('api/v1/')
export class clientesController {
constructor(private readonly clientesService: clientesService) {}
@Get('clientes/:filter')
async customer(@Param('filter') filter: string) {
return this.clientesService.customers(filter);
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { clientesService } from './clientes.service';
import { clientesController } from './clientes.controller';

View File

@@ -63,7 +63,9 @@ export class clientesService {
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
,PCCLIENT.ESTCOB as "estcob"
FROM PCCLIENT
WHERE PCCLIENT.CLIENTE LIKE '${filter.toUpperCase().replace('@', '%')}%'
WHERE PCCLIENT.CLIENTE LIKE '${filter
.toUpperCase()
.replace('@', '%')}%'
ORDER BY PCCLIENT.CLIENTE`;
customers = await queryRunner.manager.query(sql);
}
@@ -72,7 +74,7 @@ export class clientesService {
} finally {
await queryRunner.release();
}
}
},
);
}
@@ -103,7 +105,7 @@ export class clientesService {
} finally {
await queryRunner.release();
}
}
},
);
}
@@ -136,19 +138,13 @@ export class clientesService {
} finally {
await queryRunner.release();
}
}
},
);
}
/**
* Limpar cache de clientes (útil para invalidação)
* @param pattern - Padrão de chaves para limpar (opcional)
*/
async clearCustomersCache(pattern?: string) {
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 { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
import {
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 { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { ProductDto } from './dto/product.dto';
@@ -19,7 +34,11 @@ export class DataConsultController {
@ApiBearerAuth()
@Get('stores')
@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[]> {
return this.dataConsultService.stores();
}
@@ -28,14 +47,24 @@ export class DataConsultController {
@ApiBearerAuth()
@Get('sellers')
@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[]> {
return this.dataConsultService.sellers();
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('billings')
@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[]> {
return this.dataConsultService.billings();
}
@@ -45,7 +74,11 @@ export class DataConsultController {
@Get('customers/:filter')
@ApiOperation({ summary: 'Filtra clientes pelo parâmetro fornecido' })
@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[]> {
return this.dataConsultService.customers(filter);
}
@@ -55,7 +88,11 @@ export class DataConsultController {
@Get('products/:filter')
@ApiOperation({ summary: 'Busca produtos filtrados' })
@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[]> {
return this.dataConsultService.products(filter);
}
@@ -64,7 +101,11 @@ export class DataConsultController {
@ApiBearerAuth()
@Get('all')
@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[]> {
return this.dataConsultService.getAllProducts();
}
@@ -73,7 +114,11 @@ export class DataConsultController {
@ApiBearerAuth()
@Get('carriers/all')
@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 }))
async getAllCarriers(): Promise<CarrierDto[]> {
return this.dataConsultService.getAllCarriers();
@@ -83,9 +128,15 @@ export class DataConsultController {
@ApiBearerAuth()
@Get('carriers')
@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 }))
async getCarriersByDate(@Query() query: FindCarriersDto): Promise<CarrierDto[]> {
async getCarriersByDate(
@Query() query: FindCarriersDto,
): Promise<CarrierDto[]> {
return this.dataConsultService.getCarriersByDate(query);
}
@@ -94,17 +145,26 @@ export class DataConsultController {
@Get('carriers/order/:orderId')
@ApiOperation({ summary: 'Busca transportadoras de um pedido específico' })
@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 }))
async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise<CarrierDto[]> {
async getOrderCarriers(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<CarrierDto[]> {
return this.dataConsultService.getOrderCarriers(orderId);
}
@Get('regions')
@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[]> {
return this.dataConsultService.getRegions();
}
}

View File

@@ -2,18 +2,13 @@ import { Module } from '@nestjs/common';
import { DataConsultService } from './data-consult.service';
import { DataConsultController } from './data-consult.controller';
import { DataConsultRepository } from './data-consult.repository';
import { LoggerModule } from 'src/Log/logger.module';
import { ConfigModule } from '@nestjs/config';
import { RedisModule } from 'src/core/configs/cache/redis.module';
import { clientes } from './clientes.module';
@Module({
imports: [LoggerModule, ConfigModule, RedisModule, clientes],
imports: [ConfigModule, RedisModule, clientes],
controllers: [DataConsultController],
providers: [
DataConsultService,
DataConsultRepository,
],
providers: [DataConsultService, DataConsultRepository],
})
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 { StoreDto } from './dto/store.dto';
import { SellerDto } from './dto/seller.dto';
@@ -7,7 +7,6 @@ import { CustomerDto } from './dto/customer.dto';
import { ProductDto } from './dto/product.dto';
import { RegionDto } from './dto/region.dto';
import { CarrierDto, FindCarriersDto } from './dto/carrier.dto';
import { ILogger } from '../Log/ILogger';
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
import { IRedisClient } from '../core/configs/cache/IRedisClient';
import { getOrSetCache } from '../shared/cache.util';
@@ -16,6 +15,7 @@ import { DATA_SOURCE } from '../core/constants';
@Injectable()
export class DataConsultService {
private readonly logger = new Logger(DataConsultService.name);
private readonly SELLERS_CACHE_KEY = 'data-consult:sellers';
private readonly SELLERS_TTL = 3600;
private readonly STORES_TTL = 3600;
@@ -31,8 +31,7 @@ export class DataConsultService {
constructor(
private readonly repository: DataConsultRepository,
@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[]> {
@@ -41,25 +40,38 @@ export class DataConsultService {
const stores = await this.repository.findStores();
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];
return storesArray
.filter(store => {
.filter((store) => {
if (!store || typeof store !== 'object') {
return false;
}
const hasId = store.id !== undefined && store.id !== null && store.id !== '';
const hasName = store.name !== undefined && store.name !== null && store.name !== '';
const hasStore = store.store !== undefined && store.store !== null && store.store !== '';
const hasId =
store.id !== undefined && store.id !== null && store.id !== '';
const hasName =
store.name !== undefined &&
store.name !== null &&
store.name !== '';
const hasStore =
store.store !== undefined &&
store.store !== null &&
store.store !== '';
return hasId && hasName && hasStore;
})
.map(store => new StoreDto(store));
.map((store) => new StoreDto(store));
} catch (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();
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];
return sellersArray
.filter(seller => {
.filter((seller) => {
if (!seller || typeof seller !== 'object') {
return false;
}
const hasId = seller.id !== undefined && seller.id !== null && seller.id !== '';
const hasName = seller.name !== undefined && seller.name !== null && seller.name !== '';
const hasId =
seller.id !== undefined &&
seller.id !== null &&
seller.id !== '';
const hasName =
seller.name !== undefined &&
seller.name !== null &&
seller.name !== '';
return hasId && hasName;
})
.map(seller => new SellerDto(seller));
.map((seller) => new SellerDto(seller));
} catch (error) {
this.logger.error('Erro ao buscar vendedores', error);
throw error;
}
}
},
);
} catch (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();
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];
return billingsArray
.filter(billing => {
.filter((billing) => {
if (!billing || typeof billing !== 'object') {
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 hasTotal = billing.total !== undefined && billing.total !== null;
const hasTotal =
billing.total !== undefined && billing.total !== null;
return hasId && hasDate && hasTotal;
})
.map(billing => new BillingDto(billing));
.map((billing) => new BillingDto(billing));
} catch (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);
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];
return customersArray
.filter(customer => {
.filter((customer) => {
if (!customer || typeof customer !== 'object') {
return false;
}
const hasId = customer.id !== undefined && 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 !== '';
const hasId =
customer.id !== undefined &&
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;
})
.map(customer => new CustomerDto(customer));
.map((customer) => new CustomerDto(customer));
} catch (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);
}
const products = await this.repository.findProducts(filter);
return products.map(product => new ProductDto(product));
return products.map((product) => new ProductDto(product));
} catch (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();
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
.filter(product => {
.filter((product) => {
if (!product || typeof product !== 'object') {
return false;
}
const hasId = product.id !== undefined && 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 !== '';
const hasId =
product.id !== undefined &&
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;
})
.map(product => new ProductDto(product));
.map((product) => new ProductDto(product));
} catch (error) {
this.logger.error('Erro ao buscar todos os produtos', error);
throw error;
}
}
},
);
} catch (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();
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
.filter(carrier => {
.filter((carrier) => {
if (!carrier || typeof carrier !== 'object') {
return false;
}
const hasCarrierId = carrier.carrierId !== undefined && 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 !== '';
const hasCarrierId =
carrier.carrierId !== undefined &&
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;
})
.map(carrier => ({
.map((carrier) => ({
carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '',
@@ -251,19 +334,24 @@ export class DataConsultService {
this.logger.error('Erro ao buscar transportadoras', error);
throw error;
}
}
},
);
} catch (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[]> {
this.logger.log(`Buscando transportadoras por período: ${JSON.stringify(query)}`);
this.logger.log(
`Buscando transportadoras por período: ${JSON.stringify(query)}`,
);
try {
const carriers = await this.repository.findCarriersByDate(query);
return carriers.map(carrier => ({
return carriers.map((carrier) => ({
carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '',
@@ -271,7 +359,10 @@ export class DataConsultService {
}));
} catch (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}`);
try {
const carriers = await this.repository.findOrderCarriers(orderId);
return carriers.map(carrier => ({
return carriers.map((carrier) => ({
carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '',
}));
} catch (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();
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];
return regionsArray
.filter(region => {
.filter((region) => {
if (!region || typeof region !== 'object') {
return false;
}
const hasNumregiao = region.numregiao !== undefined && region.numregiao !== null;
const hasRegiao = region.regiao !== undefined && region.regiao !== null && region.regiao !== '';
const hasNumregiao =
region.numregiao !== undefined && region.numregiao !== null;
const hasRegiao =
region.regiao !== undefined &&
region.regiao !== null &&
region.regiao !== '';
return hasNumregiao && hasRegiao;
})
.map(region => new RegionDto(region));
.map((region) => new RegionDto(region));
} catch (error) {
this.logger.error('Erro ao buscar regiões', error);
throw error;
}
}
},
);
} catch (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 {
@ApiProperty({
description: 'ID da transportadora',
example: '123'
example: '123',
})
carrierId: string;
@ApiProperty({
description: 'Nome da transportadora',
example: 'TRANSPORTADORA ABC LTDA'
example: 'TRANSPORTADORA ABC LTDA',
})
carrierName: string;
@ApiProperty({
description: 'Descrição completa da transportadora (ID - Nome)',
example: '123 - TRANSPORTADORA ABC LTDA'
example: '123 - TRANSPORTADORA ABC LTDA',
})
carrierDescription: string;
@ApiProperty({
description: 'Quantidade de pedidos da transportadora no período',
example: 15,
required: false
required: false,
})
ordersCount?: number;
}
@@ -32,7 +32,7 @@ export class FindCarriersDto {
@ApiProperty({
description: 'Data inicial para filtro (formato YYYY-MM-DD)',
example: '2024-01-01',
required: false
required: false,
})
@IsOptional()
@IsDateString()
@@ -41,7 +41,7 @@ export class FindCarriersDto {
@ApiProperty({
description: 'Data final para filtro (formato YYYY-MM-DD)',
example: '2024-12-31',
required: false
required: false,
})
@IsOptional()
@IsDateString()
@@ -50,7 +50,7 @@ export class FindCarriersDto {
@ApiProperty({
description: 'ID da filial',
example: '1',
required: false
required: false,
})
@IsOptional()
@IsString()

View File

@@ -20,4 +20,3 @@ export class RegionDto {
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 @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/controllers#controllers
*/

View File

@@ -1,5 +1,5 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { LogisticController } from './logistic.controller';
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 { createPostgresConfig } from '../core/configs/typeorm.postgres.config';
import { CarOutDelivery } from '../core/models/car-out-delivery.model';
@@ -16,7 +23,6 @@ export class LogisticService {
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
try {
const sqlWMS = `select dados.*,
( select count(distinct v.numero_carga) quantidade_cargas_embarcadas
from volume v, carga c2
@@ -52,7 +58,6 @@ export class LogisticService {
where dados.data_saida >= current_date
ORDER BY dados.data_saida desc `;
const sql = `SELECT COUNT(DISTINCT PCCARREG.NUMCAR) as "qtde"
,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"
@@ -86,7 +91,11 @@ export class LogisticService {
console.log(amanha);
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;
} catch (e) {
@@ -103,7 +112,6 @@ export class LogisticService {
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
try {
const sql = `SELECT PCCARREG.NUMCAR as "id"
,PCCARREG.DTSAIDA as "createDate"
,PCCARREG.DESTINO as "comment"
@@ -138,7 +146,6 @@ export class LogisticService {
await queryRunner.release();
await dataSource.destroy();
}
}
async getStatusCar(placa: string) {
@@ -147,7 +154,6 @@ export class LogisticService {
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
try {
const sql = `SELECT ESTSAIDAVEICULO.CODSAIDA FROM ESTSAIDAVEICULO, PCVEICUL
WHERE ESTSAIDAVEICULO.CODVEICULO = PCVEICUL.CODVEICULO
AND PCVEICUL.PLACA = '${placa}'
@@ -155,8 +161,7 @@ export class LogisticService {
const outCar = await queryRunner.manager.query(sql);
return { veiculoEmViagem: ( outCar.length > 0 ) ? true : false };
return { veiculoEmViagem: outCar.length > 0 ? true : false };
} catch (e) {
console.log(e);
} finally {
@@ -188,14 +193,12 @@ export class LogisticService {
}
async createCarOut(data: CarOutDelivery) {
const dataSource = new DataSource(createPostgresConfig(this.configService));
await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const sqlSequence = `SELECT ESS_SAIDAVEICULO.NEXTVAL as "id" FROM DUAL`;
const dataSequence = await queryRunner.query(sqlSequence);
let i = 0;
@@ -207,7 +210,7 @@ export class LogisticService {
const image3 = '';
const image4 = '';
data.helpers.forEach(helper => {
data.helpers.forEach((helper) => {
switch (i) {
case 0:
helperId1 = helper.id;
@@ -246,13 +249,11 @@ export class LogisticService {
,KMINICIAL = ${data.startKm}
WHERE NUMCAR = ${data.numberLoading[y]}`;
await queryRunner.query(sql);
}
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) {
await queryRunner.rollbackTransaction();
throw e;
@@ -263,14 +264,12 @@ export class LogisticService {
}
async createCarIn(data: CarInDelivery) {
const dataSource = new DataSource(createPostgresConfig(this.configService));
await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const sqlOutCar = `SELECT ESTSAIDAVEICULO.CODSAIDA as "id"
FROM PCCARREG, PCVEICUL, ESTSAIDAVEICULO, ESTSAIDAVEICULOCARREG
WHERE PCCARREG.CODVEICULO = PCVEICUL.CODVEICULO
@@ -281,7 +280,10 @@ export class LogisticService {
const dataOutCar = await queryRunner.query(sqlOutCar);
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;
@@ -332,8 +334,7 @@ export class LogisticService {
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) {
await queryRunner.rollbackTransaction();
console.log(e);
@@ -343,5 +344,4 @@ export class LogisticService {
await dataSource.destroy();
}
}
}

View File

@@ -15,18 +15,25 @@ async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.use(helmet({
app.use(
helmet({
contentSecurityPolicy: {
directives: {
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'],
imgSrc: [`'self'`, 'data:'],
connectSrc: [`'self'`],
fontSrc: [`'self'`, 'cdnjs.cloudflare.com'],
},
},
}));
}),
);
// Configurar pasta de arquivos estáticos
app.useStaticAssets(join(__dirname, '..', 'public'), {
@@ -56,7 +63,6 @@ async function bootstrap() {
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
});
const config = new DocumentBuilder()
.setTitle('Portal Jurunense API')
.setDescription('Documentação da API do Portal Jurunense')
@@ -68,7 +74,5 @@ async function bootstrap() {
SwaggerModule.setup('docs', app, document);
await app.listen(8066);
}
bootstrap();

View File

@@ -38,7 +38,7 @@ export class CreatePaymentDto {
@ApiProperty({
description: 'Valor do pagamento',
example: 1000.00,
example: 1000.0,
required: true,
})
amount: number;

View File

@@ -69,7 +69,7 @@ export class OrderDto {
@ApiProperty({
description: 'Valor total do pedido',
example: 1000.00,
example: 1000.0,
})
amount: number;
@@ -81,7 +81,7 @@ export class OrderDto {
@ApiProperty({
description: 'Valor total pago',
example: 1000.00,
example: 1000.0,
})
amountPaid: number;

View File

@@ -39,7 +39,7 @@ export class PaymentDto {
@ApiProperty({
description: 'Valor do pagamento',
example: 1000.00,
example: 1000.0,
})
amount: number;

View File

@@ -1,5 +1,11 @@
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 { OrderDto } from './dto/order.dto';
import { PaymentDto } from './dto/payment.dto';
@@ -12,7 +18,6 @@ import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
@UseGuards(JwtAuthGuard)
@Controller('api/v1/orders-payment')
export class OrdersPaymentController {
constructor(private readonly orderPaymentService: OrdersPaymentService) {}
@Get('orders/:id')
@@ -21,7 +26,7 @@ export class OrdersPaymentController {
@ApiResponse({
status: 200,
description: 'Lista de pedidos retornada com sucesso',
type: [OrderDto]
type: [OrderDto],
})
async findOrders(@Param('id') storeId: string): Promise<OrderDto[]> {
return this.orderPaymentService.findOrders(storeId, 0);
@@ -34,7 +39,7 @@ export class OrdersPaymentController {
@ApiResponse({
status: 200,
description: 'Pedido retornado com sucesso',
type: OrderDto
type: OrderDto,
})
async findOrder(
@Param('id') storeId: string,
@@ -50,7 +55,7 @@ export class OrdersPaymentController {
@ApiResponse({
status: 200,
description: 'Lista de pagamentos retornada com sucesso',
type: [PaymentDto]
type: [PaymentDto],
})
async findPayments(@Param('id') orderId: number): Promise<PaymentDto[]> {
return this.orderPaymentService.findPayments(orderId);
@@ -59,7 +64,7 @@ export class OrdersPaymentController {
@ApiOperation({ summary: 'Cria um novo pagamento' })
@ApiResponse({
status: 201,
description: 'Pagamento criado com sucesso'
description: 'Pagamento criado com sucesso',
})
async createPayment(@Body() data: CreatePaymentDto): Promise<void> {
return this.orderPaymentService.createPayment(data);
@@ -69,7 +74,7 @@ export class OrdersPaymentController {
@ApiOperation({ summary: 'Cria uma nova fatura' })
@ApiResponse({
status: 201,
description: 'Fatura criada com sucesso'
description: 'Fatura criada com sucesso',
})
async createInvoice(@Body() data: CreateInvoiceDto): Promise<void> {
return this.orderPaymentService.createInvoice(data);

View File

@@ -1,5 +1,5 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/modules

View File

@@ -11,7 +11,7 @@ import { CreateInvoiceDto } from './dto/create-invoice.dto';
export class OrdersPaymentService {
constructor(
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[]> {
@@ -48,7 +48,7 @@ export class OrdersPaymentService {
}
const orders = await queryRunner.manager.query(sql + sqlWhere);
return orders.map(order => new OrderDto(order));
return orders.map((order) => new OrderDto(order));
} finally {
await queryRunner.release();
}
@@ -73,7 +73,7 @@ export class OrdersPaymentService {
WHERE ESTPAGAMENTO.NUMORCA = ${orderId}`;
const payments = await queryRunner.manager.query(sql);
return payments.map(payment => new PaymentDto(payment));
return payments.map((payment) => new PaymentDto(payment));
} finally {
await queryRunner.release();
}

View File

@@ -4,9 +4,7 @@ import { DebDto } from '../dto/DebDto';
@Injectable()
export class DebService {
constructor(
private readonly debRepository: DebRepository,
) {}
constructor(private readonly debRepository: DebRepository) {}
/**
* Busca débitos por CPF ou CGCENT
@@ -21,6 +19,10 @@ export class DebService {
matricula?: number,
cobranca?: string,
): 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 { CarrierDto } from '../../data-consult/dto/carrier.dto';
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 { DeliveryCompleted } from '../dto/delivery-completed.dto';
import { OrderResponseDto } from '../dto/order-response.dto';
@Injectable()
export class OrdersService {
// Cache TTL em segundos
private static readonly DEFAULT_TTL = 60;
private readonly TTL_ORDERS = OrdersService.DEFAULT_TTL;
private readonly TTL_INVOICE = OrdersService.DEFAULT_TTL;
@@ -42,19 +44,10 @@ export class OrdersService {
@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[]> {
const key = `orders:query:${this.hashObject(query)}`;
return getOrSetCache(
this.redisClient,
key,
this.TTL_ORDERS,
async () => {
return getOrSetCache(this.redisClient, key, this.TTL_ORDERS, async () => {
const orders = await this.ordersRepository.findOrders(query);
if (!query.includeCompletedDeliveries) {
@@ -65,57 +58,43 @@ export class OrdersService {
const deliveryQuery = {
orderNumber: order.invoiceNumber,
limit: 10,
offset: 0
offset: 0,
};
try {
const deliveries = await this.ordersRepository.getCompletedDeliveries(deliveryQuery);
const deliveries = await this.ordersRepository.getCompletedDeliveries(
deliveryQuery,
);
order.completedDeliveries = deliveries;
} catch (error) {
// Se houver erro, definir como array vazio
order.completedDeliveries = [];
}
}
return orders;
},
);
});
}
/**
* Buscar pedidos por data de entrega com cache
* @param query - Filtros para busca por data de entrega
* @returns Lista de pedidos
*/
async findOrdersByDeliveryDate(query: FindOrdersByDeliveryDateDto): Promise<OrderResponseDto[]> {
async findOrdersByDeliveryDate(
query: FindOrdersByDeliveryDateDto,
): Promise<OrderResponseDto[]> {
const key = `orders:delivery:${this.hashObject(query)}`;
return getOrSetCache(
this.redisClient,
key,
this.TTL_ORDERS,
() => this.ordersRepository.findOrdersByDeliveryDate(query),
return getOrSetCache(this.redisClient, key, this.TTL_ORDERS, () =>
this.ordersRepository.findOrdersByDeliveryDate(query),
);
}
/**
* Buscar pedidos com resultados de fechamento de caixa
* @param query - Filtros para busca de pedidos
* @returns Lista de pedidos com dados de fechamento de caixa
*/
async findOrdersWithCheckout(query: FindOrdersDto): Promise<(OrderResponseDto & { checkout: any })[]> {
async findOrdersWithCheckout(
query: FindOrdersDto,
): Promise<(OrderResponseDto & { checkout: any })[]> {
const key = `orders:checkout:${this.hashObject(query)}`;
return getOrSetCache(
this.redisClient,
key,
this.TTL_ORDERS,
async () => {
// Primeiro obtém a lista de pedidos
return getOrSetCache(this.redisClient, key, this.TTL_ORDERS, async () => {
const orders = await this.findOrders(query);
// Para cada pedido, busca o fechamento de caixa
const results = await Promise.all(
orders.map(async order => {
orders.map(async (order) => {
try {
const checkout = await this.ordersRepository.findOrderWithCheckoutByOrder(
const checkout =
await this.ordersRepository.findOrderWithCheckoutByOrder(
Number(order.orderId),
);
return { ...order, checkout };
@@ -125,29 +104,25 @@ export class OrdersService {
}),
);
return results;
}
);
});
}
async getOrderCheckout(orderId: number) {
const key = `orders:checkout:${orderId}`;
return getOrSetCache(
this.redisClient,
key,
this.TTL_ORDERS,
async () => {
const result = await this.ordersRepository.findOrderWithCheckoutByOrder(orderId);
return getOrSetCache(this.redisClient, key, this.TTL_ORDERS, async () => {
const result = await this.ordersRepository.findOrderWithCheckoutByOrder(
orderId,
);
if (!result) {
throw new HttpException('Nenhum fechamento encontrado', HttpStatus.NOT_FOUND);
}
return result;
}
throw new HttpException(
'Nenhum fechamento encontrado',
HttpStatus.NOT_FOUND,
);
}
return result;
});
}
/**
* Buscar nota fiscal por chave NFe com cache
*/
async findInvoice(chavenfe: string): Promise<InvoiceDto> {
const key = `orders:invoice:${chavenfe}`;
@@ -172,16 +147,13 @@ export class OrdersService {
});
}
/**
* Buscar itens de pedido com cache
*/
async getItens(orderId: string): Promise<OrderItemDto[]> {
const key = `orders:itens:${orderId}`;
return getOrSetCache(this.redisClient, key, this.TTL_ITENS, async () => {
const itens = await this.ordersRepository.getItens(orderId);
return itens.map(item => ({
return itens.map((item) => ({
productId: Number(item.productId),
description: item.description,
pacth: item.pacth,
@@ -198,20 +170,14 @@ export class OrdersService {
});
}
/**
* Buscar entregas do pedido com cache
*/
async getOrderDeliveries(
orderId: string,
query: { createDateIni: string; createDateEnd: string },
): Promise<OrderDeliveryDto[]> {
const key = `orders:deliveries:${orderId}:${query.createDateIni}:${query.createDateEnd}`;
return getOrSetCache(
this.redisClient,
key,
this.TTL_DELIVERIES,
() => this.ordersRepository.getOrderDeliveries(orderId, query),
return getOrSetCache(this.redisClient, key, this.TTL_DELIVERIES, () =>
this.ordersRepository.getOrderDeliveries(orderId),
);
}
@@ -221,7 +187,7 @@ export class OrdersService {
return getOrSetCache(this.redisClient, key, this.TTL_ITENS, async () => {
const itens = await this.ordersRepository.getCutItens(orderId);
return itens.map(item => ({
return itens.map((item) => ({
productId: Number(item.productId),
description: item.description,
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}`;
return getOrSetCache(
@@ -241,7 +210,9 @@ export class OrdersService {
key,
this.TTL_DELIVERIES,
async () => {
const orderDelivery = await this.ordersRepository.getOrderDelivery(orderId);
const orderDelivery = await this.ordersRepository.getOrderDelivery(
orderId,
);
if (!orderDelivery) {
return null;
@@ -252,8 +223,8 @@ export class OrdersService {
}
try {
// Buscar entregas realizadas usando o transactionId do pedido
const transactionId = await this.ordersRepository.getOrderTransactionId(orderId);
const transactionId =
await this.ordersRepository.getOrderTransactionId(orderId);
if (!transactionId) {
orderDelivery.completedDeliveries = [];
@@ -263,31 +234,27 @@ export class OrdersService {
const deliveryQuery = {
transactionId: transactionId,
limit: 10,
offset: 0
offset: 0,
};
const deliveries = await this.ordersRepository.getCompletedDeliveriesByTransactionId(deliveryQuery);
const deliveries =
await this.ordersRepository.getCompletedDeliveriesByTransactionId(
deliveryQuery,
);
orderDelivery.completedDeliveries = deliveries;
} catch (error) {
// Se houver erro, definir como array vazio
orderDelivery.completedDeliveries = [];
}
return orderDelivery;
}
},
);
}
/**
* Buscar leadtime do pedido com cache
*/
async getLeadtime(orderId: string): Promise<LeadtimeDto[]> {
const key = `orders:leadtime:${orderId}`;
return getOrSetCache(
this.redisClient,
key,
this.TTL_LEADTIME,
() => this.ordersRepository.getLeadtimeWMS(orderId)
return getOrSetCache(this.redisClient, 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(
orderId: number,
filters?: EstLogTransferFilterDto
filters?: EstLogTransferFilterDto,
): 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, () =>
this.ordersRepository.estlogtransfer(orderId, filters),
);
}
/**
* Buscar logs de transferência com filtros (sem especificar pedido específico)
*/
async getTransferLogs(
filters?: EstLogTransferFilterDto
filters?: EstLogTransferFilterDto,
): Promise<EstLogTransferResponseDto[] | null> {
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 {
const objRecord = obj as Record<string, unknown>;
const sortedKeys = Object.keys(objRecord).sort();
@@ -346,21 +304,19 @@ export class OrdersService {
return createHash('md5').update(str).digest('hex');
}
async createInvoiceCheck(invoice: InvoiceCheckDto): Promise<{ message: string }> {
// Não usa cache para operações de escrita
async createInvoiceCheck(
invoice: InvoiceCheckDto,
): Promise<{ message: string }> {
return this.ordersRepository.createInvoiceCheck(invoice);
}
/**
* Buscar transportadoras do pedido com cache
*/
async getOrderCarriers(orderId: number): Promise<CarrierDto[]> {
const key = `orders:carriers:${orderId}`;
return getOrSetCache(this.redisClient, key, this.TTL_CARRIERS, async () => {
const carriers = await this.ordersRepository.getOrderCarriers(orderId);
return carriers.map(carrier => ({
return carriers.map((carrier) => ({
carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '',
@@ -368,9 +324,6 @@ export class OrdersService {
});
}
/**
* Buscar marca por ID com cache
*/
async findOrderByMark(orderId: number): Promise<MarkData> {
const key = `orders:mark:${orderId}`;
@@ -383,9 +336,6 @@ export class OrdersService {
});
}
/**
* Buscar todas as marcas disponíveis com cache
*/
async getAllMarks(): Promise<MarkData[]> {
const key = 'orders:marks:all';
@@ -394,9 +344,6 @@ export class OrdersService {
});
}
/**
* Buscar marcas por nome com cache
*/
async getMarksByName(markName: string): Promise<MarkData[]> {
const key = `orders:marks:name:${markName}`;
@@ -405,10 +352,9 @@ export class OrdersService {
});
}
/**
* Buscar entregas realizadas com cache baseado nos filtros
*/
async getCompletedDeliveries(query: DeliveryCompletedQuery): Promise<DeliveryCompleted[]> {
async getCompletedDeliveries(
query: DeliveryCompletedQuery,
): Promise<DeliveryCompleted[]> {
const key = `orders:completed-deliveries:${this.hashObject(query)}`;
return getOrSetCache(

View File

@@ -18,7 +18,8 @@ export class DebController {
@Get('find-by-cpf')
@ApiOperation({
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({
status: 200,
@@ -34,9 +35,7 @@ export class DebController {
description: 'Erro interno do servidor',
})
@UsePipes(new ValidationPipe({ transform: true }))
async findByCpfCgcent(
@Query() query: FindDebDto,
): Promise<DebDto[]> {
async findByCpfCgcent(@Query() query: FindDebDto): Promise<DebDto[]> {
return await this.debService.findByCpfCgcent(
query.cpfCgcent,
query.matricula,

View File

@@ -7,21 +7,26 @@ import {
Query,
UsePipes,
UseGuards,
UseInterceptors,
ValidationPipe,
HttpException,
HttpStatus,
DefaultValuePipe,
ParseBoolPipe,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags, ApiQuery, ApiParam, ApiResponse } from '@nestjs/swagger';
import { ResponseInterceptor } from '../../common/response.interceptor';
import {
ApiBearerAuth,
ApiOperation,
ApiTags,
ApiQuery,
ApiParam,
ApiResponse,
} from '@nestjs/swagger';
import { OrdersService } from '../application/orders.service';
import { FindOrdersDto } from '../dto/find-orders.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 { OrderItemDto } from "../dto/OrderItemDto";
import { OrderItemDto } from '../dto/OrderItemDto';
import { LeadtimeDto } from '../dto/leadtime.dto';
import { CutItemDto } from '../dto/CutItemDto';
import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
@@ -34,7 +39,6 @@ import { OrderResponseDto } from '../dto/order-response.dto';
import { MarkResponseDto } from '../dto/mark-response.dto';
import { EstLogTransferResponseDto } from '../dto/estlogtransfer.dto';
@ApiTags('Orders')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@@ -45,15 +49,47 @@ export class OrdersController {
@Get('find')
@ApiOperation({
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 }))
@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(
@Query() query: FindOrdersDto,
@Query('includeCheckout', new DefaultValuePipe(false), ParseBoolPipe)
@@ -68,17 +104,42 @@ export class OrdersController {
@Get('find-by-delivery-date')
@ApiOperation({
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 }))
@ApiResponse({ status: 200, description: 'Lista de pedidos por data de entrega retornada com sucesso', type: [OrderResponseDto] })
findOrdersByDeliveryDate(
@Query() query: FindOrdersByDeliveryDateDto,
) {
@ApiResponse({
status: 200,
description: 'Lista de pedidos por data de entrega retornada com sucesso',
type: [OrderResponseDto],
})
findOrdersByDeliveryDate(@Query() query: FindOrdersByDeliveryDateDto) {
return this.ordersService.findOrdersByDeliveryDate(query);
}
@@ -86,20 +147,16 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca fechamento de caixa para um pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
getOrderCheckout(
@Param('orderId', ParseIntPipe) orderId: number,
) {
getOrderCheckout(@Param('orderId', ParseIntPipe) orderId: number) {
return this.ordersService.getOrderCheckout(orderId);
}
@Get('invoice/:chavenfe')
@ApiParam({
name: 'chavenfe',
required: true,
description: 'Chave da Nota Fiscal (44 dígitos)',
})
@ApiOperation({ summary: 'Busca NF pela chave' })
@UsePipes(new ValidationPipe({ transform: true }))
async getInvoice(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> {
@@ -117,7 +174,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca PELO numero do pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderItemDto[]> {
async getItens(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderItemDto[]> {
try {
return await this.ordersService.getItens(orderId.toString());
} catch (error) {
@@ -131,7 +190,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca itens cortados do pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getCutItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<CutItemDto[]> {
async getCutItens(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<CutItemDto[]> {
try {
return await this.ordersService.getCutItens(orderId.toString());
} catch (error) {
@@ -146,7 +207,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca dados de entrega do pedido' })
@ApiParam({ name: 'orderId' })
@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 {
return await this.ordersService.getOrderDelivery(orderId.toString());
} catch (error) {
@@ -161,7 +224,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Consulta pedidos de transferência' })
@ApiParam({ name: 'orderId' })
@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 {
return await this.ordersService.getTransfer(orderId);
} catch (error) {
@@ -176,7 +241,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Consulta status do pedido' })
@ApiParam({ name: 'orderId' })
@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 {
return await this.ordersService.getStatusOrder(orderId);
} catch (error) {
@@ -187,20 +254,32 @@ export class OrdersController {
}
}
@Get(':orderId/deliveries')
@ApiOperation({ summary: 'Consulta entregas do pedido' })
@ApiParam({ name: 'orderId' })
@ApiQuery({ 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)' })
@ApiQuery({
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(
@Param('orderId', ParseIntPipe) orderId: number,
@Query('createDateIni') createDateIni?: string,
@Query('createDateEnd') createDateEnd?: string,
): Promise<OrderDeliveryDto[]> {
// 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 defaultDateEnd = createDateEnd || new Date().toISOString().split('T')[0];
const defaultDateIni =
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(), {
createDateIni: defaultDateIni,
@@ -208,12 +287,13 @@ export class OrdersController {
});
}
@Get('leadtime/:orderId')
@ApiOperation({ summary: 'Consulta leadtime do pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getLeadtime(@Param('orderId', ParseIntPipe) orderId: number): Promise<LeadtimeDto[]> {
async getLeadtime(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<LeadtimeDto[]> {
try {
return await this.ordersService.getLeadtime(orderId.toString());
} catch (error) {
@@ -227,13 +307,15 @@ export class OrdersController {
@Post('invoice/check')
@ApiOperation({ summary: 'Cria conferência de nota fiscal' })
@UsePipes(new ValidationPipe({ transform: true }))
async createInvoiceCheck(@Body() invoice: InvoiceCheckDto): Promise<{ message: string }> {
async createInvoiceCheck(
@Body() invoice: InvoiceCheckDto,
): Promise<{ message: string }> {
try {
return await this.ordersService.createInvoiceCheck(invoice);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao salvar conferência',
error.status || HttpStatus.INTERNAL_SERVER_ERROR
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@@ -242,7 +324,9 @@ async createInvoiceCheck(@Body() invoice: InvoiceCheckDto): Promise<{ message: s
@ApiOperation({ summary: 'Busca transportadoras do pedido' })
@ApiParam({ name: 'orderId', example: 236001388 })
@UsePipes(new ValidationPipe({ transform: true }))
async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise<CarrierDto[]> {
async getOrderCarriers(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<CarrierDto[]> {
try {
return await this.ordersService.getOrderCarriers(orderId);
} catch (error) {
@@ -257,9 +341,15 @@ async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise
@ApiOperation({ summary: 'Busca marca por ID do pedido' })
@ApiParam({ name: 'orderId', example: 236001388 })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Marca encontrada com sucesso', type: MarkResponseDto })
@ApiResponse({
status: 200,
description: 'Marca encontrada com sucesso',
type: MarkResponseDto,
})
@ApiResponse({ status: 404, description: 'Marca não encontrada' })
async findOrderByMark(@Param('orderId', ParseIntPipe) orderId: number): Promise<MarkResponseDto> {
async findOrderByMark(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<MarkResponseDto> {
try {
return await this.ordersService.findOrderByMark(orderId);
} catch (error) {
@@ -273,7 +363,11 @@ async findOrderByMark(@Param('orderId', ParseIntPipe) orderId: number): Promise<
@Get('marks')
@ApiOperation({ summary: 'Busca todas as marcas disponíveis' })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Lista de marcas retornada com sucesso', type: [MarkResponseDto] })
@ApiResponse({
status: 200,
description: 'Lista de marcas retornada com sucesso',
type: [MarkResponseDto],
})
async getAllMarks(): Promise<MarkResponseDto[]> {
try {
return await this.ordersService.getAllMarks();
@@ -287,10 +381,21 @@ async getAllMarks(): Promise<MarkResponseDto[]> {
@Get('marks/search')
@ApiOperation({ summary: 'Busca marcas por nome' })
@ApiQuery({ name: 'name', required: true, type: 'string', description: 'Nome da marca para buscar' })
@ApiQuery({
name: 'name',
required: true,
type: 'string',
description: 'Nome da marca para buscar',
})
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Lista de marcas encontradas', type: [MarkResponseDto] })
async getMarksByName(@Query('name') markName: string): Promise<MarkResponseDto[]> {
@ApiResponse({
status: 200,
description: 'Lista de marcas encontradas',
type: [MarkResponseDto],
})
async getMarksByName(
@Query('name') markName: string,
): Promise<MarkResponseDto[]> {
try {
return await this.ordersService.getMarksByName(markName);
} catch (error) {
@@ -303,16 +408,51 @@ async getMarksByName(@Query('name') markName: string): Promise<MarkResponseDto[]
@Get('transfer-log/:orderId')
@ApiOperation({ summary: 'Busca log de transferência por ID do pedido' })
@ApiParam({ name: 'orderId', description: 'ID do pedido para buscar log de transferência' })
@ApiQuery({ name: 'dttransf', required: false, type: 'string', description: 'Data de transferência (formato YYYY-MM-DD)' })
@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: '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' })
@ApiParam({
name: 'orderId',
description: 'ID do pedido para buscar log de transferência',
})
@ApiQuery({
name: 'dttransf',
required: false,
type: 'string',
description: 'Data de transferência (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'codfilial',
required: false,
type: 'number',
description: 'Código da filial de origem',
})
@ApiQuery({
name: 'codfilialdest',
required: false,
type: 'number',
description: 'Código da filial de destino',
})
@ApiQuery({
name: 'numpedloja',
required: false,
type: 'number',
description: 'Número do pedido da loja',
})
@ApiQuery({
name: 'numpedtransf',
required: false,
type: 'number',
description: 'Número do pedido de transferência',
})
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Log de transferência encontrado com sucesso', type: [EstLogTransferResponseDto] })
@ApiResponse({
status: 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' })
@ApiResponse({
status: 404,
description: 'Log de transferência não encontrado',
})
async getTransferLog(
@Param('orderId', ParseIntPipe) orderId: number,
@Query('dttransf') dttransf?: string,
@@ -341,15 +481,54 @@ async getTransferLog(
@Get('transfer-log')
@ApiOperation({ summary: 'Busca logs de transferência com filtros' })
@ApiQuery({ name: 'dttransf', required: false, type: 'string', description: 'Data de transferência (formato YYYY-MM-DD)' })
@ApiQuery({ name: 'dttransfIni', required: false, type: 'string', description: 'Data de transferência inicial (formato YYYY-MM-DD)' })
@ApiQuery({ name: 'dttransfEnd', required: false, type: 'string', description: 'Data de transferência final (formato YYYY-MM-DD)' })
@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: 'Código da filial de origem' })
@ApiQuery({ name: 'codfilialdest', required: false, type: 'number', description: 'Código da filial de destino' })
@ApiQuery({ name: 'numpedloja', required: false, type: 'number', description: 'Número do pedido da loja' })
@ApiQuery({ name: 'numpedtransf', required: false, type: 'number', description: 'Número do pedido de transferência' })
@ApiQuery({
name: 'dttransf',
required: false,
type: 'string',
description: 'Data de transferência (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'dttransfIni',
required: false,
type: 'string',
description: 'Data de transferência inicial (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'dttransfEnd',
required: false,
type: 'string',
description: 'Data de transferência final (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'codfilial',
required: false,
type: 'number',
description: 'Código da filial de origem',
})
@ApiQuery({
name: 'codfilialdest',
required: false,
type: 'number',
description: 'Código da filial de destino',
})
@ApiQuery({
name: 'numpedloja',
required: false,
type: 'number',
description: 'Número do pedido da loja',
})
@ApiQuery({
name: 'numpedtransf',
required: false,
type: 'number',
description: 'Número do pedido de transferência',
})
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Logs de transferência encontrados com sucesso', type: [EstLogTransferResponseDto] })
@ApiResponse({
status: 200,
description: 'Logs de transferência encontrados com sucesso',
type: [EstLogTransferResponseDto],
})
@ApiResponse({ status: 400, description: 'Filtros inválidos' })
async getTransferLogs(
@Query('dttransf') dttransf?: string,

View File

@@ -7,4 +7,3 @@ export class CutItemDto {
cutQuantity: number;
separedQuantity: number;
}

View File

@@ -70,7 +70,7 @@ export class DebDto {
@ApiProperty({
description: 'Valor da prestação',
example: 150.50,
example: 150.5,
})
valor: number;
@@ -81,4 +81,3 @@ export class DebDto {
})
situacao: string;
}

View File

@@ -28,4 +28,3 @@ export class OrderDeliveryDto {
releaseDate: Date;
completedDeliveries?: DeliveryCompleted[];
}

View File

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

View File

@@ -10,4 +10,3 @@ export class OrderTransferDto {
userName: string;
program: string;
}

View File

@@ -2,12 +2,16 @@ import { IsOptional, IsString, IsNumber, IsDateString } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
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()
@IsDateString()
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()
@IsDateString()
endDate?: string;
@@ -42,7 +46,10 @@ export class DeliveryCompletedQuery {
@IsString()
status?: string;
@ApiPropertyOptional({ description: 'Limite de registros por página', default: 100 })
@ApiPropertyOptional({
description: 'Limite de registros por página',
default: 100,
})
@IsOptional()
@IsNumber()
limit?: number;

View File

@@ -1,5 +1,11 @@
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';
export class FindDebDto {
@@ -31,4 +37,3 @@ export class FindDebDto {
})
cobranca?: string;
}

View File

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

View File

@@ -5,8 +5,7 @@ import {
IsDateString,
IsString,
IsNumber,
IsIn,
IsBoolean
IsBoolean,
} from 'class-validator';
/**
@@ -17,7 +16,7 @@ export class FindOrdersByDeliveryDateDto {
@IsDateString()
@ApiPropertyOptional({
description: 'Data de entrega inicial (formato: YYYY-MM-DD)',
example: '2024-01-01'
example: '2024-01-01',
})
deliveryDateIni?: string;
@@ -25,7 +24,7 @@ export class FindOrdersByDeliveryDateDto {
@IsDateString()
@ApiPropertyOptional({
description: 'Data de entrega final (formato: YYYY-MM-DD)',
example: '2024-12-31'
example: '2024-12-31',
})
deliveryDateEnd?: string;
@@ -33,7 +32,7 @@ export class FindOrdersByDeliveryDateDto {
@IsString()
@ApiPropertyOptional({
description: 'Código da filial',
example: '01'
example: '01',
})
codfilial?: string;
@@ -41,7 +40,7 @@ export class FindOrdersByDeliveryDateDto {
@IsString()
@ApiPropertyOptional({
description: 'ID do vendedor (separado por vírgula para múltiplos valores)',
example: '270,431'
example: '270,431',
})
sellerId?: string;
@@ -49,7 +48,7 @@ export class FindOrdersByDeliveryDateDto {
@IsNumber()
@ApiPropertyOptional({
description: 'ID do cliente',
example: 456
example: 456,
})
customerId?: number;
@@ -57,7 +56,7 @@ export class FindOrdersByDeliveryDateDto {
@IsString()
@ApiPropertyOptional({
description: 'Tipo de entrega (EN, EF, RP, RI)',
example: 'EN'
example: 'EN',
})
deliveryType?: string;
@@ -65,7 +64,7 @@ export class FindOrdersByDeliveryDateDto {
@IsString()
@ApiPropertyOptional({
description: 'Status do pedido (L, P, B, M, F)',
example: 'L'
example: 'L',
})
status?: string;
@@ -73,7 +72,7 @@ export class FindOrdersByDeliveryDateDto {
@IsNumber()
@ApiPropertyOptional({
description: 'ID do pedido específico',
example: 236001388
example: 236001388,
})
orderId?: number;
@@ -82,7 +81,7 @@ export class FindOrdersByDeliveryDateDto {
@ApiPropertyOptional({
description: 'Filtrar por status de transferência',
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;
@@ -90,7 +89,7 @@ export class FindOrdersByDeliveryDateDto {
@IsNumber()
@ApiPropertyOptional({
description: 'ID da marca para filtrar pedidos',
example: 1
example: 1,
})
markId?: number;
@@ -98,7 +97,7 @@ export class FindOrdersByDeliveryDateDto {
@IsString()
@ApiPropertyOptional({
description: 'Nome da marca para filtrar pedidos',
example: 'Nike'
example: 'Nike',
})
markName?: string;
@@ -106,8 +105,9 @@ export class FindOrdersByDeliveryDateDto {
@Type(() => Boolean)
@IsBoolean()
@ApiPropertyOptional({
description: 'Filtrar pedidos que tenham registros na tabela de transfer log',
example: true
description:
'Filtrar pedidos que tenham registros na tabela de transfer log',
example: true,
})
hasPreBox?: boolean;
}

View File

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

View File

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

View File

@@ -9,8 +9,13 @@ import { map } from 'rxjs/operators';
import { ResultModel } from '../../shared/ResultModel';
@Injectable()
export class OrdersResponseInterceptor<T> implements NestInterceptor<T, ResultModel<T>> {
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<ResultModel<T>> {
export class OrdersResponseInterceptor<T>
implements NestInterceptor<T, ResultModel<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<ResultModel<T>> {
return next.handle().pipe(
map((data) => {
return ResultModel.success(data);

View File

@@ -2,5 +2,3 @@ export interface DebQueryParams {
cpfCgcent: string;
matricula?: number;
}

View File

@@ -6,13 +6,9 @@ import { DatabaseModule } from '../../core/database/database.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule,
DatabaseModule,
],
imports: [ConfigModule, DatabaseModule],
controllers: [DebController],
providers: [DebService, DebRepository],
exports: [DebService],
})
export class DebModule {}

View File

@@ -6,10 +6,7 @@ import { DatabaseModule } from '../../core/database/database.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule,
DatabaseModule,
],
imports: [ConfigModule, DatabaseModule],
controllers: [OrdersController],
providers: [OrdersService, OrdersRepository],
exports: [OrdersService],

View File

@@ -6,7 +6,7 @@ import { DebDto } from '../dto/DebDto';
@Injectable()
export class DebRepository {
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
* @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();
await queryRunner.connect();
try {
@@ -46,7 +50,9 @@ export class DebRepository {
.innerJoin('pcclient', 'c', 'p.codcli = c.codcli')
.innerJoin('pccob', 'cb', 'p.codcob = cb.codcob')
.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 });
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 { PartnersService } from './partners.service';
import { PartnerDto } from './dto/partner.dto';
@@ -6,17 +6,19 @@ import { PartnerDto } from './dto/partner.dto';
@ApiTags('Parceiros')
@Controller('api/v1/')
export class PartnersController {
constructor(private readonly partnersService: PartnersService) {}
@Get('parceiros/:filter')
@ApiOperation({ summary: 'Busca parceiros por filtro (ID, CPF ou nome)' })
@ApiParam({ name: 'filter', description: 'Filtro de busca (ID, CPF ou nome)' })
@ApiParam({
name: 'filter',
description: 'Filtro de busca (ID, CPF ou nome)',
})
@ApiResponse({
status: 200,
description: 'Lista de parceiros encontrados.',
type: PartnerDto,
isArray: true
isArray: true,
})
async findPartners(@Param('filter') filter: string): Promise<PartnerDto[]> {
return this.partnersService.findPartners(filter);
@@ -28,7 +30,7 @@ export class PartnersController {
status: 200,
description: 'Lista de todos os parceiros.',
type: PartnerDto,
isArray: true
isArray: true,
})
async getAllPartners(): Promise<PartnerDto[]> {
return this.partnersService.getAllPartners();
@@ -40,7 +42,7 @@ export class PartnersController {
@ApiResponse({
status: 200,
description: 'Parceiro encontrado.',
type: PartnerDto
type: PartnerDto,
})
async getPartnerById(@Param('id') id: string): Promise<PartnerDto | null> {
return this.partnersService.getPartnerById(id);

View File

@@ -8,7 +8,7 @@ import { PartnerDto } from './dto/partner.dto';
@Injectable()
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';
constructor(
@@ -18,11 +18,6 @@ export class PartnersService {
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[]> {
const cacheKey = `${this.PARTNERS_CACHE_KEY}:${filter}`;
@@ -35,7 +30,6 @@ export class PartnersService {
await queryRunner.connect();
try {
// Primeira tentativa: busca por ID do parceiro
let sql = `SELECT ESTPARCEIRO.ID as "id",
ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME ||
' ( ' || ESTPARCEIRO.CPF || ' )' as "name",
@@ -45,7 +39,6 @@ export class PartnersService {
ORDER BY ESTPARCEIRO.NOME`;
let partners = await queryRunner.manager.query(sql);
// Segunda tentativa: busca por CPF se não encontrou por ID
if (partners.length === 0) {
sql = `SELECT ESTPARCEIRO.ID as "id",
ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME ||
@@ -57,34 +50,34 @@ export class PartnersService {
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) {
sql = `SELECT ESTPARCEIRO.ID as "id",
ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME ||
' ( ' || ESTPARCEIRO.CPF || ' )' as "name",
ESTPARCEIRO.CPF as "cpf"
FROM ESTPARCEIRO
WHERE ESTPARCEIRO.NOME LIKE '${filter.toUpperCase().replace('@', '%')}%'
WHERE ESTPARCEIRO.NOME LIKE '${filter
.toUpperCase()
.replace('@', '%')}%'
ORDER BY ESTPARCEIRO.NOME`;
partners = await queryRunner.manager.query(sql);
}
return partners.map(partner => new PartnerDto({
return partners.map(
(partner) =>
new PartnerDto({
id: partner.id,
cpf: partner.cpf,
nome: partner.name
}));
nome: partner.name,
}),
);
} finally {
await queryRunner.release();
}
}
},
);
}
/**
* Buscar todos os parceiros com cache
* @returns Array de todos os parceiros
*/
async getAllPartners(): Promise<PartnerDto[]> {
const cacheKey = 'parceiros:all';
@@ -105,23 +98,21 @@ export class PartnersService {
ORDER BY ESTPARCEIRO.NOME`;
const partners = await queryRunner.manager.query(sql);
return partners.map(partner => new PartnerDto({
return partners.map(
(partner) =>
new PartnerDto({
id: partner.id,
cpf: partner.cpf,
nome: partner.name
}));
nome: partner.name,
}),
);
} finally {
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> {
const cacheKey = `parceiros:id:${partnerId}`;
@@ -142,27 +133,17 @@ export class PartnersService {
WHERE ESTPARCEIRO.ID = '${partnerId}'`;
const partners = await queryRunner.manager.query(sql);
return partners.length > 0 ? new PartnerDto({
return partners.length > 0
? new PartnerDto({
id: partners[0].id,
cpf: partners[0].cpf,
nome: partners[0].name
}) : null;
nome: partners[0].name,
})
: null;
} finally {
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}`);
}
}

View File

@@ -9,4 +9,3 @@ export class ProductValidationDto {
qtdeEstoqueLoja: number;
qtdeEstoqueCD: number;
}

View File

@@ -30,4 +30,3 @@ export class ProductDetailQueryDto {
@IsNotEmpty()
codfilial: string;
}

View File

@@ -36,7 +36,7 @@ export class ProductDetailResponseDto {
@ApiProperty({
description: 'Preço de venda do produto',
example: 99.90,
example: 99.9,
})
preco: number;
@@ -52,4 +52,3 @@ export class ProductDetailResponseDto {
})
regiao: string;
}

View File

@@ -1,8 +1,6 @@
export class ProductEcommerceDto {
productIdErp: number;
productId: number;
price: number;
priceKit: number;
}

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