This commit is contained in:
Joelson
2025-09-17 18:49:23 -03:00
parent 21c3225c52
commit e081df9ced
42 changed files with 4129 additions and 411 deletions

View File

@@ -0,0 +1,21 @@
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
import { Controller, Get, Param } from '@nestjs/common';
import { clientesService } from './clientes.service';
@ApiTags('clientes')
@Controller('api/v1/')
export class clientesController {
constructor(private readonly clientesService: clientesService) {}
@Get('clientes/:filter')
async customer(@Param('filter') filter: string) {
return this.clientesService.customers(filter);
}
}

View File

@@ -0,0 +1,19 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { clientesService } from './clientes.service';
import { clientesController } from './clientes.controller';
/*
https://docs.nestjs.com/modules
*/
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [
clientesController,],
providers: [
clientesService,],
})
export class clientes { }

View File

@@ -0,0 +1,154 @@
import { Injectable, Inject } from '@nestjs/common';
import { QueryRunner, DataSource } from 'typeorm';
import { DATA_SOURCE } from '../core/constants';
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
import { IRedisClient } from '../core/configs/cache/IRedisClient';
import { getOrSetCache } from '../shared/cache.util';
@Injectable()
export class clientesService {
private readonly CUSTOMERS_TTL = 60 * 60 * 12; // 12 horas
private readonly CUSTOMERS_CACHE_KEY = 'clientes:search';
constructor(
@Inject(DATA_SOURCE)
private readonly dataSource: DataSource,
@Inject(RedisClientToken)
private readonly redisClient: IRedisClient,
) {}
/**
* Buscar clientes com cache otimizado
* @param filter - Filtro de busca (código, CPF/CNPJ ou nome)
* @returns Array de clientes encontrados
*/
async customers(filter: string) {
const cacheKey = `${this.CUSTOMERS_CACHE_KEY}:${filter}`;
return getOrSetCache(
this.redisClient,
cacheKey,
this.CUSTOMERS_TTL,
async () => {
const queryRunner: QueryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
try {
// Primeira tentativa: busca por código do cliente
let sql = `SELECT PCCLIENT.CODCLI as "id"
,PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE||
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
,PCCLIENT.ESTCOB as "estcob"
FROM PCCLIENT
WHERE PCCLIENT.CODCLI = REGEXP_REPLACE('${filter}', '[^0-9]', '')
ORDER BY PCCLIENT.CLIENTE`;
let customers = await queryRunner.manager.query(sql);
// Segunda tentativa: busca por CPF/CNPJ se não encontrou por código
if (customers.length === 0) {
sql = `SELECT PCCLIENT.CODCLI as "id",
PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE||
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
,PCCLIENT.ESTCOB as "estcob"
FROM PCCLIENT
WHERE REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '') = REGEXP_REPLACE('${filter}', '[^0-9]', '')
ORDER BY PCCLIENT.CLIENTE`;
customers = await queryRunner.manager.query(sql);
}
// Terceira tentativa: busca por nome do cliente se não encontrou por código ou CPF/CNPJ
if (customers.length === 0) {
sql = `SELECT PCCLIENT.CODCLI as "id",
PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE||
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
,PCCLIENT.ESTCOB as "estcob"
FROM PCCLIENT
WHERE PCCLIENT.CLIENTE LIKE '${filter.toUpperCase().replace('@', '%')}%'
ORDER BY PCCLIENT.CLIENTE`;
customers = await queryRunner.manager.query(sql);
}
return customers;
} finally {
await queryRunner.release();
}
}
);
}
/**
* Buscar todos os clientes com cache
* @returns Array de todos os clientes
*/
async getAllCustomers() {
const cacheKey = 'clientes:all';
return getOrSetCache(
this.redisClient,
cacheKey,
this.CUSTOMERS_TTL,
async () => {
const queryRunner: QueryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
try {
const sql = `SELECT PCCLIENT.CODCLI as "id"
,PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE||
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
,PCCLIENT.ESTCOB as "estcob"
FROM PCCLIENT
ORDER BY PCCLIENT.CLIENTE`;
return await queryRunner.manager.query(sql);
} finally {
await queryRunner.release();
}
}
);
}
/**
* Buscar cliente por ID específico com cache
* @param customerId - ID do cliente
* @returns Cliente encontrado ou null
*/
async getCustomerById(customerId: string) {
const cacheKey = `clientes:id:${customerId}`;
return getOrSetCache(
this.redisClient,
cacheKey,
this.CUSTOMERS_TTL,
async () => {
const queryRunner: QueryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
try {
const sql = `SELECT PCCLIENT.CODCLI as "id"
,PCCLIENT.CODCLI || ' - '|| PCCLIENT.CLIENTE||
' ( '||REGEXP_REPLACE(PCCLIENT.CGCENT, '[^0-9]', '')||' )' as "name"
,PCCLIENT.ESTCOB as "estcob"
FROM PCCLIENT
WHERE PCCLIENT.CODCLI = '${customerId}'`;
const customers = await queryRunner.manager.query(sql);
return customers.length > 0 ? customers[0] : null;
} finally {
await queryRunner.release();
}
}
);
}
/**
* Limpar cache de clientes (útil para invalidação)
* @param pattern - Padrão de chaves para limpar (opcional)
*/
async clearCustomersCache(pattern?: string) {
const cachePattern = pattern || 'clientes:*';
// Nota: Esta funcionalidade requer implementação específica do Redis
// Por enquanto, mantemos a interface para futuras implementações
console.log(`Cache de clientes seria limpo para o padrão: ${cachePattern}`);
}
}

View File

@@ -1,5 +1,5 @@
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
import { Controller, Get, Param, Query, UseGuards, UsePipes, ValidationPipe, ParseIntPipe } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger';
import { DataConsultService } from './data-consult.service';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'
import { ProductDto } from './dto/product.dto';
@@ -7,6 +7,7 @@ import { StoreDto } from './dto/store.dto';
import { SellerDto } from './dto/seller.dto';
import { BillingDto } from './dto/billing.dto';
import { CustomerDto } from './dto/customer.dto';
import { CarrierDto, FindCarriersDto } from './dto/carrier.dto';
@ApiTags('DataConsult')
@Controller('api/v1/data-consult')
@@ -24,7 +25,7 @@ export class DataConsultController {
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiBearerAuth()
@Get('sellers')
@ApiOperation({ summary: 'Lista todos os vendedores' })
@ApiResponse({ status: 200, description: 'Lista de vendedores retornada com sucesso', type: [SellerDto] })
@@ -70,5 +71,36 @@ export class DataConsultController {
async getAllProducts(): Promise<ProductDto[]> {
return this.dataConsultService.getAllProducts();
}
@Get('carriers/all')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Lista todas as transportadoras cadastradas' })
@ApiResponse({ status: 200, description: 'Lista de transportadoras retornada com sucesso', type: [CarrierDto] })
@UsePipes(new ValidationPipe({ transform: true }))
async getAllCarriers(): Promise<CarrierDto[]> {
return this.dataConsultService.getAllCarriers();
}
@Get('carriers')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Busca transportadoras por período de data' })
@ApiResponse({ status: 200, description: 'Lista de transportadoras por período retornada com sucesso', type: [CarrierDto] })
@UsePipes(new ValidationPipe({ transform: true }))
async getCarriersByDate(@Query() query: FindCarriersDto): Promise<CarrierDto[]> {
return this.dataConsultService.getCarriersByDate(query);
}
@Get('carriers/order/:orderId')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Busca transportadoras de um pedido específico' })
@ApiParam({ name: 'orderId', example: 236001388 })
@ApiResponse({ status: 200, description: 'Lista de transportadoras do pedido retornada com sucesso', type: [CarrierDto] })
@UsePipes(new ValidationPipe({ transform: true }))
async getOrderCarriers(@Param('orderId', ParseIntPipe) orderId: number): Promise<CarrierDto[]> {
return this.dataConsultService.getOrderCarriers(orderId);
}
}

View File

@@ -5,9 +5,10 @@ import { DataConsultRepository } from './data-consult.repository';
import { LoggerModule } from 'src/Log/logger.module';
import { ConfigModule } from '@nestjs/config';
import { RedisModule } from 'src/core/configs/cache/redis.module';
import { clientes } from './clientes.module';
@Module({
imports: [LoggerModule, ConfigModule, RedisModule],
imports: [LoggerModule, ConfigModule, RedisModule, clientes],
controllers: [DataConsultController],
providers: [
DataConsultService,

View File

@@ -40,7 +40,7 @@ export class DataConsultRepository {
return results.map(result => new StoreDto(result));
}
async findSellers(): Promise<SellerDto[]> {
async findSellers(): Promise<SellerDto[]> {
const sql = `
SELECT PCUSUARI.CODUSUR as "id",
PCUSUARI.NOME as "name"
@@ -55,51 +55,166 @@ export class DataConsultRepository {
async findBillings(): Promise<BillingDto[]> {
const sql = `
SELECT PCPEDC.NUMPED as "id",
PCPEDC.DATA as "date",
PCPEDC.VLTOTAL as "total"
FROM PCPEDC
WHERE PCPEDC.POSICAO = 'F'
SELECT p.CODCOB, p.COBRANCA FROM PCCOB p
`;
const results = await this.executeQuery<BillingDto[]>(sql);
return results.map(result => new BillingDto(result));
}
async findCustomers(filter: string): Promise<CustomerDto[]> {
const sql = `
SELECT PCCLIENT.CODCLI as "id",
PCCLIENT.CLIENTE as "name",
PCCLIENT.CGCENT as "document"
FROM PCCLIENT
WHERE PCCLIENT.CLIENTE LIKE :filter
OR PCCLIENT.CGCENT LIKE :filter
// 1) limpa todos os não-dígitos para buscas exatas
const cleanedDigits = filter.replace(/\D/g, '');
// 2) prepara filtro para busca por nome (LIKE)
const likeFilter = `%${filter.toUpperCase().replace(/@/g, '%')}%`;
let customers: CustomerDto[] = [];
// --- 1ª tentativa: busca por código do cliente (CODCLI) ---
let sql = `
SELECT
PCCLIENT.CODCLI AS "id",
PCCLIENT.CLIENTE AS "name",
REGEXP_REPLACE(PCCLIENT.CGCENT,'[^0-9]','') AS "document",
PCCLIENT.ESTCOB AS "estcob"
FROM PCCLIENT
WHERE PCCLIENT.CODCLI = :0
ORDER BY PCCLIENT.CLIENTE
`;
const results = await this.executeQuery<CustomerDto[]>(sql, [`%${filter}%`]);
return results.map(result => new CustomerDto(result));
customers = await this.executeQuery<CustomerDto[]>(sql, [cleanedDigits]);
// --- 2ª tentativa: busca por CPF/CNPJ (CGCENT) ---
if (customers.length === 0) {
sql = `
SELECT
PCCLIENT.CODCLI AS "id",
PCCLIENT.CLIENTE AS "name",
REGEXP_REPLACE(PCCLIENT.CGCENT,'[^0-9]','') AS "document",
PCCLIENT.ESTCOB AS "estcob"
FROM PCCLIENT
WHERE REGEXP_REPLACE(PCCLIENT.CGCENT,'[^0-9]','') = :0
ORDER BY PCCLIENT.CLIENTE
`;
customers = await this.executeQuery<CustomerDto[]>(sql, [cleanedDigits]);
}
// --- 3ª tentativa: busca parcial por nome ---
if (customers.length === 0) {
sql = `
SELECT
PCCLIENT.CODCLI AS "id",
PCCLIENT.CLIENTE AS "name",
REGEXP_REPLACE(PCCLIENT.CGCENT,'[^0-9]','') AS "document",
PCCLIENT.ESTCOB AS "estcob"
FROM PCCLIENT
WHERE UPPER(PCCLIENT.CLIENTE) LIKE :0
ORDER BY PCCLIENT.CLIENTE
`;
customers = await this.executeQuery<CustomerDto[]>(sql, [likeFilter]);
}
return customers.map(row => new CustomerDto(row));
}
async findProducts(filter: string): Promise<ProductDto[]> {
const sql = `
SELECT PCPRODUT.CODPROD as "id",
PCPRODUT.DESCRICAO as "name",
PCPRODUT.CODFAB as "manufacturerCode"
PCPRODUT.CODPROD || ' - ' || PCPRODUT.DESCRICAO || ' ( ' || PCPRODUT.CODFAB || ' )' as "description"
FROM PCPRODUT
WHERE PCPRODUT.DESCRICAO LIKE :filter
OR PCPRODUT.CODFAB LIKE :filter
WHERE PCPRODUT.CODPROD = :filter
`;
const results = await this.executeQuery<ProductDto[]>(sql, [`%${filter}%`]);
const results = await this.executeQuery<ProductDto[]>(sql, [filter]);
return results.map(result => new ProductDto(result));
}
async findAllProducts(): Promise<ProductDto[]> {
const sql = `
SELECT PCPRODUT.CODPROD as "id",
PCPRODUT.DESCRICAO as "name",
PCPRODUT.CODFAB as "manufacturerCode"
PCPRODUT.CODPROD || ' - ' || PCPRODUT.DESCRICAO || ' ( ' || PCPRODUT.CODFAB || ' )' as "description"
FROM PCPRODUT
WHERE ROWNUM <= 500
`;
const results = await this.executeQuery<ProductDto[]>(sql);
return results.map(result => new ProductDto(result));
}
}
/**
* Busca todas as transportadoras cadastradas no sistema
*/
async findAllCarriers(): Promise<any[]> {
const sql = `
SELECT DISTINCT
PCFORNEC.CODFORNEC as "carrierId",
PCFORNEC.FORNECEDOR as "carrierName",
PCFORNEC.CODFORNEC || ' - ' || PCFORNEC.FORNECEDOR as "carrierDescription"
FROM PCFORNEC
WHERE PCFORNEC.CODFORNEC IS NOT NULL
AND PCFORNEC.CODFORNEC > 0
AND PCFORNEC.FORNECEDOR IS NOT NULL
ORDER BY PCFORNEC.FORNECEDOR
`;
return await this.executeQuery<any[]>(sql);
}
/**
* Busca as transportadoras por período de data
*/
async findCarriersByDate(query: any): Promise<any[]> {
let sql = `
SELECT DISTINCT
PCPEDC.CODFORNECFRETE as "carrierId",
PCFORNEC.FORNECEDOR as "carrierName",
PCPEDC.CODFORNECFRETE || ' - ' || PCFORNEC.FORNECEDOR as "carrierDescription",
COUNT(PCPEDC.NUMPED) as "ordersCount"
FROM PCPEDC
LEFT JOIN PCFORNEC ON PCPEDC.CODFORNECFRETE = PCFORNEC.CODFORNEC
WHERE PCPEDC.CODFORNECFRETE IS NOT NULL
AND PCPEDC.CODFORNECFRETE > 0
`;
const conditions: string[] = [];
const parameters: any[] = [];
let paramIndex = 0;
if (query.dateIni) {
conditions.push(`AND PCPEDC.DATA >= TO_DATE(:${paramIndex}, 'YYYY-MM-DD')`);
parameters.push(query.dateIni);
paramIndex++;
}
if (query.dateEnd) {
conditions.push(`AND PCPEDC.DATA <= TO_DATE(:${paramIndex}, 'YYYY-MM-DD')`);
parameters.push(query.dateEnd);
paramIndex++;
}
if (query.codfilial) {
conditions.push(`AND PCPEDC.CODFILIAL = :${paramIndex}`);
parameters.push(query.codfilial);
paramIndex++;
}
sql += "\n" + conditions.join("\n");
sql += "\nGROUP BY PCPEDC.CODFORNECFRETE, PCFORNEC.FORNECEDOR";
sql += "\nORDER BY PCPEDC.CODFORNECFRETE";
return await this.executeQuery<any[]>(sql, parameters);
}
/**
* Busca as transportadoras de um pedido específico
*/
async findOrderCarriers(orderId: number): Promise<any[]> {
const sql = `
SELECT DISTINCT
PCPEDC.CODFORNECFRETE as "carrierId",
PCFORNEC.FORNECEDOR as "carrierName",
PCPEDC.CODFORNECFRETE || ' - ' || PCFORNEC.FORNECEDOR as "carrierDescription"
FROM PCPEDC
LEFT JOIN PCFORNEC ON PCPEDC.CODFORNECFRETE = PCFORNEC.CODFORNEC
WHERE PCPEDC.NUMPED = :0
AND PCPEDC.CODFORNECFRETE IS NOT NULL
AND PCPEDC.CODFORNECFRETE > 0
ORDER BY PCPEDC.CODFORNECFRETE
`;
return await this.executeQuery<any[]>(sql, [orderId]);
}
}

View File

@@ -5,6 +5,7 @@ import { SellerDto } from './dto/seller.dto';
import { BillingDto } from './dto/billing.dto';
import { CustomerDto } from './dto/customer.dto';
import { ProductDto } from './dto/product.dto';
import { CarrierDto, FindCarriersDto } from './dto/carrier.dto';
import { ILogger } from '../Log/ILogger';
import { RedisClientToken } from '../core/configs/cache/redis-client.adapter.provider';
import { IRedisClient } from '../core/configs/cache/IRedisClient';
@@ -15,11 +16,14 @@ import { DATA_SOURCE } from '../core/constants';
@Injectable()
export class DataConsultService {
private readonly SELLERS_CACHE_KEY = 'data-consult:sellers';
private readonly SELLERS_TTL = 3600;
private readonly SELLERS_TTL = 3600;
private readonly STORES_TTL = 3600;
private readonly BILLINGS_TTL = 3600;
private readonly ALL_PRODUCTS_CACHE_KEY = 'data-consult:products:all';
private readonly ALL_PRODUCTS_TTL = 600;
private readonly ALL_PRODUCTS_TTL = 600;
private readonly CUSTOMERS_TTL = 3600;
private readonly CARRIERS_CACHE_KEY = 'data-consult:carriers:all';
private readonly CARRIERS_TTL = 3600;
constructor(
private readonly repository: DataConsultRepository,
@@ -63,7 +67,7 @@ export class DataConsultService {
this.logger.error('Erro ao buscar vendedores', error);
throw new HttpException('Erro ao buscar vendedores', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
/**
* @returns Array de BillingDto
@@ -134,4 +138,71 @@ export class DataConsultService {
throw new HttpException('Erro ao buscar produtos', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
/**
* Obter todas as transportadoras cadastradas
* @returns Array de CarrierDto
*/
async getAllCarriers(): Promise<CarrierDto[]> {
this.logger.log('Buscando todas as transportadoras');
try {
return getOrSetCache<CarrierDto[]>(
this.redisClient,
this.CARRIERS_CACHE_KEY,
this.CARRIERS_TTL,
async () => {
const carriers = await this.repository.findAllCarriers();
return carriers.map(carrier => ({
carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '',
}));
}
);
} catch (error) {
this.logger.error('Erro ao buscar transportadoras', error);
throw new HttpException('Erro ao buscar transportadoras', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* Obter transportadoras por período de data
* @param query - Filtros de data e filial
* @returns Array de CarrierDto
*/
async getCarriersByDate(query: FindCarriersDto): Promise<CarrierDto[]> {
this.logger.log(`Buscando transportadoras por período: ${JSON.stringify(query)}`);
try {
const carriers = await this.repository.findCarriersByDate(query);
return carriers.map(carrier => ({
carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '',
ordersCount: carrier.ordersCount ? Number(carrier.ordersCount) : 0,
}));
} catch (error) {
this.logger.error('Erro ao buscar transportadoras por período', error);
throw new HttpException('Erro ao buscar transportadoras', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* Obter transportadoras de um pedido específico
* @param orderId - ID do pedido
* @returns Array de CarrierDto
*/
async getOrderCarriers(orderId: number): Promise<CarrierDto[]> {
this.logger.log(`Buscando transportadoras do pedido: ${orderId}`);
try {
const carriers = await this.repository.findOrderCarriers(orderId);
return carriers.map(carrier => ({
carrierId: carrier.carrierId?.toString() || '',
carrierName: carrier.carrierName || '',
carrierDescription: carrier.carrierDescription || '',
}));
} catch (error) {
this.logger.error('Erro ao buscar transportadoras do pedido', error);
throw new HttpException('Erro ao buscar transportadoras do pedido', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@@ -0,0 +1,58 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString, IsDateString } from 'class-validator';
export class CarrierDto {
@ApiProperty({
description: 'ID da transportadora',
example: '123'
})
carrierId: string;
@ApiProperty({
description: 'Nome da transportadora',
example: 'TRANSPORTADORA ABC LTDA'
})
carrierName: string;
@ApiProperty({
description: 'Descrição completa da transportadora (ID - Nome)',
example: '123 - TRANSPORTADORA ABC LTDA'
})
carrierDescription: string;
@ApiProperty({
description: 'Quantidade de pedidos da transportadora no período',
example: 15,
required: false
})
ordersCount?: number;
}
export class FindCarriersDto {
@ApiProperty({
description: 'Data inicial para filtro (formato YYYY-MM-DD)',
example: '2024-01-01',
required: false
})
@IsOptional()
@IsDateString()
dateIni?: string;
@ApiProperty({
description: 'Data final para filtro (formato YYYY-MM-DD)',
example: '2024-12-31',
required: false
})
@IsOptional()
@IsDateString()
dateEnd?: string;
@ApiProperty({
description: 'ID da filial',
example: '1',
required: false
})
@IsOptional()
@IsString()
codfilial?: string;
}