Ver código fonte

feat: add service para puxar latitude e longitude a partir de cep / zip code

Gustavo Mantovani 1 semana atrás
pai
commit
3b20f19d9c

+ 9 - 8
app/Services/CustomScheduleService.php

@@ -19,7 +19,9 @@ use Illuminate\Support\Facades\Storage;
 
 class CustomScheduleService
 {
-    public function __construct() {}
+    public function __construct(
+        private readonly ZipCodeCoordinatesService $zipCodeCoordinatesService,
+    ) {}
 
     public function getAll()
     {
@@ -250,9 +252,6 @@ class CustomScheduleService
             ->orderBy('is_primary', 'desc')
             ->first();
 
-        $providerLat = $providerAddress?->latitude !== null  ? (float) $providerAddress->latitude  : null;
-        $providerLng = $providerAddress?->longitude !== null ? (float) $providerAddress->longitude : null;
-
         $opportunities = Schedule::with([
             'client.user',
             'client.profileMedia',
@@ -299,12 +298,14 @@ class CustomScheduleService
             }
         });
 
-        $availableOpportunities->each(function ($opportunity) use ($providerLat, $providerLng) {
-            $opportunity->distance_km = DistanceService::calculate(
-                $providerLat,
-                $providerLng,
+        $availableOpportunities->each(function ($opportunity) use ($providerAddress) {
+            $opportunity->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
+                $providerAddress?->latitude !== null ? (float) $providerAddress->latitude : null,
+                $providerAddress?->longitude !== null ? (float) $providerAddress->longitude : null,
+                $providerAddress?->zip_code,
                 $opportunity->address?->latitude !== null  ? (float) $opportunity->address->latitude  : null,
                 $opportunity->address?->longitude !== null ? (float) $opportunity->address->longitude : null,
+                $opportunity->address?->zip_code,
             );
 
             $photoPath = $opportunity->client->profileMedia?->path;

+ 73 - 21
app/Services/DashboardService.php

@@ -24,6 +24,7 @@ class DashboardService
 {
     public function __construct(
         private readonly CustomScheduleService $customScheduleService,
+        private readonly ZipCodeCoordinatesService $zipCodeCoordinatesService,
     ) {}
 
     public function dadosDashboardCliente(): array
@@ -206,9 +207,15 @@ class DashboardService
 
         $clientDistanceAddress = $this->addressForDistance($cliente->id, $clientPrimaryAddress);
 
-        $providersCloseDistanceSelect = $this->distanceSelect(
-            $clientDistanceAddress?->latitude  !== null ? (float) $clientDistanceAddress->latitude  : null,
+        $clientCoordinates = $this->zipCodeCoordinatesService->resolve(
+            $clientDistanceAddress?->latitude !== null ? (float) $clientDistanceAddress->latitude : null,
             $clientDistanceAddress?->longitude !== null ? (float) $clientDistanceAddress->longitude : null,
+            $clientDistanceAddress?->zip_code,
+        );
+
+        $providersCloseDistanceSelect = $this->distanceSelect(
+            $clientCoordinates['latitude'] ?? null,
+            $clientCoordinates['longitude'] ?? null,
         );
 
         $providersClose = Provider::leftJoin(
@@ -255,6 +262,7 @@ class DashboardService
                 'providers.id as provider_id',
                 'provider_user.name as provider_name',
                 'provider_address.id as address_id',
+                'provider_address.zip_code as provider_zip_code',
                 'provider_address.district',
                 'provider_address.latitude as provider_latitude',
                 'provider_address.longitude as provider_longitude',
@@ -283,12 +291,27 @@ class DashboardService
             ->orderByRaw('distance_km ASC NULLS LAST')
             ->get();
 
-        $providersClose->each(function ($item) {
+        $this->zipCodeCoordinatesService->preload(
+            $providersClose->whereNull('distance_km')->pluck('provider_zip_code')
+        );
+
+        $providersClose->each(function ($item) use ($clientDistanceAddress) {
+            if ($item->distance_km === null) {
+                $item->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
+                    $clientDistanceAddress?->latitude !== null ? (float) $clientDistanceAddress->latitude : null,
+                    $clientDistanceAddress?->longitude !== null ? (float) $clientDistanceAddress->longitude : null,
+                    $clientDistanceAddress?->zip_code,
+                    $item->provider_latitude !== null ? (float) $item->provider_latitude : null,
+                    $item->provider_longitude !== null ? (float) $item->provider_longitude : null,
+                    $item->provider_zip_code,
+                );
+            }
+
             $item->provider_photo = $item->provider_photo_path
                 ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
                 : null;
 
-            unset($item->provider_photo_path);
+            unset($item->provider_photo_path, $item->provider_zip_code);
 
             $item->daily_price_8h_base = $item->daily_price_8h;
             $item->daily_price_6h_base = $item->daily_price_6h;
@@ -301,6 +324,10 @@ class DashboardService
             $item->daily_price_2h = $this->applyCreditCardFee($item->daily_price_2h);
         });
 
+        $providersClose = $providersClose
+            ->sortBy(fn ($provider) => $provider->distance_km ?? PHP_FLOAT_MAX)
+            ->values();
+
         $pendingSchedules = Schedule::with([
             'address:district,address,number,source_id,source,id,address_type',
         ])
@@ -377,8 +404,8 @@ class DashboardService
         });
 
         $proposalsDistanceSelect = DistanceService::sqlExpression(
-            $clientPrimaryAddress?->latitude  !== null ? (float) $clientPrimaryAddress->latitude  : null,
-            $clientPrimaryAddress?->longitude !== null ? (float) $clientPrimaryAddress->longitude : null,
+            $clientCoordinates['latitude'] ?? null,
+            $clientCoordinates['longitude'] ?? null,
         );
 
         $schedulesProposals = ScheduleProposal::query()
@@ -445,12 +472,20 @@ class DashboardService
 
                 'users.name as provider_name',
 
+                'provider_address.latitude as provider_latitude',
+                'provider_address.longitude as provider_longitude',
+                'provider_address.zip_code as provider_zip_code',
+
                 $proposalsDistanceSelect,
 
                 'provider_media.path as provider_photo_path',
             ])
             ->get();
 
+        $this->zipCodeCoordinatesService->preload(
+            $schedulesProposals->whereNull('distance_km')->pluck('provider_zip_code')
+        );
+
         $custom_schedules_with_no_proposals = Schedule::where('client_id', $cliente->id)
             ->where('schedule_type', 'custom')
             ->where('status', 'pending')
@@ -458,12 +493,28 @@ class DashboardService
             ->doesntHave('proposals')
             ->get();
 
-        $schedulesProposals->each(function ($item) {
+        $schedulesProposals->each(function ($item) use ($clientDistanceAddress) {
+            if ($item->distance_km === null) {
+                $item->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
+                    $clientDistanceAddress?->latitude !== null ? (float) $clientDistanceAddress->latitude : null,
+                    $clientDistanceAddress?->longitude !== null ? (float) $clientDistanceAddress->longitude : null,
+                    $clientDistanceAddress?->zip_code,
+                    $item->provider_latitude !== null ? (float) $item->provider_latitude : null,
+                    $item->provider_longitude !== null ? (float) $item->provider_longitude : null,
+                    $item->provider_zip_code,
+                );
+            }
+
             $item->provider_photo = $item->provider_photo_path
                 ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
                 : null;
 
-            unset($item->provider_photo_path);
+            unset(
+                $item->provider_photo_path,
+                $item->provider_latitude,
+                $item->provider_longitude,
+                $item->provider_zip_code,
+            );
         });
 
         $todaySchedules = Schedule::with([
@@ -612,7 +663,7 @@ class DashboardService
         ];
 
         $solicitations = Schedule::with([
-            'address:district,source_id,source,id,latitude,longitude',
+            'address:district,source_id,source,id,zip_code,latitude,longitude',
         ])
             ->where('schedules.provider_id', $provider->id)
             ->where('schedules.status', 'pending')
@@ -666,21 +717,20 @@ class DashboardService
             ->orderBy('schedules.date', 'asc')
             ->get();
 
-        $providerLat = $address?->latitude  !== null ? (float) $address->latitude  : null;
-        $providerLng = $address?->longitude !== null ? (float) $address->longitude : null;
-
-        $solicitations->each(function ($solicitation) use ($providerLat, $providerLng) {
+        $solicitations->each(function ($solicitation) use ($address) {
             $solicitation->customer_photo = $solicitation->customer_photo_path
                 ? Storage::temporaryUrl($solicitation->customer_photo_path, now()->addMinutes(60))
                 : null;
 
             unset($solicitation->customer_photo_path);
 
-            $solicitation->distance_km = DistanceService::calculate(
-                $providerLat,
-                $providerLng,
+            $solicitation->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
+                $address?->latitude !== null ? (float) $address->latitude : null,
+                $address?->longitude !== null ? (float) $address->longitude : null,
+                $address?->zip_code,
                 $solicitation->address?->latitude  !== null ? (float) $solicitation->address->latitude  : null,
                 $solicitation->address?->longitude !== null ? (float) $solicitation->address->longitude : null,
+                $solicitation->address?->zip_code,
             );
         });
 
@@ -733,7 +783,7 @@ class DashboardService
             unset($s->customer_photo_path);
         });
 
-        $nextSchedules = Schedule::with('address:district,address,number,source_id,source,id,latitude,longitude')
+        $nextSchedules = Schedule::with('address:district,address,number,source_id,source,id,zip_code,latitude,longitude')
             ->where('schedules.provider_id', $provider->id)
             ->whereIn('schedules.status', ['accepted', 'paid'])
             ->whereDate('schedules.date', '>=', now()->toDateString())
@@ -758,18 +808,20 @@ class DashboardService
             ->orderBy('schedules.date', 'asc')
             ->get();
 
-        $nextSchedules->each(function ($schedule) use ($providerLat, $providerLng) {
+        $nextSchedules->each(function ($schedule) use ($address) {
             $schedule->customer_photo = $schedule->customer_photo_path
                 ? Storage::temporaryUrl($schedule->customer_photo_path, now()->addMinutes(60))
                 : null;
 
             unset($schedule->customer_photo_path);
 
-            $schedule->distance_km = DistanceService::calculate(
-                $providerLat,
-                $providerLng,
+            $schedule->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
+                $address?->latitude !== null ? (float) $address->latitude : null,
+                $address?->longitude !== null ? (float) $address->longitude : null,
+                $address?->zip_code,
                 $schedule->address?->latitude  !== null ? (float) $schedule->address->latitude  : null,
                 $schedule->address?->longitude !== null ? (float) $schedule->address->longitude : null,
+                $schedule->address?->zip_code,
             );
         });
 

+ 39 - 4
app/Services/SearchService.php

@@ -13,7 +13,9 @@ use Illuminate\Support\Facades\Storage;
 
 class SearchService
 {
-    public function __construct() {}
+    public function __construct(
+        private readonly ZipCodeCoordinatesService $zipCodeCoordinatesService,
+    ) {}
 
     public function buscaPrestadores(?string $name = null, ?string $date = null): array
     {
@@ -33,9 +35,15 @@ class SearchService
 
         $clientDistanceAddress = $this->addressForDistance($cliente->id, $clientPrimaryAddress);
 
-        $distanceSelect = $this->distanceSelect(
-            $clientDistanceAddress?->latitude !== null  ? (float) $clientDistanceAddress->latitude  : null,
+        $clientCoordinates = $this->zipCodeCoordinatesService->resolve(
+            $clientDistanceAddress?->latitude !== null ? (float) $clientDistanceAddress->latitude : null,
             $clientDistanceAddress?->longitude !== null ? (float) $clientDistanceAddress->longitude : null,
+            $clientDistanceAddress?->zip_code,
+        );
+
+        $distanceSelect = $this->distanceSelect(
+            $clientCoordinates['latitude'] ?? null,
+            $clientCoordinates['longitude'] ?? null,
         );
 
         $baseQuery = Provider::leftJoin(
@@ -77,6 +85,7 @@ class SearchService
                 'providers.id as provider_id',
                 'providers.profile_media_id',
                 'provider_user.name as provider_name',
+                'provider_address.zip_code as provider_zip_code',
                 'provider_address.district',
                 'provider_address.latitude as provider_latitude',
                 'provider_address.longitude as provider_longitude',
@@ -114,6 +123,29 @@ class SearchService
             $providers = $baseQuery->get();
         }
 
+        $this->zipCodeCoordinatesService->preload(
+            $providers->whereNull('distance_km')->pluck('provider_zip_code')
+        );
+
+        $providers->each(function ($provider) use ($clientDistanceAddress) {
+            if ($provider->distance_km !== null) {
+                return;
+            }
+
+            $provider->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
+                $clientDistanceAddress?->latitude !== null ? (float) $clientDistanceAddress->latitude : null,
+                $clientDistanceAddress?->longitude !== null ? (float) $clientDistanceAddress->longitude : null,
+                $clientDistanceAddress?->zip_code,
+                $provider->provider_latitude !== null ? (float) $provider->provider_latitude : null,
+                $provider->provider_longitude !== null ? (float) $provider->provider_longitude : null,
+                $provider->provider_zip_code,
+            );
+        });
+
+        $providers = $providers
+            ->sortBy(fn ($provider) => $provider->distance_km ?? PHP_FLOAT_MAX)
+            ->values();
+
         $filtered = $providers->when(
             $date,
             fn ($collection) => $collection->whereIn(
@@ -150,7 +182,10 @@ class SearchService
             $arr['daily_price_4h']      = $this->applyCreditCardFee($arr['daily_price_4h'] ?? null);
             $arr['daily_price_2h']      = $this->applyCreditCardFee($arr['daily_price_2h'] ?? null);
 
-            unset($arr['profile_media_id']);
+            unset(
+                $arr['profile_media_id'],
+                $arr['provider_zip_code'],
+            );
 
             return $arr;
         })->values()->toArray();

+ 192 - 0
app/Services/ZipCodeCoordinatesService.php

@@ -0,0 +1,192 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Http\Client\Pool;
+use Illuminate\Http\Client\Response;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Http;
+use Throwable;
+
+class ZipCodeCoordinatesService
+{
+    private array $resolvedZipCodes = [];
+
+    public function resolve(?float $latitude, ?float $longitude, ?string $zipCode): ?array
+    {
+        if ($latitude !== null && $longitude !== null) {
+            return compact('latitude', 'longitude');
+        }
+
+        return $this->findByZipCode($zipCode);
+    }
+
+    public function calculateDistance(
+        ?float  $originLatitude,
+        ?float  $originLongitude,
+        ?string $originZipCode,
+        ?float  $targetLatitude,
+        ?float  $targetLongitude,
+        ?string $targetZipCode,
+    ): ?float {
+        $origin = $this->resolve($originLatitude, $originLongitude, $originZipCode);
+        $target = $this->resolve($targetLatitude, $targetLongitude, $targetZipCode);
+
+        return DistanceService::calculate(
+            $origin['latitude']  ?? null,
+            $origin['longitude'] ?? null,
+            $target['latitude']  ?? null,
+            $target['longitude'] ?? null,
+        );
+    }
+
+    public function findByZipCode(?string $zipCode): ?array
+    {
+        $zipCode = $this->normalize($zipCode);
+
+        if ($zipCode === null) {
+            return null;
+        }
+
+        if (array_key_exists($zipCode, $this->resolvedZipCodes)) {
+            return $this->resolvedZipCodes[$zipCode];
+        }
+
+        if ($this->restoreFromCache($zipCode)) {
+            return $this->resolvedZipCodes[$zipCode];
+        }
+
+        $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 === []) {
+            return;
+        }
+
+        try {
+            $baseUrl = rtrim((string) config('services.brasil_api.base_url'), '/');
+
+            $responses = Http::pool(fn (Pool $pool) => array_map(
+                fn (string $zipCode) => $pool
+                    ->as($zipCode)
+                    ->acceptJson()
+                    ->connectTimeout((int) config('services.brasil_api.connect_timeout', 2))
+                    ->timeout((int) config('services.brasil_api.timeout', 3))
+                    ->get("{$baseUrl}/api/cep/v2/{$zipCode}"),
+                $pending,
+            ));
+        } catch (Throwable) {
+            $responses = [];
+        }
+
+        foreach ($pending as $zipCode) {
+            $response = $responses[$zipCode] ?? null;
+
+            $coordinates = $response instanceof Response
+                ? $this->coordinatesFromResponse($response)
+                : 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
+    {
+        $cached = Cache::get("zip-code-coordinates:{$zipCode}");
+
+        if (! is_array($cached) || ! array_key_exists('found', $cached)) {
+            return false;
+        }
+
+        $this->resolvedZipCodes[$zipCode] = $cached['found']
+            ? $cached['coordinates']
+            : null;
+
+        return true;
+    }
+
+    private function remember(string $zipCode, ?array $coordinates): void
+    {
+        $this->resolvedZipCodes[$zipCode] = $coordinates;
+
+        Cache::put(
+            "zip-code-coordinates:{$zipCode}",
+            [
+                'found'       => $coordinates !== null,
+                'coordinates' => $coordinates,
+            ],
+            now()->addSeconds(
+                $coordinates !== null
+                    ? (int) config('services.brasil_api.cep_cache_ttl', 2592000)
+                    : (int) config('services.brasil_api.cep_failure_cache_ttl', 600)
+            )
+        );
+    }
+
+    private function requestCoordinates(string $zipCode): ?array
+    {
+        try {
+            $response = Http::baseUrl(rtrim((string) config('services.brasil_api.base_url'), '/'))
+                ->acceptJson()
+                ->connectTimeout((int) config('services.brasil_api.connect_timeout', 2))
+                ->timeout((int) config('services.brasil_api.timeout', 3))
+                ->get("api/cep/v2/{$zipCode}");
+
+            return $this->coordinatesFromResponse($response);
+        } catch (Throwable) {
+            return null;
+        }
+    }
+
+    private function coordinatesFromResponse(Response $response): ?array
+    {
+        if (! $response->successful()) {
+            return null;
+        }
+
+        $latitude  = $response->json('location.coordinates.latitude');
+        $longitude = $response->json('location.coordinates.longitude');
+
+        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');
+    }
+}

+ 8 - 0
config/services.php

@@ -40,6 +40,14 @@ return [
         'model'   => env('GEMINI_MODEL'),
     ],
 
+    'brasil_api' => [
+        'base_url'              => env('BRASIL_API_BASE_URL', 'https://brasilapi.com.br'),
+        'connect_timeout'       => env('BRASIL_API_CONNECT_TIMEOUT', 2),
+        'timeout'               => env('BRASIL_API_TIMEOUT', 3),
+        'cep_cache_ttl'         => env('BRASIL_API_CEP_CACHE_TTL', 2592000),
+        'cep_failure_cache_ttl' => env('BRASIL_API_CEP_FAILURE_CACHE_TTL', 600),
+    ],
+
     'support' => [
         'email' => env('SUPPORT_EMAIL'),
         'from'  => env('SUPPORT_FROM_EMAIL', env('MAIL_FROM_ADDRESS')),