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

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

View File

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

View File

@@ -1,26 +0,0 @@
import { Logger } from '@nestjs/common';
import { ILogger } from './ILogger';
export class NestLoggerAdapter implements ILogger {
private readonly logger: Logger;
constructor(private readonly context: string) {
this.logger = new Logger(context);
}
log(message: string, meta?: Record<string, any>): void {
this.logger.log(this.formatMessage(message, meta));
}
warn(message: string, meta?: Record<string, any>): void {
this.logger.warn(this.formatMessage(message, meta));
}
error(message: string, trace?: string, meta?: Record<string, any>): void {
this.logger.error(this.formatMessage(message, meta), trace);
}
private formatMessage(message: string, meta?: Record<string, any>): string {
return meta ? `${message} | ${JSON.stringify(meta)}` : message;
}
}

View File

@@ -1,22 +0,0 @@
import { ILogger } from './ILogger';
export function LogExecution(label?: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
const logger: ILogger = this.logger;
const context = label || `${target.constructor.name}.${propertyKey}`;
const start = Date.now();
logger.log(`Iniciando: ${context} | ${JSON.stringify({ args })}`);
const result = await original.apply(this, args);
const duration = Date.now() - start;
logger.log(`Finalizado: ${context} em ${duration}ms`);
return result;
};
return descriptor;
};
}

View File

@@ -1,14 +0,0 @@
import { Module } from '@nestjs/common';
import { NestLoggerAdapter } from './NestLoggerAdapter';
import { ILogger } from './ILogger';
@Module({
providers: [
{
provide: 'LoggerService',
useFactory: () => new NestLoggerAdapter('DataConsultService'),
},
],
exports: ['LoggerService'],
})
export class LoggerModule {}

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,9 @@ describe('AuthService - createTokenPair', () => {
beforeEach(() => { beforeEach(() => {
context.mockJwtService.sign.mockReturnValue('mock.access.token'); context.mockJwtService.sign.mockReturnValue('mock.access.token');
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue('mock.refresh.token'); context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
'mock.refresh.token',
);
}); });
it('should handle error when createToken fails after refresh token is generated', async () => { it('should handle error when createToken fails after refresh token is generated', async () => {
@@ -39,10 +41,19 @@ describe('AuthService - createTokenPair', () => {
}); });
await expect( await expect(
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-123') context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
'session-123',
),
).rejects.toThrow(); ).rejects.toThrow();
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled(); expect(
context.mockRefreshTokenService.generateRefreshToken,
).not.toHaveBeenCalled();
}); });
it('should rollback access token if refresh token generation fails', async () => { it('should rollback access token if refresh token generation fails', async () => {
@@ -52,11 +63,18 @@ describe('AuthService - createTokenPair', () => {
* Solução esperada: Invalidar o access token ou garantir atomicidade. * Solução esperada: Invalidar o access token ou garantir atomicidade.
*/ */
context.mockRefreshTokenService.generateRefreshToken.mockRejectedValueOnce( context.mockRefreshTokenService.generateRefreshToken.mockRejectedValueOnce(
new Error('Falha ao gerar refresh token') new Error('Falha ao gerar refresh token'),
); );
await expect( await expect(
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-123') context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
'session-123',
),
).rejects.toThrow('Falha ao gerar refresh token'); ).rejects.toThrow('Falha ao gerar refresh token');
}); });
@@ -69,7 +87,13 @@ describe('AuthService - createTokenPair', () => {
context.mockJwtService.sign.mockReturnValue(''); context.mockJwtService.sign.mockReturnValue('');
await expect( await expect(
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001') context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
),
).rejects.toThrow('Token de acesso inválido gerado'); ).rejects.toThrow('Token de acesso inválido gerado');
}); });
@@ -79,18 +103,34 @@ describe('AuthService - createTokenPair', () => {
* Problema: Método não valida o retorno. * Problema: Método não valida o retorno.
* Solução esperada: Lançar exceção se token for inválido. * Solução esperada: Lançar exceção se token for inválido.
*/ */
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(''); context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
'',
);
await expect( await expect(
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001') context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
),
).rejects.toThrow('Refresh token inválido gerado'); ).rejects.toThrow('Refresh token inválido gerado');
}); });
it('should validate that refresh token is not null', async () => { it('should validate that refresh token is not null', async () => {
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(null); context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
null,
);
await expect( await expect(
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001') context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
),
).rejects.toThrow('Refresh token inválido gerado'); ).rejects.toThrow('Refresh token inválido gerado');
}); });
@@ -107,12 +147,20 @@ describe('AuthService - createTokenPair', () => {
return 'mock.access.token'; return 'mock.access.token';
}); });
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(async () => { context.mockRefreshTokenService.generateRefreshToken.mockImplementation(
callOrder.push('refreshToken'); async () => {
return 'mock.refresh.token'; callOrder.push('refreshToken');
}); return 'mock.refresh.token';
},
);
await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001'); await context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
);
expect(callOrder).toEqual(['accessToken', 'refreshToken']); expect(callOrder).toEqual(['accessToken', 'refreshToken']);
}); });
@@ -123,7 +171,13 @@ describe('AuthService - createTokenPair', () => {
* Problema: Cliente pode não saber quando renovar o token. * Problema: Cliente pode não saber quando renovar o token.
* Solução esperada: Sempre retornar um número positivo válido. * Solução esperada: Sempre retornar um número positivo válido.
*/ */
const result = await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001'); const result = await context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
);
expect(result.expiresIn).toBeGreaterThan(0); expect(result.expiresIn).toBeGreaterThan(0);
expect(typeof result.expiresIn).toBe('number'); expect(typeof result.expiresIn).toBe('number');
@@ -145,19 +199,42 @@ describe('AuthService - createTokenPair', () => {
return `mock.access.token.${callCount}`; return `mock.access.token.${callCount}`;
}); });
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(async () => { context.mockRefreshTokenService.generateRefreshToken.mockImplementation(
return `mock.refresh.token.${Math.random()}`; async () => {
}); return `mock.refresh.token.${Math.random()}`;
},
);
const promises = [ const promises = [
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-1'), context.service.createTokenPair(
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-2'), 1,
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-3'), 100,
'test',
'test@test.com',
'STORE001',
'session-1',
),
context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
'session-2',
),
context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
'session-3',
),
]; ];
const results = await Promise.all(promises); const results = await Promise.all(promises);
const uniqueTokens = new Set(results.map(r => r.accessToken)); const uniqueTokens = new Set(results.map((r) => r.accessToken));
expect(uniqueTokens.size).toBe(3); expect(uniqueTokens.size).toBe(3);
}); });
@@ -168,10 +245,18 @@ describe('AuthService - createTokenPair', () => {
* Solução esperada: Falhar rápido com mensagem clara. * Solução esperada: Falhar rápido com mensagem clara.
*/ */
await expect( await expect(
context.service.createTokenPair(-1, 100, 'test', 'test@test.com', 'STORE001') context.service.createTokenPair(
-1,
100,
'test',
'test@test.com',
'STORE001',
),
).rejects.toThrow('ID de usuário inválido'); ).rejects.toThrow('ID de usuário inválido');
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled(); expect(
context.mockRefreshTokenService.generateRefreshToken,
).not.toHaveBeenCalled();
}); });
it('should not create refresh token if validation fails', async () => { it('should not create refresh token if validation fails', async () => {
@@ -181,11 +266,19 @@ describe('AuthService - createTokenPair', () => {
* Solução esperada: Validar tudo antes de criar qualquer token. * Solução esperada: Validar tudo antes de criar qualquer token.
*/ */
await expect( await expect(
context.service.createTokenPair(1, -1, 'test', 'test@test.com', 'STORE001') context.service.createTokenPair(
1,
-1,
'test',
'test@test.com',
'STORE001',
),
).rejects.toThrow('ID de vendedor inválido'); ).rejects.toThrow('ID de vendedor inválido');
expect(context.mockJwtService.sign).not.toHaveBeenCalled(); expect(context.mockJwtService.sign).not.toHaveBeenCalled();
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled(); expect(
context.mockRefreshTokenService.generateRefreshToken,
).not.toHaveBeenCalled();
}); });
it('should handle undefined sessionId gracefully', async () => { it('should handle undefined sessionId gracefully', async () => {
@@ -194,11 +287,19 @@ describe('AuthService - createTokenPair', () => {
* Problema: Pode causar problemas ao gerar tokens sem session. * Problema: Pode causar problemas ao gerar tokens sem session.
* Solução esperada: Aceitar undefined e passar corretamente aos serviços. * Solução esperada: Aceitar undefined e passar corretamente aos serviços.
*/ */
const result = await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001'); const result = await context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
);
expect(result.accessToken).toBeDefined(); expect(result.accessToken).toBeDefined();
expect(result.refreshToken).toBeDefined(); expect(result.refreshToken).toBeDefined();
expect(context.mockRefreshTokenService.generateRefreshToken).toHaveBeenCalledWith(1, undefined); expect(
context.mockRefreshTokenService.generateRefreshToken,
).toHaveBeenCalledWith(1, undefined);
}); });
it('should include all required fields in return object', async () => { it('should include all required fields in return object', async () => {
@@ -207,7 +308,13 @@ describe('AuthService - createTokenPair', () => {
* Problema: Pode faltar campos ou ter campos extras. * Problema: Pode faltar campos ou ter campos extras.
* Solução esperada: Sempre retornar accessToken, refreshToken e expiresIn. * Solução esperada: Sempre retornar accessToken, refreshToken e expiresIn.
*/ */
const result = await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001'); const result = await context.service.createTokenPair(
1,
100,
'test',
'test@test.com',
'STORE001',
);
expect(result).toHaveProperty('accessToken'); expect(result).toHaveProperty('accessToken');
expect(result).toHaveProperty('refreshToken'); expect(result).toHaveProperty('refreshToken');
@@ -216,4 +323,3 @@ describe('AuthService - createTokenPair', () => {
}); });
}); });
}); });

View File

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

View File

@@ -19,7 +19,9 @@ describe('AuthService - refreshAccessToken', () => {
situacao: 'A', situacao: 'A',
dataDesligamento: null, dataDesligamento: null,
}); });
context.mockSessionManagementService.isSessionActive.mockResolvedValue(true); context.mockSessionManagementService.isSessionActive.mockResolvedValue(
true,
);
}); });
afterEach(() => { afterEach(() => {
@@ -40,35 +42,43 @@ describe('AuthService - refreshAccessToken', () => {
*/ */
it('should reject empty refresh token', async () => { it('should reject empty refresh token', async () => {
await expect( await expect(context.service.refreshAccessToken('')).rejects.toThrow(
context.service.refreshAccessToken('') 'Refresh token não pode estar vazio',
).rejects.toThrow('Refresh token não pode estar vazio'); );
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled(); expect(
context.mockRefreshTokenService.validateRefreshToken,
).not.toHaveBeenCalled();
}); });
it('should reject null refresh token', async () => { it('should reject null refresh token', async () => {
await expect( await expect(
context.service.refreshAccessToken(null as any) context.service.refreshAccessToken(null as any),
).rejects.toThrow('Refresh token não pode estar vazio'); ).rejects.toThrow('Refresh token não pode estar vazio');
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled(); expect(
context.mockRefreshTokenService.validateRefreshToken,
).not.toHaveBeenCalled();
}); });
it('should reject undefined refresh token', async () => { it('should reject undefined refresh token', async () => {
await expect( await expect(
context.service.refreshAccessToken(undefined as any) context.service.refreshAccessToken(undefined as any),
).rejects.toThrow('Refresh token não pode estar vazio'); ).rejects.toThrow('Refresh token não pode estar vazio');
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled(); expect(
context.mockRefreshTokenService.validateRefreshToken,
).not.toHaveBeenCalled();
}); });
it('should reject whitespace-only refresh token', async () => { it('should reject whitespace-only refresh token', async () => {
await expect( await expect(context.service.refreshAccessToken(' ')).rejects.toThrow(
context.service.refreshAccessToken(' ') 'Refresh token não pode estar vazio',
).rejects.toThrow('Refresh token não pode estar vazio'); );
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled(); expect(
context.mockRefreshTokenService.validateRefreshToken,
).not.toHaveBeenCalled();
}); });
it('should validate tokenData has required id field', async () => { it('should validate tokenData has required id field', async () => {
@@ -77,15 +87,17 @@ describe('AuthService - refreshAccessToken', () => {
} as any); } as any);
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do refresh token inválidos'); ).rejects.toThrow('Dados do refresh token inválidos');
}); });
it('should validate tokenData is not null', async () => { it('should validate tokenData is not null', async () => {
context.mockRefreshTokenService.validateRefreshToken.mockResolvedValue(null); context.mockRefreshTokenService.validateRefreshToken.mockResolvedValue(
null,
);
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do refresh token inválidos'); ).rejects.toThrow('Dados do refresh token inválidos');
}); });
@@ -101,7 +113,7 @@ describe('AuthService - refreshAccessToken', () => {
}); });
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do usuário incompletos'); ).rejects.toThrow('Dados do usuário incompletos');
}); });
@@ -117,7 +129,7 @@ describe('AuthService - refreshAccessToken', () => {
}); });
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do usuário incompletos'); ).rejects.toThrow('Dados do usuário incompletos');
}); });
@@ -133,7 +145,7 @@ describe('AuthService - refreshAccessToken', () => {
}); });
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Dados do usuário incompletos'); ).rejects.toThrow('Dados do usuário incompletos');
}); });
@@ -141,7 +153,7 @@ describe('AuthService - refreshAccessToken', () => {
context.mockJwtService.sign.mockReturnValue(''); context.mockJwtService.sign.mockReturnValue('');
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Falha ao gerar novo token de acesso'); ).rejects.toThrow('Falha ao gerar novo token de acesso');
}); });
@@ -149,7 +161,7 @@ describe('AuthService - refreshAccessToken', () => {
context.mockJwtService.sign.mockReturnValue(null as any); context.mockJwtService.sign.mockReturnValue(null as any);
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Falha ao gerar novo token de acesso'); ).rejects.toThrow('Falha ao gerar novo token de acesso');
}); });
@@ -159,10 +171,12 @@ describe('AuthService - refreshAccessToken', () => {
sessionId: 'expired-session', sessionId: 'expired-session',
}); });
context.mockSessionManagementService.isSessionActive = jest.fn().mockResolvedValue(false); context.mockSessionManagementService.isSessionActive = jest
.fn()
.mockResolvedValue(false);
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Sessão não está mais ativa'); ).rejects.toThrow('Sessão não está mais ativa');
}); });
@@ -178,7 +192,7 @@ describe('AuthService - refreshAccessToken', () => {
}); });
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('ID de vendedor inválido'); ).rejects.toThrow('ID de vendedor inválido');
}); });
@@ -186,24 +200,30 @@ describe('AuthService - refreshAccessToken', () => {
const hugeToken = 'a'.repeat(100000); const hugeToken = 'a'.repeat(100000);
await expect( await expect(
context.service.refreshAccessToken(hugeToken) context.service.refreshAccessToken(hugeToken),
).rejects.toThrow('Refresh token muito longo'); ).rejects.toThrow('Refresh token muito longo');
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled(); expect(
context.mockRefreshTokenService.validateRefreshToken,
).not.toHaveBeenCalled();
}); });
it('should sanitize refresh token input', async () => { it('should sanitize refresh token input', async () => {
const maliciousToken = "'; DROP TABLE users; --"; const maliciousToken = "'; DROP TABLE users; --";
await expect( await expect(
context.service.refreshAccessToken(maliciousToken) context.service.refreshAccessToken(maliciousToken),
).rejects.toThrow('Formato de refresh token inválido'); ).rejects.toThrow('Formato de refresh token inválido');
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled(); expect(
context.mockRefreshTokenService.validateRefreshToken,
).not.toHaveBeenCalled();
}); });
it('should include only required fields in response', async () => { it('should include only required fields in response', async () => {
const result = await context.service.refreshAccessToken('valid.refresh.token'); const result = await context.service.refreshAccessToken(
'valid.refresh.token',
);
expect(result).toHaveProperty('accessToken'); expect(result).toHaveProperty('accessToken');
expect(result).toHaveProperty('expiresIn'); expect(result).toHaveProperty('expiresIn');
@@ -213,7 +233,9 @@ describe('AuthService - refreshAccessToken', () => {
}); });
it('should validate expiresIn is correct', async () => { it('should validate expiresIn is correct', async () => {
const result = await context.service.refreshAccessToken('valid.refresh.token'); const result = await context.service.refreshAccessToken(
'valid.refresh.token',
);
expect(result.expiresIn).toBe(28800); expect(result.expiresIn).toBe(28800);
expect(result.expiresIn).toBeGreaterThan(0); expect(result.expiresIn).toBeGreaterThan(0);
@@ -231,7 +253,7 @@ describe('AuthService - refreshAccessToken', () => {
}); });
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow(); ).rejects.toThrow();
}); });
@@ -244,7 +266,7 @@ describe('AuthService - refreshAccessToken', () => {
const results = await Promise.all(promises); const results = await Promise.all(promises);
results.forEach(result => { results.forEach((result) => {
expect(result).toHaveProperty('accessToken'); expect(result).toHaveProperty('accessToken');
expect(result.accessToken).toBeTruthy(); expect(result.accessToken).toBeTruthy();
}); });
@@ -262,9 +284,8 @@ describe('AuthService - refreshAccessToken', () => {
}); });
await expect( await expect(
context.service.refreshAccessToken('valid.refresh.token') context.service.refreshAccessToken('valid.refresh.token'),
).rejects.toThrow('Usuário inválido ou inativo'); ).rejects.toThrow('Usuário inválido ou inativo');
}); });
}); });
}); });

View File

@@ -24,14 +24,17 @@ import { RateLimitingGuard } from '../guards/rate-limiting.guard';
import { RateLimitingService } from '../services/rate-limiting.service'; import { RateLimitingService } from '../services/rate-limiting.service';
import { RefreshTokenService } from '../services/refresh-token.service'; import { RefreshTokenService } from '../services/refresh-token.service';
import { SessionManagementService } from '../services/session-management.service'; import { SessionManagementService } from '../services/session-management.service';
import { RefreshTokenDto, RefreshTokenResponseDto } from './dto/refresh-token.dto'; import {
RefreshTokenDto,
RefreshTokenResponseDto,
} from './dto/refresh-token.dto';
import { SessionsResponseDto } from './dto/session.dto'; import { SessionsResponseDto } from './dto/session.dto';
import { LoginAuditService } from '../services/login-audit.service'; import { LoginAuditService } from '../services/login-audit.service';
import { import {
LoginAuditFiltersDto, LoginAuditFiltersDto,
LoginAuditResponseDto, LoginAuditResponseDto,
LoginStatsDto, LoginStatsDto,
LoginStatsFiltersDto LoginStatsFiltersDto,
} from './dto/login-audit.dto'; } from './dto/login-audit.dto';
import { import {
ApiTags, ApiTags,
@@ -66,7 +69,10 @@ export class AuthController {
}) })
@ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' }) @ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' })
@ApiTooManyRequestsResponse({ description: 'Muitas tentativas de login' }) @ApiTooManyRequestsResponse({ description: 'Muitas tentativas de login' })
async login(@Body() dto: LoginDto, @Request() req): Promise<LoginResponseDto> { async login(
@Body() dto: LoginDto,
@Request() req,
): Promise<LoginResponseDto> {
const ip = this.getClientIp(req); const ip = this.getClientIp(req);
const command = new AuthenticateUserCommand(dto.username, dto.password); const command = new AuthenticateUserCommand(dto.username, dto.password);
@@ -98,13 +104,17 @@ export class AuthController {
/** /**
* Verifica se o usuário já possui uma sessão ativa * Verifica se o usuário já possui uma sessão ativa
*/ */
const existingSession = await this.sessionManagementService.hasActiveSession(user.id); const existingSession =
await this.sessionManagementService.hasActiveSession(user.id);
if (existingSession) { if (existingSession) {
/** /**
* Encerra a sessão existente antes de criar uma nova * Encerra a sessão existente antes de criar uma nova
*/ */
await this.sessionManagementService.terminateSession(user.id, existingSession.sessionId); await this.sessionManagementService.terminateSession(
user.id,
existingSession.sessionId,
);
} }
const session = await this.sessionManagementService.createSession( const session = await this.sessionManagementService.createSession(
@@ -161,7 +171,6 @@ export class AuthController {
); );
} }
@Post('logout') @Post('logout')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@@ -173,7 +182,12 @@ export class AuthController {
if (!token) { if (!token) {
throw new HttpException( throw new HttpException(
new ResultModel(false, 'Token não fornecido', null, 'Token não fornecido'), new ResultModel(
false,
'Token não fornecido',
null,
'Token não fornecido',
),
HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED,
); );
} }
@@ -192,8 +206,12 @@ export class AuthController {
description: 'Token renovado com sucesso', description: 'Token renovado com sucesso',
type: RefreshTokenResponseDto, type: RefreshTokenResponseDto,
}) })
@ApiUnauthorizedResponse({ description: 'Refresh token inválido ou expirado' }) @ApiUnauthorizedResponse({
async refreshToken(@Body() dto: RefreshTokenDto): Promise<RefreshTokenResponseDto> { description: 'Refresh token inválido ou expirado',
})
async refreshToken(
@Body() dto: RefreshTokenDto,
): Promise<RefreshTokenResponseDto> {
const result = await this.authService.refreshAccessToken(dto.refreshToken); const result = await this.authService.refreshAccessToken(dto.refreshToken);
return result; return result;
} }
@@ -210,15 +228,20 @@ export class AuthController {
async getSessions(@Request() req): Promise<SessionsResponseDto> { async getSessions(@Request() req): Promise<SessionsResponseDto> {
const userId = req.user.id; const userId = req.user.id;
const currentSessionId = req.user.sessionId; const currentSessionId = req.user.sessionId;
const sessions = await this.sessionManagementService.getActiveSessions(userId, currentSessionId); const sessions = await this.sessionManagementService.getActiveSessions(
userId,
currentSessionId,
);
return { return {
sessions: sessions.map(session => ({ sessions: sessions.map((session) => ({
sessionId: session.sessionId, sessionId: session.sessionId,
ipAddress: session.ipAddress, ipAddress: session.ipAddress,
userAgent: session.userAgent, userAgent: session.userAgent,
createdAt: DateUtil.toBrazilISOString(new Date(session.createdAt)), createdAt: DateUtil.toBrazilISOString(new Date(session.createdAt)),
lastActivity: DateUtil.toBrazilISOString(new Date(session.lastActivity)), lastActivity: DateUtil.toBrazilISOString(
new Date(session.lastActivity),
),
isCurrent: session.sessionId === currentSessionId, isCurrent: session.sessionId === currentSessionId,
})), })),
total: sessions.length, total: sessions.length,
@@ -284,7 +307,7 @@ export class AuthController {
const logs = await this.loginAuditService.getLoginLogs(auditFilters); const logs = await this.loginAuditService.getLoginLogs(auditFilters);
return { return {
logs: logs.map(log => ({ logs: logs.map((log) => ({
...log, ...log,
timestamp: DateUtil.toBrazilISOString(log.timestamp), timestamp: DateUtil.toBrazilISOString(log.timestamp),
})), })),
@@ -333,13 +356,12 @@ export class AuthController {
ipAddress: { type: 'string' }, ipAddress: { type: 'string' },
userAgent: { type: 'string' }, userAgent: { type: 'string' },
createdAt: { type: 'string' }, createdAt: { type: 'string' },
lastActivity: { type: 'string' } lastActivity: { type: 'string' },
} },
} },
} },
} },
}) })
@Get('session/status') @Get('session/status')
async checkSessionStatus(@Query('username') username: string): Promise<{ async checkSessionStatus(@Query('username') username: string): Promise<{
hasActiveSession: boolean; hasActiveSession: boolean;
@@ -353,7 +375,12 @@ export class AuthController {
}> { }> {
if (!username) { if (!username) {
throw new HttpException( throw new HttpException(
new ResultModel(false, 'Username é obrigatório', null, 'Username é obrigatório'), new ResultModel(
false,
'Username é obrigatório',
null,
'Username é obrigatório',
),
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
); );
} }
@@ -369,7 +396,9 @@ export class AuthController {
}; };
} }
const activeSession = await this.sessionManagementService.hasActiveSession(user.id); const activeSession = await this.sessionManagementService.hasActiveSession(
user.id,
);
if (!activeSession) { if (!activeSession) {
return { return {
@@ -383,8 +412,12 @@ export class AuthController {
sessionId: activeSession.sessionId, sessionId: activeSession.sessionId,
ipAddress: activeSession.ipAddress, ipAddress: activeSession.ipAddress,
userAgent: activeSession.userAgent, userAgent: activeSession.userAgent,
createdAt: DateUtil.toBrazilISOString(new Date(activeSession.createdAt)), createdAt: DateUtil.toBrazilISOString(
lastActivity: DateUtil.toBrazilISOString(new Date(activeSession.lastActivity)), new Date(activeSession.createdAt),
),
lastActivity: DateUtil.toBrazilISOString(
new Date(activeSession.lastActivity),
),
}, },
}; };
} }

View File

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

View File

@@ -1,4 +1,8 @@
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common'; import {
Injectable,
UnauthorizedException,
BadRequestException,
} from '@nestjs/common';
import { JwtService, JwtSignOptions } from '@nestjs/jwt'; import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { UsersService } from '../users/users.service'; import { UsersService } from '../users/users.service';
import { JwtPayload } from '../models/jwt-payload.model'; import { JwtPayload } from '../models/jwt-payload.model';
@@ -7,7 +11,6 @@ import { TokenBlacklistService } from '../services/token-blacklist.service';
import { RefreshTokenService } from '../services/refresh-token.service'; import { RefreshTokenService } from '../services/refresh-token.service';
import { SessionManagementService } from '../services/session-management.service'; import { SessionManagementService } from '../services/session-management.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor( constructor(
@@ -23,7 +26,14 @@ export class AuthService {
* Cria um token JWT com validação de todos os parâmetros de entrada * Cria um token JWT com validação de todos os parâmetros de entrada
* @throws BadRequestException quando os parâmetros são inválidos * @throws BadRequestException quando os parâmetros são inválidos
*/ */
async createToken(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) { async createToken(
id: number,
sellerId: number,
username: string,
email: string,
storeId: string,
sessionId?: string,
) {
this.validateTokenParameters(id, sellerId, username, email, storeId); this.validateTokenParameters(id, sellerId, username, email, storeId);
const user: JwtPayload = { const user: JwtPayload = {
@@ -42,7 +52,13 @@ export class AuthService {
* Valida os parâmetros de entrada para criação de token * Valida os parâmetros de entrada para criação de token
* @private * @private
*/ */
private validateTokenParameters(id: number, sellerId: number, username: string, email: string, storeId: string): void { private validateTokenParameters(
id: number,
sellerId: number,
username: string,
email: string,
storeId: string,
): void {
if (!id || id <= 0) { if (!id || id <= 0) {
throw new BadRequestException('ID de usuário inválido'); throw new BadRequestException('ID de usuário inválido');
} }
@@ -64,7 +80,9 @@ export class AuthService {
} }
if (/['";\\]/.test(username)) { if (/['";\\]/.test(username)) {
throw new BadRequestException('Nome de usuário contém caracteres inválidos'); throw new BadRequestException(
'Nome de usuário contém caracteres inválidos',
);
} }
if (!email || typeof email !== 'string' || !email.trim()) { if (!email || typeof email !== 'string' || !email.trim()) {
@@ -92,16 +110,41 @@ export class AuthService {
* @throws BadRequestException quando os parâmetros são inválidos * @throws BadRequestException quando os parâmetros são inválidos
* @throws Error quando os tokens gerados são inválidos * @throws Error quando os tokens gerados são inválidos
*/ */
async createTokenPair(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) { async createTokenPair(
const accessToken = await this.createToken(id, sellerId, username, email, storeId, sessionId); id: number,
sellerId: number,
username: string,
email: string,
storeId: string,
sessionId?: string,
) {
const accessToken = await this.createToken(
id,
sellerId,
username,
email,
storeId,
sessionId,
);
if (!accessToken || typeof accessToken !== 'string' || !accessToken.trim()) { if (
!accessToken ||
typeof accessToken !== 'string' ||
!accessToken.trim()
) {
throw new Error('Token de acesso inválido gerado'); throw new Error('Token de acesso inválido gerado');
} }
const refreshToken = await this.refreshTokenService.generateRefreshToken(id, sessionId); const refreshToken = await this.refreshTokenService.generateRefreshToken(
id,
sessionId,
);
if (!refreshToken || typeof refreshToken !== 'string' || !refreshToken.trim()) { if (
!refreshToken ||
typeof refreshToken !== 'string' ||
!refreshToken.trim()
) {
throw new Error('Refresh token inválido gerado'); throw new Error('Refresh token inválido gerado');
} }
@@ -121,7 +164,9 @@ export class AuthService {
async refreshAccessToken(refreshToken: string) { async refreshAccessToken(refreshToken: string) {
this.validateRefreshTokenInput(refreshToken); this.validateRefreshTokenInput(refreshToken);
const tokenData = await this.refreshTokenService.validateRefreshToken(refreshToken); const tokenData = await this.refreshTokenService.validateRefreshToken(
refreshToken,
);
if (!tokenData || !tokenData.id) { if (!tokenData || !tokenData.id) {
throw new BadRequestException('Dados do refresh token inválidos'); throw new BadRequestException('Dados do refresh token inválidos');
@@ -135,10 +180,11 @@ export class AuthService {
this.validateUserDataForToken(user); this.validateUserDataForToken(user);
if (tokenData.sessionId) { if (tokenData.sessionId) {
const isSessionActive = await this.sessionManagementService.isSessionActive( const isSessionActive =
user.id, await this.sessionManagementService.isSessionActive(
tokenData.sessionId user.id,
); tokenData.sessionId,
);
if (!isSessionActive) { if (!isSessionActive) {
throw new UnauthorizedException('Sessão não está mais ativa'); throw new UnauthorizedException('Sessão não está mais ativa');
} }
@@ -150,10 +196,14 @@ export class AuthService {
user.name, user.name,
user.email, user.email,
user.storeId, user.storeId,
tokenData.sessionId tokenData.sessionId,
); );
if (!newAccessToken || typeof newAccessToken !== 'string' || !newAccessToken.trim()) { if (
!newAccessToken ||
typeof newAccessToken !== 'string' ||
!newAccessToken.trim()
) {
throw new Error('Falha ao gerar novo token de acesso'); throw new Error('Falha ao gerar novo token de acesso');
} }
@@ -168,7 +218,11 @@ export class AuthService {
* @private * @private
*/ */
private validateRefreshTokenInput(refreshToken: string): void { private validateRefreshTokenInput(refreshToken: string): void {
if (!refreshToken || typeof refreshToken !== 'string' || !refreshToken.trim()) { if (
!refreshToken ||
typeof refreshToken !== 'string' ||
!refreshToken.trim()
) {
throw new BadRequestException('Refresh token não pode estar vazio'); throw new BadRequestException('Refresh token não pode estar vazio');
} }
@@ -187,18 +241,32 @@ export class AuthService {
*/ */
private validateUserDataForToken(user: any): void { private validateUserDataForToken(user: any): void {
if (!user.name || typeof user.name !== 'string' || !user.name.trim()) { if (!user.name || typeof user.name !== 'string' || !user.name.trim()) {
throw new BadRequestException('Dados do usuário incompletos: nome não encontrado'); throw new BadRequestException(
'Dados do usuário incompletos: nome não encontrado',
);
} }
if (!user.email || typeof user.email !== 'string' || !user.email.trim()) { if (!user.email || typeof user.email !== 'string' || !user.email.trim()) {
throw new BadRequestException('Dados do usuário incompletos: email não encontrado'); throw new BadRequestException(
'Dados do usuário incompletos: email não encontrado',
);
} }
if (!user.storeId || typeof user.storeId !== 'string' || !user.storeId.trim()) { if (
throw new BadRequestException('Dados do usuário incompletos: storeId não encontrado'); !user.storeId ||
typeof user.storeId !== 'string' ||
!user.storeId.trim()
) {
throw new BadRequestException(
'Dados do usuário incompletos: storeId não encontrado',
);
} }
if (user.sellerId !== null && user.sellerId !== undefined && user.sellerId < 0) { if (
user.sellerId !== null &&
user.sellerId !== undefined &&
user.sellerId < 0
) {
throw new BadRequestException('ID de vendedor inválido'); throw new BadRequestException('ID de vendedor inválido');
} }
} }
@@ -228,11 +296,15 @@ export class AuthService {
try { try {
decoded = this.jwtService.decode(token) as JwtPayload; decoded = this.jwtService.decode(token) as JwtPayload;
} catch (error) { } catch (error) {
throw new BadRequestException('Token inválido ou não pode ser decodificado'); throw new BadRequestException(
'Token inválido ou não pode ser decodificado',
);
} }
if (!decoded || !decoded.id) { if (!decoded || !decoded.id) {
throw new BadRequestException('Token inválido ou não pode ser decodificado'); throw new BadRequestException(
'Token inválido ou não pode ser decodificado',
);
} }
if (decoded.id <= 0) { if (decoded.id <= 0) {
@@ -241,25 +313,34 @@ export class AuthService {
if (decoded.sessionId && decoded.id && decoded.sessionId.trim()) { if (decoded.sessionId && decoded.id && decoded.sessionId.trim()) {
try { try {
await this.sessionManagementService.terminateSession(decoded.id, decoded.sessionId); await this.sessionManagementService.terminateSession(
decoded.id,
decoded.sessionId,
);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage =
error instanceof Error ? error.message : String(error);
if (errorMessage.includes('Sessão não encontrada')) { if (errorMessage.includes('Sessão não encontrada')) {
throw new Error('Sessão não encontrada'); throw new Error('Sessão não encontrada');
} }
} }
} }
const isAlreadyBlacklisted = await this.tokenBlacklistService.isBlacklisted(token); const isAlreadyBlacklisted = await this.tokenBlacklistService.isBlacklisted(
token,
);
if (!isAlreadyBlacklisted) { if (!isAlreadyBlacklisted) {
try { try {
await this.tokenBlacklistService.addToBlacklist(token); await this.tokenBlacklistService.addToBlacklist(token);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage =
error instanceof Error ? error.message : String(error);
if (errorMessage.includes('já está na blacklist')) { if (errorMessage.includes('já está na blacklist')) {
return; return;
} }
throw new Error(`Falha ao adicionar token à blacklist: ${errorMessage}`); throw new Error(
`Falha ao adicionar token à blacklist: ${errorMessage}`,
);
} }
} }
} }

View File

@@ -1,7 +1,6 @@
export class AuthenticateUserCommand { export class AuthenticateUserCommand {
constructor( constructor(
public readonly username: string, public readonly username: string,
public readonly password: string, public readonly password: string,
) {} ) {}
} }

View File

@@ -7,13 +7,18 @@ import { UserModel } from 'src/core/models/user.model';
@CommandHandler(AuthenticateUserCommand) @CommandHandler(AuthenticateUserCommand)
@Injectable() @Injectable()
export class AuthenticateUserHandler implements ICommandHandler<AuthenticateUserCommand> { export class AuthenticateUserHandler
implements ICommandHandler<AuthenticateUserCommand>
{
constructor(private readonly userRepository: UserRepository) {} constructor(private readonly userRepository: UserRepository) {}
async execute(command: AuthenticateUserCommand): Promise<Result<UserModel>> { async execute(command: AuthenticateUserCommand): Promise<Result<UserModel>> {
const { username, password } = command; const { username, password } = command;
const user = await this.userRepository.findByUsernameAndPassword(username, password); const user = await this.userRepository.findByUsernameAndPassword(
username,
password,
);
if (!user) { if (!user) {
return Result.fail('Usuário ou senha inválidos'); return Result.fail('Usuário ou senha inválidos');
@@ -31,7 +36,6 @@ export class AuthenticateUserHandler implements ICommandHandler<AuthenticateUser
return Result.fail('Usuário bloqueado, login não permitido!'); return Result.fail('Usuário bloqueado, login não permitido!');
} }
return Result.ok(user); return Result.ok(user);
} }
} }

View File

@@ -1,5 +1,13 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsNumber, IsString, IsBoolean, IsDateString, Min, Max } from 'class-validator'; import {
IsOptional,
IsNumber,
IsString,
IsBoolean,
IsDateString,
Min,
Max,
} from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
export class LoginAuditFiltersDto { export class LoginAuditFiltersDto {
@@ -19,7 +27,10 @@ export class LoginAuditFiltersDto {
@IsString() @IsString()
ipAddress?: string; ipAddress?: string;
@ApiProperty({ description: 'Filtrar apenas logins bem-sucedidos', required: false }) @ApiProperty({
description: 'Filtrar apenas logins bem-sucedidos',
required: false,
})
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
@Type(() => Boolean) @Type(() => Boolean)
@@ -35,7 +46,12 @@ export class LoginAuditFiltersDto {
@IsDateString() @IsDateString()
endDate?: string; endDate?: string;
@ApiProperty({ description: 'Número de registros por página', required: false, minimum: 1, maximum: 1000 }) @ApiProperty({
description: 'Número de registros por página',
required: false,
minimum: 1,
maximum: 1000,
})
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number) @Type(() => Number)
@@ -43,7 +59,11 @@ export class LoginAuditFiltersDto {
@Max(1000) @Max(1000)
limit?: number; limit?: number;
@ApiProperty({ description: 'Offset para paginação', required: false, minimum: 0 }) @ApiProperty({
description: 'Offset para paginação',
required: false,
minimum: 0,
})
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number) @Type(() => Number)
@@ -84,7 +104,10 @@ export class LoginAuditLogDto {
} }
export class LoginAuditResponseDto { export class LoginAuditResponseDto {
@ApiProperty({ description: 'Lista de logs de login', type: [LoginAuditLogDto] }) @ApiProperty({
description: 'Lista de logs de login',
type: [LoginAuditLogDto],
})
logs: LoginAuditLogDto[]; logs: LoginAuditLogDto[];
@ApiProperty({ description: 'Total de registros encontrados' }) @ApiProperty({ description: 'Total de registros encontrados' })
@@ -123,13 +146,21 @@ export class LoginStatsDto {
} }
export class LoginStatsFiltersDto { export class LoginStatsFiltersDto {
@ApiProperty({ description: 'ID do usuário para estatísticas', required: false }) @ApiProperty({
description: 'ID do usuário para estatísticas',
required: false,
})
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number) @Type(() => Number)
userId?: number; userId?: number;
@ApiProperty({ description: 'Número de dias para análise', required: false, minimum: 1, maximum: 365 }) @ApiProperty({
description: 'Número de dias para análise',
required: false,
minimum: 1,
maximum: 365,
})
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Type(() => Number) @Type(() => Number)

View File

@@ -196,7 +196,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
mockGetRequest.mockReturnValue(request); mockGetRequest.mockReturnValue(request);
mockRateLimitingService.isAllowed.mockRejectedValue( mockRateLimitingService.isAllowed.mockRejectedValue(
new Error('Erro de conexão com Redis') new Error('Erro de conexão com Redis'),
); );
try { try {
@@ -225,7 +225,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
mockGetRequest.mockReturnValue(request); mockGetRequest.mockReturnValue(request);
mockRateLimitingService.isAllowed.mockResolvedValue(false); mockRateLimitingService.isAllowed.mockResolvedValue(false);
mockRateLimitingService.getAttemptInfo.mockRejectedValue( mockRateLimitingService.getAttemptInfo.mockRejectedValue(
new Error('Erro ao buscar informações') new Error('Erro ao buscar informações'),
); );
try { try {
@@ -336,7 +336,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
const result = await guard.canActivate(mockExecutionContext); const result = await guard.canActivate(mockExecutionContext);
expect(result).toBe(true); expect(result).toBe(true);
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('192.168.1.1'); expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
'192.168.1.1',
);
}); });
it('should handle concurrent requests with same IP', async () => { it('should handle concurrent requests with same IP', async () => {
@@ -363,7 +365,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
const results = await Promise.all(promises); const results = await Promise.all(promises);
results.forEach(result => { results.forEach((result) => {
expect(result).toBe(true); expect(result).toBe(true);
}); });
}); });
@@ -394,7 +396,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
fail('Deveria ter lançado exceção'); fail('Deveria ter lançado exceção');
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(HttpException); expect(error).toBeInstanceOf(HttpException);
expect((error as HttpException).getStatus()).toBe(HttpStatus.TOO_MANY_REQUESTS); expect((error as HttpException).getStatus()).toBe(
HttpStatus.TOO_MANY_REQUESTS,
);
} }
}); });
@@ -419,7 +423,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
fail('Deveria ter lançado exceção'); fail('Deveria ter lançado exceção');
} catch (error) { } catch (error) {
const response = (error as HttpException).getResponse() as any; const response = (error as HttpException).getResponse() as any;
expect(response.error).toBe('Muitas tentativas de login. Tente novamente em alguns minutos.'); expect(response.error).toBe(
'Muitas tentativas de login. Tente novamente em alguns minutos.',
);
expect(response.success).toBe(false); expect(response.success).toBe(false);
} }
}); });
@@ -512,7 +518,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
const result = await guard.canActivate(mockExecutionContext); const result = await guard.canActivate(mockExecutionContext);
expect(result).toBe(true); expect(result).toBe(true);
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('2001:0db8:85a3:0000:0000:8a2e:0370:7334'); expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
);
}); });
it('should reject invalid IPv6 format', async () => { it('should reject invalid IPv6 format', async () => {
@@ -556,7 +564,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
await guard.canActivate(mockExecutionContext); await guard.canActivate(mockExecutionContext);
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('192.168.1.1'); expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
'192.168.1.1',
);
}); });
it('should fallback to connection.remoteAddress when x-forwarded-for is missing', async () => { it('should fallback to connection.remoteAddress when x-forwarded-for is missing', async () => {
@@ -572,7 +582,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
await guard.canActivate(mockExecutionContext); await guard.canActivate(mockExecutionContext);
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('10.0.0.1'); expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
'10.0.0.1',
);
}); });
it('should use default IP when all sources are missing', async () => { it('should use default IP when all sources are missing', async () => {
@@ -603,4 +615,3 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
}); });
}); });
}); });

View File

@@ -1,4 +1,10 @@
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common'; import {
Injectable,
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { RateLimitingService } from '../services/rate-limiting.service'; import { RateLimitingService } from '../services/rate-limiting.service';
@Injectable() @Injectable()
@@ -19,7 +25,8 @@ export class RateLimitingGuard implements CanActivate {
try { try {
isAllowed = await this.rateLimitingService.isAllowed(ip); isAllowed = await this.rateLimitingService.isAllowed(ip);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage =
error instanceof Error ? error.message : String(error);
throw new HttpException( throw new HttpException(
{ {
success: false, success: false,
@@ -36,7 +43,8 @@ export class RateLimitingGuard implements CanActivate {
try { try {
attemptInfo = await this.rateLimitingService.getAttemptInfo(ip); attemptInfo = await this.rateLimitingService.getAttemptInfo(ip);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage =
error instanceof Error ? error.message : String(error);
throw new HttpException( throw new HttpException(
{ {
success: false, success: false,
@@ -53,7 +61,8 @@ export class RateLimitingGuard implements CanActivate {
throw new HttpException( throw new HttpException(
{ {
success: false, success: false,
error: 'Muitas tentativas de login. Tente novamente em alguns minutos.', error:
'Muitas tentativas de login. Tente novamente em alguns minutos.',
data: null, data: null,
details: { details: {
attempts: attemptInfo.attempts, attempts: attemptInfo.attempts,
@@ -73,13 +82,16 @@ export class RateLimitingGuard implements CanActivate {
* @returns Endereço IP do cliente ou '127.0.0.1' se não encontrado * @returns Endereço IP do cliente ou '127.0.0.1' se não encontrado
*/ */
private getClientIp(request: any): string { private getClientIp(request: any): string {
const forwardedFor = request.headers['x-forwarded-for']?.split(',')[0]?.trim(); const forwardedFor = request.headers['x-forwarded-for']
?.split(',')[0]
?.trim();
const realIp = request.headers['x-real-ip']?.trim(); const realIp = request.headers['x-real-ip']?.trim();
const connectionIp = request.connection?.remoteAddress; const connectionIp = request.connection?.remoteAddress;
const socketIp = request.socket?.remoteAddress; const socketIp = request.socket?.remoteAddress;
const requestIp = request.ip; const requestIp = request.ip;
const rawIp = forwardedFor || realIp || connectionIp || socketIp || requestIp; const rawIp =
forwardedFor || realIp || connectionIp || socketIp || requestIp;
if (rawIp === null || rawIp === undefined) { if (rawIp === null || rawIp === undefined) {
return ''; return '';
@@ -144,7 +156,11 @@ export class RateLimitingGuard implements CanActivate {
return; return;
} }
if (!ipv4Regex.test(ip) && !ipv6Regex.test(ip) && !ipv6CompressedRegex.test(ip)) { if (
!ipv4Regex.test(ip) &&
!ipv6Regex.test(ip) &&
!ipv6CompressedRegex.test(ip)
) {
if (!this.isValidIpv4(ip) && !this.isValidIpv6(ip)) { if (!this.isValidIpv4(ip) && !this.isValidIpv6(ip)) {
throw new HttpException( throw new HttpException(
{ {
@@ -166,7 +182,7 @@ export class RateLimitingGuard implements CanActivate {
const parts = ip.split('.'); const parts = ip.split('.');
if (parts.length !== 4) return false; if (parts.length !== 4) return false;
return parts.every(part => { return parts.every((part) => {
const num = parseInt(part, 10); const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255; return !isNaN(num) && num >= 0 && num <= 255;
}); });
@@ -184,13 +200,13 @@ export class RateLimitingGuard implements CanActivate {
const leftParts = parts[0] ? parts[0].split(':') : []; const leftParts = parts[0] ? parts[0].split(':') : [];
const rightParts = parts[1] ? parts[1].split(':') : []; const rightParts = parts[1] ? parts[1].split(':') : [];
return (leftParts.length + rightParts.length) <= 8; return leftParts.length + rightParts.length <= 8;
} }
const parts = ip.split(':'); const parts = ip.split(':');
if (parts.length !== 8) return false; if (parts.length !== 8) return false;
return parts.every(part => { return parts.every((part) => {
if (!part) return false; if (!part) return false;
return /^[0-9a-fA-F]{1,4}$/.test(part); return /^[0-9a-fA-F]{1,4}$/.test(part);
}); });
@@ -223,8 +239,11 @@ export class RateLimitingGuard implements CanActivate {
); );
} }
if (attemptInfo.remainingTime !== undefined && if (
(typeof attemptInfo.remainingTime !== 'number' || attemptInfo.remainingTime < 0)) { attemptInfo.remainingTime !== undefined &&
(typeof attemptInfo.remainingTime !== 'number' ||
attemptInfo.remainingTime < 0)
) {
throw new HttpException( throw new HttpException(
{ {
success: false, success: false,

View File

@@ -1,16 +1,15 @@
export class Result<T> { export class Result<T> {
private constructor( private constructor(
public readonly success: boolean, public readonly success: boolean,
public readonly data?: T, public readonly data?: T,
public readonly error?: string, public readonly error?: string,
) {} ) {}
static ok<U>(data: U): Result<U> { static ok<U>(data: U): Result<U> {
return new Result<U>(true, data); return new Result<U>(true, data);
}
static fail<U>(message: string): Result<U> {
return new Result<U>(false, undefined, message);
}
} }
static fail<U>(message: string): Result<U> {
return new Result<U>(false, undefined, message);
}
}

View File

@@ -31,11 +31,11 @@ export class LoginAuditService {
private readonly LOG_PREFIX = 'login_audit'; private readonly LOG_PREFIX = 'login_audit';
private readonly LOG_EXPIRY = 30 * 24 * 60 * 60; private readonly LOG_EXPIRY = 30 * 24 * 60 * 60;
constructor( constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}
@Inject('REDIS_CLIENT') private readonly redis: Redis,
) {}
async logLoginAttempt(log: Omit<LoginAuditLog, 'id' | 'timestamp'>): Promise<void> { async logLoginAttempt(
log: Omit<LoginAuditLog, 'id' | 'timestamp'>,
): Promise<void> {
const logId = this.generateLogId(); const logId = this.generateLogId();
const timestamp = DateUtil.now(); const timestamp = DateUtil.now();
@@ -69,7 +69,9 @@ export class LoginAuditService {
await this.redis.expire(dateLogsKey, this.LOG_EXPIRY); await this.redis.expire(dateLogsKey, this.LOG_EXPIRY);
} }
async getLoginLogs(filters: LoginAuditFilters = {}): Promise<LoginAuditLog[]> { async getLoginLogs(
filters: LoginAuditFilters = {},
): Promise<LoginAuditLog[]> {
const logIds = await this.getLogIds(filters); const logIds = await this.getLogIds(filters);
const logs: LoginAuditLog[] = []; const logs: LoginAuditLog[] = [];
@@ -102,13 +104,21 @@ export class LoginAuditService {
return logs.slice(offset, offset + limit); return logs.slice(offset, offset + limit);
} }
async getLoginStats(userId?: number, days: number = 7): Promise<{ async getLoginStats(
userId?: number,
days: number = 7,
): Promise<{
totalAttempts: number; totalAttempts: number;
successfulLogins: number; successfulLogins: number;
failedLogins: number; failedLogins: number;
uniqueIps: number; uniqueIps: number;
topIps: Array<{ ip: string; count: number }>; topIps: Array<{ ip: string; count: number }>;
dailyStats: Array<{ date: string; attempts: number; successes: number; failures: number }>; dailyStats: Array<{
date: string;
attempts: number;
successes: number;
failures: number;
}>;
}> { }> {
const endDate = DateUtil.now(); const endDate = DateUtil.now();
const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000); const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000);
@@ -127,15 +137,20 @@ export class LoginAuditService {
const stats = { const stats = {
totalAttempts: logs.length, totalAttempts: logs.length,
successfulLogins: logs.filter(log => log.success).length, successfulLogins: logs.filter((log) => log.success).length,
failedLogins: logs.filter(log => !log.success).length, failedLogins: logs.filter((log) => !log.success).length,
uniqueIps: new Set(logs.map(log => log.ipAddress)).size, uniqueIps: new Set(logs.map((log) => log.ipAddress)).size,
topIps: [] as Array<{ ip: string; count: number }>, topIps: [] as Array<{ ip: string; count: number }>,
dailyStats: [] as Array<{ date: string; attempts: number; successes: number; failures: number }>, dailyStats: [] as Array<{
date: string;
attempts: number;
successes: number;
failures: number;
}>,
}; };
const ipCounts = new Map<string, number>(); const ipCounts = new Map<string, number>();
logs.forEach(log => { logs.forEach((log) => {
ipCounts.set(log.ipAddress, (ipCounts.get(log.ipAddress) || 0) + 1); ipCounts.set(log.ipAddress, (ipCounts.get(log.ipAddress) || 0) + 1);
}); });
@@ -144,10 +159,17 @@ export class LoginAuditService {
.sort((a, b) => b.count - a.count) .sort((a, b) => b.count - a.count)
.slice(0, 10); .slice(0, 10);
const dailyCounts = new Map<string, { attempts: number; successes: number; failures: number }>(); const dailyCounts = new Map<
logs.forEach(log => { string,
{ attempts: number; successes: number; failures: number }
>();
logs.forEach((log) => {
const date = DateUtil.toBrazilString(log.timestamp, 'yyyy-MM-dd'); const date = DateUtil.toBrazilString(log.timestamp, 'yyyy-MM-dd');
const dayStats = dailyCounts.get(date) || { attempts: 0, successes: 0, failures: 0 }; const dayStats = dailyCounts.get(date) || {
attempts: 0,
successes: 0,
failures: 0,
};
dayStats.attempts++; dayStats.attempts++;
if (log.success) { if (log.success) {
@@ -168,7 +190,9 @@ export class LoginAuditService {
} }
async cleanupOldLogs(): Promise<void> { async cleanupOldLogs(): Promise<void> {
const cutoffDate = new Date(DateUtil.nowTimestamp() - 30 * 24 * 60 * 60 * 1000); const cutoffDate = new Date(
DateUtil.nowTimestamp() - 30 * 24 * 60 * 60 * 1000,
);
const cutoffDateStr = DateUtil.toBrazilString(cutoffDate, 'yyyy-MM-dd'); const cutoffDateStr = DateUtil.toBrazilString(cutoffDate, 'yyyy-MM-dd');
const oldDates = this.getDateRange(new Date('2020-01-01'), cutoffDate); const oldDates = this.getDateRange(new Date('2020-01-01'), cutoffDate);
@@ -190,7 +214,9 @@ export class LoginAuditService {
} }
if (filters.startDate || filters.endDate) { if (filters.startDate || filters.endDate) {
const startDate = filters.startDate || new Date(DateUtil.nowTimestamp() - 7 * 24 * 60 * 60 * 1000); const startDate =
filters.startDate ||
new Date(DateUtil.nowTimestamp() - 7 * 24 * 60 * 60 * 1000);
const endDate = filters.endDate || DateUtil.now(); const endDate = filters.endDate || DateUtil.now();
const dates = this.getDateRange(startDate, endDate); const dates = this.getDateRange(startDate, endDate);
@@ -210,7 +236,9 @@ export class LoginAuditService {
} }
private generateLogId(): string { private generateLogId(): string {
return `${DateUtil.nowTimestamp()}_${Math.random().toString(36).substr(2, 9)}`; return `${DateUtil.nowTimestamp()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
} }
private buildLogKey(logId: string): string { private buildLogKey(logId: string): string {
@@ -233,8 +261,14 @@ export class LoginAuditService {
return `${this.LOG_PREFIX}:date:${date}`; return `${this.LOG_PREFIX}:date:${date}`;
} }
private matchesFilters(log: LoginAuditLog, filters: LoginAuditFilters): boolean { private matchesFilters(
if (filters.username && !log.username.toLowerCase().includes(filters.username.toLowerCase())) { log: LoginAuditLog,
filters: LoginAuditFilters,
): boolean {
if (
filters.username &&
!log.username.toLowerCase().includes(filters.username.toLowerCase())
) {
return false; return false;
} }

View File

@@ -16,11 +16,12 @@ export class RateLimitingService {
blockDurationMs: 1 * 60 * 1000, blockDurationMs: 1 * 60 * 1000,
}; };
constructor( constructor(@Inject(RedisClientToken) private readonly redis: IRedisClient) {}
@Inject(RedisClientToken) private readonly redis: IRedisClient,
) {}
async isAllowed(ip: string, config?: Partial<RateLimitConfig>): Promise<boolean> { async isAllowed(
ip: string,
config?: Partial<RateLimitConfig>,
): Promise<boolean> {
const finalConfig = { ...this.defaultConfig, ...config }; const finalConfig = { ...this.defaultConfig, ...config };
const key = this.buildAttemptKey(ip); const key = this.buildAttemptKey(ip);
const blockKey = this.buildBlockKey(ip); const blockKey = this.buildBlockKey(ip);
@@ -51,21 +52,25 @@ export class RateLimitingService {
return {attempts, 0} return {attempts, 0}
`; `;
const result = await this.redis.eval( const result = (await this.redis.eval(
luaScript, luaScript,
2, 2,
key, key,
blockKey, blockKey,
finalConfig.maxAttempts, finalConfig.maxAttempts,
finalConfig.windowMs, finalConfig.windowMs,
finalConfig.blockDurationMs finalConfig.blockDurationMs,
) as [number, number]; )) as [number, number];
const [attempts, isBlockedResult] = result; const [attempts, isBlockedResult] = result;
return isBlockedResult === 0; return isBlockedResult === 0;
} }
async recordAttempt(ip: string, success: boolean, config?: Partial<RateLimitConfig>): Promise<void> { async recordAttempt(
ip: string,
success: boolean,
config?: Partial<RateLimitConfig>,
): Promise<void> {
const finalConfig = { ...this.defaultConfig, ...config }; const finalConfig = { ...this.defaultConfig, ...config };
const key = this.buildAttemptKey(ip); const key = this.buildAttemptKey(ip);
const blockKey = this.buildBlockKey(ip); const blockKey = this.buildBlockKey(ip);

View File

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

View File

@@ -19,11 +19,13 @@ export class SessionManagementService {
private readonly SESSION_TTL = 8 * 60 * 60; private readonly SESSION_TTL = 8 * 60 * 60;
private readonly MAX_SESSIONS_PER_USER = 1; private readonly MAX_SESSIONS_PER_USER = 1;
constructor( constructor(@Inject(RedisClientToken) private readonly redis: IRedisClient) {}
@Inject(RedisClientToken) private readonly redis: IRedisClient,
) {}
async createSession(userId: number, ipAddress: string, userAgent: string): Promise<SessionData> { async createSession(
userId: number,
ipAddress: string,
userAgent: string,
): Promise<SessionData> {
const sessionId = randomBytes(16).toString('hex'); const sessionId = randomBytes(16).toString('hex');
const now = DateUtil.nowTimestamp(); const now = DateUtil.nowTimestamp();
@@ -45,7 +47,10 @@ export class SessionManagementService {
return sessionData; return sessionData;
} }
async updateSessionActivity(userId: number, sessionId: string): Promise<void> { async updateSessionActivity(
userId: number,
sessionId: string,
): Promise<void> {
const key = this.buildSessionKey(userId, sessionId); const key = this.buildSessionKey(userId, sessionId);
const sessionData = await this.redis.get<SessionData>(key); const sessionData = await this.redis.get<SessionData>(key);
@@ -55,7 +60,10 @@ export class SessionManagementService {
} }
} }
async getActiveSessions(userId: number, currentSessionId?: string): Promise<SessionData[]> { async getActiveSessions(
userId: number,
currentSessionId?: string,
): Promise<SessionData[]> {
const pattern = this.buildSessionPattern(userId); const pattern = this.buildSessionPattern(userId);
const keys = await this.redis.keys(pattern); const keys = await this.redis.keys(pattern);
@@ -99,7 +107,10 @@ export class SessionManagementService {
} }
} }
async terminateOtherSessions(userId: number, currentSessionId: string): Promise<void> { async terminateOtherSessions(
userId: number,
currentSessionId: string,
): Promise<void> {
const pattern = this.buildSessionPattern(userId); const pattern = this.buildSessionPattern(userId);
const keys = await this.redis.keys(pattern); const keys = await this.redis.keys(pattern);
@@ -130,7 +141,7 @@ export class SessionManagementService {
if (activeSessions.length > this.MAX_SESSIONS_PER_USER) { if (activeSessions.length > this.MAX_SESSIONS_PER_USER) {
const sessionsToRemove = activeSessions const sessionsToRemove = activeSessions
.slice(this.MAX_SESSIONS_PER_USER) .slice(this.MAX_SESSIONS_PER_USER)
.map(session => session.sessionId); .map((session) => session.sessionId);
for (const sessionId of sessionsToRemove) { for (const sessionId of sessionsToRemove) {
await this.terminateSession(userId, sessionId); await this.terminateSession(userId, sessionId);

View File

@@ -59,12 +59,16 @@ export class TokenBlacklistService {
private calculateTokenTTL(payload: JwtPayload): number { private calculateTokenTTL(payload: JwtPayload): number {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const exp = payload.exp || (now + 8 * 60 * 60); const exp = payload.exp || now + 8 * 60 * 60;
return Math.max(0, exp - now); return Math.max(0, exp - now);
} }
private hashToken(token: string): string { private hashToken(token: string): string {
const crypto = require('crypto'); const crypto = require('crypto');
return crypto.createHash('sha256').update(token).digest('hex').substring(0, 16); return crypto
.createHash('sha256')
.update(token)
.digest('hex')
.substring(0, 16);
} }
} }

View File

@@ -31,7 +31,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
} }
const token = req.headers?.authorization?.replace('Bearer ', ''); const token = req.headers?.authorization?.replace('Bearer ', '');
if (token && await this.tokenBlacklistService.isBlacklisted(token)) { if (token && (await this.tokenBlacklistService.isBlacklisted(token))) {
throw new UnauthorizedException('Token foi invalidado'); throw new UnauthorizedException('Token foi invalidado');
} }
@@ -39,10 +39,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
const cachedUser = await this.redis.get<any>(sessionKey); const cachedUser = await this.redis.get<any>(sessionKey);
if (cachedUser) { if (cachedUser) {
const isSessionActive = await this.sessionManagementService.isSessionActive( const isSessionActive =
payload.id, await this.sessionManagementService.isSessionActive(
payload.sessionId payload.id,
); payload.sessionId,
);
if (!isSessionActive) { if (!isSessionActive) {
throw new UnauthorizedException('Sessão expirada ou inválida'); throw new UnauthorizedException('Sessão expirada ou inválida');
@@ -65,7 +66,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
} }
if (user.situacao === 'B') { if (user.situacao === 'B') {
throw new UnauthorizedException('Usuário bloqueado, acesso não permitido'); throw new UnauthorizedException(
'Usuário bloqueado, acesso não permitido',
);
} }
const userData = { const userData = {

View File

@@ -19,7 +19,10 @@ export class ResetPasswordService {
if (!user) return null; if (!user) return null;
const newPassword = Guid.create().toString().substring(0, 8); const newPassword = Guid.create().toString().substring(0, 8);
await this.userRepository.updatePassword(user.sellerId, md5(newPassword).toUpperCase()); await this.userRepository.updatePassword(
user.sellerId,
md5(newPassword).toUpperCase(),
);
await this.emailService.sendPasswordReset(user.email, newPassword); await this.emailService.sendPasswordReset(user.email, newPassword);

View File

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

View File

@@ -4,8 +4,6 @@ import { ResetPasswordService } from './reset-password.service';
import { ChangePasswordService } from './change-password.service'; import { ChangePasswordService } from './change-password.service';
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command'; import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor( constructor(
@@ -22,7 +20,15 @@ export class UsersService {
return this.resetPasswordService.execute(user.document, user.email); return this.resetPasswordService.execute(user.document, user.email);
} }
async changePassword(user: { id: number; password: string; newPassword: string }) { async changePassword(user: {
return this.changePasswordService.execute(user.id, user.password, user.newPassword); id: number;
password: string;
newPassword: string;
}) {
return this.changePasswordService.execute(
user.id,
user.password,
user.newPassword,
);
} }
} }

View File

@@ -7,7 +7,8 @@ import { ConfigService } from '@nestjs/config';
export class RateLimiterMiddleware implements NestMiddleware { export class RateLimiterMiddleware implements NestMiddleware {
private readonly ttl: number; private readonly ttl: number;
private readonly limit: number; private readonly limit: number;
private readonly store: Map<string, { count: number; expiration: number }> = new Map(); private readonly store: Map<string, { count: number; expiration: number }> =
new Map();
constructor(private configService: ConfigService) { constructor(private configService: ConfigService) {
this.ttl = this.configService.get<number>('THROTTLE_TTL', 60); this.ttl = this.configService.get<number>('THROTTLE_TTL', 60);
@@ -42,7 +43,9 @@ export class RateLimiterMiddleware implements NestMiddleware {
const timeToWait = Math.ceil((record.expiration - now) / 1000); const timeToWait = Math.ceil((record.expiration - now) / 1000);
this.setRateLimitHeaders(res, record.count); this.setRateLimitHeaders(res, record.count);
res.header('Retry-After', String(timeToWait)); res.header('Retry-After', String(timeToWait));
throw new ThrottlerException(`Too Many Requests. Retry after ${timeToWait} seconds.`); throw new ThrottlerException(
`Too Many Requests. Retry after ${timeToWait} seconds.`,
);
} }
record.count++; record.count++;
@@ -52,13 +55,17 @@ export class RateLimiterMiddleware implements NestMiddleware {
private generateKey(req: Request): string { private generateKey(req: Request): string {
// Combina IP com rota para rate limiting mais preciso // Combina IP com rota para rate limiting mais preciso
const ip = req.ip || req.headers['x-forwarded-for'] as string || 'unknown-ip'; const ip =
req.ip || (req.headers['x-forwarded-for'] as string) || 'unknown-ip';
const path = req.path || req.originalUrl || ''; const path = req.path || req.originalUrl || '';
return `${ip}:${path}`; return `${ip}:${path}`;
} }
private setRateLimitHeaders(res: Response, count: number): void { private setRateLimitHeaders(res: Response, count: number): void {
res.header('X-RateLimit-Limit', String(this.limit)); res.header('X-RateLimit-Limit', String(this.limit));
res.header('X-RateLimit-Remaining', String(Math.max(0, this.limit - count))); res.header(
'X-RateLimit-Remaining',
String(Math.max(0, this.limit - count)),
);
} }
} }

View File

@@ -20,7 +20,7 @@ export class RequestSanitizerMiddleware implements NestMiddleware {
} }
private sanitizeObject(obj: any) { private sanitizeObject(obj: any) {
Object.keys(obj).forEach(key => { Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') { if (typeof obj[key] === 'string') {
obj[key] = this.sanitizeString(obj[key]); obj[key] = this.sanitizeString(obj[key]);
} else if (typeof obj[key] === 'object' && obj[key] !== null) { } else if (typeof obj[key] === 'object' && obj[key] !== null) {

View File

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

View File

@@ -1,8 +1,12 @@
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'; import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
// Decorator para sanitizar strings e prevenir SQL/NoSQL injection // Decorator para sanitizar strings e prevenir SQL/NoSQL injection
export function IsSanitized(validationOptions?: ValidationOptions) { export function IsSanitized(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) { return function (object: object, propertyName: string) {
registerDecorator({ registerDecorator({
name: 'isSanitized', name: 'isSanitized',
target: object.constructor, target: object.constructor,
@@ -12,19 +16,22 @@ export function IsSanitized(validationOptions?: ValidationOptions) {
validate(value: any, args: ValidationArguments) { validate(value: any, args: ValidationArguments) {
if (typeof value !== 'string') return true; // Skip non-string values if (typeof value !== 'string') return true; // Skip non-string values
const sqlInjectionRegex = /('|"|;|--|\/\*|\*\/|@@|@|char|nchar|varchar|nvarchar|alter|begin|cast|create|cursor|declare|delete|drop|end|exec|execute|fetch|insert|kill|open|select|sys|sysobjects|syscolumns|table|update|xp_)/i; const sqlInjectionRegex =
/('|"|;|--|\/\*|\*\/|@@|@|char|nchar|varchar|nvarchar|alter|begin|cast|create|cursor|declare|delete|drop|end|exec|execute|fetch|insert|kill|open|select|sys|sysobjects|syscolumns|table|update|xp_)/i;
if (sqlInjectionRegex.test(value)) { if (sqlInjectionRegex.test(value)) {
return false; return false;
} }
// Check for NoSQL injection patterns (MongoDB) // Check for NoSQL injection patterns (MongoDB)
const noSqlInjectionRegex = /(\$where|\$ne|\$gt|\$lt|\$gte|\$lte|\$in|\$nin|\$or|\$and|\$regex|\$options|\$elemMatch|\{.*\:.*\})/i; const noSqlInjectionRegex =
/(\$where|\$ne|\$gt|\$lt|\$gte|\$lte|\$in|\$nin|\$or|\$and|\$regex|\$options|\$elemMatch|\{.*\:.*\})/i;
if (noSqlInjectionRegex.test(value)) { if (noSqlInjectionRegex.test(value)) {
return false; return false;
} }
// Check for XSS attempts // Check for XSS attempts
const xssRegex = /(<script|javascript:|on\w+\s*=|<%=|<img|<iframe|alert\(|window\.|document\.)/i; const xssRegex =
/(<script|javascript:|on\w+\s*=|<%=|<img|<iframe|alert\(|window\.|document\.)/i;
if (xssRegex.test(value)) { if (xssRegex.test(value)) {
return false; return false;
} }
@@ -41,7 +48,7 @@ export function IsSanitized(validationOptions?: ValidationOptions) {
// Decorator para validar IDs seguros (evita injeção em IDs) // Decorator para validar IDs seguros (evita injeção em IDs)
export function IsSecureId(validationOptions?: ValidationOptions) { export function IsSecureId(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) { return function (object: object, propertyName: string) {
registerDecorator({ registerDecorator({
name: 'isSecureId', name: 'isSecureId',
target: object.constructor, target: object.constructor,
@@ -49,11 +56,14 @@ export function IsSecureId(validationOptions?: ValidationOptions) {
options: validationOptions, options: validationOptions,
validator: { validator: {
validate(value: any, args: ValidationArguments) { validate(value: any, args: ValidationArguments) {
if (typeof value !== 'string' && typeof value !== 'number') return false; if (typeof value !== 'string' && typeof value !== 'number')
return false;
if (typeof value === 'string') { if (typeof value === 'string') {
// Permitir apenas: letras, números, hífens, underscores e GUIDs // Permitir apenas: letras, números, hífens, underscores e GUIDs
return /^[a-zA-Z0-9\-_]+$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); return /^[a-zA-Z0-9\-_]+$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
value,
);
} }
// Se for número, deve ser positivo // Se for número, deve ser positivo

View File

@@ -1,10 +1,13 @@
export interface IRedisClient { export interface IRedisClient {
get<T>(key: string): Promise<T | null>; get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>; set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
del(key: string): Promise<void>; del(key: string): Promise<void>;
del(...keys: string[]): Promise<void>; del(...keys: string[]): Promise<void>;
keys(pattern: string): Promise<string[]>; keys(pattern: string): Promise<string[]>;
ttl(key: string): Promise<number>; ttl(key: string): Promise<number>;
eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise<any>; eval(
} script: string,
numKeys: number,
...keysAndArgs: (string | number)[]
): Promise<any>;
}

View File

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

View File

@@ -6,7 +6,7 @@ import { IRedisClient } from './IRedisClient';
export class RedisClientAdapter implements IRedisClient { export class RedisClientAdapter implements IRedisClient {
constructor( constructor(
@Inject('REDIS_CLIENT') @Inject('REDIS_CLIENT')
private readonly redis: Redis private readonly redis: Redis,
) {} ) {}
async get<T>(key: string): Promise<T | null> { async get<T>(key: string): Promise<T | null> {
@@ -43,7 +43,11 @@ export class RedisClientAdapter implements IRedisClient {
return this.redis.ttl(key); return this.redis.ttl(key);
} }
async eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise<any> { async eval(
script: string,
numKeys: number,
...keysAndArgs: (string | number)[]
): Promise<any> {
return this.redis.eval(script, numKeys, ...keysAndArgs); return this.redis.eval(script, numKeys, ...keysAndArgs);
} }
} }

View File

@@ -9,4 +9,4 @@ import { RedisClientAdapterProvider } from './redis-client.adapter.provider';
providers: [RedisProvider, RedisClientAdapterProvider], providers: [RedisProvider, RedisClientAdapterProvider],
exports: [RedisProvider, RedisClientAdapterProvider], exports: [RedisProvider, RedisClientAdapterProvider],
}) })
export class RedisModule {} export class RedisModule {}

View File

@@ -1,21 +1,21 @@
import { Provider } from '@nestjs/common'; import { Provider } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
export const RedisProvider: Provider = { export const RedisProvider: Provider = {
provide: 'REDIS_CLIENT', provide: 'REDIS_CLIENT',
useFactory: (configService: ConfigService) => { useFactory: (configService: ConfigService) => {
const redis = new Redis({ const redis = new Redis({
host: configService.get<string>('REDIS_HOST', '10.1.1.109'), host: configService.get<string>('REDIS_HOST', '10.1.1.109'),
port: configService.get<number>('REDIS_PORT', 6379), port: configService.get<number>('REDIS_PORT', 6379),
password: configService.get<string>('REDIS_PASSWORD', '1234'), password: configService.get<string>('REDIS_PASSWORD', '1234'),
}); });
redis.on('error', (err) => { redis.on('error', (err) => {
console.error('Erro ao conectar ao Redis:', err); console.error('Erro ao conectar ao Redis:', err);
}); });
return redis; return redis;
}, },
inject: [ConfigService], inject: [ConfigService],
}; };

View File

@@ -2,8 +2,6 @@ import { DataSourceOptions } from 'typeorm';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import * as oracledb from 'oracledb'; import * as oracledb from 'oracledb';
oracledb.initOracleClient({ libDir: process.env.ORACLE_CLIENT_LIB_DIR }); oracledb.initOracleClient({ libDir: process.env.ORACLE_CLIENT_LIB_DIR });
// Definir a estratégia de pool padrão para Oracle // Definir a estratégia de pool padrão para Oracle
@@ -12,19 +10,22 @@ oracledb.queueTimeout = 60000; // timeout da fila em milissegundos
oracledb.poolIncrement = 1; // incremental de conexões oracledb.poolIncrement = 1; // incremental de conexões
export function createOracleConfig(config: ConfigService): DataSourceOptions { export function createOracleConfig(config: ConfigService): DataSourceOptions {
const poolMin = parseInt(config.get('ORACLE_POOL_MIN', '5')); const poolMin = parseInt(config.get('ORACLE_POOL_MIN', '5'));
const poolMax = parseInt(config.get('ORACLE_POOL_MAX', '20')); const poolMax = parseInt(config.get('ORACLE_POOL_MAX', '20'));
const poolIncrement = parseInt(config.get('ORACLE_POOL_INCREMENT', '5')); const poolIncrement = parseInt(config.get('ORACLE_POOL_INCREMENT', '5'));
const poolTimeout = parseInt(config.get('ORACLE_POOL_TIMEOUT', '30000')); const poolTimeout = parseInt(config.get('ORACLE_POOL_TIMEOUT', '30000'));
const idleTimeout = parseInt(config.get('ORACLE_POOL_IDLE_TIMEOUT', '300000')); const idleTimeout = parseInt(
config.get('ORACLE_POOL_IDLE_TIMEOUT', '300000'),
);
const validPoolMin = Math.max(1, poolMin); const validPoolMin = Math.max(1, poolMin);
const validPoolMax = Math.max(validPoolMin + 1, poolMax); const validPoolMax = Math.max(validPoolMin + 1, poolMax);
const validPoolIncrement = Math.max(1, poolIncrement); const validPoolIncrement = Math.max(1, poolIncrement);
if (validPoolMax <= validPoolMin) { if (validPoolMax <= validPoolMin) {
console.warn('Warning: poolMax deve ser maior que poolMin. Ajustando poolMax para poolMin + 1'); console.warn(
'Warning: poolMax deve ser maior que poolMin. Ajustando poolMax para poolMin + 1',
);
} }
const options: DataSourceOptions = { const options: DataSourceOptions = {

View File

@@ -5,9 +5,15 @@ export function createPostgresConfig(config: ConfigService): DataSourceOptions {
// Obter configurações de ambiente ou usar valores padrão // Obter configurações de ambiente ou usar valores padrão
const poolMin = parseInt(config.get('POSTGRES_POOL_MIN', '5')); const poolMin = parseInt(config.get('POSTGRES_POOL_MIN', '5'));
const poolMax = parseInt(config.get('POSTGRES_POOL_MAX', '20')); const poolMax = parseInt(config.get('POSTGRES_POOL_MAX', '20'));
const idleTimeout = parseInt(config.get('POSTGRES_POOL_IDLE_TIMEOUT', '30000')); const idleTimeout = parseInt(
const connectionTimeout = parseInt(config.get('POSTGRES_POOL_CONNECTION_TIMEOUT', '5000')); config.get('POSTGRES_POOL_IDLE_TIMEOUT', '30000'),
const acquireTimeout = parseInt(config.get('POSTGRES_POOL_ACQUIRE_TIMEOUT', '60000')); );
const connectionTimeout = parseInt(
config.get('POSTGRES_POOL_CONNECTION_TIMEOUT', '5000'),
);
const acquireTimeout = parseInt(
config.get('POSTGRES_POOL_ACQUIRE_TIMEOUT', '60000'),
);
// Validação de valores mínimos // Validação de valores mínimos
const validPoolMin = Math.max(1, poolMin); const validPoolMin = Math.max(1, poolMin);
@@ -25,7 +31,10 @@ export function createPostgresConfig(config: ConfigService): DataSourceOptions {
database: config.get('POSTGRES_DB'), database: config.get('POSTGRES_DB'),
synchronize: config.get('NODE_ENV') === 'development', synchronize: config.get('NODE_ENV') === 'development',
entities: [__dirname + '/../**/*.entity.{ts,js}'], entities: [__dirname + '/../**/*.entity.{ts,js}'],
ssl: config.get('NODE_ENV') === 'production' ? { rejectUnauthorized: false } : false, ssl:
config.get('NODE_ENV') === 'production'
? { rejectUnauthorized: false }
: false,
logging: config.get('NODE_ENV') === 'development', logging: config.get('NODE_ENV') === 'development',
poolSize: validPoolMax, // máximo de conexões no pool poolSize: validPoolMax, // máximo de conexões no pool
extra: { extra: {

View File

@@ -1,11 +0,0 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/controllers#controllers
*/
import { Controller } from '@nestjs/common';
@Controller()
export class NegotiationsController { }

View File

@@ -1,19 +0,0 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/modules
*/
import { Module } from '@nestjs/common';
import { NegotiationsController } from './negotiations.controller';
import { NegotiationsService } from './negotiations.service';
@Module({
imports: [],
controllers: [
NegotiationsController,],
providers: [
NegotiationsService,],
})
export class NegotiationsModule { }

View File

@@ -1,11 +0,0 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/providers#services
*/
import { Injectable } from '@nestjs/common';
@Injectable()
export class NegotiationsService { }

View File

@@ -1,17 +0,0 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/modules
*/
import { Module } from '@nestjs/common';
import { OccurrencesService } from './occurrences.service';
@Module({
imports: [],
controllers: [],
providers: [
OccurrencesService,],
})
export class OccurrencesModule { }

View File

@@ -1,10 +0,0 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/providers#services
*/
import { Injectable } from '@nestjs/common';
@Injectable()
export class OccurrencesService { }

View File

@@ -1,10 +0,0 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/controllers#controllers
*/
import { Controller } from '@nestjs/common';
@Controller()
export class OcorrencesController { }

View File

@@ -1,37 +0,0 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/controllers#controllers
*/
import { Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
@ApiTags('CRM - Reason Table')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('api/v1/crm/reason')
export class ReasonTableController {
@Get()
async getReasons() {
return null;
}
@Post()
async createReasons() {
return null;
}
@Put('/:id')
async updateReasons(@Param('id') id: number) {
return null;
}
@Delete('/:id')
async deleteReasons(@Param('id') id: number) {
return null;
}
}

View File

@@ -1,19 +0,0 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/modules
*/
import { Module } from '@nestjs/common';
import { ReasonTableController } from './reason-table.controller';
import { ReasonTableService } from './reason-table.service';
@Module({
imports: [],
controllers: [
ReasonTableController,],
providers: [
ReasonTableService,],
})
export class ReasonTableModule { }

View File

@@ -1,10 +0,0 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars *//*
https://docs.nestjs.com/providers#services
*/
import { Injectable } from '@nestjs/common';
@Injectable()
export class ReasonTableService { }

View File

@@ -1,50 +1,47 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { DataConsultService } from '../data-consult.service'; import { DataConsultService } from '../data-consult.service';
import { DataConsultRepository } from '../data-consult.repository'; import { DataConsultRepository } from '../data-consult.repository';
import { ILogger } from '../../Log/ILogger';
import { IRedisClient } from '../../core/configs/cache/IRedisClient'; import { IRedisClient } from '../../core/configs/cache/IRedisClient';
import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider'; import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { DATA_SOURCE } from '../../core/constants'; import { DATA_SOURCE } from '../../core/constants';
export const createMockRepository = (methods: Partial<DataConsultRepository> = {}) => ({ export const createMockRepository = (
findStores: jest.fn(), methods: Partial<DataConsultRepository> = {},
findSellers: jest.fn(), ) =>
findBillings: jest.fn(), ({
findCustomers: jest.fn(), findStores: jest.fn(),
findAllProducts: jest.fn(), findSellers: jest.fn(),
findAllCarriers: jest.fn(), findBillings: jest.fn(),
findRegions: jest.fn(), findCustomers: jest.fn(),
...methods, findAllProducts: jest.fn(),
} as any); findAllCarriers: jest.fn(),
findRegions: jest.fn(),
...methods,
} as any);
export const createMockLogger = () => ({ export const createMockRedisClient = () =>
log: jest.fn(), ({
error: jest.fn(), get: jest.fn().mockResolvedValue(null),
warn: jest.fn(), set: jest.fn().mockResolvedValue(undefined),
debug: jest.fn(), } as any);
} as any);
export const createMockRedisClient = () => ({
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(undefined),
} as any);
export interface DataConsultServiceTestContext { export interface DataConsultServiceTestContext {
service: DataConsultService; service: DataConsultService;
mockRepository: jest.Mocked<DataConsultRepository>; mockRepository: jest.Mocked<DataConsultRepository>;
mockLogger: jest.Mocked<ILogger>;
mockRedisClient: jest.Mocked<IRedisClient>; mockRedisClient: jest.Mocked<IRedisClient>;
mockDataSource: jest.Mocked<DataSource>; mockDataSource: jest.Mocked<DataSource>;
} }
export async function createDataConsultServiceTestModule( export async function createDataConsultServiceTestModule(
repositoryMethods: Partial<DataConsultRepository> = {}, repositoryMethods: Partial<DataConsultRepository> = {},
redisClientMethods: Partial<IRedisClient> = {} redisClientMethods: Partial<IRedisClient> = {},
): Promise<DataConsultServiceTestContext> { ): Promise<DataConsultServiceTestContext> {
const mockRepository = createMockRepository(repositoryMethods); const mockRepository = createMockRepository(repositoryMethods);
const mockLogger = createMockLogger(); const mockRedisClient = {
const mockRedisClient = { ...createMockRedisClient(), ...redisClientMethods } as any; ...createMockRedisClient(),
...redisClientMethods,
} as any;
const mockDataSource = {} as any; const mockDataSource = {} as any;
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@@ -58,10 +55,6 @@ export async function createDataConsultServiceTestModule(
provide: RedisClientToken, provide: RedisClientToken,
useValue: mockRedisClient, useValue: mockRedisClient,
}, },
{
provide: 'LoggerService',
useValue: mockLogger,
},
{ {
provide: DATA_SOURCE, provide: DATA_SOURCE,
useValue: mockDataSource, useValue: mockDataSource,
@@ -74,9 +67,7 @@ export async function createDataConsultServiceTestModule(
return { return {
service, service,
mockRepository, mockRepository,
mockLogger,
mockRedisClient, mockRedisClient,
mockDataSource, mockDataSource,
}; };
} }

View File

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

View File

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

View File

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

View File

@@ -63,7 +63,9 @@ export class clientesService {
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name" ' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
,PCCLIENT.ESTCOB as "estcob" ,PCCLIENT.ESTCOB as "estcob"
FROM PCCLIENT FROM PCCLIENT
WHERE PCCLIENT.CLIENTE LIKE '${filter.toUpperCase().replace('@', '%')}%' WHERE PCCLIENT.CLIENTE LIKE '${filter
.toUpperCase()
.replace('@', '%')}%'
ORDER BY PCCLIENT.CLIENTE`; ORDER BY PCCLIENT.CLIENTE`;
customers = await queryRunner.manager.query(sql); customers = await queryRunner.manager.query(sql);
} }
@@ -72,7 +74,7 @@ export class clientesService {
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
} }
} },
); );
} }
@@ -103,7 +105,7 @@ export class clientesService {
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
} }
} },
); );
} }
@@ -136,19 +138,13 @@ export class clientesService {
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
} }
} },
); );
} }
/**
* Limpar cache de clientes (útil para invalidação)
* @param pattern - Padrão de chaves para limpar (opcional)
*/
async clearCustomersCache(pattern?: string) { async clearCustomersCache(pattern?: string) {
const cachePattern = pattern || 'clientes:*'; const cachePattern = pattern || 'clientes:*';
// Nota: Esta funcionalidade requer implementação específica do Redis
// Por enquanto, mantemos a interface para futuras implementações
console.log(`Cache de clientes seria limpo para o padrão: ${cachePattern}`);
} }
} }

View File

@@ -1,5 +1,20 @@
import { Controller, Get, Param, Query, UseGuards, UsePipes, ValidationPipe, ParseIntPipe } from '@nestjs/common'; import {
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; Controller,
Get,
Param,
Query,
UseGuards,
UsePipes,
ValidationPipe,
ParseIntPipe,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiBearerAuth,
ApiResponse,
} from '@nestjs/swagger';
import { DataConsultService } from './data-consult.service'; import { DataConsultService } from './data-consult.service';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { ProductDto } from './dto/product.dto'; import { ProductDto } from './dto/product.dto';
@@ -19,7 +34,11 @@ export class DataConsultController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('stores') @Get('stores')
@ApiOperation({ summary: 'Lista todas as lojas' }) @ApiOperation({ summary: 'Lista todas as lojas' })
@ApiResponse({ status: 200, description: 'Lista de lojas retornada com sucesso', type: [StoreDto] }) @ApiResponse({
status: 200,
description: 'Lista de lojas retornada com sucesso',
type: [StoreDto],
})
async stores(): Promise<StoreDto[]> { async stores(): Promise<StoreDto[]> {
return this.dataConsultService.stores(); return this.dataConsultService.stores();
} }
@@ -28,14 +47,24 @@ export class DataConsultController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('sellers') @Get('sellers')
@ApiOperation({ summary: 'Lista todos os vendedores' }) @ApiOperation({ summary: 'Lista todos os vendedores' })
@ApiResponse({ status: 200, description: 'Lista de vendedores retornada com sucesso', type: [SellerDto] }) @ApiResponse({
status: 200,
description: 'Lista de vendedores retornada com sucesso',
type: [SellerDto],
})
async sellers(): Promise<SellerDto[]> { async sellers(): Promise<SellerDto[]> {
return this.dataConsultService.sellers(); return this.dataConsultService.sellers();
} }
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('billings') @Get('billings')
@ApiOperation({ summary: 'Retorna informações de faturamento' }) @ApiOperation({ summary: 'Retorna informações de faturamento' })
@ApiResponse({ status: 200, description: 'Informações de faturamento retornadas com sucesso', type: [BillingDto] }) @ApiResponse({
status: 200,
description: 'Informações de faturamento retornadas com sucesso',
type: [BillingDto],
})
async billings(): Promise<BillingDto[]> { async billings(): Promise<BillingDto[]> {
return this.dataConsultService.billings(); return this.dataConsultService.billings();
} }
@@ -45,7 +74,11 @@ export class DataConsultController {
@Get('customers/:filter') @Get('customers/:filter')
@ApiOperation({ summary: 'Filtra clientes pelo parâmetro fornecido' }) @ApiOperation({ summary: 'Filtra clientes pelo parâmetro fornecido' })
@ApiParam({ name: 'filter', description: 'Filtro de busca para clientes' }) @ApiParam({ name: 'filter', description: 'Filtro de busca para clientes' })
@ApiResponse({ status: 200, description: 'Lista de clientes filtrados retornada com sucesso', type: [CustomerDto] }) @ApiResponse({
status: 200,
description: 'Lista de clientes filtrados retornada com sucesso',
type: [CustomerDto],
})
async customer(@Param('filter') filter: string): Promise<CustomerDto[]> { async customer(@Param('filter') filter: string): Promise<CustomerDto[]> {
return this.dataConsultService.customers(filter); return this.dataConsultService.customers(filter);
} }
@@ -55,7 +88,11 @@ export class DataConsultController {
@Get('products/:filter') @Get('products/:filter')
@ApiOperation({ summary: 'Busca produtos filtrados' }) @ApiOperation({ summary: 'Busca produtos filtrados' })
@ApiParam({ name: 'filter', description: 'Filtro de busca' }) @ApiParam({ name: 'filter', description: 'Filtro de busca' })
@ApiResponse({ status: 200, description: 'Lista de produtos filtrados retornada com sucesso', type: [ProductDto] }) @ApiResponse({
status: 200,
description: 'Lista de produtos filtrados retornada com sucesso',
type: [ProductDto],
})
async products(@Param('filter') filter: string): Promise<ProductDto[]> { async products(@Param('filter') filter: string): Promise<ProductDto[]> {
return this.dataConsultService.products(filter); return this.dataConsultService.products(filter);
} }
@@ -64,7 +101,11 @@ export class DataConsultController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('all') @Get('all')
@ApiOperation({ summary: 'Lista 500 produtos' }) @ApiOperation({ summary: 'Lista 500 produtos' })
@ApiResponse({ status: 200, description: 'Lista de 500 produtos retornada com sucesso', type: [ProductDto] }) @ApiResponse({
status: 200,
description: 'Lista de 500 produtos retornada com sucesso',
type: [ProductDto],
})
async getAllProducts(): Promise<ProductDto[]> { async getAllProducts(): Promise<ProductDto[]> {
return this.dataConsultService.getAllProducts(); return this.dataConsultService.getAllProducts();
} }
@@ -73,7 +114,11 @@ export class DataConsultController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('carriers/all') @Get('carriers/all')
@ApiOperation({ summary: 'Lista todas as transportadoras cadastradas' }) @ApiOperation({ summary: 'Lista todas as transportadoras cadastradas' })
@ApiResponse({ status: 200, description: 'Lista de transportadoras retornada com sucesso', type: [CarrierDto] }) @ApiResponse({
status: 200,
description: 'Lista de transportadoras retornada com sucesso',
type: [CarrierDto],
})
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getAllCarriers(): Promise<CarrierDto[]> { async getAllCarriers(): Promise<CarrierDto[]> {
return this.dataConsultService.getAllCarriers(); return this.dataConsultService.getAllCarriers();
@@ -83,9 +128,15 @@ export class DataConsultController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('carriers') @Get('carriers')
@ApiOperation({ summary: 'Busca transportadoras por período de data' }) @ApiOperation({ summary: 'Busca transportadoras por período de data' })
@ApiResponse({ status: 200, description: 'Lista de transportadoras por período retornada com sucesso', type: [CarrierDto] }) @ApiResponse({
status: 200,
description: 'Lista de transportadoras por período retornada com sucesso',
type: [CarrierDto],
})
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getCarriersByDate(@Query() query: FindCarriersDto): Promise<CarrierDto[]> { async getCarriersByDate(
@Query() query: FindCarriersDto,
): Promise<CarrierDto[]> {
return this.dataConsultService.getCarriersByDate(query); return this.dataConsultService.getCarriersByDate(query);
} }
@@ -94,17 +145,26 @@ export class DataConsultController {
@Get('carriers/order/:orderId') @Get('carriers/order/:orderId')
@ApiOperation({ summary: 'Busca transportadoras de um pedido específico' }) @ApiOperation({ summary: 'Busca transportadoras de um pedido específico' })
@ApiParam({ name: 'orderId', example: 236001388 }) @ApiParam({ name: 'orderId', example: 236001388 })
@ApiResponse({ status: 200, description: 'Lista de transportadoras do pedido retornada com sucesso', type: [CarrierDto] }) @ApiResponse({
status: 200,
description: 'Lista de transportadoras do pedido retornada com sucesso',
type: [CarrierDto],
})
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise<CarrierDto[]> { async getOrderCarriers(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<CarrierDto[]> {
return this.dataConsultService.getOrderCarriers(orderId); return this.dataConsultService.getOrderCarriers(orderId);
} }
@Get('regions') @Get('regions')
@ApiOperation({ summary: 'Lista todas as regiões cadastradas' }) @ApiOperation({ summary: 'Lista todas as regiões cadastradas' })
@ApiResponse({ status: 200, description: 'Lista de regiões retornada com sucesso', type: [RegionDto] }) @ApiResponse({
status: 200,
description: 'Lista de regiões retornada com sucesso',
type: [RegionDto],
})
async getRegions(): Promise<RegionDto[]> { async getRegions(): Promise<RegionDto[]> {
return this.dataConsultService.getRegions(); return this.dataConsultService.getRegions();
} }
} }

View File

@@ -2,18 +2,13 @@ import { Module } from '@nestjs/common';
import { DataConsultService } from './data-consult.service'; import { DataConsultService } from './data-consult.service';
import { DataConsultController } from './data-consult.controller'; import { DataConsultController } from './data-consult.controller';
import { DataConsultRepository } from './data-consult.repository'; import { DataConsultRepository } from './data-consult.repository';
import { LoggerModule } from 'src/Log/logger.module';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { RedisModule } from 'src/core/configs/cache/redis.module'; import { RedisModule } from 'src/core/configs/cache/redis.module';
import { clientes } from './clientes.module'; import { clientes } from './clientes.module';
@Module({ @Module({
imports: [LoggerModule, ConfigModule, RedisModule, clientes], imports: [ConfigModule, RedisModule, clientes],
controllers: [DataConsultController], controllers: [DataConsultController],
providers: [ providers: [DataConsultService, DataConsultRepository],
DataConsultService,
DataConsultRepository,
],
}) })
export class DataConsultModule {} export class DataConsultModule {}

View File

@@ -1,4 +1,4 @@
import { Injectable, HttpException, HttpStatus, Inject } from '@nestjs/common'; import { Injectable, HttpException, HttpStatus, Inject, Logger } from '@nestjs/common';
import { DataConsultRepository } from './data-consult.repository'; import { DataConsultRepository } from './data-consult.repository';
import { StoreDto } from './dto/store.dto'; import { StoreDto } from './dto/store.dto';
import { SellerDto } from './dto/seller.dto'; import { SellerDto } from './dto/seller.dto';
@@ -7,7 +7,6 @@ import { CustomerDto } from './dto/customer.dto';
import { ProductDto } from './dto/product.dto'; import { ProductDto } from './dto/product.dto';
import { RegionDto } from './dto/region.dto'; import { RegionDto } from './dto/region.dto';
import { CarrierDto, FindCarriersDto } from './dto/carrier.dto'; import { CarrierDto, FindCarriersDto } from './dto/carrier.dto';
import { ILogger } from '../Log/ILogger';
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider'; import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
import { IRedisClient } from '../core/configs/cache/IRedisClient'; import { IRedisClient } from '../core/configs/cache/IRedisClient';
import { getOrSetCache } from '../shared/cache.util'; import { getOrSetCache } from '../shared/cache.util';
@@ -16,6 +15,7 @@ import { DATA_SOURCE } from '../core/constants';
@Injectable() @Injectable()
export class DataConsultService { export class DataConsultService {
private readonly logger = new Logger(DataConsultService.name);
private readonly SELLERS_CACHE_KEY = 'data-consult:sellers'; private readonly SELLERS_CACHE_KEY = 'data-consult:sellers';
private readonly SELLERS_TTL = 3600; private readonly SELLERS_TTL = 3600;
private readonly STORES_TTL = 3600; private readonly STORES_TTL = 3600;
@@ -31,8 +31,7 @@ export class DataConsultService {
constructor( constructor(
private readonly repository: DataConsultRepository, private readonly repository: DataConsultRepository,
@Inject(RedisClientToken) private readonly redisClient: IRedisClient, @Inject(RedisClientToken) private readonly redisClient: IRedisClient,
@Inject('LoggerService') private readonly logger: ILogger, @Inject(DATA_SOURCE) private readonly dataSource: DataSource,
@Inject(DATA_SOURCE) private readonly dataSource: DataSource
) {} ) {}
async stores(): Promise<StoreDto[]> { async stores(): Promise<StoreDto[]> {
@@ -41,25 +40,38 @@ export class DataConsultService {
const stores = await this.repository.findStores(); const stores = await this.repository.findStores();
if (stores === null || stores === undefined) { if (stores === null || stores === undefined) {
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Resultado inválido do repositório',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
const storesArray = Array.isArray(stores) ? stores : [stores]; const storesArray = Array.isArray(stores) ? stores : [stores];
return storesArray return storesArray
.filter(store => { .filter((store) => {
if (!store || typeof store !== 'object') { if (!store || typeof store !== 'object') {
return false; return false;
} }
const hasId = store.id !== undefined && store.id !== null && store.id !== ''; const hasId =
const hasName = store.name !== undefined && store.name !== null && store.name !== ''; store.id !== undefined && store.id !== null && store.id !== '';
const hasStore = store.store !== undefined && store.store !== null && store.store !== ''; const hasName =
store.name !== undefined &&
store.name !== null &&
store.name !== '';
const hasStore =
store.store !== undefined &&
store.store !== null &&
store.store !== '';
return hasId && hasName && hasStore; return hasId && hasName && hasStore;
}) })
.map(store => new StoreDto(store)); .map((store) => new StoreDto(store));
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar lojas', error); this.logger.error('Erro ao buscar lojas', error);
throw new HttpException('Erro ao buscar lojas', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar lojas',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@@ -75,30 +87,42 @@ export class DataConsultService {
const sellers = await this.repository.findSellers(); const sellers = await this.repository.findSellers();
if (sellers === null || sellers === undefined) { if (sellers === null || sellers === undefined) {
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Resultado inválido do repositório',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
const sellersArray = Array.isArray(sellers) ? sellers : [sellers]; const sellersArray = Array.isArray(sellers) ? sellers : [sellers];
return sellersArray return sellersArray
.filter(seller => { .filter((seller) => {
if (!seller || typeof seller !== 'object') { if (!seller || typeof seller !== 'object') {
return false; return false;
} }
const hasId = seller.id !== undefined && seller.id !== null && seller.id !== ''; const hasId =
const hasName = seller.name !== undefined && seller.name !== null && seller.name !== ''; seller.id !== undefined &&
seller.id !== null &&
seller.id !== '';
const hasName =
seller.name !== undefined &&
seller.name !== null &&
seller.name !== '';
return hasId && hasName; return hasId && hasName;
}) })
.map(seller => new SellerDto(seller)); .map((seller) => new SellerDto(seller));
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar vendedores', error); this.logger.error('Erro ao buscar vendedores', error);
throw error; throw error;
} }
} },
); );
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar vendedores', error); this.logger.error('Erro ao buscar vendedores', error);
throw new HttpException('Erro ao buscar vendedores', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar vendedores',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@@ -108,25 +132,35 @@ export class DataConsultService {
const billings = await this.repository.findBillings(); const billings = await this.repository.findBillings();
if (billings === null || billings === undefined) { if (billings === null || billings === undefined) {
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Resultado inválido do repositório',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
const billingsArray = Array.isArray(billings) ? billings : [billings]; const billingsArray = Array.isArray(billings) ? billings : [billings];
return billingsArray return billingsArray
.filter(billing => { .filter((billing) => {
if (!billing || typeof billing !== 'object') { if (!billing || typeof billing !== 'object') {
return false; return false;
} }
const hasId = billing.id !== undefined && billing.id !== null && billing.id !== ''; const hasId =
billing.id !== undefined &&
billing.id !== null &&
billing.id !== '';
const hasDate = billing.date !== undefined && billing.date !== null; const hasDate = billing.date !== undefined && billing.date !== null;
const hasTotal = billing.total !== undefined && billing.total !== null; const hasTotal =
billing.total !== undefined && billing.total !== null;
return hasId && hasDate && hasTotal; return hasId && hasDate && hasTotal;
}) })
.map(billing => new BillingDto(billing)); .map((billing) => new BillingDto(billing));
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar faturamento', error); this.logger.error('Erro ao buscar faturamento', error);
throw new HttpException('Erro ao buscar faturamento', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar faturamento',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@@ -139,25 +173,40 @@ export class DataConsultService {
const customers = await this.repository.findCustomers(filter); const customers = await this.repository.findCustomers(filter);
if (customers === null || customers === undefined) { if (customers === null || customers === undefined) {
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Resultado inválido do repositório',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
const customersArray = Array.isArray(customers) ? customers : [customers]; const customersArray = Array.isArray(customers) ? customers : [customers];
return customersArray return customersArray
.filter(customer => { .filter((customer) => {
if (!customer || typeof customer !== 'object') { if (!customer || typeof customer !== 'object') {
return false; return false;
} }
const hasId = customer.id !== undefined && customer.id !== null && customer.id !== ''; const hasId =
const hasName = customer.name !== undefined && customer.name !== null && customer.name !== ''; customer.id !== undefined &&
const hasDocument = customer.document !== undefined && customer.document !== null && customer.document !== ''; customer.id !== null &&
customer.id !== '';
const hasName =
customer.name !== undefined &&
customer.name !== null &&
customer.name !== '';
const hasDocument =
customer.document !== undefined &&
customer.document !== null &&
customer.document !== '';
return hasId && hasName && hasDocument; return hasId && hasName && hasDocument;
}) })
.map(customer => new CustomerDto(customer)); .map((customer) => new CustomerDto(customer));
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar clientes', error); this.logger.error('Erro ao buscar clientes', error);
throw new HttpException('Erro ao buscar clientes', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar clientes',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@@ -168,10 +217,13 @@ export class DataConsultService {
throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST); throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST);
} }
const products = await this.repository.findProducts(filter); const products = await this.repository.findProducts(filter);
return products.map(product => new ProductDto(product)); return products.map((product) => new ProductDto(product));
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar produtos', error); this.logger.error('Erro ao buscar produtos', error);
throw new HttpException('Erro ao buscar produtos', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar produtos',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@@ -187,31 +239,48 @@ export class DataConsultService {
const products = await this.repository.findAllProducts(); const products = await this.repository.findAllProducts();
if (products === null || products === undefined) { if (products === null || products === undefined) {
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Resultado inválido do repositório',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
const productsArray = Array.isArray(products) ? products : [products]; const productsArray = Array.isArray(products)
? products
: [products];
return productsArray return productsArray
.filter(product => { .filter((product) => {
if (!product || typeof product !== 'object') { if (!product || typeof product !== 'object') {
return false; return false;
} }
const hasId = product.id !== undefined && product.id !== null && product.id !== ''; const hasId =
const hasName = product.name !== undefined && product.name !== null && product.name !== ''; product.id !== undefined &&
const hasManufacturerCode = product.manufacturerCode !== undefined && product.manufacturerCode !== null && product.manufacturerCode !== ''; product.id !== null &&
product.id !== '';
const hasName =
product.name !== undefined &&
product.name !== null &&
product.name !== '';
const hasManufacturerCode =
product.manufacturerCode !== undefined &&
product.manufacturerCode !== null &&
product.manufacturerCode !== '';
return hasId && hasName && hasManufacturerCode; return hasId && hasName && hasManufacturerCode;
}) })
.map(product => new ProductDto(product)); .map((product) => new ProductDto(product));
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar todos os produtos', error); this.logger.error('Erro ao buscar todos os produtos', error);
throw error; throw error;
} }
} },
); );
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar todos os produtos', error); this.logger.error('Erro ao buscar todos os produtos', error);
throw new HttpException('Erro ao buscar produtos', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar produtos',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@@ -227,22 +296,36 @@ export class DataConsultService {
const carriers = await this.repository.findAllCarriers(); const carriers = await this.repository.findAllCarriers();
if (carriers === null || carriers === undefined) { if (carriers === null || carriers === undefined) {
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Resultado inválido do repositório',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
const carriersArray = Array.isArray(carriers) ? carriers : [carriers]; const carriersArray = Array.isArray(carriers)
? carriers
: [carriers];
return carriersArray return carriersArray
.filter(carrier => { .filter((carrier) => {
if (!carrier || typeof carrier !== 'object') { if (!carrier || typeof carrier !== 'object') {
return false; return false;
} }
const hasCarrierId = carrier.carrierId !== undefined && carrier.carrierId !== null && carrier.carrierId !== ''; const hasCarrierId =
const hasCarrierName = carrier.carrierName !== undefined && carrier.carrierName !== null && carrier.carrierName !== ''; carrier.carrierId !== undefined &&
const hasCarrierDescription = carrier.carrierDescription !== undefined && carrier.carrierDescription !== null && carrier.carrierDescription !== ''; carrier.carrierId !== null &&
carrier.carrierId !== '';
const hasCarrierName =
carrier.carrierName !== undefined &&
carrier.carrierName !== null &&
carrier.carrierName !== '';
const hasCarrierDescription =
carrier.carrierDescription !== undefined &&
carrier.carrierDescription !== null &&
carrier.carrierDescription !== '';
return hasCarrierId && hasCarrierName && hasCarrierDescription; return hasCarrierId && hasCarrierName && hasCarrierDescription;
}) })
.map(carrier => ({ .map((carrier) => ({
carrierId: carrier.carrierId?.toString() || '', carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '', carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '', carrierDescription: carrier.carrierDescription || '',
@@ -251,19 +334,24 @@ export class DataConsultService {
this.logger.error('Erro ao buscar transportadoras', error); this.logger.error('Erro ao buscar transportadoras', error);
throw error; throw error;
} }
} },
); );
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar transportadoras', error); this.logger.error('Erro ao buscar transportadoras', error);
throw new HttpException('Erro ao buscar transportadoras', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar transportadoras',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
async getCarriersByDate(query: FindCarriersDto): Promise<CarrierDto[]> { async getCarriersByDate(query: FindCarriersDto): Promise<CarrierDto[]> {
this.logger.log(`Buscando transportadoras por período: ${JSON.stringify(query)}`); this.logger.log(
`Buscando transportadoras por período: ${JSON.stringify(query)}`,
);
try { try {
const carriers = await this.repository.findCarriersByDate(query); const carriers = await this.repository.findCarriersByDate(query);
return carriers.map(carrier => ({ return carriers.map((carrier) => ({
carrierId: carrier.carrierId?.toString() || '', carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '', carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '', carrierDescription: carrier.carrierDescription || '',
@@ -271,7 +359,10 @@ export class DataConsultService {
})); }));
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar transportadoras por período', error); this.logger.error('Erro ao buscar transportadoras por período', error);
throw new HttpException('Erro ao buscar transportadoras', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar transportadoras',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@@ -279,14 +370,17 @@ export class DataConsultService {
this.logger.log(`Buscando transportadoras do pedido: ${orderId}`); this.logger.log(`Buscando transportadoras do pedido: ${orderId}`);
try { try {
const carriers = await this.repository.findOrderCarriers(orderId); const carriers = await this.repository.findOrderCarriers(orderId);
return carriers.map(carrier => ({ return carriers.map((carrier) => ({
carrierId: carrier.carrierId?.toString() || '', carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '', carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '', carrierDescription: carrier.carrierDescription || '',
})); }));
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar transportadoras do pedido', error); this.logger.error('Erro ao buscar transportadoras do pedido', error);
throw new HttpException('Erro ao buscar transportadoras do pedido', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar transportadoras do pedido',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@@ -302,30 +396,40 @@ export class DataConsultService {
const regions = await this.repository.findRegions(); const regions = await this.repository.findRegions();
if (regions === null || regions === undefined) { if (regions === null || regions === undefined) {
throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Resultado inválido do repositório',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
const regionsArray = Array.isArray(regions) ? regions : [regions]; const regionsArray = Array.isArray(regions) ? regions : [regions];
return regionsArray return regionsArray
.filter(region => { .filter((region) => {
if (!region || typeof region !== 'object') { if (!region || typeof region !== 'object') {
return false; return false;
} }
const hasNumregiao = region.numregiao !== undefined && region.numregiao !== null; const hasNumregiao =
const hasRegiao = region.regiao !== undefined && region.regiao !== null && region.regiao !== ''; region.numregiao !== undefined && region.numregiao !== null;
const hasRegiao =
region.regiao !== undefined &&
region.regiao !== null &&
region.regiao !== '';
return hasNumregiao && hasRegiao; return hasNumregiao && hasRegiao;
}) })
.map(region => new RegionDto(region)); .map((region) => new RegionDto(region));
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar regiões', error); this.logger.error('Erro ao buscar regiões', error);
throw error; throw error;
} }
} },
); );
} catch (error) { } catch (error) {
this.logger.error('Erro ao buscar regiões', error); this.logger.error('Erro ao buscar regiões', error);
throw new HttpException('Erro ao buscar regiões', HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpException(
'Erro ao buscar regiões',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
} }

View File

@@ -4,26 +4,26 @@ import { IsOptional, IsString, IsDateString } from 'class-validator';
export class CarrierDto { export class CarrierDto {
@ApiProperty({ @ApiProperty({
description: 'ID da transportadora', description: 'ID da transportadora',
example: '123' example: '123',
}) })
carrierId: string; carrierId: string;
@ApiProperty({ @ApiProperty({
description: 'Nome da transportadora', description: 'Nome da transportadora',
example: 'TRANSPORTADORA ABC LTDA' example: 'TRANSPORTADORA ABC LTDA',
}) })
carrierName: string; carrierName: string;
@ApiProperty({ @ApiProperty({
description: 'Descrição completa da transportadora (ID - Nome)', description: 'Descrição completa da transportadora (ID - Nome)',
example: '123 - TRANSPORTADORA ABC LTDA' example: '123 - TRANSPORTADORA ABC LTDA',
}) })
carrierDescription: string; carrierDescription: string;
@ApiProperty({ @ApiProperty({
description: 'Quantidade de pedidos da transportadora no período', description: 'Quantidade de pedidos da transportadora no período',
example: 15, example: 15,
required: false required: false,
}) })
ordersCount?: number; ordersCount?: number;
} }
@@ -32,7 +32,7 @@ export class FindCarriersDto {
@ApiProperty({ @ApiProperty({
description: 'Data inicial para filtro (formato YYYY-MM-DD)', description: 'Data inicial para filtro (formato YYYY-MM-DD)',
example: '2024-01-01', example: '2024-01-01',
required: false required: false,
}) })
@IsOptional() @IsOptional()
@IsDateString() @IsDateString()
@@ -41,7 +41,7 @@ export class FindCarriersDto {
@ApiProperty({ @ApiProperty({
description: 'Data final para filtro (formato YYYY-MM-DD)', description: 'Data final para filtro (formato YYYY-MM-DD)',
example: '2024-12-31', example: '2024-12-31',
required: false required: false,
}) })
@IsOptional() @IsOptional()
@IsDateString() @IsDateString()
@@ -50,7 +50,7 @@ export class FindCarriersDto {
@ApiProperty({ @ApiProperty({
description: 'ID da filial', description: 'ID da filial',
example: '1', example: '1',
required: false required: false,
}) })
@IsOptional() @IsOptional()
@IsString() @IsString()

View File

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

View File

@@ -1,258 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
import { HealthCheckResult } from '@nestjs/terminus';
@Injectable()
export class HealthAlertService {
private readonly logger = new Logger(HealthAlertService.name);
private readonly webhookUrls: Record<string, string>;
private readonly alertThresholds: Record<string, any>;
private readonly alertCooldowns: Map<string, number> = new Map();
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {
// Configurações de webhooks para diferentes canais de alerta
this.webhookUrls = {
slack: this.configService.get<string>('ALERT_WEBHOOK_SLACK'),
teams: this.configService.get<string>('ALERT_WEBHOOK_TEAMS'),
email: this.configService.get<string>('ALERT_WEBHOOK_EMAIL'),
};
// Thresholds para diferentes tipos de alerta
this.alertThresholds = {
disk: {
criticalPercent: this.configService.get<number>('ALERT_DISK_CRITICAL_PERCENT', 90),
warningPercent: this.configService.get<number>('ALERT_DISK_WARNING_PERCENT', 80),
},
memory: {
criticalPercent: this.configService.get<number>('ALERT_MEMORY_CRITICAL_PERCENT', 90),
warningPercent: this.configService.get<number>('ALERT_MEMORY_WARNING_PERCENT', 80),
},
db: {
cooldownMinutes: this.configService.get<number>('ALERT_DB_COOLDOWN_MINUTES', 15),
},
};
}
async processHealthCheckResult(result: HealthCheckResult): Promise<void> {
try {
const { status, info, error, details } = result;
// Se o status geral não for 'ok', envie um alerta
if (status !== 'ok') {
// Verificar quais componentes estão com problema
const failedComponents = Object.entries(error)
.map(([key, value]) => ({ key, value }));
if (failedComponents.length > 0) {
await this.sendAlert('critical', 'Health Check Falhou',
`Os seguintes componentes estão com problemas: ${failedComponents.map(c => c.key).join(', ')}`,
{ result, failedComponents }
);
}
}
// Verificar alertas específicos para cada tipo de componente
if (details.disk) {
await this.checkDiskAlerts(details.disk);
}
if (details.memory_heap) {
await this.checkMemoryAlerts(details.memory_heap);
}
// Verificar alertas de banco de dados
['oracle', 'postgres'].forEach(db => {
if (details[db] && details[db].status !== 'up') {
this.checkDatabaseAlerts(db, details[db]);
}
});
} catch (error) {
this.logger.error(`Erro ao processar health check result: ${error.message}`, error.stack);
}
}
private async checkDiskAlerts(diskDetails: any): Promise<void> {
try {
if (!diskDetails.freeBytes || !diskDetails.totalBytes) {
return;
}
const usedPercent = ((diskDetails.totalBytes - diskDetails.freeBytes) / diskDetails.totalBytes) * 100;
if (usedPercent >= this.alertThresholds.disk.criticalPercent) {
await this.sendAlert('critical', 'Espaço em Disco Crítico',
`O uso de disco está em ${usedPercent.toFixed(1)}%, acima do limite crítico de ${this.alertThresholds.disk.criticalPercent}%`,
{ diskDetails, usedPercent }
);
} else if (usedPercent >= this.alertThresholds.disk.warningPercent) {
await this.sendAlert('warning', 'Alerta de Espaço em Disco',
`O uso de disco está em ${usedPercent.toFixed(1)}%, acima do limite de alerta de ${this.alertThresholds.disk.warningPercent}%`,
{ diskDetails, usedPercent }
);
}
} catch (error) {
this.logger.error(`Erro ao verificar alertas de disco: ${error.message}`);
}
}
private async checkMemoryAlerts(memoryDetails: any): Promise<void> {
try {
if (!memoryDetails.usedBytes || !memoryDetails.thresholdBytes) {
return;
}
const usedPercent = (memoryDetails.usedBytes / memoryDetails.thresholdBytes) * 100;
if (usedPercent >= this.alertThresholds.memory.criticalPercent) {
await this.sendAlert('critical', 'Uso de Memória Crítico',
`O uso de memória heap está em ${usedPercent.toFixed(1)}%, acima do limite crítico de ${this.alertThresholds.memory.criticalPercent}%`,
{ memoryDetails, usedPercent }
);
} else if (usedPercent >= this.alertThresholds.memory.warningPercent) {
await this.sendAlert('warning', 'Alerta de Uso de Memória',
`O uso de memória heap está em ${usedPercent.toFixed(1)}%, acima do limite de alerta de ${this.alertThresholds.memory.warningPercent}%`,
{ memoryDetails, usedPercent }
);
}
} catch (error) {
this.logger.error(`Erro ao verificar alertas de memória: ${error.message}`);
}
}
private async checkDatabaseAlerts(dbName: string, dbDetails: any): Promise<void> {
try {
const now = Date.now();
const lastAlertTime = this.alertCooldowns.get(dbName) || 0;
const cooldownMs = this.alertThresholds.db.cooldownMinutes * 60 * 1000;
// Verifica se já passou o período de cooldown para este banco
if (now - lastAlertTime >= cooldownMs) {
await this.sendAlert('critical', `Problema de Conexão com Banco de Dados ${dbName}`,
`A conexão com o banco de dados ${dbName} está com problemas: ${dbDetails.message || 'Erro não especificado'}`,
{ dbName, dbDetails }
);
// Atualiza o timestamp do último alerta
this.alertCooldowns.set(dbName, now);
}
} catch (error) {
this.logger.error(`Erro ao verificar alertas de banco de dados: ${error.message}`);
}
}
private async sendAlert(
severity: 'critical' | 'warning' | 'info',
title: string,
message: string,
details?: any,
): Promise<void> {
try {
const environment = this.configService.get<string>('NODE_ENV', 'development');
const appName = this.configService.get<string>('APP_NAME', 'Portal Jurunense API');
this.logger.warn(`[${severity.toUpperCase()}] ${title}: ${message}`);
const payload = {
severity,
title: `[${environment.toUpperCase()}] [${appName}] ${title}`,
message,
timestamp: new Date().toISOString(),
details: details || {},
environment,
};
// Enviar para Slack, se configurado
if (this.webhookUrls.slack) {
await this.sendSlackAlert(payload);
}
// Enviar para Microsoft Teams, se configurado
if (this.webhookUrls.teams) {
await this.sendTeamsAlert(payload);
}
// Enviar para serviço de email, se configurado
if (this.webhookUrls.email) {
await this.sendEmailAlert(payload);
}
} catch (error) {
this.logger.error(`Erro ao enviar alerta: ${error.message}`, error.stack);
}
}
private async sendSlackAlert(payload: any): Promise<void> {
try {
const slackPayload = {
text: `${payload.title}`,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: payload.title,
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Mensagem:* ${payload.message}\n*Severidade:* ${payload.severity}\n*Ambiente:* ${payload.environment}\n*Timestamp:* ${payload.timestamp}`,
},
},
],
};
await firstValueFrom(this.httpService.post(this.webhookUrls.slack, slackPayload));
} catch (error) {
this.logger.error(`Erro ao enviar alerta para Slack: ${error.message}`);
}
}
private async sendTeamsAlert(payload: any): Promise<void> {
try {
const teamsPayload = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": payload.severity === 'critical' ? "FF0000" : (payload.severity === 'warning' ? "FFA500" : "0078D7"),
"summary": payload.title,
"sections": [
{
"activityTitle": payload.title,
"activitySubtitle": `Severidade: ${payload.severity} | Ambiente: ${payload.environment}`,
"text": payload.message,
"facts": [
{
"name": "Timestamp",
"value": payload.timestamp
}
]
}
]
};
await firstValueFrom(this.httpService.post(this.webhookUrls.teams, teamsPayload));
} catch (error) {
this.logger.error(`Erro ao enviar alerta para Microsoft Teams: ${error.message}`);
}
}
private async sendEmailAlert(payload: any): Promise<void> {
try {
const emailPayload = {
subject: payload.title,
text: `${payload.message}\n\nSeveridade: ${payload.severity}\nAmbiente: ${payload.environment}\nTimestamp: ${payload.timestamp}`,
html: `<h2>${payload.title}</h2><p>${payload.message}</p><p><strong>Severidade:</strong> ${payload.severity}<br><strong>Ambiente:</strong> ${payload.environment}<br><strong>Timestamp:</strong> ${payload.timestamp}</p>`,
};
await firstValueFrom(this.httpService.post(this.webhookUrls.email, emailPayload));
} catch (error) {
this.logger.error(`Erro ao enviar alerta por email: ${error.message}`);
}
}
}

View File

@@ -1,119 +0,0 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import {
HealthCheck,
HealthCheckService,
HttpHealthIndicator,
DiskHealthIndicator,
MemoryHealthIndicator,
} from '@nestjs/terminus';
import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
import { ConfigService } from '@nestjs/config';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import * as os from 'os';
@ApiTags('Health Check')
@Controller('health')
export class HealthController {
private readonly diskPath: string;
constructor(
private health: HealthCheckService,
private http: HttpHealthIndicator,
private disk: DiskHealthIndicator,
private memory: MemoryHealthIndicator,
private typeOrmHealth: TypeOrmHealthIndicator,
private dbPoolStats: DbPoolStatsIndicator,
private configService: ConfigService,
) {
this.diskPath = os.platform() === 'win32' ? 'C:\\' : '/';
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get()
@HealthCheck()
@ApiOperation({ summary: 'Verificar saúde geral da aplicação' })
check() {
return this.health.check([
// Verifica o status da própria aplicação
() => this.http.pingCheck('api', 'http://localhost:8066/docs'),
// Verifica espaço em disco (espaço livre < 80%)
() => this.disk.checkStorage('disk_percent', {
path: this.diskPath,
thresholdPercent: 0.8, // 80%
}),
// Verifica espaço em disco (pelo menos 500MB livres)
() => this.disk.checkStorage('disk_space', {
path: this.diskPath,
threshold: 500 * 1024 * 1024, // 500MB em bytes
}),
// Verifica uso de memória (heap <150MB)
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024), // 150MB
// Verifica as conexões de banco de dados
() => this.typeOrmHealth.checkOracle(),
() => this.typeOrmHealth.checkPostgres(),
]);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('db')
@HealthCheck()
@ApiOperation({ summary: 'Verificar saúde das conexões de banco de dados' })
checkDatabase() {
return this.health.check([
() => this.typeOrmHealth.checkOracle(),
() => this.typeOrmHealth.checkPostgres(),
]);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('memory')
@HealthCheck()
@ApiOperation({ summary: 'Verificar uso de memória' })
checkMemory() {
return this.health.check([
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
() => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),
]);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('disk')
@HealthCheck()
@ApiOperation({ summary: 'Verificar espaço em disco' })
checkDisk() {
return this.health.check([
// Verificar espaço em disco usando porcentagem
() => this.disk.checkStorage('disk_percent', {
path: this.diskPath,
thresholdPercent: 0.8,
}),
// Verificar espaço em disco usando valor absoluto
() => this.disk.checkStorage('disk_space', {
path: this.diskPath,
threshold: 500 * 1024 * 1024,
}),
]);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Get('pool')
@HealthCheck()
@ApiOperation({ summary: 'Verificar estatísticas do pool de conexões' })
checkPoolStats() {
return this.health.check([
() => this.dbPoolStats.checkOraclePoolStats(),
() => this.dbPoolStats.checkPostgresPoolStats(),
]);
}
}

View File

@@ -1,44 +0,0 @@
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HttpModule } from '@nestjs/axios';
import { HealthController } from './health.controller';
import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
import { ConfigModule } from '@nestjs/config';
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
import { metricProviders } from './metrics/metrics.config';
import { CustomMetricsService } from './metrics/custom.metrics';
import { MetricsInterceptor } from './metrics/metrics.interceptor';
import { HealthAlertService } from './alert/health-alert.service';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
imports: [
TerminusModule,
HttpModule,
ConfigModule,
PrometheusModule.register({
path: '/metrics',
defaultMetrics: {
enabled: true,
},
}),
],
controllers: [HealthController],
providers: [
TypeOrmHealthIndicator,
DbPoolStatsIndicator,
CustomMetricsService,
HealthAlertService,
{
provide: APP_INTERCEPTOR,
useClass: MetricsInterceptor,
},
...metricProviders,
],
exports: [
CustomMetricsService,
HealthAlertService,
],
})
export class HealthModule {}

View File

@@ -1,193 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import {
HealthIndicator,
HealthIndicatorResult,
HealthCheckError, // Import HealthCheckError for better terminus integration
} from '@nestjs/terminus';
import { InjectConnection } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
const ORACLE_HEALTH_KEY = 'oracle_pool_stats';
const POSTGRES_HEALTH_KEY = 'postgres_pool_stats';
const ORACLE_PROGRAM_PATTERN = 'node%'; // Default pattern for Oracle
const POSTGRES_APP_NAME_PATTERN = 'nodejs%'; // Default pattern for PostgreSQL
@Injectable()
export class DbPoolStatsIndicator extends HealthIndicator {
private readonly logger = new Logger(DbPoolStatsIndicator.name);
constructor(
@InjectConnection('oracle') private readonly oracleDataSource: DataSource,
@InjectConnection('postgres') private readonly postgresDataSource: DataSource,
) {
super();
}
/**
* Verifica a integridade do pool de conexões Oracle consultando V$SESSION.
* Observações: Requer privilégios SELECT em V$SESSION e depende da coluna PROGRAM.
* Isso verifica principalmente a acessibilidade do banco de dados e o sucesso da execução da consulta.
* Considere estatísticas de pool em nível de driver para obter uma integridade de pool mais precisa, se disponível.
*
* @param key Custom key for the health indicator component.
* @param programLike Optional pattern to match the PROGRAM column in V$SESSION.
*/
async checkOraclePoolStats(
key: string = ORACLE_HEALTH_KEY,
programLike: string = ORACLE_PROGRAM_PATTERN,
): Promise<HealthIndicatorResult> {
try {
// Usar parâmetros de consulta é uma boa prática, embora menos crítica para LIKE com um padrão fixo.
// Oracle usa a sintaxe :paramName
const query = `
SELECT
COUNT(*) AS "totalConnections" -- Use quoted identifiers if needed, or match case below
FROM
V$SESSION
WHERE
TYPE = 'USER'
AND PROGRAM LIKE :pattern
`;
const params = { pattern: programLike };
const results: { totalConnections: number | string }[] =
await this.oracleDataSource.query(query, [params.pattern]); // Pass parameters as an array for Oracle usually
if (!results || results.length === 0) {
this.logger.warn(`Oracle V$SESSION query returned no results for pattern '${programLike}'`);
}
const totalConnections = parseInt(String(results?.[0]?.totalConnections ?? 0), 10);
if (isNaN(totalConnections)) {
throw new Error('Failed to parse totalConnections from Oracle V$SESSION query result.');
}
// isHealthy é verdadeiro se a consulta for executada sem gerar um erro.
// Adicione lógica aqui se contagens de conexão específicas indicarem estado não íntegro (por exemplo, > poolMax)
const isHealthy = true;
const details = {
totalConnections: totalConnections,
programPattern: programLike,
};
return this.getStatus(key, isHealthy, details);
} catch (error) {
this.logger.error(`Oracle pool stats check failed for key "${key}": ${error.message}`, error.stack);
throw new HealthCheckError(
`${key} check failed`,
this.getStatus(key, false, { message: error.message }),
);
}
}
/**
* Verifica a integridade do pool de conexões do PostgreSQL consultando pg_stat_activity.
* Observações: Depende de o application_name estar definido corretamente na string de conexão ou nas opções.
* Isso verifica principalmente a acessibilidade do banco de dados e o sucesso da execução da consulta.
* Considere estatísticas de pool em nível de driver para obter uma integridade de pool mais precisa, se disponível.
*
* @param key Custom key for the health indicator component.
* @param appNameLike Optional pattern to match the application_name column.
*/
async checkPostgresPoolStats(
key: string = POSTGRES_HEALTH_KEY,
appNameLike: string = POSTGRES_APP_NAME_PATTERN,
): Promise<HealthIndicatorResult> {
try {
const query = `
SELECT
count(*) AS "totalConnections",
sum(CASE WHEN state = 'active' THEN 1 ELSE 0 END) AS "activeConnections",
sum(CASE WHEN state = 'idle' THEN 1 ELSE 0 END) AS "idleConnections",
sum(CASE WHEN state = 'idle in transaction' THEN 1 ELSE 0 END) AS "idleInTransactionConnections"
FROM
pg_stat_activity
WHERE
datname = current_database()
AND application_name LIKE $1
`;
const params = [appNameLike];
const results: {
totalConnections: string | number;
activeConnections: string | number;
idleConnections: string | number;
idleInTransactionConnections: string | number;
}[] = await this.postgresDataSource.query(query, params);
if (!results || results.length === 0) {
throw new Error('PostgreSQL pg_stat_activity query returned no results unexpectedly.');
}
const result = results[0];
const totalConnections = parseInt(String(result.totalConnections ?? 0), 10);
const activeConnections = parseInt(String(result.activeConnections ?? 0), 10);
const idleConnections = parseInt(String(result.idleConnections ?? 0), 10);
const idleInTransactionConnections = parseInt(String(result.idleInTransactionConnections ?? 0), 10);
// Validate parsing
if (isNaN(totalConnections) || isNaN(activeConnections) || isNaN(idleConnections) || isNaN(idleInTransactionConnections)) {
throw new Error('Failed to parse connection counts from PostgreSQL pg_stat_activity query result.');
}
const isHealthy = true;
const details = {
totalConnections,
activeConnections,
idleConnections,
idleInTransactionConnections,
applicationNamePattern: appNameLike,
};
return this.getStatus(key, isHealthy, details);
} catch (error) {
this.logger.error(`PostgreSQL pool stats check failed for key "${key}": ${error.message}`, error.stack);
throw new HealthCheckError(
`${key} check failed`,
this.getStatus(key, false, { message: error.message }),
);
}
}
/**
* Convenience method to run all pool checks defined in this indicator.
* You would typically call this from your main HealthController.
*/
async checkAllPools() : Promise<HealthIndicatorResult[]> {
const results = await Promise.allSettled([
this.checkOraclePoolStats(),
this.checkPostgresPoolStats()
]);
// Processa os resultados para se ajustar à estrutura do Terminus, se necessário, ou retorna diretamente
// Observações: Métodos individuais já retornam HealthIndicatorResult ou lançam HealthCheckError
// Este método pode não ser estritamente necessário se você chamar verificações individuais no controlador.
// Para simplificar, vamos supor que o controlador chama as verificações individuais.
// Se você quisesse que esse método retornasse um único status, precisaria de mais lógica.
// Relançar erros ou agregar status.
// Example: Log results (individual methods handle the Terminus return/error)
results.forEach(result => {
if (result.status === 'rejected') {
// Already logged and thrown as HealthCheckError inside the check methods
} else {
// Optionally log success details
this.logger.log(`Pool check successful: ${JSON.stringify(result.value)}`);
}
});
return results
.filter((r): r is PromiseFulfilledResult<HealthIndicatorResult> => r.status === 'fulfilled')
.map(r => r.value);
}
}

View File

@@ -1,52 +0,0 @@
import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { InjectConnection } from '@nestjs/typeorm';
import { Connection, DataSource } from 'typeorm';
@Injectable()
export class TypeOrmHealthIndicator extends HealthIndicator {
constructor(
@InjectConnection('oracle') private oracleConnection: DataSource,
@InjectConnection('postgres') private postgresConnection: DataSource,
) {
super();
}
async checkOracle(): Promise<HealthIndicatorResult> {
const key = 'oracle';
try {
const isHealthy = this.oracleConnection.isInitialized;
const result = this.getStatus(key, isHealthy);
if (isHealthy) {
return result;
}
throw new HealthCheckError('Oracle healthcheck failed', result);
} catch (error) {
const result = this.getStatus(key, false, { message: error.message });
throw new HealthCheckError('Oracle healthcheck failed', result);
}
}
async checkPostgres(): Promise<HealthIndicatorResult> {
const key = 'postgres';
try {
const isHealthy = this.postgresConnection.isInitialized;
const result = this.getStatus(key, isHealthy);
if (isHealthy) {
return result;
}
throw new HealthCheckError('Postgres healthcheck failed', result);
} catch (error) {
const result = this.getStatus(key, false, { message: error.message });
throw new HealthCheckError('Postgres healthcheck failed', result);
}
}
}

View File

@@ -1,93 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { Counter, Gauge, Histogram } from 'prom-client';
import { InjectConnection } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
@Injectable()
export class CustomMetricsService {
constructor(
@InjectMetric('http_request_total')
private readonly requestCounter: Counter<string>,
@InjectMetric('http_request_duration_seconds')
private readonly requestDuration: Histogram<string>,
@InjectMetric('api_memory_usage_bytes')
private readonly memoryGauge: Gauge<string>,
@InjectMetric('api_db_connection_pool_used')
private readonly dbPoolUsedGauge: Gauge<string>,
@InjectMetric('api_db_connection_pool_total')
private readonly dbPoolTotalGauge: Gauge<string>,
@InjectMetric('api_db_query_duration_seconds')
private readonly dbQueryDuration: Histogram<string>,
@InjectConnection('oracle')
private oracleConnection: DataSource,
@InjectConnection('postgres')
private postgresConnection: DataSource,
) {
// Iniciar coleta de métricas de memória
this.startMemoryMetrics();
// Iniciar coleta de métricas do pool de conexões
this.startDbPoolMetrics();
}
recordHttpRequest(method: string, route: string, statusCode: number): void {
this.requestCounter.inc({ method, route, statusCode: statusCode.toString() });
}
startTimingRequest(): (labels?: Record<string, string>) => void {
const end = this.requestDuration.startTimer();
return (labels?: Record<string, string>) => end(labels);
}
recordDbQueryDuration(db: 'oracle' | 'postgres', operation: string, durationMs: number): void {
this.dbQueryDuration.observe({ db, operation }, durationMs / 1000);
}
private startMemoryMetrics(): void {
// Coletar métricas de memória a cada 15 segundos
setInterval(() => {
const memoryUsage = process.memoryUsage();
this.memoryGauge.set({ type: 'rss' }, memoryUsage.rss);
this.memoryGauge.set({ type: 'heapTotal' }, memoryUsage.heapTotal);
this.memoryGauge.set({ type: 'heapUsed' }, memoryUsage.heapUsed);
this.memoryGauge.set({ type: 'external' }, memoryUsage.external);
}, 15000);
}
private startDbPoolMetrics(): void {
// Coletar métricas do pool de conexões a cada 15 segundos
setInterval(async () => {
try {
// Tente obter estatísticas do pool do Oracle
// Nota: depende da implementação específica do OracleDB
if (this.oracleConnection && this.oracleConnection.driver) {
const oraclePoolStats = (this.oracleConnection.driver as any).pool?.getStatistics?.();
if (oraclePoolStats) {
this.dbPoolUsedGauge.set({ db: 'oracle' }, oraclePoolStats.busy || 0);
this.dbPoolTotalGauge.set({ db: 'oracle' }, oraclePoolStats.poolMax || 0);
}
}
// Tente obter estatísticas do pool do Postgres
// Nota: depende da implementação específica do TypeORM
if (this.postgresConnection && this.postgresConnection.driver) {
const pgPoolStats = (this.postgresConnection.driver as any).pool;
if (pgPoolStats) {
this.dbPoolUsedGauge.set({ db: 'postgres' }, pgPoolStats.totalCount - pgPoolStats.idleCount || 0);
this.dbPoolTotalGauge.set({ db: 'postgres' }, pgPoolStats.totalCount || 0);
}
}
} catch (error) {
console.error('Erro ao coletar métricas do pool de conexões:', error);
}
}, 15000);
}
}

View File

@@ -1,51 +0,0 @@
import {
makeCounterProvider,
makeGaugeProvider,
makeHistogramProvider
} from '@willsoto/nestjs-prometheus';
export const metricProviders = [
// Contador de requisições HTTP
makeCounterProvider({
name: 'http_request_total',
help: 'Total de requisições HTTP',
labelNames: ['method', 'route', 'statusCode'],
}),
// Histograma de duração de requisições HTTP
makeHistogramProvider({
name: 'http_request_duration_seconds',
help: 'Duração das requisições HTTP em segundos',
labelNames: ['method', 'route', 'error'], // 👈 adicionado "error"
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
}),
// Gauge para uso de memória
makeGaugeProvider({
name: 'api_memory_usage_bytes',
help: 'Uso de memória da aplicação em bytes',
labelNames: ['type'],
}),
// Gauge para conexões de banco de dados usadas
makeGaugeProvider({
name: 'api_db_connection_pool_used',
help: 'Número de conexões de banco de dados em uso',
labelNames: ['db'],
}),
// Gauge para total de conexões no pool de banco de dados
makeGaugeProvider({
name: 'api_db_connection_pool_total',
help: 'Número total de conexões no pool de banco de dados',
labelNames: ['db'],
}),
// Histograma para duração de consultas de banco de dados
makeHistogramProvider({
name: 'api_db_query_duration_seconds',
help: 'Duração das consultas de banco de dados em segundos',
labelNames: ['db', 'operation'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2],
}),
];

View File

@@ -1,64 +0,0 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CustomMetricsService } from './custom.metrics';
@Injectable()
export class MetricsInterceptor implements NestInterceptor {
constructor(private metricsService: CustomMetricsService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
if (context.getType() !== 'http') {
return next.handle();
}
const request = context.switchToHttp().getRequest();
const { method, url } = request;
// Simplificar a rota para evitar cardinalidade alta no Prometheus
// Ex: /users/123 -> /users/:id
const route = this.normalizeRoute(url);
// Inicia o timer para medir a duração da requisição
const endTimer = this.metricsService.startTimingRequest();
return next.handle().pipe(
tap({
next: (data) => {
const response = context.switchToHttp().getResponse();
const statusCode = response.statusCode;
// Registra a requisição concluída
this.metricsService.recordHttpRequest(method, route, statusCode);
// Finaliza o timer com labels adicionais
endTimer({ method, route });
},
error: (error) => {
// Determina o código de status do erro
const statusCode = error.status || 500;
// Registra a requisição com erro
this.metricsService.recordHttpRequest(method, route, statusCode);
// Finaliza o timer com labels adicionais
endTimer({ method, route, error: 'true' });
}
})
);
}
private normalizeRoute(url: string): string {
// Remove query parameters
const path = url.split('?')[0];
// Normaliza rotas com IDs e outros parâmetros dinâmicos
// Por exemplo, /users/123 -> /users/:id
return path.replace(/\/[0-9a-f]{8,}|\/[0-9]+/g, '/:id');
}
}

View File

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

View File

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

View File

@@ -1,4 +1,11 @@
import { Get, HttpException, HttpStatus, Injectable, Query, UseGuards } from '@nestjs/common'; import {
Get,
HttpException,
HttpStatus,
Injectable,
Query,
UseGuards,
} from '@nestjs/common';
import { createOracleConfig } from '../core/configs/typeorm.oracle.config'; import { createOracleConfig } from '../core/configs/typeorm.oracle.config';
import { createPostgresConfig } from '../core/configs/typeorm.postgres.config'; import { createPostgresConfig } from '../core/configs/typeorm.postgres.config';
import { CarOutDelivery } from '../core/models/car-out-delivery.model'; import { CarOutDelivery } from '../core/models/car-out-delivery.model';
@@ -8,16 +15,15 @@ import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class LogisticService { export class LogisticService {
constructor(private readonly configService: ConfigService) {} constructor(private readonly configService: ConfigService) {}
async getExpedicao() { async getExpedicao() {
const dataSource = new DataSource(createPostgresConfig(this.configService)); const dataSource = new DataSource(createPostgresConfig(this.configService));
await dataSource.initialize(); await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner(); const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
try { try {
const sqlWMS = `select dados.*,
const sqlWMS = `select dados.*,
( select count(distinct v.numero_carga) quantidade_cargas_embarcadas ( select count(distinct v.numero_carga) quantidade_cargas_embarcadas
from volume v, carga c2 from volume v, carga c2
where v.numero_carga = c2.numero where v.numero_carga = c2.numero
@@ -52,8 +58,7 @@ export class LogisticService {
where dados.data_saida >= current_date where dados.data_saida >= current_date
ORDER BY dados.data_saida desc `; ORDER BY dados.data_saida desc `;
const sql = `SELECT COUNT(DISTINCT PCCARREG.NUMCAR) as "qtde"
const sql = `SELECT COUNT(DISTINCT PCCARREG.NUMCAR) as "qtde"
,SUM(PCPEDI.QT * PCPRODUT.PESOBRUTO) as "totalKG" ,SUM(PCPEDI.QT * PCPRODUT.PESOBRUTO) as "totalKG"
,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_nao_iniciado" ,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_nao_iniciado"
,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NOT NULL ,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NOT NULL
@@ -74,37 +79,40 @@ export class LogisticService {
AND PCPEDI.TIPOENTREGA IN ('EN', 'EF') AND PCPEDI.TIPOENTREGA IN ('EN', 'EF')
AND PCCARREG.DTSAIDA = TRUNC(SYSDATE)`; AND PCCARREG.DTSAIDA = TRUNC(SYSDATE)`;
const mov = await queryRunner.manager.query(sqlWMS); const mov = await queryRunner.manager.query(sqlWMS);
const hoje = new Date(); const hoje = new Date();
let amanha = new Date(hoje); let amanha = new Date(hoje);
amanha.setDate(hoje.getDate() + 1); amanha.setDate(hoje.getDate() + 1);
const amanhaString = amanha.toISOString().split('T')[0]; const amanhaString = amanha.toISOString().split('T')[0];
amanha = new Date(amanhaString); amanha = new Date(amanhaString);
console.log(amanha); console.log(amanha);
console.log(JSON.stringify(mov)); console.log(JSON.stringify(mov));
const movFiltered = mov.filter((m) => m.data_saida.toISOString().split('T')[0] == amanha.toISOString().split('T')[0]); const movFiltered = mov.filter(
(m) =>
m.data_saida.toISOString().split('T')[0] ==
amanha.toISOString().split('T')[0],
);
return movFiltered; return movFiltered;
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
await dataSource.destroy(); await dataSource.destroy();
}
} }
}
async getDeliveries(placa: string) { async getDeliveries(placa: string) {
const dataSource = new DataSource(createOracleConfig(this.configService)); const dataSource = new DataSource(createOracleConfig(this.configService));
await dataSource.initialize(); await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner(); const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
try { try {
const sql = `SELECT PCCARREG.NUMCAR as "id"
const sql = `SELECT PCCARREG.NUMCAR as "id"
,PCCARREG.DTSAIDA as "createDate" ,PCCARREG.DTSAIDA as "createDate"
,PCCARREG.DESTINO as "comment" ,PCCARREG.DESTINO as "comment"
,PCCARREG.TOTPESO as "weight" ,PCCARREG.TOTPESO as "weight"
@@ -129,190 +137,184 @@ export class LogisticService {
AND PCCARREG.DTFECHA IS NULL AND PCCARREG.DTFECHA IS NULL
AND PCCARREG.DTSAIDA >= TRUNC(SYSDATE)`; AND PCCARREG.DTSAIDA >= TRUNC(SYSDATE)`;
const deliveries = await queryRunner.manager.query(sql); const deliveries = await queryRunner.manager.query(sql);
return deliveries;
} catch (e) {
console.log(e);
} finally {
await queryRunner.release();
await dataSource.destroy();
}
return deliveries;
} catch (e) {
console.log(e);
} finally {
await queryRunner.release();
await dataSource.destroy();
} }
}
async getStatusCar(placa: string) { async getStatusCar(placa: string) {
const dataSource = new DataSource(createPostgresConfig(this.configService)); const dataSource = new DataSource(createPostgresConfig(this.configService));
await dataSource.initialize(); await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner(); const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
try { try {
const sql = `SELECT ESTSAIDAVEICULO.CODSAIDA FROM ESTSAIDAVEICULO, PCVEICUL
const sql = `SELECT ESTSAIDAVEICULO.CODSAIDA FROM ESTSAIDAVEICULO, PCVEICUL
WHERE ESTSAIDAVEICULO.CODVEICULO = PCVEICUL.CODVEICULO WHERE ESTSAIDAVEICULO.CODVEICULO = PCVEICUL.CODVEICULO
AND PCVEICUL.PLACA = '${placa}' AND PCVEICUL.PLACA = '${placa}'
AND ESTSAIDAVEICULO.DTRETORNO IS NULL`; AND ESTSAIDAVEICULO.DTRETORNO IS NULL`;
const outCar = await queryRunner.manager.query(sql); const outCar = await queryRunner.manager.query(sql);
return { veiculoEmViagem: ( outCar.length > 0 ) ? true : false }; return { veiculoEmViagem: outCar.length > 0 ? true : false };
} catch (e) {
} catch (e) { console.log(e);
console.log(e); } finally {
} finally { await queryRunner.release();
await queryRunner.release(); await dataSource.destroy();
await dataSource.destroy();
}
} }
}
async getEmployee() { async getEmployee() {
const dataSource = new DataSource(createOracleConfig(this.configService)); const dataSource = new DataSource(createOracleConfig(this.configService));
await dataSource.initialize(); await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner(); const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
try { try {
const sql = `SELECT PCEMPR.MATRICULA as "id" const sql = `SELECT PCEMPR.MATRICULA as "id"
,PCEMPR.NOME as "name" ,PCEMPR.NOME as "name"
,PCEMPR.FUNCAO as "fuctionName" ,PCEMPR.FUNCAO as "fuctionName"
FROM PCEMPR, PCCONSUM FROM PCEMPR, PCCONSUM
WHERE PCEMPR.DTDEMISSAO IS NULL WHERE PCEMPR.DTDEMISSAO IS NULL
AND PCEMPR.CODSETOR = PCCONSUM.CODSETOREXPED AND PCEMPR.CODSETOR = PCCONSUM.CODSETOREXPED
ORDER BY PCEMPR.NOME `; ORDER BY PCEMPR.NOME `;
const dataEmployee = await queryRunner.query(sql); const dataEmployee = await queryRunner.query(sql);
return dataEmployee; return dataEmployee;
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
await dataSource.destroy(); await dataSource.destroy();
}
} }
}
async createCarOut(data: CarOutDelivery) { async createCarOut(data: CarOutDelivery) {
const dataSource = new DataSource(createPostgresConfig(this.configService));
await dataSource.initialize();
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const sqlSequence = `SELECT ESS_SAIDAVEICULO.NEXTVAL as "id" FROM DUAL`;
const dataSequence = await queryRunner.query(sqlSequence);
let i = 0;
let helperId1 = 0;
let helperId2 = 0;
let helperId3 = 0;
const image1 = '';
const image2 = '';
const image3 = '';
const image4 = '';
const dataSource = new DataSource(createPostgresConfig(this.configService)); data.helpers.forEach((helper) => {
await dataSource.initialize(); switch (i) {
const queryRunner = dataSource.createQueryRunner(); case 0:
await queryRunner.connect(); helperId1 = helper.id;
await queryRunner.startTransaction(); break;
try { case 1:
helperId2 = helper.id;
break;
case 2:
helperId3 = helper.id;
break;
}
i++;
});
const sqlSequence = `SELECT ESS_SAIDAVEICULO.NEXTVAL as "id" FROM DUAL`; for (let y = 0; y < data.photos.length; y++) {
const dataSequence = await queryRunner.query(sqlSequence); const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
let i = 0;
let helperId1 = 0;
let helperId2 = 0;
let helperId3 = 0;
const image1 = '';
const image2 = '';
const image3 = '';
const image4 = '';
data.helpers.forEach(helper => {
switch (i) {
case 0:
helperId1 = helper.id;
break;
case 1:
helperId2 = helper.id;
break;
case 2:
helperId3 = helper.id;
break;
}
i++;
});
for (let y = 0; y < data.photos.length; y++) {
const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
VALUES (${dataSequence[0].id}, 'SA', '${data.photos[y]}' )`; VALUES (${dataSequence[0].id}, 'SA', '${data.photos[y]}' )`;
await queryRunner.query(sqlImage); await queryRunner.query(sqlImage);
} }
const sqlSaidaVeiculo = `INSERT INTO ESTSAIDAVEICULO ( CODSAIDA, CODVEICULO, DTSAIDA, QTAJUDANTES, CODFUNCSAIDA ) const sqlSaidaVeiculo = `INSERT INTO ESTSAIDAVEICULO ( CODSAIDA, CODVEICULO, DTSAIDA, QTAJUDANTES, CODFUNCSAIDA )
VALUES ( ${dataSequence[0].id}, ${data.vehicleCode}, SYSDATE, ${data.helpers.length}, VALUES ( ${dataSequence[0].id}, ${data.vehicleCode}, SYSDATE, ${data.helpers.length},
${data.userCode} )`; ${data.userCode} )`;
await queryRunner.query(sqlSaidaVeiculo); await queryRunner.query(sqlSaidaVeiculo);
for (let y = 0; y < data.numberLoading.length; y++) { for (let y = 0; y < data.numberLoading.length; y++) {
const sqlLoading = `INSERT INTO ESTSAIDAVEICULOCARREG ( CODSAIDA, NUMCAR ) const sqlLoading = `INSERT INTO ESTSAIDAVEICULOCARREG ( CODSAIDA, NUMCAR )
VALUES ( ${dataSequence[0].id}, ${data.numberLoading[y]})`; VALUES ( ${dataSequence[0].id}, ${data.numberLoading[y]})`;
await queryRunner.query(sqlLoading); await queryRunner.query(sqlLoading);
const sql = `UPDATE PCCARREG SET const sql = `UPDATE PCCARREG SET
DTSAIDAVEICULO = SYSDATE DTSAIDAVEICULO = SYSDATE
,CODFUNCAJUD = ${helperId1} ,CODFUNCAJUD = ${helperId1}
,CODFUNCAJUD2 = ${helperId2} ,CODFUNCAJUD2 = ${helperId2}
,CODFUNCAJUD3 = ${helperId3} ,CODFUNCAJUD3 = ${helperId3}
,KMINICIAL = ${data.startKm} ,KMINICIAL = ${data.startKm}
WHERE NUMCAR = ${data.numberLoading[y]}`; WHERE NUMCAR = ${data.numberLoading[y]}`;
await queryRunner.query(sql); await queryRunner.query(sql);
}
} await queryRunner.commitTransaction();
await queryRunner.commitTransaction(); return { message: 'Dados da saída de veículo gravada com sucesso!' };
} catch (e) {
return { message: 'Dados da saída de veículo gravada com sucesso!'} await queryRunner.rollbackTransaction();
throw e;
} catch (e) { } finally {
await queryRunner.rollbackTransaction(); await queryRunner.release();
throw e; await dataSource.destroy();
} finally {
await queryRunner.release();
await dataSource.destroy();
}
} }
}
async createCarIn(data: CarInDelivery) { async createCarIn(data: CarInDelivery) {
const dataSource = new DataSource(createPostgresConfig(this.configService));
const dataSource = new DataSource(createPostgresConfig(this.configService)); await dataSource.initialize();
await dataSource.initialize(); const queryRunner = dataSource.createQueryRunner();
const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect();
await queryRunner.connect(); await queryRunner.startTransaction();
await queryRunner.startTransaction(); try {
try { const sqlOutCar = `SELECT ESTSAIDAVEICULO.CODSAIDA as "id"
const sqlOutCar = `SELECT ESTSAIDAVEICULO.CODSAIDA as "id"
FROM PCCARREG, PCVEICUL, ESTSAIDAVEICULO, ESTSAIDAVEICULOCARREG FROM PCCARREG, PCVEICUL, ESTSAIDAVEICULO, ESTSAIDAVEICULOCARREG
WHERE PCCARREG.CODVEICULO = PCVEICUL.CODVEICULO WHERE PCCARREG.CODVEICULO = PCVEICUL.CODVEICULO
AND PCCARREG.NUMCAR = ESTSAIDAVEICULOCARREG.NUMCAR AND PCCARREG.NUMCAR = ESTSAIDAVEICULOCARREG.NUMCAR
AND ESTSAIDAVEICULOCARREG.CODSAIDA = ESTSAIDAVEICULO.CODSAIDA AND ESTSAIDAVEICULOCARREG.CODSAIDA = ESTSAIDAVEICULO.CODSAIDA
-- AND ESTSAIDAVEICULO.DTRETORNO IS NULL -- AND ESTSAIDAVEICULO.DTRETORNO IS NULL
AND PCVEICUL.PLACA = '${data.licensePlate}'`; AND PCVEICUL.PLACA = '${data.licensePlate}'`;
const dataOutCar = await queryRunner.query(sqlOutCar); const dataOutCar = await queryRunner.query(sqlOutCar);
if ( dataOutCar.length == 0 ) { if (dataOutCar.length == 0) {
throw new HttpException('Não foi localiza viagens em aberto para este veículo.', HttpStatus.BAD_REQUEST ); throw new HttpException(
} 'Não foi localiza viagens em aberto para este veículo.',
HttpStatus.BAD_REQUEST,
);
}
const i = 0; const i = 0;
const image1 = ''; const image1 = '';
const image2 = ''; const image2 = '';
const image3 = ''; const image3 = '';
const image4 = ''; const image4 = '';
for (let y = 0; y < data.invoices.length; y++) { for (let y = 0; y < data.invoices.length; y++) {
const invoice = data.invoices[y]; const invoice = data.invoices[y];
const sqlInvoice = `INSERT INTO ESTRETORNONF ( CODSAIDA, NUMCAR, NUMNOTA, SITUACAO, MOTIVO ) const sqlInvoice = `INSERT INTO ESTRETORNONF ( CODSAIDA, NUMCAR, NUMNOTA, SITUACAO, MOTIVO )
VALUES ( ${dataOutCar[0].id}, ${invoice.loadingNumber}, ${invoice.invoiceNumber}, VALUES ( ${dataOutCar[0].id}, ${invoice.loadingNumber}, ${invoice.invoiceNumber},
'${invoice.status}', '${invoice.reasonText}')`; '${invoice.status}', '${invoice.reasonText}')`;
await queryRunner.query(sqlInvoice); await queryRunner.query(sqlInvoice);
} }
const updateCarreg = `UPDATE PCCARREG SET const updateCarreg = `UPDATE PCCARREG SET
PCCARREG.DTRETORNO = SYSDATE PCCARREG.DTRETORNO = SYSDATE
,PCCARREG.KMFINAL = ${data.finalKm} ,PCCARREG.KMFINAL = ${data.finalKm}
WHERE PCCARREG.NUMCAR IN ( SELECT SC.NUMCAR WHERE PCCARREG.NUMCAR IN ( SELECT SC.NUMCAR
FROM ESTSAIDAVEICULOCARREG SC FROM ESTSAIDAVEICULOCARREG SC
WHERE SC.CODSAIDA = ${dataOutCar[0].id} )`; WHERE SC.CODSAIDA = ${dataOutCar[0].id} )`;
await queryRunner.query(updateCarreg); await queryRunner.query(updateCarreg);
for (let i = 0; i < data.images.length; i++) { for (let i = 0; i < data.images.length; i++) {
const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL ) const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
VALUES (${dataOutCar[0].id}, 'RE', '${data.images[i]}' )`; VALUES (${dataOutCar[0].id}, 'RE', '${data.images[i]}' )`;
await queryRunner.query(sqlImage); await queryRunner.query(sqlImage);
} }
const sqlInCar = `UPDATE ESTSAIDAVEICULO SET const sqlInCar = `UPDATE ESTSAIDAVEICULO SET
ESTSAIDAVEICULO.DTRETORNO = SYSDATE ESTSAIDAVEICULO.DTRETORNO = SYSDATE
,ESTSAIDAVEICULO.QTPALETES_PBR = ${data.qtdPaletesPbr} ,ESTSAIDAVEICULO.QTPALETES_PBR = ${data.qtdPaletesPbr}
,ESTSAIDAVEICULO.QTPALETES_CIM = ${data.qtdPaletesCim} ,ESTSAIDAVEICULO.QTPALETES_CIM = ${data.qtdPaletesCim}
@@ -323,25 +325,23 @@ export class LogisticService {
,ESTSAIDAVEICULO.OBSSOBRA = '${data.observationRemnant}' ,ESTSAIDAVEICULO.OBSSOBRA = '${data.observationRemnant}'
WHERE ESTSAIDAVEICULO.CODSAIDA = ${dataOutCar[0].id}`; WHERE ESTSAIDAVEICULO.CODSAIDA = ${dataOutCar[0].id}`;
await queryRunner.query(sqlInCar); await queryRunner.query(sqlInCar);
for (let i = 0; i < data.imagesRemnant.length; i++) { for (let i = 0; i < data.imagesRemnant.length; i++) {
const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL ) const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL )
VALUES (${dataOutCar[0].id}, 'SO', '${data.imagesRemnant[i]}' )`; VALUES (${dataOutCar[0].id}, 'SO', '${data.imagesRemnant[i]}' )`;
await queryRunner.query(sqlImage); await queryRunner.query(sqlImage);
} }
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
return { message: 'Dados de retorno do veículo gravada com sucesso!'} return { message: 'Dados de retorno do veículo gravada com sucesso!' };
} catch (e) {
} catch (e) { await queryRunner.rollbackTransaction();
await queryRunner.rollbackTransaction(); console.log(e);
console.log(e); throw e;
throw e; } finally {
} finally { await queryRunner.release();
await queryRunner.release(); await dataSource.destroy();
await dataSource.destroy();
}
} }
}
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,11 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import {
ApiTags,
ApiOperation,
ApiParam,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { OrdersPaymentService } from './orders-payment.service'; import { OrdersPaymentService } from './orders-payment.service';
import { OrderDto } from './dto/order.dto'; import { OrderDto } from './dto/order.dto';
import { PaymentDto } from './dto/payment.dto'; import { PaymentDto } from './dto/payment.dto';
@@ -12,66 +18,65 @@ import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('api/v1/orders-payment') @Controller('api/v1/orders-payment')
export class OrdersPaymentController { export class OrdersPaymentController {
constructor(private readonly orderPaymentService: OrdersPaymentService) {}
constructor(private readonly orderPaymentService: OrdersPaymentService){} @Get('orders/:id')
@ApiOperation({ summary: 'Lista todos os pedidos de uma loja' })
@ApiParam({ name: 'id', description: 'ID da loja' })
@ApiResponse({
status: 200,
description: 'Lista de pedidos retornada com sucesso',
type: [OrderDto],
})
async findOrders(@Param('id') storeId: string): Promise<OrderDto[]> {
return this.orderPaymentService.findOrders(storeId, 0);
}
@Get('orders/:id') @Get('orders/:id/:orderId')
@ApiOperation({ summary: 'Lista todos os pedidos de uma loja' }) @ApiOperation({ summary: 'Busca um pedido específico' })
@ApiParam({ name: 'id', description: 'ID da loja' }) @ApiParam({ name: 'id', description: 'ID da loja' })
@ApiResponse({ @ApiParam({ name: 'orderId', description: 'ID do pedido' })
status: 200, @ApiResponse({
description: 'Lista de pedidos retornada com sucesso', status: 200,
type: [OrderDto] description: 'Pedido retornado com sucesso',
}) type: OrderDto,
async findOrders(@Param('id') storeId: string): Promise<OrderDto[]> { })
return this.orderPaymentService.findOrders(storeId, 0); async findOrder(
} @Param('id') storeId: string,
@Param('orderId') orderId: number,
): Promise<OrderDto> {
const orders = await this.orderPaymentService.findOrders(storeId, orderId);
return orders[0];
}
@Get('orders/:id/:orderId') @Get('payments/:id')
@ApiOperation({ summary: 'Busca um pedido específico' }) @ApiOperation({ summary: 'Lista todos os pagamentos de um pedido' })
@ApiParam({ name: 'id', description: 'ID da loja' }) @ApiParam({ name: 'id', description: 'ID do pedido' })
@ApiParam({ name: 'orderId', description: 'ID do pedido' }) @ApiResponse({
@ApiResponse({ status: 200,
status: 200, description: 'Lista de pagamentos retornada com sucesso',
description: 'Pedido retornado com sucesso', type: [PaymentDto],
type: OrderDto })
}) async findPayments(@Param('id') orderId: number): Promise<PaymentDto[]> {
async findOrder( return this.orderPaymentService.findPayments(orderId);
@Param('id') storeId: string, }
@Param('orderId') orderId: number, @Post('payments/create')
): Promise<OrderDto> { @ApiOperation({ summary: 'Cria um novo pagamento' })
const orders = await this.orderPaymentService.findOrders(storeId, orderId); @ApiResponse({
return orders[0]; status: 201,
} description: 'Pagamento criado com sucesso',
})
async createPayment(@Body() data: CreatePaymentDto): Promise<void> {
return this.orderPaymentService.createPayment(data);
}
@Get('payments/:id') @Post('invoice/create')
@ApiOperation({ summary: 'Lista todos os pagamentos de um pedido' }) @ApiOperation({ summary: 'Cria uma nova fatura' })
@ApiParam({ name: 'id', description: 'ID do pedido' }) @ApiResponse({
@ApiResponse({ status: 201,
status: 200, description: 'Fatura criada com sucesso',
description: 'Lista de pagamentos retornada com sucesso', })
type: [PaymentDto] async createInvoice(@Body() data: CreateInvoiceDto): Promise<void> {
}) return this.orderPaymentService.createInvoice(data);
async findPayments(@Param('id') orderId: number): Promise<PaymentDto[]> { }
return this.orderPaymentService.findPayments(orderId); }
}
@Post('payments/create')
@ApiOperation({ summary: 'Cria um novo pagamento' })
@ApiResponse({
status: 201,
description: 'Pagamento criado com sucesso'
})
async createPayment(@Body() data: CreatePaymentDto): Promise<void> {
return this.orderPaymentService.createPayment(data);
}
@Post('invoice/create')
@ApiOperation({ summary: 'Cria uma nova fatura' })
@ApiResponse({
status: 201,
description: 'Fatura criada com sucesso'
})
async createInvoice(@Body() data: CreateInvoiceDto): Promise<void> {
return this.orderPaymentService.createInvoice(data);
}
}

View File

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

View File

@@ -9,16 +9,16 @@ import { CreateInvoiceDto } from './dto/create-invoice.dto';
@Injectable() @Injectable()
export class OrdersPaymentService { export class OrdersPaymentService {
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
@Inject(DATA_SOURCE) private readonly dataSource: DataSource @Inject(DATA_SOURCE) private readonly dataSource: DataSource,
) {} ) {}
async findOrders(storeId: string, orderId: number): Promise<OrderDto[]> { async findOrders(storeId: string, orderId: number): Promise<OrderDto[]> {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
try { try {
const sql = `SELECT PCPEDC.DATA as "createDate" const sql = `SELECT PCPEDC.DATA as "createDate"
,PCPEDC.CODFILIAL as "storeId" ,PCPEDC.CODFILIAL as "storeId"
,PCPEDC.NUMPED as "orderId" ,PCPEDC.NUMPED as "orderId"
,PCPEDC.CODCLI as "customerId" ,PCPEDC.CODCLI as "customerId"
@@ -42,23 +42,23 @@ export class OrdersPaymentService {
AND PCPEDC.POSICAO IN ('L') AND PCPEDC.POSICAO IN ('L')
AND PCPEDC.DATA >= TRUNC(SYSDATE) - 5 AND PCPEDC.DATA >= TRUNC(SYSDATE) - 5
AND PCPEDC.CODFILIAL = ${storeId} `; AND PCPEDC.CODFILIAL = ${storeId} `;
let sqlWhere = ''; let sqlWhere = '';
if (orderId > 0) { if (orderId > 0) {
sqlWhere += ` AND PCPEDC.NUMPED = ${orderId}`; sqlWhere += ` AND PCPEDC.NUMPED = ${orderId}`;
} }
const orders = await queryRunner.manager.query(sql + sqlWhere); const orders = await queryRunner.manager.query(sql + sqlWhere);
return orders.map(order => new OrderDto(order)); return orders.map((order) => new OrderDto(order));
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
}
} }
}
async findPayments(orderId: number): Promise<PaymentDto[]> { async findPayments(orderId: number): Promise<PaymentDto[]> {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
try { try {
const sql = `SELECT const sql = `SELECT
ESTPAGAMENTO.NUMORCA as "orderId" ESTPAGAMENTO.NUMORCA as "orderId"
,ESTPAGAMENTO.DTPAGAMENTO as "payDate" ,ESTPAGAMENTO.DTPAGAMENTO as "payDate"
,ESTPAGAMENTO.CARTAO as "card" ,ESTPAGAMENTO.CARTAO as "card"
@@ -72,49 +72,49 @@ export class OrdersPaymentService {
FROM ESTPAGAMENTO FROM ESTPAGAMENTO
WHERE ESTPAGAMENTO.NUMORCA = ${orderId}`; WHERE ESTPAGAMENTO.NUMORCA = ${orderId}`;
const payments = await queryRunner.manager.query(sql); const payments = await queryRunner.manager.query(sql);
return payments.map(payment => new PaymentDto(payment)); return payments.map((payment) => new PaymentDto(payment));
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
}
} }
}
async createPayment(payment: CreatePaymentDto): Promise<void> { async createPayment(payment: CreatePaymentDto): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
await queryRunner.startTransaction(); await queryRunner.startTransaction();
try { try {
const sql = `INSERT INTO ESTPAGAMENTO ( NUMORCA, DTPAGAMENTO, CARTAO, CODAUTORIZACAO, CODRESPOSTA, DTREQUISICAO, DTSERVIDOR, IDTRANSACAO, const sql = `INSERT INTO ESTPAGAMENTO ( NUMORCA, DTPAGAMENTO, CARTAO, CODAUTORIZACAO, CODRESPOSTA, DTREQUISICAO, DTSERVIDOR, IDTRANSACAO,
NSU, PARCELAS, VALOR, NOMEBANDEIRA, FORMAPAGTO, DTPROCESSAMENTO, CODFUNC ) NSU, PARCELAS, VALOR, NOMEBANDEIRA, FORMAPAGTO, DTPROCESSAMENTO, CODFUNC )
VALUES ( ${payment.orderId}, TRUNC(SYSDATE), '${payment.card}', '${payment.auth}', '00', SYSDATE, SYSDATE, NULL, VALUES ( ${payment.orderId}, TRUNC(SYSDATE), '${payment.card}', '${payment.auth}', '00', SYSDATE, SYSDATE, NULL,
'${payment.nsu}', ${payment.installments}, ${payment.amount}, '${payment.flagName}', '${payment.nsu}', ${payment.installments}, ${payment.amount}, '${payment.flagName}',
'${payment.paymentType}', SYSDATE, ${payment.userId} ) `; '${payment.paymentType}', SYSDATE, ${payment.userId} ) `;
await queryRunner.manager.query(sql); await queryRunner.manager.query(sql);
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
} catch (error) { } catch (error) {
await queryRunner.rollbackTransaction(); await queryRunner.rollbackTransaction();
throw error; throw error;
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
}
} }
}
async createInvoice(data: CreateInvoiceDto): Promise<void> { async createInvoice(data: CreateInvoiceDto): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner(); const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
await queryRunner.startTransaction(); await queryRunner.startTransaction();
try { try {
const sql = `BEGIN const sql = `BEGIN
ESK_FATURAMENTO.FATURAMENTO_VENDA_ASSISTIDA(${data.orderId}, ${data.userId}); ESK_FATURAMENTO.FATURAMENTO_VENDA_ASSISTIDA(${data.orderId}, ${data.userId});
END;`; END;`;
await queryRunner.manager.query(sql); await queryRunner.manager.query(sql);
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
} catch (error) { } catch (error) {
await queryRunner.rollbackTransaction(); await queryRunner.rollbackTransaction();
throw error; throw error;
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
}
} }
}
} }

View File

@@ -4,9 +4,7 @@ import { DebDto } from '../dto/DebDto';
@Injectable() @Injectable()
export class DebService { export class DebService {
constructor( constructor(private readonly debRepository: DebRepository) {}
private readonly debRepository: DebRepository,
) {}
/** /**
* Busca débitos por CPF ou CGCENT * Busca débitos por CPF ou CGCENT
@@ -21,6 +19,10 @@ export class DebService {
matricula?: number, matricula?: number,
cobranca?: string, cobranca?: string,
): Promise<DebDto[]> { ): Promise<DebDto[]> {
return await this.debRepository.findByCpfCgcent(cpfCgcent, matricula, cobranca); return await this.debRepository.findByCpfCgcent(
cpfCgcent,
matricula,
cobranca,
);
} }
} }

View File

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

View File

@@ -18,7 +18,8 @@ export class DebController {
@Get('find-by-cpf') @Get('find-by-cpf')
@ApiOperation({ @ApiOperation({
summary: 'Busca débitos por CPF/CGCENT', summary: 'Busca débitos por CPF/CGCENT',
description: 'Busca débitos de um cliente usando CPF ou CGCENT. Opcionalmente pode filtrar por matrícula do funcionário ou código de cobrança.', description:
'Busca débitos de um cliente usando CPF ou CGCENT. Opcionalmente pode filtrar por matrícula do funcionário ou código de cobrança.',
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -34,9 +35,7 @@ export class DebController {
description: 'Erro interno do servidor', description: 'Erro interno do servidor',
}) })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async findByCpfCgcent( async findByCpfCgcent(@Query() query: FindDebDto): Promise<DebDto[]> {
@Query() query: FindDebDto,
): Promise<DebDto[]> {
return await this.debService.findByCpfCgcent( return await this.debService.findByCpfCgcent(
query.cpfCgcent, query.cpfCgcent,
query.matricula, query.matricula,

View File

@@ -7,21 +7,26 @@ import {
Query, Query,
UsePipes, UsePipes,
UseGuards, UseGuards,
UseInterceptors,
ValidationPipe, ValidationPipe,
HttpException, HttpException,
HttpStatus, HttpStatus,
DefaultValuePipe, DefaultValuePipe,
ParseBoolPipe, ParseBoolPipe,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags, ApiQuery, ApiParam, ApiResponse } from '@nestjs/swagger'; import {
import { ResponseInterceptor } from '../../common/response.interceptor'; ApiBearerAuth,
ApiOperation,
ApiTags,
ApiQuery,
ApiParam,
ApiResponse,
} from '@nestjs/swagger';
import { OrdersService } from '../application/orders.service'; import { OrdersService } from '../application/orders.service';
import { FindOrdersDto } from '../dto/find-orders.dto'; import { FindOrdersDto } from '../dto/find-orders.dto';
import { FindOrdersByDeliveryDateDto } from '../dto/find-orders-by-delivery-date.dto'; import { FindOrdersByDeliveryDateDto } from '../dto/find-orders-by-delivery-date.dto';
import { JwtAuthGuard, } from 'src/auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { InvoiceDto } from '../dto/find-invoice.dto'; import { InvoiceDto } from '../dto/find-invoice.dto';
import { OrderItemDto } from "../dto/OrderItemDto"; import { OrderItemDto } from '../dto/OrderItemDto';
import { LeadtimeDto } from '../dto/leadtime.dto'; import { LeadtimeDto } from '../dto/leadtime.dto';
import { CutItemDto } from '../dto/CutItemDto'; import { CutItemDto } from '../dto/CutItemDto';
import { OrderDeliveryDto } from '../dto/OrderDeliveryDto'; import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
@@ -34,7 +39,6 @@ import { OrderResponseDto } from '../dto/order-response.dto';
import { MarkResponseDto } from '../dto/mark-response.dto'; import { MarkResponseDto } from '../dto/mark-response.dto';
import { EstLogTransferResponseDto } from '../dto/estlogtransfer.dto'; import { EstLogTransferResponseDto } from '../dto/estlogtransfer.dto';
@ApiTags('Orders') @ApiTags('Orders')
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@@ -45,15 +49,47 @@ export class OrdersController {
@Get('find') @Get('find')
@ApiOperation({ @ApiOperation({
summary: 'Busca pedidos', summary: 'Busca pedidos',
description: 'Busca pedidos com filtros avançados. Suporta filtros por data, cliente, vendedor, status, tipo de entrega e status de transferência.' description:
'Busca pedidos com filtros avançados. Suporta filtros por data, cliente, vendedor, status, tipo de entrega e status de transferência.',
})
@ApiQuery({
name: 'includeCheckout',
required: false,
type: 'boolean',
description: 'Incluir dados de checkout',
})
@ApiQuery({
name: 'statusTransfer',
required: false,
type: 'string',
description:
'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)',
})
@ApiQuery({
name: 'markId',
required: false,
type: 'number',
description: 'ID da marca para filtrar pedidos',
})
@ApiQuery({
name: 'markName',
required: false,
type: 'string',
description: 'Nome da marca para filtrar pedidos (busca parcial)',
})
@ApiQuery({
name: 'hasPreBox',
required: false,
type: 'boolean',
description:
'Filtrar pedidos que tenham registros na tabela de transfer log',
}) })
@ApiQuery({ name: 'includeCheckout', required: false, type: 'boolean', description: 'Incluir dados de checkout' })
@ApiQuery({ name: 'statusTransfer', required: false, type: 'string', description: 'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)' })
@ApiQuery({ name: 'markId', required: false, type: 'number', description: 'ID da marca para filtrar pedidos' })
@ApiQuery({ name: 'markName', required: false, type: 'string', description: 'Nome da marca para filtrar pedidos (busca parcial)' })
@ApiQuery({ name: 'hasPreBox', required: false, type: 'boolean', description: 'Filtrar pedidos que tenham registros na tabela de transfer log' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Lista de pedidos retornada com sucesso', type: [OrderResponseDto] }) @ApiResponse({
status: 200,
description: 'Lista de pedidos retornada com sucesso',
type: [OrderResponseDto],
})
findOrders( findOrders(
@Query() query: FindOrdersDto, @Query() query: FindOrdersDto,
@Query('includeCheckout', new DefaultValuePipe(false), ParseBoolPipe) @Query('includeCheckout', new DefaultValuePipe(false), ParseBoolPipe)
@@ -68,17 +104,42 @@ export class OrdersController {
@Get('find-by-delivery-date') @Get('find-by-delivery-date')
@ApiOperation({ @ApiOperation({
summary: 'Busca pedidos por data de entrega', summary: 'Busca pedidos por data de entrega',
description: 'Busca pedidos filtrados por data de entrega. Suporta filtros adicionais como status de transferência, cliente, vendedor, etc.' description:
'Busca pedidos filtrados por data de entrega. Suporta filtros adicionais como status de transferência, cliente, vendedor, etc.',
})
@ApiQuery({
name: 'statusTransfer',
required: false,
type: 'string',
description:
'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)',
})
@ApiQuery({
name: 'markId',
required: false,
type: 'number',
description: 'ID da marca para filtrar pedidos',
})
@ApiQuery({
name: 'markName',
required: false,
type: 'string',
description: 'Nome da marca para filtrar pedidos (busca parcial)',
})
@ApiQuery({
name: 'hasPreBox',
required: false,
type: 'boolean',
description:
'Filtrar pedidos que tenham registros na tabela de transfer log',
}) })
@ApiQuery({ name: 'statusTransfer', required: false, type: 'string', description: 'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)' })
@ApiQuery({ name: 'markId', required: false, type: 'number', description: 'ID da marca para filtrar pedidos' })
@ApiQuery({ name: 'markName', required: false, type: 'string', description: 'Nome da marca para filtrar pedidos (busca parcial)' })
@ApiQuery({ name: 'hasPreBox', required: false, type: 'boolean', description: 'Filtrar pedidos que tenham registros na tabela de transfer log' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Lista de pedidos por data de entrega retornada com sucesso', type: [OrderResponseDto] }) @ApiResponse({
findOrdersByDeliveryDate( status: 200,
@Query() query: FindOrdersByDeliveryDateDto, description: 'Lista de pedidos por data de entrega retornada com sucesso',
) { type: [OrderResponseDto],
})
findOrdersByDeliveryDate(@Query() query: FindOrdersByDeliveryDateDto) {
return this.ordersService.findOrdersByDeliveryDate(query); return this.ordersService.findOrdersByDeliveryDate(query);
} }
@@ -86,20 +147,16 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca fechamento de caixa para um pedido' }) @ApiOperation({ summary: 'Busca fechamento de caixa para um pedido' })
@ApiParam({ name: 'orderId' }) @ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
getOrderCheckout( getOrderCheckout(@Param('orderId', ParseIntPipe) orderId: number) {
@Param('orderId', ParseIntPipe) orderId: number,
) {
return this.ordersService.getOrderCheckout(orderId); return this.ordersService.getOrderCheckout(orderId);
} }
@Get('invoice/:chavenfe') @Get('invoice/:chavenfe')
@ApiParam({ @ApiParam({
name: 'chavenfe', name: 'chavenfe',
required: true, required: true,
description: 'Chave da Nota Fiscal (44 dígitos)', description: 'Chave da Nota Fiscal (44 dígitos)',
}) })
@ApiOperation({ summary: 'Busca NF pela chave' }) @ApiOperation({ summary: 'Busca NF pela chave' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getInvoice(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> { async getInvoice(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> {
@@ -117,7 +174,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca PELO numero do pedido' }) @ApiOperation({ summary: 'Busca PELO numero do pedido' })
@ApiParam({ name: 'orderId' }) @ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderItemDto[]> { async getItens(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderItemDto[]> {
try { try {
return await this.ordersService.getItens(orderId.toString()); return await this.ordersService.getItens(orderId.toString());
} catch (error) { } catch (error) {
@@ -131,7 +190,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca itens cortados do pedido' }) @ApiOperation({ summary: 'Busca itens cortados do pedido' })
@ApiParam({ name: 'orderId' }) @ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getCutItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<CutItemDto[]> { async getCutItens(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<CutItemDto[]> {
try { try {
return await this.ordersService.getCutItens(orderId.toString()); return await this.ordersService.getCutItens(orderId.toString());
} catch (error) { } catch (error) {
@@ -146,7 +207,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca dados de entrega do pedido' }) @ApiOperation({ summary: 'Busca dados de entrega do pedido' })
@ApiParam({ name: 'orderId' }) @ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getOrderDelivery(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderDeliveryDto | null> { async getOrderDelivery(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderDeliveryDto | null> {
try { try {
return await this.ordersService.getOrderDelivery(orderId.toString()); return await this.ordersService.getOrderDelivery(orderId.toString());
} catch (error) { } catch (error) {
@@ -157,50 +220,66 @@ export class OrdersController {
} }
} }
@Get('transfer/:orderId') @Get('transfer/:orderId')
@ApiOperation({ summary: 'Consulta pedidos de transferência' }) @ApiOperation({ summary: 'Consulta pedidos de transferência' })
@ApiParam({ name: 'orderId' }) @ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getTransfer(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderTransferDto[] | null> { async getTransfer(
try { @Param('orderId', ParseIntPipe) orderId: number,
return await this.ordersService.getTransfer(orderId); ): Promise<OrderTransferDto[] | null> {
} catch (error) { try {
throw new HttpException( return await this.ordersService.getTransfer(orderId);
error.message || 'Erro ao buscar transferências do pedido', } catch (error) {
error.status || HttpStatus.INTERNAL_SERVER_ERROR, throw new HttpException(
); error.message || 'Erro ao buscar transferências do pedido',
} error.status || HttpStatus.INTERNAL_SERVER_ERROR,
} );
@Get('status/:orderId')
@ApiOperation({ summary: 'Consulta status do pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getStatusOrder(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderStatusDto[] | null> {
try {
return await this.ordersService.getStatusOrder(orderId);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar status do pedido',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@Get('status/:orderId')
@ApiOperation({ summary: 'Consulta status do pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getStatusOrder(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderStatusDto[] | null> {
try {
return await this.ordersService.getStatusOrder(orderId);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar status do pedido',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get(':orderId/deliveries') @Get(':orderId/deliveries')
@ApiOperation({ summary: 'Consulta entregas do pedido' }) @ApiOperation({ summary: 'Consulta entregas do pedido' })
@ApiParam({ name: 'orderId' }) @ApiParam({ name: 'orderId' })
@ApiQuery({ name: 'createDateIni', required: false, description: 'Data inicial para filtro (formato YYYY-MM-DD)' }) @ApiQuery({
@ApiQuery({ name: 'createDateEnd', required: false, description: 'Data final para filtro (formato YYYY-MM-DD)' }) name: 'createDateIni',
required: false,
description: 'Data inicial para filtro (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'createDateEnd',
required: false,
description: 'Data final para filtro (formato YYYY-MM-DD)',
})
async getOrderDeliveries( async getOrderDeliveries(
@Param('orderId', ParseIntPipe) orderId: number, @Param('orderId', ParseIntPipe) orderId: number,
@Query('createDateIni') createDateIni?: string, @Query('createDateIni') createDateIni?: string,
@Query('createDateEnd') createDateEnd?: string, @Query('createDateEnd') createDateEnd?: string,
): Promise<OrderDeliveryDto[]> { ): Promise<OrderDeliveryDto[]> {
// Definir datas padrão caso não sejam fornecidas // Definir datas padrão caso não sejam fornecidas
const defaultDateIni = createDateIni || new Date(new Date().setDate(new Date().getDate() - 30)).toISOString().split('T')[0]; const defaultDateIni =
const defaultDateEnd = createDateEnd || new Date().toISOString().split('T')[0]; createDateIni ||
new Date(new Date().setDate(new Date().getDate() - 30))
.toISOString()
.split('T')[0];
const defaultDateEnd =
createDateEnd || new Date().toISOString().split('T')[0];
return this.ordersService.getOrderDeliveries(orderId.toString(), { return this.ordersService.getOrderDeliveries(orderId.toString(), {
createDateIni: defaultDateIni, createDateIni: defaultDateIni,
@@ -208,175 +287,275 @@ export class OrdersController {
}); });
} }
@Get('leadtime/:orderId')
@Get('leadtime/:orderId') @ApiOperation({ summary: 'Consulta leadtime do pedido' })
@ApiOperation({ summary: 'Consulta leadtime do pedido' })
@ApiParam({ name: 'orderId' }) @ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async getLeadtime(@Param('orderId', ParseIntPipe) orderId: number): Promise<LeadtimeDto[]> { async getLeadtime(
try { @Param('orderId', ParseIntPipe) orderId: number,
): Promise<LeadtimeDto[]> {
try {
return await this.ordersService.getLeadtime(orderId.toString()); return await this.ordersService.getLeadtime(orderId.toString());
} catch (error) { } catch (error) {
throw new HttpException( throw new HttpException(
error.message || 'Erro ao buscar leadtime do pedido', error.message || 'Erro ao buscar leadtime do pedido',
error.status || HttpStatus.INTERNAL_SERVER_ERROR, error.status || HttpStatus.INTERNAL_SERVER_ERROR,
); );
}
}
@Post('invoice/check')
@ApiOperation({ summary: 'Cria conferência de nota fiscal' })
@UsePipes(new ValidationPipe({ transform: true }))
async createInvoiceCheck(
@Body() invoice: InvoiceCheckDto,
): Promise<{ message: string }> {
try {
return await this.ordersService.createInvoiceCheck(invoice);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao salvar conferência',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('carriers/:orderId')
@ApiOperation({ summary: 'Busca transportadoras do pedido' })
@ApiParam({ name: 'orderId', example: 236001388 })
@UsePipes(new ValidationPipe({ transform: true }))
async getOrderCarriers(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<CarrierDto[]> {
try {
return await this.ordersService.getOrderCarriers(orderId);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar transportadoras do pedido',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('mark/:orderId')
@ApiOperation({ summary: 'Busca marca por ID do pedido' })
@ApiParam({ name: 'orderId', example: 236001388 })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({
status: 200,
description: 'Marca encontrada com sucesso',
type: MarkResponseDto,
})
@ApiResponse({ status: 404, description: 'Marca não encontrada' })
async findOrderByMark(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<MarkResponseDto> {
try {
return await this.ordersService.findOrderByMark(orderId);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar marca do pedido',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('marks')
@ApiOperation({ summary: 'Busca todas as marcas disponíveis' })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({
status: 200,
description: 'Lista de marcas retornada com sucesso',
type: [MarkResponseDto],
})
async getAllMarks(): Promise<MarkResponseDto[]> {
try {
return await this.ordersService.getAllMarks();
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar marcas',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('marks/search')
@ApiOperation({ summary: 'Busca marcas por nome' })
@ApiQuery({
name: 'name',
required: true,
type: 'string',
description: 'Nome da marca para buscar',
})
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({
status: 200,
description: 'Lista de marcas encontradas',
type: [MarkResponseDto],
})
async getMarksByName(
@Query('name') markName: string,
): Promise<MarkResponseDto[]> {
try {
return await this.ordersService.getMarksByName(markName);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar marcas',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('transfer-log/:orderId')
@ApiOperation({ summary: 'Busca log de transferência por ID do pedido' })
@ApiParam({
name: 'orderId',
description: 'ID do pedido para buscar log de transferência',
})
@ApiQuery({
name: 'dttransf',
required: false,
type: 'string',
description: 'Data de transferência (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'codfilial',
required: false,
type: 'number',
description: 'Código da filial de origem',
})
@ApiQuery({
name: 'codfilialdest',
required: false,
type: 'number',
description: 'Código da filial de destino',
})
@ApiQuery({
name: 'numpedloja',
required: false,
type: 'number',
description: 'Número do pedido da loja',
})
@ApiQuery({
name: 'numpedtransf',
required: false,
type: 'number',
description: 'Número do pedido de transferência',
})
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({
status: 200,
description: 'Log de transferência encontrado com sucesso',
type: [EstLogTransferResponseDto],
})
@ApiResponse({ status: 400, description: 'OrderId inválido' })
@ApiResponse({
status: 404,
description: 'Log de transferência não encontrado',
})
async getTransferLog(
@Param('orderId', ParseIntPipe) orderId: number,
@Query('dttransf') dttransf?: string,
@Query('codfilial') codfilial?: number,
@Query('codfilialdest') codfilialdest?: number,
@Query('numpedloja') numpedloja?: number,
@Query('numpedtransf') numpedtransf?: number,
) {
try {
const filters = {
dttransf,
codfilial,
codfilialdest,
numpedloja,
numpedtransf,
};
return await this.ordersService.getTransferLog(orderId, filters);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar log de transferência',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('transfer-log')
@ApiOperation({ summary: 'Busca logs de transferência com filtros' })
@ApiQuery({
name: 'dttransf',
required: false,
type: 'string',
description: 'Data de transferência (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'dttransfIni',
required: false,
type: 'string',
description: 'Data de transferência inicial (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'dttransfEnd',
required: false,
type: 'string',
description: 'Data de transferência final (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'codfilial',
required: false,
type: 'number',
description: 'Código da filial de origem',
})
@ApiQuery({
name: 'codfilialdest',
required: false,
type: 'number',
description: 'Código da filial de destino',
})
@ApiQuery({
name: 'numpedloja',
required: false,
type: 'number',
description: 'Número do pedido da loja',
})
@ApiQuery({
name: 'numpedtransf',
required: false,
type: 'number',
description: 'Número do pedido de transferência',
})
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({
status: 200,
description: 'Logs de transferência encontrados com sucesso',
type: [EstLogTransferResponseDto],
})
@ApiResponse({ status: 400, description: 'Filtros inválidos' })
async getTransferLogs(
@Query('dttransf') dttransf?: string,
@Query('dttransfIni') dttransfIni?: string,
@Query('dttransfEnd') dttransfEnd?: string,
@Query('codfilial') codfilial?: number,
@Query('codfilialdest') codfilialdest?: number,
@Query('numpedloja') numpedloja?: number,
@Query('numpedtransf') numpedtransf?: number,
) {
try {
const filters = {
dttransf,
dttransfIni,
dttransfEnd,
codfilial,
codfilialdest,
numpedloja,
numpedtransf,
};
return await this.ordersService.getTransferLogs(filters);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar logs de transferência',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} }
} }
@Post('invoice/check')
@ApiOperation({ summary: 'Cria conferência de nota fiscal' })
@UsePipes(new ValidationPipe({ transform: true }))
async createInvoiceCheck(@Body() invoice: InvoiceCheckDto): Promise<{ message: string }> {
try {
return await this.ordersService.createInvoiceCheck(invoice);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao salvar conferência',
error.status || HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
@Get('carriers/:orderId')
@ApiOperation({ summary: 'Busca transportadoras do pedido' })
@ApiParam({ name: 'orderId', example: 236001388 })
@UsePipes(new ValidationPipe({ transform: true }))
async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise<CarrierDto[]> {
try {
return await this.ordersService.getOrderCarriers(orderId);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar transportadoras do pedido',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('mark/:orderId')
@ApiOperation({ summary: 'Busca marca por ID do pedido' })
@ApiParam({ name: 'orderId', example: 236001388 })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Marca encontrada com sucesso', type: MarkResponseDto })
@ApiResponse({ status: 404, description: 'Marca não encontrada' })
async findOrderByMark(@Param('orderId', ParseIntPipe) orderId: number): Promise<MarkResponseDto> {
try {
return await this.ordersService.findOrderByMark(orderId);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar marca do pedido',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('marks')
@ApiOperation({ summary: 'Busca todas as marcas disponíveis' })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Lista de marcas retornada com sucesso', type: [MarkResponseDto] })
async getAllMarks(): Promise<MarkResponseDto[]> {
try {
return await this.ordersService.getAllMarks();
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar marcas',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('marks/search')
@ApiOperation({ summary: 'Busca marcas por nome' })
@ApiQuery({ name: 'name', required: true, type: 'string', description: 'Nome da marca para buscar' })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Lista de marcas encontradas', type: [MarkResponseDto] })
async getMarksByName(@Query('name') markName: string): Promise<MarkResponseDto[]> {
try {
return await this.ordersService.getMarksByName(markName);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar marcas',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('transfer-log/:orderId')
@ApiOperation({ summary: 'Busca log de transferência por ID do pedido' })
@ApiParam({ name: 'orderId', description: 'ID do pedido para buscar log de transferência' })
@ApiQuery({ name: 'dttransf', required: false, type: 'string', description: 'Data de transferência (formato YYYY-MM-DD)' })
@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: 'Código da filial de origem' })
@ApiQuery({ name: 'codfilialdest', required: false, type: 'number', description: 'Código da filial de destino' })
@ApiQuery({ name: 'numpedloja', required: false, type: 'number', description: 'Número do pedido da loja' })
@ApiQuery({ name: 'numpedtransf', required: false, type: 'number', description: 'Número do pedido de transferência' })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Log de transferência encontrado com sucesso', type: [EstLogTransferResponseDto] })
@ApiResponse({ status: 400, description: 'OrderId inválido' })
@ApiResponse({ status: 404, description: 'Log de transferência não encontrado' })
async getTransferLog(
@Param('orderId', ParseIntPipe) orderId: number,
@Query('dttransf') dttransf?: string,
@Query('codfilial') codfilial?: number,
@Query('codfilialdest') codfilialdest?: number,
@Query('numpedloja') numpedloja?: number,
@Query('numpedtransf') numpedtransf?: number,
) {
try {
const filters = {
dttransf,
codfilial,
codfilialdest,
numpedloja,
numpedtransf,
};
return await this.ordersService.getTransferLog(orderId, filters);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar log de transferência',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@Get('transfer-log')
@ApiOperation({ summary: 'Busca logs de transferência com filtros' })
@ApiQuery({ name: 'dttransf', required: false, type: 'string', description: 'Data de transferência (formato YYYY-MM-DD)' })
@ApiQuery({ name: 'dttransfIni', required: false, type: 'string', description: 'Data de transferência inicial (formato YYYY-MM-DD)' })
@ApiQuery({ name: 'dttransfEnd', required: false, type: 'string', description: 'Data de transferência final (formato YYYY-MM-DD)' })
@ApiQuery({ name: 'codfilial', required: false, type: 'number', description: 'Código da filial de origem' })
@ApiQuery({ name: 'codfilialdest', required: false, type: 'number', description: 'Código da filial de destino' })
@ApiQuery({ name: 'numpedloja', required: false, type: 'number', description: 'Número do pedido da loja' })
@ApiQuery({ name: 'numpedtransf', required: false, type: 'number', description: 'Número do pedido de transferência' })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Logs de transferência encontrados com sucesso', type: [EstLogTransferResponseDto] })
@ApiResponse({ status: 400, description: 'Filtros inválidos' })
async getTransferLogs(
@Query('dttransf') dttransf?: string,
@Query('dttransfIni') dttransfIni?: string,
@Query('dttransfEnd') dttransfEnd?: string,
@Query('codfilial') codfilial?: number,
@Query('codfilialdest') codfilialdest?: number,
@Query('numpedloja') numpedloja?: number,
@Query('numpedtransf') numpedtransf?: number,
) {
try {
const filters = {
dttransf,
dttransfIni,
dttransfEnd,
codfilial,
codfilialdest,
numpedloja,
numpedtransf,
};
return await this.ordersService.getTransferLogs(filters);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar logs de transferência',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@@ -1,10 +1,9 @@
export class CutItemDto { export class CutItemDto {
productId: number; productId: number;
description: string; description: string;
pacth: string; pacth: string;
stockId: number; stockId: number;
saleQuantity: number; saleQuantity: number;
cutQuantity: number; cutQuantity: number;
separedQuantity: number; separedQuantity: number;
} }

View File

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

View File

@@ -1,31 +1,30 @@
import { DeliveryCompleted } from './delivery-completed.dto'; import { DeliveryCompleted } from './delivery-completed.dto';
export class OrderDeliveryDto { export class OrderDeliveryDto {
placeId: number; placeId: number;
placeName: string; placeName: string;
street: string; street: string;
addressNumber: string; addressNumber: string;
bairro: string; bairro: string;
city: string; city: string;
state: string; state: string;
addressComplement: string; addressComplement: string;
cep: string; cep: string;
commentOrder1: string; commentOrder1: string;
commentOrder2: string; commentOrder2: string;
commentDelivery1: string; commentDelivery1: string;
commentDelivery2: string; commentDelivery2: string;
commentDelivery3: string; commentDelivery3: string;
commentDelivery4: string; commentDelivery4: string;
shippimentId: number; shippimentId: number;
shippimentDate: Date; shippimentDate: Date;
shippimentComment: string; shippimentComment: string;
place: string; place: string;
driver: string; driver: string;
car: string; car: string;
closeDate: Date; closeDate: Date;
separatorName: string; separatorName: string;
confName: string; confName: string;
releaseDate: Date; releaseDate: Date;
completedDeliveries?: DeliveryCompleted[]; completedDeliveries?: DeliveryCompleted[];
} }

View File

@@ -1,14 +1,14 @@
export class OrderItemDto { export class OrderItemDto {
productId: number; productId: number;
description: string; description: string;
pacth: string; pacth: string;
color: string; color: string;
stockId: number; stockId: number;
quantity: number; quantity: number;
salePrice: number; salePrice: number;
deliveryType: string; deliveryType: string;
total: number; total: number;
weight: number; weight: number;
department: string; department: string;
brand: string; brand: string;
} }

View File

@@ -1,8 +1,7 @@
export class OrderStatusDto { export class OrderStatusDto {
orderId: number; orderId: number;
status: string; status: string;
statusDate: Date; statusDate: Date;
userName: string; userName: string;
comments: string | null; comments: string | null;
} }

View File

@@ -1,13 +1,12 @@
export class OrderTransferDto { export class OrderTransferDto {
orderId: number; orderId: number;
transferDate: Date; transferDate: Date;
invoiceId: number; invoiceId: number;
transactionId: number; transactionId: number;
oldShipment: number; oldShipment: number;
newShipment: number; newShipment: number;
transferText: string; transferText: string;
cause: string; cause: string;
userName: string; userName: string;
program: string; program: string;
} }

View File

@@ -2,12 +2,16 @@ import { IsOptional, IsString, IsNumber, IsDateString } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
export class DeliveryCompletedQuery { export class DeliveryCompletedQuery {
@ApiPropertyOptional({ description: 'Data de início para filtro (formato YYYY-MM-DD)' }) @ApiPropertyOptional({
description: 'Data de início para filtro (formato YYYY-MM-DD)',
})
@IsOptional() @IsOptional()
@IsDateString() @IsDateString()
startDate?: string; startDate?: string;
@ApiPropertyOptional({ description: 'Data de fim para filtro (formato YYYY-MM-DD)' }) @ApiPropertyOptional({
description: 'Data de fim para filtro (formato YYYY-MM-DD)',
})
@IsOptional() @IsOptional()
@IsDateString() @IsDateString()
endDate?: string; endDate?: string;
@@ -42,7 +46,10 @@ export class DeliveryCompletedQuery {
@IsString() @IsString()
status?: string; status?: string;
@ApiPropertyOptional({ description: 'Limite de registros por página', default: 100 }) @ApiPropertyOptional({
description: 'Limite de registros por página',
default: 100,
})
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
limit?: number; limit?: number;

View File

@@ -1,5 +1,11 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsNumber, IsNotEmpty, IsOptional, MinLength } from 'class-validator'; import {
IsString,
IsNumber,
IsNotEmpty,
IsOptional,
MinLength,
} from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
export class FindDebDto { export class FindDebDto {
@@ -31,4 +37,3 @@ export class FindDebDto {
}) })
cobranca?: string; cobranca?: string;
} }

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
export class InvoiceCheckItemDto { export class InvoiceCheckItemDto {
productId: number; productId: number;
seq: number; seq: number;
qt: number; qt: number;
confDate: string; confDate: string;
} }

View File

@@ -1,9 +1,9 @@
export class LeadtimeDto { export class LeadtimeDto {
orderId: number; orderId: number;
etapa: number; etapa: number;
descricaoEtapa: string; descricaoEtapa: string;
data: Date | string; data: Date | string;
codigoFuncionario: number | null; codigoFuncionario: number | null;
nomeFuncionario: string | null; nomeFuncionario: string | null;
numeroPedido: number; numeroPedido: number;
} }

View File

@@ -1,23 +1,21 @@
export class OrderDeliveryDto { export class OrderDeliveryDto {
storeId: number; storeId: number;
createDate: Date; createDate: Date;
orderId: number; orderId: number;
orderIdSale: number | null; orderIdSale: number | null;
deliveryDate: Date | null; deliveryDate: Date | null;
cnpj: string | null; cnpj: string | null;
customerId: number; customerId: number;
customer: string; customer: string;
deliveryType: string | null; deliveryType: string | null;
quantityItens: number; quantityItens: number;
status: string; status: string;
weight: number; weight: number;
shipmentId: number; shipmentId: number;
driverId: number | null; driverId: number | null;
driverName: string | null; driverName: string | null;
carPlate: string | null; carPlate: string | null;
carIdentification: string | null; carIdentification: string | null;
observation: string | null; observation: string | null;
deliveryConfirmationDate: Date | null; deliveryConfirmationDate: Date | null;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,68 +1,74 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm'; import { InjectDataSource } from '@nestjs/typeorm';
import { DebDto } from '../dto/DebDto'; import { DebDto } from '../dto/DebDto';
@Injectable() @Injectable()
export class DebRepository { export class DebRepository {
constructor( constructor(
@InjectDataSource("oracle") private readonly oracleDataSource: DataSource, @InjectDataSource('oracle') private readonly oracleDataSource: DataSource,
) {} ) {}
/** /**
* Busca débitos por CPF/CGCENT * Busca débitos por CPF/CGCENT
* @param cpfCgcent - CPF ou CGCENT do cliente * @param cpfCgcent - CPF ou CGCENT do cliente
* @param matricula - Matrícula do funcionário (opcional) * @param matricula - Matrícula do funcionário (opcional)
* @param cobranca - Código de cobrança (opcional) * @param cobranca - Código de cobrança (opcional)
* @returns Lista de débitos do cliente * @returns Lista de débitos do cliente
* @throws {Error} Erro ao executar a query no banco de dados * @throws {Error} Erro ao executar a query no banco de dados
*/ */
async findByCpfCgcent(cpfCgcent: string, matricula?: number, cobranca?: string): Promise<DebDto[]> { async findByCpfCgcent(
const queryRunner = this.oracleDataSource.createQueryRunner(); cpfCgcent: string,
await queryRunner.connect(); matricula?: number,
try { cobranca?: string,
const queryBuilder = queryRunner.manager ): Promise<DebDto[]> {
.createQueryBuilder() const queryRunner = this.oracleDataSource.createQueryRunner();
.select([ await queryRunner.connect();
'p.dtemissao AS "dtemissao"', try {
'p.codfilial AS "codfilial"', const queryBuilder = queryRunner.manager
'p.duplic AS "duplic"', .createQueryBuilder()
'p.prest AS "prest"', .select([
'p.codcli AS "codcli"', 'p.dtemissao AS "dtemissao"',
'c.cliente AS "cliente"', 'p.codfilial AS "codfilial"',
'p.codcob AS "codcob"', 'p.duplic AS "duplic"',
'cb.cobranca AS "cobranca"', 'p.prest AS "prest"',
'p.dtvenc AS "dtvenc"', 'p.codcli AS "codcli"',
'p.dtpag AS "dtpag"', 'c.cliente AS "cliente"',
'p.valor AS "valor"', 'p.codcob AS "codcob"',
`CASE 'cb.cobranca AS "cobranca"',
'p.dtvenc AS "dtvenc"',
'p.dtpag AS "dtpag"',
'p.valor AS "valor"',
`CASE
WHEN p.dtpag IS NOT NULL THEN 'PAGO' WHEN p.dtpag IS NOT NULL THEN 'PAGO'
WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO' WHEN p.dtvenc < TRUNC(SYSDATE) THEN 'EM ATRASO'
WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER' WHEN p.dtvenc >= TRUNC(SYSDATE) THEN 'A VENCER'
ELSE 'NENHUM' ELSE 'NENHUM'
END AS "situacao"`, END AS "situacao"`,
]) ])
.from('pcprest', 'p') .from('pcprest', 'p')
.innerJoin('pcclient', 'c', 'p.codcli = c.codcli') .innerJoin('pcclient', 'c', 'p.codcli = c.codcli')
.innerJoin('pccob', 'cb', 'p.codcob = cb.codcob') .innerJoin('pccob', 'cb', 'p.codcob = cb.codcob')
.innerJoin('pcempr', 'e', 'c.cgcent = e.cpf') .innerJoin('pcempr', 'e', 'c.cgcent = e.cpf')
.where('p.codcob NOT IN (:...excludedCob)', { excludedCob: ['DESD', 'CANC'] }) .where('p.codcob NOT IN (:...excludedCob)', {
.andWhere('c.cgcent = :cpfCgcent', { cpfCgcent }); excludedCob: ['DESD', 'CANC'],
})
.andWhere('c.cgcent = :cpfCgcent', { cpfCgcent });
if (matricula) { if (matricula) {
queryBuilder.andWhere('e.matricula = :matricula', { matricula }); queryBuilder.andWhere('e.matricula = :matricula', { matricula });
} }
if (cobranca) { if (cobranca) {
queryBuilder.andWhere('p.codcob = :cobranca', { cobranca }); queryBuilder.andWhere('p.codcob = :cobranca', { cobranca });
} }
queryBuilder.orderBy('p.dtvenc', 'ASC'); queryBuilder.orderBy('p.dtvenc', 'ASC');
const result = await queryBuilder.getRawMany(); const result = await queryBuilder.getRawMany();
return result; return result;
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
}
} }
}
} }

View File

@@ -1,4 +1,4 @@
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { Controller, Get, Param } from '@nestjs/common'; import { Controller, Get, Param } from '@nestjs/common';
import { PartnersService } from './partners.service'; import { PartnersService } from './partners.service';
import { PartnerDto } from './dto/partner.dto'; import { PartnerDto } from './dto/partner.dto';
@@ -6,43 +6,45 @@ import { PartnerDto } from './dto/partner.dto';
@ApiTags('Parceiros') @ApiTags('Parceiros')
@Controller('api/v1/') @Controller('api/v1/')
export class PartnersController { export class PartnersController {
constructor(private readonly partnersService: PartnersService) {}
constructor(private readonly partnersService: PartnersService) {} @Get('parceiros/:filter')
@ApiOperation({ summary: 'Busca parceiros por filtro (ID, CPF ou nome)' })
@ApiParam({
name: 'filter',
description: 'Filtro de busca (ID, CPF ou nome)',
})
@ApiResponse({
status: 200,
description: 'Lista de parceiros encontrados.',
type: PartnerDto,
isArray: true,
})
async findPartners(@Param('filter') filter: string): Promise<PartnerDto[]> {
return this.partnersService.findPartners(filter);
}
@Get('parceiros/:filter') @Get('parceiros')
@ApiOperation({ summary: 'Busca parceiros por filtro (ID, CPF ou nome)' }) @ApiOperation({ summary: 'Lista todos os parceiros' })
@ApiParam({ name: 'filter', description: 'Filtro de busca (ID, CPF ou nome)' }) @ApiResponse({
@ApiResponse({ status: 200,
status: 200, description: 'Lista de todos os parceiros.',
description: 'Lista de parceiros encontrados.', type: PartnerDto,
type: PartnerDto, isArray: true,
isArray: true })
}) async getAllPartners(): Promise<PartnerDto[]> {
async findPartners(@Param('filter') filter: string): Promise<PartnerDto[]> { return this.partnersService.getAllPartners();
return this.partnersService.findPartners(filter); }
}
@Get('parceiros') @Get('parceiros/id/:id')
@ApiOperation({ summary: 'Lista todos os parceiros' }) @ApiOperation({ summary: 'Busca parceiro por ID específico' })
@ApiResponse({ @ApiParam({ name: 'id', description: 'ID do parceiro' })
status: 200, @ApiResponse({
description: 'Lista de todos os parceiros.', status: 200,
type: PartnerDto, description: 'Parceiro encontrado.',
isArray: true type: PartnerDto,
}) })
async getAllPartners(): Promise<PartnerDto[]> { async getPartnerById(@Param('id') id: string): Promise<PartnerDto | null> {
return this.partnersService.getAllPartners(); return this.partnersService.getPartnerById(id);
} }
@Get('parceiros/id/:id')
@ApiOperation({ summary: 'Busca parceiro por ID específico' })
@ApiParam({ name: 'id', description: 'ID do parceiro' })
@ApiResponse({
status: 200,
description: 'Parceiro encontrado.',
type: PartnerDto
})
async getPartnerById(@Param('id') id: string): Promise<PartnerDto | null> {
return this.partnersService.getPartnerById(id);
}
} }

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