9 Commity 719e3b8620 ... 1f912724c6

Autor SHA1 Wiadomość Data
  Gustavo Mantovani 1f912724c6 refactor: add retorno do user (client ou provider) no retorno de fotos pendentes de aprovacao 1 tydzień temu
  Gustavo Mantovani 14599876a0 feat: add endpoints para listar fotos pendentes de aprovacao e para aprovar fotos 1 tydzień temu
  Gustavo Mantovani f3ae83fc05 feat: add mesma logica de validacao da foto de perfil que existe em prestador para o cliente 1 tydzień temu
  Gustavo Mantovani 8c1859762e feat: add bloqueio automatico de foto de perfil caso nao aprovada e bloqueia novamente mediante atualizacao de foto 1 tydzień temu
  Gustavo Mantovani 6e34471d14 feat: add edicao de genero para os endpoints de user assim como retorna esse value ao puxar dados proprios 1 tydzień temu
  Gustavo Mantovani 633a3c7cab chore: .env.example 1 tydzień temu
  Gustavo Mantovani 5bc1aa2540 feat: add gender nos providers + adiciona obrigatoriedade de campos em cadastros + adicao de config para determinar se saque pix e se sms estao habilitados 1 tydzień temu
  Gustavo Mantovani 69ee44049e refactor: services de payment agora lidam com package services, adicionadas novas exceptions customs + messages e try catch nos controllers, ajuste em payloads de arrays para usar data_get, rename de cart para package service e restricao para somente um mesmo provider estar nessas schedules 1 tydzień temu
  Gustavo Mantovani 73ec7aaa33 refactor: fluxo de cart agora permite somente schedules do mesmo prestador 1 tydzień temu
100 zmienionych plików z 1279 dodań i 1096 usunięć
  1. 4 2
      .env.example
  2. 7 7
      app/Broadcasting/Entity/WebsocketEventData.php
  3. 54 39
      app/Commands/CreateCrud.php
  4. 7 1
      app/Commands/RefreshPermissions.php
  5. 5 5
      app/Commands/TestWebsocketEvent.php
  6. 3 3
      app/Data/Pagarme/Order/OrderResponseData.php
  7. 9 17
      app/Data/Pagarme/PagarmeResponseData.php
  8. 29 0
      app/Enums/GenderEnum.php
  9. 12 0
      app/Enums/PaymentStatusEnum.php
  10. 1 1
      app/Enums/ServicePackageStatusEnum.php
  11. 9 0
      app/Exceptions/PaymentException.php
  12. 9 0
      app/Exceptions/PaymentFailedException.php
  13. 9 0
      app/Exceptions/ScheduleStatusTransitionException.php
  14. 42 46
      app/Http/Controllers/AuthController.php
  15. 5 0
      app/Http/Controllers/BankController.php
  16. 0 82
      app/Http/Controllers/CartController.php
  17. 0 50
      app/Http/Controllers/CartItemController.php
  18. 2 2
      app/Http/Controllers/ChatbotController.php
  19. 1 4
      app/Http/Controllers/ClientCalendarController.php
  20. 2 2
      app/Http/Controllers/ClientController.php
  21. 28 2
      app/Http/Controllers/Controller.php
  22. 18 36
      app/Http/Controllers/CustomScheduleController.php
  23. 5 12
      app/Http/Controllers/DashboardController.php
  24. 32 0
      app/Http/Controllers/MediaController.php
  25. 48 37
      app/Http/Controllers/PaymentController.php
  26. 1 4
      app/Http/Controllers/ProviderCalendarController.php
  27. 2 2
      app/Http/Controllers/ProviderController.php
  28. 9 2
      app/Http/Controllers/ProviderWithdrawalController.php
  29. 1 1
      app/Http/Controllers/ProviderWorkingDayController.php
  30. 1 1
      app/Http/Controllers/ReviewController.php
  31. 12 43
      app/Http/Controllers/ScheduleController.php
  32. 1 4
      app/Http/Controllers/SearchController.php
  33. 15 0
      app/Http/Controllers/ServiceConfigurationController.php
  34. 82 0
      app/Http/Controllers/ServicePackageController.php
  35. 50 0
      app/Http/Controllers/ServicePackageItemController.php
  36. 1 1
      app/Http/Controllers/SupportRequestController.php
  37. 20 16
      app/Http/Controllers/UserController.php
  38. 5 2
      app/Http/Middleware/CheckPermission.php
  39. 40 30
      app/Http/Middleware/PerformanceMonitor.php
  40. 1 0
      app/Http/Middleware/SetUserLanguage.php
  41. 29 0
      app/Http/Requests/ApproveProfileMediaRequest.php
  42. 6 6
      app/Http/Requests/ClientFavoriteProviderRequest.php
  43. 14 14
      app/Http/Requests/ClientPaymentMethodRequest.php
  44. 1 1
      app/Http/Requests/ClientProviderBlockRequest.php
  45. 7 0
      app/Http/Requests/ClientRequest.php
  46. 2 2
      app/Http/Requests/CustomScheduleAvailableRequest.php
  47. 3 3
      app/Http/Requests/CustomScheduleRequest.php
  48. 23 0
      app/Http/Requests/PayScheduleRequest.php
  49. 23 0
      app/Http/Requests/PayServicePackageRequest.php
  50. 22 0
      app/Http/Requests/PendingProfileMediaRequest.php
  51. 1 1
      app/Http/Requests/ProviderClientBlockRequest.php
  52. 12 4
      app/Http/Requests/ProviderRequest.php
  53. 25 17
      app/Http/Requests/RegisterClientRequest.php
  54. 53 74
      app/Http/Requests/RegisterProviderRequest.php
  55. 1 1
      app/Http/Requests/ReviewImprovementRequest.php
  56. 21 28
      app/Http/Requests/ScheduleRequest.php
  57. 5 5
      app/Http/Requests/ServicePackageItemRequest.php
  58. 30 1
      app/Http/Requests/ServicePackageRequest.php
  59. 2 0
      app/Http/Requests/UserRequest.php
  60. 3 3
      app/Http/Resources/AuthResource.php
  61. 0 37
      app/Http/Resources/CartItemResource.php
  62. 0 4
      app/Http/Resources/CityResource.php
  63. 7 3
      app/Http/Resources/ClientFavoriteProviderResource.php
  64. 9 4
      app/Http/Resources/ClientProviderBlockResource.php
  65. 3 2
      app/Http/Resources/ClientResource.php
  66. 0 4
      app/Http/Resources/CountryResource.php
  67. 12 12
      app/Http/Resources/DashboardClienteResource.php
  68. 8 8
      app/Http/Resources/DashboardPrestadorResource.php
  69. 0 4
      app/Http/Resources/ImprovementTypeResource.php
  70. 0 4
      app/Http/Resources/MediaResource.php
  71. 11 29
      app/Http/Resources/PaymentResource.php
  72. 2 0
      app/Http/Resources/PaymentSplitResource.php
  73. 3 5
      app/Http/Resources/ProviderResource.php
  74. 1 1
      app/Http/Resources/ReviewResource.php
  75. 23 21
      app/Http/Resources/ScheduleResource.php
  76. 37 0
      app/Http/Resources/ServicePackageItemResource.php
  77. 6 5
      app/Http/Resources/ServicePackageResource.php
  78. 0 4
      app/Http/Resources/ServiceTypeResource.php
  79. 0 4
      app/Http/Resources/SpecialityResource.php
  80. 0 4
      app/Http/Resources/StateResource.php
  81. 0 4
      app/Http/Resources/SupportRequestResource.php
  82. 6 1
      app/Http/Resources/UserResource.php
  83. 28 4
      app/Models/Client.php
  84. 32 0
      app/Models/Payment.php
  85. 25 1
      app/Models/Provider.php
  86. 31 20
      app/Models/ServicePackage.php
  87. 15 15
      app/Models/ServicePackageItem.php
  88. 22 15
      app/Models/User.php
  89. 22 0
      app/Observers/ClientObserver.php
  90. 22 0
      app/Observers/ProviderObserver.php
  91. 7 0
      app/Providers/AppServiceProvider.php
  92. 1 1
      app/Rules/ScheduleBusinessRules.php
  93. 5 5
      app/Services/AddressService.php
  94. 34 34
      app/Services/AuthService.php
  95. 0 52
      app/Services/CartItemService.php
  96. 0 149
      app/Services/CartService.php
  97. 33 18
      app/Services/ClientCalendarService.php
  98. 2 2
      app/Services/ClientFavoriteProviderService.php
  99. 4 4
      app/Services/ClientPaymentMethodService.php
  100. 3 2
      app/Services/ClientProviderBlockService.php

+ 4 - 2
.env.example

@@ -41,7 +41,9 @@ FIREBASE_CREDENTIALS=/var/www/sfp_api_laravel_diarista/storage/diariaappsfp-fire
 FIREBASE_PROJECT=diariaappsfp
 FIREBASE_CACHE_STORE=redis
 
+PIX_WITHDRAWALS_ENABLED=false
 PROVIDER_DAILY_PRICE_MIN=18
+SMS_ENABLED=false
 
 PAGARME_BASE_URL=https://api.pagar.me/core/v5
 PAGARME_SECRET_KEY=
@@ -51,7 +53,7 @@ PAGARME_PLATFORM_RECIPIENT_ID=
 PAGARME_PIX_DISABLE_SPLIT=false
 PAGARME_PLATFORM_PIX_FEE_RATE=0.12
 PAGARME_PLATFORM_CREDIT_CARD_FEE_RATE=0.16
-PAGARME_PLATFORM_CART_MIN_3_SCHEDULES_FEE_RATE=0.10
+PAGARME_PLATFORM_SERVICE_PACKAGE_MIN_3_SCHEDULES_FEE_RATE=0.10
 PAGARME_TRANSFER_FEE_AMOUNT=3.67
 PAGARME_WITHDRAWAL_RELEASE_DAYS=2
 
@@ -62,4 +64,4 @@ GOOGLE_GEOCODING_BASE_URL=https://maps.googleapis.com
 GOOGLE_GEOCODING_API_KEY=
 GOOGLE_GEOCODING_CONNECT_TIMEOUT=2
 GOOGLE_GEOCODING_TIMEOUT=5
-GOOGLE_GEOCODING_CACHE_TTL=2592000
+GOOGLE_GEOCODING_CACHE_TTL=2592000

+ 7 - 7
app/Broadcasting/Entity/WebsocketEventData.php

@@ -5,21 +5,21 @@ namespace App\Broadcasting\Entity;
 final readonly class WebsocketEventData
 {
     public function __construct(
-        public string $room,
-        public mixed $data,
-        public string $event = 'event',
+        public string  $room,
+        public mixed   $data,
+        public string  $event       = 'event',
         public ?string $projectName = null
     ) {}
 
     public static function from(
         string $room,
-        mixed $data,
+        mixed  $data,
         string $event = 'event'
     ): self {
         return new self(
-            room: $room,
-            data: $data,
-            event: $event,
+            room:        $room,
+            data:        $data,
+            event:       $event,
             projectName: config('app.name')
         );
     }

+ 54 - 39
app/Commands/CreateCrud.php

@@ -26,9 +26,12 @@ class CreateCrud extends Command
     public function __construct(Filesystem $files)
     {
         parent::__construct();
+
         $this->files = $files;
     }
 
+    //
+
     public function handle(): void
     {
         $name = Str::studly($this->argument('name'));
@@ -80,48 +83,31 @@ class CreateCrud extends Command
         if ($createAll) {
             $this->newLine();
             $this->info('Next steps:');
+
             $this->info(
                 "1. Update the migration: database/migrations/*_create_{$this->getTableName(
                     $name,
                 )}_table.php",
             );
+
             $this->info(
                 "2. Update the model properties in: app/Models/{$name}.php",
             );
+
             $this->info(
                 "3. Update validation rules in: app/Http/Requests/{$name}Request.php",
             );
+
             $this->info(
                 "4. Update resource fields in: app/Http/Resources/{$name}Resource.php",
             );
+
             $this->info('5. Run: php artisan migrate');
             $this->info('6. Run: php artisan ide-helper:models -RW');
         }
     }
 
-    protected function createModel(string $name): void
-    {
-        $modelTemplate = str_replace(
-            ['{{modelName}}', '{{tableNameSnakeCase}}'],
-            [$name, $this->getTableName($name)],
-            $this->getStub('Model'),
-        );
-
-        $this->put("app/Models/{$name}.php", $modelTemplate);
-        $this->info("✓ Created Model: app/Models/{$name}.php");
-    }
-
-    protected function createService(string $name): void
-    {
-        $serviceTemplate = str_replace(
-            ['{{modelName}}'],
-            [$name],
-            $this->getStub('Service'),
-        );
-
-        $this->put("app/Services/{$name}Service.php", $serviceTemplate);
-        $this->info("✓ Created Service: app/Services/{$name}Service.php");
-    }
+    //
 
     protected function createController(string $name): void
     {
@@ -135,11 +121,41 @@ class CreateCrud extends Command
             "app/Http/Controllers/{$name}Controller.php",
             $controllerTemplate,
         );
+
         $this->info(
             "✓ Created Controller: app/Http/Controllers/{$name}Controller.php",
         );
     }
 
+    protected function createMigration(string $name): void
+    {
+        $tableName = $this->getTableName($name);
+
+        $migrationTemplate = str_replace(
+            ['{{tableNameSnakeCase}}'],
+            [$tableName],
+            $this->getStub('Migration'),
+        );
+
+        $timestamp = date('Y_m_d_His');
+        $filename  = $timestamp."_create_{$tableName}_table.php";
+
+        $this->put(database_path("migrations/{$filename}"), $migrationTemplate);
+        $this->info("✓ Created Migration: database/migrations/{$filename}");
+    }
+
+    protected function createModel(string $name): void
+    {
+        $modelTemplate = str_replace(
+            ['{{modelName}}', '{{tableNameSnakeCase}}'],
+            [$name, $this->getTableName($name)],
+            $this->getStub('Model'),
+        );
+
+        $this->put("app/Models/{$name}.php", $modelTemplate);
+        $this->info("✓ Created Model: app/Models/{$name}.php");
+    }
+
     protected function createRequest(string $name): void
     {
         $requestTemplate = str_replace(
@@ -161,6 +177,7 @@ class CreateCrud extends Command
         );
 
         $this->put("app/Http/Resources/{$name}Resource.php", $resourceTemplate);
+
         $this->info(
             "✓ Created Resource: app/Http/Resources/{$name}Resource.php",
         );
@@ -187,38 +204,36 @@ class CreateCrud extends Command
         }
 
         $this->put($routePath, $routeTemplate);
+
         $this->info(
             '✓ Created Route: routes/authRoutes/'.Str::snake($name).'.php',
         );
     }
 
-    protected function createMigration(string $name): void
+    protected function createService(string $name): void
     {
-        $tableName = $this->getTableName($name);
-
-        $migrationTemplate = str_replace(
-            ['{{tableNameSnakeCase}}'],
-            [$tableName],
-            $this->getStub('Migration'),
+        $serviceTemplate = str_replace(
+            ['{{modelName}}'],
+            [$name],
+            $this->getStub('Service'),
         );
 
-        $timestamp = date('Y_m_d_His');
-        $filename = $timestamp."_create_{$tableName}_table.php";
-
-        $this->put(database_path("migrations/{$filename}"), $migrationTemplate);
-        $this->info("✓ Created Migration: database/migrations/{$filename}");
+        $this->put("app/Services/{$name}Service.php", $serviceTemplate);
+        $this->info("✓ Created Service: app/Services/{$name}Service.php");
     }
 
-    protected function getTableName(string $name): string
-    {
-        return Str::snake(Str::plural($name));
-    }
+    //
 
     protected function getStub(string $type): string
     {
         return $this->files->get(storage_path("stubs/{$type}.stub"));
     }
 
+    protected function getTableName(string $name): string
+    {
+        return Str::snake(Str::plural($name));
+    }
+
     protected function put(string $path, string $contents): void
     {
         $this->files->put($path, $contents);

+ 7 - 1
app/Commands/RefreshPermissions.php

@@ -42,17 +42,23 @@ class RefreshPermissions extends Command
 
         try {
             // Run the Permission seeder
+
             $this->info('Running Permission seeder...');
+
             Permission::truncate();
+
             $this->permissionSeeder->run();
             $this->info('Permission seeder completed successfully.');
 
             // Run the UserTypePermission seeder
+
             $this->info('Running UserTypePermission seeder...');
+
             UserTypePermission::truncate();
+
             $this->userTypePermissionSeeder->run();
-            $this->info('UserTypePermission seeder completed successfully.');
 
+            $this->info('UserTypePermission seeder completed successfully.');
             $this->info('Permissions refresh completed successfully!');
 
             return Command::SUCCESS;

+ 5 - 5
app/Commands/TestWebsocketEvent.php

@@ -18,21 +18,23 @@ final class TestWebsocketEvent extends Command
     public function handle(): int
     {
         try {
-            $room = $this->argument('room');
+            $room  = $this->argument('room');
             $event = $this->option('event');
+
             $data = $this->option('data')
                 ? json_decode($this->option('data'), true, 512, JSON_THROW_ON_ERROR)
                 : ['message' => 'Test message'];
 
             $eventData = WebsocketEventData::from(
-                room: $room,
-                data: $data,
+                room:  $room,
+                data:  $data,
                 event: $event
             );
 
             event(new WebsocketEvent($eventData));
 
             $this->info('Event broadcasted successfully');
+
             $this->table(
                 ['Component', 'Value'],
                 [
@@ -45,11 +47,9 @@ final class TestWebsocketEvent extends Command
             return self::SUCCESS;
         } catch (\JsonException $e) {
             $this->error('Invalid JSON data provided');
-
             return self::FAILURE;
         } catch (\Throwable $e) {
             $this->error("Error broadcasting event: {$e->getMessage()}");
-
             return self::FAILURE;
         }
     }

+ 3 - 3
app/Data/Pagarme/Order/OrderResponseData.php

@@ -59,7 +59,7 @@ final readonly class OrderResponseData extends PagarmeResponseData
             return null;
         }
 
-        $code = $this->filledValue($transaction->gatewayResponse['code'] ?? null);
+        $code = $this->filledValue(data_get($transaction->gatewayResponse, 'code'));
 
         if ($code === null || $this->isMisleadingFailureCode($code)) {
             return $this->filledValue($transaction->status);
@@ -82,7 +82,7 @@ final readonly class OrderResponseData extends PagarmeResponseData
             return $acquirerMessage;
         }
 
-        $gatewayErrors = $transaction->gatewayResponse['errors'] ?? [];
+        $gatewayErrors = data_get($transaction->gatewayResponse, 'errors', []);
 
         if (! is_array($gatewayErrors) || empty($gatewayErrors)) {
             return $this->failureMessageFromStatus($transaction->status);
@@ -102,7 +102,7 @@ final readonly class OrderResponseData extends PagarmeResponseData
 
     public function firstCharge(): ?ChargeData
     {
-        return $this->charges[0] ?? null;
+        return data_get($this->charges, 0);
     }
 
     public function gatewayEntityLabel(): string

+ 9 - 17
app/Data/Pagarme/PagarmeResponseData.php

@@ -12,47 +12,39 @@ abstract readonly class PagarmeResponseData
 
     protected static function arrGet(array $payload, int|string $key, mixed $default = null): mixed
     {
-        return array_key_exists($key, $payload) ? $payload[$key] : $default;
+        return data_get($payload, $key, $default);
     }
 
     protected static function arrInt(array $payload, string $key): ?int
     {
-        if (! array_key_exists($key, $payload)) {
+        $value = data_get($payload, $key);
+
+        if ($value === null) {
             return null;
         }
 
-        return (int) $payload[$key];
+        return (int) $value;
     }
 
     protected static function arrString(array $payload, string $key): ?string
     {
-        if (! array_key_exists($key, $payload)) {
-            return null;
-        }
-
-        $value = $payload[$key];
+        $value = data_get($payload, $key);
 
         return $value !== null && $value !== '' ? (string) $value : null;
     }
 
     protected static function arrBool(array $payload, string $key): ?bool
     {
-        if (! array_key_exists($key, $payload)) {
-            return null;
-        }
-
-        $value = $payload[$key];
+        $value = data_get($payload, $key);
 
         return $value !== null && $value !== '' ? (bool) $value : null;
     }
 
     protected static function arrArray(array $payload, string $key): array
     {
-        if (! array_key_exists($key, $payload) || ! is_array($payload[$key])) {
-            return [];
-        }
+        $value = data_get($payload, $key, []);
 
-        return $payload[$key];
+        return is_array($value) ? $value : [];
     }
 
     protected static function arrMap(array $payload, string $key, callable $fn): array

+ 29 - 0
app/Enums/GenderEnum.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Enums;
+
+use App\Traits\EnumHelper;
+
+enum GenderEnum: string
+{
+    use EnumHelper;
+
+    case MALE              = 'male';
+    case FEMALE            = 'female';
+    case OTHER             = 'other';
+    case PREFER_NOT_TO_SAY = 'prefer_not_to_say';
+
+    public static function labelFor(self|string|null $gender): ?string
+    {
+        if (is_string($gender)) {
+            $gender = self::tryFrom($gender);
+        }
+
+        return $gender?->label();
+    }
+
+    public function label(): string
+    {
+        return __("genders.{$this->value}");
+    }
+}

+ 12 - 0
app/Enums/PaymentStatusEnum.php

@@ -14,4 +14,16 @@ enum PaymentStatusEnum: string
     case PAID        = 'paid';
     case FAILED      = 'failed';
     case CANCELLED   = 'cancelled';
+
+    public function message(): string
+    {
+        return match ($this) {
+            self::PAID       => __('messages.payment_confirmed'),
+            self::AUTHORIZED => __('messages.payment_pending_confirmation'),
+            self::PROCESSING => __('messages.payment_pending_confirmation'),
+            self::PENDING    => __('messages.payment_pending_confirmation'),
+            self::FAILED     => __('messages.payment_not_confirmed'),
+            default          => __('messages.created'),
+        };
+    }
 }

+ 1 - 1
app/Enums/CartStatusEnum.php → app/Enums/ServicePackageStatusEnum.php

@@ -4,7 +4,7 @@ namespace App\Enums;
 
 use App\Traits\EnumHelper;
 
-enum CartStatusEnum: string
+enum ServicePackageStatusEnum: string
 {
     use EnumHelper;
 

+ 9 - 0
app/Exceptions/PaymentException.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace App\Exceptions;
+
+use RuntimeException;
+
+class PaymentException extends RuntimeException
+{
+}

+ 9 - 0
app/Exceptions/PaymentFailedException.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace App\Exceptions;
+
+use RuntimeException;
+
+class PaymentFailedException extends RuntimeException
+{
+}

+ 9 - 0
app/Exceptions/ScheduleStatusTransitionException.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace App\Exceptions;
+
+use RuntimeException;
+
+class ScheduleStatusTransitionException extends RuntimeException
+{
+}

+ 42 - 46
app/Http/Controllers/AuthController.php

@@ -20,8 +20,8 @@ class AuthController extends Controller
         $validated = $request->validated();
 
         $result = $this->authService->login(
-            email: $validated['email'],
-            password: $validated['password'],
+            email:    data_get($validated, 'email'),
+            password: data_get($validated, 'password'),
         );
 
         if (! $result) {
@@ -29,12 +29,12 @@ class AuthController extends Controller
         }
 
         return $this->successResponse(
-            payload: new AuthResource($result['payload']),
+            payload: new AuthResource(data_get($result, 'payload')),
             message: __('auth.logged_in'),
         )->withCookie(
             cookie(
                 'refresh_token',
-                $result['refreshToken'],
+                data_get($result, 'refreshToken'),
                 config('sanctum.rt_expiration') * 60,
                 '/',
                 config('session.domain'),
@@ -51,8 +51,8 @@ class AuthController extends Controller
         $validated = $request->validated();
 
         $result = $this->authService->login(
-            email: $validated['email'],
-            password: $validated['password'],
+            email:    data_get($validated, 'email'),
+            password: data_get($validated, 'password'),
         );
 
         if (! $result) {
@@ -61,8 +61,8 @@ class AuthController extends Controller
 
         return $this->successResponse(
             payload: new AuthResource([
-                ...$result['payload'],
-                'refresh_token' => $result['refreshToken'],
+                ...data_get($result, 'payload'),
+                'refresh_token' => data_get($result, 'refreshToken'),
             ]),
             message: __('auth.logged_in'),
         );
@@ -93,17 +93,16 @@ class AuthController extends Controller
 
         if (is_null($result)) {
             return $this->errorResponse(
-                message: __('auth.unauthorized'),
-                code: 403,
+                message: __('auth.unauthorized'), code: 403,
             )->withoutCookie('refresh_token');
         }
 
         return $this->successResponse(
-            payload: new AuthResource($result['payload']),
+            payload: new AuthResource(data_get($result, 'payload')),
         )->withCookie(
             cookie(
                 'refresh_token',
-                $result['refreshToken'],
+                data_get($result, 'refreshToken'),
                 config('sanctum.rt_expiration') * 60,
                 '/',
                 config('session.domain'),
@@ -122,22 +121,21 @@ class AuthController extends Controller
         if (is_null($refresh_token)) {
             return $this->errorResponse(code: 403);
         }
+
         $result = $this->authService->refresh(
-            $refresh_token,
-            isApp: true,
+            $refresh_token, isApp: true,
         );
 
         if (is_null($result)) {
             return $this->errorResponse(
-                message: __('auth.unauthorized'),
-                code: 403,
+                message: __('auth.unauthorized'), code: 403,
             );
         }
 
         return $this->successResponse(
             payload: new AuthResource([
-                ...$result['payload'],
-                'refresh_token' => $result['refreshToken'],
+                ...data_get($result, 'payload'),
+                'refresh_token' => data_get($result, 'refreshToken'),
             ]),
         );
     }
@@ -146,13 +144,13 @@ class AuthController extends Controller
     {
         $result = $this->authService->clientSendCode($request->validated());
 
-        if (is_array($result) && isset($result['error'])) {
-            return $this->errorResponse(message: __("auth.{$result['error']}"), code: 403);
+        if (is_array($result) && data_get($result, 'error') !== null) {
+            return $this->errorResponse(message: __('auth.'.data_get($result, 'error')), code: 403);
         }
 
         return $this->successResponse(
             message: __('messages.code_sent'),
-            code: 201,
+            code:    201,
             payload: ['isLogin' => $result],
         );
     }
@@ -161,22 +159,22 @@ class AuthController extends Controller
     {
         $result = $this->authService->providerSendCode($request->validated());
 
-        if (is_array($result) && isset($result['error'])) {
-            return $this->errorResponse(message: __("auth.{$result['error']}"), code: 403);
+        if (is_array($result) && data_get($result, 'error') !== null) {
+            return $this->errorResponse(message: __('auth.'.data_get($result, 'error')), code: 403);
         }
 
         return $this->successResponse(
             message: __('messages.code_sent'),
-            code: 201,
+            code:    201,
             payload: ['isLogin' => $result],
         );
     }
 
     public function validateCodeClient(UserAppsValidateCodeRequest $request): JsonResponse
     {
-        $email = $request->input('email');
-        $phone = $request->input('phone');
-        $code = $request->input('code');
+        $email   = $request->input('email');
+        $phone   = $request->input('phone');
+        $code    = $request->input('code');
         $isLogin = (bool) $request->input('isLogin', false);
 
         $result = $this->authService->validateCodeClient($request->validated(), $isLogin);
@@ -185,13 +183,13 @@ class AuthController extends Controller
             return $this->errorResponse(message: __('auth.invalid_code'), code: 400);
         }
 
-        if (is_array($result) && isset($result['error'])) {
-            return $this->errorResponse(message: __("auth.{$result['error']}"), code: 403);
+        if (is_array($result) && data_get($result, 'error') !== null) {
+            return $this->errorResponse(message: __('auth.'.data_get($result, 'error')), code: 403);
         }
 
         if ($isLogin) {
             return $this->successResponse(
-                payload: new AuthResource([...$result['payload'], 'refresh_token' => $result['refreshToken']]),
+                payload: new AuthResource([...data_get($result, 'payload'), 'refresh_token' => data_get($result, 'refreshToken')]),
                 message: __('auth.logged_in'),
             );
         }
@@ -204,9 +202,9 @@ class AuthController extends Controller
 
     public function validateCodeProvider(UserAppsValidateCodeRequest $request): JsonResponse
     {
-        $email = $request->input('email');
-        $phone = $request->input('phone');
-        $code = $request->input('code');
+        $email   = $request->input('email');
+        $phone   = $request->input('phone');
+        $code    = $request->input('code');
         $isLogin = (bool) $request->input('isLogin', false);
 
         $result = $this->authService->validateCodeProvider($request->validated(), $isLogin);
@@ -215,13 +213,13 @@ class AuthController extends Controller
             return $this->errorResponse(message: __('auth.invalid_code'), code: 400);
         }
 
-        if (is_array($result) && isset($result['error'])) {
-            return $this->errorResponse(message: __("auth.{$result['error']}"), code: 403);
+        if (is_array($result) && data_get($result, 'error') !== null) {
+            return $this->errorResponse(message: __('auth.'.data_get($result, 'error')), code: 403);
         }
 
         if ($isLogin) {
             return $this->successResponse(
-                payload: new AuthResource([...$result['payload'], 'refresh_token' => $result['refreshToken']]),
+                payload: new AuthResource([...data_get($result, 'payload'), 'refresh_token' => data_get($result, 'refreshToken')]),
                 message: __('auth.logged_in'),
             );
         }
@@ -235,25 +233,24 @@ class AuthController extends Controller
     public function validateCode(UserAppsValidateCodeRequest $request): JsonResponse
     {
         try {
-            $email = $request->input('email');
-            $phone = $request->input('phone');
-            $code = $request->input('code');
+            $email   = $request->input('email');
+            $phone   = $request->input('phone');
+            $code    = $request->input('code');
             $isLogin = $request->input('isLogin');
 
             $result = $this->authService->validateCode($request->validated(), $isLogin);
 
             if (! $result) {
                 return $this->errorResponse(
-                    message: __('auth.invalid_code'),
-                    code: 400,
+                    message: __('auth.invalid_code'), code: 400,
                 );
             }
 
             if ($isLogin) {
                 return $this->successResponse(
                     payload: new AuthResource([
-                        ...$result['payload'],
-                        'refresh_token' => $result['refreshToken'],
+                        ...data_get($result, 'payload'),
+                        'refresh_token' => data_get($result, 'refreshToken'),
                     ]),
                     message: __('auth.logged_in'),
                 );
@@ -261,14 +258,13 @@ class AuthController extends Controller
                 return $this->successResponse(
                     payload: ['email' => $email, 'phone' => $phone, 'code' => $code],
                     message: __('auth.valid_code'),
-                    code: 200,
+                    code:    200,
                 );
             }
 
         } catch (\Exception $e) {
             return $this->errorResponse(
-                message: __('auth.validation_error'),
-                code: 500,
+                message: __('auth.validation_error'), code: 500, exception: $e,
             );
         }
     }

+ 5 - 0
app/Http/Controllers/BankController.php

@@ -16,30 +16,35 @@ class BankController extends Controller
     public function index(): JsonResponse
     {
         $items = $this->service->getAll();
+
         return $this->successResponse(payload: BankResource::collection($items));
     }
 
     public function store(BankRequest $request): JsonResponse
     {
         $item = $this->service->create($request->validated());
+
         return $this->successResponse(payload: new BankResource($item), message: __('messages.created'), code: 201);
     }
 
     public function show(int $id): JsonResponse
     {
         $item = $this->service->findById($id);
+
         return $this->successResponse(payload: new BankResource($item));
     }
 
     public function update(BankRequest $request, int $id): JsonResponse
     {
         $item = $this->service->update($id, $request->validated());
+
         return $this->successResponse(payload: new BankResource($item), message: __('messages.updated'));
     }
 
     public function destroy(int $id): JsonResponse
     {
         $this->service->delete($id);
+
         return $this->successResponse(message: __('messages.deleted'), code: 204);
     }
 }

+ 0 - 82
app/Http/Controllers/CartController.php

@@ -1,82 +0,0 @@
-<?php
-
-namespace App\Http\Controllers;
-
-use App\Http\Requests\CartRequest;
-use App\Http\Resources\CartResource;
-use App\Services\CartService;
-use Illuminate\Http\JsonResponse;
-use Illuminate\Support\Facades\Auth;
-
-class CartController extends Controller
-{
-    public function __construct(
-        protected CartService $service,
-    ) {}
-
-    public function index(): JsonResponse
-    {
-        $items = $this->service->getAll();
-
-        return $this->successResponse(payload: CartResource::collection($items));
-    }
-
-    public function store(CartRequest $request): JsonResponse
-    {
-        $validated = $request->validated();
-
-        $item = $this->hasSchedulePayload($validated)
-            ? $this->service->createWithSchedules($validated)
-            : $this->service->create($validated);
-
-        return $this->successResponse(payload: new CartResource($item), message: __('messages.created'), code: 201);
-    }
-
-    public function show(int $id): JsonResponse
-    {
-        $item = $this->service->findById($id);
-
-        return $this->successResponse(payload: new CartResource($item));
-    }
-
-    public function update(CartRequest $request, int $id): JsonResponse
-    {
-        $item = $this->service->update($id, $request->validated());
-
-        return $this->successResponse(payload: new CartResource($item), message: __('messages.updated'));
-    }
-
-    public function destroy(int $id): JsonResponse
-    {
-        $this->service->delete($id);
-
-        return $this->successResponse(message: __('messages.deleted'), code: 204);
-    }
-
-    //
-
-    public function myCarts(): JsonResponse
-    {
-        $items = $this->service->getByUserId(Auth::id());
-
-        return $this->successResponse(payload: CartResource::collection($items));
-    }
-
-    public function storeMine(CartRequest $request): JsonResponse
-    {
-        $request->merge(['client_id' => Auth::user()?->client?->id]);
-
-        $item = $this->service->createForUser($request->validated(), Auth::id());
-
-        return $this->successResponse(payload: new CartResource($item), message: __('messages.created'), code: 201);
-    }
-
-    //
-
-    private function hasSchedulePayload(array $data): bool
-    {
-        return ! empty($data['schedules'])
-            || ! empty($data['dates'])
-            || ! empty($data['date']);
-    }
-}

+ 0 - 50
app/Http/Controllers/CartItemController.php

@@ -1,50 +0,0 @@
-<?php
-
-namespace App\Http\Controllers;
-
-use App\Http\Requests\CartItemRequest;
-use App\Http\Resources\CartItemResource;
-use App\Services\CartItemService;
-use Illuminate\Http\JsonResponse;
-
-class CartItemController extends Controller
-{
-    public function __construct(
-        protected CartItemService $service,
-    ) {}
-
-    public function index(): JsonResponse
-    {
-        $items = $this->service->getAll();
-
-        return $this->successResponse(payload: CartItemResource::collection($items));
-    }
-
-    public function store(CartItemRequest $request): JsonResponse
-    {
-        $item = $this->service->create($request->validated());
-
-        return $this->successResponse(payload: new CartItemResource($item), message: __('messages.created'), code: 201);
-    }
-
-    public function show(int $id): JsonResponse
-    {
-        $item = $this->service->findById($id);
-
-        return $this->successResponse(payload: new CartItemResource($item));
-    }
-
-    public function update(CartItemRequest $request, int $id): JsonResponse
-    {
-        $item = $this->service->update($id, $request->validated());
-
-        return $this->successResponse(payload: new CartItemResource($item), message: __('messages.updated'));
-    }
-
-    public function destroy(int $id): JsonResponse
-    {
-        $this->service->delete($id);
-
-        return $this->successResponse(message: __('messages.deleted'), code: 204);
-    }
-}

+ 2 - 2
app/Http/Controllers/ChatbotController.php

@@ -25,9 +25,9 @@ class ChatbotController extends Controller
         $user = Auth::user();
 
         $reply = $this->geminiService->sendMessage(
-            message:  $validated['message'],
+            message:  data_get($validated, 'message'),
             userType: $user->type,
-            history:  $validated['history'] ?? [],
+            history:  data_get($validated, 'history', []),
         );
 
         return $this->successResponse(payload: ['reply' => $reply]);

+ 1 - 4
app/Http/Controllers/ClientCalendarController.php

@@ -4,7 +4,6 @@ namespace App\Http\Controllers;
 
 use App\Services\ClientCalendarService;
 use Illuminate\Http\JsonResponse;
-use Illuminate\Support\Facades\Log;
 
 class ClientCalendarController extends Controller
 {
@@ -17,9 +16,7 @@ class ClientCalendarController extends Controller
 
             return $this->successResponse(payload: $dados);
         } catch (\Exception $e) {
-            Log::error('Error fetching client calendar: '.$e->getMessage());
-
-            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 }

+ 2 - 2
app/Http/Controllers/ClientController.php

@@ -72,8 +72,8 @@ class ClientController extends Controller
 
         return $this->successResponse(
             payload: new AuthResource([
-                ...$result['payload'],
-                'refresh_token' => $result['refreshToken'],
+                ...data_get($result, 'payload'),
+                'refresh_token' => data_get($result, 'refreshToken'),
             ]),
             message: __('auth.logged_in'),
         );

+ 28 - 2
app/Http/Controllers/Controller.php

@@ -3,6 +3,9 @@
 namespace App\Http\Controllers;
 
 use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Route;
+use Throwable;
 
 abstract class Controller
 {
@@ -11,8 +14,31 @@ abstract class Controller
         return response()->json(data: ['payload' => $payload, 'message' => $message], status: $code);
     }
 
-    public function errorResponse(?string $message = null, int $code = 404): JsonResponse
-    {
+    public function errorResponse(
+        ?string    $message   = null,
+        int        $code      = 404,
+        ?Throwable $exception = null,
+    ): JsonResponse {
+        if ($code >= 500) {
+            $route = Route::current();
+
+            Log::withContext([
+                'route'      => $route?->uri(),
+                'route_name' => $route?->getName(),
+                'controller' => $route?->getActionName(),
+                'method'     => request()->method(),
+            ]);
+
+            if ($exception) {
+                report($exception);
+            } else {
+                Log::error('Resposta de erro interno retornada pela API.', [
+                    'message' => $message,
+                    'code'    => $code,
+                ]);
+            }
+        }
+
         return response()->json(data: ['message' => $message], status: $code);
     }
 }

+ 18 - 36
app/Http/Controllers/CustomScheduleController.php

@@ -10,7 +10,6 @@ use App\Http\Requests\CustomScheduleVerifyCodeRequest;
 use App\Http\Resources\CustomScheduleResource;
 use App\Services\CustomScheduleService;
 use Illuminate\Http\JsonResponse;
-use Illuminate\Support\Facades\Log;
 
 class CustomScheduleController extends Controller
 {
@@ -38,14 +37,13 @@ class CustomScheduleController extends Controller
             $customSchedules = $this->customScheduleService->create($validated);
 
             $count = count($customSchedules);
-            $message = $count > 1
-              ? "{$count} oportunidades criadas com sucesso!"
-              : __('messages.created');
+
+            $message = trans_choice('messages.opportunities_created', $count, ['count' => $count]);
 
             return $this->successResponse(
                 payload: CustomScheduleResource::collection($customSchedules),
                 message: $message,
-                code: 201,
+                code:    201,
             );
         } catch (\Exception $e) {
             return $this->errorResponse($e->getMessage(), 422);
@@ -96,16 +94,14 @@ class CustomScheduleController extends Controller
             $providerId = $request->query('provider_id');
 
             if (! $providerId) {
-                return $this->errorResponse('Provider ID é obrigatório', 400);
+                return $this->errorResponse(__('messages.provider_id_required'), 400);
             }
 
             $opportunities = $this->customScheduleService->getAvailableOpportunities($providerId);
 
             return $this->successResponse($opportunities);
         } catch (\Exception $e) {
-            Log::error('Error fetching available opportunities: '.$e->getMessage());
-
-            return $this->errorResponse($e->getMessage(), 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 
@@ -116,9 +112,7 @@ class CustomScheduleController extends Controller
 
             return $this->successResponse($data);
         } catch (\Exception $e) {
-            Log::error('Error fetching provider proposals and opportunities: '.$e->getMessage());
-
-            return $this->errorResponse($e->getMessage(), 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 
@@ -136,9 +130,7 @@ class CustomScheduleController extends Controller
 
             return $this->successResponse($proposals);
         } catch (\Exception $e) {
-            Log::error('Error fetching opportunity proposals: '.$e->getMessage());
-
-            return $this->errorResponse($e->getMessage(), 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 
@@ -148,16 +140,14 @@ class CustomScheduleController extends Controller
             $providerId = $request->query('provider_id');
 
             if (! $providerId) {
-                return $this->errorResponse('Provider ID é obrigatório', 400);
+                return $this->errorResponse(__('messages.provider_id_required'), 400);
             }
 
             $proposals = $this->customScheduleService->getProviderProposals($providerId);
 
             return $this->successResponse($proposals);
         } catch (\Exception $e) {
-            Log::error('Error fetching provider proposals: '.$e->getMessage());
-
-            return $this->errorResponse($e->getMessage(), 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 
@@ -169,15 +159,13 @@ class CustomScheduleController extends Controller
             $providerId = $request->input('provider_id');
 
             if (! $providerId) {
-                return $this->errorResponse('Provider ID é obrigatório', 400);
+                return $this->errorResponse(__('messages.provider_id_required'), 400);
             }
 
             $proposal = $this->customScheduleService->proposeOpportunity($scheduleId, $providerId);
 
-            return $this->successResponse($proposal, 'Proposta enviada com sucesso!', 201);
+            return $this->successResponse($proposal, __('messages.proposal_sent'), 201);
         } catch (\Exception $e) {
-            Log::error('Error creating proposal: '.$e->getMessage());
-
             return $this->errorResponse($e->getMessage(), 400);
         }
     }
@@ -187,10 +175,8 @@ class CustomScheduleController extends Controller
         try {
             $schedule = $this->customScheduleService->acceptProposal($proposalId);
 
-            return $this->successResponse($schedule, 'Prestador aceito com sucesso!');
+            return $this->successResponse($schedule, __('messages.provider_accepted'));
         } catch (\Exception $e) {
-            Log::error('Error accepting proposal: '.$e->getMessage());
-
             return $this->errorResponse($e->getMessage(), 400);
         }
     }
@@ -200,23 +186,21 @@ class CustomScheduleController extends Controller
         try {
             $this->customScheduleService->refuseProposal($proposalId);
 
-            return $this->successResponse(null, 'Prestador recusado.');
+            return $this->successResponse(null, __('messages.provider_refused'));
         } catch (\Exception $e) {
-            Log::error('Error refusing proposal: '.$e->getMessage());
-
             return $this->errorResponse($e->getMessage(), 400);
         }
     }
 
+    //
+
     public function refuseOpportunity($scheduleId, CustomScheduleRefuseOpportunityRequest $request)
     {
         try {
             $this->customScheduleService->refuseOpportunity($scheduleId, $request->input('provider_id'));
 
-            return $this->successResponse(null, 'Oportunidade recusada.');
+            return $this->successResponse(null, __('messages.opportunity_refused'));
         } catch (\Exception $e) {
-            Log::error('Error refusing opportunity: '.$e->getMessage());
-
             return $this->errorResponse($e->getMessage(), 400);
         }
     }
@@ -228,11 +212,9 @@ class CustomScheduleController extends Controller
         try {
             $result = $this->customScheduleService->verifyScheduleCode($scheduleId, $request->input('code'));
 
-            return $this->successResponse($result, 'Código verificado com sucesso!');
+            return $this->successResponse($result, __('messages.code_verified'));
         } catch (\Exception $e) {
-            Log::error('Error verifying schedule code: '.$e->getMessage());
-
-            return $this->errorResponse($e->getMessage(), 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 }

+ 5 - 12
app/Http/Controllers/DashboardController.php

@@ -8,7 +8,6 @@ use App\Services\DashboardService;
 use Illuminate\Auth\Access\AuthorizationException;
 use Illuminate\Database\Eloquent\ModelNotFoundException;
 use Illuminate\Http\JsonResponse;
-use Illuminate\Support\Facades\Log;
 
 class DashboardController extends Controller
 {
@@ -25,11 +24,9 @@ class DashboardController extends Controller
         } catch (AuthorizationException $e) {
             return $this->errorResponse(message: $e->getMessage(), code: 403);
         } catch (ModelNotFoundException) {
-            return $this->errorResponse(message: 'Cliente não encontrado.', code: 404);
+            return $this->errorResponse(message: __('messages.client_not_found'), code: 404);
         } catch (\Exception $e) {
-            Log::error('Erro ao obter dados do dashboard do cliente: '.$e->getMessage());
-
-            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 
@@ -44,11 +41,9 @@ class DashboardController extends Controller
         } catch (AuthorizationException $e) {
             return $this->errorResponse(message: $e->getMessage(), code: 403);
         } catch (ModelNotFoundException) {
-            return $this->errorResponse(message: 'Prestador não encontrado.', code: 404);
+            return $this->errorResponse(message: __('messages.provider_not_found'), code: 404);
         } catch (\Exception $e) {
-            Log::error('Erro ao obter dados do dashboard do prestador: '.$e->getMessage());
-
-            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 
@@ -59,9 +54,7 @@ class DashboardController extends Controller
 
             return $this->successResponse(payload: $dados);
         } catch (\Exception $e) {
-            Log::error('Erro ao obter detalhes do schedule do cliente: '.$e->getMessage());
-
-            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 }

+ 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'),
+        );
+    }
 }

+ 48 - 37
app/Http/Controllers/PaymentController.php

@@ -3,6 +3,10 @@
 namespace App\Http\Controllers;
 
 use App\Enums\PaymentStatusEnum;
+use App\Exceptions\PaymentFailedException;
+use App\Exceptions\PaymentException;
+use App\Http\Requests\PayScheduleRequest;
+use App\Http\Requests\PayServicePackageRequest;
 use App\Http\Requests\PaymentRequest;
 use App\Http\Resources\PaymentResource;
 use App\Models\Schedule;
@@ -30,7 +34,7 @@ class PaymentController extends Controller
 
         return $this->successResponse(
             payload: new PaymentResource($item),
-            message: $this->paymentMessage($item->status->value),
+            message: $item->status->message(),
             code:    201,
         );
     }
@@ -58,40 +62,58 @@ class PaymentController extends Controller
 
     //
 
-    public function paySchedule(Request $request, Schedule $schedule): JsonResponse
+    public function paySchedule(PayScheduleRequest $request, int $scheduleId): JsonResponse
     {
-        $validated = $request->validate([
-            'payment_method'           => ['required', 'in:credit_card,pix'],
-            'client_payment_method_id' => ['nullable', 'integer', 'exists:client_payment_methods,id'],
-            'card_id'                  => ['nullable', 'string', 'max:255'],
-            'phone'                    => ['nullable', 'string', 'max:20'],
-        ]);
-
-        if ($schedule->client?->user_id !== Auth::id()) {
-            abort(403);
+        $validated = $request->validated();
+
+        try {
+            $item = $this->service->paySchedule(
+                scheduleId:            $scheduleId,
+                paymentMethod:         data_get($validated, 'payment_method'),
+                clientPaymentMethodId: data_get($validated, 'client_payment_method_id'),
+
+                options: [
+                    'phone'   => data_get($validated, 'phone'),
+                    'card_id' => data_get($validated, 'card_id'),
+                ],
+            );
+        } catch (PaymentFailedException) {
+            return $this->errorResponse(message: __('messages.payment_not_confirmed'), code: 422);
+        } catch (PaymentException) {
+            return $this->errorResponse(message: __('messages.payment_error'), code: 422);
         }
 
-        $item = $this->service->payAcceptedSchedule(
-            schedule:              $schedule,
-            paymentMethod:         $validated['payment_method'],
-            clientPaymentMethodId: $validated['client_payment_method_id'] ?? null,
-
-            options: [
-                'phone'   => $validated['phone'] ?? null,
-                'card_id' => $validated['card_id'] ?? null,
-            ],
+        return $this->successResponse(
+            payload: new PaymentResource($item),
+            message: $item->status->message(),
+            code:    201,
         );
+    }
 
-        if ($item->status === PaymentStatusEnum::FAILED) {
-            return response()->json([
-                'payload' => new PaymentResource($item),
-                'message' => $item->failure_message ?: $this->paymentMessage($item->status->value),
-            ], 422);
+    public function payServicePackage(PayServicePackageRequest $request, int $servicePackageId): JsonResponse
+    {
+        $validated = $request->validated();
+
+        try {
+            $item = $this->service->payServicePackage(
+                servicePackageId:      $servicePackageId,
+                paymentMethod:         data_get($validated, 'payment_method'),
+                clientPaymentMethodId: data_get($validated, 'client_payment_method_id'),
+
+                options: [
+                    'phone'   => data_get($validated, 'phone'),
+                    'card_id' => data_get($validated, 'card_id'),
+                ],
+            );
+        } catch (PaymentFailedException) {
+            return $this->errorResponse(message: __('messages.payment_not_confirmed'), code: 422);
+        } catch (PaymentException) {
+            return $this->errorResponse(message: __('messages.payment_error'), code: 422);
         }
 
         return $this->successResponse(
             payload: new PaymentResource($item),
-            message: $this->paymentMessage($item->status->value),
+            message: $item->status->message(),
             code:    201,
         );
     }
@@ -114,15 +136,4 @@ class PaymentController extends Controller
         return $this->successResponse(payload: $this->service->platformFees());
     }
 
-    //
-
-    private function paymentMessage(string $status): string
-    {
-        return match ($status) {
-            'paid'                                => __('messages.payment_confirmed'),
-            'authorized', 'processing', 'pending' => __('messages.payment_pending_confirmation'),
-            'failed'                              => __('messages.payment_not_confirmed'),
-            default                               => __('messages.created'),
-        };
-    }
 }

+ 1 - 4
app/Http/Controllers/ProviderCalendarController.php

@@ -4,7 +4,6 @@ namespace App\Http\Controllers;
 
 use App\Services\ProviderCalendarService;
 use Illuminate\Http\JsonResponse;
-use Illuminate\Support\Facades\Log;
 
 class ProviderCalendarController extends Controller
 {
@@ -17,9 +16,7 @@ class ProviderCalendarController extends Controller
 
             return $this->successResponse(payload: $dados);
         } catch (\Exception $e) {
-            Log::error('Error fetching provider calendar: '.$e->getMessage());
-
-            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 }

+ 2 - 2
app/Http/Controllers/ProviderController.php

@@ -119,8 +119,8 @@ class ProviderController extends Controller
 
         return $this->successResponse(
             payload: new AuthResource([
-                ...$result['payload'],
-                'refresh_token' => $result['refreshToken'],
+                ...data_get($result, 'payload'),
+                'refresh_token' => data_get($result, 'refreshToken'),
             ]),
             message: __('messages.provider_registered_pending'),
             code: 201,

+ 9 - 2
app/Http/Controllers/ProviderWithdrawalController.php

@@ -39,8 +39,8 @@ class ProviderWithdrawalController extends Controller
 
         return $this->successResponse(
             payload: new ProviderWithdrawalResource($withdrawal),
-            message: 'Saque solicitado com sucesso.',
-            code: 201,
+            message: __('messages.withdrawal_requested'),
+            code:    201,
         );
     }
 
@@ -65,6 +65,13 @@ class ProviderWithdrawalController extends Controller
         ]);
     }
 
+    public function configuration(): JsonResponse
+    {
+        return $this->successResponse(payload: [
+            'pix_enabled' => config('withdrawals.pix_enabled'),
+        ]);
+    }
+
     public function fees(): JsonResponse
     {
         return $this->successResponse(payload: $this->service->getWithdrawalFees());

+ 1 - 1
app/Http/Controllers/ProviderWorkingDayController.php

@@ -30,7 +30,7 @@ class ProviderWorkingDayController extends Controller
 
     public function destroy($id, Request $request): JsonResponse
     {
-        $day = $request->query('day');
+        $day    = $request->query('day');
         $period = $request->query('period');
 
         $this->service->delete($id, $day, $period);

+ 1 - 1
app/Http/Controllers/ReviewController.php

@@ -30,7 +30,7 @@ class ReviewController extends Controller
         return $this->successResponse(
             payload: new ReviewResource($review),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 

+ 12 - 43
app/Http/Controllers/ScheduleController.php

@@ -2,13 +2,12 @@
 
 namespace App\Http\Controllers;
 
+use App\Exceptions\ScheduleStatusTransitionException;
 use App\Http\Requests\ScheduleRequest;
 use App\Http\Resources\ScheduleResource;
-use App\Services\ScheduleCartService;
 use App\Services\ScheduleService;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
-use Illuminate\Support\Arr;
 
 class ScheduleController extends Controller
 {
@@ -33,14 +32,11 @@ class ScheduleController extends Controller
         try {
             $validated = $request->validated();
 
-            $schedules = $this->scheduleService->createSingleOrMultiple(
-                Arr::except($validated, ['date', 'dates']),
-                $this->scheduleItems($validated),
-            );
+            $schedule = $this->scheduleService->create($validated);
 
             return $this->successResponse(
-                payload: ScheduleResource::collection($schedules),
-                message: count($schedules).' '.__('schedules.schedules_created'),
+                payload: new ScheduleResource($schedule),
+                message: __('messages.created'),
                 code:    201,
             );
         } catch (\Exception $e) {
@@ -100,8 +96,8 @@ class ScheduleController extends Controller
         ]);
 
         $blocks = $this->scheduleService->getClientProviderBlocks(
-            (int) $validated['client_id'],
-            (int) $validated['provider_id'],
+            (int) data_get($validated, 'client_id'),
+            (int) data_get($validated, 'provider_id'),
         );
 
         return $this->successResponse(payload: $blocks);
@@ -123,27 +119,14 @@ class ScheduleController extends Controller
                 'cancel_text' => 'required|string|min:5|max:1000',
             ]);
 
-            $schedule = $this->scheduleService->cancelWithReason((int) $id, $validated['cancel_text']);
+            $schedule = $this->scheduleService->cancelWithReason((int) $id, data_get($validated, 'cancel_text'));
 
             return $this->successResponse(
                 payload: new ScheduleResource($schedule),
                 message: __('messages.updated'),
             );
-        } catch (\Exception $e) {
-            return $this->errorResponse($e->getMessage(), 422);
-        }
-    }
-
-    public function storeCart(ScheduleRequest $request, ScheduleCartService $cartService): JsonResponse
-    {
-        try {
-            $schedules = $cartService->create($request->validated());
-
-            return $this->successResponse(
-                payload: ScheduleResource::collection($schedules),
-                message: count($schedules).' '.__('schedules.schedules_created'),
-                code:    201,
-            );
+        } catch (ScheduleStatusTransitionException) {
+            return $this->errorResponse(__('messages.cancellation_not_allowed_for_status'), 422);
         } catch (\Exception $e) {
             return $this->errorResponse($e->getMessage(), 422);
         }
@@ -156,30 +139,16 @@ class ScheduleController extends Controller
                 'status' => 'required|in:pending,accepted,rejected,paid,cancelled,started,finished',
             ]);
 
-            $schedule = $this->scheduleService->updateStatus($id, $validated['status']);
+            $schedule = $this->scheduleService->updateStatus($id, data_get($validated, 'status'));
 
             return $this->successResponse(
                 payload: new ScheduleResource($schedule),
                 message: __('messages.updated'),
             );
+        } catch (ScheduleStatusTransitionException) {
+            return $this->errorResponse(__('messages.status_transition_not_allowed'), 422);
         } catch (\Exception $e) {
             return $this->errorResponse($e->getMessage(), 422);
         }
     }
-
-    //
-
-    private function scheduleItems(array $data): array
-    {
-        if (! empty($data['dates'])) {
-            return array_map(
-                fn (string $date) => ['date' => $date],
-                $data['dates'],
-            );
-        }
-
-        return [
-            ['date' => $data['date']],
-        ];
-    }
 }

+ 1 - 4
app/Http/Controllers/SearchController.php

@@ -5,7 +5,6 @@ namespace App\Http\Controllers;
 use App\Services\SearchService;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Log;
 
 class SearchController extends Controller
 {
@@ -18,9 +17,7 @@ class SearchController extends Controller
 
             return $this->successResponse(payload: $dados);
         } catch (\Exception $e) {
-            Log::error('Erro ao buscar prestadores: '.$e->getMessage());
-
-            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500);
+            return $this->errorResponse(message: __('messages.error_fetching_data'), code: 500, exception: $e);
         }
     }
 }

+ 15 - 0
app/Http/Controllers/ServiceConfigurationController.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\JsonResponse;
+
+class ServiceConfigurationController extends Controller
+{
+    public function index(): JsonResponse
+    {
+        return $this->successResponse(payload: [
+            'sms_enabled' => config('services.sms.enabled'),
+        ]);
+    }
+}

+ 82 - 0
app/Http/Controllers/ServicePackageController.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\ServicePackageRequest;
+use App\Http\Resources\ServicePackageResource;
+use App\Services\ServicePackageService;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\Auth;
+
+class ServicePackageController extends Controller
+{
+    public function __construct(
+        protected ServicePackageService $service,
+    ) {}
+
+    public function index(): JsonResponse
+    {
+        $items = $this->service->getAll();
+
+        return $this->successResponse(payload: ServicePackageResource::collection($items));
+    }
+
+    public function store(ServicePackageRequest $request): JsonResponse
+    {
+        $validated = $request->validated();
+
+        $item = $this->hasSchedulePayload($validated)
+            ? $this->service->createWithSchedules($validated)
+            : $this->service->create($validated);
+
+        return $this->successResponse(payload: new ServicePackageResource($item), message: __('messages.created'), code: 201);
+    }
+
+    public function show(int $id): JsonResponse
+    {
+        $item = $this->service->findById($id);
+
+        return $this->successResponse(payload: new ServicePackageResource($item));
+    }
+
+    public function update(ServicePackageRequest $request, int $id): JsonResponse
+    {
+        $item = $this->service->update($id, $request->validated());
+
+        return $this->successResponse(payload: new ServicePackageResource($item), message: __('messages.updated'));
+    }
+
+    public function destroy(int $id): JsonResponse
+    {
+        $this->service->delete($id);
+
+        return $this->successResponse(message: __('messages.deleted'), code: 204);
+    }
+
+    //
+
+    public function myServicePackages(): JsonResponse
+    {
+        $items = $this->service->getByUserId(Auth::id());
+
+        return $this->successResponse(payload: ServicePackageResource::collection($items));
+    }
+
+    public function storeMine(ServicePackageRequest $request): JsonResponse
+    {
+        $request->merge(['client_id' => Auth::user()?->client?->id]);
+
+        $item = $this->service->createForUser($request->validated(), Auth::id());
+
+        return $this->successResponse(payload: new ServicePackageResource($item), message: __('messages.created'), code: 201);
+    }
+
+    //
+
+    private function hasSchedulePayload(array $data): bool
+    {
+        return ! empty(data_get($data, 'schedules'))
+            || ! empty(data_get($data, 'dates'))
+            || ! empty(data_get($data, 'date'));
+    }
+}

+ 50 - 0
app/Http/Controllers/ServicePackageItemController.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\ServicePackageItemRequest;
+use App\Http\Resources\ServicePackageItemResource;
+use App\Services\ServicePackageItemService;
+use Illuminate\Http\JsonResponse;
+
+class ServicePackageItemController extends Controller
+{
+    public function __construct(
+        protected ServicePackageItemService $service,
+    ) {}
+
+    public function index(): JsonResponse
+    {
+        $items = $this->service->getAll();
+
+        return $this->successResponse(payload: ServicePackageItemResource::collection($items));
+    }
+
+    public function store(ServicePackageItemRequest $request): JsonResponse
+    {
+        $item = $this->service->create($request->validated());
+
+        return $this->successResponse(payload: new ServicePackageItemResource($item), message: __('messages.created'), code: 201);
+    }
+
+    public function show(int $id): JsonResponse
+    {
+        $item = $this->service->findById($id);
+
+        return $this->successResponse(payload: new ServicePackageItemResource($item));
+    }
+
+    public function update(ServicePackageItemRequest $request, int $id): JsonResponse
+    {
+        $item = $this->service->update($id, $request->validated());
+
+        return $this->successResponse(payload: new ServicePackageItemResource($item), message: __('messages.updated'));
+    }
+
+    public function destroy(int $id): JsonResponse
+    {
+        $this->service->delete($id);
+
+        return $this->successResponse(message: __('messages.deleted'), code: 204);
+    }
+}

+ 1 - 1
app/Http/Controllers/SupportRequestController.php

@@ -28,7 +28,7 @@ class SupportRequestController extends Controller
             'data'  => SupportRequestResource::collection($paginated->items()),
             'total' => $paginated->total(),
             'from'  => $paginated->firstItem() ?? 0,
-            'to'    => $paginated->lastItem() ?? 0,
+            'to'    => $paginated->lastItem()  ?? 0,
         ]);
     }
 

+ 20 - 16
app/Http/Controllers/UserController.php

@@ -32,13 +32,14 @@ class UserController extends Controller
         return $this->successResponse(
             payload: new UserResource($item),
             message: __("messages.created"),
-            code: 201,
+            code:    201,
         );
     }
 
     public function show(int $id): JsonResponse
     {
         $item = $this->service->findById($id);
+
         return $this->successResponse(payload: new UserResource($item));
     }
 
@@ -58,7 +59,7 @@ class UserController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 
@@ -76,6 +77,7 @@ class UserController extends Controller
     public function me(): JsonResponse
     {
         $user = $this->service->me();
+
         return $this->successResponse(payload: new UserResource($user));
     }
 
@@ -89,24 +91,24 @@ class UserController extends Controller
         $avatar    = $request->file('avatar');
 
         $data       = $request->validated();
-        $hasBase64  = ! empty($data['avatar_base64']);
+        $hasBase64  = ! empty(data_get($data, 'avatar_base64'));
 
         Log::warning('[avatar-upload] Requisição recebida em updateMe', [
             'user_id'           => auth()->id(),
             'has_avatar'        => $hasAvatar,
             'has_avatar_base64' => $hasBase64,
-            'avatar_base64_len' => $hasBase64 ? strlen($data['avatar_base64']) : null,
-            'avatar_valid'      => $hasAvatar ? $avatar->isValid() : null,
-            'avatar_size'       => $hasAvatar ? $avatar->getSize() : null,
-            'avatar_mime'       => $hasAvatar ? $avatar->getMimeType() : null,
-            'avatar_extension'  => $hasAvatar ? $avatar->getClientOriginalExtension() : null,
-            'avatar_error'      => $hasAvatar ? $avatar->getError() : null,
+            'avatar_base64_len' => $hasBase64 ? strlen(data_get($data, 'avatar_base64')) : null,
+            'avatar_valid'      => $hasAvatar ? $avatar->isValid()                       : null,
+            'avatar_size'       => $hasAvatar ? $avatar->getSize()                       : null,
+            'avatar_mime'       => $hasAvatar ? $avatar->getMimeType()                   : null,
+            'avatar_extension'  => $hasAvatar ? $avatar->getClientOriginalExtension()    : null,
+            'avatar_error'      => $hasAvatar ? $avatar->getError()                      : null,
             'method'            => $request->method(),
             'content_type'      => $request->header('Content-Type'),
         ]);
 
         if ($hasBase64) {
-            $decoded = $this->decodeBase64ToUploadedFile($data['avatar_base64']);
+            $decoded = $this->decodeBase64ToUploadedFile(data_get($data, 'avatar_base64'));
 
             Log::warning('[avatar-upload] Avatar base64 decodificado', [
                 'decode_ok'    => $decoded !== null,
@@ -142,8 +144,8 @@ class UserController extends Controller
     private function decodeBase64ToUploadedFile(string $base64): ?UploadedFile
     {
         if (preg_match('/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/s', $base64, $matches)) {
-            $mime    = $matches[1];
-            $content = base64_decode($matches[2], true);
+            $mime    = data_get($matches, 1);
+            $content = base64_decode(data_get($matches, 2), true);
         } else {
             $mime    = 'image/jpeg';
             $content = base64_decode($base64, true);
@@ -166,11 +168,13 @@ class UserController extends Controller
             return null;
         }
 
+        $originalName = 'avatar.'.$extension;
+
         return new UploadedFile(
-            path: $tmpPath,
-            originalName: 'avatar.'.$extension,
-            mimeType: $mime,
-            test: true,
+            path:         $tmpPath,
+            originalName: $originalName,
+            mimeType:     $mime,
+            test:         true,
         );
     }
 }

+ 5 - 2
app/Http/Middleware/CheckPermission.php

@@ -31,15 +31,17 @@ class CheckPermission
             : $this->userTypePermissionService->allGuestPermissions();
 
         $hasPermission = false;
+
         foreach (explode(separator: '|', string: $scopes) as $scope) {
             if ($this->hasPermission(userPermissions: $userPermissions, scope: $scope, permissionType: $permissionType)) {
                 $hasPermission = true;
+
                 break;
             }
         }
 
         if (! $hasPermission) {
-            return response()->json(data: ['message' => 'Unauthorized'], status: 403);
+            return response()->json(data: ['message' => __('auth.unauthorized')], status: 403);
         }
 
         return $next($request);
@@ -59,7 +61,8 @@ class CheckPermission
             'menu'   => 256,
         ];
 
-        $requiredPermission = $bitwisePermissionTable[$permissionType] ?? 0;
+        $requiredPermission = data_get($bitwisePermissionTable, $permissionType, 0);
+
         $permissionRecord = $userPermissions->first(function ($permission) use ($scope) {
             return $permission->permission->scope === $scope;
         });

+ 40 - 30
app/Http/Middleware/PerformanceMonitor.php

@@ -45,8 +45,8 @@ class PerformanceMonitor
 
     public function __construct()
     {
-        $this->startTime = defined('LARAVEL_START') ? LARAVEL_START : microtime(true);
-        $this->startMemory = memory_get_usage();
+        $this->startTime       = defined('LARAVEL_START') ? LARAVEL_START : microtime(true);
+        $this->startMemory     = memory_get_usage();
         $this->startPeakMemory = memory_get_peak_usage();
     }
 
@@ -100,13 +100,15 @@ class PerformanceMonitor
     private function getMemoryLimit(): int
     {
         $limit = ini_get('memory_limit');
+
         if (! $limit) {
             return 0;
         }
 
         preg_match('/^(\d+)(K|M|G)?$/i', $limit, $matches);
-        $value = (int) ($matches[1] ?? 0);
-        $unit = strtoupper($matches[2] ?? '');
+
+        $value = (int) data_get($matches, 1, 0);
+        $unit  = strtoupper(data_get($matches, 2, ''));
 
         return match ($unit) {
             'G'     => $value * 1024 * 1024 * 1024,
@@ -128,18 +130,22 @@ class PerformanceMonitor
     private function getQueryMetrics(): ?array
     {
         $queries = DB::getQueryLog();
+
         if (empty($queries)) {
             return null;
         }
 
         $totalTime = 0;
+
         $formattedQueries = [];
+
         foreach ($queries as $query) {
-            $totalTime += $query['time'];
-            if ($query['time'] > self::SLOW_QUERY_THRESHOLD) {
+            $totalTime += data_get($query, 'time');
+
+            if (data_get($query, 'time') > self::SLOW_QUERY_THRESHOLD) {
                 $formattedQueries[] = [
-                    'sql'  => $this->formatSql($query['query'], $query['bindings']),
-                    'time' => $query['time'],
+                    'sql'  => $this->formatSql(data_get($query, 'query'), data_get($query, 'bindings')),
+                    'time' => data_get($query, 'time'),
                 ];
             }
         }
@@ -165,8 +171,8 @@ class PerformanceMonitor
         $usage = getrusage();
 
         return [
-            'user'   => ($usage['ru_utime.tv_sec'] * 1000 + intval($usage['ru_utime.tv_usec'] / 1000)),
-            'system' => ($usage['ru_stime.tv_sec'] * 1000 + intval($usage['ru_stime.tv_usec'] / 1000)),
+            'user'   => (data_get($usage, 'ru_utime.tv_sec') * 1000 + intval(data_get($usage, 'ru_utime.tv_usec') / 1000)),
+            'system' => (data_get($usage, 'ru_stime.tv_sec') * 1000 + intval(data_get($usage, 'ru_stime.tv_usec') / 1000)),
         ];
     }
 
@@ -177,18 +183,19 @@ class PerformanceMonitor
         }
 
         $status = opcache_get_status(false);
+
         if (! $status) {
             return null;
         }
 
-        $stats = $status['opcache_statistics'];
-        $hits = $stats['hits'];
-        $misses = $stats['misses'];
-        $total = $hits + $misses;
+        $stats  = data_get($status, 'opcache_statistics');
+        $hits   = data_get($stats, 'hits');
+        $misses = data_get($stats, 'misses');
+        $total  = $hits + $misses;
 
         return [
             'hit_rate'     => $total > 0 ? ($hits / $total * 100) : 0,
-            'memory_usage' => $status['memory_usage'],
+            'memory_usage' => data_get($status, 'memory_usage'),
         ];
     }
 
@@ -205,6 +212,7 @@ class PerformanceMonitor
 
         try {
             $written = file_put_contents('php://stderr', $this->buffer);
+
             if ($written === false) {
                 error_log('Failed to write performance metrics to stderr');
             }
@@ -238,33 +246,34 @@ class PerformanceMonitor
         $this->appendToBuffer(sprintf(
             '➜ Time: %s%.2fms%s | Memory: %s%.2fMB%s (%.1f%%) | Peak: %s%.2fMB%s',
             self::COLORS['yellow'],
-            $metrics['time'],
+            data_get($metrics, 'time'),
             self::COLORS['reset'],
             self::COLORS['magenta'],
-            $metrics['memory'],
+            data_get($metrics, 'memory'),
             self::COLORS['reset'],
-            $metrics['memoryPercentage'],
+            data_get($metrics, 'memoryPercentage'),
             self::COLORS['magenta'],
-            $metrics['peakMemory'],
+            data_get($metrics, 'peakMemory'),
             self::COLORS['reset']
         ));
 
         // Query metrics
-        if ($metrics['queries']) {
+        if (data_get($metrics, 'queries')) {
             $this->appendToBuffer(sprintf(
                 ' | Queries: %s%d%s (%s%.2fms%s)',
                 self::COLORS['cyan'],
-                $metrics['queries']['count'],
+                data_get($metrics, 'queries.count'),
                 self::COLORS['reset'],
                 self::COLORS['cyan'],
-                $metrics['queries']['time'],
+                data_get($metrics, 'queries.time'),
                 self::COLORS['reset']
             ));
         }
 
         // CPU metrics
-        if ($metrics['cpu']) {
-            $totalCpu = $metrics['cpu']['user'] + $metrics['cpu']['system'];
+        if (data_get($metrics, 'cpu')) {
+            $totalCpu = data_get($metrics, 'cpu.user') + data_get($metrics, 'cpu.system');
+
             $this->appendToBuffer(sprintf(
                 ' | CPU: %s%.2fms%s',
                 self::COLORS['cyan'],
@@ -274,11 +283,11 @@ class PerformanceMonitor
         }
 
         // Cache metrics
-        if ($metrics['cache']) {
+        if (data_get($metrics, 'cache')) {
             $this->appendToBuffer(sprintf(
                 ' | OPcache: %s%.1f%%%s',
                 self::COLORS['cyan'],
-                $metrics['cache']['hit_rate'],
+                data_get($metrics, 'cache.hit_rate'),
                 self::COLORS['reset']
             ));
         }
@@ -286,15 +295,16 @@ class PerformanceMonitor
         $this->appendToBuffer("\n");
 
         // Log slow queries
-        if ($metrics['time'] > self::SLOW_REQUEST_THRESHOLD && ! empty($metrics['queries']['slow_queries'])) {
+        if (data_get($metrics, 'time') > self::SLOW_REQUEST_THRESHOLD && ! empty(data_get($metrics, 'queries.slow_queries'))) {
             $this->appendToBuffer("\n".$this->colorize('Slow Queries Detected:', 'yellow')."\n");
-            foreach ($metrics['queries']['slow_queries'] as $query) {
+
+            foreach (data_get($metrics, 'queries.slow_queries') as $query) {
                 $this->appendToBuffer(sprintf(
                     "➜ %s%.2fms%s: %s\n",
                     self::COLORS['yellow'],
-                    $query['time'],
+                    data_get($query, 'time'),
                     self::COLORS['reset'],
-                    $query['sql']
+                    data_get($query, 'sql')
                 ));
             }
         }

+ 1 - 0
app/Http/Middleware/SetUserLanguage.php

@@ -19,6 +19,7 @@ class SetUserLanguage
     {
         $language = substr($request->header('Accept-Language'), 0, 2);
         $language = LanguageEnum::fromString($language);
+
         if ($language) {
             App::setLocale($language->value);
         } else {

+ 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',
+        ];
+    }
+}

+ 6 - 6
app/Http/Requests/ClientFavoriteProviderRequest.php

@@ -53,12 +53,12 @@ class ClientFavoriteProviderRequest extends FormRequest
     public function messages(): array
     {
         return [
-            'client_id.required'   => 'O cliente é obrigatório.',
-            'client_id.exists'     => 'Cliente não encontrado.',
-            'provider_id.required' => 'O prestador é obrigatório.',
-            'provider_id.exists'   => 'Prestador não encontrado.',
-            'provider_id.unique'   => 'Este prestador já está favoritado.',
-            'notes.max'            => 'As observações não podem exceder 1000 caracteres.',
+            'client_id.required'   => __('requests.client_favorite_provider.client_required'),
+            'client_id.exists'     => __('requests.client_favorite_provider.client_not_found'),
+            'provider_id.required' => __('requests.client_favorite_provider.provider_required'),
+            'provider_id.exists'   => __('requests.client_favorite_provider.provider_not_found'),
+            'provider_id.unique'   => __('requests.client_favorite_provider.already_favorite'),
+            'notes.max'            => __('requests.client_favorite_provider.notes_max'),
         ];
     }
 }

+ 14 - 14
app/Http/Requests/ClientPaymentMethodRequest.php

@@ -45,20 +45,20 @@ class ClientPaymentMethodRequest extends FormRequest
     public function messages(): array
     {
         return [
-            'client_id.required'        => 'O cliente é obrigatório.',
-            'client_id.exists'          => 'Cliente não encontrado.',
-            'token.required'            => 'O token do cartão é obrigatório.',
-            'card_number.required'      => 'O número do cartão é obrigatório.',
-            'card_number.min'           => 'O número do cartão deve ter no mínimo 13 dígitos.',
-            'card_number.max'           => 'O número do cartão deve ter no máximo 19 dígitos.',
-            'holder_name.required'      => 'O nome do titular é obrigatório.',
-            'expiration.required'       => 'A data de validade é obrigatória.',
-            'expiration.regex'          => 'A data de validade deve estar no formato MM/YYYY.',
-            'cvv.required'              => 'O CVV é obrigatório.',
-            'cvv.min'                   => 'O CVV deve ter no mínimo 3 dígitos.',
-            'cvv.max'                   => 'O CVV deve ter no máximo 4 dígitos.',
-            'last_four_digits.required' => 'Os últimos 4 dígitos são obrigatórios.',
-            'last_four_digits.size'     => 'Devem ser exatamente 4 dígitos.',
+            'client_id.required'        => __('requests.client_payment_method.client_required'),
+            'client_id.exists'          => __('requests.client_payment_method.client_not_found'),
+            'token.required'            => __('requests.client_payment_method.token_required'),
+            'card_number.required'      => __('requests.client_payment_method.card_number_required'),
+            'card_number.min'           => __('requests.client_payment_method.card_number_min'),
+            'card_number.max'           => __('requests.client_payment_method.card_number_max'),
+            'holder_name.required'      => __('requests.client_payment_method.holder_name_required'),
+            'expiration.required'       => __('requests.client_payment_method.expiration_required'),
+            'expiration.regex'          => __('requests.client_payment_method.expiration_format'),
+            'cvv.required'              => __('requests.client_payment_method.cvv_required'),
+            'cvv.min'                   => __('requests.client_payment_method.cvv_min'),
+            'cvv.max'                   => __('requests.client_payment_method.cvv_max'),
+            'last_four_digits.required' => __('requests.client_payment_method.last_four_required'),
+            'last_four_digits.size'     => __('requests.client_payment_method.last_four_size'),
         ];
     }
 

+ 1 - 1
app/Http/Requests/ClientProviderBlockRequest.php

@@ -55,7 +55,7 @@ class ClientProviderBlockRequest extends FormRequest
             'client_id.exists'     => __('validation.exists', ['attribute' => __('validation.attributes.client')]),
             'provider_id.required' => __('validation.required', ['attribute' => __('validation.attributes.provider')]),
             'provider_id.exists'   => __('validation.exists', ['attribute' => __('validation.attributes.provider')]),
-            'provider_id.unique'   => __('validation.client_provider_block.already_blocked'),
+            'provider_id.unique'   => __('validation.custom.client_provider_block.already_blocked'),
         ];
     }
 }

+ 7 - 0
app/Http/Requests/ClientRequest.php

@@ -3,6 +3,7 @@
 namespace App\Http\Requests;
 
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
 
 class ClientRequest extends FormRequest
 {
@@ -13,6 +14,12 @@ class ClientRequest extends FormRequest
         $rules = [
             'avatar' => 'sometimes|file|image|mimes:jpg,jpeg,png,webp|max:5120',
 
+            'selfie_verified' => [
+                'sometimes',
+                'boolean',
+                Rule::prohibitedIf(fn () => ! $this->user()?->canModerateClients()),
+            ],
+
             'document' => [
                 'sometimes',
                 'string',

+ 2 - 2
app/Http/Requests/CustomScheduleAvailableRequest.php

@@ -34,8 +34,8 @@ class CustomScheduleAvailableRequest extends FormRequest
     public function messages(): array
     {
         return [
-            'provider_id.required' => 'O ID do prestador é obrigatório.',
-            'provider_id.exists'   => 'O prestador informado não existe.',
+            'provider_id.required' => __('requests.custom_schedule_available.provider_id_required'),
+            'provider_id.exists'   => __('requests.custom_schedule_available.provider_not_found'),
         ];
     }
 }

+ 3 - 3
app/Http/Requests/CustomScheduleRequest.php

@@ -56,9 +56,9 @@ class CustomScheduleRequest extends FormRequest
     public function messages(): array
     {
         return [
-            'max_price.gte'       => 'O preço máximo deve ser maior ou igual ao preço mínimo.',
-            'date.after_or_equal' => 'A data deve ser hoje ou uma data futura.',
-            'end_time.after'      => 'O horário de término deve ser posterior ao horário de início.',
+            'max_price.gte'       => __('requests.custom_schedule.max_price_gte'),
+            'date.after_or_equal' => __('requests.custom_schedule.date_after_or_equal'),
+            'end_time.after'      => __('requests.custom_schedule.end_time_after'),
         ];
     }
 }

+ 23 - 0
app/Http/Requests/PayScheduleRequest.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class PayScheduleRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        return [
+            'payment_method'           => ['required', 'in:credit_card,pix'],
+            'client_payment_method_id' => ['nullable', 'integer', 'exists:client_payment_methods,id'],
+            'card_id'                  => ['nullable', 'string', 'max:255'],
+            'phone'                    => ['nullable', 'string', 'max:20'],
+        ];
+    }
+}

+ 23 - 0
app/Http/Requests/PayServicePackageRequest.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class PayServicePackageRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        return [
+            'payment_method'           => ['required', 'in:credit_card,pix'],
+            'client_payment_method_id' => ['nullable', 'integer', 'exists:client_payment_methods,id'],
+            'card_id'                  => ['nullable', 'string', 'max:255'],
+            'phone'                    => ['nullable', 'string', 'max:20'],
+        ];
+    }
+}

+ 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',
+        ];
+    }
+}

+ 1 - 1
app/Http/Requests/ProviderClientBlockRequest.php

@@ -55,7 +55,7 @@ class ProviderClientBlockRequest extends FormRequest
             'provider_id.exists'   => __('validation.exists', ['attribute' => __('validation.attributes.provider')]),
             'client_id.required'   => __('validation.required', ['attribute' => __('validation.attributes.client')]),
             'client_id.exists'     => __('validation.exists', ['attribute' => __('validation.attributes.client')]),
-            'client_id.unique'     => __('validation.provider_client_block.already_blocked'),
+            'client_id.unique'     => __('validation.custom.provider_client_block.already_blocked'),
         ];
     }
 }

+ 12 - 4
app/Http/Requests/ProviderRequest.php

@@ -3,6 +3,7 @@
 namespace App\Http\Requests;
 
 use App\Enums\ApprovalStatusEnum;
+use App\Enums\GenderEnum;
 use App\Enums\UserTypeEnum;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Validation\Rule;
@@ -62,10 +63,17 @@ class ProviderRequest extends FormRequest
                 },
             ],
 
-            'average_rating'         => 'sometimes|nullable|numeric|min:0|max:5',
-            'total_services'         => 'sometimes|integer|min:0',
-            'birth_date'             => 'sometimes|nullable|date|before:today',
-            'selfie_verified'        => 'sometimes|boolean',
+            'average_rating' => 'sometimes|nullable|numeric|min:0|max:5',
+            'total_services' => 'sometimes|integer|min:0',
+            'birth_date'     => 'sometimes|nullable|date|before:today',
+            'gender'         => ['sometimes', 'nullable', Rule::enum(GenderEnum::class)],
+
+            'selfie_verified' => [
+                'sometimes',
+                'boolean',
+                Rule::prohibitedIf(fn () => ! $this->user()?->canModerateProviders()),
+            ],
+
             'document_verified'      => 'sometimes|boolean',
             'approval_status'        => ['sometimes', Rule::enum(ApprovalStatusEnum::class)],
             'daily_price_8h'         => "sometimes|nullable|numeric|min:{$dailyPriceMin}|max:500",

+ 25 - 17
app/Http/Requests/RegisterClientRequest.php

@@ -9,25 +9,33 @@ class RegisterClientRequest extends FormRequest
     public function rules(): array
     {
         return [
-            'email'          => 'sometimes|email|nullable',
-            'phone'          => 'sometimes|string|nullable',
-            'name'           => 'sometimes|string|max:255|nullable',
             'code'           => 'required|string|max:6',
-            'document'       => 'sometimes|string|nullable',
-            'zip_code'       => 'sometimes|string|max:20|nullable',
-            'address'        => 'sometimes|string|max:255|nullable',
-            'number'         => 'sometimes|string|max:20|nullable',
-            'district'       => 'sometimes|string|max:255|nullable',
-            'complement'     => 'sometimes|string|max:255|nullable',
-            'has_complement' => 'sometimes|boolean|nullable',
-            'nickname'       => 'sometimes|string|max:255|nullable',
-            'instructions'   => 'sometimes|string|max:500|nullable',
-            'address_type'   => 'sometimes|string|in:home,commercial,other|nullable',
-            'city'           => 'sometimes|string|max:255|nullable',
-            'state'          => 'sometimes|string|max:10|nullable',
-            'latitude'       => 'sometimes|numeric|nullable',
-            'longitude'      => 'sometimes|numeric|nullable',
+            'email'          => 'required|string|email|max:255',
+            'name'           => 'required|string|max:255',
+            'phone'          => 'required|string|max:20',
+            'document'       => 'required|string|max:20',
+            'zip_code'       => 'required|string|max:20',
+            'number'         => 'required|string|max:20',
+            'address'        => 'required|string|max:255',
+            'district'       => 'required|string|max:255',
+            'city'           => 'required|string|max:255',
+            'state'          => 'required|string|max:10',
+            'has_complement' => 'required|boolean',
+
+            'complement'     => 'sometimes|nullable|string|max:255',
+            'nickname'       => 'sometimes|nullable|string|max:255',
+            'instructions'   => 'sometimes|nullable|string|max:500',
+            'address_type'   => 'sometimes|nullable|string|in:home,commercial,other',
+            'latitude'       => 'sometimes|nullable|numeric',
+            'longitude'      => 'sometimes|nullable|numeric',
             'avatar'         => 'sometimes|nullable|file|image|mimes:jpg,jpeg,png,webp|max:5120',
         ];
     }
+
+    protected function prepareForValidation(): void
+    {
+        if (! $this->filled('has_complement')) {
+            $this->merge(['has_complement' => false]);
+        }
+    }
 }

+ 53 - 74
app/Http/Requests/RegisterProviderRequest.php

@@ -2,9 +2,9 @@
 
 namespace App\Http\Requests;
 
+use App\Enums\GenderEnum;
 use App\Enums\WorkingPeriodEnum;
 use Illuminate\Foundation\Http\FormRequest;
-use Illuminate\Validation\Rule;
 
 class RegisterProviderRequest extends FormRequest
 {
@@ -13,93 +13,72 @@ class RegisterProviderRequest extends FormRequest
     $dailyPriceMin = config('provider.daily_price_min');
 
     $rules = [
-      'email'      => 'sometimes|email',
-      'phone'      => 'sometimes|string|nullable|max:20',
-      'name'       => 'required|string|max:255',
-      'code'       => 'required|string|max:6',
-      'document'   => ['required', 'string', 'max:20'],
-      'rg'         => 'required|string|max:20',
-      'birth_date' => 'required|date|before:today',
+      'code'            => 'required|string|max:6',
+      'email'           => 'required|string|email|max:255',
+      'name'            => 'required|string|max:255',
+      'phone'           => 'required|string|max:20',
+      'document'        => 'required|string|max:20',
+      'rg'              => 'required|string|max:20',
+      'gender'          => 'required|string|in:'.implode(',', GenderEnum::toArray()),
+      'birth_date'      => 'required|date|before:today',
+
+      'zip_code'        => 'required|string|max:20',
+      'number'          => 'required|string|max:20',
+      'address'         => 'required|string|max:255',
+      'district'        => 'required|string|max:255',
+      'city'            => 'required|string|max:255',
+      'state'           => 'required|string|max:2',
+      'address_type'    => 'required|string|in:home,commercial,other',
+      'has_complement'  => 'required|boolean',
+
+      'daily_price_8h'  => "required|numeric|min:{$dailyPriceMin}|max:500",
+      'daily_price_6h'  => 'required|numeric|min:0',
+      'daily_price_4h'  => 'required|numeric|min:0',
+      'daily_price_2h'  => 'required|numeric|min:0',
 
-      'recipient_name'        => 'sometimes|string|max:255',
-      'recipient_email'       => 'sometimes|email|max:255',
-      'recipient_description' => 'sometimes|string',
-      'recipient_document'    => 'sometimes|string|max:20',
-      'recipient_type'        => ['sometimes', Rule::in(['individual', 'company'])],
+      'selfie'          => 'required|file|image|mimes:jpg,jpeg,png,webp|max:5120',
+      'document_front'  => 'required|file|image|mimes:jpg,jpeg,png,webp|max:10240',
+      'document_back'   => 'required|file|image|mimes:jpg,jpeg,png,webp|max:10240',
+
+      'working_days'          => 'required|array|min:1',
+      'working_days.*.day'    => 'required|integer|min:0|max:6',
+      'working_days.*.period' => 'required|string|in:'.implode(',', array_column(WorkingPeriodEnum::cases(), 'value')),
 
-      'recipient_payment_mode' => ['sometimes', Rule::in(['bank_transfer'])],
+      'complement'   => 'sometimes|nullable|string|max:255',
+      'nickname'     => 'sometimes|nullable|string|max:255',
+      'latitude'     => 'sometimes|nullable|numeric',
+      'longitude'    => 'sometimes|nullable|numeric',
+      'instructions' => 'sometimes|nullable|string',
 
-      'recipient_default_bank_account'                     => 'sometimes|array',
+      //
+
+      'recipient_email'        => 'sometimes|string|email|max:255',
+      'recipient_name'         => 'sometimes|string|max:255',
+      'recipient_document'     => 'sometimes|string|max:20',
+      'recipient_description'  => 'sometimes|string',
+      'recipient_type'         => 'sometimes|string|in:individual,company',
+      'recipient_payment_mode' => 'sometimes|string|in:bank_transfer',
+
+      'recipient_default_bank_account' => 'sometimes|array',
+ 
       'recipient_default_bank_account.holder_name'         => 'sometimes|string|max:255',
-      'recipient_default_bank_account.holder_type'         => ['sometimes', Rule::in(['individual', 'company'])],
       'recipient_default_bank_account.holder_document'     => 'sometimes|string|max:20',
+      'recipient_default_bank_account.holder_type'         => 'sometimes|string|in:individual,company',
       'recipient_default_bank_account.bank'                => 'sometimes|string|max:3',
       'recipient_default_bank_account.branch_number'       => 'sometimes|string|max:20',
-      'recipient_default_bank_account.branch_check_digit'  => 'sometimes|nullable|string|max:10',
+      'recipient_default_bank_account.branch_check_digit'  => 'sometimes|string|max:10',
       'recipient_default_bank_account.account_number'      => 'sometimes|string|max:20',
       'recipient_default_bank_account.account_check_digit' => 'sometimes|string|max:10',
-      'recipient_default_bank_account.type'                => ['sometimes', Rule::in(['checking', 'savings'])],
+      'recipient_default_bank_account.type'                => 'sometimes|string|in:checking,savings',
       'recipient_default_bank_account.metadata'            => 'sometimes|array',
-      'recipient_default_bank_account.pix_key'             => 'sometimes|nullable|string|max:255',
+      'recipient_default_bank_account.pix_key'             => 'sometimes|string|nullable|max:255',
 
       'recipient_metadata' => 'sometimes|array',
-
-      'zip_code'       => 'required|string|max:20',
-      'address'        => 'required|string|max:255',
-      'number'         => 'sometimes|string|max:20|nullable',
-      'district'       => 'sometimes|string|max:255|nullable',
-      'has_complement' => 'sometimes|boolean',
-      'complement'     => 'nullable|string|max:255',
-      'nickname'       => 'nullable|string|max:255',
-      'instructions'   => 'nullable|string',
-      'address_type'   => ['required', Rule::in(['home', 'commercial', 'other'])],
-      'city'           => 'nullable|string|max:255',
-      'state'          => 'nullable|string|max:2',
-      'latitude'       => 'sometimes|numeric|nullable',
-      'longitude'      => 'sometimes|numeric|nullable',
-
-      'daily_price_8h' => "required|numeric|min:{$dailyPriceMin}|max:500",
-      'daily_price_6h' => 'required|numeric|min:0',
-      'daily_price_4h' => 'required|numeric|min:0',
-      'daily_price_2h' => 'required|numeric|min:0',
-
-      'services_types_ids' => 'sometimes|array',
-
-      'services_types_ids.*' => [
-        'integer',
-        Rule::exists('service_types', 'id')->where(function ($query) {
-          $query->whereNull('deleted_at')->where('is_active', true);
-        }),
-      ],
-
-      'service_types_ids' => 'sometimes|array',
-
-      'service_types_ids.*' => [
-        'integer',
-        Rule::exists('service_types', 'id')->where(function ($query) {
-          $query->whereNull('deleted_at')->where('is_active', true);
-        }),
-      ],
-
-      'working_days'          => 'required|array|min:1',
-      'working_days.*.day'    => 'required|integer|min:0|max:6',
-      'working_days.*.period' => ['required', Rule::in([WorkingPeriodEnum::MORNING->value, WorkingPeriodEnum::AFTERNOON->value])],
-
-      'selfie'          => 'required|file|image|mimes:jpg,jpeg,png,webp|max:5120',
-      'document_front'  => 'required|file|image|mimes:jpg,jpeg,png,webp|max:10240',
-      'document_back'   => 'required|file|image|mimes:jpg,jpeg,png,webp|max:10240',
+      
+      'services_types_ids'   => 'sometimes|array',
+      'services_types_ids.*' => 'integer|exists:service_types,id,deleted_at,NULL,is_active,1',
     ];
 
-    if (!$this->has('email')) {
-      $rules['phone'] = 'required|string|max:20';
-      $rules['email'] = 'nullable';
-    }
-
-    if (!$this->has('phone')) {
-      $rules['email'] = 'required|email';
-      $rules['phone'] = 'nullable';
-    }
-
     return $rules;
   }
 }

+ 1 - 1
app/Http/Requests/ReviewImprovementRequest.php

@@ -42,7 +42,7 @@ class ReviewImprovementRequest extends FormRequest
     public function messages(): array
     {
         return [
-            'improvement_type_id.unique' => __('validation.review_improvement.already_exists'),
+            'improvement_type_id.unique' => __('validation.custom.review_improvement.already_exists'),
         ];
     }
 }

+ 21 - 28
app/Http/Requests/ScheduleRequest.php

@@ -18,8 +18,6 @@ class ScheduleRequest extends FormRequest
             'provider_id'              => 'sometimes|required|exists:providers,id',
             'address_id'               => 'sometimes|required|exists:addresses,id',
             'date'                     => 'nullable|date|after_or_equal:today',
-            'dates'                    => 'nullable|array',
-            'dates.*'                  => 'date|after_or_equal:today',
             'offers_meal'              => 'sometimes|nullable|boolean',
             'period_type'              => 'sometimes|required|in:2,4,6,8',
             'schedule_type'            => 'sometimes|in:default,custom',
@@ -34,9 +32,7 @@ class ScheduleRequest extends FormRequest
             $rules['client_id']    = 'required|exists:clients,id';
             $rules['provider_id']  = 'required|exists:providers,id';
             $rules['address_id']   = 'required|exists:addresses,id';
-            $rules['date']         = 'required_without:dates|nullable|date|after_or_equal:today';
-            $rules['dates']        = 'nullable|array';
-            $rules['dates.*']      = 'date|after_or_equal:today';
+            $rules['date']         = 'required|date|after_or_equal:today';
             $rules['period_type']  = 'required|in:2,4,6,8';
             $rules['start_time']   = 'required|date_format:H:i';
             $rules['end_time']     = 'required|date_format:H:i|after:start_time';
@@ -50,29 +46,26 @@ class ScheduleRequest extends FormRequest
     public function messages(): array
     {
         return [
-            'client_id.required'     => 'O cliente é obrigatório.',
-            'client_id.exists'       => 'Cliente não encontrado.',
-            'provider_id.required'   => 'O prestador é obrigatório.',
-            'provider_id.exists'     => 'Prestador não encontrado.',
-            'address_id.required'    => 'O endereço é obrigatório.',
-            'address_id.exists'      => 'Endereço não encontrado.',
-            'date.required_without'  => 'A data é obrigatória quando não há múltiplas datas.',
-            'date.date'              => 'Data inválida.',
-            'date.after_or_equal'    => 'A data deve ser hoje ou futura.',
-            'dates.array'            => 'Datas devem ser um array.',
-            'dates.*.date'           => 'Uma das datas é inválida.',
-            'dates.*.after_or_equal' => 'Todas as datas devem ser hoje ou futuras.',
-            'period_type.required'   => 'O período é obrigatório.',
-            'period_type.in'         => 'Período inválido.',
-            'start_time.required'    => 'O horário de início é obrigatório.',
-            'start_time.date_format' => 'Formato de horário inválido.',
-            'end_time.required'      => 'O horário de término é obrigatório.',
-            'end_time.date_format'   => 'Formato de horário inválido.',
-            'end_time.after'         => 'O horário de término deve ser após o horário de início.',
-            'total_amount.required'  => 'O valor total é obrigatório.',
-            'total_amount.numeric'   => 'O valor total deve ser numérico.',
-            'total_amount.min'       => 'O valor total deve ser maior ou igual a zero.',
-            'status.in'              => 'Status inválido.',
+            'client_id.required'     => __('requests.schedule.client_required'),
+            'client_id.exists'       => __('requests.schedule.client_not_found'),
+            'provider_id.required'   => __('requests.schedule.provider_required'),
+            'provider_id.exists'     => __('requests.schedule.provider_not_found'),
+            'address_id.required'    => __('requests.schedule.address_required'),
+            'address_id.exists'      => __('requests.schedule.address_not_found'),
+            'date.required'          => __('requests.schedule.date_required'),
+            'date.date'              => __('requests.schedule.invalid_date'),
+            'date.after_or_equal'    => __('requests.schedule.date_after_or_equal'),
+            'period_type.required'   => __('requests.schedule.period_required'),
+            'period_type.in'         => __('requests.schedule.invalid_period'),
+            'start_time.required'    => __('requests.schedule.start_time_required'),
+            'start_time.date_format' => __('requests.schedule.invalid_time_format'),
+            'end_time.required'      => __('requests.schedule.end_time_required'),
+            'end_time.date_format'   => __('requests.schedule.invalid_time_format'),
+            'end_time.after'         => __('requests.schedule.end_time_after'),
+            'total_amount.required'  => __('requests.schedule.total_amount_required'),
+            'total_amount.numeric'   => __('requests.schedule.total_amount_numeric'),
+            'total_amount.min'       => __('requests.schedule.total_amount_min'),
+            'status.in'              => __('requests.schedule.invalid_status'),
         ];
     }
 }

+ 5 - 5
app/Http/Requests/CartItemRequest.php → app/Http/Requests/ServicePackageItemRequest.php

@@ -4,18 +4,18 @@ namespace App\Http\Requests;
 
 use Illuminate\Foundation\Http\FormRequest;
 
-class CartItemRequest extends FormRequest
+class ServicePackageItemRequest extends FormRequest
 {
     public function rules(): array
     {
         $rules = [
-            'cart_id'     => ['sometimes', 'integer', 'exists:carts,id'],
-            'schedule_id' => ['sometimes', 'integer', 'exists:schedules,id'],
+            'service_package_id' => ['sometimes', 'integer', 'exists:service_packages,id'],
+            'schedule_id'        => ['sometimes', 'integer', 'exists:schedules,id'],
         ];
 
         if ($this->isMethod('POST')) {
-            $rules['cart_id']     = ['required', 'integer', 'exists:carts,id'];
-            $rules['schedule_id'] = ['required', 'integer', 'exists:schedules,id'];
+            $rules['service_package_id'] = ['required', 'integer', 'exists:service_packages,id'];
+            $rules['schedule_id']        = ['required', 'integer', 'exists:schedules,id'];
         }
 
         return $rules;

+ 30 - 1
app/Http/Requests/CartRequest.php → app/Http/Requests/ServicePackageRequest.php

@@ -3,8 +3,9 @@
 namespace App\Http\Requests;
 
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Validator;
 
-class CartRequest extends FormRequest
+class ServicePackageRequest extends FormRequest
 {
     public function authorize(): bool
     {
@@ -62,6 +63,34 @@ class CartRequest extends FormRequest
         return $rules;
     }
 
+    public function after(): array
+    {
+        return [function (Validator $validator): void {
+            if (! $this->hasSchedulePayload()) {
+                return;
+            }
+
+            $rootProviderId = $this->integer('provider_id') ?: null;
+
+            $providerIds = collect($this->input('schedules', []))
+                ->map(fn (array $schedule) => data_get($schedule, 'provider_id', $rootProviderId))
+                ->when(
+                    ! $this->filled('schedules') && $rootProviderId,
+                    fn ($ids) => $ids->push($rootProviderId),
+                )
+                ->filter()
+                ->unique()
+                ->values();
+
+            if ($providerIds->count() > 1) {
+                $validator->errors()->add(
+                    'schedules',
+                    __('messages.service_package_same_provider'),
+                );
+            }
+        }];
+    }
+
     private function hasSchedulePayload(): bool
     {
         return $this->filled('date')

+ 2 - 0
app/Http/Requests/UserRequest.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Requests;
 
+use App\Enums\GenderEnum;
 use App\Enums\LanguageEnum;
 use App\Enums\UserTypeEnum;
 use Illuminate\Foundation\Http\FormRequest;
@@ -25,6 +26,7 @@ class UserRequest extends FormRequest
             'type'     => ['sometimes', Rule::enum(UserTypeEnum::class)],
             'language' => ['sometimes', Rule::enum(LanguageEnum::class)],
             'phone'    => 'sometimes|string|nullable',
+            'gender'   => 'sometimes|string|in:'.implode(',', GenderEnum::toArray()),
         ];
 
         if ($this->isMethod('post')) {

+ 3 - 3
app/Http/Resources/AuthResource.php

@@ -16,11 +16,11 @@ class AuthResource extends JsonResource
     public function toArray(Request $request): array
     {
         return [
-            'access_token'  => $this['access_token'],
-            'refresh_token' => $this['refresh_token'] ?? null,
+            'access_token'  => data_get($this, 'access_token'),
+            'refresh_token' => data_get($this, 'refresh_token'),
             'token_type'    => 'Bearer',
             'expires_in'    => 900,
-            'user'          => new UserResource($this['user']),
+            'user'          => new UserResource(data_get($this, 'user')),
         ];
     }
 

+ 0 - 37
app/Http/Resources/CartItemResource.php

@@ -1,37 +0,0 @@
-<?php
-
-namespace App\Http\Resources;
-
-use App\Models\CartItem;
-use Illuminate\Http\Request;
-use Illuminate\Http\Resources\Json\JsonResource;
-use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
-
-class CartItemResource extends JsonResource
-{
-    /**
-     * Transform the resource into an array.
-     *
-     * @return array<string, mixed>
-     */
-    public function toArray(Request $request): array
-    {
-        return [
-            'id'          => $this->id,
-            'cart_id'     => $this->cart_id,
-            'schedule_id' => $this->schedule_id,
-            'schedule'    => new ScheduleResource($this->whenLoaded('schedule')),
-            'created_at'  => $this->created_at?->toISOString(),
-            'updated_at'  => $this->updated_at?->toISOString(),
-        ];
-    }
-
-    /**
-     * @param \Illuminate\Database\Eloquent\Collection<CartItem> $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<CartItemResource>
-     */
-    public static function collection($resource): AnonymousResourceCollection
-    {
-        return parent::collection($resource);
-    }
-}

+ 0 - 4
app/Http/Resources/CityResource.php

@@ -25,10 +25,6 @@ class CityResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<City>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<CityResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);

+ 7 - 3
app/Http/Resources/ClientFavoriteProviderResource.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Resources;
 
+use App\Enums\GenderEnum;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Support\Facades\Storage;
@@ -12,20 +13,23 @@ class ClientFavoriteProviderResource extends JsonResource
     {
         $provider = $this->resource->relationLoaded('provider') ? $this->resource->provider : null;
 
-        $profileMediaPath = $this->profile_media_path
-            ?? ($provider?->relationLoaded('profileMedia') ? $provider->profileMedia?->path : null);
+        $profileMediaPath = $provider?->relationLoaded('profileMedia') ? $provider->profileMedia?->path : null;
 
         $dailyPrice8h = $this->daily_price_8h ?? $provider?->daily_price_8h;
         $dailyPrice6h = $this->daily_price_6h ?? $provider?->daily_price_6h;
         $dailyPrice4h = $this->daily_price_4h ?? $provider?->daily_price_4h;
         $dailyPrice2h = $this->daily_price_2h ?? $provider?->daily_price_2h;
 
+        $gender = $this->gender ?? $provider?->gender?->value;
+
         return [
             'id'             => $this->id,
             'client_id'      => $this->client_id,
             'provider_id'    => $this->provider_id,
             'provider_name'  => $this->provider_name ?? ($provider?->relationLoaded('user') ? $provider->user?->name : null),
-            'city_name'      => $this->city_name ?? null,
+            'gender'         => $gender,
+            'gender_label'   => GenderEnum::labelFor($gender),
+            'city_name'      => $this->city_name      ?? null,
             'average_rating' => $this->average_rating ?? $provider?->average_rating,
 
             'profile_media_url' => $profileMediaPath

+ 9 - 4
app/Http/Resources/ClientProviderBlockResource.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Resources;
 
+use App\Enums\GenderEnum;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 
@@ -9,15 +10,19 @@ class ClientProviderBlockResource extends JsonResource
 {
     public function toArray(Request $request): array
     {
+        $gender = $this->gender ?? $this->provider?->gender?->value;
+
         return [
             'id'                => $this->id,
             'client_id'         => $this->client_id,
             'provider_id'       => $this->provider_id,
-            'provider_name'     => $this->provider_name ?? $this->provider?->user?->name,
-            'provider_email'    => $this->provider_email ?? $this->provider?->user?->email,
-            'provider_phone'    => $this->provider_phone ?? $this->provider?->user?->phone,
+            'provider_name'     => $this->provider_name     ?? $this->provider?->user?->name,
+            'gender'            => $gender,
+            'gender_label'      => GenderEnum::labelFor($gender),
+            'provider_email'    => $this->provider_email    ?? $this->provider?->user?->email,
+            'provider_phone'    => $this->provider_phone    ?? $this->provider?->user?->phone,
             'provider_district' => $this->provider_district ?? null,
-            'provider_rating'   => $this->provider_rating ?? null,
+            'provider_rating'   => $this->provider_rating   ?? null,
             'created_at'        => $this->created_at?->format('Y-m-d H:i'),
             'updated_at'        => $this->updated_at?->format('Y-m-d H:i'),
         ];

+ 3 - 2
app/Http/Resources/ClientResource.php

@@ -18,8 +18,9 @@ class ClientResource extends JsonResource
             'id'               => $this->id,
             'document'         => $this->document,
             'user_id'          => $this->user_id,
-            'profile_media_id' => $this->profile_media_id,
-            'profile_media'    => new MediaResource($this->whenLoaded('profileMedia')),
+            'selfie_verified'  => $this->selfie_verified,
+            'profile_media_id' => $this->profileMedia?->id,
+            'profile_media'    => $this->profileMedia ? new MediaResource($this->profileMedia) : null,
             'user'             => new UserResource($this->whenLoaded('user')),
             'created_at'       => $this->created_at,
             'updated_at'       => $this->updated_at,

+ 0 - 4
app/Http/Resources/CountryResource.php

@@ -22,10 +22,6 @@ class CountryResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<Country>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<CountryResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);

+ 12 - 12
app/Http/Resources/DashboardClienteResource.php

@@ -15,18 +15,18 @@ class DashboardClienteResource extends JsonResource
     public function toArray(Request $request): array
     {
         return [
-            'headerBar'                  => $this['headerBar'],
-            'summaryInfos'               => $this['summaryInfos'],
-            'pendingSchedules'           => $this['pendingSchedules'],
-            'nextSchedules'              => $this['nextSchedules'],
-            'lastDoneSchedules'          => $this['lastDoneSchedules'],
-            'favoriteProviders'          => $this['favoriteProviders'],
-            'providersClose'             => $this['providersClose'],
-            'schedulesProposals'         => $this['schedulesProposals'],
-            'customSchedulesNoProposals' => $this['customSchedulesNoProposals'],
-            'todaySchedules'             => $this['todaySchedules'],
-            'notifications'              => $this['notifications'],
-            'has_payment_methods'        => $this['has_payment_methods'],
+            'headerBar'                  => data_get($this, 'headerBar'),
+            'summaryInfos'               => data_get($this, 'summaryInfos'),
+            'pendingSchedules'           => data_get($this, 'pendingSchedules'),
+            'nextSchedules'              => data_get($this, 'nextSchedules'),
+            'lastDoneSchedules'          => data_get($this, 'lastDoneSchedules'),
+            'favoriteProviders'          => data_get($this, 'favoriteProviders'),
+            'providersClose'             => data_get($this, 'providersClose'),
+            'schedulesProposals'         => data_get($this, 'schedulesProposals'),
+            'customSchedulesNoProposals' => data_get($this, 'customSchedulesNoProposals'),
+            'todaySchedules'             => data_get($this, 'todaySchedules'),
+            'notifications'              => data_get($this, 'notifications'),
+            'has_payment_methods'        => data_get($this, 'has_payment_methods'),
         ];
     }
 }

+ 8 - 8
app/Http/Resources/DashboardPrestadorResource.php

@@ -15,14 +15,14 @@ class DashboardPrestadorResource extends JsonResource
     public function toArray(Request $request): array
     {
         return [
-            'headerBar'      => $this['headerBar'],
-            'summaryInfos'   => $this['summaryInfos'],
-            'priceSuggested' => $this['priceSuggested'],
-            'todayServices'  => $this['todayServices'],
-            'solicitations'  => $this['solicitations'],
-            'nextSchedules'  => $this['nextSchedules'],
-            'opportunities'  => $this['opportunities'],
-            'notifications'  => $this['notifications'],
+            'headerBar'      => data_get($this, 'headerBar'),
+            'summaryInfos'   => data_get($this, 'summaryInfos'),
+            'priceSuggested' => data_get($this, 'priceSuggested'),
+            'todayServices'  => data_get($this, 'todayServices'),
+            'solicitations'  => data_get($this, 'solicitations'),
+            'nextSchedules'  => data_get($this, 'nextSchedules'),
+            'opportunities'  => data_get($this, 'opportunities'),
+            'notifications'  => data_get($this, 'notifications'),
         ];
     }
 }

+ 0 - 4
app/Http/Resources/ImprovementTypeResource.php

@@ -26,10 +26,6 @@ class ImprovementTypeResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<ImprovementType>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<ImprovementTypeResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);

+ 0 - 4
app/Http/Resources/MediaResource.php

@@ -27,10 +27,6 @@ class MediaResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<Media>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<MediaResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);

+ 11 - 29
app/Http/Resources/PaymentResource.php

@@ -17,9 +17,16 @@ class PaymentResource extends JsonResource
     public function toArray(Request $request): array
     {
         return [
-            'id'                          => $this->id,
-            'schedule_id'                 => $this->schedule_id,
-            'schedule'                    => new ScheduleResource($this->whenLoaded('schedule')),
+            'id'                 => $this->id,
+            'schedule_id'        => $this->schedule_id,
+            'schedule'           => new ScheduleResource($this->whenLoaded('schedule')),
+            'service_package_id' => $this->service_package_id,
+            'schedule_ids'       => $this->whenLoaded('servicePackage', fn () => $this->servicePackage?->items->pluck('schedule_id')->values()),
+
+            'schedules' => $this->whenLoaded('servicePackage', fn () => ScheduleResource::collection(
+                $this->servicePackage?->items->pluck('schedule')->filter()->values() ?? collect(),
+            )),
+
             'client_id'                   => $this->client_id,
             'provider_id'                 => $this->provider_id,
             'client_name'                 => $this->client?->user?->name,
@@ -45,7 +52,7 @@ class PaymentResource extends JsonResource
             'expires_at'                  => $this->expires_at?->toISOString(),
             'failure_code'                => $this->failure_code,
             'failure_message'             => $this->failure_message,
-            'pix'                         => $this->pixData(),
+            'pix'                         => $this->resource->pixData(),
             'gateway_payload'             => $this->gateway_payload,
             'metadata'                    => $this->metadata,
             'created_at'                  => $this->created_at?->toISOString(),
@@ -53,34 +60,9 @@ class PaymentResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<Payment>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<PaymentResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);
     }
 
-    private function pixData(): ?array
-    {
-        if ($this->payment_method !== 'pix') {
-            return null;
-        }
-
-        $charge      = $this->gateway_payload['charges'][0] ?? [];
-        $transaction = $charge['last_transaction']          ?? [];
-
-        $expiresAt = $charge['expires_at']
-            ?? $transaction['expires_at']
-            ?? $this->expires_at?->toISOString();
-
-        return [
-            'qr_code'     => $transaction['qr_code']     ?? null,
-            'qr_code_url' => $transaction['qr_code_url'] ?? null,
-            'expires_at'  => $expiresAt,
-            'status'      => $transaction['status']      ?? null,
-        ];
-    }
-
 }

+ 2 - 0
app/Http/Resources/PaymentSplitResource.php

@@ -27,6 +27,8 @@ class PaymentSplitResource extends JsonResource
             'metadata'                          => $this->metadata,
             'payment_status'                    => $this->payment?->status?->value,
             'payment_paid_at'                   => $this->payment?->paid_at?->toISOString(),
+            'service_package_id'                => $this->payment?->service_package_id,
+            'schedule_ids'                      => $this->payment?->servicePackage?->items?->pluck('schedule_id')->values(),
             'schedule_date'                     => $this->payment?->schedule?->date,
             'schedule_status'                   => $this->payment?->schedule?->status,
             'schedule_period_type'              => $this->payment?->schedule?->period_type,

+ 3 - 5
app/Http/Resources/ProviderResource.php

@@ -21,6 +21,8 @@ class ProviderResource extends JsonResource
             'average_rating'                 => $this->average_rating,
             'total_services'                 => $this->total_services,
             'birth_date'                     => $this->birth_date ? Carbon::parse($this->birth_date)->format('d/m/Y') : null,
+            'gender'                         => $this->gender?->value,
+            'gender_label'                   => $this->gender?->label(),
             'selfie_verified'                => $this->selfie_verified,
             'document_verified'              => $this->document_verified,
             'approval_status'                => $this->approval_status?->value ?? $this->approval_status,
@@ -28,7 +30,7 @@ class ProviderResource extends JsonResource
             'daily_price_6h'                 => $this->daily_price_6h,
             'daily_price_4h'                 => $this->daily_price_4h,
             'daily_price_2h'                 => $this->daily_price_2h,
-            'profile_media_id'               => $this->profile_media_id,
+            'profile_media_id'               => $this->profileMedia?->id,
             'recipient_id'                   => $this->recipient_id,
             'recipient_default_bank_account' => $this->recipient_default_bank_account,
             'profile_media'                  => $this->profileMedia ? new MediaResource($this->profileMedia) : null,
@@ -39,10 +41,6 @@ class ProviderResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<Provider>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<ProviderResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);

+ 1 - 1
app/Http/Resources/ReviewResource.php

@@ -16,7 +16,7 @@ class ReviewResource extends JsonResource
 
             'schedule_label' => $this->schedule
                 ? ($this->schedule->id.' - '.
-                   ($this->schedule->client?->user?->name ?? '?').' - '.
+                   ($this->schedule->client?->user?->name   ?? '?').' - '.
                    ($this->schedule->provider?->user?->name ?? '?').' - '.
                    ($this->schedule->date?->format('d/m/Y') ?? '?'))
                 : null,

+ 23 - 21
app/Http/Resources/ScheduleResource.php

@@ -2,7 +2,7 @@
 
 namespace App\Http\Resources;
 
-use App\Models\CartItem;
+use App\Models\ServicePackageItem;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 
@@ -10,12 +10,12 @@ class ScheduleResource extends JsonResource
 {
     public function toArray(Request $request): array
     {
-        $cartId = $this->cart_id ?? CartItem::query()
+        $servicePackageId = $this->service_package_id ?? ServicePackageItem::query()
             ->where('schedule_id', $this->id)
-            ->value('cart_id');
+            ->value('service_package_id');
 
-        $cartItemsCount = $this->cart_items_count ?? (
-            $cartId ? CartItem::query()->where('cart_id', $cartId)->count() : 0
+        $servicePackageItemsCount = $this->service_package_items_count ?? (
+            $servicePackageId ? ServicePackageItem::query()->where('service_package_id', $servicePackageId)->count() : 0
         );
 
         return [
@@ -24,6 +24,8 @@ class ScheduleResource extends JsonResource
             'client_name'   => $this->client?->user?->name,
             'provider_id'   => $this->provider_id,
             'provider_name' => $this->provider?->user?->name,
+            'gender'        => $this->provider?->gender?->value,
+            'gender_label'  => $this->provider?->gender?->label(),
             'address_id'    => $this->address_id,
 
             'address_full' => $this->address ? implode(', ', array_filter([
@@ -33,22 +35,22 @@ class ScheduleResource extends JsonResource
                 $this->address->city ? "{$this->address->city->name}/{$this->address->state?->code}" : null,
             ])) : null,
 
-            'address'          => new AddressResource($this->whenLoaded('address')),
-            'date'             => $this->date?->format('Y-m-d'),
-            'period_type'      => $this->period_type,
-            'schedule_type'    => $this->schedule_type,
-            'start_time'       => $this->start_time,
-            'end_time'         => $this->end_time,
-            'offers_meal'      => $this->offers_meal,
-            'status'           => $this->status,
-            'total_amount'     => $this->total_amount,
-            'code'             => $this->code,
-            'code_verified'    => $this->code_verified,
-            'cart_id'          => $cartId,
-            'cart_items_count' => $cartItemsCount,
-            'created_at'       => $this->created_at?->toISOString(),
-            'updated_at'       => $this->updated_at?->toISOString(),
-            'deleted_at'       => $this->deleted_at?->toISOString(),
+            'address'                     => new AddressResource($this->whenLoaded('address')),
+            'date'                        => $this->date?->format('Y-m-d'),
+            'period_type'                 => $this->period_type,
+            'schedule_type'               => $this->schedule_type,
+            'start_time'                  => $this->start_time,
+            'end_time'                    => $this->end_time,
+            'offers_meal'                 => $this->offers_meal,
+            'status'                      => $this->status,
+            'total_amount'                => $this->total_amount,
+            'code'                        => $this->code,
+            'code_verified'               => $this->code_verified,
+            'service_package_id'          => $servicePackageId,
+            'service_package_items_count' => $servicePackageItemsCount,
+            'created_at'                  => $this->created_at?->toISOString(),
+            'updated_at'                  => $this->updated_at?->toISOString(),
+            'deleted_at'                  => $this->deleted_at?->toISOString(),
         ];
     }
 }

+ 37 - 0
app/Http/Resources/ServicePackageItemResource.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace App\Http\Resources;
+
+use App\Models\ServicePackageItem;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
+
+class ServicePackageItemResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id'                 => $this->id,
+            'service_package_id' => $this->service_package_id,
+            'schedule_id'        => $this->schedule_id,
+            'schedule'           => new ScheduleResource($this->whenLoaded('schedule')),
+            'created_at'         => $this->created_at?->toISOString(),
+            'updated_at'         => $this->updated_at?->toISOString(),
+        ];
+    }
+
+    /**
+     * @param \Illuminate\Database\Eloquent\Collection<ServicePackageItem> $resource
+     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<ServicePackageItemResource>
+     */
+    public static function collection($resource): AnonymousResourceCollection
+    {
+        return parent::collection($resource);
+    }
+}

+ 6 - 5
app/Http/Resources/CartResource.php → app/Http/Resources/ServicePackageResource.php

@@ -2,12 +2,12 @@
 
 namespace App\Http\Resources;
 
-use App\Models\Cart;
+use App\Models\ServicePackage;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
 
-class CartResource extends JsonResource
+class ServicePackageResource extends JsonResource
 {
     /**
      * Transform the resource into an array.
@@ -19,9 +19,10 @@ class CartResource extends JsonResource
         return [
             'id'           => $this->id,
             'client_id'    => $this->client_id,
+            'provider_id'  => $this->provider_id,
             'status'       => $this->status?->value,
             'schedule_ids' => $this->whenLoaded('items', fn () => $this->items->pluck('schedule_id')->values()),
-            'items'        => $this->whenLoaded('items', fn () => CartItemResource::collection($this->items)),
+            'items'        => $this->whenLoaded('items', fn () => ServicePackageItemResource::collection($this->items)),
 
             'schedules' => $this->whenLoaded('items', fn () => ScheduleResource::collection(
                 $this->items->pluck('schedule')->filter()->values(),
@@ -33,8 +34,8 @@ class CartResource extends JsonResource
     }
 
     /**
-     * @param \Illuminate\Database\Eloquent\Collection<Cart> $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<CartResource>
+     * @param \Illuminate\Database\Eloquent\Collection<ServicePackage> $resource
+     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<ServicePackageResource>
      */
     public static function collection($resource): AnonymousResourceCollection
     {

+ 0 - 4
app/Http/Resources/ServiceTypeResource.php

@@ -25,10 +25,6 @@ class ServiceTypeResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<ServiceType>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<ServiceTypeResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);

+ 0 - 4
app/Http/Resources/SpecialityResource.php

@@ -25,10 +25,6 @@ class SpecialityResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<Speciality>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<SpecialityResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);

+ 0 - 4
app/Http/Resources/StateResource.php

@@ -24,10 +24,6 @@ class StateResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<State>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<StateResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);

+ 0 - 4
app/Http/Resources/SupportRequestResource.php

@@ -35,10 +35,6 @@ class SupportRequestResource extends JsonResource
         ];
     }
 
-    /**
-     * @param  \Illuminate\Database\Eloquent\Collection<SupportRequest>  $resource
-     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<SupportRequestResource>
-     */
     public static function collection($resource): AnonymousResourceCollection
     {
         return parent::collection($resource);

+ 6 - 1
app/Http/Resources/UserResource.php

@@ -22,6 +22,7 @@ class UserResource extends JsonResource
       'name'        => $this->name,
       'email'       => $this->email,
       'phone'       => $this->phone,
+      'gender'      => $this->provider?->gender?->value,
       'language'    => $this->language,
       'type'        => $this->type,
       'provider_id' => $this->provider?->id,
@@ -45,7 +46,11 @@ class UserResource extends JsonResource
 
   private function resolveProfilePhoto(): ?string
   {
-    $path = $this->provider?->profileMedia?->path ?? $this->client?->profileMedia?->path;
+    if ($this->provider) {
+      $path = $this->provider->profileMedia?->path;
+    } else {
+      $path = $this->client?->profileMedia?->path;
+    }
 
     return $path ? Storage::temporaryUrl($path, now()->addMinutes(60)) : null;
   }

+ 28 - 4
app/Models/Client.php

@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Database\Eloquent\Relations\HasOne;
 use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\Facades\Auth;
 
 /**
  * @property int $id
@@ -21,6 +22,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property string|null $gateway_customer_id
  * @property string|null $gateway_customer_code
  * @property int|null $profile_media_id
+ * @property bool $selfie_verified
  * @property string|null $idempotency_key
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProviderClientBlock> $blockedByProviders
  * @property-read int|null $blocked_by_providers_count
@@ -61,12 +63,14 @@ class Client extends Model
         'idempotency_key',
         'user_id',
         'profile_media_id',
+        'selfie_verified',
     ];
 
     protected $casts = [
-        'created_at' => 'datetime',
-        'updated_at' => 'datetime',
-        'deleted_at' => 'datetime',
+        'created_at'      => 'datetime',
+        'updated_at'      => 'datetime',
+        'deleted_at'      => 'datetime',
+        'selfie_verified' => 'boolean',
     ];
 
     public function user(): BelongsTo
@@ -88,7 +92,27 @@ class Client extends Model
 
     public function profileMedia(): BelongsTo
     {
-        return $this->belongsTo(Media::class, 'profile_media_id');
+        $authenticatedUser = Auth::user();
+
+        $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')
+            ->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()

+ 32 - 0
app/Models/Payment.php

@@ -95,6 +95,7 @@ class Payment extends Model
 
     protected $fillable = [
         'schedule_id',
+        'service_package_id',
         'client_id',
         'provider_id',
         'client_payment_method_id',
@@ -158,6 +159,11 @@ class Payment extends Model
         return $this->belongsTo(Schedule::class);
     }
 
+    public function servicePackage()
+    {
+        return $this->belongsTo(ServicePackage::class);
+    }
+
     //
 
     public function clientPaymentMethod()
@@ -189,4 +195,30 @@ class Payment extends Model
 
         return $code;
     }
+
+    public function pixData(): ?array
+    {
+        if ($this->payment_method !== 'pix') {
+            return null;
+        }
+
+        $charge = data_get($this->gateway_payload, 'charges.0', []);
+
+        $transaction = data_get($charge, 'last_transaction', []);
+
+        $expiresAt = data_get($charge, 'expires_at')
+            ?? data_get($transaction, 'expires_at')
+            ?? $this->expires_at?->toISOString();
+
+        $qrCode    = data_get($transaction, 'qr_code');
+        $qrCodeUrl = data_get($transaction, 'qr_code_url');
+        $status    = data_get($transaction, 'status');
+
+        return [
+            'qr_code'     => $qrCode,
+            'qr_code_url' => $qrCodeUrl,
+            'expires_at'  => $expiresAt,
+            'status'      => $status,
+        ];
+    }
 }

+ 25 - 1
app/Models/Provider.php

@@ -3,12 +3,14 @@
 namespace App\Models;
 
 use App\Enums\ApprovalStatusEnum;
+use App\Enums\GenderEnum;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Database\Eloquent\Relations\HasOne;
 use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\Facades\Auth;
 
 /**
  * @property int $id
@@ -18,6 +20,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property numeric|null $average_rating
  * @property int $total_services
  * @property \Illuminate\Support\Carbon|null $birth_date
+ * @property GenderEnum|null $gender
  * @property bool $selfie_verified
  * @property bool $document_verified
  * @property numeric|null $daily_price_8h
@@ -114,6 +117,7 @@ class Provider extends Model
     {
         return [
             'birth_date'                                => 'date',
+            'gender'                                    => GenderEnum::class,
             'selfie_verified'                           => 'boolean',
             'document_verified'                         => 'boolean',
             'approval_status'                           => ApprovalStatusEnum::class,
@@ -149,7 +153,27 @@ class Provider extends Model
 
     public function profileMedia(): BelongsTo
     {
-        return $this->belongsTo(Media::class, "profile_media_id");
+        $authenticatedUser = Auth::user();
+
+        $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')
+            ->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);
+            }
+        });
     }
 
     //

+ 31 - 20
app/Models/Cart.php → app/Models/ServicePackage.php

@@ -2,7 +2,7 @@
 
 namespace App\Models;
 
-use App\Enums\CartStatusEnum;
+use App\Enums\ServicePackageStatusEnum;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\SoftDeletes;
@@ -10,42 +10,43 @@ use Illuminate\Database\Eloquent\SoftDeletes;
 /**
  * @property int $id
  * @property int $client_id
- * @property CartStatusEnum $status
+ * @property ServicePackageStatusEnum $status
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at
  * @property-read \App\Models\Client $client
- * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\CartItem> $items
+ * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ServicePackageItem> $items
  * @property-read int|null $items_count
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Schedule> $schedules
  * @property-read int|null $schedules_count
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart newModelQuery()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart newQuery()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart onlyTrashed()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart query()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereClientId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereCreatedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereDeletedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereStatus($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereUpdatedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart withTrashed(bool $withTrashed = true)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart withoutTrashed()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage onlyTrashed()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage query()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage whereClientId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage whereCreatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage whereDeletedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage whereStatus($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage whereUpdatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage withTrashed(bool $withTrashed = true)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackage withoutTrashed()
  * @mixin \Eloquent
  */
-class Cart extends Model
+class ServicePackage extends Model
 {
     use HasFactory, SoftDeletes;
 
-    protected $table = 'carts';
+    protected $table = 'service_packages';
 
     protected $fillable = [
         'client_id',
+        'provider_id',
         'status',
     ];
 
     protected $casts = [
-        'status'     => CartStatusEnum::class,
+        'status'     => ServicePackageStatusEnum::class,
         'created_at' => 'datetime',
         'updated_at' => 'datetime',
         'deleted_at' => 'datetime',
@@ -56,14 +57,24 @@ class Cart extends Model
         return $this->belongsTo(Client::class);
     }
 
+    public function provider()
+    {
+        return $this->belongsTo(Provider::class);
+    }
+
+    public function payments()
+    {
+        return $this->hasMany(Payment::class);
+    }
+
     public function items()
     {
-        return $this->hasMany(CartItem::class);
+        return $this->hasMany(ServicePackageItem::class);
     }
 
     public function schedules()
     {
-        return $this->belongsToMany(Schedule::class, 'cart_items')
+        return $this->belongsToMany(Schedule::class, 'service_package_items')
             ->withTimestamps();
     }
 }

+ 15 - 15
app/Models/CartItem.php → app/Models/ServicePackageItem.php

@@ -7,30 +7,30 @@ use Illuminate\Database\Eloquent\Model;
 
 /**
  * @property int $id
- * @property int $cart_id
+ * @property int $service_package_id
  * @property int $schedule_id
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
- * @property-read \App\Models\Cart $cart
+ * @property-read \App\Models\ServicePackage $servicePackage
  * @property-read \App\Models\Schedule $schedule
- * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem newModelQuery()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem newQuery()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem query()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem whereCartId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem whereCreatedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem whereId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem whereScheduleId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem whereUpdatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackageItem newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackageItem newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackageItem query()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackageItem whereServicePackageId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackageItem whereCreatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackageItem whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackageItem whereScheduleId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ServicePackageItem whereUpdatedAt($value)
  * @mixin \Eloquent
  */
-class CartItem extends Model
+class ServicePackageItem extends Model
 {
     use HasFactory;
 
-    protected $table = 'cart_items';
+    protected $table = 'service_package_items';
 
     protected $fillable = [
-        'cart_id',
+        'service_package_id',
         'schedule_id',
     ];
 
@@ -39,9 +39,9 @@ class CartItem extends Model
         'updated_at' => 'datetime',
     ];
 
-    public function cart()
+    public function servicePackage()
     {
-        return $this->belongsTo(Cart::class);
+        return $this->belongsTo(ServicePackage::class);
     }
 
     public function schedule()

+ 22 - 15
app/Models/User.php

@@ -86,11 +86,23 @@ class User extends Authenticatable
         ];
     }
 
+    //
+
     public function isAdmin(): bool
     {
         return $this->type === UserTypeEnum::ADMIN;
     }
 
+    public function canModerateProviders(): bool
+    {
+        return $this->isAdmin();
+    }
+
+    public function canModerateClients(): bool
+    {
+        return $this->isAdmin();
+    }
+
     //
 
     public function client()
@@ -117,9 +129,8 @@ class User extends Authenticatable
 
     //
 
-    /**
-     * Create a new access token for the user.
-     */
+    // Create a new access token for the user.
+
     public function createAccessToken(string $deviceId): string
     {
         return $this->createToken(
@@ -129,9 +140,8 @@ class User extends Authenticatable
         )->plainTextToken;
     }
 
-    /**
-     * Create a new refresh token for the user.
-     */
+    // Create a new refresh token for the user.
+
     public function createRefreshToken(string $deviceId): string
     {
         return $this->createToken(
@@ -141,9 +151,8 @@ class User extends Authenticatable
         )->plainTextToken;
     }
 
-    /**
-     * Delete all tokens (access and refresh) for a specific device.
-     */
+    // Delete all tokens (access and refresh) for a specific device.
+
     public function deleteTokensByDevice(string $deviceId): void
     {
         $this->tokens()
@@ -161,9 +170,8 @@ class User extends Authenticatable
         );
     }
 
-    /**
-     * Create a new access token for the user in the app.
-     */
+    // Create a new access token for the user in the app.
+
     public function createAccessTokenApp(string $deviceId): string
     {
         return $this->createToken(
@@ -173,9 +181,8 @@ class User extends Authenticatable
         )->plainTextToken;
     }
 
-    /**
-     * Create a new refresh token for the user in the app.
-     */
+    // Create a new refresh token for the user in the app.
+
     public function createRefreshTokenApp(string $deviceId): string
     {
         return $this->createToken(

+ 22 - 0
app/Observers/ClientObserver.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Observers;
+
+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')) {
+            $client->selfie_verified = false;
+        }
+    }
+}

+ 22 - 0
app/Observers/ProviderObserver.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Observers;
+
+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')) {
+            $provider->selfie_verified = false;
+        }
+    }
+}

+ 7 - 0
app/Providers/AppServiceProvider.php

@@ -2,7 +2,11 @@
 
 namespace App\Providers;
 
+use App\Models\Client;
+use App\Models\Provider;
 use App\Models\Schedule;
+use App\Observers\ClientObserver;
+use App\Observers\ProviderObserver;
 use App\Observers\ScheduleObserver;
 use Illuminate\Support\ServiceProvider;
 
@@ -30,6 +34,9 @@ class AppServiceProvider extends ServiceProvider
      */
     public function boot(): void
     {
+        Client::observe(ClientObserver::class);
+        Provider::observe(ProviderObserver::class);
+
         if ($this->app->environment(['local', 'development', 'dev'])) {
             Schedule::observe(ScheduleObserver::class);
         }

+ 1 - 1
app/Rules/ScheduleBusinessRules.php

@@ -26,7 +26,7 @@ class ScheduleBusinessRules
             ->exists();
 
         if (! $provider) {
-            throw new \Exception('Prestador indisponível para agendamento.');
+            throw new \Exception(__('messages.provider_unavailable_for_schedule'));
         }
 
         return true;

+ 5 - 5
app/Services/AddressService.php

@@ -21,7 +21,7 @@ class AddressService
     {
         $address = Address::findOrFail($id);
 
-        if (isset($data['is_primary']) && $data['is_primary']) {
+        if (data_get($data, 'is_primary') !== null && data_get($data, 'is_primary')) {
             Address::where('source', $address->source)
                 ->where('source_id', $address->source_id)
                 ->where('id', '!=', $id)
@@ -45,8 +45,8 @@ class AddressService
 
     public function findBySource(array $data): ?Collection
     {
-        $allAddresses = Address::where('source', $data['source'])
-            ->where('source_id', $data['source_id'])
+        $allAddresses = Address::where('source', data_get($data, 'source'))
+            ->where('source_id', data_get($data, 'source_id'))
             ->with(['city', 'state'])
             ->whereNull('deleted_at')
             ->orderBy('is_primary', 'desc')
@@ -57,8 +57,8 @@ class AddressService
 
     public function findPrimaryBySource(array $data): ?Address
     {
-        return Address::where('source', $data['source'])
-            ->where('source_id', $data['source_id'])
+        return Address::where('source', data_get($data, 'source'))
+            ->where('source_id', data_get($data, 'source_id'))
             ->whereNull('deleted_at')
             ->orderBy('is_primary', 'desc')
             ->first();

+ 34 - 34
app/Services/AuthService.php

@@ -74,11 +74,11 @@ class AuthService
 
         return [
             'payload' => [
-                'access_token' => $tokens['access_token'],
+                'access_token' => data_get($tokens, 'access_token'),
                 'user'         => $user,
             ],
 
-            'refreshToken' => $tokens['refresh_token'],
+            'refreshToken' => data_get($tokens, 'refresh_token'),
         ];
     }
 
@@ -138,10 +138,10 @@ class AuthService
             $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
 
             $user = User::where(function ($query) use ($data) {
-                $query->when(! empty($data['email']), function ($q) use ($data) {
-                    $q->where('email', $data['email']);
-                })->when(! empty($data['phone']), function ($q) use ($data) {
-                    $q->where('phone', $data['phone']);
+                $query->when(! empty(data_get($data, 'email')), function ($q) use ($data) {
+                    $q->where('email', data_get($data, 'email'));
+                })->when(! empty(data_get($data, 'phone')), function ($q) use ($data) {
+                    $q->where('phone', data_get($data, 'phone'));
                 });
             })->where('type', UserTypeEnum::CLIENT->value)->first();
 
@@ -167,23 +167,23 @@ class AuthService
                 $user->fill($data);
 
                 $user->code = $code;
-                $user->name = $data['name'] ?? 'Usuário';
-                $user->type = $data['type'] ?? UserTypeEnum::CLIENT->value;
+                $user->name = data_get($data, 'name', 'Usuário');
+                $user->type = data_get($data, 'type', UserTypeEnum::CLIENT->value);
 
                 $user->save();
 
                 Client::create(['user_id' => $user->id]);
             }
 
-            if (! empty($data['email'])) {
+            if (! empty(data_get($data, 'email'))) {
                 $this->emailService->sendVerificationCode(
-                    email: $data['email'],
-                    code: $code,
-                    recipientName: $data['name'] ?? '',
+                    email:         data_get($data, 'email'),
+                    code:          $code,
+                    recipientName: data_get($data, 'name', ''),
                 );
-            } elseif (! empty($data['phone'])) {
+            } elseif (! empty(data_get($data, 'phone'))) {
                 Log::info('SMS: envio de código por telefone ainda não implementado.', [
-                    'phone' => $data['phone'],
+                    'phone' => data_get($data, 'phone'),
                 ]);
             }
 
@@ -210,10 +210,10 @@ class AuthService
             $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
 
             $user = User::where(function ($query) use ($data) {
-                $query->when(! empty($data['email']), function ($q) use ($data) {
-                    $q->where('email', $data['email']);
-                })->when(! empty($data['phone']), function ($q) use ($data) {
-                    $q->where('phone', $data['phone']);
+                $query->when(! empty(data_get($data, 'email')), function ($q) use ($data) {
+                    $q->where('email', data_get($data, 'email'));
+                })->when(! empty(data_get($data, 'phone')), function ($q) use ($data) {
+                    $q->where('phone', data_get($data, 'phone'));
                 });
             })->where('type', UserTypeEnum::PROVIDER->value)->first();
 
@@ -234,21 +234,21 @@ class AuthService
                 $user->fill($data);
 
                 $user->code = $code;
-                $user->name = $data['name'] ?? 'Usuário';
-                $user->type = $data['type'] ?? UserTypeEnum::PROVIDER->value;
+                $user->name = data_get($data, 'name', 'Usuário');
+                $user->type = data_get($data, 'type', UserTypeEnum::PROVIDER->value);
 
                 $user->save();
             }
 
-            if (! empty($data['email'])) {
+            if (! empty(data_get($data, 'email'))) {
                 $this->emailService->sendVerificationCode(
-                    email: $data['email'],
+                    email: data_get($data, 'email'),
                     code: $code,
-                    recipientName: $data['name'] ?? '',
+                    recipientName: data_get($data, 'name', ''),
                 );
-            } elseif (! empty($data['phone'])) {
+            } elseif (! empty(data_get($data, 'phone'))) {
                 Log::info('SMS: envio de código por telefone ainda não implementado.', [
-                    'phone' => $data['phone'],
+                    'phone' => data_get($data, 'phone'),
                 ]);
             }
 
@@ -271,9 +271,9 @@ class AuthService
 
     public function validateCodeClient(array $data, bool $isLogin): bool|array
     {
-        $email = $data['email'] ?? null;
-        $phone = $data['phone'] ?? null;
-        $code  = $data['code']  ?? '';
+        $email = data_get($data, 'email');
+        $phone = data_get($data, 'phone');
+        $code  = data_get($data, 'code', '');
 
         $user = User::where(function ($query) use ($email, $phone) {
             $query->when($email, fn ($q) => $q->where('email', $email))
@@ -305,9 +305,9 @@ class AuthService
 
     public function validateCodeProvider(array $data, bool $isLogin): bool|array
     {
-        $email = $data['email'] ?? null;
-        $phone = $data['phone'] ?? null;
-        $code  = $data['code']  ?? '';
+        $email = data_get($data, 'email');
+        $phone = data_get($data, 'phone');
+        $code  = data_get($data, 'code', '');
 
         $user = User::where(function ($query) use ($email, $phone) {
             $query->when($email, fn ($q) => $q->where('email', $email))
@@ -337,9 +337,9 @@ class AuthService
 
     public function validateCode(array $data, bool $isLogin): bool|array
     {
-        $email = $data['email'] ?? null;
-        $phone = $data['phone'] ?? null;
-        $code  = $data['code']  ?? '';
+        $email = data_get($data, 'email');
+        $phone = data_get($data, 'phone');
+        $code  = data_get($data, 'code', '');
 
         $user = User::where(function ($query) use ($email, $phone) {
             $query->when($email, function ($q) use ($email) {

+ 0 - 52
app/Services/CartItemService.php

@@ -1,52 +0,0 @@
-<?php
-
-namespace App\Services;
-
-use App\Models\CartItem;
-use Illuminate\Database\Eloquent\Collection;
-
-class CartItemService
-{
-    public function getAll(): Collection
-    {
-        return CartItem::query()
-            ->orderBy('created_at', 'desc')
-            ->get();
-    }
-
-    public function findById(int $id): ?CartItem
-    {
-        return CartItem::find($id);
-    }
-
-    public function create(array $data): CartItem
-    {
-        return CartItem::create($data);
-    }
-
-    public function update(int $id, array $data): ?CartItem
-    {
-        $model = $this->findById($id);
-
-        if (!$model) {
-            return null;
-        }
-
-        $model->update($data);
-
-        return $model->fresh();
-    }
-
-    public function delete(int $id): bool
-    {
-        $model = $this->findById($id);
-
-        if (!$model) {
-            return false;
-        }
-
-        return $model->delete();
-    }
-
-    // Add custom business logic methods here
-}

+ 0 - 149
app/Services/CartService.php

@@ -1,149 +0,0 @@
-<?php
-
-namespace App\Services;
-
-use App\Models\Cart;
-use App\Models\Client;
-use Illuminate\Support\Arr;
-use Illuminate\Database\Eloquent\Collection;
-use Illuminate\Support\Facades\DB;
-
-class CartService
-{
-    public function getAll(): Collection
-    {
-        return Cart::query()
-            ->with(['items.schedule'])
-            ->orderBy('created_at', 'desc')
-            ->get();
-    }
-
-    public function findById(int $id): ?Cart
-    {
-        return Cart::query()
-            ->with(['items.schedule'])
-            ->find($id);
-    }
-
-    public function create(array $data): Cart
-    {
-        return Cart::create($data);
-    }
-
-    public function update(int $id, array $data): ?Cart
-    {
-        $model = $this->findById($id);
-
-        if (! $model) {
-            return null;
-        }
-
-        $model->update($data);
-
-        return $model->fresh();
-    }
-
-    public function delete(int $id): bool
-    {
-        $model = $this->findById($id);
-
-        if (! $model) {
-            return false;
-        }
-
-        return $model->delete();
-    }
-
-    //
-
-    public function getByUserId(int $userId): Collection
-    {
-        return Cart::query()
-            ->with(['items.schedule.client.user', 'items.schedule.provider.user', 'items.schedule.address'])
-            ->whereHas('client', fn ($query) => $query->where('user_id', $userId))
-            ->orderBy('created_at', 'desc')
-            ->get();
-    }
-
-    public function createForUser(array $data, int $userId): Cart
-    {
-        $client = Client::query()
-            ->where('user_id', $userId)
-            ->firstOrFail();
-
-        $data['client_id'] = $client->id;
-
-        return $this->hasSchedulePayload($data)
-            ? $this->createWithSchedules($data)
-            : $this->create($data);
-    }
-
-    public function createWithSchedules(array $data): Cart
-    {
-        return DB::transaction(function () use ($data) {
-            $items = $this->scheduleItems($data);
-
-            $schedules = app(ScheduleService::class)->createSingleOrMultiple(
-                baseData:  $this->baseScheduleData($data),
-                schedules: $items,
-            );
-
-            $cart = Cart::create([
-                'client_id' => $data['client_id'],
-            ]);
-
-            foreach ($schedules as $schedule) {
-                $cart->items()->create([
-                    'schedule_id' => $schedule->id,
-                ]);
-            }
-
-            return $cart->fresh(['items.schedule.client.user', 'items.schedule.provider.user', 'items.schedule.address']);
-        });
-    }
-
-    //
-
-    private function baseScheduleData(array $data): array
-    {
-        return Arr::except($data, [
-            'schedules',
-            'dates',
-            'date',
-            'period_type',
-            'start_time',
-            'end_time',
-            'total_amount',
-            'offers_meal',
-        ]);
-    }
-
-    private function hasSchedulePayload(array $data): bool
-    {
-        return ! empty($data['schedules'])
-            || ! empty($data['dates'])
-            || ! empty($data['date']);
-    }
-
-    private function scheduleItems(array $data): array
-    {
-        if (! empty($data['schedules'])) {
-            return $data['schedules'];
-        }
-
-        if (! empty($data['dates'])) {
-            return array_map(
-                fn (string $date) => array_merge(
-                    Arr::only($data, ['period_type', 'start_time', 'end_time', 'total_amount', 'offers_meal']),
-                    ['date' => $date],
-                ),
-                $data['dates'],
-            );
-        }
-
-        return [
-            Arr::only($data, ['date', 'period_type', 'start_time', 'end_time', 'total_amount', 'offers_meal']),
-        ];
-    }
-
-}

+ 33 - 18
app/Services/ClientCalendarService.php

@@ -2,7 +2,9 @@
 
 namespace App\Services;
 
+use App\Enums\GenderEnum;
 use App\Models\Client;
+use App\Models\Provider;
 use App\Models\Schedule;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\DB;
@@ -31,20 +33,10 @@ class ClientCalendarService
             'schedules.status',
             'custom_schedules.offers_meal',
             'provider_user.name as provider_name',
+            'providers.gender',
             'providers.average_rating',
             'providers.total_services',
 
-            DB::raw("
-                (
-                    SELECT media.path
-                    FROM media
-                    WHERE media.source_id = providers.id
-                    AND media.source = 'provider'
-                    AND media.deleted_at IS NULL
-                    LIMIT 1
-                ) AS provider_photo
-            "),
-
             DB::raw("
                 EXISTS (
                     SELECT 1
@@ -69,10 +61,6 @@ class ClientCalendarService
             "),
         ];
 
-        $signPhoto = fn ($item) => tap($item, fn ($i) => $i->provider_photo = $i->provider_photo
-            ? Storage::temporaryUrl($i->provider_photo, now()->addMinutes(60))
-            : null);
-
         $upcomingSchedules = Schedule::with('address:district,address,number,source_id,source,id')
             ->where('schedules.client_id', $client->id)
             ->whereIn('schedules.status', ['pending', 'accepted', 'paid', 'started'])
@@ -83,8 +71,7 @@ class ClientCalendarService
             ->select($selectFields)
             ->orderBy('schedules.date', 'asc')
             ->orderBy('schedules.start_time', 'asc')
-            ->get()
-            ->map($signPhoto);
+            ->get();
 
         $completedSchedules = Schedule::with('address:district,address,number,source_id,source,id')
             ->where('schedules.client_id', $client->id)
@@ -95,8 +82,36 @@ class ClientCalendarService
             ->select($selectFields)
             ->orderBy('schedules.date', 'desc')
             ->orderBy('schedules.start_time', 'desc')
+            ->get();
+
+        $providerPhotos = Provider::query()
+            ->select('id', 'profile_media_id')
+            ->with('profileMedia')
+            ->whereIn(
+                'id',
+                $upcomingSchedules->pluck('provider_id')
+                    ->merge($completedSchedules->pluck('provider_id'))
+                    ->filter()
+                    ->unique(),
+            )
             ->get()
-            ->map($signPhoto);
+            ->mapWithKeys(fn (Provider $provider) => [
+                $provider->id => $provider->profileMedia?->path,
+            ]);
+
+        $signPhoto = fn ($item) => tap($item, function ($i) use ($providerPhotos) {
+            $i->gender_label = GenderEnum::labelFor($i->gender);
+
+            $path = $providerPhotos->get($i->provider_id);
+
+            $i->provider_photo = $path
+                ? Storage::temporaryUrl($path, now()->addMinutes(60))
+                : null;
+        });
+
+        $upcomingSchedules->transform($signPhoto);
+
+        $completedSchedules->transform($signPhoto);
 
         return [
             'upcomingSchedules'  => $upcomingSchedules,

+ 2 - 2
app/Services/ClientFavoriteProviderService.php

@@ -11,12 +11,12 @@ class ClientFavoriteProviderService
     public function getByClientId(int $clientId): Collection
     {
         return ClientFavoriteProvider::where('client_favorite_providers.client_id', $clientId)
+            ->with('provider.profileMedia')
             ->leftJoin('providers', 'providers.id', '=', 'client_favorite_providers.provider_id')
             ->whereNotNull('providers.recipient_default_bank_account')
             ->whereRaw("providers.recipient_default_bank_account::text <> '{}'")
             ->whereRaw("providers.recipient_default_bank_account::text <> '[]'")
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
-            ->leftJoin('media as provider_profile_media', 'provider_profile_media.id', '=', 'providers.profile_media_id')
             ->leftJoin('addresses as provider_address', function ($join) {
                 $join->on('provider_address.source_id', '=', 'providers.id')
                     ->where('provider_address.source', 'provider')
@@ -31,7 +31,7 @@ class ClientFavoriteProviderService
                 'client_favorite_providers.created_at',
                 'client_favorite_providers.updated_at',
                 'provider_user.name as provider_name',
-                'provider_profile_media.path as profile_media_path',
+                'providers.gender',
                 'providers.average_rating',
                 'providers.daily_price_8h',
                 'providers.daily_price_6h',

+ 4 - 4
app/Services/ClientPaymentMethodService.php

@@ -36,8 +36,8 @@ class ClientPaymentMethodService
     public function create(array $data): ClientPaymentMethod
     {
         return DB::transaction(function () use ($data) {
-            if ($data['is_active'] ?? true) {
-                $this->deactivateOtherCards($data['client_id']);
+            if (data_get($data, 'is_active', true)) {
+                $this->deactivateOtherCards(data_get($data, 'client_id'));
             }
 
             $paymentMethod = ClientPaymentMethod::create($data);
@@ -45,7 +45,7 @@ class ClientPaymentMethodService
             $cardId = $this->pagarmeCardService->createCardForPaymentMethod($paymentMethod);
 
             if (empty($cardId)) {
-                throw new \RuntimeException('Nao foi possivel salvar o cartao no Pagar.me.');
+                throw new \RuntimeException(__('messages.pagarme_card_save_failed'));
             }
 
             return $paymentMethod->fresh();
@@ -54,7 +54,7 @@ class ClientPaymentMethodService
 
     public function update(ClientPaymentMethod $paymentMethod, array $data): ClientPaymentMethod
     {
-        if (isset($data['is_active']) && $data['is_active']) {
+        if (data_get($data, 'is_active') !== null && data_get($data, 'is_active')) {
             $this->deactivateOtherCards($paymentMethod->client_id, $paymentMethod->id);
         }
 

+ 3 - 2
app/Services/ClientProviderBlockService.php

@@ -26,6 +26,7 @@ class ClientProviderBlockService
                 'provider_user.name as provider_name',
                 'provider_user.email as provider_email',
                 'provider_user.phone as provider_phone',
+                'providers.gender',
                 'providers.average_rating as provider_rating',
                 'provider_address.district as provider_district',
             )
@@ -35,8 +36,8 @@ class ClientProviderBlockService
 
     public function create(array $data): ClientProviderBlock
     {
-        $existing = ClientProviderBlock::where('client_id', $data['client_id'])
-            ->where('provider_id', $data['provider_id'])
+        $existing = ClientProviderBlock::where('client_id', data_get($data, 'client_id'))
+            ->where('provider_id', data_get($data, 'provider_id'))
             ->first();
 
         if ($existing) {

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików