refactor: atualizações e remoção de módulos não utilizados
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
export interface ILogger {
|
||||
log(message: string): void;
|
||||
warn(message: string): void;
|
||||
error(message: string, trace?: string): void;
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export class NestLoggerAdapter implements ILogger {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(private readonly context: string) {
|
||||
this.logger = new Logger(context);
|
||||
}
|
||||
|
||||
log(message: string, meta?: Record<string, any>): void {
|
||||
this.logger.log(this.formatMessage(message, meta));
|
||||
}
|
||||
|
||||
warn(message: string, meta?: Record<string, any>): void {
|
||||
this.logger.warn(this.formatMessage(message, meta));
|
||||
}
|
||||
|
||||
error(message: string, trace?: string, meta?: Record<string, any>): void {
|
||||
this.logger.error(this.formatMessage(message, meta), trace);
|
||||
}
|
||||
|
||||
private formatMessage(message: string, meta?: Record<string, any>): string {
|
||||
return meta ? `${message} | ${JSON.stringify(meta)}` : message;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export function LogExecution(label?: string) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const original = descriptor.value;
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const logger: ILogger = this.logger;
|
||||
const context = label || `${target.constructor.name}.${propertyKey}`;
|
||||
const start = Date.now();
|
||||
logger.log(`Iniciando: ${context} | ${JSON.stringify({ args })}`);
|
||||
const result = await original.apply(this, args);
|
||||
const duration = Date.now() - start;
|
||||
logger.log(`Finalizado: ${context} em ${duration}ms`);
|
||||
return result;
|
||||
};
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { NestLoggerAdapter } from './NestLoggerAdapter';
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'LoggerService',
|
||||
useFactory: () => new NestLoggerAdapter('DataConsultService'),
|
||||
},
|
||||
],
|
||||
exports: ['LoggerService'],
|
||||
})
|
||||
export class LoggerModule {}
|
||||
@@ -8,32 +8,23 @@ import { OrdersPaymentModule } from './orders-payment/orders-payment.module';
|
||||
import { AuthModule } from './auth/auth/auth.module';
|
||||
import { 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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ import { LoginAuditService } from '../services/login-audit.service';
|
||||
RefreshTokenService,
|
||||
SessionManagementService,
|
||||
LoginAuditService,
|
||||
AuthenticateUserHandler
|
||||
AuthenticateUserHandler,
|
||||
],
|
||||
exports: [AuthService],
|
||||
})
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { 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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
21
src/core/configs/cache/IRedisClient.ts
vendored
21
src/core/configs/cache/IRedisClient.ts
vendored
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { RedisClientAdapter } from './redis-client.adapter';
|
||||
export const RedisClientToken = 'RedisClientInterface';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
2
src/core/configs/cache/redis.module.ts
vendored
2
src/core/configs/cache/redis.module.ts
vendored
@@ -9,4 +9,4 @@ import { RedisClientAdapterProvider } from './redis-client.adapter.provider';
|
||||
providers: [RedisProvider, RedisClientAdapterProvider],
|
||||
exports: [RedisProvider, RedisClientAdapterProvider],
|
||||
})
|
||||
export class RedisModule {}
|
||||
export class RedisModule {}
|
||||
|
||||
36
src/core/configs/cache/redis.provider.ts
vendored
36
src/core/configs/cache/redis.provider.ts
vendored
@@ -1,21 +1,21 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import 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],
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/controllers#controllers
|
||||
*/
|
||||
|
||||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class NegotiationsController { }
|
||||
@@ -1,19 +0,0 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/modules
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { NegotiationsController } from './negotiations.controller';
|
||||
import { NegotiationsService } from './negotiations.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [
|
||||
NegotiationsController,],
|
||||
providers: [
|
||||
NegotiationsService,],
|
||||
})
|
||||
export class NegotiationsModule { }
|
||||
@@ -1,11 +0,0 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/providers#services
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class NegotiationsService { }
|
||||
@@ -1,17 +0,0 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/modules
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OccurrencesService } from './occurrences.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [],
|
||||
providers: [
|
||||
OccurrencesService,],
|
||||
})
|
||||
export class OccurrencesModule { }
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
https://docs.nestjs.com/providers#services
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class OccurrencesService { }
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
https://docs.nestjs.com/controllers#controllers
|
||||
*/
|
||||
|
||||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class OcorrencesController { }
|
||||
@@ -1,37 +0,0 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
https://docs.nestjs.com/controllers#controllers
|
||||
*/
|
||||
|
||||
import { Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||
|
||||
@ApiTags('CRM - Reason Table')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('api/v1/crm/reason')
|
||||
export class ReasonTableController {
|
||||
|
||||
@Get()
|
||||
async getReasons() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createReasons() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
async updateReasons(@Param('id') id: number) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
async deleteReasons(@Param('id') id: number) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/modules
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ReasonTableController } from './reason-table.controller';
|
||||
import { ReasonTableService } from './reason-table.service';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [
|
||||
ReasonTableController,],
|
||||
providers: [
|
||||
ReasonTableService,],
|
||||
})
|
||||
export class ReasonTableModule { }
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars *//*
|
||||
|
||||
https://docs.nestjs.com/providers#services
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ReasonTableService { }
|
||||
@@ -1,50 +1,47 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -20,4 +20,3 @@ export class RegionDto {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { HealthCheckResult } from '@nestjs/terminus';
|
||||
|
||||
@Injectable()
|
||||
export class HealthAlertService {
|
||||
private readonly logger = new Logger(HealthAlertService.name);
|
||||
private readonly webhookUrls: Record<string, string>;
|
||||
private readonly alertThresholds: Record<string, any>;
|
||||
private readonly alertCooldowns: Map<string, number> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
// Configurações de webhooks para diferentes canais de alerta
|
||||
this.webhookUrls = {
|
||||
slack: this.configService.get<string>('ALERT_WEBHOOK_SLACK'),
|
||||
teams: this.configService.get<string>('ALERT_WEBHOOK_TEAMS'),
|
||||
email: this.configService.get<string>('ALERT_WEBHOOK_EMAIL'),
|
||||
};
|
||||
|
||||
// Thresholds para diferentes tipos de alerta
|
||||
this.alertThresholds = {
|
||||
disk: {
|
||||
criticalPercent: this.configService.get<number>('ALERT_DISK_CRITICAL_PERCENT', 90),
|
||||
warningPercent: this.configService.get<number>('ALERT_DISK_WARNING_PERCENT', 80),
|
||||
},
|
||||
memory: {
|
||||
criticalPercent: this.configService.get<number>('ALERT_MEMORY_CRITICAL_PERCENT', 90),
|
||||
warningPercent: this.configService.get<number>('ALERT_MEMORY_WARNING_PERCENT', 80),
|
||||
},
|
||||
db: {
|
||||
cooldownMinutes: this.configService.get<number>('ALERT_DB_COOLDOWN_MINUTES', 15),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async processHealthCheckResult(result: HealthCheckResult): Promise<void> {
|
||||
try {
|
||||
const { status, info, error, details } = result;
|
||||
|
||||
// Se o status geral não for 'ok', envie um alerta
|
||||
if (status !== 'ok') {
|
||||
// Verificar quais componentes estão com problema
|
||||
const failedComponents = Object.entries(error)
|
||||
.map(([key, value]) => ({ key, value }));
|
||||
|
||||
if (failedComponents.length > 0) {
|
||||
await this.sendAlert('critical', 'Health Check Falhou',
|
||||
`Os seguintes componentes estão com problemas: ${failedComponents.map(c => c.key).join(', ')}`,
|
||||
{ result, failedComponents }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar alertas específicos para cada tipo de componente
|
||||
if (details.disk) {
|
||||
await this.checkDiskAlerts(details.disk);
|
||||
}
|
||||
|
||||
if (details.memory_heap) {
|
||||
await this.checkMemoryAlerts(details.memory_heap);
|
||||
}
|
||||
|
||||
// Verificar alertas de banco de dados
|
||||
['oracle', 'postgres'].forEach(db => {
|
||||
if (details[db] && details[db].status !== 'up') {
|
||||
this.checkDatabaseAlerts(db, details[db]);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao processar health check result: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkDiskAlerts(diskDetails: any): Promise<void> {
|
||||
try {
|
||||
if (!diskDetails.freeBytes || !diskDetails.totalBytes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usedPercent = ((diskDetails.totalBytes - diskDetails.freeBytes) / diskDetails.totalBytes) * 100;
|
||||
|
||||
if (usedPercent >= this.alertThresholds.disk.criticalPercent) {
|
||||
await this.sendAlert('critical', 'Espaço em Disco Crítico',
|
||||
`O uso de disco está em ${usedPercent.toFixed(1)}%, acima do limite crítico de ${this.alertThresholds.disk.criticalPercent}%`,
|
||||
{ diskDetails, usedPercent }
|
||||
);
|
||||
} else if (usedPercent >= this.alertThresholds.disk.warningPercent) {
|
||||
await this.sendAlert('warning', 'Alerta de Espaço em Disco',
|
||||
`O uso de disco está em ${usedPercent.toFixed(1)}%, acima do limite de alerta de ${this.alertThresholds.disk.warningPercent}%`,
|
||||
{ diskDetails, usedPercent }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao verificar alertas de disco: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkMemoryAlerts(memoryDetails: any): Promise<void> {
|
||||
try {
|
||||
if (!memoryDetails.usedBytes || !memoryDetails.thresholdBytes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usedPercent = (memoryDetails.usedBytes / memoryDetails.thresholdBytes) * 100;
|
||||
|
||||
if (usedPercent >= this.alertThresholds.memory.criticalPercent) {
|
||||
await this.sendAlert('critical', 'Uso de Memória Crítico',
|
||||
`O uso de memória heap está em ${usedPercent.toFixed(1)}%, acima do limite crítico de ${this.alertThresholds.memory.criticalPercent}%`,
|
||||
{ memoryDetails, usedPercent }
|
||||
);
|
||||
} else if (usedPercent >= this.alertThresholds.memory.warningPercent) {
|
||||
await this.sendAlert('warning', 'Alerta de Uso de Memória',
|
||||
`O uso de memória heap está em ${usedPercent.toFixed(1)}%, acima do limite de alerta de ${this.alertThresholds.memory.warningPercent}%`,
|
||||
{ memoryDetails, usedPercent }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao verificar alertas de memória: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkDatabaseAlerts(dbName: string, dbDetails: any): Promise<void> {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const lastAlertTime = this.alertCooldowns.get(dbName) || 0;
|
||||
const cooldownMs = this.alertThresholds.db.cooldownMinutes * 60 * 1000;
|
||||
|
||||
// Verifica se já passou o período de cooldown para este banco
|
||||
if (now - lastAlertTime >= cooldownMs) {
|
||||
await this.sendAlert('critical', `Problema de Conexão com Banco de Dados ${dbName}`,
|
||||
`A conexão com o banco de dados ${dbName} está com problemas: ${dbDetails.message || 'Erro não especificado'}`,
|
||||
{ dbName, dbDetails }
|
||||
);
|
||||
|
||||
// Atualiza o timestamp do último alerta
|
||||
this.alertCooldowns.set(dbName, now);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao verificar alertas de banco de dados: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendAlert(
|
||||
severity: 'critical' | 'warning' | 'info',
|
||||
title: string,
|
||||
message: string,
|
||||
details?: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const environment = this.configService.get<string>('NODE_ENV', 'development');
|
||||
const appName = this.configService.get<string>('APP_NAME', 'Portal Jurunense API');
|
||||
|
||||
this.logger.warn(`[${severity.toUpperCase()}] ${title}: ${message}`);
|
||||
|
||||
const payload = {
|
||||
severity,
|
||||
title: `[${environment.toUpperCase()}] [${appName}] ${title}`,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: details || {},
|
||||
environment,
|
||||
};
|
||||
|
||||
// Enviar para Slack, se configurado
|
||||
if (this.webhookUrls.slack) {
|
||||
await this.sendSlackAlert(payload);
|
||||
}
|
||||
|
||||
// Enviar para Microsoft Teams, se configurado
|
||||
if (this.webhookUrls.teams) {
|
||||
await this.sendTeamsAlert(payload);
|
||||
}
|
||||
|
||||
// Enviar para serviço de email, se configurado
|
||||
if (this.webhookUrls.email) {
|
||||
await this.sendEmailAlert(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao enviar alerta: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendSlackAlert(payload: any): Promise<void> {
|
||||
try {
|
||||
const slackPayload = {
|
||||
text: `${payload.title}`,
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: payload.title,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `*Mensagem:* ${payload.message}\n*Severidade:* ${payload.severity}\n*Ambiente:* ${payload.environment}\n*Timestamp:* ${payload.timestamp}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await firstValueFrom(this.httpService.post(this.webhookUrls.slack, slackPayload));
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao enviar alerta para Slack: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendTeamsAlert(payload: any): Promise<void> {
|
||||
try {
|
||||
const teamsPayload = {
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"themeColor": payload.severity === 'critical' ? "FF0000" : (payload.severity === 'warning' ? "FFA500" : "0078D7"),
|
||||
"summary": payload.title,
|
||||
"sections": [
|
||||
{
|
||||
"activityTitle": payload.title,
|
||||
"activitySubtitle": `Severidade: ${payload.severity} | Ambiente: ${payload.environment}`,
|
||||
"text": payload.message,
|
||||
"facts": [
|
||||
{
|
||||
"name": "Timestamp",
|
||||
"value": payload.timestamp
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await firstValueFrom(this.httpService.post(this.webhookUrls.teams, teamsPayload));
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao enviar alerta para Microsoft Teams: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendEmailAlert(payload: any): Promise<void> {
|
||||
try {
|
||||
const emailPayload = {
|
||||
subject: payload.title,
|
||||
text: `${payload.message}\n\nSeveridade: ${payload.severity}\nAmbiente: ${payload.environment}\nTimestamp: ${payload.timestamp}`,
|
||||
html: `<h2>${payload.title}</h2><p>${payload.message}</p><p><strong>Severidade:</strong> ${payload.severity}<br><strong>Ambiente:</strong> ${payload.environment}<br><strong>Timestamp:</strong> ${payload.timestamp}</p>`,
|
||||
};
|
||||
|
||||
await firstValueFrom(this.httpService.post(this.webhookUrls.email, emailPayload));
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro ao enviar alerta por email: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
HealthCheck,
|
||||
HealthCheckService,
|
||||
HttpHealthIndicator,
|
||||
DiskHealthIndicator,
|
||||
MemoryHealthIndicator,
|
||||
} from '@nestjs/terminus';
|
||||
import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
|
||||
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||
import * as os from 'os';
|
||||
|
||||
@ApiTags('Health Check')
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
private readonly diskPath: string;
|
||||
|
||||
constructor(
|
||||
private health: HealthCheckService,
|
||||
private http: HttpHealthIndicator,
|
||||
private disk: DiskHealthIndicator,
|
||||
private memory: MemoryHealthIndicator,
|
||||
private typeOrmHealth: TypeOrmHealthIndicator,
|
||||
private dbPoolStats: DbPoolStatsIndicator,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.diskPath = os.platform() === 'win32' ? 'C:\\' : '/';
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar saúde geral da aplicação' })
|
||||
check() {
|
||||
return this.health.check([
|
||||
// Verifica o status da própria aplicação
|
||||
() => this.http.pingCheck('api', 'http://localhost:8066/docs'),
|
||||
|
||||
// Verifica espaço em disco (espaço livre < 80%)
|
||||
() => this.disk.checkStorage('disk_percent', {
|
||||
path: this.diskPath,
|
||||
thresholdPercent: 0.8, // 80%
|
||||
}),
|
||||
|
||||
// Verifica espaço em disco (pelo menos 500MB livres)
|
||||
() => this.disk.checkStorage('disk_space', {
|
||||
path: this.diskPath,
|
||||
threshold: 500 * 1024 * 1024, // 500MB em bytes
|
||||
}),
|
||||
|
||||
// Verifica uso de memória (heap <150MB)
|
||||
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024), // 150MB
|
||||
|
||||
// Verifica as conexões de banco de dados
|
||||
() => this.typeOrmHealth.checkOracle(),
|
||||
() => this.typeOrmHealth.checkPostgres(),
|
||||
]);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('db')
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar saúde das conexões de banco de dados' })
|
||||
checkDatabase() {
|
||||
return this.health.check([
|
||||
() => this.typeOrmHealth.checkOracle(),
|
||||
() => this.typeOrmHealth.checkPostgres(),
|
||||
]);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('memory')
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar uso de memória' })
|
||||
checkMemory() {
|
||||
return this.health.check([
|
||||
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
|
||||
() => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),
|
||||
]);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('disk')
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar espaço em disco' })
|
||||
checkDisk() {
|
||||
return this.health.check([
|
||||
// Verificar espaço em disco usando porcentagem
|
||||
() => this.disk.checkStorage('disk_percent', {
|
||||
path: this.diskPath,
|
||||
thresholdPercent: 0.8,
|
||||
}),
|
||||
// Verificar espaço em disco usando valor absoluto
|
||||
() => this.disk.checkStorage('disk_space', {
|
||||
path: this.diskPath,
|
||||
threshold: 500 * 1024 * 1024,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('pool')
|
||||
@HealthCheck()
|
||||
@ApiOperation({ summary: 'Verificar estatísticas do pool de conexões' })
|
||||
checkPoolStats() {
|
||||
return this.health.check([
|
||||
() => this.dbPoolStats.checkOraclePoolStats(),
|
||||
() => this.dbPoolStats.checkPostgresPoolStats(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { HealthController } from './health.controller';
|
||||
import { TypeOrmHealthIndicator } from './indicators/typeorm.health';
|
||||
import { DbPoolStatsIndicator } from './indicators/db-pool-stats.health';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
|
||||
import { metricProviders } from './metrics/metrics.config';
|
||||
import { CustomMetricsService } from './metrics/custom.metrics';
|
||||
import { MetricsInterceptor } from './metrics/metrics.interceptor';
|
||||
import { HealthAlertService } from './alert/health-alert.service';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TerminusModule,
|
||||
HttpModule,
|
||||
ConfigModule,
|
||||
PrometheusModule.register({
|
||||
path: '/metrics',
|
||||
defaultMetrics: {
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
TypeOrmHealthIndicator,
|
||||
DbPoolStatsIndicator,
|
||||
CustomMetricsService,
|
||||
HealthAlertService,
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: MetricsInterceptor,
|
||||
},
|
||||
...metricProviders,
|
||||
],
|
||||
exports: [
|
||||
CustomMetricsService,
|
||||
HealthAlertService,
|
||||
],
|
||||
})
|
||||
export class HealthModule {}
|
||||
@@ -1,193 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
HealthIndicator,
|
||||
HealthIndicatorResult,
|
||||
HealthCheckError, // Import HealthCheckError for better terminus integration
|
||||
} from '@nestjs/terminus';
|
||||
import { InjectConnection } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const ORACLE_HEALTH_KEY = 'oracle_pool_stats';
|
||||
const POSTGRES_HEALTH_KEY = 'postgres_pool_stats';
|
||||
const ORACLE_PROGRAM_PATTERN = 'node%'; // Default pattern for Oracle
|
||||
const POSTGRES_APP_NAME_PATTERN = 'nodejs%'; // Default pattern for PostgreSQL
|
||||
|
||||
@Injectable()
|
||||
export class DbPoolStatsIndicator extends HealthIndicator {
|
||||
private readonly logger = new Logger(DbPoolStatsIndicator.name);
|
||||
|
||||
constructor(
|
||||
|
||||
@InjectConnection('oracle') private readonly oracleDataSource: DataSource,
|
||||
@InjectConnection('postgres') private readonly postgresDataSource: DataSource,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica a integridade do pool de conexões Oracle consultando V$SESSION.
|
||||
* Observações: Requer privilégios SELECT em V$SESSION e depende da coluna PROGRAM.
|
||||
* Isso verifica principalmente a acessibilidade do banco de dados e o sucesso da execução da consulta.
|
||||
* Considere estatísticas de pool em nível de driver para obter uma integridade de pool mais precisa, se disponível.
|
||||
*
|
||||
* @param key Custom key for the health indicator component.
|
||||
* @param programLike Optional pattern to match the PROGRAM column in V$SESSION.
|
||||
*/
|
||||
async checkOraclePoolStats(
|
||||
key: string = ORACLE_HEALTH_KEY,
|
||||
programLike: string = ORACLE_PROGRAM_PATTERN,
|
||||
): Promise<HealthIndicatorResult> {
|
||||
try {
|
||||
// Usar parâmetros de consulta é uma boa prática, embora menos crítica para LIKE com um padrão fixo.
|
||||
// Oracle usa a sintaxe :paramName
|
||||
const query = `
|
||||
SELECT
|
||||
COUNT(*) AS "totalConnections" -- Use quoted identifiers if needed, or match case below
|
||||
FROM
|
||||
V$SESSION
|
||||
WHERE
|
||||
TYPE = 'USER'
|
||||
AND PROGRAM LIKE :pattern
|
||||
`;
|
||||
const params = { pattern: programLike };
|
||||
|
||||
const results: { totalConnections: number | string }[] =
|
||||
await this.oracleDataSource.query(query, [params.pattern]); // Pass parameters as an array for Oracle usually
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
this.logger.warn(`Oracle V$SESSION query returned no results for pattern '${programLike}'`);
|
||||
|
||||
}
|
||||
|
||||
const totalConnections = parseInt(String(results?.[0]?.totalConnections ?? 0), 10);
|
||||
|
||||
if (isNaN(totalConnections)) {
|
||||
throw new Error('Failed to parse totalConnections from Oracle V$SESSION query result.');
|
||||
}
|
||||
// isHealthy é verdadeiro se a consulta for executada sem gerar um erro.
|
||||
// Adicione lógica aqui se contagens de conexão específicas indicarem estado não íntegro (por exemplo, > poolMax)
|
||||
const isHealthy = true;
|
||||
const details = {
|
||||
totalConnections: totalConnections,
|
||||
programPattern: programLike,
|
||||
};
|
||||
|
||||
return this.getStatus(key, isHealthy, details);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Oracle pool stats check failed for key "${key}": ${error.message}`, error.stack);
|
||||
throw new HealthCheckError(
|
||||
`${key} check failed`,
|
||||
this.getStatus(key, false, { message: error.message }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica a integridade do pool de conexões do PostgreSQL consultando pg_stat_activity.
|
||||
* Observações: Depende de o application_name estar definido corretamente na string de conexão ou nas opções.
|
||||
* Isso verifica principalmente a acessibilidade do banco de dados e o sucesso da execução da consulta.
|
||||
* Considere estatísticas de pool em nível de driver para obter uma integridade de pool mais precisa, se disponível.
|
||||
*
|
||||
* @param key Custom key for the health indicator component.
|
||||
* @param appNameLike Optional pattern to match the application_name column.
|
||||
*/
|
||||
async checkPostgresPoolStats(
|
||||
key: string = POSTGRES_HEALTH_KEY,
|
||||
appNameLike: string = POSTGRES_APP_NAME_PATTERN,
|
||||
): Promise<HealthIndicatorResult> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
count(*) AS "totalConnections",
|
||||
sum(CASE WHEN state = 'active' THEN 1 ELSE 0 END) AS "activeConnections",
|
||||
sum(CASE WHEN state = 'idle' THEN 1 ELSE 0 END) AS "idleConnections",
|
||||
sum(CASE WHEN state = 'idle in transaction' THEN 1 ELSE 0 END) AS "idleInTransactionConnections"
|
||||
FROM
|
||||
pg_stat_activity
|
||||
WHERE
|
||||
datname = current_database()
|
||||
AND application_name LIKE $1
|
||||
`;
|
||||
const params = [appNameLike];
|
||||
|
||||
const results: {
|
||||
totalConnections: string | number;
|
||||
activeConnections: string | number;
|
||||
idleConnections: string | number;
|
||||
idleInTransactionConnections: string | number;
|
||||
}[] = await this.postgresDataSource.query(query, params);
|
||||
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
|
||||
throw new Error('PostgreSQL pg_stat_activity query returned no results unexpectedly.');
|
||||
}
|
||||
|
||||
const result = results[0];
|
||||
|
||||
const totalConnections = parseInt(String(result.totalConnections ?? 0), 10);
|
||||
const activeConnections = parseInt(String(result.activeConnections ?? 0), 10);
|
||||
const idleConnections = parseInt(String(result.idleConnections ?? 0), 10);
|
||||
const idleInTransactionConnections = parseInt(String(result.idleInTransactionConnections ?? 0), 10);
|
||||
|
||||
// Validate parsing
|
||||
if (isNaN(totalConnections) || isNaN(activeConnections) || isNaN(idleConnections) || isNaN(idleInTransactionConnections)) {
|
||||
throw new Error('Failed to parse connection counts from PostgreSQL pg_stat_activity query result.');
|
||||
}
|
||||
|
||||
|
||||
const isHealthy = true;
|
||||
const details = {
|
||||
totalConnections,
|
||||
activeConnections,
|
||||
idleConnections,
|
||||
idleInTransactionConnections,
|
||||
applicationNamePattern: appNameLike,
|
||||
};
|
||||
|
||||
return this.getStatus(key, isHealthy, details);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`PostgreSQL pool stats check failed for key "${key}": ${error.message}`, error.stack);
|
||||
|
||||
throw new HealthCheckError(
|
||||
`${key} check failed`,
|
||||
this.getStatus(key, false, { message: error.message }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to run all pool checks defined in this indicator.
|
||||
* You would typically call this from your main HealthController.
|
||||
*/
|
||||
async checkAllPools() : Promise<HealthIndicatorResult[]> {
|
||||
const results = await Promise.allSettled([
|
||||
this.checkOraclePoolStats(),
|
||||
this.checkPostgresPoolStats()
|
||||
]);
|
||||
|
||||
// Processa os resultados para se ajustar à estrutura do Terminus, se necessário, ou retorna diretamente
|
||||
// Observações: Métodos individuais já retornam HealthIndicatorResult ou lançam HealthCheckError
|
||||
// Este método pode não ser estritamente necessário se você chamar verificações individuais no controlador.
|
||||
// Para simplificar, vamos supor que o controlador chama as verificações individuais.
|
||||
// Se você quisesse que esse método retornasse um único status, precisaria de mais lógica.
|
||||
// Relançar erros ou agregar status.
|
||||
|
||||
// Example: Log results (individual methods handle the Terminus return/error)
|
||||
results.forEach(result => {
|
||||
if (result.status === 'rejected') {
|
||||
// Already logged and thrown as HealthCheckError inside the check methods
|
||||
} else {
|
||||
// Optionally log success details
|
||||
this.logger.log(`Pool check successful: ${JSON.stringify(result.value)}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return results
|
||||
.filter((r): r is PromiseFulfilledResult<HealthIndicatorResult> => r.status === 'fulfilled')
|
||||
.map(r => r.value);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
|
||||
import { InjectConnection } from '@nestjs/typeorm';
|
||||
import { Connection, DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class TypeOrmHealthIndicator extends HealthIndicator {
|
||||
constructor(
|
||||
@InjectConnection('oracle') private oracleConnection: DataSource,
|
||||
@InjectConnection('postgres') private postgresConnection: DataSource,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async checkOracle(): Promise<HealthIndicatorResult> {
|
||||
const key = 'oracle';
|
||||
|
||||
try {
|
||||
const isHealthy = this.oracleConnection.isInitialized;
|
||||
|
||||
const result = this.getStatus(key, isHealthy);
|
||||
|
||||
if (isHealthy) {
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new HealthCheckError('Oracle healthcheck failed', result);
|
||||
} catch (error) {
|
||||
const result = this.getStatus(key, false, { message: error.message });
|
||||
throw new HealthCheckError('Oracle healthcheck failed', result);
|
||||
}
|
||||
}
|
||||
|
||||
async checkPostgres(): Promise<HealthIndicatorResult> {
|
||||
const key = 'postgres';
|
||||
|
||||
try {
|
||||
const isHealthy = this.postgresConnection.isInitialized;
|
||||
|
||||
const result = this.getStatus(key, isHealthy);
|
||||
|
||||
if (isHealthy) {
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new HealthCheckError('Postgres healthcheck failed', result);
|
||||
} catch (error) {
|
||||
const result = this.getStatus(key, false, { message: error.message });
|
||||
throw new HealthCheckError('Postgres healthcheck failed', result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectMetric } from '@willsoto/nestjs-prometheus';
|
||||
import { Counter, Gauge, Histogram } from 'prom-client';
|
||||
import { InjectConnection } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class CustomMetricsService {
|
||||
constructor(
|
||||
@InjectMetric('http_request_total')
|
||||
private readonly requestCounter: Counter<string>,
|
||||
|
||||
@InjectMetric('http_request_duration_seconds')
|
||||
private readonly requestDuration: Histogram<string>,
|
||||
|
||||
@InjectMetric('api_memory_usage_bytes')
|
||||
private readonly memoryGauge: Gauge<string>,
|
||||
|
||||
@InjectMetric('api_db_connection_pool_used')
|
||||
private readonly dbPoolUsedGauge: Gauge<string>,
|
||||
|
||||
@InjectMetric('api_db_connection_pool_total')
|
||||
private readonly dbPoolTotalGauge: Gauge<string>,
|
||||
|
||||
@InjectMetric('api_db_query_duration_seconds')
|
||||
private readonly dbQueryDuration: Histogram<string>,
|
||||
|
||||
@InjectConnection('oracle')
|
||||
private oracleConnection: DataSource,
|
||||
|
||||
@InjectConnection('postgres')
|
||||
private postgresConnection: DataSource,
|
||||
) {
|
||||
// Iniciar coleta de métricas de memória
|
||||
this.startMemoryMetrics();
|
||||
|
||||
// Iniciar coleta de métricas do pool de conexões
|
||||
this.startDbPoolMetrics();
|
||||
}
|
||||
|
||||
recordHttpRequest(method: string, route: string, statusCode: number): void {
|
||||
this.requestCounter.inc({ method, route, statusCode: statusCode.toString() });
|
||||
}
|
||||
|
||||
startTimingRequest(): (labels?: Record<string, string>) => void {
|
||||
const end = this.requestDuration.startTimer();
|
||||
return (labels?: Record<string, string>) => end(labels);
|
||||
}
|
||||
|
||||
recordDbQueryDuration(db: 'oracle' | 'postgres', operation: string, durationMs: number): void {
|
||||
this.dbQueryDuration.observe({ db, operation }, durationMs / 1000);
|
||||
}
|
||||
|
||||
private startMemoryMetrics(): void {
|
||||
// Coletar métricas de memória a cada 15 segundos
|
||||
setInterval(() => {
|
||||
const memoryUsage = process.memoryUsage();
|
||||
this.memoryGauge.set({ type: 'rss' }, memoryUsage.rss);
|
||||
this.memoryGauge.set({ type: 'heapTotal' }, memoryUsage.heapTotal);
|
||||
this.memoryGauge.set({ type: 'heapUsed' }, memoryUsage.heapUsed);
|
||||
this.memoryGauge.set({ type: 'external' }, memoryUsage.external);
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
private startDbPoolMetrics(): void {
|
||||
// Coletar métricas do pool de conexões a cada 15 segundos
|
||||
setInterval(async () => {
|
||||
try {
|
||||
// Tente obter estatísticas do pool do Oracle
|
||||
// Nota: depende da implementação específica do OracleDB
|
||||
if (this.oracleConnection && this.oracleConnection.driver) {
|
||||
const oraclePoolStats = (this.oracleConnection.driver as any).pool?.getStatistics?.();
|
||||
if (oraclePoolStats) {
|
||||
this.dbPoolUsedGauge.set({ db: 'oracle' }, oraclePoolStats.busy || 0);
|
||||
this.dbPoolTotalGauge.set({ db: 'oracle' }, oraclePoolStats.poolMax || 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Tente obter estatísticas do pool do Postgres
|
||||
// Nota: depende da implementação específica do TypeORM
|
||||
if (this.postgresConnection && this.postgresConnection.driver) {
|
||||
const pgPoolStats = (this.postgresConnection.driver as any).pool;
|
||||
if (pgPoolStats) {
|
||||
this.dbPoolUsedGauge.set({ db: 'postgres' }, pgPoolStats.totalCount - pgPoolStats.idleCount || 0);
|
||||
this.dbPoolTotalGauge.set({ db: 'postgres' }, pgPoolStats.totalCount || 0);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao coletar métricas do pool de conexões:', error);
|
||||
}
|
||||
}, 15000);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import {
|
||||
makeCounterProvider,
|
||||
makeGaugeProvider,
|
||||
makeHistogramProvider
|
||||
} from '@willsoto/nestjs-prometheus';
|
||||
|
||||
export const metricProviders = [
|
||||
// Contador de requisições HTTP
|
||||
makeCounterProvider({
|
||||
name: 'http_request_total',
|
||||
help: 'Total de requisições HTTP',
|
||||
labelNames: ['method', 'route', 'statusCode'],
|
||||
}),
|
||||
|
||||
// Histograma de duração de requisições HTTP
|
||||
makeHistogramProvider({
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'Duração das requisições HTTP em segundos',
|
||||
labelNames: ['method', 'route', 'error'], // 👈 adicionado "error"
|
||||
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
|
||||
}),
|
||||
|
||||
// Gauge para uso de memória
|
||||
makeGaugeProvider({
|
||||
name: 'api_memory_usage_bytes',
|
||||
help: 'Uso de memória da aplicação em bytes',
|
||||
labelNames: ['type'],
|
||||
}),
|
||||
|
||||
// Gauge para conexões de banco de dados usadas
|
||||
makeGaugeProvider({
|
||||
name: 'api_db_connection_pool_used',
|
||||
help: 'Número de conexões de banco de dados em uso',
|
||||
labelNames: ['db'],
|
||||
}),
|
||||
|
||||
// Gauge para total de conexões no pool de banco de dados
|
||||
makeGaugeProvider({
|
||||
name: 'api_db_connection_pool_total',
|
||||
help: 'Número total de conexões no pool de banco de dados',
|
||||
labelNames: ['db'],
|
||||
}),
|
||||
|
||||
// Histograma para duração de consultas de banco de dados
|
||||
makeHistogramProvider({
|
||||
name: 'api_db_query_duration_seconds',
|
||||
help: 'Duração das consultas de banco de dados em segundos',
|
||||
labelNames: ['db', 'operation'],
|
||||
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2],
|
||||
}),
|
||||
];
|
||||
@@ -1,64 +0,0 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { CustomMetricsService } from './custom.metrics';
|
||||
|
||||
@Injectable()
|
||||
export class MetricsInterceptor implements NestInterceptor {
|
||||
constructor(private metricsService: CustomMetricsService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
if (context.getType() !== 'http') {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const { method, url } = request;
|
||||
|
||||
// Simplificar a rota para evitar cardinalidade alta no Prometheus
|
||||
// Ex: /users/123 -> /users/:id
|
||||
const route = this.normalizeRoute(url);
|
||||
|
||||
// Inicia o timer para medir a duração da requisição
|
||||
const endTimer = this.metricsService.startTimingRequest();
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: (data) => {
|
||||
const response = context.switchToHttp().getResponse();
|
||||
const statusCode = response.statusCode;
|
||||
|
||||
// Registra a requisição concluída
|
||||
this.metricsService.recordHttpRequest(method, route, statusCode);
|
||||
|
||||
// Finaliza o timer com labels adicionais
|
||||
endTimer({ method, route });
|
||||
},
|
||||
error: (error) => {
|
||||
// Determina o código de status do erro
|
||||
const statusCode = error.status || 500;
|
||||
|
||||
// Registra a requisição com erro
|
||||
this.metricsService.recordHttpRequest(method, route, statusCode);
|
||||
|
||||
// Finaliza o timer com labels adicionais
|
||||
endTimer({ method, route, error: 'true' });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeRoute(url: string): string {
|
||||
// Remove query parameters
|
||||
const path = url.split('?')[0];
|
||||
|
||||
// Normaliza rotas com IDs e outros parâmetros dinâmicos
|
||||
// Por exemplo, /users/123 -> /users/:id
|
||||
return path.replace(/\/[0-9a-f]{8,}|\/[0-9]+/g, '/:id');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/controllers#controllers
|
||||
*/
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
32
src/main.ts
32
src/main.ts
@@ -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();
|
||||
|
||||
@@ -38,7 +38,7 @@ export class CreatePaymentDto {
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Valor do pagamento',
|
||||
example: 1000.00,
|
||||
example: 1000.0,
|
||||
required: true,
|
||||
})
|
||||
amount: number;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export class PaymentDto {
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Valor do pagamento',
|
||||
example: 1000.00,
|
||||
example: 1000.0,
|
||||
})
|
||||
amount: number;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
|
||||
/*
|
||||
https://docs.nestjs.com/modules
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export class FindInvoiceDto {
|
||||
chavenfe: string;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
export class InvoiceCheckItemDto {
|
||||
productId: number;
|
||||
seq: number;
|
||||
qt: number;
|
||||
confDate: string;
|
||||
}
|
||||
|
||||
productId: number;
|
||||
seq: number;
|
||||
qt: number;
|
||||
confDate: string;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export interface DebQueryParams {
|
||||
cpfCgcent: string;
|
||||
matricula?: number;
|
||||
}
|
||||
|
||||
|
||||
cpfCgcent: string;
|
||||
matricula?: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface MarkData {
|
||||
MARCA: string;
|
||||
CODMARCA: number;
|
||||
ATIVO: string;
|
||||
}
|
||||
MARCA: string;
|
||||
CODMARCA: number;
|
||||
ATIVO: string;
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user