فهرست منبع

feat: add endpoints para listar fotos pendentes de aprovacao e para aprovar fotos

Gustavo Mantovani 1 هفته پیش
والد
کامیت
14599876a0

+ 32 - 0
app/Http/Controllers/MediaController.php

@@ -2,7 +2,9 @@
 
 namespace App\Http\Controllers;
 
+use App\Http\Requests\ApproveProfileMediaRequest;
 use App\Http\Requests\MediaRequest;
+use App\Http\Requests\PendingProfileMediaRequest;
 use App\Http\Resources\MediaResource;
 use App\Services\MediaService;
 use Illuminate\Http\JsonResponse;
@@ -57,4 +59,34 @@ class MediaController extends Controller
             code:    204,
         );
     }
+
+    //
+
+    public function pendingProfiles(PendingProfileMediaRequest $request): JsonResponse
+    {
+        $paginated = $this->service->getPendingProfileMedia(
+            page:    $request->integer('page', 1),
+            perPage: $request->integer('per_page', 10),
+        );
+
+        return $this->successResponse(payload: [
+            'data'  => MediaResource::collection($paginated->items()),
+            'total' => $paginated->total(),
+            'from'  => $paginated->firstItem() ?? 0,
+            'to'    => $paginated->lastItem()  ?? 0,
+        ]);
+    }
+
+    public function approveProfile(ApproveProfileMediaRequest $request): JsonResponse
+    {
+        $media = $this->service->approveProfileMedia(
+            source:   $request->validated('source'),
+            sourceId: $request->integer('source_id'),
+        );
+
+        return $this->successResponse(
+            payload: new MediaResource($media),
+            message: __('messages.updated'),
+        );
+    }
 }

+ 29 - 0
app/Http/Requests/ApproveProfileMediaRequest.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ApproveProfileMediaRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return $this->user()?->isAdmin() ?? false;
+    }
+
+    protected function prepareForValidation(): void
+    {
+        $this->merge([
+            'source'    => $this->route('source'),
+            'source_id' => $this->route('sourceId'),
+        ]);
+    }
+
+    public function rules(): array
+    {
+        return [
+            'source'    => 'required|string|in:client,provider',
+            'source_id' => 'required|integer|min:1',
+        ];
+    }
+}

+ 22 - 0
app/Http/Requests/PendingProfileMediaRequest.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class PendingProfileMediaRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return $this->user()?->canModerateClients()
+            && $this->user()->canModerateProviders();
+    }
+
+    public function rules(): array
+    {
+        return [
+            'page'     => 'sometimes|integer|min:1',
+            'per_page' => 'sometimes|integer|min:1|max:100',
+        ];
+    }
+}

+ 14 - 9
app/Models/Client.php

@@ -92,22 +92,27 @@ class Client extends Model
 
     public function profileMedia(): BelongsTo
     {
-        $authenticatedUserId = Auth::id();
+        $authenticatedUser = Auth::user();
 
-        return $this->belongsTo(Media::class, 'profile_media_id')
+        $relation = $this->belongsTo(Media::class, 'profile_media_id')
             ->join('clients as profile_media_clients', function ($join) {
                 $join->on('profile_media_clients.id', '=', 'media.source_id')
                     ->on('profile_media_clients.profile_media_id', '=', 'media.id');
             })
             ->where('media.source', 'client')
-            ->where(function ($query) use ($authenticatedUserId) {
-                $query->where('profile_media_clients.selfie_verified', true);
-
-                if ($authenticatedUserId !== null) {
-                    $query->orWhere('profile_media_clients.user_id', $authenticatedUserId);
-                }
-            })
             ->select('media.*');
+
+        if ($authenticatedUser?->canModerateClients()) {
+            return $relation;
+        }
+
+        return $relation->where(function ($query) use ($authenticatedUser) {
+            $query->where('profile_media_clients.selfie_verified', true);
+
+            if ($authenticatedUser !== null) {
+                $query->orWhere('profile_media_clients.user_id', $authenticatedUser->id);
+            }
+        });
     }
 
     public function schedules()

+ 14 - 9
app/Models/Provider.php

@@ -153,22 +153,27 @@ class Provider extends Model
 
     public function profileMedia(): BelongsTo
     {
-        $authenticatedUserId = Auth::id();
+        $authenticatedUser = Auth::user();
 
-        return $this->belongsTo(Media::class, 'profile_media_id')
+        $relation = $this->belongsTo(Media::class, 'profile_media_id')
             ->join('providers as profile_media_providers', function ($join) {
                 $join->on('profile_media_providers.id', '=', 'media.source_id')
                     ->on('profile_media_providers.profile_media_id', '=', 'media.id');
             })
             ->where('media.source', 'provider')
-            ->where(function ($query) use ($authenticatedUserId) {
-                $query->where('profile_media_providers.selfie_verified', true);
-
-                if ($authenticatedUserId !== null) {
-                    $query->orWhere('profile_media_providers.user_id', $authenticatedUserId);
-                }
-            })
             ->select('media.*');
+
+        if ($authenticatedUser?->canModerateProviders()) {
+            return $relation;
+        }
+
+        return $relation->where(function ($query) use ($authenticatedUser) {
+            $query->where('profile_media_providers.selfie_verified', true);
+
+            if ($authenticatedUser !== null) {
+                $query->orWhere('profile_media_providers.user_id', $authenticatedUser->id);
+            }
+        });
     }
 
     //

+ 7 - 0
app/Observers/ClientObserver.php

@@ -6,6 +6,13 @@ use App\Models\Client;
 
 class ClientObserver
 {
+    public function creating(Client $client): void
+    {
+        if ($client->profile_media_id !== null) {
+            $client->selfie_verified = false;
+        }
+    }
+
     public function updating(Client $client): void
     {
         if ($client->isDirty('profile_media_id')) {

+ 7 - 0
app/Observers/ProviderObserver.php

@@ -6,6 +6,13 @@ use App\Models\Provider;
 
 class ProviderObserver
 {
+    public function creating(Provider $provider): void
+    {
+        if ($provider->profile_media_id !== null) {
+            $provider->selfie_verified = false;
+        }
+    }
+
     public function updating(Provider $provider): void
     {
         if ($provider->isDirty('profile_media_id')) {

+ 0 - 102
app/Services/DashboardService.php

@@ -20,7 +20,6 @@ use Illuminate\Auth\Access\AuthorizationException;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
 
 class DashboardService
@@ -222,75 +221,6 @@ class DashboardService
             ) AS provider_address
         ");
 
-        $providersCloseDebugQuery = Provider::leftJoin(
-            'users as provider_user',
-            'provider_user.id',
-            '=',
-            'providers.user_id'
-        )
-        ->visibleToCustomers()
-        ->leftJoin(
-            $providerAddressLatestSubquery,
-            'provider_address.source_id',
-            '=',
-            'providers.id'
-        )
-        ->whereNull('providers.deleted_at');
-
-        Log::info('Contexto de prestadores próximos no dashboard', [
-            'client_id'                       => $cliente->id,
-            'user_id'                         => $user->id,
-            'client_primary_address_id'       => $clientPrimaryAddress?->id,
-            'client_primary_address_city_id'  => $clientPrimaryAddress?->city_id,
-            'client_primary_address_state_id' => $clientPrimaryAddress?->state_id,
-            'client_distance_address_id'      => $clientDistanceAddress?->id,
-            'client_distance_zip_code'        => $clientDistanceAddress?->zip_code,
-            'client_coordinates'              => $clientCoordinates,
-            'blocked_provider_ids_count'      => $blockedProviderIds->count(),
-            'providers_working_days_count'    => $providersWithWorkingDays->count(),
-        ]);
-
-        Log::info('Contagem dos filtros de prestadores próximos no dashboard', [
-            'visible_with_bank' => (clone $providersCloseDebugQuery)
-                ->distinct('providers.id')
-                ->count('providers.id'),
-
-            'with_provider_address' => (clone $providersCloseDebugQuery)
-                ->whereNotNull('provider_address.id')
-                ->distinct('providers.id')
-                ->count('providers.id'),
-
-            'after_city_filter' => (clone $providersCloseDebugQuery)
-                ->whereNotNull('provider_address.id')
-                ->when(
-                    $clientPrimaryAddress?->city_id,
-                    fn ($query, int $cityId) => $query->where('provider_address.city_id', $cityId)
-                )
-                ->distinct('providers.id')
-                ->count('providers.id'),
-
-            'after_block_filter' => (clone $providersCloseDebugQuery)
-                ->whereNotNull('provider_address.id')
-                ->when(
-                    $clientPrimaryAddress?->city_id,
-                    fn ($query, int $cityId) => $query->where('provider_address.city_id', $cityId)
-                )
-                ->whereNotIn('providers.id', $blockedProviderIds)
-                ->distinct('providers.id')
-                ->count('providers.id'),
-
-            'after_working_days_filter' => (clone $providersCloseDebugQuery)
-                ->whereNotNull('provider_address.id')
-                ->when(
-                    $clientPrimaryAddress?->city_id,
-                    fn ($query, int $cityId) => $query->where('provider_address.city_id', $cityId)
-                )
-                ->whereNotIn('providers.id', $blockedProviderIds)
-                ->whereIn('providers.id', $providersWithWorkingDays)
-                ->distinct('providers.id')
-                ->count('providers.id'),
-        ]);
-
         $providersClose = Provider::leftJoin(
             'users as provider_user',
             'provider_user.id',
@@ -344,28 +274,6 @@ class DashboardService
         ->orderByRaw('distance_km ASC NULLS LAST')
         ->get();
 
-        Log::info('Resultado da consulta de prestadores próximos no dashboard', [
-            'count'                => $providersClose->count(),
-            'provider_ids_sample'  => $providersClose->pluck('provider_id')->take(20)->values()->all(),
-            'null_distance_count'  => $providersClose->whereNull('distance_km')->count(),
-            'provider_city_filter' => $clientPrimaryAddress?->city_id,
-            'items_sample'         => $providersClose->take(5)->map(fn ($provider) => [
-                'provider_id'        => $provider->provider_id,
-                'provider_name'      => $provider->provider_name,
-                'provider_zip_code'  => $provider->provider_zip_code,
-                'provider_latitude'  => $provider->provider_latitude,
-                'provider_longitude' => $provider->provider_longitude,
-                'distance_km'        => $provider->distance_km,
-            ])->values()->all(),
-        ]);
-
-        if ($providersClose->whereNull('distance_km')->isEmpty()) {
-            Log::info('Fallback por CEP de prestadores próximos ignorado no dashboard', [
-                'reason' => 'all_distances_calculated_by_sql',
-                'count'  => $providersClose->count(),
-            ]);
-        }
-
         $this->zipCodeCoordinatesService->preload(
             $providersClose->whereNull('distance_km')->pluck('provider_zip_code')
         );
@@ -401,16 +309,6 @@ class DashboardService
             ->sortBy(fn ($provider) => $provider->distance_km ?? PHP_FLOAT_MAX)
             ->values();
 
-        Log::info('Resultado final de prestadores próximos no dashboard', [
-            'count' => $providersClose->count(),
-            'items_sample' => $providersClose->take(5)->map(fn ($provider) => [
-                'provider_id'   => $provider->provider_id,
-                'provider_name' => $provider->provider_name,
-                'district'      => $provider->district,
-                'distance_km'   => $provider->distance_km,
-            ])->values()->all(),
-        ]);
-
         $pendingSchedules = Schedule::with([
             'address:district,address,number,source_id,source,id,address_type',
         ])

+ 59 - 0
app/Services/MediaService.php

@@ -2,11 +2,17 @@
 
 namespace App\Services;
 
+use App\Enums\ApprovalStatusEnum;
+use App\Models\Client;
 use App\Models\Media;
+use App\Models\Provider;
 use App\Traits\RemoveArchiveS3;
 use App\Traits\UploadsFile;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\ModelNotFoundException;
 use Illuminate\Http\UploadedFile;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
 class MediaService
@@ -57,6 +63,59 @@ class MediaService
 
     //
 
+    public function getPendingProfileMedia(int $page = 1, int $perPage = 10): LengthAwarePaginator
+    {
+        return Media::query()
+            ->where(function ($query) {
+                $query->where(function ($query) {
+                    $query->where('media.source', 'provider')
+                        ->whereExists(function ($query) {
+                            $query->selectRaw('1')
+                                ->from('providers')
+                                ->whereColumn('providers.id', 'media.source_id')
+                                ->whereColumn('providers.profile_media_id', 'media.id')
+                                ->where('providers.approval_status', ApprovalStatusEnum::ACCEPTED->value)
+                                ->where('providers.selfie_verified', false)
+                                ->whereNull('providers.deleted_at');
+                        });
+                })->orWhere(function ($query) {
+                    $query->where('media.source', 'client')
+                        ->whereExists(function ($query) {
+                            $query->selectRaw('1')
+                                ->from('clients')
+                                ->whereColumn('clients.id', 'media.source_id')
+                                ->whereColumn('clients.profile_media_id', 'media.id')
+                                ->where('clients.selfie_verified', false)
+                                ->whereNull('clients.deleted_at');
+                        });
+                });
+            })
+            ->orderBy('media.created_at')
+            ->paginate($perPage, ['media.*'], 'page', $page);
+    }
+
+    public function approveProfileMedia(string $source, int $sourceId): Media
+    {
+        return DB::transaction(function () use ($source, $sourceId) {
+            $profile = match ($source) {
+                'client'   => Client::query()->lockForUpdate()->findOrFail($sourceId),
+                'provider' => Provider::query()->lockForUpdate()->findOrFail($sourceId),
+            };
+
+            $profile->load('profileMedia');
+
+            $media = $profile->profileMedia;
+
+            if ($media === null) {
+                throw (new ModelNotFoundException)->setModel(Media::class);
+            }
+
+            $profile->update(['selfie_verified' => true]);
+
+            return $media;
+        });
+    }
+
     public function createFromFile(UploadedFile $file, string $folder, string $source, int $sourceId, ?string $filename = null): Media
     {
         Log::warning('[avatar-upload] MediaService::createFromFile — iniciando upload para S3', [

+ 5 - 2
app/Services/ProviderService.php

@@ -315,11 +315,14 @@ class ProviderService
     public function approve(int $id): Provider
     {
         [$provider, $wasAccepted] = DB::transaction(function () use ($id) {
-            $provider = Provider::findOrFail($id);
+            $provider = Provider::query()->lockForUpdate()->findOrFail($id);
 
             $wasAccepted = $provider->approval_status === ApprovalStatusEnum::ACCEPTED;
 
-            $provider->update(['approval_status' => ApprovalStatusEnum::ACCEPTED->value]);
+            $provider->update([
+                'approval_status' => ApprovalStatusEnum::ACCEPTED->value,
+                'selfie_verified' => true,
+            ]);
 
             return [$provider->fresh(['user', 'profileMedia']), $wasAccepted];
         });

+ 9 - 33
app/Services/ZipCodeCoordinatesService.php

@@ -103,19 +103,13 @@ class ZipCodeCoordinatesService
             return;
         }
 
-        Log::channel('google_geocoding')->info('Consulta ao Google Geocoding necessária para as coordenadas do CEP', [
-            'mode'             => 'preload',
-            'pending_count'    => count($pending),
-            'zip_codes_sample' => array_slice(array_values($pending), 0, 20),
-            'connect_timeout'  => (int) config('services.google_geocoding.connect_timeout', 2),
-            'timeout'          => (int) config('services.google_geocoding.timeout', 5),
-            'ip_resolve'       => 'ipv4',
-        ]);
-
         try {
-            $baseUrl = rtrim((string) config('services.google_geocoding.base_url'), '/');
+            Log::channel('google_geocoding')->info('Requisição em lote ao Google Geocoding', [
+                'request_count'    => count($pending),
+                'zip_codes_sample' => array_slice(array_values($pending), 0, 20),
+            ]);
 
-            $startedAt = microtime(true);
+            $baseUrl = rtrim((string) config('services.google_geocoding.base_url'), '/');
 
             $responses = Http::pool(fn (Pool $pool) => array_map(
                 fn (string $zipCode) => $pool
@@ -127,13 +121,7 @@ class ZipCodeCoordinatesService
                     ->get("{$baseUrl}/maps/api/geocode/json", $this->googleQuery($zipCode)),
                 $pending,
             ));
-        } catch (Throwable $exception) {
-            Log::channel('google_geocoding')->warning('Falha ao pré-carregar coordenadas de CEP pelo Google Geocoding', [
-                'pending_count' => count($pending),
-                'elapsed_ms'    => isset($startedAt) ? round((microtime(true) - $startedAt) * 1000, 2) : null,
-                'error'         => $exception->getMessage(),
-            ]);
-
+        } catch (Throwable) {
             $responses = [];
         }
 
@@ -252,15 +240,9 @@ class ZipCodeCoordinatesService
 
     private function requestCoordinates(string $zipCode): ?array
     {
-        $startedAt = microtime(true);
-
         try {
-            Log::channel('google_geocoding')->info('Consulta de coordenadas de CEP no Google Geocoding iniciada', [
-                'zip_code'        => $zipCode,
-                'base_url'        => rtrim((string) config('services.google_geocoding.base_url'), '/'),
-                'connect_timeout' => (int) config('services.google_geocoding.connect_timeout', 2),
-                'timeout'         => (int) config('services.google_geocoding.timeout', 5),
-                'ip_resolve'      => 'ipv4',
+            Log::channel('google_geocoding')->info('Requisição ao Google Geocoding', [
+                'zip_code' => $zipCode,
             ]);
 
             $baseUrl = rtrim((string) config('services.google_geocoding.base_url'), '/');
@@ -272,13 +254,7 @@ class ZipCodeCoordinatesService
                 ->get("{$baseUrl}/maps/api/geocode/json", $this->googleQuery($zipCode));
 
             return $this->coordinatesFromResponse($response);
-        } catch (Throwable $exception) {
-            Log::channel('google_geocoding')->warning('Falha na consulta de coordenadas de CEP ao Google Geocoding', [
-                'zip_code'   => $zipCode,
-                'elapsed_ms' => round((microtime(true) - $startedAt) * 1000, 2),
-                'error'      => $exception->getMessage(),
-            ]);
-
+        } catch (Throwable) {
             return null;
         }
     }

+ 4 - 0
routes/authRoutes/media.php

@@ -3,6 +3,10 @@
 use App\Http\Controllers\MediaController;
 use Illuminate\Support\Facades\Route;
 
+Route::get('/profile-media/pending', [MediaController::class, 'pendingProfiles'])->middleware('permission:config.media,view');
+
+Route::patch('/profile-media/{source}/{sourceId}/approve', [MediaController::class, 'approveProfile'])->middleware('permission:config.media,edit');
+
 Route::get('/media',         [MediaController::class, 'index'])->middleware('permission:config.media,view');
 Route::post('/media',        [MediaController::class, 'store'])->middleware('permission:config.media,add');
 Route::get('/media/{id}',    [MediaController::class, 'show'])->middleware('permission:config.media,view');