sinka/api/src/geo/geo.service.ts
armando 8e4203e4c9 sinka
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 03:56:18 -03:00

85 lines
3.0 KiB
TypeScript

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<ZipPlace> {
const cep = digitsZip(zip);
if (!/^\d{8}$/.test(cep)) {
throw new BadRequestException('CEP inválido.');
}
const cached = await this.redis.getJson<ZipPlace>(`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) };
}
}