Pular para conteúdo

🗺️ Módulo Locations - Geolocalização e CEP

✅ Status: Módulo implementado e funcional (Novembro 2025)

🎯 Visão Geral

O módulo Locations é responsável por abstrair funcionalidades de geolocalização e lookup de CEP brasileiro, fornecendo uma API unificada e resiliente para outros módulos consumirem dados de localização.

Responsabilidades

  • Lookup de CEP com fallback automático entre APIs brasileiras
  • Geocoding de endereços para coordenadas (planejado)
  • Value Objects para CEP, Coordenadas e Endereço
  • Validação de CEP brasileiro
  • Resiliência com retry e circuit breaker
  • API pública para comunicação inter-módulos

🏗️ Arquitetura Implementada

Bounded Context: Locations

  • Sem schema próprio (stateless module)
  • Padrão: Service Layer + Value Objects
  • Integrações: ViaCEP, BrasilAPI, OpenCEP

Value Objects

Cep

public sealed class Cep
{
    private const string CepPattern = @"^\d{8}$";
    public string Value { get; }           // 12345678 (apenas números)
    public string Formatted => $"{Value.Substring(0, 5)}-{Value.Substring(5)}"; // 12345-678

    public static Cep? Create(string value)
    {
        var cleaned = Regex.Replace(value, @"\D", "");
        return Regex.IsMatch(cleaned, CepPattern) ? new Cep(cleaned) : null;
    }

    public static bool IsValid(string value) => Create(value) is not null;
}

Validações: - ✅ Deve ter exatamente 8 dígitos - ✅ Remove automaticamente formatação (-, . e outros caracteres especiais) - ✅ Factory method seguro (retorna null se inválido)

Coordinates

public sealed class Coordinates
{
    public double Latitude { get; }
    public double Longitude { get; }

    public Coordinates(double latitude, double longitude)
    {
        if (latitude < -90 || latitude > 90)
            throw new ArgumentException("Latitude must be between -90 and 90");

        if (longitude < -180 || longitude > 180)
            throw new ArgumentException("Longitude must be between -180 and 180");

        Latitude = latitude;
        Longitude = longitude;
    }
}

Validações: - ✅ Latitude: -90 a +90 - ✅ Longitude: -180 a +180

Address

public sealed class Address
{
    public Cep Cep { get; }
    public string Street { get; }
    public string Neighborhood { get; }
    public string City { get; }
    public string State { get; }          // Sigla UF (SP, RJ, etc.)
    public string? Complement { get; }
    public Coordinates? GeoPoint { get; }

    public Address(
        Cep cep,
        string street,
        string neighborhood,
        string city,
        string state,
        string? complement = null,
        Coordinates? geoPoint = null)
    {
        // Validações...
        Cep = cep;
        Street = street;
        Neighborhood = neighborhood;
        City = city;
        State = state;
        Complement = complement;
        GeoPoint = geoPoint;
    }
}

Validações: - ✅ CEP válido - ✅ Campos obrigatórios não vazios - ✅ State com 2 caracteres (UF)

🔌 Serviços Implementados

ICepLookupService

public interface ICepLookupService
{
    Task<Address?> LookupAsync(Cep cep, CancellationToken cancellationToken = default);
}

Implementação: Chain of Responsibility com Fallback

public class CepLookupService : ICepLookupService
{
    private readonly IViaCepClient _viaCepClient;
    private readonly IBrasilApiCepClient _brasilApiClient;
    private readonly IOpenCepClient _openCepClient;

    public async Task<Address?> LookupAsync(Cep cep, CancellationToken ct = default)
    {
        // 1ª tentativa: ViaCEP (principal)
        try
        {
            var result = await _viaCepClient.GetAddressAsync(cep.Value, ct);
            if (result != null) return result;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "ViaCEP failed for {Cep}, trying BrasilAPI", cep.Value);
        }

        // 2ª tentativa: BrasilAPI (fallback 1)
        try
        {
            var result = await _brasilApiClient.GetAddressAsync(cep.Value, ct);
            if (result != null) return result;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "BrasilAPI failed for {Cep}, trying OpenCEP", cep.Value);
        }

        // 3ª tentativa: OpenCEP (fallback 2)
        try
        {
            return await _openCepClient.GetAddressAsync(cep.Value, ct);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "All CEP providers failed for {Cep}", cep.Value);
            return null;
        }
    }
}

Recursos: - ✅ Fallback automático entre 3 providers - ✅ Logging detalhado de falhas - ✅ Resiliência via Polly (retry, circuit breaker, timeout) - ✅ Configurável via appsettings.json

IGeocodingService (Stub)

public interface IGeocodingService
{
    Task<Coordinates?> GetCoordinatesAsync(string address, CancellationToken ct = default);
    Task<Address?> ReverseGeocodeAsync(Coordinates coordinates, CancellationToken ct = default);
}

Status: Interface definida, implementação futura (Nominatim ou Google Maps API)

🌐 API Pública - Module API

Interface ILocationModuleApi

public interface ILocationModuleApi : IModuleApi
{
    Task<Result<ModuleAddressDto>> GetAddressFromCepAsync(
        string cep, CancellationToken ct = default);

    Task<Result<ModuleCoordinatesDto>> GetCoordinatesFromAddressAsync(
        string address, CancellationToken ct = default);
}

DTOs Públicos

public sealed record ModuleAddressDto(
    string Cep,
    string Street,
    string Neighborhood,
    string City,
    string State,
    string? Complement,
    ModuleCoordinatesDto? Coordinates
);

public sealed record ModuleCoordinatesDto(
    double Latitude,
    double Longitude
);

Implementação

[ModuleApi(ModuleMetadata.Name, ModuleMetadata.Version)]
public sealed class LocationsModuleApi : ILocationModuleApi
{
    private static class ModuleMetadata
    {
        public const string Name = "Locations";
        public const string Version = "1.0";
    }

    // Health check via CEP real
    public async Task<bool> IsAvailableAsync(CancellationToken ct = default)
    {
        var testCep = Cep.Create("01310100"); // Av. Paulista, SP
        if (testCep is not null)
        {
            var result = await cepLookupService.LookupAsync(testCep, ct);
            return true; // Se conseguiu fazer request, módulo está disponível
        }
        return false;
    }

    public async Task<Result<ModuleAddressDto>> GetAddressFromCepAsync(
        string cep, CancellationToken ct = default)
    {
        var cepValueObject = Cep.Create(cep);
        if (cepValueObject is null)
            return Result<ModuleAddressDto>.Failure($"CEP inválido: {cep}");

        var address = await cepLookupService.LookupAsync(cepValueObject, ct);
        if (address is null)
            return Result<ModuleAddressDto>.Failure($"CEP {cep} não encontrado");

        var dto = new ModuleAddressDto(
            address.Cep.Formatted,
            address.Street,
            address.Neighborhood,
            address.City,
            address.State,
            address.Complement,
            address.GeoPoint is not null
                ? new ModuleCoordinatesDto(address.GeoPoint.Latitude, address.GeoPoint.Longitude)
                : null);

        return Result<ModuleAddressDto>.Success(dto);
    }
}

Recursos: - ✅ Validação de CEP antes de lookup - ✅ Mensagens de erro claras - ✅ Health check via API real (não mock)

🔧 Integrações com APIs Externas

ViaCEP

  • URL: https://viacep.com.br/ws/{cep}/json/
  • Prioridade: 1ª escolha
  • Rate Limit: Sem limite oficial
  • Timeout: 5 segundos

BrasilAPI

  • URL: https://brasilapi.com.br/api/cep/v1/{cep}
  • Prioridade: Fallback 1
  • Rate Limit: Sem limite
  • Timeout: 5 segundos

OpenCEP

  • URL: https://opencep.com/v1/{cep}
  • Prioridade: Fallback 2
  • Rate Limit: 200 req/min
  • Timeout: 5 segundos

Resiliência (Polly)

// ServiceDefaults configurado para todos os HttpClients
services.AddHttpClient<IViaCepClient, ViaCepClient>()
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.CircuitBreaker.FailureRatio = 0.5;
        options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(10);
    });

Políticas: - ✅ Retry: 3 tentativas com backoff exponencial - ✅ Circuit Breaker: Abre após 50% de falhas em 30s - ✅ Timeout: 10s total (5s por tentativa)

📊 Estrutura de Pastas

src/Modules/Locations/
├── API/
│   └── MeAjudaAi.Modules.Locations.API.csproj
├── Application/
│   ├── ModuleApi/
│   │   └── LocationsModuleApi.cs
│   ├── Services/
│   │   ├── ICepLookupService.cs
│   │   ├── CepLookupService.cs
│   │   └── IGeocodingService.cs
│   └── MeAjudaAi.Modules.Locations.Application.csproj
├── Domain/
│   ├── ValueObjects/
│   │   ├── Cep.cs
│   │   ├── Coordinates.cs
│   │   └── Address.cs
│   └── MeAjudaAi.Modules.Locations.Domain.csproj
├── Infrastructure/
│   ├── ExternalServices/
│   │   ├── ViaCEP/
│   │   │   ├── IViaCepClient.cs
│   │   │   └── ViaCepClient.cs
│   │   ├── BrasilAPI/
│   │   │   ├── IBrasilApiCepClient.cs
│   │   │   └── BrasilApiCepClient.cs
│   │   └── OpenCEP/
│   │       ├── IOpenCepClient.cs
│   │       └── OpenCepClient.cs
│   ├── Extensions.cs
│   └── MeAjudaAi.Modules.Locations.Infrastructure.csproj
└── Tests/
    └── Unit/
        └── Domain/
            └── ValueObjects/
                ├── CepTests.cs           # 20+ testes
                ├── CoordinatesTests.cs   # 15+ testes
                └── AddressTests.cs       # 17+ testes

🧪 Testes Implementados

Testes Unitários de Value Objects

  • CepTests: 20+ testes
  • Validação de formato
  • Remoção de caracteres especiais
  • CEPs válidos/inválidos
  • Formatação
  • CoordinatesTests: 15+ testes
  • Limites de latitude/longitude
  • Edge cases (polos, linha do equador)
  • AddressTests: 17+ testes
  • Validação de campos obrigatórios
  • State UF validation
  • GeoPoint opcional

Cobertura de Código

  • Domain (Value Objects): 100%
  • Application (Services): ~70%
  • Infrastructure (Clients): ~60%

Total: 52 testes unitários passando

🔗 Integração com Outros Módulos

Providers Module

public class BusinessProfile
{
    public Address PrimaryAddress { get; private set; }

    // Usa Location.ModuleAPI para validar/enriquecer endereço
    public async Task SetAddressFromCep(string cep)
    {
        var result = await _locationApi.GetAddressFromCepAsync(cep);
        if (result.IsSuccess)
        {
            PrimaryAddress = result.Value.ToAddress();
        }
    }
}

Search Module

public class SearchableProvider
{
    public GeoPoint Location { get; set; } // Latitude/Longitude

    // Location module fornece coordenadas para queries espaciais
}

📈 Métricas e Desempenho

SLAs Esperados

  • Lookup de CEP: <500ms (com fallback)
  • Geocoding: <1000ms (quando implementado)
  • Health check: <200ms

Otimizações Futuras

  • Cache Redis para CEPs (TTL: 24h)
  • Warm-up de circuit breakers no startup
  • Métricas customizadas (Polly telemetry)

🚀 Próximos Passos

Fase 2 - Geocoding

  • Implementar GeocodingService
  • Integração com Nominatim (OpenStreetMap) ou Google Maps API
  • Reverse geocoding (coordenadas → endereço)

Fase 3 - Caching

  • Redis cache para CEPs
  • Cache de coordenadas
  • Invalidação por TTL

Fase 4 - Enriquecimento

  • Integração com IBGE para municípios
  • Validação de logradouros
  • Distância entre pontos (Haversine)

⚙️ Configuração

appsettings.json

{
  "ExternalServices": {
    "ViaCEP": {
      "BaseUrl": "https://viacep.com.br/ws",
      "Timeout": 5000
    },
    "BrasilAPI": {
      "BaseUrl": "https://brasilapi.com.br/api/cep/v1",
      "Timeout": 5000
    },
    "OpenCEP": {
      "BaseUrl": "https://opencep.com/v1",
      "Timeout": 5000
    }
  }
}

📚 Referências


📅 Implementado: Novembro 2025
✅ Status: Produção Ready (CEP lookup)
🔄 Geocoding: Planejado (Q1 2026)
🧪 Testes: 52 unit tests (100% passing)