Adiciona busca por codauxiliar em findProducts e cria API de busca unificada
- Modifica findProducts para buscar por CODPROD e CODAUXILIAR - Adiciona testes para o método products - Cria endpoint unified-search para busca unificada por nome, código de barras ou codprod - Adiciona @IsOptional aos campos opcionais do ProductDetailQueryDto - Adiciona testes para products.service
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
import { DataConsultService } from '../data-consult.service';
|
import { DataConsultService } from '../data-consult.service';
|
||||||
import { DataConsultRepository } from '../data-consult.repository';
|
import { DataConsultRepository } from '../data-consult.repository';
|
||||||
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
import { IRedisClient } from '../../core/configs/cache/IRedisClient';
|
||||||
@@ -14,6 +15,8 @@ export const createMockRepository = (
|
|||||||
findSellers: jest.fn(),
|
findSellers: jest.fn(),
|
||||||
findBillings: jest.fn(),
|
findBillings: jest.fn(),
|
||||||
findCustomers: jest.fn(),
|
findCustomers: jest.fn(),
|
||||||
|
findProducts: jest.fn(),
|
||||||
|
findProductsByCodauxiliar: jest.fn(),
|
||||||
findAllProducts: jest.fn(),
|
findAllProducts: jest.fn(),
|
||||||
findAllCarriers: jest.fn(),
|
findAllCarriers: jest.fn(),
|
||||||
findRegions: jest.fn(),
|
findRegions: jest.fn(),
|
||||||
@@ -31,6 +34,9 @@ export interface DataConsultServiceTestContext {
|
|||||||
mockRepository: jest.Mocked<DataConsultRepository>;
|
mockRepository: jest.Mocked<DataConsultRepository>;
|
||||||
mockRedisClient: jest.Mocked<IRedisClient>;
|
mockRedisClient: jest.Mocked<IRedisClient>;
|
||||||
mockDataSource: jest.Mocked<DataSource>;
|
mockDataSource: jest.Mocked<DataSource>;
|
||||||
|
mockLogger: {
|
||||||
|
error: jest.Mock;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDataConsultServiceTestModule(
|
export async function createDataConsultServiceTestModule(
|
||||||
@@ -64,10 +70,21 @@ export async function createDataConsultServiceTestModule(
|
|||||||
|
|
||||||
const service = module.get<DataConsultService>(DataConsultService);
|
const service = module.get<DataConsultService>(DataConsultService);
|
||||||
|
|
||||||
|
const mockLogger = {
|
||||||
|
error: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(Logger.prototype, 'error').mockImplementation(
|
||||||
|
(message: any, ...optionalParams: any[]) => {
|
||||||
|
mockLogger.error(message, ...optionalParams);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
service,
|
service,
|
||||||
mockRepository,
|
mockRepository,
|
||||||
mockRedisClient,
|
mockRedisClient,
|
||||||
mockDataSource,
|
mockDataSource,
|
||||||
|
mockLogger,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,23 @@ describe('DataConsultService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should filter out sellers with null id', async () => {
|
||||||
|
context.mockRepository.findSellers.mockResolvedValue([
|
||||||
|
{ id: null, name: 'Vendedor 1' },
|
||||||
|
{ id: '002', name: 'Vendedor 2' },
|
||||||
|
{ id: null, name: 'Vendedor 3' },
|
||||||
|
] as any);
|
||||||
|
|
||||||
|
const result = await context.service.sellers();
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].id).toBe('002');
|
||||||
|
expect(result[0].name).toBe('Vendedor 2');
|
||||||
|
result.forEach((seller) => {
|
||||||
|
expect(seller.id).not.toBeNull();
|
||||||
|
expect(seller.id).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should log error when repository throws exception', async () => {
|
it('should log error when repository throws exception', async () => {
|
||||||
const repositoryError = new Error('Database connection failed');
|
const repositoryError = new Error('Database connection failed');
|
||||||
context.mockRepository.findSellers.mockRejectedValue(repositoryError);
|
context.mockRepository.findSellers.mockRejectedValue(repositoryError);
|
||||||
@@ -510,4 +527,124 @@ describe('DataConsultService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('products', () => {
|
||||||
|
let context: Awaited<ReturnType<typeof createDataConsultServiceTestModule>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
context = await createDataConsultServiceTestModule();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tests that expose problems', () => {
|
||||||
|
it('should search products by CODPROD', async () => {
|
||||||
|
context.mockRepository.findProducts.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: '12345',
|
||||||
|
name: 'PRODUTO EXEMPLO',
|
||||||
|
manufacturerCode: 'FAB001',
|
||||||
|
},
|
||||||
|
] as any);
|
||||||
|
|
||||||
|
const result = await context.service.products('12345');
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].id).toBe('12345');
|
||||||
|
expect(result[0].name).toBe('PRODUTO EXEMPLO');
|
||||||
|
expect(context.mockRepository.findProducts).toHaveBeenCalledWith(
|
||||||
|
'12345',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search products by CODAUXILIAR', async () => {
|
||||||
|
context.mockRepository.findProducts.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: '12345',
|
||||||
|
name: 'PRODUTO EXEMPLO',
|
||||||
|
manufacturerCode: 'FAB001',
|
||||||
|
},
|
||||||
|
] as any);
|
||||||
|
|
||||||
|
const result = await context.service.products('7891234567890');
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].id).toBe('12345');
|
||||||
|
expect(context.mockRepository.findProducts).toHaveBeenCalledWith(
|
||||||
|
'7891234567890',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search products by CODPROD or CODAUXILIAR', async () => {
|
||||||
|
context.mockRepository.findProducts.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: '12345',
|
||||||
|
name: 'PRODUTO EXEMPLO',
|
||||||
|
manufacturerCode: 'FAB001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12346',
|
||||||
|
name: 'OUTRO PRODUTO',
|
||||||
|
manufacturerCode: 'FAB002',
|
||||||
|
},
|
||||||
|
] as any);
|
||||||
|
|
||||||
|
const result = await context.service.products('12345');
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].id).toBe('12345');
|
||||||
|
expect(result[1].id).toBe('12346');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty result from repository', async () => {
|
||||||
|
context.mockRepository.findProducts.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await context.service.products('99999');
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate that all products have required properties (id, name)', async () => {
|
||||||
|
context.mockRepository.findProducts.mockResolvedValue([
|
||||||
|
{ id: '12345', name: 'PRODUTO 1' },
|
||||||
|
{ id: '12346', name: 'PRODUTO 2' },
|
||||||
|
{ id: '12347', name: 'PRODUTO 3' },
|
||||||
|
] as any);
|
||||||
|
|
||||||
|
const result = await context.service.products('12345');
|
||||||
|
|
||||||
|
result.forEach((product) => {
|
||||||
|
expect(product.id).toBeDefined();
|
||||||
|
expect(product.name).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when filter is invalid', async () => {
|
||||||
|
await expect(
|
||||||
|
context.service.products(null as any),
|
||||||
|
).rejects.toThrow(HttpException);
|
||||||
|
await expect(
|
||||||
|
context.service.products(undefined as any),
|
||||||
|
).rejects.toThrow(HttpException);
|
||||||
|
await expect(
|
||||||
|
context.service.products('' as any),
|
||||||
|
).rejects.toThrow(HttpException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log error when repository throws exception', async () => {
|
||||||
|
const repositoryError = new Error('Database connection failed');
|
||||||
|
context.mockRepository.findProducts.mockRejectedValue(repositoryError);
|
||||||
|
await expect(context.service.products('12345')).rejects.toThrow(
|
||||||
|
HttpException,
|
||||||
|
);
|
||||||
|
expect(context.mockLogger.error).toHaveBeenCalledWith(
|
||||||
|
'Erro ao buscar produtos',
|
||||||
|
repositoryError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,13 +116,15 @@ export class DataConsultRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findProducts(filter: string): Promise<ProductDto[]> {
|
async findProducts(filter: string): Promise<ProductDto[]> {
|
||||||
|
const cleanedFilter = filter.replace(/\D/g, '');
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT PCPRODUT.CODPROD as "id",
|
SELECT PCPRODUT.CODPROD as "id",
|
||||||
PCPRODUT.CODPROD || ' - ' || PCPRODUT.DESCRICAO || ' ( ' || PCPRODUT.CODFAB || ' )' as "description"
|
PCPRODUT.CODPROD || ' - ' || PCPRODUT.DESCRICAO || ' ( ' || PCPRODUT.CODFAB || ' )' as "description"
|
||||||
FROM PCPRODUT
|
FROM PCPRODUT
|
||||||
WHERE PCPRODUT.CODPROD = :filter
|
WHERE PCPRODUT.CODPROD = :0
|
||||||
|
OR REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '') = :1
|
||||||
`;
|
`;
|
||||||
const results = await this.executeQuery<ProductDto[]>(sql, [filter]);
|
const results = await this.executeQuery<ProductDto[]>(sql, [filter, cleanedFilter]);
|
||||||
return results.map((result) => new ProductDto(result));
|
return results.map((result) => new ProductDto(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
src/products/__tests__/products.service.spec.helper.ts
Normal file
41
src/products/__tests__/products.service.spec.helper.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ProductsService } from '../products.service';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
export const createMockDataSource = () =>
|
||||||
|
({
|
||||||
|
query: jest.fn(),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
export interface ProductsServiceTestContext {
|
||||||
|
service: ProductsService;
|
||||||
|
mockDataSource: jest.Mocked<DataSource>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createProductsServiceTestModule(
|
||||||
|
dataSourceMethods: Partial<DataSource> = {},
|
||||||
|
): Promise<ProductsServiceTestContext> {
|
||||||
|
const mockDataSource = {
|
||||||
|
...createMockDataSource(),
|
||||||
|
...dataSourceMethods,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ProductsService,
|
||||||
|
{
|
||||||
|
provide: getDataSourceToken('oracle'),
|
||||||
|
useValue: mockDataSource,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
const service = module.get<ProductsService>(ProductsService);
|
||||||
|
|
||||||
|
return {
|
||||||
|
service,
|
||||||
|
mockDataSource,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
175
src/products/__tests__/products.service.spec.ts
Normal file
175
src/products/__tests__/products.service.spec.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { HttpException } from '@nestjs/common';
|
||||||
|
import { createProductsServiceTestModule } from './products.service.spec.helper';
|
||||||
|
import { ProductDetailQueryDto } from '../dto/product-detail-query.dto';
|
||||||
|
import { ProductDetailResponseDto } from '../dto/product-detail-response.dto';
|
||||||
|
|
||||||
|
describe('ProductsService', () => {
|
||||||
|
describe('getProductDetails', () => {
|
||||||
|
let context: Awaited<ReturnType<typeof createProductsServiceTestModule>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
context = await createProductsServiceTestModule();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('busca por codauxiliar', () => {
|
||||||
|
it('deve buscar produtos por codauxiliar com sucesso', async () => {
|
||||||
|
const query: ProductDetailQueryDto = {
|
||||||
|
numregiao: 1,
|
||||||
|
codauxiliar: ['7891234567890', '7891234567891'],
|
||||||
|
codfilial: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProducts = [
|
||||||
|
{
|
||||||
|
codprod: 12345,
|
||||||
|
descricao: 'PRODUTO 1 - MARCA 1',
|
||||||
|
embalagem: 'UN',
|
||||||
|
codauxiliar: '7891234567890',
|
||||||
|
marca: 'MARCA 1',
|
||||||
|
preco: 99.9,
|
||||||
|
filial: 'FILIAL MATRIZ',
|
||||||
|
regiao: 'REGIÃO SUL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codprod: 12346,
|
||||||
|
descricao: 'PRODUTO 2 - MARCA 2',
|
||||||
|
embalagem: 'UN',
|
||||||
|
codauxiliar: '7891234567891',
|
||||||
|
marca: 'MARCA 2',
|
||||||
|
preco: 149.9,
|
||||||
|
filial: 'FILIAL MATRIZ',
|
||||||
|
regiao: 'REGIÃO SUL',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
context.mockDataSource.query.mockResolvedValue(mockProducts);
|
||||||
|
|
||||||
|
const result = await context.service.getProductDetails(query);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].codprod).toBe(12345);
|
||||||
|
expect(result[0].codauxiliar).toBe('7891234567890');
|
||||||
|
expect(result[0].descricao).toBe('PRODUTO 1 - MARCA 1');
|
||||||
|
expect(result[0].preco).toBe('99,90');
|
||||||
|
expect(result[1].codprod).toBe(12346);
|
||||||
|
expect(result[1].codauxiliar).toBe('7891234567891');
|
||||||
|
expect(result[1].preco).toBe('149,90');
|
||||||
|
|
||||||
|
expect(context.mockDataSource.query).toHaveBeenCalledTimes(1);
|
||||||
|
const callArgs = context.mockDataSource.query.mock.calls[0];
|
||||||
|
expect(callArgs[0]).toContain('REGEXP_REPLACE(PCPRODUT.CODAUXILIAR');
|
||||||
|
expect(callArgs[1]).toContain(1);
|
||||||
|
expect(callArgs[1]).toContain('1');
|
||||||
|
expect(callArgs[1]).toContain('7891234567890');
|
||||||
|
expect(callArgs[1]).toContain('7891234567891');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve remover caracteres não numéricos do codauxiliar na query', async () => {
|
||||||
|
const query: ProductDetailQueryDto = {
|
||||||
|
numregiao: 1,
|
||||||
|
codauxiliar: ['789.123.456.789-0', '789-123-456-789-1'],
|
||||||
|
codfilial: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProducts = [
|
||||||
|
{
|
||||||
|
codprod: 12345,
|
||||||
|
descricao: 'PRODUTO 1 - MARCA 1',
|
||||||
|
embalagem: 'UN',
|
||||||
|
codauxiliar: '7891234567890',
|
||||||
|
marca: 'MARCA 1',
|
||||||
|
preco: 99.9,
|
||||||
|
filial: 'FILIAL MATRIZ',
|
||||||
|
regiao: 'REGIÃO SUL',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
context.mockDataSource.query.mockResolvedValue(mockProducts);
|
||||||
|
|
||||||
|
const result = await context.service.getProductDetails(query);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(context.mockDataSource.query).toHaveBeenCalledTimes(1);
|
||||||
|
const callArgs = context.mockDataSource.query.mock.calls[0];
|
||||||
|
expect(callArgs[1]).toContain('7891234567890');
|
||||||
|
expect(callArgs[1]).toContain('7891234567891');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve retornar array vazio quando nenhum produto é encontrado', async () => {
|
||||||
|
const query: ProductDetailQueryDto = {
|
||||||
|
numregiao: 1,
|
||||||
|
codauxiliar: ['9999999999999'],
|
||||||
|
codfilial: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
context.mockDataSource.query.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await context.service.getProductDetails(query);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
expect(context.mockDataSource.query).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve formatar o preço corretamente', async () => {
|
||||||
|
const query: ProductDetailQueryDto = {
|
||||||
|
numregiao: 1,
|
||||||
|
codauxiliar: ['7891234567890'],
|
||||||
|
codfilial: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProducts = [
|
||||||
|
{
|
||||||
|
codprod: 12345,
|
||||||
|
descricao: 'PRODUTO 1 - MARCA 1',
|
||||||
|
embalagem: 'UN',
|
||||||
|
codauxiliar: '7891234567890',
|
||||||
|
marca: 'MARCA 1',
|
||||||
|
preco: 1234.56,
|
||||||
|
filial: 'FILIAL MATRIZ',
|
||||||
|
regiao: 'REGIÃO SUL',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
context.mockDataSource.query.mockResolvedValue(mockProducts);
|
||||||
|
|
||||||
|
const result = await context.service.getProductDetails(query);
|
||||||
|
|
||||||
|
expect(result[0].preco).toBe('1.234,56');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar exceção quando codprod e codauxiliar não são informados', async () => {
|
||||||
|
const query: ProductDetailQueryDto = {
|
||||||
|
numregiao: 1,
|
||||||
|
codfilial: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
context.service.getProductDetails(query),
|
||||||
|
).rejects.toThrow(HttpException);
|
||||||
|
await expect(
|
||||||
|
context.service.getProductDetails(query),
|
||||||
|
).rejects.toThrow('É necessário informar codprod ou codauxiliar.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve lançar exceção quando codauxiliar é array vazio', async () => {
|
||||||
|
const query: ProductDetailQueryDto = {
|
||||||
|
numregiao: 1,
|
||||||
|
codauxiliar: [],
|
||||||
|
codfilial: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
context.service.getProductDetails(query),
|
||||||
|
).rejects.toThrow(HttpException);
|
||||||
|
await expect(
|
||||||
|
context.service.getProductDetails(query),
|
||||||
|
).rejects.toThrow('É necessário informar codprod ou codauxiliar.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsArray, IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO para requisição de detalhes de produtos
|
* DTO para requisição de detalhes de produtos
|
||||||
@@ -19,6 +19,7 @@ export class ProductDetailQueryDto {
|
|||||||
type: [Number],
|
type: [Number],
|
||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
codprod?: number[];
|
codprod?: number[];
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export class ProductDetailQueryDto {
|
|||||||
type: [String],
|
type: [String],
|
||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
codauxiliar?: string[];
|
codauxiliar?: string[];
|
||||||
|
|
||||||
|
|||||||
32
src/products/dto/unified-product-search.dto.ts
Normal file
32
src/products/dto/unified-product-search.dto.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO para busca unificada de produtos
|
||||||
|
*/
|
||||||
|
export class UnifiedProductSearchDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Termo de busca (nome, código de barras ou codprod)',
|
||||||
|
example: '7891234567890',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
search: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Código da região para buscar o preço',
|
||||||
|
example: 1,
|
||||||
|
})
|
||||||
|
@IsNumber()
|
||||||
|
@IsNotEmpty()
|
||||||
|
numregiao: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Código da filial',
|
||||||
|
example: '1',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
codfilial: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import { ProductDetailQueryDto } from './dto/product-detail-query.dto';
|
|||||||
import { ProductDetailResponseDto } from './dto/product-detail-response.dto';
|
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';
|
||||||
|
|
||||||
|
|
||||||
//@ApiBearerAuth()
|
//@ApiBearerAuth()
|
||||||
@@ -95,4 +96,21 @@ export class ProductsController {
|
|||||||
async getRotinaA4(@Body() query: RotinaA4QueryDto): Promise<RotinaA4ResponseDto> {
|
async getRotinaA4(@Body() query: RotinaA4QueryDto): Promise<RotinaA4ResponseDto> {
|
||||||
return this.productsService.getRotinaA4(query);
|
return this.productsService.getRotinaA4(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint para busca unificada de produtos por nome, código de barras ou codprod
|
||||||
|
*/
|
||||||
|
@Post('unified-search')
|
||||||
|
@ApiOperation({ summary: 'Busca unificada de produtos por nome, código de barras ou codprod' })
|
||||||
|
@ApiBody({ type: UnifiedProductSearchDto })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de produtos encontrados retornada com sucesso.',
|
||||||
|
type: ProductDetailResponseDto,
|
||||||
|
isArray: true
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: 'Parâmetros inválidos.' })
|
||||||
|
async unifiedProductSearch(@Body() query: UnifiedProductSearchDto): Promise<ProductDetailResponseDto[]> {
|
||||||
|
return this.productsService.unifiedProductSearch(query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ProductDetailQueryDto } from './dto/product-detail-query.dto';
|
|||||||
import { ProductDetailResponseDto } from './dto/product-detail-response.dto';
|
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';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProductsService {
|
export class ProductsService {
|
||||||
@@ -306,6 +307,83 @@ export class ProductsService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca unificada de produtos por nome, código de barras ou codprod
|
||||||
|
*/
|
||||||
|
async unifiedProductSearch(
|
||||||
|
query: UnifiedProductSearchDto,
|
||||||
|
): Promise<ProductDetailResponseDto[]> {
|
||||||
|
const { search, numregiao, codfilial } = query;
|
||||||
|
|
||||||
|
if (!search || search.trim().length === 0) {
|
||||||
|
throw new HttpException(
|
||||||
|
'É necessário informar um termo de busca.',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTerm = search.trim();
|
||||||
|
const numericOnly = searchTerm.replace(/\D/g, '');
|
||||||
|
const isNumeric = numericOnly.length > 0 && /^\d+$/.test(numericOnly);
|
||||||
|
|
||||||
|
const baseParams: any[] = [numregiao, codfilial, numregiao];
|
||||||
|
let whereCondition: string;
|
||||||
|
let params: any[];
|
||||||
|
|
||||||
|
if (isNumeric) {
|
||||||
|
const numericSearch = numericOnly;
|
||||||
|
whereCondition = `(
|
||||||
|
PCPRODUT.CODPROD = :4
|
||||||
|
OR REGEXP_REPLACE(PCPRODUT.CODAUXILIAR, '[^0-9]', '') = :5
|
||||||
|
OR PCPRODUT.DESCRICAO LIKE '%' || :6 || '%'
|
||||||
|
)`;
|
||||||
|
params = [
|
||||||
|
...baseParams,
|
||||||
|
parseInt(numericSearch, 10),
|
||||||
|
numericSearch,
|
||||||
|
searchTerm,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
whereCondition = `PCPRODUT.DESCRICAO LIKE '%' || :4 || '%'`;
|
||||||
|
params = [...baseParams, searchTerm];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
PCPRODUT.CODPROD AS "codprod",
|
||||||
|
PCPRODUT.DESCRICAO || ' - ' || PCMARCA.MARCA AS "descricao",
|
||||||
|
PCPRODUT.EMBALAGEM AS "embalagem",
|
||||||
|
PCPRODUT.CODAUXILIAR AS "codauxiliar",
|
||||||
|
PCMARCA.MARCA AS "marca",
|
||||||
|
(
|
||||||
|
SELECT PCTABPR.PVENDA1
|
||||||
|
FROM PCTABPR
|
||||||
|
WHERE PCTABPR.CODPROD = PCPRODUT.CODPROD
|
||||||
|
AND PCTABPR.NUMREGIAO = :1
|
||||||
|
) AS "preco",
|
||||||
|
(
|
||||||
|
SELECT TRIM(REPLACE(RAZAOSOCIAL, 'LTDA', ''))
|
||||||
|
FROM PCFILIAL F
|
||||||
|
WHERE CODIGO = :2
|
||||||
|
) AS "filial",
|
||||||
|
(
|
||||||
|
SELECT REGIAO
|
||||||
|
FROM PCREGIAO
|
||||||
|
WHERE NUMREGIAO = :3
|
||||||
|
) AS "regiao"
|
||||||
|
FROM PCPRODUT
|
||||||
|
LEFT JOIN PCMARCA ON PCPRODUT.CODMARCA = PCMARCA.CODMARCA
|
||||||
|
WHERE ${whereCondition}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const products = await this.dataSource.query(sql, params);
|
||||||
|
|
||||||
|
return products.map((product) => ({
|
||||||
|
...product,
|
||||||
|
preco: this.formatarMoedaBrasileira(product.preco),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private buildRotinaA4WhereCondition(
|
private buildRotinaA4WhereCondition(
|
||||||
codprod: number | undefined,
|
codprod: number | undefined,
|
||||||
codauxiliar: string | undefined,
|
codauxiliar: string | undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user