feat: adiciona endpoint placa-8122 e remove newrelic/pm2

Simplifica start:prod e ajusta consultas de ofertas para 10x (codplpagmax=42), com melhorias de sanitização e imports.
This commit is contained in:
2026-01-28 09:19:11 -03:00
parent b13e2775b4
commit 83a1fd78be
11 changed files with 689 additions and 1462 deletions

View File

@@ -1,29 +0,0 @@
module.exports = {
apps: [
{
name: 'portaljuru-api',
script: 'dist/main.js',
instances: 1,
exec_mode: 'fork',
interpreter: 'node',
interpreter_args: '-r newrelic',
env: {
NODE_ENV: 'production',
TZ: 'America/Sao_Paulo',
},
error_file: './logs/pm2-error.log',
out_file: './logs/pm2-out.log',
log_file: './logs/pm2-combined.log',
time: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
autorestart: true,
max_restarts: 10,
min_uptime: '10s',
max_memory_restart: '1G',
watch: false,
ignore_watch: ['node_modules', 'logs', 'dist'],
},
],
};

1428
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,14 +12,7 @@
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node -r newrelic dist/main",
"pm2:start": "pm2 start ecosystem.config.js",
"pm2:stop": "pm2 stop portaljuru-api",
"pm2:restart": "pm2 restart portaljuru-api",
"pm2:delete": "pm2 delete portaljuru-api",
"pm2:logs": "pm2 logs portaljuru-api",
"pm2:monit": "pm2 monit",
"pm2:status": "pm2 status",
"start:prod": "node dist/main",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
@@ -64,7 +57,6 @@
"md5": "^2.3.0",
"md5-typescript": "^1.0.5",
"multer": "^1.4.5-lts.2",
"newrelic": "^13.8.1",
"oracledb": "^6.8.0",
"passport": "^0.7.0",
"passport-http-bearer": "^1.0.1",
@@ -100,8 +92,7 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^3.9.0",
"typescript": "^5.8.2",
"pm2": "^5.3.0"
"typescript": "^5.8.2"
},
"jest": {
"moduleFileExtensions": [
@@ -125,8 +116,6 @@
"transformIgnorePatterns": [
"node_modules/(?!(typeorm|@nestjs)/)"
],
"setupFilesAfterEnv": [
"../jest.setup.js"
]
"setupFilesAfterEnv": ["../jest.setup.js"]
}
}

View File

@@ -25,7 +25,7 @@ describe('AuthService - logout', () => {
jest.clearAllMocks();
});
describe('logout - Tests that expose problems', () => {
describe('logout - Testes que expõem problemas', () => {
/**
* NOTA: Estes testes identificam problemas no método logout.
*
@@ -40,7 +40,7 @@ describe('AuthService - logout', () => {
* 8. Não sanitiza entrada
*/
it('should reject empty token', async () => {
it('deve rejeitar token vazio', async () => {
await expect(context.service.logout('')).rejects.toThrow(
'Token não pode estar vazio',
);
@@ -51,7 +51,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should reject null token', async () => {
it('deve rejeitar token null', async () => {
await expect(context.service.logout(null as any)).rejects.toThrow(
'Token não pode estar vazio',
);
@@ -62,7 +62,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should reject undefined token', async () => {
it('deve rejeitar token undefined', async () => {
await expect(context.service.logout(undefined as any)).rejects.toThrow(
'Token não pode estar vazio',
);
@@ -73,7 +73,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should reject whitespace-only token', async () => {
it('deve rejeitar token contendo apenas espaços em branco', async () => {
await expect(context.service.logout(' ')).rejects.toThrow(
'Token não pode estar vazio',
);
@@ -84,7 +84,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should reject extremely long tokens (DoS prevention)', async () => {
it('deve rejeitar tokens extremamente longos (prevenção de DoS)', async () => {
const hugeToken = 'a'.repeat(100000);
await expect(context.service.logout(hugeToken)).rejects.toThrow(
@@ -97,7 +97,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should validate decoded token is not null', async () => {
it('deve validar que token decodificado não é null', async () => {
context.mockJwtService.decode.mockReturnValue(null);
await expect(context.service.logout('invalid.token')).rejects.toThrow(
@@ -105,7 +105,7 @@ describe('AuthService - logout', () => {
);
});
it('should validate decoded token has required fields', async () => {
it('deve validar que token decodificado possui campos obrigatórios', async () => {
context.mockJwtService.decode.mockReturnValue({} as any);
await expect(context.service.logout('incomplete.token')).rejects.toThrow(
@@ -113,7 +113,7 @@ describe('AuthService - logout', () => {
);
});
it('should not add token to blacklist if already blacklisted', async () => {
it('não deve adicionar token à blacklist se já estiver na blacklist', async () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true);
await context.service.logout('already.blacklisted.token');
@@ -123,7 +123,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should validate session exists before terminating', async () => {
it('deve validar que sessão existe antes de terminar', async () => {
context.mockJwtService.decode.mockReturnValue({
id: 1,
sessionId: 'non-existent-session',
@@ -138,7 +138,7 @@ describe('AuthService - logout', () => {
).rejects.toThrow('Sessão não encontrada');
});
it('should handle decode errors gracefully', async () => {
it('deve tratar erros de decodificação de forma graciosa', async () => {
context.mockJwtService.decode.mockImplementation(() => {
throw new Error('Token inválido');
});
@@ -148,7 +148,7 @@ describe('AuthService - logout', () => {
).rejects.toThrow('Token inválido ou não pode ser decodificado');
});
it('should sanitize token input', async () => {
it('deve sanitizar entrada do token', async () => {
const maliciousToken = "'; DROP TABLE users; --";
await expect(context.service.logout(maliciousToken)).rejects.toThrow(
@@ -158,7 +158,7 @@ describe('AuthService - logout', () => {
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
});
it('should validate id is a positive number', async () => {
it('deve validar que id é um número positivo', async () => {
context.mockJwtService.decode.mockReturnValue({
id: -1,
sessionId: 'session-123',
@@ -169,7 +169,7 @@ describe('AuthService - logout', () => {
).rejects.toThrow('ID de usuário inválido no token');
});
it('should validate sessionId format if present', async () => {
it('deve validar formato do sessionId se presente', async () => {
context.mockJwtService.decode.mockReturnValue({
id: 1,
sessionId: '',
@@ -182,7 +182,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should complete logout even if session termination fails', async () => {
it('deve completar logout mesmo se terminação de sessão falhar', async () => {
context.mockJwtService.decode.mockReturnValue({
id: 1,
sessionId: 'session-123',
@@ -200,7 +200,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledWith('valid.token');
});
it('should not throw if token is already blacklisted', async () => {
it('não deve lançar erro se token já estiver na blacklist', async () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true);
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
new Error('Token já está na blacklist'),
@@ -213,7 +213,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should validate token format before decoding', async () => {
it('deve validar formato do token antes de decodificar', async () => {
const invalidFormatToken = 'not.a.jwt.token';
await context.service.logout(invalidFormatToken);
@@ -221,7 +221,7 @@ describe('AuthService - logout', () => {
expect(context.mockJwtService.decode).toHaveBeenCalled();
});
it('should handle concurrent logout requests safely', async () => {
it('deve tratar requisições de logout concorrentes com segurança', async () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
context.mockJwtService.decode.mockReturnValue({
id: 1,
@@ -241,7 +241,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledTimes(3);
});
it('should validate decoded payload structure', async () => {
it('deve validar estrutura do payload decodificado', async () => {
context.mockJwtService.decode.mockReturnValue({
invalidField: 'value',
} as any);
@@ -258,7 +258,7 @@ describe('AuthService - logout', () => {
).not.toHaveBeenCalled();
});
it('should ensure token is always blacklisted on success', async () => {
it('deve garantir que token seja sempre adicionado à blacklist em caso de sucesso', async () => {
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
await context.service.logout('valid.token');
@@ -271,7 +271,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledTimes(1);
});
it('should handle race condition when token becomes blacklisted between check and add', async () => {
it('deve tratar condição de corrida quando token é adicionado à blacklist entre verificação e adição', async () => {
/**
* Cenário: Race condition - token não estava na blacklist quando verificamos,
* mas foi adicionado por outra requisição antes de adicionarmos.
@@ -293,7 +293,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledWith('token.with.race.condition');
});
it('should throw error if addToBlacklist fails with non-blacklist error', async () => {
it('deve lançar erro se addToBlacklist falhar com erro não relacionado à blacklist', async () => {
/**
* Cenário: Falha ao adicionar token à blacklist por outro motivo.
* Problema: Pode falhar silenciosamente.
@@ -318,7 +318,7 @@ describe('AuthService - logout', () => {
).toHaveBeenCalledWith('token.with.blacklist.error');
});
it('should verify isBlacklisted is called before addToBlacklist', async () => {
it('deve verificar que isBlacklisted é chamado antes de addToBlacklist', async () => {
/**
* Cenário: Garantir ordem correta das chamadas.
* Problema: Pode adicionar sem verificar primeiro.

View File

@@ -3,6 +3,7 @@ import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
import { JwtService } from '@nestjs/jwt';
import { JwtPayload } from '../models/jwt-payload.model';
import * as crypto from 'crypto';
@Injectable()
export class TokenBlacklistService {
@@ -64,7 +65,6 @@ export class TokenBlacklistService {
}
private hashToken(token: string): string {
const crypto = require('crypto');
return crypto
.createHash('sha256')
.update(token)

View File

@@ -1,6 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { UserRepository } from '../users/UserRepository';
import md5 = require('md5');
import * as md5 from 'md5';
@Injectable()
export class ChangePasswordService {

View File

@@ -31,7 +31,7 @@ export class RequestSanitizerMiddleware implements NestMiddleware {
private sanitizeString(str: string): string {
// Remover tags HTML básicas
str = str.replace(/<(|\/|[^>\/bi]|\/[^>bi]|[^\/>][^>]+|\/[^>][^>]+)>/g, '');
str = str.replace(/<[^>]*>/g, '');
// Remover scripts JavaScript
str = str.replace(/javascript:/g, '');

View File

@@ -0,0 +1,317 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProductsController } from '../products.controller';
import { ProductsService } from '../products.service';
import { Oferta8026QueryDto } from '../dto/oferta-8026-query.dto';
import { Placa8122ResponseDto } from '../dto/placa-8122-response.dto';
import { HttpException, HttpStatus } from '@nestjs/common';
describe('ProductsController', () => {
let controller: ProductsController;
let service: ProductsService;
const mockProductsService = {
getPlaca8122: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ProductsController],
providers: [
{
provide: ProductsService,
useValue: mockProductsService,
},
],
}).compile();
controller = module.get<ProductsController>(ProductsController);
service = module.get<ProductsService>(ProductsService);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getPlaca8122', () => {
it('deve retornar lista de placas com sucesso', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [62602, 62603],
numregiao: 1,
};
const mockResponse: Placa8122ResponseDto[] = [
{
codprod: 62602,
descricao: 'PRODUTO EXEMPLO 1',
marca: 'MARCA EXEMPLO',
unidade: 'UN',
pvenda1: 99.9,
precofixo: 79.9,
dec: '99',
preco: 79,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 42,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 7.99,
inicio: new Date('2024-11-19'),
},
{
codprod: 62603,
descricao: 'PRODUTO EXEMPLO 2',
marca: 'MARCA EXEMPLO 2',
unidade: 'UN',
pvenda1: 149.9,
precofixo: 119.9,
dec: '99',
preco: 119,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 46,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 11.99,
inicio: new Date('2024-11-19'),
},
];
mockProductsService.getPlaca8122.mockResolvedValue(mockResponse);
const result = await controller.getPlaca8122(query);
expect(result).toEqual(mockResponse);
expect(result).toHaveLength(2);
expect(result[0].codprod).toBe(62602);
expect(result[0].descricao).toBe('PRODUTO EXEMPLO 1');
expect(result[0].marca).toBe('MARCA EXEMPLO');
expect(result[0].pvenda1).toBe(99.9);
expect(result[0].precofixo).toBe(79.9);
expect(result[0].percdesconto).toBe(20);
expect(result[0].mensagem4).toBe('10X DE');
expect(result[0].parcelas).toBe(7.99);
expect(service.getPlaca8122).toHaveBeenCalledTimes(1);
expect(service.getPlaca8122).toHaveBeenCalledWith(query);
});
it('deve retornar array vazio quando nenhuma placa é encontrada', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [99999],
numregiao: 1,
};
mockProductsService.getPlaca8122.mockResolvedValue([]);
const result = await controller.getPlaca8122(query);
expect(result).toEqual([]);
expect(result).toHaveLength(0);
expect(service.getPlaca8122).toHaveBeenCalledTimes(1);
expect(service.getPlaca8122).toHaveBeenCalledWith(query);
});
it('deve propagar exceção quando service lança erro de validação', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [],
numregiao: 1,
};
const error = new HttpException(
'É necessário informar pelo menos um código de produto.',
HttpStatus.BAD_REQUEST,
);
mockProductsService.getPlaca8122.mockRejectedValue(error);
await expect(controller.getPlaca8122(query)).rejects.toThrow(
HttpException,
);
await expect(controller.getPlaca8122(query)).rejects.toThrow(
'É necessário informar pelo menos um código de produto.',
);
expect(service.getPlaca8122).toHaveBeenCalledTimes(2);
expect(service.getPlaca8122).toHaveBeenCalledWith(query);
});
it('deve retornar placa com mensagem para pagamento à vista', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [62602],
numregiao: 1,
};
const mockResponse: Placa8122ResponseDto[] = [
{
codprod: 62602,
descricao: 'PRODUTO EXEMPLO',
marca: 'MARCA EXEMPLO',
unidade: 'UN',
pvenda1: 99.9,
precofixo: 79.9,
dec: '99',
preco: 79,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 10,
mensagem2: null,
mensagem3: 'À VISTA | R$',
mensagem4: null,
mensagem6: 'OU R$',
mensagem7: 'NO CARTÃO',
mensagem8: null,
parcelas: 0,
inicio: new Date('2024-11-19'),
},
];
mockProductsService.getPlaca8122.mockResolvedValue(mockResponse);
const result = await controller.getPlaca8122(query);
expect(result).toHaveLength(1);
expect(result[0].codplpagmax).toBe(10);
expect(result[0].mensagem3).toBe('À VISTA | R$');
expect(result[0].mensagem6).toBe('OU R$');
expect(result[0].mensagem7).toBe('NO CARTÃO');
expect(result[0].parcelas).toBe(0);
});
it('deve retornar placa com mensagem para débito', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [62602],
numregiao: 1,
};
const mockResponse: Placa8122ResponseDto[] = [
{
codprod: 62602,
descricao: 'PRODUTO EXEMPLO',
marca: 'MARCA EXEMPLO',
unidade: 'UN',
pvenda1: 99.9,
precofixo: 79.9,
dec: '99',
preco: 79,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 2,
mensagem2: 'DEBITO',
mensagem3: null,
mensagem4: null,
mensagem6: null,
mensagem7: null,
mensagem8: null,
parcelas: 0,
inicio: new Date('2024-11-19'),
},
];
mockProductsService.getPlaca8122.mockResolvedValue(mockResponse);
const result = await controller.getPlaca8122(query);
expect(result).toHaveLength(1);
expect(result[0].codplpagmax).toBe(2);
expect(result[0].mensagem2).toBe('DEBITO');
});
it('deve processar múltiplos produtos corretamente', async () => {
const query: Oferta8026QueryDto = {
data: '19/11/2024',
codprod: [62602, 62603, 62604],
numregiao: 1,
};
const mockResponse: Placa8122ResponseDto[] = [
{
codprod: 62602,
descricao: 'PRODUTO 1',
marca: 'MARCA 1',
unidade: 'UN',
pvenda1: 99.9,
precofixo: 79.9,
dec: '99',
preco: 79,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 42,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 7.99,
inicio: new Date('2024-11-19'),
},
{
codprod: 62603,
descricao: 'PRODUTO 2',
marca: 'MARCA 2',
unidade: 'UN',
pvenda1: 149.9,
precofixo: 119.9,
dec: '99',
preco: 119,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 46,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 11.99,
inicio: new Date('2024-11-19'),
},
{
codprod: 62604,
descricao: 'PRODUTO 3',
marca: 'MARCA 3',
unidade: 'UN',
pvenda1: 199.9,
precofixo: 159.9,
dec: '99',
preco: 159,
percdesconto: 20,
dtfimvigencia: new Date('2024-12-31'),
codplpagmax: 42,
mensagem2: null,
mensagem3: null,
mensagem4: '10X DE',
mensagem6: null,
mensagem7: null,
mensagem8: 'TOTAL: R$',
parcelas: 15.99,
inicio: new Date('2024-11-19'),
},
];
mockProductsService.getPlaca8122.mockResolvedValue(mockResponse);
const result = await controller.getPlaca8122(query);
expect(result).toHaveLength(3);
expect(result[0].codprod).toBe(62602);
expect(result[1].codprod).toBe(62603);
expect(result[2].codprod).toBe(62604);
expect(service.getPlaca8122).toHaveBeenCalledWith(query);
});
});
});

View File

@@ -0,0 +1,127 @@
import { ApiProperty } from '@nestjs/swagger';
/**
* DTO para resposta de placa 8122
*/
export class Placa8122ResponseDto {
@ApiProperty({
description: 'Código do produto',
example: 12345,
})
codprod: number;
@ApiProperty({
description: 'Descrição do produto',
example: 'PRODUTO EXEMPLO',
})
descricao: string;
@ApiProperty({
description: 'Marca do produto',
example: 'MARCA EXEMPLO',
})
marca: string;
@ApiProperty({
description: 'Unidade do produto',
example: 'UN',
})
unidade: string;
@ApiProperty({
description: 'Preço de venda 1',
example: 99.9,
})
pvenda1: number;
@ApiProperty({
description: 'Preço fixo promocional',
example: 79.9,
})
precofixo: number;
@ApiProperty({
description: 'Parte decimal do preço',
example: '99',
})
dec: string;
@ApiProperty({
description: 'Preço truncado',
example: 79,
})
preco: number;
@ApiProperty({
description: 'Percentual de desconto',
example: 20,
})
percdesconto: number;
@ApiProperty({
description: 'Data de fim da vigência',
example: '2024-12-31',
})
dtfimvigencia: Date;
@ApiProperty({
description: 'Código do plano de pagamento máximo',
example: 42,
})
codplpagmax: number;
@ApiProperty({
description: 'Mensagem para débito',
example: 'DEBITO',
required: false,
})
mensagem2: string | null;
@ApiProperty({
description: 'Mensagem para à vista',
example: 'À VISTA | R$',
required: false,
})
mensagem3: string | null;
@ApiProperty({
description: 'Mensagem para 10x',
example: '10X DE',
required: false,
})
mensagem4: string | null;
@ApiProperty({
description: 'Mensagem 6',
example: 'OU R$',
required: false,
})
mensagem6: string | null;
@ApiProperty({
description: 'Mensagem 7',
example: 'NO CARTÃO',
required: false,
})
mensagem7: string | null;
@ApiProperty({
description: 'Mensagem 8',
example: 'TOTAL: R$',
required: false,
})
mensagem8: string | null;
@ApiProperty({
description: 'Valor das parcelas',
example: 15.99,
})
parcelas: number;
@ApiProperty({
description: 'Data de início da vigência',
example: '2024-11-19',
})
inicio: Date;
}

View File

@@ -29,6 +29,7 @@ import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto';
import { UnifiedProductSearchDto } from './dto/unified-product-search.dto';
import { Oferta8026QueryDto } from './dto/oferta-8026-query.dto';
import { Oferta8026ResponseDto } from './dto/oferta-8026-response.dto';
import { Placa8122ResponseDto } from './dto/placa-8122-response.dto';
//@ApiBearerAuth()
//@UseGuards(JwtAuthGuard)
@@ -162,7 +163,7 @@ export class ProductsController {
@ApiBody({ type: Oferta8026QueryDto })
@ApiResponse({
status: 200,
description: 'Lista de ofertas retornada com sucesso.',
description: 'Oferta 8026 retornada com sucesso.',
type: Oferta8026ResponseDto,
isArray: true,
})
@@ -172,4 +173,23 @@ export class ProductsController {
): Promise<Oferta8026ResponseDto[]> {
return this.productsService.getOferta8026(query);
}
/**
* Endpoint para buscar placa 8122
*/
@Post('placa-8122')
@ApiOperation({ summary: 'Busca placa 8122 conforme parâmetros específicos' })
@ApiBody({ type: Oferta8026QueryDto })
@ApiResponse({
status: 200,
description: 'Placa 8122 retornada com sucesso.',
type: Placa8122ResponseDto,
isArray: true,
})
@ApiResponse({ status: 400, description: 'Parâmetros inválidos.' })
async getPlaca8122(
@Body() query: Oferta8026QueryDto,
): Promise<Placa8122ResponseDto[]> {
return this.productsService.getPlaca8122(query);
}
}

View File

@@ -11,6 +11,7 @@ import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto';
import { UnifiedProductSearchDto } from './dto/unified-product-search.dto';
import { Oferta8026QueryDto } from './dto/oferta-8026-query.dto';
import { Oferta8026ResponseDto } from './dto/oferta-8026-response.dto';
import { Placa8122ResponseDto } from './dto/placa-8122-response.dto';
@Injectable()
export class ProductsService {
@@ -593,12 +594,13 @@ export class ProductsService {
pcmarca.marca,
pcprodut.unidade,
pctabpr.pvenda1,
pcprodut.codauxiliar,
pcprecoprom.precofixo,
TRUNC(((pctabpr.pvenda1 - pcprecoprom.precofixo) / pctabpr.pvenda1) * 100, 0) percdesconto,
pcprecoprom.dtfimvigencia,
CASE WHEN pcprecoprom.codplpagmax = 2 THEN 'DEBITO' ELSE NULL END mensagem2,
CASE WHEN pcprecoprom.codplpagmax = 10 THEN 'À VISTA' ELSE NULL END mensagem3,
CASE WHEN pcprecoprom.codplpagmax = 42 OR pcprecoprom.codplpagmax = 46 THEN '10X' ELSE NULL END mensagem4
CASE WHEN pcprecoprom.codplpagmax = 42 THEN '10X' ELSE NULL END mensagem4
FROM pctabpr, pcprecoprom, pcplpag, pcprodut, pcmarca
WHERE pctabpr.codprod = pcprecoprom.codprod
AND pctabpr.numregiao = pcprecoprom.numregiao
@@ -608,7 +610,7 @@ export class ProductsService {
AND TRUNC(pcprecoprom.dtiniciovigencia) = TRUNC(TO_DATE(:0, 'DD/MM/YYYY'))
AND pcprecoprom.codprod IN (${codprodPlaceholders.join(',')})
AND PCPRECOPROM.DTFIMVIGENCIA >= TRUNC(SYSDATE)
AND pcprecoprom.codplpagmax IN (42, 46)
AND pcprecoprom.codplpagmax = 42
AND pctabpr.numregiao = :${paramIndex}
`;
@@ -620,6 +622,7 @@ export class ProductsService {
marca: row.MARCA,
unidade: row.UNIDADE,
pvenda1: row.PVENDA1,
codauxiliar: row.CODAUXILIAR,
precofixo: row.PRECOFIXO,
percdesconto: row.PERCDESCONTO,
dtfimvigencia: row.DTFIMVIGENCIA,
@@ -628,4 +631,156 @@ export class ProductsService {
mensagem4: row.MENSAGEM4,
}));
}
async getPlaca8122(
query: Oferta8026QueryDto,
): Promise<Placa8122ResponseDto[]> {
const { data, codprod, numregiao } = query;
if (!codprod || codprod.length === 0) {
throw new HttpException(
'É necessário informar pelo menos um código de produto.',
HttpStatus.BAD_REQUEST,
);
}
let dataFormatada = data;
const dateMatch = data.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (dateMatch) {
const [, part1, part2, year] = dateMatch;
const num2 = parseInt(part2, 10);
dataFormatada = num2 > 12
? `${part2}/${part1}/${year}`
: `${part1}/${part2}/${year}`;
}
const codprodPlaceholders: string[] = [];
const params: any[] = [];
let paramIndex = 0;
params.push(dataFormatada);
paramIndex++;
codprod.forEach((cod) => {
codprodPlaceholders.push(`:${paramIndex}`);
params.push(cod);
paramIndex++;
});
const regiaoParamIndex = paramIndex;
params.push(numregiao);
paramIndex++;
const dataParamIndex2 = paramIndex;
params.push(dataFormatada);
paramIndex++;
const codprodPlaceholders2: string[] = [];
codprod.forEach((cod) => {
codprodPlaceholders2.push(`:${paramIndex}`);
params.push(cod);
paramIndex++;
});
const regiaoParamIndex2 = paramIndex;
params.push(numregiao);
const sql = `
SELECT PCTABPR.CODPROD, PCPRODUT.DESCRICAO, PCMARCA.MARCA, PCPRODUT.UNIDADE,
PCTABPR.PVENDA1, PCPRECOPROM.PRECOFIXO,
CASE WHEN PCTABPR.PVENDA1 < 500 and nvl(PCPRECOPROM.PRECOFIXO,0) = 0 THEN REPLACE(TO_CHAR((PCTABPR.PVENDA1) - TRUNC(PCTABPR.PVENDA1), '0.00'),'0.', '')
ELSE REPLACE(TO_CHAR(TRUNC(((PCPRECOPROM.PRECOFIXO /10) - TRUNC(PCPRECOPROM.PRECOFIXO /10)),2), '0.00'),'0.', '') END DEC,
TRUNC((PCPRECOPROM.PRECOFIXO),0) PRECO,
TRUNC((((PCTABPR.PVENDA1 - PCPRECOPROM.PRECOFIXO) / PCTABPR.PVENDA1) * 100),0) PERCDESCONTO,
PCPRECOPROM.DTFIMVIGENCIA, PCPRECOPROM.CODPLPAGMAX,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 2 THEN 'DEBITO' ELSE NULL END MENSAGEM2,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'À VISTA | R$' ELSE NULL END MENSAGEM3,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 42 THEN '10X DE' ELSE NULL END MENSAGEM4,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'OU R$' ELSE NULL END MENSAGEM6,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'NO CARTÃO' ELSE NULL END MENSAGEM7,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 42 THEN 'TOTAL: R$' ELSE NULL END MENSAGEM8,
CASE
WHEN NVL(PCPRECOPROM.CODPLPAGMAX,0) = 42 THEN
TRUNC((PCPRECOPROM.PRECOFIXO / 10),2)
ELSE 0
END PARCELAS,
PCPRECOPROM.DTINICIOVIGENCIA INICIO
FROM PCTABPR, PCPRECOPROM, PCPLPAG, PCPRODUT, PCMARCA
WHERE PCTABPR.CODPROD = PCPRECOPROM.CODPROD
AND PCTABPR.NUMREGIAO = PCPRECOPROM.NUMREGIAO
AND PCTABPR.CODPROD = PCPRODUT.CODPROD
AND PCPRODUT.CODMARCA = PCMARCA.CODMARCA (+)
AND PCPRECOPROM.CODPLPAGMAX = PCPLPAG.CODPLPAG (+)
AND PCPRECOPROM.DTFIMVIGENCIA >= TRUNC(SYSDATE)
AND PCPRECOPROM.CODPLPAGMAX = 42
AND PCPRECOPROM.DTINICIOVIGENCIA = TRUNC(TO_DATE(:0, 'DD/MM/YYYY'))
AND PCPRECOPROM.CODPROD IN (${codprodPlaceholders.join(',')})
AND PCTABPR.NUMREGIAO = :${regiaoParamIndex}
AND NOT EXISTS(SELECT PCFORMPROD.CODPRODMP FROM PCFORMPROD WHERE PCFORMPROD.CODPRODACAB = PCPRODUT.CODPROD)
UNION ALL
SELECT PCTABPR.CODPROD, PCPRODUT.DESCRICAO, PCMARCA.MARCA, PCPRODUT.UNIDADE,
PCTABPR.PVENDA1, PCPRECOPROM.PRECOFIXO,
CASE WHEN PCTABPR.PVENDA1 < 500 and nvl(PCPRECOPROM.PRECOFIXO,0) = 0 THEN REPLACE(TO_CHAR((PCTABPR.PVENDA1) - TRUNC(PCTABPR.PVENDA1), '0.00'),'0.', '')
ELSE REPLACE(TO_CHAR(TRUNC(((PCPRECOPROM.PRECOFIXO / 10) - TRUNC(PCPRECOPROM.PRECOFIXO / 10)),2), '0.00'),'0.', '') END DEC,
TRUNC((PCPRECOPROM.PRECOFIXO),0) PRECO,
TRUNC((((PCTABPR.PVENDA1 - PCPRECOPROM.PRECOFIXO) / PCTABPR.PVENDA1) * 100),0) PERCDESCONTO,
PCPRECOPROM.DTFIMVIGENCIA, PCPRECOPROM.CODPLPAGMAX,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 2 THEN 'DEBITO' ELSE NULL END MENSAGEM2,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'À VISTA | R$' ELSE NULL END MENSAGEM3,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 42 THEN '10X DE' ELSE NULL END MENSAGEM4,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'OU R$' ELSE NULL END MENSAGEM6,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 10 THEN 'NO CARTÃO' ELSE NULL END MENSAGEM7,
CASE WHEN PCPRECOPROM.CODPLPAGMAX = 42 THEN 'TOTAL: R$' ELSE NULL END MENSAGEM8,
CASE
WHEN NVL(PCPRECOPROM.CODPLPAGMAX,0) = 42 THEN
TRUNC((PCPRECOPROM.PRECOFIXO / 10),2)
ELSE 0
END PARCELAS,
PCPRECOPROM.INICIO
FROM PCTABPR,
(SELECT PCFORMPROD.CODPRODACAB, PROMOCAO.DTINICIOVIGENCIA INICIO, PROMOCAO.DTINICIOVIGENCIA, PROMOCAO.NUMREGIAO, PROMOCAO.CODPLPAGMAX, SUM(NVL(PROMOCAO.PRECOFIXO, TABELA.PVENDA1) * PCFORMPROD.QTPRODMP) PRECOFIXO,
PROMOCAO.DTFIMVIGENCIA
FROM PCFORMPROD, PCPRECOPROM PROMOCAO, PCTABPR TABELA
WHERE PROMOCAO.CODPLPAGMAX = 42
AND PROMOCAO.DTINICIOVIGENCIA = TRUNC(TO_DATE(:${dataParamIndex2}, 'DD/MM/YYYY'))
AND PCFORMPROD.CODPRODMP = PROMOCAO.CODPROD
AND PCFORMPROD.CODPRODMP = TABELA.CODPROD
AND TABELA.NUMREGIAO = PROMOCAO.NUMREGIAO
GROUP BY PCFORMPROD.CODPRODACAB, PROMOCAO.NUMREGIAO, PROMOCAO.CODPLPAGMAX, PROMOCAO.DTFIMVIGENCIA, PROMOCAO.DTINICIOVIGENCIA) PCPRECOPROM, PCPLPAG, PCPRODUT, PCMARCA
WHERE PCTABPR.CODPROD = PCPRECOPROM.CODPRODACAB (+)
AND PCTABPR.NUMREGIAO = PCPRECOPROM.NUMREGIAO (+)
AND PCTABPR.CODPROD = PCPRODUT.CODPROD
AND PCPRODUT.CODMARCA = PCMARCA.CODMARCA (+)
AND PCPRECOPROM.DTFIMVIGENCIA >= TRUNC(SYSDATE)
AND PCPLPAG.CODPLPAG = 42
AND PCPRODUT.CODPROD IN (${codprodPlaceholders2.join(',')})
AND PCTABPR.NUMREGIAO = :${regiaoParamIndex2}
AND EXISTS(SELECT PCFORMPROD.CODPRODMP FROM PCFORMPROD WHERE PCFORMPROD.CODPRODACAB = PCPRODUT.CODPROD)
`;
const result = await this.dataSource.query(sql, params);
return result.map((row) => ({
codprod: row.CODPROD,
descricao: row.DESCRICAO,
marca: row.MARCA,
unidade: row.UNIDADE,
pvenda1: row.PVENDA1,
precofixo: row.PRECOFIXO,
dec: row.DEC,
preco: row.PRECO,
percdesconto: row.PERCDESCONTO,
dtfimvigencia: row.DTFIMVIGENCIA,
codplpagmax: row.CODPLPAGMAX,
mensagem2: row.MENSAGEM2,
mensagem3: row.MENSAGEM3,
mensagem4: row.MENSAGEM4,
mensagem6: row.MENSAGEM6,
mensagem7: row.MENSAGEM7,
mensagem8: row.MENSAGEM8,
parcelas: row.PARCELAS,
inicio: row.INICIO,
}));
}
}