import { BadRequestException, Injectable } from '@nestjs/common'; import { RedisService } from '../redis/redis.service'; import { digitsZip, fallbackDistanceKm, type ZipPlace } from './geo.types'; import { haversineKm } from '../freight/freight-calculator'; @Injectable() export class GeoService { constructor(private readonly redis: RedisService) {} async placeFromZip(zip: string): Promise { const cep = digitsZip(zip); if (!/^\d{8}$/.test(cep)) { throw new BadRequestException('CEP inválido.'); } const cached = await this.redis.getJson(`geo:cep:${cep}`); if (cached) { return cached; } const viaCep = await this.lookupViaCep(cep); const coords = await this.lookupNominatim(viaCep.city, viaCep.state); const place: ZipPlace = { zip: cep, city: viaCep.city, state: viaCep.state, label: `${viaCep.city}/${viaCep.state}`, lat: coords?.lat ?? 0, lon: coords?.lon ?? 0, }; await this.redis.setJson(`geo:cep:${cep}`, place, 60 * 60 * 24 * 7); return place; } async distanceKm(originZip: string, destinationZip: string): Promise<{ origin: ZipPlace; destination: ZipPlace; km: number; source: 'nominatim' | 'fallback'; }> { const origin = await this.placeFromZip(originZip); const destination = await this.placeFromZip(destinationZip); if (origin.lat && origin.lon && destination.lat && destination.lon) { return { origin, destination, km: Math.max(haversineKm(origin, destination), 1), source: 'nominatim' }; } return { origin, destination, km: fallbackDistanceKm(origin, destination), source: 'fallback' }; } private async lookupViaCep(cep: string): Promise<{ city: string; state: string }> { const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`, { signal: AbortSignal.timeout(8000), }); if (!response.ok) { throw new BadRequestException('Não foi possível consultar o CEP.'); } const data = (await response.json()) as { erro?: boolean; localidade?: string; uf?: string }; if (data.erro || !data.localidade || !data.uf) { throw new BadRequestException('CEP não encontrado.'); } return { city: data.localidade, state: data.uf }; } private async lookupNominatim(city: string, state: string): Promise<{ lat: number; lon: number } | null> { const query = new URLSearchParams({ format: 'json', limit: '1', countrycodes: 'br', q: `${city}, ${state}, Brasil`, }); const response = await fetch(`https://nominatim.openstreetmap.org/search?${query.toString()}`, { headers: { 'User-Agent': 'SinkaLogistica/1.0 (desafio; tina.r@example.net)' }, signal: AbortSignal.timeout(8000), }); if (!response.ok) { return null; } const rows = (await response.json()) as Array<{ lat: string; lon: string }>; const first = rows[0]; if (!first) { return null; } return { lat: Number(first.lat), lon: Number(first.lon) }; } }