🗺️ 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¶
- Roadmap - Planejamento estratégico
- Architecture - Padrões arquiteturais
- Providers Module - Integração com endereços
- ViaCEP API - Documentação oficial
- BrasilAPI - Documentação oficial
📅 Implementado: Novembro 2025
✅ Status: Produção Ready (CEP lookup)
🔄 Geocoding: Planejado (Q1 2026)
🧪 Testes: 52 unit tests (100% passing)