$latitude !== null, 'has_longitude' => $longitude !== null, 'zip_code' => $this->normalize($zipCode), ]); if ($latitude !== null && $longitude !== null) { Log::info('ZipCodeCoordinates resolve using address coordinates', [ 'latitude' => $latitude, 'longitude' => $longitude, 'zip_code' => $this->normalize($zipCode), ]); return compact('latitude', 'longitude'); } Log::info('ZipCodeCoordinates fallback to zip code lookup', [ 'zip_code' => $this->normalize($zipCode), 'has_latitude' => $latitude !== null, 'has_longitude' => $longitude !== null, 'reason' => 'missing_address_coordinates', ]); return $this->findByZipCode($zipCode); } public function calculateDistance( ?float $originLatitude, ?float $originLongitude, ?string $originZipCode, ?float $targetLatitude, ?float $targetLongitude, ?string $targetZipCode, ): ?float { Log::info('ZipCodeCoordinates calculate distance requested', [ 'origin_has_latitude' => $originLatitude !== null, 'origin_has_longitude' => $originLongitude !== null, 'origin_zip_code' => $this->normalize($originZipCode), 'target_has_latitude' => $targetLatitude !== null, 'target_has_longitude' => $targetLongitude !== null, 'target_zip_code' => $this->normalize($targetZipCode), ]); $origin = $this->resolve($originLatitude, $originLongitude, $originZipCode); $target = $this->resolve($targetLatitude, $targetLongitude, $targetZipCode); $distance = DistanceService::calculate( $origin['latitude'] ?? null, $origin['longitude'] ?? null, $target['latitude'] ?? null, $target['longitude'] ?? null, ); Log::info('ZipCodeCoordinates calculate distance result', [ 'origin_found' => $origin !== null, 'target_found' => $target !== null, 'distance_km' => $distance, ]); return $distance; } public function findByZipCode(?string $zipCode): ?array { $zipCode = $this->normalize($zipCode); if ($zipCode === null) { Log::info('ZipCodeCoordinates skipped lookup: invalid zip code'); return null; } if (array_key_exists($zipCode, $this->resolvedZipCodes)) { Log::info('ZipCodeCoordinates memory cache hit', [ 'zip_code' => $zipCode, 'found' => $this->resolvedZipCodes[$zipCode] !== null, ]); return $this->resolvedZipCodes[$zipCode]; } if ($this->restoreFromCache($zipCode)) { Log::info('ZipCodeCoordinates persistent cache hit', [ 'zip_code' => $zipCode, 'found' => $this->resolvedZipCodes[$zipCode] !== null, ]); return $this->resolvedZipCodes[$zipCode]; } Log::info('ZipCodeCoordinates Brasil API lookup required', [ 'zip_code' => $zipCode, 'mode' => 'single', ]); $coordinates = $this->requestCoordinates($zipCode); $this->remember($zipCode, $coordinates); return $coordinates; } public function preload(iterable $zipCodes): void { $pending = []; foreach ($zipCodes as $zipCode) { $zipCode = $this->normalize($zipCode); if ( $zipCode === null || array_key_exists($zipCode, $this->resolvedZipCodes) || $this->restoreFromCache($zipCode) ) { continue; } $pending[$zipCode] = $zipCode; } if ($pending === []) { Log::info('ZipCodeCoordinates preload skipped: no pending zip codes'); return; } Log::info('ZipCodeCoordinates Brasil API lookup required', [ 'mode' => 'preload', 'pending_count' => count($pending), 'zip_codes_sample' => array_slice(array_values($pending), 0, 20), 'connect_timeout' => (int) config('services.brasil_api.connect_timeout', 2), 'timeout' => (int) config('services.brasil_api.timeout', 3), 'ip_resolve' => 'ipv4', ]); try { $baseUrl = rtrim((string) config('services.brasil_api.base_url'), '/'); $startedAt = microtime(true); $responses = Http::pool(fn (Pool $pool) => array_map( fn (string $zipCode) => $pool ->as($zipCode) ->acceptJson() ->withUserAgent($this->brasilApiUserAgent()) ->connectTimeout((int) config('services.brasil_api.connect_timeout', 2)) ->timeout((int) config('services.brasil_api.timeout', 3)) ->withOptions($this->curlForceIpv4Options()) ->get("{$baseUrl}/api/cep/v2/{$zipCode}"), $pending, )); } catch (Throwable $exception) { Log::warning('ZipCodeCoordinates Brasil API preload failed', [ 'pending_count' => count($pending), 'elapsed_ms' => isset($startedAt) ? round((microtime(true) - $startedAt) * 1000, 2) : null, 'error' => $exception->getMessage(), ]); $responses = []; } foreach ($pending as $zipCode) { $response = $responses[$zipCode] ?? null; $coordinates = $response instanceof Response ? $this->coordinatesFromResponse($response) : null; Log::info('ZipCodeCoordinates Brasil API preload response', [ 'zip_code' => $zipCode, 'has_response' => $response instanceof Response, 'status' => $response instanceof Response ? $response->status() : null, 'elapsed_ms' => isset($startedAt) ? round((microtime(true) - $startedAt) * 1000, 2) : null, 'found' => $coordinates !== null, 'coordinates' => $coordinates, 'body' => $coordinates === null && $response instanceof Response ? $response->json() : null, ]); $this->remember($zipCode, $coordinates); } } private function normalize(?string $zipCode): ?string { $zipCode = preg_replace('/\D/', '', $zipCode ?? ''); return strlen($zipCode) === 8 ? $zipCode : null; } private function restoreFromCache(string $zipCode): bool { $cacheKey = "zip-code-coordinates:{$zipCode}"; $cached = Cache::get($cacheKey); if (! is_array($cached) || ! array_key_exists('found', $cached)) { return false; } if (! $cached['found']) { Cache::forget($cacheKey); Log::info('ZipCodeCoordinates negative cache ignored', [ 'zip_code' => $zipCode, 'reason' => 'retry_lookup_after_previous_failure', ]); return false; } $this->resolvedZipCodes[$zipCode] = $cached['found'] ? $cached['coordinates'] : null; return true; } private function remember(string $zipCode, ?array $coordinates): void { $this->resolvedZipCodes[$zipCode] = $coordinates; if ($coordinates === null) { Log::info('ZipCodeCoordinates failure not cached', [ 'zip_code' => $zipCode, ]); return; } Cache::put( "zip-code-coordinates:{$zipCode}", [ 'found' => $coordinates !== null, 'coordinates' => $coordinates, ], now()->addSeconds( (int) config('services.brasil_api.cep_cache_ttl', 2592000) ) ); } private function requestCoordinates(string $zipCode): ?array { $startedAt = microtime(true); try { Log::info('ZipCodeCoordinates calling Brasil API', [ 'zip_code' => $zipCode, 'base_url' => rtrim((string) config('services.brasil_api.base_url'), '/'), 'user_agent' => $this->brasilApiUserAgent(), 'connect_timeout' => (int) config('services.brasil_api.connect_timeout', 2), 'timeout' => (int) config('services.brasil_api.timeout', 3), 'ip_resolve' => 'ipv4', ]); $baseUrl = rtrim((string) config('services.brasil_api.base_url'), '/'); $response = Http::acceptJson() ->withUserAgent($this->brasilApiUserAgent()) ->withOptions($this->curlForceIpv4Options()) ->connectTimeout((int) config('services.brasil_api.connect_timeout', 2)) ->timeout((int) config('services.brasil_api.timeout', 3)) ->get("{$baseUrl}/api/cep/v2/{$zipCode}"); $coordinates = $this->coordinatesFromResponse($response); Log::info('ZipCodeCoordinates Brasil API response', [ 'zip_code' => $zipCode, 'status' => $response->status(), 'elapsed_ms' => round((microtime(true) - $startedAt) * 1000, 2), 'found' => $coordinates !== null, 'coordinates' => $coordinates, 'body' => $coordinates === null ? $response->json() : null, ]); return $coordinates; } catch (Throwable $exception) { Log::warning('ZipCodeCoordinates Brasil API request failed', [ 'zip_code' => $zipCode, 'elapsed_ms' => round((microtime(true) - $startedAt) * 1000, 2), 'error' => $exception->getMessage(), ]); return null; } } private function curlForceIpv4Options(): array { if (! defined('CURLOPT_IPRESOLVE') || ! defined('CURL_IPRESOLVE_V4')) { return []; } return [ 'curl' => [ CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, ], ]; } private function brasilApiUserAgent(): string { return 'insomnia/11.3.0'; } private function coordinatesFromResponse(Response $response): ?array { if (! $response->successful()) { return null; } $coordinates = $response->json('location.coordinates'); $latitude = is_array($coordinates) ? ($coordinates['latitude'] ?? $coordinates[1] ?? null) : null; $longitude = is_array($coordinates) ? ($coordinates['longitude'] ?? $coordinates[0] ?? null) : null; if (! is_numeric($latitude) || ! is_numeric($longitude)) { return null; } $latitude = (float) $latitude; $longitude = (float) $longitude; if ($latitude < -90 || $latitude > 90 || $longitude < -180 || $longitude > 180) { return null; } return compact('latitude', 'longitude'); } }