| Server IP : 189.126.111.107 / Your IP : 216.73.217.69 Web Server : Apache System : Linux www.gadotticar.com.br 5.4.0-135-generic #152-Ubuntu SMP Wed Nov 23 20:19:22 UTC 2022 x86_64 User : gadotticar ( 1000) PHP Version : 8.2.27 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /home/gadotticar/frete-api/src/frete/transportadoras/ |
Upload File : |
import { Injectable, Logger } from '@nestjs/common';
import { Client } from '@googlemaps/google-maps-services-js';
import axios from 'axios';
interface CepCache {
address?: string;
coords?: { lat: number; lng: number };
timestamp: number;
}
@Injectable()
export class TrasDedicadoService {
private readonly client = new Client();
private readonly logger = new Logger(TrasDedicadoService.name);
// Cache em memória: CEP → { endereço, coordenadas, timestamp }
private readonly cache = new Map<string, CepCache>();
private readonly CACHE_TTL = 24 * 60 * 60 * 1000; // 24h
// Token fixo do CEP Aberto
private readonly token_cep_aberto = '71ec4f09c7b4080ae650a9fa40f8a92c';
// ---------- CONFIGURAÇÃO DAS FONTES (com prioridade, url, headers, parser) ----------
private readonly cepSources = [
{
nome: 'CepAberto1',
url: 'https://www.cepaberto.com/api/v3/cep?cep={cep}',
headers: { Authorization: `Token token=${this.token_cep_aberto}` },
parser: this._parse_cepaberto.bind(this),
prioridade: 1, // Primeiro a ser tentado
},
{
nome: 'BrasilAPI',
url: 'https://brasilapi.com.br/api/cep/v2/{cep}',
headers: { 'User-Agent': 'TrasDedicadoService/1.0' },
parser: this._parse_brasilapi.bind(this),
prioridade: 2,
},
{
nome: 'ViaCEP',
url: 'https://viacep.com.br/ws/{cep}/json/',
headers: { 'User-Agent': 'TrasDedicadoService/1.0' },
parser: this._parse_viacep.bind(this),
prioridade: 3,
},
{
nome: 'Nominatim',
url: 'https://nominatim.openstreetmap.org/search?q={cep},+Brasil&format=json&limit=1&addressdetails=1',
headers: { 'User-Agent': 'TrasDedicadoService/1.0' },
parser: this._parse_nominatim.bind(this),
prioridade: 4,
},
];
// ---------------------------------------------------------------------------------
// ---------- CÁLCULO DE CUBAGEM ----------
private calcularCubagem(produtos: any[]): number {
return produtos.reduce((total, produto) => {
let volume = (produto.altura / 100) * (produto.largura / 100) * (produto.comprimento / 100);
volume = Math.max(volume, 1);
return total + volume;
}, 0);
}
// ---------- VALIDAÇÃO / FORMATAÇÃO DE CEP ----------
private formatCep(cep: string): string {
cep = cep.replace(/\D/g, '');
if (cep.length !== 8) throw new Error(`CEP inválido: ${cep}`);
return cep;
}
// ---------- RESOLUÇÃO DE ENDEREÇO COM FALLBACK E PRIORIDADE ----------
private async resolveCepWithFallback(cep: string): Promise<string> {
const rawCep = this.formatCep(cep);
const cacheKey = rawCep;
// ---- CACHE ----
const cached = this.cache.get(cacheKey);
if (cached?.address && Date.now() - cached.timestamp < this.CACHE_TTL) {
this.logger.log(`CEP ${rawCep} resolvido via cache`);
return cached.address;
}
// Ordena por prioridade (menor número = maior prioridade)
const sortedSources = [...this.cepSources].sort((a, b) => a.prioridade - b.prioridade);
const errors: string[] = [];
for (const source of sortedSources) {
try {
const url = source.url.replace('{cep}', rawCep);
const response = await axios.get(url, {
headers: source.headers,
timeout: 5000,
});
const address = source.parser(response.data);
// Armazena no cache
const cacheEntry: CepCache = {
address,
timestamp: Date.now(),
};
this.cache.set(cacheKey, cacheEntry);
this.logger.log(`CEP ${rawCep} resolvido via ${source.nome} (prioridade: ${source.prioridade})`);
return address;
} catch (error: any) {
const msg = error.response?.data || error.message || 'Erro desconhecido';
errors.push(`${source.nome}: ${msg}`);
this.logger.warn(`Falha em ${source.nome} (prioridade ${source.prioridade}) para CEP ${rawCep}: ${msg}`);
}
}
this.logger.error(`Todas as APIs falharam para o CEP ${rawCep}: ${errors.join(' | ')}`);
throw new Error(`Não foi possível resolver o CEP ${cep} em nenhuma API.`);
}
// ---------- PARSERS ----------
private _parse_cepaberto(data: any): string {
const { logradouro, bairro, cidade, estado } = data;
if (!logradouro || !cidade?.nome) {
throw new Error('Dados incompletos no CEP Aberto');
}
return `${logradouro}, ${bairro || 'Bairro não informado'}, ${cidade.nome} - ${estado.sigla}, Brasil`;
}
private _parse_brasilapi(data: any): string {
const { street, neighborhood, city, state } = data;
if (!street || !city) throw new Error('Dados incompletos');
return `${street}, ${neighborhood || 'Bairro não informado'}, ${city} - ${state}, Brasil`;
}
private _parse_viacep(data: any): string {
const { logradouro, bairro, localidade, uf, erro } = data;
if (erro || !logradouro) throw new Error('CEP não encontrado');
return `${logradouro}, ${bairro || 'Bairro não informado'}, ${localidade} - ${uf}, Brasil`;
}
private _parse_nominatim(data: any[]): string {
const result = data[0];
if (!result) throw new Error('CEP não encontrado no Nominatim');
const { road, suburb, city, state } = result.address || {};
const street = road || 'Rua não informada';
const neighborhood = suburb || 'Bairro não informado';
const cityName = city || 'Cidade não informada';
return `${street}, ${neighborhood}, ${cityName} - ${state}, Brasil`;
}
// ---------- GEOCODIFICAÇÃO (Nominatim) ----------
private async geocodeCep(cep: string): Promise<{ lat: number; lng: number }> {
const rawCep = this.formatCep(cep);
const cacheKey = rawCep;
const cached = this.cache.get(cacheKey);
if (cached?.coords && Date.now() - cached.timestamp < this.CACHE_TTL) {
return cached.coords;
}
try {
const coords = await this.geocodeNominatimCoords(rawCep);
this.cache.set(cacheKey, { ...(this.cache.get(cacheKey) || {}), coords, timestamp: Date.now() });
return coords;
} catch (error) {
this.logger.warn(`Geocodificação falhou para CEP ${cep}: ${error.message}`);
throw error;
}
}
private async geocodeNominatimCoords(cep: string): Promise<{ lat: number; lng: number }> {
const response = await axios.get('https://nominatim.openstreetmap.org/search', {
params: {
q: `${cep}, Brasil`,
format: 'json',
limit: 1,
},
headers: { 'User-Agent': 'TrasDedicadoService/1.0' },
timeout: 5000,
});
const result = response.data[0];
if (!result) throw new Error(`CEP não encontrado: ${cep}`);
return { lat: parseFloat(result.lat), lng: parseFloat(result.lon) };
}
// ---------- HAVERSINE ----------
private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
// ---------- DISTÂNCIA (Google + Haversine fallback) ----------
private async consultarDistancia(origem: string, destino: string): Promise<number> {
const origCep = this.formatCep(origem);
const destCep = this.formatCep(destino);
try {
const origemAddress = await this.resolveCepWithFallback(origCep);
const destinoAddress = await this.resolveCepWithFallback(destCep);
this.logger.log(`Endereços resolvidos: ${origemAddress} -> ${destinoAddress}`);
const response = await this.client.distancematrix({
params: {
origins: [origemAddress],
destinations: [destinoAddress],
key: process.env.GOOGLE_MAPS_API_KEY,
language: 'pt-BR',
},
});
const result = response.data.rows[0]?.elements[0];
if (result?.status === 'OK' && result.distance?.value) {
return result.distance.value / 1000;
}
throw new Error(`Google Maps: ${result?.status}`);
} catch (error) {
this.logger.warn(`Google Maps falhou, usando Haversine: ${error.message}`);
}
// Fallback: Haversine
try {
const [origCoords, destCoords] = await Promise.all([
this.geocodeCep(origCep),
this.geocodeCep(destCep),
]);
const distance = this.haversineDistance(
origCoords.lat,
origCoords.lng,
destCoords.lat,
destCoords.lng,
);
this.logger.log(`Distância Haversine: ${distance.toFixed(2)} km`);
return distance;
} catch (error) {
throw new Error('Falha total ao calcular distância.');
}
}
// ---------- CÁLCULO DO FRETE ----------
public async calcularFrete(payload: any): Promise<any> {
this.logger.log('Calculando frete para TrasDedicado...');
const { Origem, Destino, Produtos } = payload;
if (!Origem?.cep || !Destino?.cep) {
throw new Error('Origem e destino devem conter CEPs válidos.');
}
if (!Produtos?.length) {
throw new Error('A lista de produtos não pode estar vazia.');
}
try {
const cubagem = this.calcularCubagem(Produtos);
this.logger.log(`Cubagem: ${cubagem.toFixed(2)} m³`);
const distancia = await this.consultarDistancia(Origem.cep, Destino.cep);
this.logger.log(`Distância: ${distancia.toFixed(2)} km`);
let valorFrete = cubagem * distancia * 0.6;
valorFrete = Math.max(valorFrete, 649);
const diasExtras = Math.ceil(distancia / 300);
const prazoMin = 13 + diasExtras;
const prazoMax = 15 + diasExtras;
this.logger.log(`Frete: R$${valorFrete.toFixed(2)}, Prazo: ${prazoMin}-${prazoMax} dias`);
return {
transportadora: 'TransRof',
codigo: 101,
descricao: 'TransRof',
cotacao: {
codigo: 999999,
msg: 'Transrof',
valor: valorFrete.toFixed(2),
prazo_min: prazoMin,
prazo_max: prazoMax,
},
};
} catch (error) {
this.logger.error(`Erro ao calcular frete: ${error.message}`);
return null;
}
}
}