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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,16 @@
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
import {
ApiTags,
} from '@nestjs/swagger';
import { Controller, Get, Param } from '@nestjs/common';
import { clientesService } from './clientes.service';
@ApiTags('clientes')
@Controller('api/v1/')
export class clientesController {
constructor(private readonly clientesService: clientesService) {}
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 @typescript-eslint/no-unused-vars */
import { clientesService } from './clientes.service';
import { clientesController } from './clientes.controller';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,11 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { OrdersPaymentService } from './orders-payment.service';
import { OrderDto } from './dto/order.dto';
import { PaymentDto } from './dto/payment.dto';
@@ -12,66 +18,65 @@ import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
@UseGuards(JwtAuthGuard)
@Controller('api/v1/orders-payment')
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')
@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/:orderId')
@ApiOperation({ summary: 'Busca um pedido específico' })
@ApiParam({ name: 'id', description: 'ID da loja' })
@ApiParam({ name: 'orderId', description: 'ID do pedido' })
@ApiResponse({
status: 200,
description: 'Pedido retornado com sucesso',
type: OrderDto,
})
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')
@ApiOperation({ summary: 'Busca um pedido específico' })
@ApiParam({ name: 'id', description: 'ID da loja' })
@ApiParam({ name: 'orderId', description: 'ID do pedido' })
@ApiResponse({
status: 200,
description: 'Pedido retornado com sucesso',
type: OrderDto
})
async findOrder(
@Param('id') storeId: string,
@Param('orderId') orderId: number,
): Promise<OrderDto> {
const orders = await this.orderPaymentService.findOrders(storeId, orderId);
return orders[0];
}
@Get('payments/:id')
@ApiOperation({ summary: 'Lista todos os pagamentos de um pedido' })
@ApiParam({ name: 'id', description: 'ID do pedido' })
@ApiResponse({
status: 200,
description: 'Lista de pagamentos retornada com sucesso',
type: [PaymentDto],
})
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);
}
@Get('payments/:id')
@ApiOperation({ summary: 'Lista todos os pagamentos de um pedido' })
@ApiParam({ name: 'id', description: 'ID do pedido' })
@ApiResponse({
status: 200,
description: 'Lista de pagamentos retornada com sucesso',
type: [PaymentDto]
})
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);
}
}
@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 @typescript-eslint/no-unused-vars */
/*
https://docs.nestjs.com/modules

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,21 +7,26 @@ import {
Query,
UsePipes,
UseGuards,
UseInterceptors,
ValidationPipe,
HttpException,
HttpStatus,
DefaultValuePipe,
ParseBoolPipe,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags, ApiQuery, ApiParam, ApiResponse } from '@nestjs/swagger';
import { ResponseInterceptor } from '../../common/response.interceptor';
import {
ApiBearerAuth,
ApiOperation,
ApiTags,
ApiQuery,
ApiParam,
ApiResponse,
} from '@nestjs/swagger';
import { OrdersService } from '../application/orders.service';
import { FindOrdersDto } from '../dto/find-orders.dto';
import { FindOrdersByDeliveryDateDto } from '../dto/find-orders-by-delivery-date.dto';
import { JwtAuthGuard, } from 'src/auth/guards/jwt-auth.guard';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { InvoiceDto } from '../dto/find-invoice.dto';
import { OrderItemDto } from "../dto/OrderItemDto";
import { OrderItemDto } from '../dto/OrderItemDto';
import { LeadtimeDto } from '../dto/leadtime.dto';
import { CutItemDto } from '../dto/CutItemDto';
import { OrderDeliveryDto } from '../dto/OrderDeliveryDto';
@@ -34,7 +39,6 @@ import { OrderResponseDto } from '../dto/order-response.dto';
import { MarkResponseDto } from '../dto/mark-response.dto';
import { EstLogTransferResponseDto } from '../dto/estlogtransfer.dto';
@ApiTags('Orders')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@@ -45,15 +49,47 @@ export class OrdersController {
@Get('find')
@ApiOperation({
summary: 'Busca pedidos',
description: 'Busca pedidos com filtros avançados. Suporta filtros por data, cliente, vendedor, status, tipo de entrega e status de transferência.'
description:
'Busca pedidos com filtros avançados. Suporta filtros por data, cliente, vendedor, status, tipo de entrega e status de transferência.',
})
@ApiQuery({
name: 'includeCheckout',
required: false,
type: 'boolean',
description: 'Incluir dados de checkout',
})
@ApiQuery({
name: 'statusTransfer',
required: false,
type: 'string',
description:
'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)',
})
@ApiQuery({
name: 'markId',
required: false,
type: 'number',
description: 'ID da marca para filtrar pedidos',
})
@ApiQuery({
name: 'markName',
required: false,
type: 'string',
description: 'Nome da marca para filtrar pedidos (busca parcial)',
})
@ApiQuery({
name: 'hasPreBox',
required: false,
type: 'boolean',
description:
'Filtrar pedidos que tenham registros na tabela de transfer log',
})
@ApiQuery({ name: 'includeCheckout', required: false, type: 'boolean', description: 'Incluir dados de checkout' })
@ApiQuery({ name: 'statusTransfer', required: false, type: 'string', description: 'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)' })
@ApiQuery({ name: 'markId', required: false, type: 'number', description: 'ID da marca para filtrar pedidos' })
@ApiQuery({ name: 'markName', required: false, type: 'string', description: 'Nome da marca para filtrar pedidos (busca parcial)' })
@ApiQuery({ name: 'hasPreBox', required: false, type: 'boolean', description: 'Filtrar pedidos que tenham registros na tabela de transfer log' })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Lista de pedidos retornada com sucesso', type: [OrderResponseDto] })
@ApiResponse({
status: 200,
description: 'Lista de pedidos retornada com sucesso',
type: [OrderResponseDto],
})
findOrders(
@Query() query: FindOrdersDto,
@Query('includeCheckout', new DefaultValuePipe(false), ParseBoolPipe)
@@ -68,17 +104,42 @@ export class OrdersController {
@Get('find-by-delivery-date')
@ApiOperation({
summary: 'Busca pedidos por data de entrega',
description: 'Busca pedidos filtrados por data de entrega. Suporta filtros adicionais como status de transferência, cliente, vendedor, etc.'
description:
'Busca pedidos filtrados por data de entrega. Suporta filtros adicionais como status de transferência, cliente, vendedor, etc.',
})
@ApiQuery({
name: 'statusTransfer',
required: false,
type: 'string',
description:
'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)',
})
@ApiQuery({
name: 'markId',
required: false,
type: 'number',
description: 'ID da marca para filtrar pedidos',
})
@ApiQuery({
name: 'markName',
required: false,
type: 'string',
description: 'Nome da marca para filtrar pedidos (busca parcial)',
})
@ApiQuery({
name: 'hasPreBox',
required: false,
type: 'boolean',
description:
'Filtrar pedidos que tenham registros na tabela de transfer log',
})
@ApiQuery({ name: 'statusTransfer', required: false, type: 'string', description: 'Filtrar por status de transferência (Em Trânsito, Em Separação, Aguardando Separação, Concluída)' })
@ApiQuery({ name: 'markId', required: false, type: 'number', description: 'ID da marca para filtrar pedidos' })
@ApiQuery({ name: 'markName', required: false, type: 'string', description: 'Nome da marca para filtrar pedidos (busca parcial)' })
@ApiQuery({ name: 'hasPreBox', required: false, type: 'boolean', description: 'Filtrar pedidos que tenham registros na tabela de transfer log' })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiResponse({ status: 200, description: 'Lista de pedidos por data de entrega retornada com sucesso', type: [OrderResponseDto] })
findOrdersByDeliveryDate(
@Query() query: FindOrdersByDeliveryDateDto,
) {
@ApiResponse({
status: 200,
description: 'Lista de pedidos por data de entrega retornada com sucesso',
type: [OrderResponseDto],
})
findOrdersByDeliveryDate(@Query() query: FindOrdersByDeliveryDateDto) {
return this.ordersService.findOrdersByDeliveryDate(query);
}
@@ -86,20 +147,16 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca fechamento de caixa para um pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
getOrderCheckout(
@Param('orderId', ParseIntPipe) orderId: number,
) {
getOrderCheckout(@Param('orderId', ParseIntPipe) orderId: number) {
return this.ordersService.getOrderCheckout(orderId);
}
@Get('invoice/:chavenfe')
@ApiParam({
name: 'chavenfe',
required: true,
description: 'Chave da Nota Fiscal (44 dígitos)',
})
@ApiOperation({ summary: 'Busca NF pela chave' })
@UsePipes(new ValidationPipe({ transform: true }))
async getInvoice(@Param('chavenfe') chavenfe: string): Promise<InvoiceDto> {
@@ -117,7 +174,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca PELO numero do pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderItemDto[]> {
async getItens(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderItemDto[]> {
try {
return await this.ordersService.getItens(orderId.toString());
} catch (error) {
@@ -131,7 +190,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca itens cortados do pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getCutItens(@Param('orderId', ParseIntPipe) orderId: number): Promise<CutItemDto[]> {
async getCutItens(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<CutItemDto[]> {
try {
return await this.ordersService.getCutItens(orderId.toString());
} catch (error) {
@@ -146,7 +207,9 @@ export class OrdersController {
@ApiOperation({ summary: 'Busca dados de entrega do pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getOrderDelivery(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderDeliveryDto | null> {
async getOrderDelivery(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderDeliveryDto | null> {
try {
return await this.ordersService.getOrderDelivery(orderId.toString());
} catch (error) {
@@ -157,50 +220,66 @@ export class OrdersController {
}
}
@Get('transfer/:orderId')
@Get('transfer/:orderId')
@ApiOperation({ summary: 'Consulta pedidos de transferência' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getTransfer(@Param('orderId', ParseIntPipe) orderId: number): Promise<OrderTransferDto[] | null> {
try {
return await this.ordersService.getTransfer(orderId);
} catch (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,
);
@UsePipes(new ValidationPipe({ transform: true }))
async getTransfer(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderTransferDto[] | null> {
try {
return await this.ordersService.getTransfer(orderId);
} catch (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(':orderId/deliveries')
@ApiOperation({ summary: 'Consulta entregas do pedido' })
@ApiParam({ name: 'orderId' })
@ApiQuery({ name: 'createDateIni', required: false, description: 'Data inicial para filtro (formato YYYY-MM-DD)' })
@ApiQuery({ name: 'createDateEnd', required: false, description: 'Data final para filtro (formato YYYY-MM-DD)' })
@ApiQuery({
name: 'createDateIni',
required: false,
description: 'Data inicial para filtro (formato YYYY-MM-DD)',
})
@ApiQuery({
name: 'createDateEnd',
required: false,
description: 'Data final para filtro (formato YYYY-MM-DD)',
})
async getOrderDeliveries(
@Param('orderId', ParseIntPipe) orderId: number,
@Query('createDateIni') createDateIni?: string,
@Query('createDateEnd') createDateEnd?: string,
): Promise<OrderDeliveryDto[]> {
// Definir datas padrão caso não sejam fornecidas
const defaultDateIni = createDateIni || new Date(new Date().setDate(new Date().getDate() - 30)).toISOString().split('T')[0];
const defaultDateEnd = createDateEnd || new Date().toISOString().split('T')[0];
const defaultDateIni =
createDateIni ||
new Date(new Date().setDate(new Date().getDate() - 30))
.toISOString()
.split('T')[0];
const defaultDateEnd =
createDateEnd || new Date().toISOString().split('T')[0];
return this.ordersService.getOrderDeliveries(orderId.toString(), {
createDateIni: defaultDateIni,
@@ -208,175 +287,275 @@ export class OrdersController {
});
}
@Get('leadtime/:orderId')
@ApiOperation({ summary: 'Consulta leadtime do pedido' })
@Get('leadtime/:orderId')
@ApiOperation({ summary: 'Consulta leadtime do pedido' })
@ApiParam({ name: 'orderId' })
@UsePipes(new ValidationPipe({ transform: true }))
async getLeadtime(@Param('orderId', ParseIntPipe) orderId: number): Promise<LeadtimeDto[]> {
try {
@UsePipes(new ValidationPipe({ transform: true }))
async getLeadtime(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<LeadtimeDto[]> {
try {
return await this.ordersService.getLeadtime(orderId.toString());
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar leadtime do pedido',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
} catch (error) {
throw new HttpException(
error.message || 'Erro ao buscar leadtime do pedido',
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 {
productId: number;
description: string;
pacth: string;
stockId: number;
saleQuantity: number;
cutQuantity: number;
separedQuantity: number;
}
productId: number;
description: string;
pacth: string;
stockId: number;
saleQuantity: number;
cutQuantity: number;
separedQuantity: number;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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