Compare commits
4 Commits
17bec31bf8
...
feat/placa
| Author | SHA1 | Date | |
|---|---|---|---|
| e2e5bf5b3f | |||
| 83a1fd78be | |||
|
|
b13e2775b4 | ||
|
|
8cfcaf3910 |
@@ -2,7 +2,6 @@ import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { createOracleConfig } from './core/configs/typeorm.oracle.config';
|
import { createOracleConfig } from './core/configs/typeorm.oracle.config';
|
||||||
import { createPostgresConfig } from './core/configs/typeorm.postgres.config';
|
|
||||||
import { LogisticModule } from './logistic/logistic.module';
|
import { LogisticModule } from './logistic/logistic.module';
|
||||||
import { AuthModule } from './auth/auth/auth.module';
|
import { AuthModule } from './auth/auth/auth.module';
|
||||||
import { DataConsultModule } from './data-consult/data-consult.module';
|
import { DataConsultModule } from './data-consult/data-consult.module';
|
||||||
@@ -27,11 +26,12 @@ import { PartnersModule } from './partners/partners.module';
|
|||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: createOracleConfig,
|
useFactory: createOracleConfig,
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forRootAsync({
|
// Postgres desativado: conexao removida e trechos de uso comentados no codigo.
|
||||||
name: 'postgres',
|
// TypeOrmModule.forRootAsync({
|
||||||
inject: [ConfigService],
|
// name: 'postgres',
|
||||||
useFactory: createPostgresConfig,
|
// inject: [ConfigService],
|
||||||
}),
|
// useFactory: createPostgresConfig,
|
||||||
|
// }),
|
||||||
ThrottlerModule.forRootAsync({
|
ThrottlerModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ describe('AuthService - logout', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('logout - Tests that expose problems', () => {
|
describe('logout - Testes que expõem problemas', () => {
|
||||||
/**
|
/**
|
||||||
* NOTA: Estes testes identificam problemas no método logout.
|
* NOTA: Estes testes identificam problemas no método logout.
|
||||||
*
|
*
|
||||||
@@ -40,7 +40,7 @@ describe('AuthService - logout', () => {
|
|||||||
* 8. Não sanitiza entrada
|
* 8. Não sanitiza entrada
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('should reject empty token', async () => {
|
it('deve rejeitar token vazio', async () => {
|
||||||
await expect(context.service.logout('')).rejects.toThrow(
|
await expect(context.service.logout('')).rejects.toThrow(
|
||||||
'Token não pode estar vazio',
|
'Token não pode estar vazio',
|
||||||
);
|
);
|
||||||
@@ -51,7 +51,7 @@ describe('AuthService - logout', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject null token', async () => {
|
it('deve rejeitar token null', async () => {
|
||||||
await expect(context.service.logout(null as any)).rejects.toThrow(
|
await expect(context.service.logout(null as any)).rejects.toThrow(
|
||||||
'Token não pode estar vazio',
|
'Token não pode estar vazio',
|
||||||
);
|
);
|
||||||
@@ -62,7 +62,7 @@ describe('AuthService - logout', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject undefined token', async () => {
|
it('deve rejeitar token undefined', async () => {
|
||||||
await expect(context.service.logout(undefined as any)).rejects.toThrow(
|
await expect(context.service.logout(undefined as any)).rejects.toThrow(
|
||||||
'Token não pode estar vazio',
|
'Token não pode estar vazio',
|
||||||
);
|
);
|
||||||
@@ -73,7 +73,7 @@ describe('AuthService - logout', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).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(
|
await expect(context.service.logout(' ')).rejects.toThrow(
|
||||||
'Token não pode estar vazio',
|
'Token não pode estar vazio',
|
||||||
);
|
);
|
||||||
@@ -84,7 +84,7 @@ describe('AuthService - logout', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).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);
|
const hugeToken = 'a'.repeat(100000);
|
||||||
|
|
||||||
await expect(context.service.logout(hugeToken)).rejects.toThrow(
|
await expect(context.service.logout(hugeToken)).rejects.toThrow(
|
||||||
@@ -97,7 +97,7 @@ describe('AuthService - logout', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).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);
|
context.mockJwtService.decode.mockReturnValue(null);
|
||||||
|
|
||||||
await expect(context.service.logout('invalid.token')).rejects.toThrow(
|
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);
|
context.mockJwtService.decode.mockReturnValue({} as any);
|
||||||
|
|
||||||
await expect(context.service.logout('incomplete.token')).rejects.toThrow(
|
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);
|
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true);
|
||||||
|
|
||||||
await context.service.logout('already.blacklisted.token');
|
await context.service.logout('already.blacklisted.token');
|
||||||
@@ -123,7 +123,7 @@ describe('AuthService - logout', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).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({
|
context.mockJwtService.decode.mockReturnValue({
|
||||||
id: 1,
|
id: 1,
|
||||||
sessionId: 'non-existent-session',
|
sessionId: 'non-existent-session',
|
||||||
@@ -138,7 +138,7 @@ describe('AuthService - logout', () => {
|
|||||||
).rejects.toThrow('Sessão não encontrada');
|
).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(() => {
|
context.mockJwtService.decode.mockImplementation(() => {
|
||||||
throw new Error('Token inválido');
|
throw new Error('Token inválido');
|
||||||
});
|
});
|
||||||
@@ -148,7 +148,7 @@ describe('AuthService - logout', () => {
|
|||||||
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sanitize token input', async () => {
|
it('deve sanitizar entrada do token', async () => {
|
||||||
const maliciousToken = "'; DROP TABLE users; --";
|
const maliciousToken = "'; DROP TABLE users; --";
|
||||||
|
|
||||||
await expect(context.service.logout(maliciousToken)).rejects.toThrow(
|
await expect(context.service.logout(maliciousToken)).rejects.toThrow(
|
||||||
@@ -158,7 +158,7 @@ describe('AuthService - logout', () => {
|
|||||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
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({
|
context.mockJwtService.decode.mockReturnValue({
|
||||||
id: -1,
|
id: -1,
|
||||||
sessionId: 'session-123',
|
sessionId: 'session-123',
|
||||||
@@ -169,7 +169,7 @@ describe('AuthService - logout', () => {
|
|||||||
).rejects.toThrow('ID de usuário inválido no token');
|
).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({
|
context.mockJwtService.decode.mockReturnValue({
|
||||||
id: 1,
|
id: 1,
|
||||||
sessionId: '',
|
sessionId: '',
|
||||||
@@ -182,7 +182,7 @@ describe('AuthService - logout', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).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({
|
context.mockJwtService.decode.mockReturnValue({
|
||||||
id: 1,
|
id: 1,
|
||||||
sessionId: 'session-123',
|
sessionId: 'session-123',
|
||||||
@@ -200,7 +200,7 @@ describe('AuthService - logout', () => {
|
|||||||
).toHaveBeenCalledWith('valid.token');
|
).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.isBlacklisted.mockResolvedValue(true);
|
||||||
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
||||||
new Error('Token já está na blacklist'),
|
new Error('Token já está na blacklist'),
|
||||||
@@ -213,7 +213,7 @@ describe('AuthService - logout', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).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';
|
const invalidFormatToken = 'not.a.jwt.token';
|
||||||
|
|
||||||
await context.service.logout(invalidFormatToken);
|
await context.service.logout(invalidFormatToken);
|
||||||
@@ -221,7 +221,7 @@ describe('AuthService - logout', () => {
|
|||||||
expect(context.mockJwtService.decode).toHaveBeenCalled();
|
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.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
||||||
context.mockJwtService.decode.mockReturnValue({
|
context.mockJwtService.decode.mockReturnValue({
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -241,7 +241,7 @@ describe('AuthService - logout', () => {
|
|||||||
).toHaveBeenCalledTimes(3);
|
).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate decoded payload structure', async () => {
|
it('deve validar estrutura do payload decodificado', async () => {
|
||||||
context.mockJwtService.decode.mockReturnValue({
|
context.mockJwtService.decode.mockReturnValue({
|
||||||
invalidField: 'value',
|
invalidField: 'value',
|
||||||
} as any);
|
} as any);
|
||||||
@@ -258,7 +258,7 @@ describe('AuthService - logout', () => {
|
|||||||
).not.toHaveBeenCalled();
|
).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);
|
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
||||||
|
|
||||||
await context.service.logout('valid.token');
|
await context.service.logout('valid.token');
|
||||||
@@ -271,7 +271,7 @@ describe('AuthService - logout', () => {
|
|||||||
).toHaveBeenCalledTimes(1);
|
).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,
|
* Cenário: Race condition - token não estava na blacklist quando verificamos,
|
||||||
* mas foi adicionado por outra requisição antes de adicionarmos.
|
* mas foi adicionado por outra requisição antes de adicionarmos.
|
||||||
@@ -293,7 +293,7 @@ describe('AuthService - logout', () => {
|
|||||||
).toHaveBeenCalledWith('token.with.race.condition');
|
).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.
|
* Cenário: Falha ao adicionar token à blacklist por outro motivo.
|
||||||
* Problema: Pode falhar silenciosamente.
|
* Problema: Pode falhar silenciosamente.
|
||||||
@@ -318,7 +318,7 @@ describe('AuthService - logout', () => {
|
|||||||
).toHaveBeenCalledWith('token.with.blacklist.error');
|
).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.
|
* Cenário: Garantir ordem correta das chamadas.
|
||||||
* Problema: Pode adicionar sem verificar primeiro.
|
* Problema: Pode adicionar sem verificar primeiro.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.
|
|||||||
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { JwtPayload } from '../models/jwt-payload.model';
|
import { JwtPayload } from '../models/jwt-payload.model';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TokenBlacklistService {
|
export class TokenBlacklistService {
|
||||||
@@ -64,7 +65,6 @@ export class TokenBlacklistService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private hashToken(token: string): string {
|
private hashToken(token: string): string {
|
||||||
const crypto = require('crypto');
|
|
||||||
return crypto
|
return crypto
|
||||||
.createHash('sha256')
|
.createHash('sha256')
|
||||||
.update(token)
|
.update(token)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { UserRepository } from '../users/UserRepository';
|
import { UserRepository } from '../users/UserRepository';
|
||||||
import md5 = require('md5');
|
import * as md5 from 'md5';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChangePasswordService {
|
export class ChangePasswordService {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class RequestSanitizerMiddleware implements NestMiddleware {
|
|||||||
|
|
||||||
private sanitizeString(str: string): string {
|
private sanitizeString(str: string): string {
|
||||||
// Remover tags HTML básicas
|
// Remover tags HTML básicas
|
||||||
str = str.replace(/<(|\/|[^>\/bi]|\/[^>bi]|[^\/>][^>]+|\/[^>][^>]+)>/g, '');
|
str = str.replace(/<[^>]*>/g, '');
|
||||||
|
|
||||||
// Remover scripts JavaScript
|
// Remover scripts JavaScript
|
||||||
str = str.replace(/javascript:/g, '');
|
str = str.replace(/javascript:/g, '');
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ export const databaseConfig = registerAs('database', () => ({
|
|||||||
username: process.env.ORACLE_USER,
|
username: process.env.ORACLE_USER,
|
||||||
password: process.env.ORACLE_PASSWORD,
|
password: process.env.ORACLE_PASSWORD,
|
||||||
},
|
},
|
||||||
postgres: {
|
// postgres: {
|
||||||
host: process.env.POSTGRES_HOST,
|
// host: process.env.POSTGRES_HOST,
|
||||||
port: parseInt(process.env.POSTGRES_PORT || '5432', 10),
|
// port: parseInt(process.env.POSTGRES_PORT || '5432', 10),
|
||||||
username: process.env.POSTGRES_USER,
|
// username: process.env.POSTGRES_USER,
|
||||||
password: process.env.POSTGRES_PASSWORD,
|
// password: process.env.POSTGRES_PASSWORD,
|
||||||
database: process.env.POSTGRES_DB,
|
// database: process.env.POSTGRES_DB,
|
||||||
},
|
// },
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { createOracleConfig } from '../core/configs/typeorm.oracle.config';
|
import { createOracleConfig } from '../core/configs/typeorm.oracle.config';
|
||||||
import { createPostgresConfig } from '../core/configs/typeorm.postgres.config';
|
|
||||||
import { CarOutDelivery } from '../core/models/car-out-delivery.model';
|
import { CarOutDelivery } from '../core/models/car-out-delivery.model';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { CarInDelivery } from '../core/models/car-in-delivery.model';
|
import { CarInDelivery } from '../core/models/car-in-delivery.model';
|
||||||
@@ -11,6 +10,14 @@ export class LogisticService {
|
|||||||
constructor(private readonly configService: ConfigService) {}
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
async getExpedicao() {
|
async getExpedicao() {
|
||||||
|
// Postgres desativado: este endpoint dependia do WMS (Postgres).
|
||||||
|
// Mantido como referencia, mas sem executar queries.
|
||||||
|
throw new HttpException(
|
||||||
|
'Integracao com WMS (Postgres) desativada. Conexao com Postgres removida.',
|
||||||
|
HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
@@ -97,6 +104,7 @@ export class LogisticService {
|
|||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
await dataSource.destroy();
|
await dataSource.destroy();
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeliveries(_placa: string) {
|
async getDeliveries(_placa: string) {
|
||||||
@@ -142,7 +150,8 @@ export class LogisticService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getStatusCar(placa: string) {
|
async getStatusCar(placa: string) {
|
||||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
// Postgres desativado: este metodo deve usar Oracle.
|
||||||
|
const dataSource = new DataSource(createOracleConfig(this.configService));
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
@@ -186,7 +195,8 @@ export class LogisticService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createCarOut(data: CarOutDelivery) {
|
async createCarOut(data: CarOutDelivery) {
|
||||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
// Postgres desativado: este fluxo usa SQL Oracle (DUAL/sequence).
|
||||||
|
const dataSource = new DataSource(createOracleConfig(this.configService));
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
@@ -257,7 +267,8 @@ export class LogisticService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createCarIn(data: CarInDelivery) {
|
async createCarIn(data: CarInDelivery) {
|
||||||
const dataSource = new DataSource(createPostgresConfig(this.configService));
|
// Postgres desativado: este fluxo usa SQL Oracle (DUAL/sequence).
|
||||||
|
const dataSource = new DataSource(createOracleConfig(this.configService));
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import { DeliveryCompleted } from '../dto/delivery-completed.dto';
|
|||||||
export class OrdersRepository {
|
export class OrdersRepository {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectDataSource('oracle') private readonly oracleDataSource: DataSource,
|
@InjectDataSource('oracle') private readonly oracleDataSource: DataSource,
|
||||||
@InjectDataSource('postgres')
|
) // Postgres desativado: conexao removida e trechos de uso comentados.
|
||||||
private readonly postgresDataSource: DataSource,
|
// @InjectDataSource('postgres')
|
||||||
) {}
|
// private readonly postgresDataSource: DataSource,
|
||||||
|
{}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca log de transferência por ID do pedido
|
* Busca log de transferência por ID do pedido
|
||||||
@@ -1365,10 +1366,8 @@ WHERE
|
|||||||
}
|
}
|
||||||
async getOrderDeliveries(orderId: string) {
|
async getOrderDeliveries(orderId: string) {
|
||||||
const queryRunnerOracle = this.oracleDataSource.createQueryRunner();
|
const queryRunnerOracle = this.oracleDataSource.createQueryRunner();
|
||||||
const queryRunnerPostgres = this.postgresDataSource.createQueryRunner();
|
|
||||||
|
|
||||||
await queryRunnerOracle.connect();
|
await queryRunnerOracle.connect();
|
||||||
await queryRunnerPostgres.connect();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sqlOracle = `SELECT PCPEDC.CODFILIAL as "storeId"
|
const sqlOracle = `SELECT PCPEDC.CODFILIAL as "storeId"
|
||||||
@@ -1407,75 +1406,8 @@ WHERE
|
|||||||
|
|
||||||
const orders = await queryRunnerOracle.manager.query(sqlOracle);
|
const orders = await queryRunnerOracle.manager.query(sqlOracle);
|
||||||
|
|
||||||
// Consulta no WMS (Postgres) - Modificada para buscar pelo número do pedido
|
// Postgres desativado: consulta no WMS (Postgres) comentada.
|
||||||
const sqlWMS = `
|
// O status retornado aqui fica apenas com base no Oracle.
|
||||||
SELECT p.numero,
|
|
||||||
p.posicao,
|
|
||||||
CASE
|
|
||||||
WHEN p.posicao = 'F' THEN 'FATURADO'
|
|
||||||
WHEN p.posicao = 'C' THEN 'CONCLUIDO'
|
|
||||||
WHEN p.posicao = 'L' THEN 'LIBERADO'
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
|
|
||||||
AND m2.data_inicio_separacao IS NULL
|
|
||||||
) AND p.posicao = 'M' THEN 'MONTADO'
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
|
|
||||||
AND m2.data_inicio_separacao IS NOT NULL
|
|
||||||
AND m2.data_fim_separacao IS NULL
|
|
||||||
) AND p.posicao = 'M' THEN 'EM SEPARACAO'
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
|
|
||||||
AND m2.data_fim_separacao IS NOT NULL
|
|
||||||
AND m2.data_inicio_conferencia IS NULL
|
|
||||||
) AND p.posicao = 'M' THEN 'SEPARACAO FINALIZADA'
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
|
|
||||||
AND m2.data_inicio_conferencia IS NOT NULL
|
|
||||||
AND m2.data_fim_conferencia IS NULL
|
|
||||||
) AND p.posicao = 'M' THEN 'EM CONFERENCIA'
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
|
|
||||||
AND m2.data_fim_conferencia IS NOT NULL
|
|
||||||
AND (SELECT COUNT(1) FROM volume v WHERE v.numero_pedido = m2.numero_pedido AND v.data_embarque IS NULL) = 0
|
|
||||||
) AND p.posicao = 'M' THEN 'CONFERENCIA FINALIZADA'
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM movimentacao m2 WHERE m2.numero_pedido = p.numero
|
|
||||||
AND m2.data_fim_conferencia IS NOT NULL
|
|
||||||
AND (SELECT COUNT(1) FROM volume v WHERE v.numero_pedido = m2.numero_pedido AND v.data_embarque IS NULL) > 0
|
|
||||||
) AND p.posicao = 'M' THEN 'EMBARCADO'
|
|
||||||
END as "situacaoPedido"
|
|
||||||
FROM pedido p
|
|
||||||
WHERE p.numero IN (
|
|
||||||
SELECT DISTINCT m.numero_pedido
|
|
||||||
FROM movimentacao m
|
|
||||||
WHERE m.numero_pedido = $1::bigint
|
|
||||||
OR m.numero_pedido IN (
|
|
||||||
SELECT CAST(o.orderId AS bigint) FROM UNNEST($2::text[]) o(orderId)
|
|
||||||
WHERE o.orderId ~ '^[0-9]+$'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Criar array com os IDs de pedidos obtidos do Oracle
|
|
||||||
const orderIds = orders.map((o) => o.orderId?.toString() || '');
|
|
||||||
|
|
||||||
// Converter orderId para número para evitar erro de tipo
|
|
||||||
const numericOrderId = parseInt(orderId, 10);
|
|
||||||
const ordersWMS = await queryRunnerPostgres.manager.query(sqlWMS, [
|
|
||||||
numericOrderId,
|
|
||||||
orderIds,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Atualizar status baseado no WMS
|
|
||||||
for (const order of orders) {
|
|
||||||
const orderWMS = ordersWMS.find(
|
|
||||||
(o) => Number(o.numero) === Number(order.orderId),
|
|
||||||
);
|
|
||||||
if (orderWMS && !order.deliveryConfirmationDate) {
|
|
||||||
order.status = orderWMS.situacaoPedido;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return orders;
|
return orders;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@@ -1485,15 +1417,12 @@ WHERE
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunnerOracle.release();
|
await queryRunnerOracle.release();
|
||||||
await queryRunnerPostgres.release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLeadtimeWMS(orderId: string): Promise<LeadtimeDto[]> {
|
async getLeadtimeWMS(orderId: string): Promise<LeadtimeDto[]> {
|
||||||
const queryRunnerPostgres = this.postgresDataSource.createQueryRunner();
|
|
||||||
const queryRunnerOracle = this.oracleDataSource.createQueryRunner();
|
const queryRunnerOracle = this.oracleDataSource.createQueryRunner();
|
||||||
|
|
||||||
await queryRunnerPostgres.connect();
|
|
||||||
await queryRunnerOracle.connect();
|
await queryRunnerOracle.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1512,84 +1441,14 @@ WHERE
|
|||||||
`;
|
`;
|
||||||
const dataOracle = await queryRunnerOracle.manager.query(sqlOracle);
|
const dataOracle = await queryRunnerOracle.manager.query(sqlOracle);
|
||||||
|
|
||||||
// Consulta no Postgres
|
// Postgres desativado: consulta do leadtime no WMS (Postgres) comentada.
|
||||||
const sqlPostgres = `
|
return dataOracle;
|
||||||
SELECT DADOS.ETAPA as "etapa",
|
|
||||||
DADOS.DESCRICAO_ETAPA as "descricaoEtapa",
|
|
||||||
DADOS.DATA as "data",
|
|
||||||
DADOS.CODIGO_FUNCIONARIO as "codigoFuncionario",
|
|
||||||
DADOS.NOME_FUNCIONARIO as "nomeFuncionario",
|
|
||||||
DADOS.NUMERO_PEDIDO as "numeroPedido"
|
|
||||||
FROM (
|
|
||||||
SELECT 3 AS ETAPA, 'Inicio Separação' AS DESCRICAO_ETAPA,
|
|
||||||
MIN(m.data_inicio_separacao) AS DATA,
|
|
||||||
MAX(m.codigo_separador) AS CODIGO_FUNCIONARIO,
|
|
||||||
(SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_separador)) AS NOME_FUNCIONARIO,
|
|
||||||
m.numero_pedido
|
|
||||||
FROM movimentacao m
|
|
||||||
WHERE m.data_inicio_separacao >= '2025-01-01'
|
|
||||||
AND m.numero_pedido > 0
|
|
||||||
AND m.data_inicio_separacao IS NOT NULL
|
|
||||||
GROUP BY m.numero_pedido
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT 4, 'Separado', MIN(m.data_fim_separacao), MAX(m.codigo_separador),
|
|
||||||
(SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_separador)), m.numero_pedido
|
|
||||||
FROM movimentacao m
|
|
||||||
WHERE m.data_inicio_separacao >= '2025-01-01'
|
|
||||||
AND m.numero_pedido > 0
|
|
||||||
AND m.data_fim_separacao IS NOT NULL
|
|
||||||
GROUP BY m.numero_pedido
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT 5, 'Inicio Conferência', MIN(m.data_inicio_conferencia), MAX(m.codigo_conferente),
|
|
||||||
(SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_conferente)), m.numero_pedido
|
|
||||||
FROM movimentacao m
|
|
||||||
WHERE m.data_inicio_conferencia IS NOT NULL
|
|
||||||
AND m.numero_pedido > 0
|
|
||||||
GROUP BY m.numero_pedido
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT 6, 'Fim Conferência', MIN(m.data_fim_conferencia), MAX(m.codigo_conferente),
|
|
||||||
(SELECT u.nome FROM usuario u WHERE u.id = MAX(m.codigo_conferente)), m.numero_pedido
|
|
||||||
FROM movimentacao m
|
|
||||||
WHERE m.data_fim_conferencia IS NOT NULL
|
|
||||||
AND m.numero_pedido > 0
|
|
||||||
GROUP BY m.numero_pedido
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT 7, 'Embarcado', MAX(v.data_embarque), v.usuario_embarque_id,
|
|
||||||
(SELECT u.nome FROM usuario u WHERE u.id = v.usuario_embarque_id), m.numero_pedido
|
|
||||||
FROM movimentacao m
|
|
||||||
JOIN volume v ON m.numero_pedido = v.numero_pedido
|
|
||||||
WHERE v.data_embarque IS NOT NULL
|
|
||||||
AND m.numero_pedido > 0
|
|
||||||
GROUP BY v.usuario_embarque_id, m.numero_pedido
|
|
||||||
) DADOS
|
|
||||||
WHERE DADOS.numero_pedido = $1
|
|
||||||
ORDER BY DADOS.numero_pedido, DADOS.ETAPA;
|
|
||||||
`;
|
|
||||||
const dataPostgres = await queryRunnerPostgres.manager.query(
|
|
||||||
sqlPostgres,
|
|
||||||
[orderId],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Junta os dados Oracle + Postgres
|
|
||||||
const leadtime = [...dataOracle, ...dataPostgres];
|
|
||||||
|
|
||||||
// Ordena pela etapa (opcional, para garantir ordem)
|
|
||||||
return leadtime.sort((a, b) => a.etapa - b.etapa);
|
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
'Erro ao buscar dados de leadtime do WMS',
|
'Erro ao buscar dados de leadtime do WMS',
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunnerPostgres.release();
|
|
||||||
await queryRunnerOracle.release();
|
await queryRunnerOracle.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
317
src/products/__tests__/products.controller.spec.ts
Normal file
317
src/products/__tests__/products.controller.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
36
src/products/dto/oferta-8026-query.dto.ts
Normal file
36
src/products/dto/oferta-8026-query.dto.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsArray, IsNotEmpty, IsNumber, IsString, Matches } from 'class-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO para requisição de ofertas 8026
|
||||||
|
*/
|
||||||
|
export class Oferta8026QueryDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Data de início da vigência da promoção (formato DD/MM/YYYY)',
|
||||||
|
example: '19/11/2025',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^\d{2}\/\d{2}\/\d{4}$/, {
|
||||||
|
message: 'Data deve estar no formato DD/MM/YYYY',
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
data: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Array de códigos de produtos',
|
||||||
|
example: [62602],
|
||||||
|
type: [Number],
|
||||||
|
})
|
||||||
|
@IsArray()
|
||||||
|
@IsNotEmpty()
|
||||||
|
codprod: number[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Número da região',
|
||||||
|
example: 1,
|
||||||
|
})
|
||||||
|
@IsNumber()
|
||||||
|
@IsNotEmpty()
|
||||||
|
numregiao: number;
|
||||||
|
}
|
||||||
|
|
||||||
76
src/products/dto/oferta-8026-response.dto.ts
Normal file
76
src/products/dto/oferta-8026-response.dto.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO para resposta de ofertas 8026
|
||||||
|
*/
|
||||||
|
export class Oferta8026ResponseDto {
|
||||||
|
@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: 'Percentual de desconto',
|
||||||
|
example: 20,
|
||||||
|
})
|
||||||
|
percdesconto: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Data de fim da vigência',
|
||||||
|
example: '2024-12-31',
|
||||||
|
})
|
||||||
|
dtfimvigencia: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Mensagem para débito',
|
||||||
|
example: 'DEBITO',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
mensagem2: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Mensagem para à vista',
|
||||||
|
example: 'À VISTA',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
mensagem3: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Mensagem para 10x',
|
||||||
|
example: '10X',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
mensagem4: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
127
src/products/dto/placa-8122-response.dto.ts
Normal file
127
src/products/dto/placa-8122-response.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -27,6 +27,9 @@ import { ProductDetailResponseDto } from './dto/product-detail-response.dto';
|
|||||||
import { RotinaA4QueryDto } from './dto/rotina-a4-query.dto';
|
import { RotinaA4QueryDto } from './dto/rotina-a4-query.dto';
|
||||||
import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto';
|
import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto';
|
||||||
import { UnifiedProductSearchDto } from './dto/unified-product-search.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()
|
//@ApiBearerAuth()
|
||||||
//@UseGuards(JwtAuthGuard)
|
//@UseGuards(JwtAuthGuard)
|
||||||
@@ -151,4 +154,42 @@ export class ProductsController {
|
|||||||
): Promise<ProductDetailResponseDto[]> {
|
): Promise<ProductDetailResponseDto[]> {
|
||||||
return this.productsService.unifiedProductSearch(query);
|
return this.productsService.unifiedProductSearch(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint para buscar ofertas 8026
|
||||||
|
*/
|
||||||
|
@Post('oferta-8026')
|
||||||
|
@ApiOperation({ summary: 'Busca ofertas 8026 conforme parâmetros específicos' })
|
||||||
|
@ApiBody({ type: Oferta8026QueryDto })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Oferta 8026 retornada com sucesso.',
|
||||||
|
type: Oferta8026ResponseDto,
|
||||||
|
isArray: true,
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: 'Parâmetros inválidos.' })
|
||||||
|
async getOferta8026(
|
||||||
|
@Body() query: Oferta8026QueryDto,
|
||||||
|
): 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { ProductDetailResponseDto } from './dto/product-detail-response.dto';
|
|||||||
import { RotinaA4QueryDto } from './dto/rotina-a4-query.dto';
|
import { RotinaA4QueryDto } from './dto/rotina-a4-query.dto';
|
||||||
import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto';
|
import { RotinaA4ResponseDto } from './dto/rotina-a4-response.dto';
|
||||||
import { UnifiedProductSearchDto } from './dto/unified-product-search.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()
|
@Injectable()
|
||||||
export class ProductsService {
|
export class ProductsService {
|
||||||
@@ -322,9 +325,6 @@ export class ProductsService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Busca unificada de produtos por nome, código de barras ou codprod
|
|
||||||
*/
|
|
||||||
async unifiedProductSearch(
|
async unifiedProductSearch(
|
||||||
query: UnifiedProductSearchDto,
|
query: UnifiedProductSearchDto,
|
||||||
): Promise<ProductDetailResponseDto[]> {
|
): Promise<ProductDetailResponseDto[]> {
|
||||||
@@ -548,4 +548,239 @@ export class ProductsService {
|
|||||||
|
|
||||||
return produto;
|
return produto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOferta8026(
|
||||||
|
query: Oferta8026QueryDto,
|
||||||
|
): Promise<Oferta8026ResponseDto[]> {
|
||||||
|
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++;
|
||||||
|
});
|
||||||
|
|
||||||
|
params.push(numregiao);
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
pctabpr.codprod,
|
||||||
|
pcprodut.descricao,
|
||||||
|
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 THEN '10X' ELSE NULL END mensagem4
|
||||||
|
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 TRUNC(pcprecoprom.dtiniciovigencia) = TRUNC(TO_DATE(:0, 'DD/MM/YYYY'))
|
||||||
|
AND pcprecoprom.codprod IN (${codprodPlaceholders.join(',')})
|
||||||
|
AND PCPRECOPROM.DTFIMVIGENCIA >= TRUNC(SYSDATE)
|
||||||
|
AND pcprecoprom.codplpagmax = 42
|
||||||
|
AND pctabpr.numregiao = :${paramIndex}
|
||||||
|
`;
|
||||||
|
|
||||||
|
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,
|
||||||
|
codauxiliar: row.CODAUXILIAR,
|
||||||
|
precofixo: row.PRECOFIXO,
|
||||||
|
percdesconto: row.PERCDESCONTO,
|
||||||
|
dtfimvigencia: row.DTFIMVIGENCIA,
|
||||||
|
mensagem2: row.MENSAGEM2,
|
||||||
|
mensagem3: row.MENSAGEM3,
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user