diff --git a/src/Log/ILogger.ts b/src/Log/ILogger.ts deleted file mode 100644 index 8406271..0000000 --- a/src/Log/ILogger.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ILogger { - log(message: string): void; - warn(message: string): void; - error(message: string, trace?: string): void; - } - \ No newline at end of file diff --git a/src/Log/NestLoggerAdapter.ts b/src/Log/NestLoggerAdapter.ts deleted file mode 100644 index d5ee169..0000000 --- a/src/Log/NestLoggerAdapter.ts +++ /dev/null @@ -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): void { - this.logger.log(this.formatMessage(message, meta)); - } - - warn(message: string, meta?: Record): void { - this.logger.warn(this.formatMessage(message, meta)); - } - - error(message: string, trace?: string, meta?: Record): void { - this.logger.error(this.formatMessage(message, meta), trace); - } - - private formatMessage(message: string, meta?: Record): string { - return meta ? `${message} | ${JSON.stringify(meta)}` : message; - } -} \ No newline at end of file diff --git a/src/Log/logger.decorator.ts b/src/Log/logger.decorator.ts deleted file mode 100644 index bd6d6bb..0000000 --- a/src/Log/logger.decorator.ts +++ /dev/null @@ -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; - }; -} diff --git a/src/Log/logger.module.ts b/src/Log/logger.module.ts deleted file mode 100644 index 8b4bdec..0000000 --- a/src/Log/logger.module.ts +++ /dev/null @@ -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 {} diff --git a/src/app.module.ts b/src/app.module.ts index 9d4fed3..3752ee4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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'); } } diff --git a/src/auth/auth/__tests__/auth.service.spec.helper.ts b/src/auth/auth/__tests__/auth.service.spec.helper.ts index e23b77b..5605b39 100644 --- a/src/auth/auth/__tests__/auth.service.spec.helper.ts +++ b/src/auth/auth/__tests__/auth.service.spec.helper.ts @@ -46,7 +46,9 @@ export interface AuthServiceTestContext { mockUserRepository: ReturnType; mockTokenBlacklistService: ReturnType; mockRefreshTokenService: ReturnType; - mockSessionManagementService: ReturnType; + mockSessionManagementService: ReturnType< + typeof createMockSessionManagementService + >; } export async function createAuthServiceTestModule(): Promise { @@ -101,4 +103,3 @@ export async function createAuthServiceTestModule(): Promise { 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); }); @@ -183,7 +189,7 @@ describe('AuthService - createToken', () => { * de validação no método createToken. Atualmente, o método não valida * os parâmetros de entrada, o que pode causar problemas de segurança * e tokens inválidos. - * + * * PROBLEMAS IDENTIFICADOS: * 1. Não valida se IDs são positivos * 2. Não valida se strings estão vazias @@ -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'); }); }); }); - diff --git a/src/auth/auth/__tests__/createTokenPair.spec.ts b/src/auth/auth/__tests__/createTokenPair.spec.ts index a7ec45b..6c1a5e8 100644 --- a/src/auth/auth/__tests__/createTokenPair.spec.ts +++ b/src/auth/auth/__tests__/createTokenPair.spec.ts @@ -14,7 +14,7 @@ describe('AuthService - createTokenPair', () => { describe('createTokenPair - Tests that expose problems', () => { /** * NOTA: Estes testes identificam problemas no método createTokenPair. - * + * * PROBLEMAS IDENTIFICADOS: * 1. Não há rollback se um token é criado mas o outro falha * 2. Não valida se os tokens foram realmente gerados @@ -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'); }); @@ -101,18 +141,26 @@ describe('AuthService - createTokenPair', () => { * Solução esperada: Access token sempre primeiro. */ const callOrder = []; - + context.mockJwtService.sign.mockImplementation(() => { callOrder.push('accessToken'); return 'mock.access.token'; }); - - context.mockRefreshTokenService.generateRefreshToken.mockImplementation(async () => { - callOrder.push('refreshToken'); - return 'mock.refresh.token'; - }); - await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001'); + context.mockRefreshTokenService.generateRefreshToken.mockImplementation( + async () => { + callOrder.push('refreshToken'); + return 'mock.refresh.token'; + }, + ); + + 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'); @@ -135,7 +189,7 @@ describe('AuthService - createTokenPair', () => { * Cenário: Múltiplas chamadas simultâneas para o mesmo usuário. * Problema: Pode criar múltiplos pares de tokens inconsistentes. * Solução esperada: JWT service gera tokens únicos com timestamps diferentes. - * + * * Nota: Na implementação real, o JWT service inclui timestamp e outros dados * que garantem unicidade. Aqui simulamos isso no mock. */ @@ -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', () => { }); }); }); - diff --git a/src/auth/auth/__tests__/logout.spec.ts b/src/auth/auth/__tests__/logout.spec.ts index cb6b1af..d5c4ae3 100644 --- a/src/auth/auth/__tests__/logout.spec.ts +++ b/src/auth/auth/__tests__/logout.spec.ts @@ -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(() => { @@ -24,7 +28,7 @@ describe('AuthService - logout', () => { describe('logout - Tests that expose problems', () => { /** * NOTA: Estes testes identificam problemas no método logout. - * + * * PROBLEMAS IDENTIFICADOS: * 1. Não valida token de entrada (vazio, null, undefined) * 2. Não valida se token foi decodificado corretamente @@ -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); }); }); }); - diff --git a/src/auth/auth/__tests__/refreshAccessToken.spec.ts b/src/auth/auth/__tests__/refreshAccessToken.spec.ts index b382b87..0c7606e 100644 --- a/src/auth/auth/__tests__/refreshAccessToken.spec.ts +++ b/src/auth/auth/__tests__/refreshAccessToken.spec.ts @@ -19,7 +19,9 @@ describe('AuthService - refreshAccessToken', () => { situacao: 'A', dataDesligamento: null, }); - context.mockSessionManagementService.isSessionActive.mockResolvedValue(true); + context.mockSessionManagementService.isSessionActive.mockResolvedValue( + true, + ); }); afterEach(() => { @@ -29,7 +31,7 @@ describe('AuthService - refreshAccessToken', () => { describe('refreshAccessToken - Tests that expose problems', () => { /** * NOTA: Estes testes identificam problemas no método refreshAccessToken. - * + * * PROBLEMAS IDENTIFICADOS: * 1. Não valida refresh token antes de processar * 2. Não valida dados retornados pelo refresh token service @@ -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'); }); }); }); - diff --git a/src/auth/auth/auth.controller.ts b/src/auth/auth/auth.controller.ts index e32b67e..a2db478 100644 --- a/src/auth/auth/auth.controller.ts +++ b/src/auth/auth/auth.controller.ts @@ -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 +import { + LoginAuditFiltersDto, + LoginAuditResponseDto, + LoginStatsDto, + LoginStatsFiltersDto, } from './dto/login-audit.dto'; import { ApiTags, @@ -66,9 +69,12 @@ 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 { + async login( + @Body() dto: LoginDto, + @Request() req, + ): Promise { const ip = this.getClientIp(req); - + const command = new AuthenticateUserCommand(dto.username, dto.password); const result = await this.commandBus.execute(command); @@ -76,7 +82,7 @@ export class AuthController { if (!result.success) { await this.rateLimitingService.recordAttempt(ip, false); - + await this.loginAuditService.logLoginAttempt({ username: dto.username, ipAddress: ip, @@ -84,7 +90,7 @@ export class AuthController { success: false, failureReason: result.error, }); - + throw new HttpException( new ResultModel(false, result.error, null, result.error), HttpStatus.UNAUTHORIZED, @@ -94,19 +100,23 @@ export class AuthController { await this.rateLimitingService.recordAttempt(ip, true); const user = result.data; - + /** * 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( user.id, ip, @@ -161,7 +171,6 @@ export class AuthController { ); } - @Post('logout') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @@ -170,10 +179,15 @@ export class AuthController { @ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' }) async logout(@Request() req): Promise<{ message: string }> { const token = req.headers.authorization?.replace('Bearer ', ''); - + 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 { + @ApiUnauthorizedResponse({ + description: 'Refresh token inválido ou expirado', + }) + async refreshToken( + @Body() dto: RefreshTokenDto, + ): Promise { const result = await this.authService.refreshAccessToken(dto.refreshToken); return result; } @@ -210,15 +228,20 @@ export class AuthController { async getSessions(@Request() req): Promise { 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, @@ -238,7 +261,7 @@ export class AuthController { ): Promise<{ message: string }> { const userId = req.user.id; await this.sessionManagementService.terminateSession(userId, sessionId); - + return { message: 'Sessão encerrada com sucesso', }; @@ -253,7 +276,7 @@ export class AuthController { async terminateAllSessions(@Request() req): Promise<{ message: string }> { const userId = req.user.id; await this.sessionManagementService.terminateAllSessions(userId); - + return { message: 'Todas as sessões foram encerradas com sucesso', }; @@ -273,7 +296,7 @@ export class AuthController { @Request() req, ): Promise { const userId = req.user.id; - + const auditFilters = { ...filters, userId: filters.userId || userId, @@ -282,9 +305,9 @@ 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), })), @@ -309,12 +332,12 @@ export class AuthController { ): Promise { const userId = req.user.id; const days = filters.days || 7; - + const stats = await this.loginAuditService.getLoginStats( filters.userId || userId, days, ); - + return stats; } @@ -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, ); } @@ -362,15 +389,17 @@ export class AuthController { * Busca o usuário pelo username para obter o ID */ const user = await this.authService.findUserByUsername(username); - + if (!user) { return { hasActiveSession: false, }; } - const activeSession = await this.sessionManagementService.hasActiveSession(user.id); - + const activeSession = await this.sessionManagementService.hasActiveSession( + user.id, + ); + if (!activeSession) { return { hasActiveSession: false, @@ -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), + ), }, }; } diff --git a/src/auth/auth/auth.module.ts b/src/auth/auth/auth.module.ts index 2851a01..8297769 100644 --- a/src/auth/auth/auth.module.ts +++ b/src/auth/auth/auth.module.ts @@ -35,14 +35,14 @@ import { LoginAuditService } from '../services/login-audit.service'; ], controllers: [AuthController], providers: [ - AuthService, - JwtStrategy, - TokenBlacklistService, - RateLimitingService, + AuthService, + JwtStrategy, + TokenBlacklistService, + RateLimitingService, RefreshTokenService, SessionManagementService, LoginAuditService, - AuthenticateUserHandler + AuthenticateUserHandler, ], exports: [AuthService], }) diff --git a/src/auth/auth/auth.service.ts b/src/auth/auth/auth.service.ts index 2974c36..120d31e 100644 --- a/src/auth/auth/auth.service.ts +++ b/src/auth/auth/auth.service.ts @@ -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()) { @@ -77,7 +95,7 @@ export class AuthService { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const multipleAtSymbols = (email.match(/@/g) || []).length > 1; - + if (!emailRegex.test(email) || multipleAtSymbols) { throw new BadRequestException('Formato de email inválido'); } @@ -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); - - if (!accessToken || typeof accessToken !== 'string' || !accessToken.trim()) { + 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() + ) { throw new Error('Token de acesso inválido gerado'); } - const refreshToken = await this.refreshTokenService.generateRefreshToken(id, sessionId); - - if (!refreshToken || typeof refreshToken !== 'string' || !refreshToken.trim()) { + const refreshToken = await this.refreshTokenService.generateRefreshToken( + id, + sessionId, + ); + + if ( + !refreshToken || + typeof refreshToken !== 'string' || + !refreshToken.trim() + ) { throw new Error('Refresh token inválido gerado'); } @@ -121,8 +164,10 @@ 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}`, + ); } } } @@ -289,4 +370,4 @@ export class AuthService { async findUserByUsername(username: string) { return this.userRepository.findByUsername(username); } -} \ No newline at end of file +} diff --git a/src/auth/auth/commands/authenticate-user.command.ts b/src/auth/auth/commands/authenticate-user.command.ts index 8ceedb3..f4334c1 100644 --- a/src/auth/auth/commands/authenticate-user.command.ts +++ b/src/auth/auth/commands/authenticate-user.command.ts @@ -1,7 +1,6 @@ export class AuthenticateUserCommand { - constructor( - public readonly username: string, - public readonly password: string, - ) {} - } - \ No newline at end of file + constructor( + public readonly username: string, + public readonly password: string, + ) {} +} diff --git a/src/auth/auth/commands/authenticate-user.service.ts b/src/auth/auth/commands/authenticate-user.service.ts index 6e4848c..ee8c175 100644 --- a/src/auth/auth/commands/authenticate-user.service.ts +++ b/src/auth/auth/commands/authenticate-user.service.ts @@ -7,13 +7,18 @@ import { UserModel } from 'src/core/models/user.model'; @CommandHandler(AuthenticateUserCommand) @Injectable() -export class AuthenticateUserHandler implements ICommandHandler { +export class AuthenticateUserHandler + implements ICommandHandler +{ constructor(private readonly userRepository: UserRepository) {} async execute(command: AuthenticateUserCommand): Promise> { 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'); @@ -30,7 +35,6 @@ export class AuthenticateUserHandler implements ICommandHandler 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' }) @@ -114,22 +137,30 @@ export class LoginStatsDto { topIps: Array<{ ip: string; count: number }>; @ApiProperty({ description: 'Estatísticas diárias' }) - dailyStats: Array<{ - date: string; - attempts: number; - successes: number; - failures: number; + dailyStats: Array<{ + date: string; + attempts: number; + successes: number; + failures: number; }>; } 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) diff --git a/src/auth/guards/__tests__/rate-limiting.guard.spec.ts b/src/auth/guards/__tests__/rate-limiting.guard.spec.ts index fb2d0d3..a79e8b7 100644 --- a/src/auth/guards/__tests__/rate-limiting.guard.spec.ts +++ b/src/auth/guards/__tests__/rate-limiting.guard.spec.ts @@ -50,7 +50,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => { describe('canActivate', () => { /** * NOTA: Estes testes identificam problemas no método canActivate. - * + * * PROBLEMAS IDENTIFICADOS: * 1. Não valida se IP extraído é válido * 2. Não valida se rate limiting service retorna dados válidos @@ -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', () => { }); }); }); - diff --git a/src/auth/guards/rate-limiting.guard.ts b/src/auth/guards/rate-limiting.guard.ts index 3ec33b8..c422847 100644 --- a/src/auth/guards/rate-limiting.guard.ts +++ b/src/auth/guards/rate-limiting.guard.ts @@ -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, @@ -30,13 +37,14 @@ export class RateLimitingGuard implements CanActivate { HttpStatus.INTERNAL_SERVER_ERROR, ); } - + if (!isAllowed) { let attemptInfo; 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, @@ -49,11 +57,12 @@ export class RateLimitingGuard implements CanActivate { } this.validateAttemptInfo(attemptInfo); - + 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 ''; @@ -90,7 +102,7 @@ export class RateLimitingGuard implements CanActivate { } const trimmedIp = rawIp.trim(); - + if (trimmedIp === '') { 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; }); @@ -180,17 +196,17 @@ export class RateLimitingGuard implements CanActivate { if (ip.includes('::')) { const parts = ip.split('::'); if (parts.length > 2) return false; - + 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, diff --git a/src/auth/models/result.ts b/src/auth/models/result.ts index 4d67e32..93618e2 100644 --- a/src/auth/models/result.ts +++ b/src/auth/models/result.ts @@ -1,16 +1,15 @@ export class Result { - private constructor( - public readonly success: boolean, - public readonly data?: T, - public readonly error?: string, - ) {} - - static ok(data: U): Result { - return new Result(true, data); - } - - static fail(message: string): Result { - return new Result(false, undefined, message); - } + private constructor( + public readonly success: boolean, + public readonly data?: T, + public readonly error?: string, + ) {} + + static ok(data: U): Result { + return new Result(true, data); } - \ No newline at end of file + + static fail(message: string): Result { + return new Result(false, undefined, message); + } +} diff --git a/src/auth/services/login-audit.service.ts b/src/auth/services/login-audit.service.ts index 48689b7..e060869 100644 --- a/src/auth/services/login-audit.service.ts +++ b/src/auth/services/login-audit.service.ts @@ -31,14 +31,14 @@ 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): Promise { + async logLoginAttempt( + log: Omit, + ): Promise { const logId = this.generateLogId(); const timestamp = DateUtil.now(); - + const auditLog: LoginAuditLog = { ...log, id: logId, @@ -69,24 +69,26 @@ export class LoginAuditService { await this.redis.expire(dateLogsKey, this.LOG_EXPIRY); } - async getLoginLogs(filters: LoginAuditFilters = {}): Promise { + async getLoginLogs( + filters: LoginAuditFilters = {}, + ): Promise { const logIds = await this.getLogIds(filters); const logs: LoginAuditLog[] = []; for (const logId of logIds) { const logKey = this.buildLogKey(logId); const logData = await this.redis.get(logKey); - + if (!logData) { continue; } const log: LoginAuditLog = JSON.parse(logData as string); - + if (typeof log.timestamp === 'string') { log.timestamp = new Date(log.timestamp); } - + if (!this.matchesFilters(log, filters)) { continue; } @@ -98,21 +100,29 @@ export class LoginAuditService { const offset = filters.offset || 0; const limit = filters.limit || 100; - + 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); - + const filters: LoginAuditFilters = { startDate, endDate, @@ -124,38 +134,50 @@ export class LoginAuditService { } const logs = await this.getLoginLogs(filters); - + 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(); - logs.forEach(log => { + logs.forEach((log) => { ipCounts.set(log.ipAddress, (ipCounts.get(log.ipAddress) || 0) + 1); }); - + stats.topIps = Array.from(ipCounts.entries()) .map(([ip, count]) => ({ ip, count })) .sort((a, b) => b.count - a.count) .slice(0, 10); - const dailyCounts = new Map(); - 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) { dayStats.successes++; dailyCounts.set(date, dayStats); return; } - + dayStats.failures++; dailyCounts.set(date, dayStats); }); @@ -168,9 +190,11 @@ export class LoginAuditService { } async cleanupOldLogs(): Promise { - 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); for (const date of oldDates) { const dateLogsKey = this.buildDateLogsKey(date); @@ -190,18 +214,20 @@ 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); const logIds: string[] = []; - + for (const date of dates) { const dateLogsKey = this.buildDateLogsKey(date); const dateLogIds = await this.redis.lrange(dateLogsKey, 0, -1); logIds.push(...dateLogIds); } - + return logIds; } @@ -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,11 +261,17 @@ 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; } - + if (filters.success !== undefined && log.success !== filters.success) { return false; } @@ -256,12 +290,12 @@ export class LoginAuditService { private getDateRange(startDate: Date, endDate: Date): string[] { const dates: string[] = []; const currentDate = new Date(startDate); - + while (currentDate <= endDate) { dates.push(DateUtil.toBrazilString(currentDate, 'yyyy-MM-dd')); currentDate.setDate(currentDate.getDate() + 1); } - + return dates; } } diff --git a/src/auth/services/rate-limiting.service.ts b/src/auth/services/rate-limiting.service.ts index 8ad328c..dc8ff22 100644 --- a/src/auth/services/rate-limiting.service.ts +++ b/src/auth/services/rate-limiting.service.ts @@ -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): Promise { + async isAllowed( + ip: string, + config?: Partial, + ): Promise { 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): Promise { + async recordAttempt( + ip: string, + success: boolean, + config?: Partial, + ): Promise { const finalConfig = { ...this.defaultConfig, ...config }; const key = this.buildAttemptKey(ip); const blockKey = this.buildBlockKey(ip); @@ -98,7 +103,7 @@ export class RateLimitingService { async clearAttempts(ip: string): Promise { const key = this.buildAttemptKey(ip); const blockKey = this.buildBlockKey(ip); - + await this.redis.del(key); await this.redis.del(blockKey); } diff --git a/src/auth/services/refresh-token.service.ts b/src/auth/services/refresh-token.service.ts index 605e9a9..017179d 100644 --- a/src/auth/services/refresh-token.service.ts +++ b/src/auth/services/refresh-token.service.ts @@ -24,18 +24,21 @@ export class RefreshTokenService { private readonly jwtService: JwtService, ) {} - async generateRefreshToken(userId: number, sessionId?: string): Promise { + async generateRefreshToken( + userId: number, + sessionId?: string, + ): Promise { 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(), }; @@ -50,7 +53,7 @@ export class RefreshTokenService { async validateRefreshToken(refreshToken: string): Promise { try { const decoded = this.jwtService.verify(refreshToken) as any; - + if (decoded.type !== 'refresh') { throw new UnauthorizedException('Token inválido'); } @@ -68,14 +71,14 @@ export class RefreshTokenService { throw new UnauthorizedException('Refresh token expirado'); } - return { - id: userId, - sellerId: 0, - storeId: '', - username: '', + return { + id: userId, + sellerId: 0, + storeId: '', + username: '', email: '', sessionId: sessionId || tokenData.sessionId, - tokenId + tokenId, } as JwtPayload; } catch (error) { throw new UnauthorizedException('Refresh token inválido'); @@ -90,7 +93,7 @@ export class RefreshTokenService { async revokeAllRefreshTokens(userId: number): Promise { const pattern = this.buildRefreshTokenPattern(userId); const keys = await this.redis.keys(pattern); - + if (keys.length > 0) { await this.redis.del(...keys); } @@ -99,9 +102,9 @@ export class RefreshTokenService { async getActiveRefreshTokens(userId: number): Promise { const pattern = this.buildRefreshTokenPattern(userId); const keys = await this.redis.keys(pattern); - + const tokens: RefreshTokenData[] = []; - + for (const key of keys) { const tokenData = await this.redis.get(key); if (tokenData && tokenData.expiresAt > DateUtil.nowTimestamp()) { @@ -114,11 +117,11 @@ export class RefreshTokenService { private async limitRefreshTokensPerUser(userId: number): Promise { const activeTokens = await this.getActiveRefreshTokens(userId); - + 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); diff --git a/src/auth/services/session-management.service.ts b/src/auth/services/session-management.service.ts index 3ee3017..b4a5ddf 100644 --- a/src/auth/services/session-management.service.ts +++ b/src/auth/services/session-management.service.ts @@ -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 { + async createSession( + userId: number, + ipAddress: string, + userAgent: string, + ): Promise { 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 { + async updateSessionActivity( + userId: number, + sessionId: string, + ): Promise { const key = this.buildSessionKey(userId, sessionId); const sessionData = await this.redis.get(key); @@ -55,12 +60,15 @@ export class SessionManagementService { } } - async getActiveSessions(userId: number, currentSessionId?: string): Promise { + async getActiveSessions( + userId: number, + currentSessionId?: string, + ): Promise { const pattern = this.buildSessionPattern(userId); const keys = await this.redis.keys(pattern); - + const sessions: SessionData[] = []; - + for (const key of keys) { const sessionData = await this.redis.get(key); if (sessionData && sessionData.isActive) { @@ -89,7 +97,7 @@ export class SessionManagementService { async terminateAllSessions(userId: number): Promise { const pattern = this.buildSessionPattern(userId); const keys = await this.redis.keys(pattern); - + for (const key of keys) { const sessionData = await this.redis.get(key); if (sessionData) { @@ -99,10 +107,13 @@ export class SessionManagementService { } } - async terminateOtherSessions(userId: number, currentSessionId: string): Promise { + async terminateOtherSessions( + userId: number, + currentSessionId: string, + ): Promise { const pattern = this.buildSessionPattern(userId); const keys = await this.redis.keys(pattern); - + for (const key of keys) { const sessionData = await this.redis.get(key); if (sessionData && sessionData.sessionId !== currentSessionId) { @@ -115,7 +126,7 @@ export class SessionManagementService { async isSessionActive(userId: number, sessionId: string): Promise { const key = this.buildSessionKey(userId, sessionId); const sessionData = await this.redis.get(key); - + return sessionData ? sessionData.isActive : false; } @@ -126,11 +137,11 @@ export class SessionManagementService { private async limitSessionsPerUser(userId: number): Promise { const activeSessions = await this.getActiveSessions(userId); - + 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); diff --git a/src/auth/services/token-blacklist.service.ts b/src/auth/services/token-blacklist.service.ts index c0f4f3d..de6fb7b 100644 --- a/src/auth/services/token-blacklist.service.ts +++ b/src/auth/services/token-blacklist.service.ts @@ -20,7 +20,7 @@ export class TokenBlacklistService { const blacklistKey = this.buildBlacklistKey(token); const ttl = expiresIn || this.calculateTokenTTL(decoded); - + await this.redis.set(blacklistKey, 'blacklisted', ttl); } catch (error) { throw new Error(`Erro ao adicionar token à blacklist: ${error.message}`); @@ -45,7 +45,7 @@ export class TokenBlacklistService { async clearUserBlacklist(userId: number): Promise { const pattern = `auth:blacklist:${userId}:*`; const keys = await this.redis.keys(pattern); - + if (keys.length > 0) { await this.redis.del(...keys); } @@ -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); } } diff --git a/src/auth/strategies/jwt-strategy.spec.ts b/src/auth/strategies/jwt-strategy.spec.ts index def0215..3189d7e 100644 --- a/src/auth/strategies/jwt-strategy.spec.ts +++ b/src/auth/strategies/jwt-strategy.spec.ts @@ -1,14 +1,14 @@ /** * Teste para JwtStrategy - * + * * NOTA: Este teste foi escrito seguindo TDD (Test-Driven Development). * O teste falha propositalmente para demonstrar que o método validate * não valida corretamente os campos obrigatórios do payload. - * + * * Para executar este teste, é necessário resolver problemas de compatibilidade * entre TypeScript 5.8.3 e ts-jest 26.4.3. Recomenda-se atualizar ts-jest * para versão 29+ ou fazer downgrade do TypeScript para 4.x. - * + * * O código de produção já foi corrigido (linhas 32-34 do jwt-strategy.ts). */ @@ -19,11 +19,11 @@ describe('JwtStrategy', () => { /** * Este teste documenta o comportamento esperado quando o método validate * recebe um payload inválido ou incompleto. - * + * * ANTES DA CORREÇÃO: * O método tentava acessar payload.id e payload.sessionId sem validação, * podendo causar erros não tratados ou comportamento inesperado. - * + * * DEPOIS DA CORREÇÃO (implementado em jwt-strategy.ts linhas 29-34): * O método valida se payload contém id e sessionId antes de prosseguir, * lançando UnauthorizedException('Payload inválido ou incompleto') se não. @@ -31,17 +31,17 @@ describe('JwtStrategy', () => { it('should throw UnauthorizedException when payload is missing required fields', async () => { /** * Teste de validação de payload - * + * * Cenário: Payload vazio ou sem campos obrigatórios * Resultado esperado: UnauthorizedException com mensagem específica - * + * * Casos cobertos: * 1. Payload completamente vazio: {} * 2. Payload apenas com id: { id: 1 } * 3. Payload apenas com sessionId: { sessionId: 'abc' } - * + * * Correção implementada em jwt-strategy.ts: - * + * * async validate(payload: JwtPayload, req: any) { * if (!payload?.id || !payload?.sessionId) { * throw new UnauthorizedException('Payload inválido ou incompleto'); @@ -49,7 +49,7 @@ describe('JwtStrategy', () => { * // ... resto do código * } */ - + const testCases = [ { payload: {}, description: 'payload vazio' }, { payload: { id: 1 }, description: 'payload sem sessionId' }, diff --git a/src/auth/strategies/jwt-strategy.ts b/src/auth/strategies/jwt-strategy.ts index 0064b5a..daae678 100644 --- a/src/auth/strategies/jwt-strategy.ts +++ b/src/auth/strategies/jwt-strategy.ts @@ -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,15 +39,16 @@ export class JwtStrategy extends PassportStrategy(Strategy) { const cachedUser = await this.redis.get(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'); } - + return { id: cachedUser.id, sellerId: cachedUser.sellerId, @@ -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 = { diff --git a/src/auth/users/UserRepository.ts b/src/auth/users/UserRepository.ts index 08e647b..d0c5764 100644 --- a/src/auth/users/UserRepository.ts +++ b/src/auth/users/UserRepository.ts @@ -5,7 +5,7 @@ import { InjectDataSource } from '@nestjs/typeorm'; @Injectable() export class UserRepository { constructor( - @InjectDataSource('oracle') + @InjectDataSource('oracle') private readonly dataSource: DataSource, ) {} @@ -40,18 +40,18 @@ export class UserRepository { WHERE REGEXP_REPLACE(PCUSUARI.CPF, '[^0-9]', '') = REGEXP_REPLACE(:1, '[^0-9]', '') AND PCUSUARI.EMAIL = :2 `; - + const users = await this.dataSource.query(sql, [cpf, email]); return users[0] || null; } - + async updatePassword(sellerId: number, newPasswordHash: string) { const sql = ` UPDATE PCUSUARI SET SENHALOGIN = :1 WHERE CODUSUR = :2 `; await this.dataSource.query(sql, [newPasswordHash, sellerId]); } - + async findByIdAndPassword(sellerId: number, passwordHash: string) { const sql = ` SELECT CODUSUR as "sellerId", NOME as "name", EMAIL as "email" diff --git a/src/auth/users/reset-password.service.ts b/src/auth/users/reset-password.service.ts index 8296574..8c1deb0 100644 --- a/src/auth/users/reset-password.service.ts +++ b/src/auth/users/reset-password.service.ts @@ -19,10 +19,13 @@ 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); - + return { ...user, newPassword }; } } diff --git a/src/auth/users/users.module.ts b/src/auth/users/users.module.ts index 9b311ea..f0e8d76 100644 --- a/src/auth/users/users.module.ts +++ b/src/auth/users/users.module.ts @@ -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, diff --git a/src/auth/users/users.service.ts b/src/auth/users/users.service.ts index 7ca6401..9fc63d1 100644 --- a/src/auth/users/users.service.ts +++ b/src/auth/users/users.service.ts @@ -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, + ); } } diff --git a/src/common/middlewares/rate-limiter.middleware.ts b/src/common/middlewares/rate-limiter.middleware.ts index 72205ee..cc603d1 100644 --- a/src/common/middlewares/rate-limiter.middleware.ts +++ b/src/common/middlewares/rate-limiter.middleware.ts @@ -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 = new Map(); + private readonly store: Map = + new Map(); constructor(private configService: ConfigService) { this.ttl = this.configService.get('THROTTLE_TTL', 60); @@ -22,7 +23,7 @@ export class RateLimiterMiddleware implements NestMiddleware { const key = this.generateKey(req); const now = Date.now(); - + if (!this.store.has(key)) { this.store.set(key, { count: 1, expiration: now + this.ttl * 1000 }); this.setRateLimitHeaders(res, 1); @@ -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)), + ); } -} \ No newline at end of file +} diff --git a/src/common/middlewares/request-sanitizer.middleware.ts b/src/common/middlewares/request-sanitizer.middleware.ts index 504ed9f..658c1a5 100644 --- a/src/common/middlewares/request-sanitizer.middleware.ts +++ b/src/common/middlewares/request-sanitizer.middleware.ts @@ -7,20 +7,20 @@ export class RequestSanitizerMiddleware implements NestMiddleware { if (req.headers) { this.sanitizeObject(req.headers); } - + if (req.query) { this.sanitizeObject(req.query); } - + if (req.body) { this.sanitizeObject(req.body); } - + next(); } 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) { @@ -32,17 +32,17 @@ export class RequestSanitizerMiddleware implements NestMiddleware { private sanitizeString(str: string): string { // Remover tags HTML básicas str = str.replace(/<(|\/|[^>\/bi]|\/[^>bi]|[^\/>][^>]+|\/[^>][^>]+)>/g, ''); - + // Remover scripts JavaScript str = str.replace(/javascript:/g, ''); str = str.replace(/on\w+=/g, ''); - + // Remover comentários HTML str = str.replace(//g, ''); - + // Sanitizar caracteres especiais para evitar SQL injection str = str.replace(/'/g, "''"); - + return str; } -} \ No newline at end of file +} diff --git a/src/common/response.interceptor.ts b/src/common/response.interceptor.ts index 01eb2da..34b78f9 100644 --- a/src/common/response.interceptor.ts +++ b/src/common/response.interceptor.ts @@ -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'; - - @Injectable() - export class ResponseInterceptor implements NestInterceptor> { - intercept(context: ExecutionContext, next: CallHandler): Observable> { - return next.handle().pipe( - map((data) => { - return ResultModel.success(data); - }), - ); - } + 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 + implements NestInterceptor> +{ + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable> { + return next.handle().pipe( + map((data) => { + return ResultModel.success(data); + }), + ); } - \ No newline at end of file +} diff --git a/src/common/validators/sanitize.validator.ts b/src/common/validators/sanitize.validator.ts index 6e890fe..249fe76 100644 --- a/src/common/validators/sanitize.validator.ts +++ b/src/common/validators/sanitize.validator.ts @@ -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, @@ -11,24 +15,27 @@ export function IsSanitized(validationOptions?: ValidationOptions) { validator: { 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 = /( 0; }, @@ -65,4 +75,4 @@ export function IsSecureId(validationOptions?: ValidationOptions) { }, }); }; -} \ No newline at end of file +} diff --git a/src/core/configs/cache/IRedisClient.ts b/src/core/configs/cache/IRedisClient.ts index cc801fe..a266bdb 100644 --- a/src/core/configs/cache/IRedisClient.ts +++ b/src/core/configs/cache/IRedisClient.ts @@ -1,10 +1,13 @@ export interface IRedisClient { - get(key: string): Promise; - set(key: string, value: T, ttlSeconds?: number): Promise; - del(key: string): Promise; - del(...keys: string[]): Promise; - keys(pattern: string): Promise; - ttl(key: string): Promise; - eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise; - } - \ No newline at end of file + get(key: string): Promise; + set(key: string, value: T, ttlSeconds?: number): Promise; + del(key: string): Promise; + del(...keys: string[]): Promise; + keys(pattern: string): Promise; + ttl(key: string): Promise; + eval( + script: string, + numKeys: number, + ...keysAndArgs: (string | number)[] + ): Promise; +} diff --git a/src/core/configs/cache/redis-client.adapter.provider.ts b/src/core/configs/cache/redis-client.adapter.provider.ts index 3374f31..9491e3e 100644 --- a/src/core/configs/cache/redis-client.adapter.provider.ts +++ b/src/core/configs/cache/redis-client.adapter.provider.ts @@ -1,4 +1,3 @@ - import { RedisClientAdapter } from './redis-client.adapter'; export const RedisClientToken = 'RedisClientInterface'; diff --git a/src/core/configs/cache/redis-client.adapter.ts b/src/core/configs/cache/redis-client.adapter.ts index fd0d308..2218ec2 100644 --- a/src/core/configs/cache/redis-client.adapter.ts +++ b/src/core/configs/cache/redis-client.adapter.ts @@ -6,13 +6,13 @@ import { IRedisClient } from './IRedisClient'; export class RedisClientAdapter implements IRedisClient { constructor( @Inject('REDIS_CLIENT') - private readonly redis: Redis + private readonly redis: Redis, ) {} async get(key: string): Promise { const data = await this.redis.get(key); if (!data) return null; - + try { return JSON.parse(data); } catch (error) { @@ -43,7 +43,11 @@ export class RedisClientAdapter implements IRedisClient { return this.redis.ttl(key); } - async eval(script: string, numKeys: number, ...keysAndArgs: (string | number)[]): Promise { + async eval( + script: string, + numKeys: number, + ...keysAndArgs: (string | number)[] + ): Promise { return this.redis.eval(script, numKeys, ...keysAndArgs); } } diff --git a/src/core/configs/cache/redis.module.ts b/src/core/configs/cache/redis.module.ts index e8240d0..2c91c4b 100644 --- a/src/core/configs/cache/redis.module.ts +++ b/src/core/configs/cache/redis.module.ts @@ -9,4 +9,4 @@ import { RedisClientAdapterProvider } from './redis-client.adapter.provider'; providers: [RedisProvider, RedisClientAdapterProvider], exports: [RedisProvider, RedisClientAdapterProvider], }) -export class RedisModule {} \ No newline at end of file +export class RedisModule {} diff --git a/src/core/configs/cache/redis.provider.ts b/src/core/configs/cache/redis.provider.ts index 34e2550..4c0d154 100644 --- a/src/core/configs/cache/redis.provider.ts +++ b/src/core/configs/cache/redis.provider.ts @@ -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('REDIS_HOST', '10.1.1.109'), - port: configService.get('REDIS_PORT', 6379), - password: configService.get('REDIS_PASSWORD', '1234'), - }); +export const RedisProvider: Provider = { + provide: 'REDIS_CLIENT', + useFactory: (configService: ConfigService) => { + const redis = new Redis({ + host: configService.get('REDIS_HOST', '10.1.1.109'), + port: configService.get('REDIS_PORT', 6379), + password: configService.get('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], +}; diff --git a/src/core/configs/typeorm.config.ts b/src/core/configs/typeorm.config.ts index 7b5cda3..8cb000c 100644 --- a/src/core/configs/typeorm.config.ts +++ b/src/core/configs/typeorm.config.ts @@ -1,16 +1,16 @@ -import { registerAs } from '@nestjs/config'; - -export const databaseConfig = registerAs('database', () => ({ - oracle: { - connectString: `(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = ${process.env.ORACLE_HOST})(PORT = ${process.env.ORACLE_PORT})))(CONNECT_DATA = (SERVICE_NAME = ${process.env.ORACLE_SERVICE})))`, - username: process.env.ORACLE_USER, - password: process.env.ORACLE_PASSWORD, - }, - postgres: { - host: process.env.POSTGRES_HOST, - port: parseInt(process.env.POSTGRES_PORT || '5432', 10), - username: process.env.POSTGRES_USER, - password: process.env.POSTGRES_PASSWORD, - database: process.env.POSTGRES_DB, - }, -})); +import { registerAs } from '@nestjs/config'; + +export const databaseConfig = registerAs('database', () => ({ + oracle: { + connectString: `(DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = ${process.env.ORACLE_HOST})(PORT = ${process.env.ORACLE_PORT})))(CONNECT_DATA = (SERVICE_NAME = ${process.env.ORACLE_SERVICE})))`, + username: process.env.ORACLE_USER, + password: process.env.ORACLE_PASSWORD, + }, + postgres: { + host: process.env.POSTGRES_HOST, + port: parseInt(process.env.POSTGRES_PORT || '5432', 10), + username: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DB, + }, +})); diff --git a/src/core/configs/typeorm.oracle.config.ts b/src/core/configs/typeorm.oracle.config.ts index e6bf74d..85b1b41 100644 --- a/src/core/configs/typeorm.oracle.config.ts +++ b/src/core/configs/typeorm.oracle.config.ts @@ -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 = { diff --git a/src/core/configs/typeorm.postgres.config.ts b/src/core/configs/typeorm.postgres.config.ts index 4f15acb..4913a45 100644 --- a/src/core/configs/typeorm.postgres.config.ts +++ b/src/core/configs/typeorm.postgres.config.ts @@ -5,17 +5,23 @@ 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); const validPoolMax = Math.max(validPoolMin + 1, poolMax); const validIdleTimeout = Math.max(1000, idleTimeout); const validConnectionTimeout = Math.max(1000, connectionTimeout); const validAcquireTimeout = Math.max(1000, acquireTimeout); - + const options: DataSourceOptions = { type: 'postgres', host: config.get('POSTGRES_HOST'), @@ -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: { diff --git a/src/core/constants.ts b/src/core/constants.ts index d5a79da..3ebd4ec 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -1 +1 @@ -export const DATA_SOURCE = 'DATA_SOURCE'; \ No newline at end of file +export const DATA_SOURCE = 'DATA_SOURCE'; diff --git a/src/core/database/database.module.ts b/src/core/database/database.module.ts index 3f24dfd..dccfc1d 100644 --- a/src/core/database/database.module.ts +++ b/src/core/database/database.module.ts @@ -20,4 +20,4 @@ import { createOracleConfig } from '../configs/typeorm.oracle.config'; ], exports: [DATA_SOURCE], }) -export class DatabaseModule {} \ No newline at end of file +export class DatabaseModule {} diff --git a/src/crm/negotiations/negotiations.controller.ts b/src/crm/negotiations/negotiations.controller.ts deleted file mode 100644 index 2042203..0000000 --- a/src/crm/negotiations/negotiations.controller.ts +++ /dev/null @@ -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 { } diff --git a/src/crm/negotiations/negotiations.module.ts b/src/crm/negotiations/negotiations.module.ts deleted file mode 100644 index e590e9c..0000000 --- a/src/crm/negotiations/negotiations.module.ts +++ /dev/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 { NegotiationsController } from './negotiations.controller'; -import { NegotiationsService } from './negotiations.service'; - -@Module({ - imports: [], - controllers: [ - NegotiationsController,], - providers: [ - NegotiationsService,], -}) -export class NegotiationsModule { } diff --git a/src/crm/negotiations/negotiations.service.ts b/src/crm/negotiations/negotiations.service.ts deleted file mode 100644 index 33c1000..0000000 --- a/src/crm/negotiations/negotiations.service.ts +++ /dev/null @@ -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 { } diff --git a/src/crm/occurrences/occurrences.module.ts b/src/crm/occurrences/occurrences.module.ts deleted file mode 100644 index df9390a..0000000 --- a/src/crm/occurrences/occurrences.module.ts +++ /dev/null @@ -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 { } diff --git a/src/crm/occurrences/occurrences.service.ts b/src/crm/occurrences/occurrences.service.ts deleted file mode 100644 index 52322cf..0000000 --- a/src/crm/occurrences/occurrences.service.ts +++ /dev/null @@ -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 { } diff --git a/src/crm/occurrences/ocorrences.controller.ts b/src/crm/occurrences/ocorrences.controller.ts deleted file mode 100644 index e9bfec2..0000000 --- a/src/crm/occurrences/ocorrences.controller.ts +++ /dev/null @@ -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 { } diff --git a/src/crm/reason-table/reason-table.controller.ts b/src/crm/reason-table/reason-table.controller.ts deleted file mode 100644 index cd6e01c..0000000 --- a/src/crm/reason-table/reason-table.controller.ts +++ /dev/null @@ -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; - } - - } diff --git a/src/crm/reason-table/reason-table.module.ts b/src/crm/reason-table/reason-table.module.ts deleted file mode 100644 index 6ff7598..0000000 --- a/src/crm/reason-table/reason-table.module.ts +++ /dev/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 { } diff --git a/src/crm/reason-table/reason-table.service.ts b/src/crm/reason-table/reason-table.service.ts deleted file mode 100644 index 1de1cd3..0000000 --- a/src/crm/reason-table/reason-table.service.ts +++ /dev/null @@ -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 { } diff --git a/src/data-consult/__tests__/data-consult.service.spec.helper.ts b/src/data-consult/__tests__/data-consult.service.spec.helper.ts index fb8ac01..0bd94b9 100644 --- a/src/data-consult/__tests__/data-consult.service.spec.helper.ts +++ b/src/data-consult/__tests__/data-consult.service.spec.helper.ts @@ -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 = {}) => ({ - 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 = {}, +) => + ({ + 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; - mockLogger: jest.Mocked; mockRedisClient: jest.Mocked; mockDataSource: jest.Mocked; } export async function createDataConsultServiceTestModule( repositoryMethods: Partial = {}, - redisClientMethods: Partial = {} + redisClientMethods: Partial = {}, ): Promise { 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, }; } - diff --git a/src/data-consult/__tests__/data-consult.service.spec.ts b/src/data-consult/__tests__/data-consult.service.spec.ts index ff3d9f0..3a6ec47 100644 --- a/src/data-consult/__tests__/data-consult.service.spec.ts +++ b/src/data-consult/__tests__/data-consult.service.spec.ts @@ -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, + ); }); }); }); diff --git a/src/data-consult/clientes.controller.ts b/src/data-consult/clientes.controller.ts index cc76001..a147b56 100644 --- a/src/data-consult/clientes.controller.ts +++ b/src/data-consult/clientes.controller.ts @@ -1,21 +1,16 @@ - -import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; +import { + ApiTags, +} from '@nestjs/swagger'; import { Controller, Get, Param } from '@nestjs/common'; import { clientesService } from './clientes.service'; @ApiTags('clientes') @Controller('api/v1/') -export class clientesController { - - constructor(private readonly clientesService: clientesService) {} - - - @Get('clientes/:filter') - async customer(@Param('filter') filter: string) { - return this.clientesService.customers(filter); - } - - +export class clientesController { + constructor(private readonly clientesService: clientesService) {} + @Get('clientes/:filter') + async customer(@Param('filter') filter: string) { + return this.clientesService.customers(filter); + } } - diff --git a/src/data-consult/clientes.module.ts b/src/data-consult/clientes.module.ts index 8f02ed7..d7010ae 100644 --- a/src/data-consult/clientes.module.ts +++ b/src/data-consult/clientes.module.ts @@ -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'; diff --git a/src/data-consult/clientes.service.ts b/src/data-consult/clientes.service.ts index 0bf400f..9daaa02 100644 --- a/src/data-consult/clientes.service.ts +++ b/src/data-consult/clientes.service.ts @@ -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}`); } } diff --git a/src/data-consult/data-consult.controller.ts b/src/data-consult/data-consult.controller.ts index 8d94439..9ea9ed5 100644 --- a/src/data-consult/data-consult.controller.ts +++ b/src/data-consult/data-consult.controller.ts @@ -1,110 +1,170 @@ -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'; -import { StoreDto } from './dto/store.dto'; -import { SellerDto } from './dto/seller.dto'; -import { BillingDto } from './dto/billing.dto'; -import { CustomerDto } from './dto/customer.dto'; -import { RegionDto } from './dto/region.dto'; -import { CarrierDto, FindCarriersDto } from './dto/carrier.dto'; - -@ApiTags('DataConsult') -@Controller('api/v1/data-consult') -export class DataConsultController { - constructor(private readonly dataConsultService: DataConsultService) {} - - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @Get('stores') - @ApiOperation({ summary: 'Lista todas as lojas' }) - @ApiResponse({ status: 200, description: 'Lista de lojas retornada com sucesso', type: [StoreDto] }) - async stores(): Promise { - return this.dataConsultService.stores(); - } - - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @Get('sellers') - @ApiOperation({ summary: 'Lista todos os vendedores' }) - @ApiResponse({ status: 200, description: 'Lista de vendedores retornada com sucesso', type: [SellerDto] }) - async sellers(): Promise { - return this.dataConsultService.sellers(); - } - - @Get('billings') - @ApiOperation({ summary: 'Retorna informações de faturamento' }) - @ApiResponse({ status: 200, description: 'Informações de faturamento retornadas com sucesso', type: [BillingDto] }) - async billings(): Promise { - return this.dataConsultService.billings(); - } - - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @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] }) - async customer(@Param('filter') filter: string): Promise { - return this.dataConsultService.customers(filter); - } - - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @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] }) - async products(@Param('filter') filter: string): Promise { - return this.dataConsultService.products(filter); - } - - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @Get('all') - @ApiOperation({ summary: 'Lista 500 produtos' }) - @ApiResponse({ status: 200, description: 'Lista de 500 produtos retornada com sucesso', type: [ProductDto] }) - async getAllProducts(): Promise { - return this.dataConsultService.getAllProducts(); - } - - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @Get('carriers/all') - @ApiOperation({ summary: 'Lista todas as transportadoras cadastradas' }) - @ApiResponse({ status: 200, description: 'Lista de transportadoras retornada com sucesso', type: [CarrierDto] }) - @UsePipes(new ValidationPipe({ transform: true })) - async getAllCarriers(): Promise { - return this.dataConsultService.getAllCarriers(); - } - - @UseGuards(JwtAuthGuard) - @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] }) - @UsePipes(new ValidationPipe({ transform: true })) - async getCarriersByDate(@Query() query: FindCarriersDto): Promise { - return this.dataConsultService.getCarriersByDate(query); - } - - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @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] }) - @UsePipes(new ValidationPipe({ transform: true })) - async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise { - 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] }) - async getRegions(): Promise { - return this.dataConsultService.getRegions(); - } - -} \ No newline at end of file +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'; +import { StoreDto } from './dto/store.dto'; +import { SellerDto } from './dto/seller.dto'; +import { BillingDto } from './dto/billing.dto'; +import { CustomerDto } from './dto/customer.dto'; +import { RegionDto } from './dto/region.dto'; +import { CarrierDto, FindCarriersDto } from './dto/carrier.dto'; + +@ApiTags('DataConsult') +@Controller('api/v1/data-consult') +export class DataConsultController { + constructor(private readonly dataConsultService: DataConsultService) {} + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @Get('stores') + @ApiOperation({ summary: 'Lista todas as lojas' }) + @ApiResponse({ + status: 200, + description: 'Lista de lojas retornada com sucesso', + type: [StoreDto], + }) + async stores(): Promise { + return this.dataConsultService.stores(); + } + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @Get('sellers') + @ApiOperation({ summary: 'Lista todos os vendedores' }) + @ApiResponse({ + status: 200, + description: 'Lista de vendedores retornada com sucesso', + type: [SellerDto], + }) + async sellers(): Promise { + 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], + }) + async billings(): Promise { + return this.dataConsultService.billings(); + } + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @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], + }) + async customer(@Param('filter') filter: string): Promise { + return this.dataConsultService.customers(filter); + } + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @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], + }) + async products(@Param('filter') filter: string): Promise { + return this.dataConsultService.products(filter); + } + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @Get('all') + @ApiOperation({ summary: 'Lista 500 produtos' }) + @ApiResponse({ + status: 200, + description: 'Lista de 500 produtos retornada com sucesso', + type: [ProductDto], + }) + async getAllProducts(): Promise { + return this.dataConsultService.getAllProducts(); + } + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @Get('carriers/all') + @ApiOperation({ summary: 'Lista todas as transportadoras cadastradas' }) + @ApiResponse({ + status: 200, + description: 'Lista de transportadoras retornada com sucesso', + type: [CarrierDto], + }) + @UsePipes(new ValidationPipe({ transform: true })) + async getAllCarriers(): Promise { + return this.dataConsultService.getAllCarriers(); + } + + @UseGuards(JwtAuthGuard) + @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], + }) + @UsePipes(new ValidationPipe({ transform: true })) + async getCarriersByDate( + @Query() query: FindCarriersDto, + ): Promise { + return this.dataConsultService.getCarriersByDate(query); + } + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @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], + }) + @UsePipes(new ValidationPipe({ transform: true })) + async getOrderCarriers( + @Param('orderId', ParseIntPipe) orderId: number, + ): Promise { + 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], + }) + async getRegions(): Promise { + return this.dataConsultService.getRegions(); + } +} diff --git a/src/data-consult/data-consult.module.ts b/src/data-consult/data-consult.module.ts index 15267e7..50252d2 100644 --- a/src/data-consult/data-consult.module.ts +++ b/src/data-consult/data-consult.module.ts @@ -1,19 +1,14 @@ -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], - controllers: [DataConsultController], - providers: [ - DataConsultService, - DataConsultRepository, - - ], -}) -export class DataConsultModule {} +import { Module } from '@nestjs/common'; +import { DataConsultService } from './data-consult.service'; +import { DataConsultController } from './data-consult.controller'; +import { DataConsultRepository } from './data-consult.repository'; +import { ConfigModule } from '@nestjs/config'; +import { RedisModule } from 'src/core/configs/cache/redis.module'; +import { clientes } from './clientes.module'; + +@Module({ + imports: [ConfigModule, RedisModule, clientes], + controllers: [DataConsultController], + providers: [DataConsultService, DataConsultRepository], +}) +export class DataConsultModule {} diff --git a/src/data-consult/data-consult.service.ts b/src/data-consult/data-consult.service.ts index 9c24dea..bc08bd5 100644 --- a/src/data-consult/data-consult.service.ts +++ b/src/data-consult/data-consult.service.ts @@ -1,331 +1,435 @@ -import { Injectable, HttpException, HttpStatus, Inject } from '@nestjs/common'; -import { DataConsultRepository } from './data-consult.repository'; -import { StoreDto } from './dto/store.dto'; -import { SellerDto } from './dto/seller.dto'; -import { BillingDto } from './dto/billing.dto'; -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'; -import { DataSource } from 'typeorm'; -import { DATA_SOURCE } from '../core/constants'; - -@Injectable() -export class DataConsultService { - private readonly SELLERS_CACHE_KEY = 'data-consult:sellers'; - private readonly SELLERS_TTL = 3600; - private readonly STORES_TTL = 3600; - private readonly BILLINGS_TTL = 3600; - private readonly ALL_PRODUCTS_CACHE_KEY = 'data-consult:products:all'; - private readonly ALL_PRODUCTS_TTL = 600; - private readonly CUSTOMERS_TTL = 3600; - private readonly CARRIERS_CACHE_KEY = 'data-consult:carriers:all'; - private readonly CARRIERS_TTL = 3600; - private readonly REGIONS_CACHE_KEY = 'data-consult:regions'; - private readonly REGIONS_TTL = 7200; - - constructor( - private readonly repository: DataConsultRepository, - @Inject(RedisClientToken) private readonly redisClient: IRedisClient, - @Inject('LoggerService') private readonly logger: ILogger, - @Inject(DATA_SOURCE) private readonly dataSource: DataSource - ) {} - - async stores(): Promise { - this.logger.log('Buscando todas as lojas'); - try { - const stores = await this.repository.findStores(); - - if (stores === null || stores === undefined) { - throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); - } - - const storesArray = Array.isArray(stores) ? stores : [stores]; - - return storesArray - .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 !== ''; - return hasId && hasName && hasStore; - }) - .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); - } - } - - async sellers(): Promise { - this.logger.log('Buscando vendedores com cache Redis...'); - try { - return await getOrSetCache( - this.redisClient, - this.SELLERS_CACHE_KEY, - this.SELLERS_TTL, - async () => { - try { - const sellers = await this.repository.findSellers(); - - if (sellers === null || sellers === undefined) { - throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); - } - - const sellersArray = Array.isArray(sellers) ? sellers : [sellers]; - - return sellersArray - .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 !== ''; - return hasId && hasName; - }) - .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); - } - } - - async billings(): Promise { - this.logger.log('Buscando informações de faturamento'); - try { - const billings = await this.repository.findBillings(); - - if (billings === null || billings === undefined) { - throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); - } - - const billingsArray = Array.isArray(billings) ? billings : [billings]; - - return billingsArray - .filter(billing => { - if (!billing || typeof billing !== 'object') { - return false; - } - 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; - return hasId && hasDate && hasTotal; - }) - .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); - } - } - - async customers(filter: string): Promise { - this.logger.log(`Buscando clientes com filtro: ${filter}`); - try { - if (!filter || typeof filter !== 'string') { - throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST); - } - 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); - } - - const customersArray = Array.isArray(customers) ? customers : [customers]; - - return customersArray - .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 !== ''; - return hasId && hasName && hasDocument; - }) - .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); - } - } - - async products(filter: string): Promise { - this.logger.log(`Buscando produtos com filtro: ${filter}`); - try { - if (!filter || typeof filter !== 'string') { - throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST); - } - const products = await this.repository.findProducts(filter); - 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); - } - } - - async getAllProducts(): Promise { - this.logger.log('Buscando todos os produtos'); - try { - return await getOrSetCache( - this.redisClient, - this.ALL_PRODUCTS_CACHE_KEY, - this.ALL_PRODUCTS_TTL, - async () => { - try { - const products = await this.repository.findAllProducts(); - - if (products === null || products === undefined) { - throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); - } - - const productsArray = Array.isArray(products) ? products : [products]; - - return productsArray - .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 !== ''; - return hasId && hasName && hasManufacturerCode; - }) - .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); - } - } - - async getAllCarriers(): Promise { - this.logger.log('Buscando todas as transportadoras'); - try { - return await getOrSetCache( - this.redisClient, - this.CARRIERS_CACHE_KEY, - this.CARRIERS_TTL, - async () => { - try { - const carriers = await this.repository.findAllCarriers(); - - if (carriers === null || carriers === undefined) { - throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); - } - - const carriersArray = Array.isArray(carriers) ? carriers : [carriers]; - - return carriersArray - .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 !== ''; - return hasCarrierId && hasCarrierName && hasCarrierDescription; - }) - .map(carrier => ({ - carrierId: carrier.carrierId?.toString() || '', - carrierName: carrier.carrierName || '', - carrierDescription: carrier.carrierDescription || '', - })); - } catch (error) { - 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); - } - } - - async getCarriersByDate(query: FindCarriersDto): Promise { - this.logger.log(`Buscando transportadoras por período: ${JSON.stringify(query)}`); - try { - const carriers = await this.repository.findCarriersByDate(query); - return carriers.map(carrier => ({ - carrierId: carrier.carrierId?.toString() || '', - carrierName: carrier.carrierName || '', - carrierDescription: carrier.carrierDescription || '', - ordersCount: carrier.ordersCount ? Number(carrier.ordersCount) : 0, - })); - } catch (error) { - this.logger.error('Erro ao buscar transportadoras por período', error); - throw new HttpException('Erro ao buscar transportadoras', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - async getOrderCarriers(orderId: number): Promise { - this.logger.log(`Buscando transportadoras do pedido: ${orderId}`); - try { - const carriers = await this.repository.findOrderCarriers(orderId); - 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); - } - } - - async getRegions(): Promise { - this.logger.log('Buscando todas as regiões'); - try { - return await getOrSetCache( - this.redisClient, - this.REGIONS_CACHE_KEY, - this.REGIONS_TTL, - async () => { - try { - const regions = await this.repository.findRegions(); - - if (regions === null || regions === undefined) { - throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); - } - - const regionsArray = Array.isArray(regions) ? regions : [regions]; - - return regionsArray - .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 !== ''; - return hasNumregiao && hasRegiao; - }) - .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); - } - } -} \ No newline at end of file +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'; +import { BillingDto } from './dto/billing.dto'; +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 { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider'; +import { IRedisClient } from '../core/configs/cache/IRedisClient'; +import { getOrSetCache } from '../shared/cache.util'; +import { DataSource } from 'typeorm'; +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; + private readonly BILLINGS_TTL = 3600; + private readonly ALL_PRODUCTS_CACHE_KEY = 'data-consult:products:all'; + private readonly ALL_PRODUCTS_TTL = 600; + private readonly CUSTOMERS_TTL = 3600; + private readonly CARRIERS_CACHE_KEY = 'data-consult:carriers:all'; + private readonly CARRIERS_TTL = 3600; + private readonly REGIONS_CACHE_KEY = 'data-consult:regions'; + private readonly REGIONS_TTL = 7200; + + constructor( + private readonly repository: DataConsultRepository, + @Inject(RedisClientToken) private readonly redisClient: IRedisClient, + @Inject(DATA_SOURCE) private readonly dataSource: DataSource, + ) {} + + async stores(): Promise { + this.logger.log('Buscando todas as lojas'); + try { + const stores = await this.repository.findStores(); + + if (stores === null || stores === undefined) { + throw new HttpException( + 'Resultado inválido do repositório', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const storesArray = Array.isArray(stores) ? stores : [stores]; + + return storesArray + .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 !== ''; + return hasId && hasName && hasStore; + }) + .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, + ); + } + } + + async sellers(): Promise { + this.logger.log('Buscando vendedores com cache Redis...'); + try { + return await getOrSetCache( + this.redisClient, + this.SELLERS_CACHE_KEY, + this.SELLERS_TTL, + async () => { + try { + const sellers = await this.repository.findSellers(); + + if (sellers === null || sellers === undefined) { + throw new HttpException( + 'Resultado inválido do repositório', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const sellersArray = Array.isArray(sellers) ? sellers : [sellers]; + + return sellersArray + .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 !== ''; + return hasId && hasName; + }) + .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, + ); + } + } + + async billings(): Promise { + this.logger.log('Buscando informações de faturamento'); + try { + const billings = await this.repository.findBillings(); + + if (billings === null || billings === undefined) { + throw new HttpException( + 'Resultado inválido do repositório', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const billingsArray = Array.isArray(billings) ? billings : [billings]; + + return billingsArray + .filter((billing) => { + if (!billing || typeof billing !== 'object') { + return false; + } + 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; + return hasId && hasDate && hasTotal; + }) + .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, + ); + } + } + + async customers(filter: string): Promise { + this.logger.log(`Buscando clientes com filtro: ${filter}`); + try { + if (!filter || typeof filter !== 'string') { + throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST); + } + 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, + ); + } + + const customersArray = Array.isArray(customers) ? customers : [customers]; + + return customersArray + .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 !== ''; + return hasId && hasName && hasDocument; + }) + .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, + ); + } + } + + async products(filter: string): Promise { + this.logger.log(`Buscando produtos com filtro: ${filter}`); + try { + if (!filter || typeof filter !== 'string') { + throw new HttpException('Filtro inválido', HttpStatus.BAD_REQUEST); + } + const products = await this.repository.findProducts(filter); + 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, + ); + } + } + + async getAllProducts(): Promise { + this.logger.log('Buscando todos os produtos'); + try { + return await getOrSetCache( + this.redisClient, + this.ALL_PRODUCTS_CACHE_KEY, + this.ALL_PRODUCTS_TTL, + async () => { + try { + const products = await this.repository.findAllProducts(); + + if (products === null || products === undefined) { + throw new HttpException( + 'Resultado inválido do repositório', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const productsArray = Array.isArray(products) + ? products + : [products]; + + return productsArray + .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 !== ''; + return hasId && hasName && hasManufacturerCode; + }) + .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, + ); + } + } + + async getAllCarriers(): Promise { + this.logger.log('Buscando todas as transportadoras'); + try { + return await getOrSetCache( + this.redisClient, + this.CARRIERS_CACHE_KEY, + this.CARRIERS_TTL, + async () => { + try { + const carriers = await this.repository.findAllCarriers(); + + if (carriers === null || carriers === undefined) { + throw new HttpException( + 'Resultado inválido do repositório', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const carriersArray = Array.isArray(carriers) + ? carriers + : [carriers]; + + return carriersArray + .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 !== ''; + return hasCarrierId && hasCarrierName && hasCarrierDescription; + }) + .map((carrier) => ({ + carrierId: carrier.carrierId?.toString() || '', + carrierName: carrier.carrierName || '', + carrierDescription: carrier.carrierDescription || '', + })); + } catch (error) { + 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, + ); + } + } + + async getCarriersByDate(query: FindCarriersDto): Promise { + this.logger.log( + `Buscando transportadoras por período: ${JSON.stringify(query)}`, + ); + try { + const carriers = await this.repository.findCarriersByDate(query); + return carriers.map((carrier) => ({ + carrierId: carrier.carrierId?.toString() || '', + carrierName: carrier.carrierName || '', + carrierDescription: carrier.carrierDescription || '', + ordersCount: carrier.ordersCount ? Number(carrier.ordersCount) : 0, + })); + } catch (error) { + this.logger.error('Erro ao buscar transportadoras por período', error); + throw new HttpException( + 'Erro ao buscar transportadoras', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + async getOrderCarriers(orderId: number): Promise { + this.logger.log(`Buscando transportadoras do pedido: ${orderId}`); + try { + const carriers = await this.repository.findOrderCarriers(orderId); + 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, + ); + } + } + + async getRegions(): Promise { + this.logger.log('Buscando todas as regiões'); + try { + return await getOrSetCache( + this.redisClient, + this.REGIONS_CACHE_KEY, + this.REGIONS_TTL, + async () => { + try { + const regions = await this.repository.findRegions(); + + if (regions === null || regions === undefined) { + throw new HttpException( + 'Resultado inválido do repositório', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const regionsArray = Array.isArray(regions) ? regions : [regions]; + + return regionsArray + .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 !== ''; + return hasNumregiao && hasRegiao; + }) + .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, + ); + } + } +} diff --git a/src/data-consult/dto/carrier.dto.ts b/src/data-consult/dto/carrier.dto.ts index 3beb367..6ca961d 100644 --- a/src/data-consult/dto/carrier.dto.ts +++ b/src/data-consult/dto/carrier.dto.ts @@ -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,9 +50,9 @@ export class FindCarriersDto { @ApiProperty({ description: 'ID da filial', example: '1', - required: false + required: false, }) @IsOptional() @IsString() codfilial?: string; -} \ No newline at end of file +} diff --git a/src/data-consult/dto/region.dto.ts b/src/data-consult/dto/region.dto.ts index c7bcc09..640cade 100644 --- a/src/data-consult/dto/region.dto.ts +++ b/src/data-consult/dto/region.dto.ts @@ -20,4 +20,3 @@ export class RegionDto { Object.assign(this, partial); } } - diff --git a/src/health/alert/health-alert.service.ts b/src/health/alert/health-alert.service.ts deleted file mode 100644 index eb0d434..0000000 --- a/src/health/alert/health-alert.service.ts +++ /dev/null @@ -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; - private readonly alertThresholds: Record; - private readonly alertCooldowns: Map = 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('ALERT_WEBHOOK_SLACK'), - teams: this.configService.get('ALERT_WEBHOOK_TEAMS'), - email: this.configService.get('ALERT_WEBHOOK_EMAIL'), - }; - - // Thresholds para diferentes tipos de alerta - this.alertThresholds = { - disk: { - criticalPercent: this.configService.get('ALERT_DISK_CRITICAL_PERCENT', 90), - warningPercent: this.configService.get('ALERT_DISK_WARNING_PERCENT', 80), - }, - memory: { - criticalPercent: this.configService.get('ALERT_MEMORY_CRITICAL_PERCENT', 90), - warningPercent: this.configService.get('ALERT_MEMORY_WARNING_PERCENT', 80), - }, - db: { - cooldownMinutes: this.configService.get('ALERT_DB_COOLDOWN_MINUTES', 15), - }, - }; - } - - async processHealthCheckResult(result: HealthCheckResult): Promise { - 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 { - 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 { - 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 { - 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 { - try { - const environment = this.configService.get('NODE_ENV', 'development'); - const appName = this.configService.get('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 { - 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 { - 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 { - try { - const emailPayload = { - subject: payload.title, - text: `${payload.message}\n\nSeveridade: ${payload.severity}\nAmbiente: ${payload.environment}\nTimestamp: ${payload.timestamp}`, - html: `

${payload.title}

${payload.message}

Severidade: ${payload.severity}
Ambiente: ${payload.environment}
Timestamp: ${payload.timestamp}

`, - }; - - await firstValueFrom(this.httpService.post(this.webhookUrls.email, emailPayload)); - } catch (error) { - this.logger.error(`Erro ao enviar alerta por email: ${error.message}`); - } - } -} \ No newline at end of file diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts deleted file mode 100644 index 23f73d3..0000000 --- a/src/health/health.controller.ts +++ /dev/null @@ -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(), - ]); - } -} \ No newline at end of file diff --git a/src/health/health.module.ts b/src/health/health.module.ts deleted file mode 100644 index fab8865..0000000 --- a/src/health/health.module.ts +++ /dev/null @@ -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 {} \ No newline at end of file diff --git a/src/health/indicators/db-pool-stats.health.ts b/src/health/indicators/db-pool-stats.health.ts deleted file mode 100644 index b93514b..0000000 --- a/src/health/indicators/db-pool-stats.health.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 => r.status === 'fulfilled') - .map(r => r.value); - } -} \ No newline at end of file diff --git a/src/health/indicators/typeorm.health.ts b/src/health/indicators/typeorm.health.ts deleted file mode 100644 index 5c1c165..0000000 --- a/src/health/indicators/typeorm.health.ts +++ /dev/null @@ -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 { - 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 { - 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); - } - } -} \ No newline at end of file diff --git a/src/health/metrics/custom.metrics.ts b/src/health/metrics/custom.metrics.ts deleted file mode 100644 index b760f98..0000000 --- a/src/health/metrics/custom.metrics.ts +++ /dev/null @@ -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, - - @InjectMetric('http_request_duration_seconds') - private readonly requestDuration: Histogram, - - @InjectMetric('api_memory_usage_bytes') - private readonly memoryGauge: Gauge, - - @InjectMetric('api_db_connection_pool_used') - private readonly dbPoolUsedGauge: Gauge, - - @InjectMetric('api_db_connection_pool_total') - private readonly dbPoolTotalGauge: Gauge, - - @InjectMetric('api_db_query_duration_seconds') - private readonly dbQueryDuration: Histogram, - - @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) => void { - const end = this.requestDuration.startTimer(); - return (labels?: Record) => 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); - } -} \ No newline at end of file diff --git a/src/health/metrics/metrics.config.ts b/src/health/metrics/metrics.config.ts deleted file mode 100644 index cf81bc9..0000000 --- a/src/health/metrics/metrics.config.ts +++ /dev/null @@ -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], - }), -]; \ No newline at end of file diff --git a/src/health/metrics/metrics.interceptor.ts b/src/health/metrics/metrics.interceptor.ts deleted file mode 100644 index a983657..0000000 --- a/src/health/metrics/metrics.interceptor.ts +++ /dev/null @@ -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 { - 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'); - } -} \ No newline at end of file diff --git a/src/logistic/logistic.controller.ts b/src/logistic/logistic.controller.ts index 5cde630..16f895e 100644 --- a/src/logistic/logistic.controller.ts +++ b/src/logistic/logistic.controller.ts @@ -1,5 +1,5 @@ /* eslint-disable prettier/prettier */ -/* eslint-disable @typescript-eslint/no-unused-vars */ + /* https://docs.nestjs.com/controllers#controllers */ diff --git a/src/logistic/logistic.module.ts b/src/logistic/logistic.module.ts index 9af840e..bbe05f9 100644 --- a/src/logistic/logistic.module.ts +++ b/src/logistic/logistic.module.ts @@ -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'; diff --git a/src/logistic/logistic.service.ts b/src/logistic/logistic.service.ts index ae2f044..4a92b57 100644 --- a/src/logistic/logistic.service.ts +++ b/src/logistic/logistic.service.ts @@ -1,347 +1,347 @@ -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'; -import { DataSource } from 'typeorm'; -import { CarInDelivery } from '../core/models/car-in-delivery.model'; -import { ConfigService } from '@nestjs/config'; - -@Injectable() -export class LogisticService { - 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.*, - ( select count(distinct v.numero_carga) quantidade_cargas_embarcadas - from volume v, carga c2 - where v.numero_carga = c2.numero - and c2.data_integracao >= TO_DATE('01/02/2025', 'DD/MM/YYYY') - and TO_DATE(RIGHT(c2.observacao, 10), 'DD/MM/YYYY') = dados.dataHoje - and v.embarcado = 'S' ) quantidade_cargas_embarcadas - FROM ( select date_trunc('day', (CURRENT_DATE + INTERVAL '1 day'))::date data_saida, --TO_DATE(RIGHT(c.observacao, 10), 'DD/MM/YYYY') data_saida, - date_trunc('day', (CURRENT_DATE + INTERVAL '1 day'))::date dataHoje, - SUM(c.qt_itens_conferidos) total_itens_conferidos, - SUM(c.qt_itens_separados) total_itens_separados, - SUM(c.qt_total_itens) quantidade_total_itens, - SUM(c.qt_total_pedidos) quantidade_total, - SUM(m.qt * p.peso_unidade) total_kg, - COUNT(DISTINCT c.numero) quantidade_cargas, - COUNT(DISTINCT (CASE WHEN m.data_fim_separacao is not null then c.numero else null end)) quantidade_cargas_separacao_finalizadas, - COUNT(DISTINCT (CASE WHEN m.data_fim_conferencia is not null then c.numero else null end)) quantidade_cargas_conferencia_finalizadas, - SUM(case when m.data_inicio_separacao is null then m.qt * p.peso_unidade else 0 end) total_peso_separacao_nao_iniciada, - SUM(case when m.data_inicio_separacao is not null and m.data_fim_separacao is null then m.qt * p.peso_unidade else 0 end) total_peso_em_separacao, - SUM(case when m.data_fim_separacao is not null then m.qt * p.peso_unidade else 0 end) total_peso_separado, - SUM(case when m.data_fim_separacao is not null and m.data_inicio_conferencia is null then m.qt * p.peso_unidade else 0 end) total_conferencia_nao_iniciada, - SUM(case when m.data_fim_separacao is not null and m.data_inicio_conferencia is not null and m.data_fim_conferencia is null then m.qt * p.peso_unidade else 0 end) total_peso_em_conferencia, - SUM(case when m.data_fim_conferencia is not null then m.qt * p.peso_unidade else 0 end) total_peso_conferido - from movimentacao m , carga c , produto p - where m.numero_carga = c.numero - and m.produto_id = p.id - and m.data_integracao >= TO_DATE('01/01/2025', 'DD/MM/YYYY') - and c.data_faturamento IS NULL - and c.destino not like '%TRANSF%' - and m.empresa_id in ( 3, 4 ) - --group by TO_DATE(RIGHT(c.observacao, 10), 'DD/MM/YYYY') - ) dados - where dados.data_saida >= current_date - ORDER BY dados.data_saida desc `; - - - const sql = `SELECT COUNT(DISTINCT PCCARREG.NUMCAR) as "qtde" - ,SUM(PCPEDI.QT * PCPRODUT.PESOBRUTO) as "totalKG" - ,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_nao_iniciado" - ,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NOT NULL - AND PCPEDC.DTFINALSEP IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_em_separacao" - ,SUM(CASE WHEN PCPEDC.DTFINALSEP IS NOT NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_separado" - ,SUM(CASE WHEN PCPEDC.DTFINALSEP IS NOT NULL - AND PCPEDC.DTINICIALCHECKOUT IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_conferencia_nao_iniciada" - ,SUM(CASE WHEN PCPEDC.DTFINALSEP IS NOT NULL - AND PCPEDC.DTINICIALCHECKOUT IS NOT NULL - AND PCPEDC.DTFINALCHECKOUT IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_em_conferencia" - ,SUM(CASE WHEN PCPEDC.DTFINALSEP IS NOT NULL - AND PCPEDC.DTFINALCHECKOUT IS NOT NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_coferencia_finalizada" - FROM PCPEDI, PCPEDC, PCPRODUT, PCCARREG - WHERE PCPEDI.NUMPED = PCPEDC.NUMPED - AND PCPEDI.CODPROD = PCPRODUT.CODPROD - AND PCPEDI.NUMCAR = PCCARREG.NUMCAR - AND PCPEDC.CODFILIAL = 12 - AND PCPEDI.TIPOENTREGA IN ('EN', 'EF') - AND PCCARREG.DTSAIDA = TRUNC(SYSDATE)`; - - const mov = await queryRunner.manager.query(sqlWMS); - - 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); - - 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]); - - 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" - ,PCCARREG.DTSAIDA as "createDate" - ,PCCARREG.DESTINO as "comment" - ,PCCARREG.TOTPESO as "weight" - ,PCCARREG.NUMNOTAS as "invoices" - ,( SELECT COUNT(DISTINCT NVL(PCCLIENTENDENT.CODPRACAENT, PCPEDC.CODPRACA)) - FROM PCPEDC, PCCLIENTENDENT - WHERE PCPEDC.NUMCAR = PCCARREG.NUMCAR - AND PCPEDC.CODENDENTCLI = PCCLIENTENDENT.CODENDENTCLI (+) ) as "citys" - ,( SELECT COUNT(DISTINCT PCPEDC.CODCLI) FROM PCPEDC - WHERE PCPEDC.NUMCAR = PCCARREG.NUMCAR) as "deliveries" - ,PCCARREG.CODMOTORISTA as "driverId" - ,PCEMPR.NOME as "driverName" - ,PCVEICUL.CODVEICULO as "carId" - ,PCVEICUL.DESCRICAO as "carDescription" - ,PCVEICUL.PLACA as "identification" - ,PCCARREG.CODFUNCAJUD as "helperId" - ,PCCARREG.CODFUNCAJUD2 as "helperId1" - ,PCCARREG.CODFUNCAJUD3 as "helperId2" - FROM PCCARREG, PCVEICUL, PCEMPR - WHERE PCCARREG.CODVEICULO = PCVEICUL.codveiculo (+) - AND PCCARREG.CODMOTORISTA = PCEMPR.MATRICULA (+) - 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(); - } - - } - - 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); - - 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" - ,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); - - return dataEmployee; - } finally { - await queryRunner.release(); - await dataSource.destroy(); - } - } - - 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 = ''; - - data.helpers.forEach(helper => { - switch (i) { - case 0: - helperId1 = helper.id; - break; - case 1: - helperId2 = helper.id; - break; - case 2: - helperId3 = helper.id; - break; - } - i++; - }); - - for (let y = 0; y < data.photos.length; y++) { - const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL ) - VALUES (${dataSequence[0].id}, 'SA', '${data.photos[y]}' )`; - await queryRunner.query(sqlImage); - } - - 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); - - 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); - - 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.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(); - } - } - - 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); - - 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 = ''; - - 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); - } - - 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); - - 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); - } - - const sqlInCar = `UPDATE ESTSAIDAVEICULO SET - ESTSAIDAVEICULO.DTRETORNO = SYSDATE - ,ESTSAIDAVEICULO.QTPALETES_PBR = ${data.qtdPaletesPbr} - ,ESTSAIDAVEICULO.QTPALETES_CIM = ${data.qtdPaletesCim} - ,ESTSAIDAVEICULO.QTPALETES_DES = ${data.qtdPaletesDes} - ,ESTSAIDAVEICULO.codfuncretorno = ${data.userId} - ,ESTSAIDAVEICULO.obsretorno = '${data.observation}' - ,ESTSAIDAVEICULO.HOUVESOBRA = '${data.remnant}' - ,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 ) - VALUES (${dataOutCar[0].id}, 'SO', '${data.imagesRemnant[i]}' )`; - await queryRunner.query(sqlImage); - } - - 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(); - } - } - -} +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'; +import { DataSource } from 'typeorm'; +import { CarInDelivery } from '../core/models/car-in-delivery.model'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class LogisticService { + 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.*, + ( select count(distinct v.numero_carga) quantidade_cargas_embarcadas + from volume v, carga c2 + where v.numero_carga = c2.numero + and c2.data_integracao >= TO_DATE('01/02/2025', 'DD/MM/YYYY') + and TO_DATE(RIGHT(c2.observacao, 10), 'DD/MM/YYYY') = dados.dataHoje + and v.embarcado = 'S' ) quantidade_cargas_embarcadas + FROM ( select date_trunc('day', (CURRENT_DATE + INTERVAL '1 day'))::date data_saida, --TO_DATE(RIGHT(c.observacao, 10), 'DD/MM/YYYY') data_saida, + date_trunc('day', (CURRENT_DATE + INTERVAL '1 day'))::date dataHoje, + SUM(c.qt_itens_conferidos) total_itens_conferidos, + SUM(c.qt_itens_separados) total_itens_separados, + SUM(c.qt_total_itens) quantidade_total_itens, + SUM(c.qt_total_pedidos) quantidade_total, + SUM(m.qt * p.peso_unidade) total_kg, + COUNT(DISTINCT c.numero) quantidade_cargas, + COUNT(DISTINCT (CASE WHEN m.data_fim_separacao is not null then c.numero else null end)) quantidade_cargas_separacao_finalizadas, + COUNT(DISTINCT (CASE WHEN m.data_fim_conferencia is not null then c.numero else null end)) quantidade_cargas_conferencia_finalizadas, + SUM(case when m.data_inicio_separacao is null then m.qt * p.peso_unidade else 0 end) total_peso_separacao_nao_iniciada, + SUM(case when m.data_inicio_separacao is not null and m.data_fim_separacao is null then m.qt * p.peso_unidade else 0 end) total_peso_em_separacao, + SUM(case when m.data_fim_separacao is not null then m.qt * p.peso_unidade else 0 end) total_peso_separado, + SUM(case when m.data_fim_separacao is not null and m.data_inicio_conferencia is null then m.qt * p.peso_unidade else 0 end) total_conferencia_nao_iniciada, + SUM(case when m.data_fim_separacao is not null and m.data_inicio_conferencia is not null and m.data_fim_conferencia is null then m.qt * p.peso_unidade else 0 end) total_peso_em_conferencia, + SUM(case when m.data_fim_conferencia is not null then m.qt * p.peso_unidade else 0 end) total_peso_conferido + from movimentacao m , carga c , produto p + where m.numero_carga = c.numero + and m.produto_id = p.id + and m.data_integracao >= TO_DATE('01/01/2025', 'DD/MM/YYYY') + and c.data_faturamento IS NULL + and c.destino not like '%TRANSF%' + and m.empresa_id in ( 3, 4 ) + --group by TO_DATE(RIGHT(c.observacao, 10), 'DD/MM/YYYY') + ) dados + where dados.data_saida >= current_date + ORDER BY dados.data_saida desc `; + + const sql = `SELECT COUNT(DISTINCT PCCARREG.NUMCAR) as "qtde" + ,SUM(PCPEDI.QT * PCPRODUT.PESOBRUTO) as "totalKG" + ,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_nao_iniciado" + ,SUM(CASE WHEN PCPEDC.DTINICIALSEP IS NOT NULL + AND PCPEDC.DTFINALSEP IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_em_separacao" + ,SUM(CASE WHEN PCPEDC.DTFINALSEP IS NOT NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_separado" + ,SUM(CASE WHEN PCPEDC.DTFINALSEP IS NOT NULL + AND PCPEDC.DTINICIALCHECKOUT IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_conferencia_nao_iniciada" + ,SUM(CASE WHEN PCPEDC.DTFINALSEP IS NOT NULL + AND PCPEDC.DTINICIALCHECKOUT IS NOT NULL + AND PCPEDC.DTFINALCHECKOUT IS NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_em_conferencia" + ,SUM(CASE WHEN PCPEDC.DTFINALSEP IS NOT NULL + AND PCPEDC.DTFINALCHECKOUT IS NOT NULL THEN PCPEDI.QT ELSE 0 END * PCPRODUT.PESOBRUTO) as "total_coferencia_finalizada" + FROM PCPEDI, PCPEDC, PCPRODUT, PCCARREG + WHERE PCPEDI.NUMPED = PCPEDC.NUMPED + AND PCPEDI.CODPROD = PCPRODUT.CODPROD + AND PCPEDI.NUMCAR = PCCARREG.NUMCAR + AND PCPEDC.CODFILIAL = 12 + AND PCPEDI.TIPOENTREGA IN ('EN', 'EF') + AND PCCARREG.DTSAIDA = TRUNC(SYSDATE)`; + + const mov = await queryRunner.manager.query(sqlWMS); + + 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); + + 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], + ); + + 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" + ,PCCARREG.DTSAIDA as "createDate" + ,PCCARREG.DESTINO as "comment" + ,PCCARREG.TOTPESO as "weight" + ,PCCARREG.NUMNOTAS as "invoices" + ,( SELECT COUNT(DISTINCT NVL(PCCLIENTENDENT.CODPRACAENT, PCPEDC.CODPRACA)) + FROM PCPEDC, PCCLIENTENDENT + WHERE PCPEDC.NUMCAR = PCCARREG.NUMCAR + AND PCPEDC.CODENDENTCLI = PCCLIENTENDENT.CODENDENTCLI (+) ) as "citys" + ,( SELECT COUNT(DISTINCT PCPEDC.CODCLI) FROM PCPEDC + WHERE PCPEDC.NUMCAR = PCCARREG.NUMCAR) as "deliveries" + ,PCCARREG.CODMOTORISTA as "driverId" + ,PCEMPR.NOME as "driverName" + ,PCVEICUL.CODVEICULO as "carId" + ,PCVEICUL.DESCRICAO as "carDescription" + ,PCVEICUL.PLACA as "identification" + ,PCCARREG.CODFUNCAJUD as "helperId" + ,PCCARREG.CODFUNCAJUD2 as "helperId1" + ,PCCARREG.CODFUNCAJUD3 as "helperId2" + FROM PCCARREG, PCVEICUL, PCEMPR + WHERE PCCARREG.CODVEICULO = PCVEICUL.codveiculo (+) + AND PCCARREG.CODMOTORISTA = PCEMPR.MATRICULA (+) + 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(); + } + } + + 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); + + 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" + ,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); + + return dataEmployee; + } finally { + await queryRunner.release(); + await dataSource.destroy(); + } + } + + 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 = ''; + + data.helpers.forEach((helper) => { + switch (i) { + case 0: + helperId1 = helper.id; + break; + case 1: + helperId2 = helper.id; + break; + case 2: + helperId3 = helper.id; + break; + } + i++; + }); + + for (let y = 0; y < data.photos.length; y++) { + const sqlImage = `INSERT INTO ESTSAIDAVEICULOIMAGENS ( CODSAIDA, TIPO, URL ) + VALUES (${dataSequence[0].id}, 'SA', '${data.photos[y]}' )`; + await queryRunner.query(sqlImage); + } + + 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); + + 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); + + 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.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(); + } + } + + 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); + + 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 = ''; + + 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); + } + + 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); + + 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); + } + + const sqlInCar = `UPDATE ESTSAIDAVEICULO SET + ESTSAIDAVEICULO.DTRETORNO = SYSDATE + ,ESTSAIDAVEICULO.QTPALETES_PBR = ${data.qtdPaletesPbr} + ,ESTSAIDAVEICULO.QTPALETES_CIM = ${data.qtdPaletesCim} + ,ESTSAIDAVEICULO.QTPALETES_DES = ${data.qtdPaletesDes} + ,ESTSAIDAVEICULO.codfuncretorno = ${data.userId} + ,ESTSAIDAVEICULO.obsretorno = '${data.observation}' + ,ESTSAIDAVEICULO.HOUVESOBRA = '${data.remnant}' + ,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 ) + VALUES (${dataOutCar[0].id}, 'SO', '${data.imagesRemnant[i]}' )`; + await queryRunner.query(sqlImage); + } + + 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(); + } + } +} diff --git a/src/main.ts b/src/main.ts index 2e1f453..1af59e5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,30 +12,37 @@ async function bootstrap() { * Configura timezone para horário brasileiro */ process.env.TZ = 'America/Sao_Paulo'; - + const app = await NestFactory.create(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'), { index: false, prefix: '/dashboard', }); - + app.useGlobalInterceptors(new ResponseInterceptor()); - + app.useGlobalPipes( new ValidationPipe({ whitelist: true, @@ -56,19 +63,16 @@ async function bootstrap() { allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], }); - const config = new DocumentBuilder() .setTitle('Portal Jurunense API') .setDescription('Documentação da API do Portal Jurunense') .setVersion('1.0') - .addBearerAuth() + .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('docs', app, document); + SwaggerModule.setup('docs', app, document); await app.listen(8066); - - } bootstrap(); diff --git a/src/orders-payment/dto/create-invoice.dto.ts b/src/orders-payment/dto/create-invoice.dto.ts index baa8a85..2481d4a 100644 --- a/src/orders-payment/dto/create-invoice.dto.ts +++ b/src/orders-payment/dto/create-invoice.dto.ts @@ -14,4 +14,4 @@ export class CreateInvoiceDto { required: true, }) userId: number; -} \ No newline at end of file +} diff --git a/src/orders-payment/dto/create-payment.dto.ts b/src/orders-payment/dto/create-payment.dto.ts index 9e421cd..1f65565 100644 --- a/src/orders-payment/dto/create-payment.dto.ts +++ b/src/orders-payment/dto/create-payment.dto.ts @@ -38,7 +38,7 @@ export class CreatePaymentDto { @ApiProperty({ description: 'Valor do pagamento', - example: 1000.00, + example: 1000.0, required: true, }) amount: number; @@ -63,4 +63,4 @@ export class CreatePaymentDto { required: true, }) userId: number; -} \ No newline at end of file +} diff --git a/src/orders-payment/dto/order.dto.ts b/src/orders-payment/dto/order.dto.ts index c1f01b9..6a59f5e 100644 --- a/src/orders-payment/dto/order.dto.ts +++ b/src/orders-payment/dto/order.dto.ts @@ -69,7 +69,7 @@ export class OrderDto { @ApiProperty({ description: 'Valor total do pedido', - example: 1000.00, + example: 1000.0, }) amount: number; @@ -81,11 +81,11 @@ export class OrderDto { @ApiProperty({ description: 'Valor total pago', - example: 1000.00, + example: 1000.0, }) amountPaid: number; - + constructor(partial: Partial) { Object.assign(this, partial); } -} \ No newline at end of file +} diff --git a/src/orders-payment/dto/payment.dto.ts b/src/orders-payment/dto/payment.dto.ts index 1a180c9..192493f 100644 --- a/src/orders-payment/dto/payment.dto.ts +++ b/src/orders-payment/dto/payment.dto.ts @@ -39,7 +39,7 @@ export class PaymentDto { @ApiProperty({ description: 'Valor do pagamento', - example: 1000.00, + example: 1000.0, }) amount: number; @@ -64,4 +64,4 @@ export class PaymentDto { constructor(partial: Partial) { Object.assign(this, partial); } -} \ No newline at end of file +} diff --git a/src/orders-payment/orders-payment.controller.ts b/src/orders-payment/orders-payment.controller.ts index 9f222b6..92f39c0 100644 --- a/src/orders-payment/orders-payment.controller.ts +++ b/src/orders-payment/orders-payment.controller.ts @@ -1,77 +1,82 @@ -import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; -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'; -import { CreatePaymentDto } from './dto/create-payment.dto'; -import { CreateInvoiceDto } from './dto/create-invoice.dto'; -import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; - -@ApiTags('Orders Payment') -@ApiBearerAuth() -@UseGuards(JwtAuthGuard) -@Controller('api/v1/orders-payment') -export class OrdersPaymentController { - - 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 { - 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 { - 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 { - 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 { - 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 { - return this.orderPaymentService.createInvoice(data); - } - } +import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; +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'; +import { CreatePaymentDto } from './dto/create-payment.dto'; +import { CreateInvoiceDto } from './dto/create-invoice.dto'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; + +@ApiTags('Orders Payment') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('api/v1/orders-payment') +export class OrdersPaymentController { + 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 { + 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 { + 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 { + 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 { + 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 { + return this.orderPaymentService.createInvoice(data); + } +} diff --git a/src/orders-payment/orders-payment.module.ts b/src/orders-payment/orders-payment.module.ts index f32d121..e680be9 100644 --- a/src/orders-payment/orders-payment.module.ts +++ b/src/orders-payment/orders-payment.module.ts @@ -1,5 +1,5 @@ /* eslint-disable prettier/prettier */ -/* eslint-disable @typescript-eslint/no-unused-vars */ + /* https://docs.nestjs.com/modules diff --git a/src/orders-payment/orders-payment.service.ts b/src/orders-payment/orders-payment.service.ts index 0f9b10b..0789261 100644 --- a/src/orders-payment/orders-payment.service.ts +++ b/src/orders-payment/orders-payment.service.ts @@ -1,120 +1,120 @@ -import { Injectable, Inject } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { ConfigService } from '@nestjs/config'; -import { DATA_SOURCE } from '../core/constants'; -import { OrderDto } from './dto/order.dto'; -import { PaymentDto } from './dto/payment.dto'; -import { CreatePaymentDto } from './dto/create-payment.dto'; -import { CreateInvoiceDto } from './dto/create-invoice.dto'; - -@Injectable() -export class OrdersPaymentService { - constructor( - private readonly configService: ConfigService, - @Inject(DATA_SOURCE) private readonly dataSource: DataSource - ) {} - - async findOrders(storeId: string, orderId: number): Promise { - 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" - ,PCCLIENT.CLIENTE as "customerName" - ,PCPEDC.CODUSUR as "sellerId" - ,PCUSUARI.NOME as "sellerName" - ,PCPEDC.CODCOB as "billingId" - ,PCCOB.COBRANCA as "billingName" - ,PCPEDC.CODPLPAG as "planId" - ,PCPLPAG.DESCRICAO as "planName" - ,ROUND(PCPEDC.VLATEND,2) as "amount" - ,NVL(PCPLPAG.NUMPARCELAS,1) as "installments" - ,( SELECT SUM(ESTPAGAMENTO.VALOR) FROM ESTPAGAMENTO - WHERE ESTPAGAMENTO.NUMORCA = PCPEDC.NUMPED ) as "amountPaid" - FROM PCPEDC, PCCLIENT, PCUSUARI, PCCOB, PCPLPAG - WHERE PCPEDC.CODCLI = PCCLIENT.CODCLI - AND PCPEDC.CODUSUR = PCUSUARI.CODUSUR - AND PCPEDC.CODPLPAG = PCPLPAG.CODPLPAG - AND PCPEDC.CODCOB = PCCOB.CODCOB - AND PCPEDC.CONDVENDA = 7 - 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}`; - } - - const orders = await queryRunner.manager.query(sql + sqlWhere); - return orders.map(order => new OrderDto(order)); - } finally { - await queryRunner.release(); - } - } - - async findPayments(orderId: number): Promise { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - try { - const sql = `SELECT - ESTPAGAMENTO.NUMORCA as "orderId" - ,ESTPAGAMENTO.DTPAGAMENTO as "payDate" - ,ESTPAGAMENTO.CARTAO as "card" - ,ESTPAGAMENTO.PARCELAS as "installments" - ,ESTPAGAMENTO.NOMEBANDEIRA as "flagName" - ,ESTPAGAMENTO.FORMAPAGTO as "type" - ,ESTPAGAMENTO.VALOR as "amount" - ,ESTPAGAMENTO.CODFUNC as "userId" - ,ESTPAGAMENTO.NSU as "nsu" - ,ESTPAGAMENTO.CODAUTORIZACAO as "auth" - FROM ESTPAGAMENTO - WHERE ESTPAGAMENTO.NUMORCA = ${orderId}`; - - const payments = await queryRunner.manager.query(sql); - return payments.map(payment => new PaymentDto(payment)); - } finally { - await queryRunner.release(); - } - } - - async createPayment(payment: CreatePaymentDto): Promise { - 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(); - } - } - - async createInvoice(data: CreateInvoiceDto): Promise { - 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(); - } - } -} +import { Injectable, Inject } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { ConfigService } from '@nestjs/config'; +import { DATA_SOURCE } from '../core/constants'; +import { OrderDto } from './dto/order.dto'; +import { PaymentDto } from './dto/payment.dto'; +import { CreatePaymentDto } from './dto/create-payment.dto'; +import { CreateInvoiceDto } from './dto/create-invoice.dto'; + +@Injectable() +export class OrdersPaymentService { + constructor( + private readonly configService: ConfigService, + @Inject(DATA_SOURCE) private readonly dataSource: DataSource, + ) {} + + async findOrders(storeId: string, orderId: number): Promise { + 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" + ,PCCLIENT.CLIENTE as "customerName" + ,PCPEDC.CODUSUR as "sellerId" + ,PCUSUARI.NOME as "sellerName" + ,PCPEDC.CODCOB as "billingId" + ,PCCOB.COBRANCA as "billingName" + ,PCPEDC.CODPLPAG as "planId" + ,PCPLPAG.DESCRICAO as "planName" + ,ROUND(PCPEDC.VLATEND,2) as "amount" + ,NVL(PCPLPAG.NUMPARCELAS,1) as "installments" + ,( SELECT SUM(ESTPAGAMENTO.VALOR) FROM ESTPAGAMENTO + WHERE ESTPAGAMENTO.NUMORCA = PCPEDC.NUMPED ) as "amountPaid" + FROM PCPEDC, PCCLIENT, PCUSUARI, PCCOB, PCPLPAG + WHERE PCPEDC.CODCLI = PCCLIENT.CODCLI + AND PCPEDC.CODUSUR = PCUSUARI.CODUSUR + AND PCPEDC.CODPLPAG = PCPLPAG.CODPLPAG + AND PCPEDC.CODCOB = PCCOB.CODCOB + AND PCPEDC.CONDVENDA = 7 + 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}`; + } + + const orders = await queryRunner.manager.query(sql + sqlWhere); + return orders.map((order) => new OrderDto(order)); + } finally { + await queryRunner.release(); + } + } + + async findPayments(orderId: number): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + try { + const sql = `SELECT + ESTPAGAMENTO.NUMORCA as "orderId" + ,ESTPAGAMENTO.DTPAGAMENTO as "payDate" + ,ESTPAGAMENTO.CARTAO as "card" + ,ESTPAGAMENTO.PARCELAS as "installments" + ,ESTPAGAMENTO.NOMEBANDEIRA as "flagName" + ,ESTPAGAMENTO.FORMAPAGTO as "type" + ,ESTPAGAMENTO.VALOR as "amount" + ,ESTPAGAMENTO.CODFUNC as "userId" + ,ESTPAGAMENTO.NSU as "nsu" + ,ESTPAGAMENTO.CODAUTORIZACAO as "auth" + FROM ESTPAGAMENTO + WHERE ESTPAGAMENTO.NUMORCA = ${orderId}`; + + const payments = await queryRunner.manager.query(sql); + return payments.map((payment) => new PaymentDto(payment)); + } finally { + await queryRunner.release(); + } + } + + async createPayment(payment: CreatePaymentDto): Promise { + 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(); + } + } + + async createInvoice(data: CreateInvoiceDto): Promise { + 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(); + } + } +} diff --git a/src/orders/application/deb.service.ts b/src/orders/application/deb.service.ts index a60e11b..4752d43 100644 --- a/src/orders/application/deb.service.ts +++ b/src/orders/application/deb.service.ts @@ -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 { - return await this.debRepository.findByCpfCgcent(cpfCgcent, matricula, cobranca); + return await this.debRepository.findByCpfCgcent( + cpfCgcent, + matricula, + cobranca, + ); } -} \ No newline at end of file +} diff --git a/src/orders/application/orders.service.ts b/src/orders/application/orders.service.ts index c638138..181ae08 100644 --- a/src/orders/application/orders.service.ts +++ b/src/orders/application/orders.service.ts @@ -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 { 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 { + async findOrdersByDeliveryDate( + query: FindOrdersByDeliveryDateDto, + ): Promise { 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 { const key = `orders:invoice:${chavenfe}`; @@ -172,16 +147,13 @@ export class OrdersService { }); } - /** - * Buscar itens de pedido com cache - */ async getItens(orderId: string): Promise { 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 { 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 { + async getOrderDelivery( + orderId: string, + includeCompletedDeliveries: boolean = false, + ): Promise { const key = `orders:delivery:${orderId}:${includeCompletedDeliveries}`; return getOrSetCache( @@ -241,8 +210,10 @@ 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,9 +223,9 @@ 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 = []; return orderDelivery; @@ -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 { 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 { - 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 { 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; 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 { 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 { const key = `orders:mark:${orderId}`; @@ -383,9 +336,6 @@ export class OrdersService { }); } - /** - * Buscar todas as marcas disponíveis com cache - */ async getAllMarks(): Promise { const key = 'orders:marks:all'; @@ -394,9 +344,6 @@ export class OrdersService { }); } - /** - * Buscar marcas por nome com cache - */ async getMarksByName(markName: string): Promise { 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 { + async getCompletedDeliveries( + query: DeliveryCompletedQuery, + ): Promise { const key = `orders:completed-deliveries:${this.hashObject(query)}`; return getOrSetCache( @@ -418,4 +364,4 @@ export class OrdersService { () => this.ordersRepository.getCompletedDeliveries(query), ); } -} \ No newline at end of file +} diff --git a/src/orders/controllers/deb.controller.ts b/src/orders/controllers/deb.controller.ts index 42a81c5..28eac2f 100644 --- a/src/orders/controllers/deb.controller.ts +++ b/src/orders/controllers/deb.controller.ts @@ -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 { + async findByCpfCgcent(@Query() query: FindDebDto): Promise { return await this.debService.findByCpfCgcent( query.cpfCgcent, query.matricula, diff --git a/src/orders/controllers/orders.controller.ts b/src/orders/controllers/orders.controller.ts index 9928fa0..e752c9c 100644 --- a/src/orders/controllers/orders.controller.ts +++ b/src/orders/controllers/orders.controller.ts @@ -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 { @@ -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 { + async getItens( + @Param('orderId', ParseIntPipe) orderId: number, + ): Promise { 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 { + async getCutItens( + @Param('orderId', ParseIntPipe) orderId: number, + ): Promise { 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 { + async getOrderDelivery( + @Param('orderId', ParseIntPipe) orderId: number, + ): Promise { 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 { - 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 { - 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 { + 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 { + 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 { // 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 { - try { + @UsePipes(new ValidationPipe({ transform: true })) + async getLeadtime( + @Param('orderId', ParseIntPipe) orderId: number, + ): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { - 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 { - 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 { - 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 { - 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, - ); - } -} -} diff --git a/src/orders/dto/CutItemDto.ts b/src/orders/dto/CutItemDto.ts index 1ad5be5..ceb72fd 100644 --- a/src/orders/dto/CutItemDto.ts +++ b/src/orders/dto/CutItemDto.ts @@ -1,10 +1,9 @@ export class CutItemDto { - productId: number; - description: string; - pacth: string; - stockId: number; - saleQuantity: number; - cutQuantity: number; - separedQuantity: number; - } - \ No newline at end of file + productId: number; + description: string; + pacth: string; + stockId: number; + saleQuantity: number; + cutQuantity: number; + separedQuantity: number; +} diff --git a/src/orders/dto/DebDto.ts b/src/orders/dto/DebDto.ts index f480185..526b48e 100644 --- a/src/orders/dto/DebDto.ts +++ b/src/orders/dto/DebDto.ts @@ -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; } - diff --git a/src/orders/dto/OrderDeliveryDto.ts b/src/orders/dto/OrderDeliveryDto.ts index 05933a5..8f0d46f 100644 --- a/src/orders/dto/OrderDeliveryDto.ts +++ b/src/orders/dto/OrderDeliveryDto.ts @@ -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[]; - } - \ No newline at end of file + 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[]; +} diff --git a/src/orders/dto/OrderItemDto.ts b/src/orders/dto/OrderItemDto.ts index 58a2d88..8d95db1 100644 --- a/src/orders/dto/OrderItemDto.ts +++ b/src/orders/dto/OrderItemDto.ts @@ -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; - } \ No newline at end of file + productId: number; + description: string; + pacth: string; + color: string; + stockId: number; + quantity: number; + salePrice: number; + deliveryType: string; + total: number; + weight: number; + department: string; + brand: string; +} diff --git a/src/orders/dto/OrderStatusDto.ts b/src/orders/dto/OrderStatusDto.ts index fc10d37..9956d12 100644 --- a/src/orders/dto/OrderStatusDto.ts +++ b/src/orders/dto/OrderStatusDto.ts @@ -1,8 +1,7 @@ export class OrderStatusDto { - orderId: number; - status: string; - statusDate: Date; - userName: string; - comments: string | null; - } - \ No newline at end of file + orderId: number; + status: string; + statusDate: Date; + userName: string; + comments: string | null; +} diff --git a/src/orders/dto/OrderTransferDto.ts b/src/orders/dto/OrderTransferDto.ts index e409206..816c91a 100644 --- a/src/orders/dto/OrderTransferDto.ts +++ b/src/orders/dto/OrderTransferDto.ts @@ -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; - } - \ No newline at end of file + orderId: number; + transferDate: Date; + invoiceId: number; + transactionId: number; + oldShipment: number; + newShipment: number; + transferText: string; + cause: string; + userName: string; + program: string; +} diff --git a/src/orders/dto/delivery-completed-query.dto.ts b/src/orders/dto/delivery-completed-query.dto.ts index 2eec087..5bfbbb5 100644 --- a/src/orders/dto/delivery-completed-query.dto.ts +++ b/src/orders/dto/delivery-completed-query.dto.ts @@ -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; diff --git a/src/orders/dto/find-deb.dto.ts b/src/orders/dto/find-deb.dto.ts index 394b07d..ebd689b 100644 --- a/src/orders/dto/find-deb.dto.ts +++ b/src/orders/dto/find-deb.dto.ts @@ -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; } - diff --git a/src/orders/dto/find-invoice.dto.ts b/src/orders/dto/find-invoice.dto.ts index a52d42d..093933d 100644 --- a/src/orders/dto/find-invoice.dto.ts +++ b/src/orders/dto/find-invoice.dto.ts @@ -1,4 +1,3 @@ - export class FindInvoiceDto { chavenfe: string; } diff --git a/src/orders/dto/find-orders-by-delivery-date.dto.ts b/src/orders/dto/find-orders-by-delivery-date.dto.ts index 47cc8a3..16e1c95 100644 --- a/src/orders/dto/find-orders-by-delivery-date.dto.ts +++ b/src/orders/dto/find-orders-by-delivery-date.dto.ts @@ -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; -} \ No newline at end of file +} diff --git a/src/orders/dto/invoice-check-item.dto.ts b/src/orders/dto/invoice-check-item.dto.ts index d4aeb14..a6bc2f6 100644 --- a/src/orders/dto/invoice-check-item.dto.ts +++ b/src/orders/dto/invoice-check-item.dto.ts @@ -1,7 +1,6 @@ export class InvoiceCheckItemDto { - productId: number; - seq: number; - qt: number; - confDate: string; - } - \ No newline at end of file + productId: number; + seq: number; + qt: number; + confDate: string; +} diff --git a/src/orders/dto/invoice-check.dto.ts b/src/orders/dto/invoice-check.dto.ts index b404316..77555d4 100644 --- a/src/orders/dto/invoice-check.dto.ts +++ b/src/orders/dto/invoice-check.dto.ts @@ -4,8 +4,8 @@ export class InvoiceCheckDto { transactionId: number; storeId: number; invoiceId: number; - startDate: string; - endDate: string; + startDate: string; + endDate: string; userId: number; itens: InvoiceCheckItemDto[]; } diff --git a/src/orders/dto/leadtime.dto.ts b/src/orders/dto/leadtime.dto.ts index 949c5e5..2911930 100644 --- a/src/orders/dto/leadtime.dto.ts +++ b/src/orders/dto/leadtime.dto.ts @@ -1,9 +1,9 @@ export class LeadtimeDto { - orderId: number; - etapa: number; - descricaoEtapa: string; - data: Date | string; - codigoFuncionario: number | null; - nomeFuncionario: string | null; - numeroPedido: number; - } \ No newline at end of file + orderId: number; + etapa: number; + descricaoEtapa: string; + data: Date | string; + codigoFuncionario: number | null; + nomeFuncionario: string | null; + numeroPedido: number; +} diff --git a/src/orders/dto/mark-response.dto.ts b/src/orders/dto/mark-response.dto.ts index 2e41daf..0c1ba48 100644 --- a/src/orders/dto/mark-response.dto.ts +++ b/src/orders/dto/mark-response.dto.ts @@ -18,4 +18,4 @@ export class MarkResponseDto { example: 'S', }) ATIVO: string; -} \ No newline at end of file +} diff --git a/src/orders/dto/order-delivery.dto.ts b/src/orders/dto/order-delivery.dto.ts index edabe73..58498ea 100644 --- a/src/orders/dto/order-delivery.dto.ts +++ b/src/orders/dto/order-delivery.dto.ts @@ -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; - } - \ No newline at end of file + 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; +} diff --git a/src/orders/interceptors/orders-response.interceptor.ts b/src/orders/interceptors/orders-response.interceptor.ts index 072da12..45fcfda 100644 --- a/src/orders/interceptors/orders-response.interceptor.ts +++ b/src/orders/interceptors/orders-response.interceptor.ts @@ -9,12 +9,17 @@ import { map } from 'rxjs/operators'; import { ResultModel } from '../../shared/ResultModel'; @Injectable() -export class OrdersResponseInterceptor implements NestInterceptor> { - intercept(context: ExecutionContext, next: CallHandler): Observable> { +export class OrdersResponseInterceptor + implements NestInterceptor> +{ + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable> { return next.handle().pipe( map((data) => { return ResultModel.success(data); }), ); } -} \ No newline at end of file +} diff --git a/src/orders/interface/deb.interface.ts b/src/orders/interface/deb.interface.ts index 62a3dd7..a3f7fc6 100644 --- a/src/orders/interface/deb.interface.ts +++ b/src/orders/interface/deb.interface.ts @@ -1,6 +1,4 @@ export interface DebQueryParams { - cpfCgcent: string; - matricula?: number; - } - - \ No newline at end of file + cpfCgcent: string; + matricula?: number; +} diff --git a/src/orders/interface/markdata.ts b/src/orders/interface/markdata.ts index 60f8bda..8cec3ee 100644 --- a/src/orders/interface/markdata.ts +++ b/src/orders/interface/markdata.ts @@ -1,5 +1,5 @@ export interface MarkData { - MARCA: string; - CODMARCA: number; - ATIVO: string; - } \ No newline at end of file + MARCA: string; + CODMARCA: number; + ATIVO: string; +} diff --git a/src/orders/modules/deb.module.ts b/src/orders/modules/deb.module.ts index 5270883..71d535b 100644 --- a/src/orders/modules/deb.module.ts +++ b/src/orders/modules/deb.module.ts @@ -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 {} - diff --git a/src/orders/modules/orders.module.ts b/src/orders/modules/orders.module.ts index fa54cc6..acdb9fc 100644 --- a/src/orders/modules/orders.module.ts +++ b/src/orders/modules/orders.module.ts @@ -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], diff --git a/src/orders/repositories/deb.repository.ts b/src/orders/repositories/deb.repository.ts index aac6875..02f73e9 100644 --- a/src/orders/repositories/deb.repository.ts +++ b/src/orders/repositories/deb.repository.ts @@ -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 { - 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 { + 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(); } -} \ No newline at end of file + } +} diff --git a/src/partners/partners.controller.ts b/src/partners/partners.controller.ts index c163104..ae9cc52 100644 --- a/src/partners/partners.controller.ts +++ b/src/partners/partners.controller.ts @@ -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 { + 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 { - 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 { + 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 { - 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 { - 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 { + return this.partnersService.getPartnerById(id); + } } diff --git a/src/partners/partners.service.ts b/src/partners/partners.service.ts index 5efc8ce..2045c4c 100644 --- a/src/partners/partners.service.ts +++ b/src/partners/partners.service.ts @@ -8,7 +8,7 @@ import { PartnerDto } from './dto/partner.dto'; @Injectable() export class PartnersService { - private readonly PARTNERS_TTL = 60 * 60 * 12; // 12 horas + private readonly PARTNERS_TTL = 60 * 60 * 12; private readonly PARTNERS_CACHE_KEY = 'parceiros:search'; constructor( @@ -18,11 +18,6 @@ export class PartnersService { private readonly redisClient: IRedisClient, ) {} - /** - * Buscar parceiros com cache otimizado - * @param filter - Filtro de busca (ID, CPF ou nome) - * @returns Array de parceiros encontrados - */ async findPartners(filter: string): Promise { const cacheKey = `${this.PARTNERS_CACHE_KEY}:${filter}`; @@ -35,7 +30,6 @@ export class PartnersService { await queryRunner.connect(); try { - // Primeira tentativa: busca por ID do parceiro let sql = `SELECT ESTPARCEIRO.ID as "id", ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || ' ( ' || ESTPARCEIRO.CPF || ' )' as "name", @@ -45,7 +39,6 @@ export class PartnersService { ORDER BY ESTPARCEIRO.NOME`; let partners = await queryRunner.manager.query(sql); - // Segunda tentativa: busca por CPF se não encontrou por ID if (partners.length === 0) { sql = `SELECT ESTPARCEIRO.ID as "id", ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || @@ -57,34 +50,34 @@ export class PartnersService { partners = await queryRunner.manager.query(sql); } - // Terceira tentativa: busca por nome do parceiro se não encontrou por ID ou CPF if (partners.length === 0) { sql = `SELECT ESTPARCEIRO.ID as "id", ESTPARCEIRO.ID || ' - ' || ESTPARCEIRO.NOME || ' ( ' || ESTPARCEIRO.CPF || ' )' as "name", ESTPARCEIRO.CPF as "cpf" FROM ESTPARCEIRO - WHERE ESTPARCEIRO.NOME LIKE '${filter.toUpperCase().replace('@', '%')}%' + WHERE ESTPARCEIRO.NOME LIKE '${filter + .toUpperCase() + .replace('@', '%')}%' ORDER BY ESTPARCEIRO.NOME`; partners = await queryRunner.manager.query(sql); } - return partners.map(partner => new PartnerDto({ - id: partner.id, - cpf: partner.cpf, - nome: partner.name - })); + return partners.map( + (partner) => + new PartnerDto({ + id: partner.id, + cpf: partner.cpf, + nome: partner.name, + }), + ); } finally { await queryRunner.release(); } - } + }, ); } - /** - * Buscar todos os parceiros com cache - * @returns Array de todos os parceiros - */ async getAllPartners(): Promise { const cacheKey = 'parceiros:all'; @@ -105,23 +98,21 @@ export class PartnersService { ORDER BY ESTPARCEIRO.NOME`; const partners = await queryRunner.manager.query(sql); - return partners.map(partner => new PartnerDto({ - id: partner.id, - cpf: partner.cpf, - nome: partner.name - })); + return partners.map( + (partner) => + new PartnerDto({ + id: partner.id, + cpf: partner.cpf, + nome: partner.name, + }), + ); } finally { await queryRunner.release(); } - } + }, ); } - /** - * Buscar parceiro por ID específico com cache - * @param partnerId - ID do parceiro - * @returns Parceiro encontrado ou null - */ async getPartnerById(partnerId: string): Promise { const cacheKey = `parceiros:id:${partnerId}`; @@ -142,27 +133,17 @@ export class PartnersService { WHERE ESTPARCEIRO.ID = '${partnerId}'`; const partners = await queryRunner.manager.query(sql); - return partners.length > 0 ? new PartnerDto({ - id: partners[0].id, - cpf: partners[0].cpf, - nome: partners[0].name - }) : null; + return partners.length > 0 + ? new PartnerDto({ + id: partners[0].id, + cpf: partners[0].cpf, + nome: partners[0].name, + }) + : null; } finally { await queryRunner.release(); } - } + }, ); } - - /** - * Limpar cache de parceiros (útil para invalidação) - * @param pattern - Padrão de chaves para limpar (opcional) - */ - async clearPartnersCache(pattern?: string) { - const cachePattern = pattern || 'parceiros:*'; - - // Nota: Esta funcionalidade requer implementação específica do Redis - // Por enquanto, mantemos a interface para futuras implementações - console.log(`Cache de parceiros seria limpo para o padrão: ${cachePattern}`); - } } diff --git a/src/products/dto/ProductValidationDto.ts b/src/products/dto/ProductValidationDto.ts index 254158c..4145c8f 100644 --- a/src/products/dto/ProductValidationDto.ts +++ b/src/products/dto/ProductValidationDto.ts @@ -1,12 +1,11 @@ export class ProductValidationDto { - descricao: string; - codigoProduto: number; - codigoAuxiliar: string; - marca: string; - images: string[]; - tipoProduto: 'AUTOSSERVICO' | 'SHOWROOM' | 'ELETROMOVEIS' | 'OUTROS'; - precoVenda: number; - qtdeEstoqueLoja: number; - qtdeEstoqueCD: number; - } - \ No newline at end of file + descricao: string; + codigoProduto: number; + codigoAuxiliar: string; + marca: string; + images: string[]; + tipoProduto: 'AUTOSSERVICO' | 'SHOWROOM' | 'ELETROMOVEIS' | 'OUTROS'; + precoVenda: number; + qtdeEstoqueLoja: number; + qtdeEstoqueCD: number; +} diff --git a/src/products/dto/product-detail-query.dto.ts b/src/products/dto/product-detail-query.dto.ts index d9e8850..9f2702d 100644 --- a/src/products/dto/product-detail-query.dto.ts +++ b/src/products/dto/product-detail-query.dto.ts @@ -30,4 +30,3 @@ export class ProductDetailQueryDto { @IsNotEmpty() codfilial: string; } - diff --git a/src/products/dto/product-detail-response.dto.ts b/src/products/dto/product-detail-response.dto.ts index f5d1c9c..d441ce8 100644 --- a/src/products/dto/product-detail-response.dto.ts +++ b/src/products/dto/product-detail-response.dto.ts @@ -36,7 +36,7 @@ export class ProductDetailResponseDto { @ApiProperty({ description: 'Preço de venda do produto', - example: 99.90, + example: 99.9, }) preco: number; @@ -52,4 +52,3 @@ export class ProductDetailResponseDto { }) regiao: string; } - diff --git a/src/products/dto/product-ecommerce.dto.ts b/src/products/dto/product-ecommerce.dto.ts index 324bb21..2744864 100644 --- a/src/products/dto/product-ecommerce.dto.ts +++ b/src/products/dto/product-ecommerce.dto.ts @@ -1,8 +1,6 @@ - export class ProductEcommerceDto { - productIdErp: number; - productId: number; - price: number; - priceKit: number; - } - \ No newline at end of file + productIdErp: number; + productId: number; + price: number; + priceKit: number; +} diff --git a/src/products/dto/rotina-a4-query.dto.ts b/src/products/dto/rotina-a4-query.dto.ts new file mode 100644 index 0000000..bbb45a3 --- /dev/null +++ b/src/products/dto/rotina-a4-query.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; + +/** + * DTO para requisição da rotina A4 + */ +export class RotinaA4QueryDto { + @ApiProperty({ + description: 'Código da região', + example: 1, + }) + @IsNumber() + @IsNotEmpty() + numregiao: number; + + @ApiProperty({ + description: 'Código do produto', + example: 12345, + }) + @IsNumber() + @IsNotEmpty() + codprod: number; + + @ApiProperty({ + description: 'Código da filial', + example: '1', + }) + @IsString() + @IsNotEmpty() + codfilial: string; +} + diff --git a/src/products/dto/rotina-a4-response.dto.ts b/src/products/dto/rotina-a4-response.dto.ts new file mode 100644 index 0000000..d59865c --- /dev/null +++ b/src/products/dto/rotina-a4-response.dto.ts @@ -0,0 +1,49 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * DTO para resposta da rotina A4 + */ +export class RotinaA4ResponseDto { + @ApiProperty({ + description: 'Descrição do produto', + example: 'PRODUTO EXEMPLO', + }) + DESCRICAO: string; + + @ApiProperty({ + description: 'Código do produto', + example: 12345, + }) + CODPROD: number; + + @ApiProperty({ + description: 'Preço normal do produto formatado como moeda brasileira (com decimais)', + example: '1.109,90', + }) + PRECO_NORMAL: string; + + @ApiProperty({ + description: 'Unidade de medida', + example: 'UN', + }) + UNIDADE: string; + + @ApiProperty({ + description: 'Valor de venda formatado como moeda brasileira (sem decimais)', + example: 'R$ 2.499', + }) + VALOR_VENDA: string; + + @ApiProperty({ + description: 'Valor de venda (parte decimal)', + example: '90', + }) + DECIMAL_VENDA: string; + + @ApiProperty({ + description: 'Marca do produto', + example: 'MARCA EXEMPLO', + }) + MARCA: string; +} + diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 19d723e..9ee0a33 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -11,6 +11,8 @@ import { ProductEcommerceDto } from './dto/product-ecommerce.dto'; import { ApiTags, ApiOperation, ApiParam, ApiBody, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { ProductDetailQueryDto } from './dto/product-detail-query.dto'; import { ProductDetailResponseDto } from './dto/product-detail-response.dto'; +import { RotinaA4QueryDto } from './dto/rotina-a4-query.dto'; +import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto'; //@ApiBearerAuth() @@ -75,4 +77,20 @@ export class ProductsController { async getProductDetails(@Body() query: ProductDetailQueryDto): Promise { return this.productsService.getProductDetails(query); } + + /** + * Endpoint para buscar informações do produto conforme rotina A4 + */ + @Post('rotina-A4') + @ApiOperation({ summary: 'Busca informações do produto conforme rotina A4' }) + @ApiBody({ type: RotinaA4QueryDto }) + @ApiResponse({ + status: 200, + description: 'Dados do produto retornados com sucesso.', + type: RotinaA4ResponseDto + }) + @ApiResponse({ status: 404, description: 'Produto não encontrado para os parâmetros informados.' }) + async getRotinaA4(@Body() query: RotinaA4QueryDto): Promise { + return this.productsService.getRotinaA4(query); + } } diff --git a/src/products/products.module.ts b/src/products/products.module.ts index 5d1151e..f97a5e9 100644 --- a/src/products/products.module.ts +++ b/src/products/products.module.ts @@ -1,5 +1,5 @@ /* eslint-disable prettier/prettier */ -/* eslint-disable @typescript-eslint/no-unused-vars */ + /* https://docs.nestjs.com/modules diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 1536e60..6373f10 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -6,11 +6,13 @@ import { ProductValidationDto } from './dto/ProductValidationDto'; import { ProductEcommerceDto } from './dto/product-ecommerce.dto'; import { ProductDetailQueryDto } from './dto/product-detail-query.dto'; import { ProductDetailResponseDto } from './dto/product-detail-response.dto'; +import { RotinaA4QueryDto } from './dto/rotina-a4-query.dto'; +import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto'; @Injectable() export class ProductsService { constructor( - @InjectDataSource("oracle") private readonly dataSource: DataSource + @InjectDataSource('oracle') private readonly dataSource: DataSource, ) {} /** @@ -20,7 +22,10 @@ export class ProductsService { * @returns Dados do produto encontrado com estoque e preço * @throws HttpException quando produto não é encontrado */ - async productsValidation(storeId: string, filtro: string): Promise { + async productsValidation( + storeId: string, + filtro: string, + ): Promise { const sql = `SELECT PCPRODUT.DESCRICAO as "descricao" ,PCPRODUT.CODPROD as "codigoProduto" ,PCPRODUT.CODAUXILIAR as "codigoAuxiliar" @@ -46,7 +51,12 @@ export class ProductsService { PCPRODUT.CODPROD = REGEXP_REPLACE(:filtro2, '[^0-9]', '') OR PCPRODUT.DESCRICAO LIKE '%'||:filtro3||'%' )`; - const products = await this.dataSource.query(sql, [storeId, filtro, filtro, filtro]); + const products = await this.dataSource.query(sql, [ + storeId, + filtro, + filtro, + filtro, + ]); if (products.length === 0) { throw new HttpException('Produto não localizado!', HttpStatus.NOT_FOUND); @@ -56,7 +66,7 @@ export class ProductsService { if (!product.images) { product.images = []; - } else { + } else { product.images = product.images.includes(';') ? product.images.split(';') : [product.images]; @@ -79,7 +89,11 @@ export class ProductsService { try { const sqlInsert = `INSERT INTO ESTPRODUTOEXPOSICAO ( CODFILIAL, DATA, CODAUXILIAR, CODFUNC ) VALUES ( :storeId, TRUNC(SYSDATE), :ean, :userId )`; - await queryRunner.query(sqlInsert, [product.storeId, product.ean, product.userId]); + await queryRunner.query(sqlInsert, [ + product.storeId, + product.ean, + product.userId, + ]); await queryRunner.commitTransaction(); return { message: 'Registro incluído com sucesso!' }; } catch (err) { @@ -118,7 +132,7 @@ export class ProductsService { } return valor.toLocaleString('pt-BR', { minimumFractionDigits: 2, - maximumFractionDigits: 2 + maximumFractionDigits: 2, }); } @@ -127,10 +141,14 @@ export class ProductsService { * @param query - Parâmetros de busca (codprod, numregiao, codfilial) * @returns Lista de produtos com detalhes */ - async getProductDetails(query: ProductDetailQueryDto): Promise { + async getProductDetails( + query: ProductDetailQueryDto, + ): Promise { const { numregiao, codprod, codfilial } = query; - const placeholders = codprod.map((_, index) => `:codprod${index}`).join(','); + const placeholders = codprod + .map((_, index) => `:codprod${index}`) + .join(','); const sql = ` SELECT @@ -156,10 +174,106 @@ export class ProductsService { const params = [numregiao, codfilial, numregiao, ...codprod]; const products = await this.dataSource.query(sql, params); - - return products.map(product => ({ + + return products.map((product) => ({ ...product, - preco: this.formatarMoedaBrasileira(product.preco) + preco: this.formatarMoedaBrasileira(product.preco), })); } + + /** + * Busca informações do produto conforme rotina A4 + * @param query - Parâmetros de busca (numregiao, codprod, codfilial) + * @returns Dados do produto conforme rotina A4 + */ + async getRotinaA4(query: RotinaA4QueryDto): Promise { + const { numregiao, codprod, codfilial } = query; + + const sql = ` + SELECT + DADOS.DESCRICAO, + DADOS.CODPROD, + DADOS.PRECO_NORMAL, + DADOS.UNIDADE, + TRUNC(DADOS.VALOR_VENDA,0) VALOR_VENDA, + REPLACE(REPLACE(TO_CHAR( (DADOS.VALOR_VENDA - TRUNC(DADOS.VALOR_VENDA,0)) ,'FM999G9D00'),',',''),'.','') DECIMAL_VENDA, + DADOS.MARCA + FROM + (SELECT + pcprodut.DESCRICAO, + pcprodut.embalagem as unidade, + pctabpr.PVENDA PRECO_NORMAL, + pcprodut.CODPROD, + TRUNC(pctabpr.PVENDA1,2) PVENDA1, + (CASE WHEN + NVL(TRUNC((SELECT P.PRECOFIXO + FROM PCPRECOPROM P + WHERE P.CODPROD = PCTABPR.CODPROD + AND TRUNC(SYSDATE) BETWEEN P.DTINICIOVIGENCIA AND P.DTFIMVIGENCIA + AND (P.CODPLPAGMAX = 10 OR P.CODPLPAGMAX = 1) + AND ROWNUM = 1 + AND P.NUMREGIAO = PCTABPR.NUMREGIAO),1) ,0) = 0 + THEN PCTABPR.PVENDA1 + ELSE + TRUNC((SELECT P.PRECOFIXO + FROM PCPRECOPROM P + WHERE P.CODPROD = PCTABPR.CODPROD + AND TRUNC(SYSDATE) BETWEEN P.DTINICIOVIGENCIA AND P.DTFIMVIGENCIA + AND (P.CODPLPAGMAX = 10 OR P.CODPLPAGMAX = 1) + AND ROWNUM = 1 + AND P.NUMREGIAO = PCTABPR.NUMREGIAO),2) END ) VALOR_VENDA, + (select marca from pcmarca where pcmarca.codmarca = pcprodut.codmarca)marca + FROM + pctabpr, + pcprodut, + pcest + WHERE + pctabpr.CODPROD = pcprodut.CODPROD + and pcest.codprod = pctabpr.CODPROD + AND pctabpr.NUMREGIAO = :numregiao + and pcprodut.DTEXCLUSAO is null + and pcprodut.codprod = :codprod + AND pcest.codfilial = :codfilial + and pctabpr.PVENDA is not null ) DADOS + `; + + const result = await this.dataSource.query(sql, [ + numregiao, + codprod, + codfilial, + ]); + + if (result.length === 0) { + throw new HttpException( + 'Produto não encontrado para os parâmetros informados.', + HttpStatus.NOT_FOUND, + ); + } + + const produto = result[0]; + + /** + * Formata o preço normal como moeda brasileira com decimais + * Exemplo: 1109.9 -> "1.109,90" + */ + if (produto.PRECO_NORMAL !== null && produto.PRECO_NORMAL !== undefined) { + produto.PRECO_NORMAL = produto.PRECO_NORMAL.toLocaleString('pt-BR', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + } + + /** + * Formata o valor de venda como moeda brasileira sem decimais + * Exemplo: 2499 -> "R$ 2.499" + */ + if (produto.VALOR_VENDA !== null && produto.VALOR_VENDA !== undefined) { + produto.VALOR_VENDA = `R$ ${produto.VALOR_VENDA.toLocaleString('pt-BR', { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + })}`; + } + + return produto; + } } diff --git a/src/shared/ResultModel.ts b/src/shared/ResultModel.ts index 52b3103..ab6bd70 100644 --- a/src/shared/ResultModel.ts +++ b/src/shared/ResultModel.ts @@ -1,17 +1,16 @@ export class ResultModel { - constructor( - public success: boolean, - public message?: string, - public data?: T, - public error?: any - ) {} - - static success(data?: T, message?: string): ResultModel { - return new ResultModel(true, message, data); - } - - static failure(message: string, error?: any): ResultModel { - return new ResultModel(false, message, undefined, error); - } + constructor( + public success: boolean, + public message?: string, + public data?: T, + public error?: any, + ) {} + + static success(data?: T, message?: string): ResultModel { + return new ResultModel(true, message, data); } - \ No newline at end of file + + static failure(message: string, error?: any): ResultModel { + return new ResultModel(false, message, undefined, error); + } +} diff --git a/src/shared/cache.util.ts b/src/shared/cache.util.ts index 3c0dc29..cf1c96d 100644 --- a/src/shared/cache.util.ts +++ b/src/shared/cache.util.ts @@ -4,7 +4,7 @@ export async function getOrSetCache( redisClient: IRedisClient, key: string, ttlSeconds: number, - fallback: () => Promise + fallback: () => Promise, ): Promise { const cached = await redisClient.get(key); if (cached) return cached; diff --git a/src/shared/date.util.ts b/src/shared/date.util.ts index e113039..f35ee59 100644 --- a/src/shared/date.util.ts +++ b/src/shared/date.util.ts @@ -18,16 +18,20 @@ export class DateUtil { * @returns String da data no formato ISO com timezone brasileiro */ static toBrazilISOString(date: Date): string { - return date.toLocaleString('sv-SE', { - timeZone: this.BRAZIL_TIMEZONE, - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false - }).replace(' ', 'T') + '.000Z'; + return ( + date + .toLocaleString('sv-SE', { + timeZone: this.BRAZIL_TIMEZONE, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }) + .replace(' ', 'T') + '.000Z' + ); } /** @@ -36,7 +40,10 @@ export class DateUtil { * @param format Formato desejado (padrão: 'dd/MM/yyyy HH:mm:ss') * @returns String da data formatada no horário brasileiro */ - static toBrazilString(date: Date, format: string = 'dd/MM/yyyy HH:mm:ss'): string { + static toBrazilString( + date: Date, + format: string = 'dd/MM/yyyy HH:mm:ss', + ): string { const options: Intl.DateTimeFormatOptions = { timeZone: this.BRAZIL_TIMEZONE, year: 'numeric', @@ -45,18 +52,18 @@ export class DateUtil { hour: '2-digit', minute: '2-digit', second: '2-digit', - hour12: false + hour12: false, }; const formatter = new Intl.DateTimeFormat('pt-BR', options); const parts = formatter.formatToParts(date); - - const year = parts.find(part => part.type === 'year')?.value; - const month = parts.find(part => part.type === 'month')?.value; - const day = parts.find(part => part.type === 'day')?.value; - const hour = parts.find(part => part.type === 'hour')?.value; - const minute = parts.find(part => part.type === 'minute')?.value; - const second = parts.find(part => part.type === 'second')?.value; + + const year = parts.find((part) => part.type === 'year')?.value; + const month = parts.find((part) => part.type === 'month')?.value; + const day = parts.find((part) => part.type === 'day')?.value; + const hour = parts.find((part) => part.type === 'hour')?.value; + const minute = parts.find((part) => part.type === 'minute')?.value; + const second = parts.find((part) => part.type === 'second')?.value; return format .replace('dd', day || '') @@ -91,12 +98,12 @@ export class DateUtil { */ static isBrazilianDaylightSavingTime(date: Date): boolean { const year = date.getFullYear(); - + // Horário de verão no Brasil geralmente vai de outubro a fevereiro // (regras podem variar, esta é uma implementação simplificada) const october = new Date(year, 9, 1); // Outubro const february = new Date(year + 1, 1, 1); // Fevereiro do ano seguinte - + return date >= october || date < february; } @@ -106,8 +113,10 @@ export class DateUtil { * @returns Offset em minutos (negativo para oeste) */ static getBrazilTimezoneOffset(date: Date): number { - const utc = new Date(date.getTime() + (date.getTimezoneOffset() * 60000)); - const brazil = new Date(utc.toLocaleString('en-US', { timeZone: this.BRAZIL_TIMEZONE })); + const utc = new Date(date.getTime() + date.getTimezoneOffset() * 60000); + const brazil = new Date( + utc.toLocaleString('en-US', { timeZone: this.BRAZIL_TIMEZONE }), + ); return (utc.getTime() - brazil.getTime()) / 60000; } }