فهرست منبع

Merge remote-tracking branch 'origin' into fix/diaria-kay-correções-backend

Gustavo Zanatta 1 هفته پیش
والد
کامیت
e6bca3d9df
32فایلهای تغییر یافته به همراه1573 افزوده شده و 918 حذف شده
  1. 20 0
      app/Exceptions/FavoriteProviderException.php
  2. 17 0
      app/Http/Controllers/ClientFavoriteProviderController.php
  3. 2 1
      app/Http/Controllers/PaymentController.php
  4. 2 2
      app/Http/Controllers/ProviderWithdrawalController.php
  5. 46 0
      app/Http/Requests/ClientFavoriteProviderByCodeRequest.php
  6. 1 0
      app/Http/Resources/ClientFavoriteProviderResource.php
  7. 4 0
      app/Http/Resources/PaymentSplitResource.php
  8. 1 0
      app/Http/Resources/ProviderResource.php
  9. 1 0
      app/Http/Resources/UserResource.php
  10. 38 1
      app/Models/Provider.php
  11. 6 4
      app/Models/Schedule.php
  12. 64 0
      app/Notifications/Push/Cliente/Agendamento/PrestadorAceitouPush.php
  13. 59 0
      app/Notifications/Push/Prestador/Agendamento/CodigoNaoPreenchidoPush.php
  14. 1 5
      app/Services/ClientCalendarService.php
  15. 66 0
      app/Services/ClientFavoriteProviderService.php
  16. 30 0
      app/Services/CustomScheduleService.php
  17. 199 6
      app/Services/DashboardService.php
  18. 1 10
      app/Services/Pagarme/PagarmePaymentService.php
  19. 887 886
      app/Services/PaymentService.php
  20. 1 1
      app/Services/ProviderCalendarService.php
  21. 11 2
      app/Services/ProviderWithdrawalService.php
  22. 4 0
      app/Services/PushNotificationService.php
  23. 29 0
      app/Services/ScheduleService.php
  24. 22 0
      database/migrations/2026_08_28_141841_add_code_reminder_sent_at_to_schedules_table.php
  25. 42 0
      database/migrations/2026_08_31_120000_add_share_code_to_providers_table.php
  26. 4 0
      lang/en/messages.php
  27. 2 0
      lang/en/requests.php
  28. 4 0
      lang/es/messages.php
  29. 2 0
      lang/es/requests.php
  30. 4 0
      lang/pt/messages.php
  31. 2 0
      lang/pt/requests.php
  32. 1 0
      routes/authRoutes/client_favorite_provider.php

+ 20 - 0
app/Exceptions/FavoriteProviderException.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Exceptions;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use RuntimeException;
+
+class FavoriteProviderException extends RuntimeException
+{
+    public function __construct(string $messageKey, private readonly int $status = 422)
+    {
+        parent::__construct(__("messages.{$messageKey}"));
+    }
+
+    public function render(Request $request): JsonResponse
+    {
+        return response()->json(['payload' => null, 'message' => $this->getMessage()], $this->status);
+    }
+}

+ 17 - 0
app/Http/Controllers/ClientFavoriteProviderController.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers;
 
+use App\Http\Requests\ClientFavoriteProviderByCodeRequest;
 use App\Http\Requests\ClientFavoriteProviderRequest;
 use App\Http\Resources\ClientFavoriteProviderResource;
 use App\Services\ClientFavoriteProviderService;
@@ -64,6 +65,22 @@ class ClientFavoriteProviderController extends Controller
 
     //
 
+    public function storeByCode(ClientFavoriteProviderByCodeRequest $request): JsonResponse
+    {
+        $data = $request->validated();
+
+        $favorite = $this->service->createByCode(
+            clientId: (int) data_get($data, 'client_id'),
+            code:     (string) data_get($data, 'code'),
+        );
+
+        return $this->successResponse(
+            payload: $favorite,
+            message: __('messages.favorite_code_added'),
+            code:    201,
+        );
+    }
+
     public function getFavoritedProviders(int $clientId): JsonResponse
     {
         $providerIds = $this->service->getFavoritedProviderIds($clientId);

+ 2 - 1
app/Http/Controllers/PaymentController.php

@@ -13,6 +13,7 @@ use App\Models\ServicePackage;
 use App\Services\PaymentService;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Log;
 
 class PaymentController extends Controller
 {
@@ -64,7 +65,7 @@ class PaymentController extends Controller
     public function payServicePackage(PayServicePackageRequest $request, int $servicePackageId): JsonResponse
     {
         $validated = $request->validated();
-
+        Log::info($validated);
         try {
             $item = $this->service->payServicePackage(
                 servicePackageId:      $servicePackageId,

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

@@ -70,11 +70,11 @@ class ProviderWithdrawalController extends Controller
         return $this->successResponse(payload: $this->service->getWithdrawalFees());
     }
 
-    public function splits(): JsonResponse
+    public function splits(Request $request): JsonResponse
     {
         $provider = $this->resolveProvider();
 
-        $splits = $this->service->getPaymentSplits($provider);
+        $splits = $this->service->getPaymentSplits($provider, $request->query('payment_status'));
 
         return $this->successResponse(payload: PaymentSplitResource::collection($splits));
     }

+ 46 - 0
app/Http/Requests/ClientFavoriteProviderByCodeRequest.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Http\Requests;
+
+use App\Models\Provider;
+use Illuminate\Foundation\Http\FormRequest;
+
+class ClientFavoriteProviderByCodeRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    protected function prepareForValidation(): void
+    {
+        $this->merge([
+            'code' => Provider::normalizeShareCode($this->input('code')),
+        ]);
+    }
+
+    public function rules(): array
+    {
+        return [
+            'client_id' => ['required', 'integer', 'exists:clients,id'],
+
+            'code' => [
+                'required',
+                'string',
+                'size:'.Provider::SHARE_CODE_LENGTH,
+                'regex:/^['.Provider::SHARE_CODE_ALPHABET.']+$/',
+            ],
+        ];
+    }
+
+    public function messages(): array
+    {
+        return [
+            'client_id.required' => __('requests.client_favorite_provider.client_required'),
+            'client_id.exists'   => __('requests.client_favorite_provider.client_not_found'),
+            'code.required'      => __('requests.client_favorite_provider.code_required'),
+            'code.size'          => __('requests.client_favorite_provider.code_invalid'),
+            'code.regex'         => __('requests.client_favorite_provider.code_invalid'),
+        ];
+    }
+}

+ 1 - 0
app/Http/Resources/ClientFavoriteProviderResource.php

@@ -27,6 +27,7 @@ class ClientFavoriteProviderResource extends JsonResource
             'client_id'      => $this->client_id,
             'provider_id'    => $this->provider_id,
             'provider_name'  => $this->provider_name ?? ($provider?->relationLoaded('user') ? $provider->user?->name : null),
+            'share_code'     => $this->share_code     ?? $provider?->share_code,
             'gender'         => $gender,
             'gender_label'   => GenderEnum::labelFor($gender),
             'city_name'      => $this->city_name      ?? null,

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

@@ -5,6 +5,7 @@ namespace App\Http\Resources;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
 use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Support\Facades\Storage;
 
 class PaymentSplitResource extends JsonResource
 {
@@ -46,6 +47,9 @@ class PaymentSplitResource extends JsonResource
         'schedule_status' => $schedule?->status,
         'schedule_period_type' => $schedule?->period_type,
         'client_name' => $schedule?->client?->user?->name,
+        'client_photo' => $schedule?->client?->profileMedia?->path
+            ? Storage::temporaryUrl($schedule->client->profileMedia->path, now()->addMinutes(60))
+            : null,
 
         'created_at' => $this->created_at?->toISOString(),
         'updated_at' => $this->updated_at?->toISOString(),

+ 1 - 0
app/Http/Resources/ProviderResource.php

@@ -17,6 +17,7 @@ class ProviderResource extends JsonResource
             'document'                       => $this->document,
             'rg'                             => $this->rg,
             'user_id'                        => $this->user_id,
+            'share_code'                     => $this->share_code,
             'user'                           => $this->user,
             'average_rating'                 => $this->average_rating,
             'total_services'                 => $this->total_services,

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

@@ -26,6 +26,7 @@ class UserResource extends JsonResource
       'language'    => $this->language,
       'type'        => $this->type,
       'provider_id' => $this->provider?->id,
+      'provider_share_code' => $this->provider?->share_code,
 
       'push_notifications_enabled' => $this->push_notifications_enabled,
 

+ 38 - 1
app/Models/Provider.php

@@ -39,6 +39,7 @@ use Illuminate\Support\Facades\Auth;
  * @property string|null $recipient_document
  * @property string|null $recipient_type
  * @property string|null $recipient_code
+ * @property string|null $share_code
  * @property string|null $recipient_payment_mode
  * @property array<array-key, mixed>|null $recipient_default_bank_account
  * @property array<array-key, mixed>|null $recipient_transfer_settings
@@ -106,7 +107,11 @@ class Provider extends Model
 
     protected $table = "providers";
 
-    protected $guarded = ["id", "recipient_code"];
+    public const SHARE_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
+
+    public const SHARE_CODE_LENGTH = 6;
+
+    protected $guarded = ["id", "recipient_code", "share_code"];
 
     /**
      * Get the attributes that should be cast.
@@ -134,6 +139,15 @@ class Provider extends Model
         ];
     }
 
+    protected static function booted(): void
+    {
+        static::creating(function (self $provider) {
+            if (empty($provider->share_code)) {
+                $provider->share_code = self::generateUniqueShareCode();
+            }
+        });
+    }
+
     public function user(): BelongsTo
     {
         return $this->belongsTo(User::class, 'user_id');
@@ -276,4 +290,27 @@ class Provider extends Model
             ->where('provider_bank_accounts.is_active', true)
             ->whereNull('provider_bank_accounts.deleted_at');
     }
+
+    public static function generateUniqueShareCode(): string
+    {
+        $alphabet = self::SHARE_CODE_ALPHABET;
+        $lastIndex = strlen($alphabet) - 1;
+
+        do {
+            $code = '';
+
+            for ($i = 0; $i < self::SHARE_CODE_LENGTH; $i++) {
+                $code .= $alphabet[random_int(0, $lastIndex)];
+            }
+
+            $exists = self::withTrashed()->where('share_code', $code)->exists();
+        } while ($exists);
+
+        return $code;
+    }
+
+    public static function normalizeShareCode(?string $code): string
+    {
+        return strtoupper(trim((string) $code));
+    }
 }

+ 6 - 4
app/Models/Schedule.php

@@ -82,16 +82,18 @@ class Schedule extends Model
         'total_amount',
         'code',
         'code_verified',
+        'code_reminder_sent_at',
         'offers_meal',
         'cancel_text',
         'cancelled_by',
     ];
 
     protected $casts = [
-        'date'          => 'date',
-        'code_verified' => 'boolean',
-        'total_amount'  => 'decimal:2',
-        'offers_meal'   => 'boolean',
+        'date'                   => 'date',
+        'code_verified'          => 'boolean',
+        'code_reminder_sent_at'  => 'datetime',
+        'total_amount'           => 'decimal:2',
+        'offers_meal'            => 'boolean',
     ];
 
     public function client()

+ 64 - 0
app/Notifications/Push/Cliente/Agendamento/PrestadorAceitouPush.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Notifications\Push\Cliente\Agendamento;
+
+use App\Enums\PushNotificationCategoryEnum;
+use App\Enums\PushNotificationTargetEnum;
+use App\Notifications\Push\BasePushNotification;
+use Illuminate\Database\Eloquent\Collection;
+
+/**
+ * Notificação enviada ao cliente quando um prestador aceita seu agendamento
+ * (agendamento padrão) ou envia uma proposta para um pedido sob medida.
+ */
+class PrestadorAceitouPush extends BasePushNotification
+{
+    public function __construct(
+        private readonly string $providerName,
+        private readonly bool $isProposal = false,
+    ) {}
+
+    public function label(): string
+    {
+        return 'client_provider_accepted';
+    }
+
+    public function title(): string
+    {
+        return $this->isProposal
+            ? 'Nova proposta recebida'
+            : 'Prestador confirmado! 🎉';
+    }
+
+    public function body(): string
+    {
+        return $this->isProposal
+            ? "{$this->providerName} enviou uma proposta para o seu pedido sob medida."
+            : "{$this->providerName} aceitou seu agendamento.";
+    }
+
+    public function target(): PushNotificationTargetEnum
+    {
+        return PushNotificationTargetEnum::CLIENTE;
+    }
+
+    public function category(): PushNotificationCategoryEnum
+    {
+        return PushNotificationCategoryEnum::AGENDA;
+    }
+
+    public function eligibleUsers(): Collection
+    {
+        return new Collection();
+    }
+
+    public function notificationCooldownDays(): int
+    {
+        return 0;
+    }
+
+    public function categoryCooldownDays(): int
+    {
+        return 0;
+    }
+}

+ 59 - 0
app/Notifications/Push/Prestador/Agendamento/CodigoNaoPreenchidoPush.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace App\Notifications\Push\Prestador\Agendamento;
+
+use App\Enums\PushNotificationCategoryEnum;
+use App\Enums\PushNotificationTargetEnum;
+use App\Notifications\Push\BasePushNotification;
+use Illuminate\Database\Eloquent\Collection;
+
+/**
+ * Lembrete enviado ao prestador quando um atendimento já iniciou
+ * há 20 minutos e o código de confirmação ainda não foi preenchido.
+ */
+class CodigoNaoPreenchidoPush extends BasePushNotification
+{
+    public function __construct(
+        private readonly string $clientName,
+    ) {}
+
+    public function label(): string
+    {
+        return 'provider_code_reminder';
+    }
+
+    public function title(): string
+    {
+        return 'Confirme o código do atendimento';
+    }
+
+    public function body(): string
+    {
+        return "Não esqueça de confirmar o código informado por {$this->clientName} para iniciar o registro do serviço.";
+    }
+
+    public function target(): PushNotificationTargetEnum
+    {
+        return PushNotificationTargetEnum::PRESTADOR;
+    }
+
+    public function category(): PushNotificationCategoryEnum
+    {
+        return PushNotificationCategoryEnum::TRANSACIONAL;
+    }
+
+    public function eligibleUsers(): Collection
+    {
+        return new Collection();
+    }
+
+    public function notificationCooldownDays(): int
+    {
+        return 0;
+    }
+
+    public function categoryCooldownDays(): int
+    {
+        return 0;
+    }
+}

+ 1 - 5
app/Services/ClientCalendarService.php

@@ -63,11 +63,7 @@ class ClientCalendarService
 
         $upcomingSchedules = Schedule::with('address:district,address,number,source_id,source,id')
             ->where('schedules.client_id', $client->id)
-            ->whereIn('schedules.status', ['pending', 'accepted', 'paid', 'started'])
-            ->where(function ($query) {
-                $query->where('schedules.schedule_type', '!=', 'custom')
-                    ->orWhereIn('schedules.status', ['paid', 'started']);
-            })
+            ->whereIn('schedules.status', ['paid', 'started'])
             ->whereDate('schedules.date', '>=', now()->toDateString())
             ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')

+ 66 - 0
app/Services/ClientFavoriteProviderService.php

@@ -2,10 +2,15 @@
 
 namespace App\Services;
 
+use App\Enums\ApprovalStatusEnum;
+use App\Exceptions\FavoriteProviderException;
 use App\Http\Resources\ClientFavoriteProviderResource;
 use App\Models\ClientFavoriteProvider;
+use App\Models\ClientProviderBlock;
 use App\Models\Provider;
+use App\Models\ProviderClientBlock;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Facades\DB;
 
 class ClientFavoriteProviderService
 {
@@ -30,6 +35,7 @@ class ClientFavoriteProviderService
                 'client_favorite_providers.created_at',
                 'client_favorite_providers.updated_at',
                 'provider_user.name as provider_name',
+                'providers.share_code',
                 'providers.gender',
                 'providers.average_rating',
                 'providers.daily_price_8h',
@@ -79,6 +85,66 @@ class ClientFavoriteProviderService
 
     //
 
+    public function createByCode(int $clientId, string $code): ClientFavoriteProviderResource
+    {
+        $code = Provider::normalizeShareCode($code);
+
+        $provider = Provider::query()
+            ->where('share_code', $code)
+            ->where('approval_status', ApprovalStatusEnum::ACCEPTED->value)
+            ->visibleToCustomers()
+            ->with(['user', 'profileMedia'])
+            ->first();
+
+        if (! $provider) {
+            throw new FavoriteProviderException('favorite_code_not_found', 404);
+        }
+
+        $isBlocked = ProviderClientBlock::where('provider_id', $provider->id)
+            ->where('client_id', $clientId)
+            ->exists()
+            || ClientProviderBlock::where('client_id', $clientId)
+                ->where('provider_id', $provider->id)
+                ->exists();
+
+        if ($isBlocked) {
+            throw new FavoriteProviderException('favorite_code_blocked');
+        }
+
+        $favorite = DB::transaction(function () use ($clientId, $provider) {
+            $alreadyFavorite = ClientFavoriteProvider::where('client_id', $clientId)
+                ->where('provider_id', $provider->id)
+                ->exists();
+
+            if ($alreadyFavorite) {
+                throw new FavoriteProviderException('favorite_code_already_favorite');
+            }
+
+            $removed = ClientFavoriteProvider::onlyTrashed()
+                ->where('client_id', $clientId)
+                ->where('provider_id', $provider->id)
+                ->latest('id')
+                ->first();
+
+            if ($removed) {
+                $removed->restore();
+
+                return $removed;
+            }
+
+            return ClientFavoriteProvider::create([
+                'client_id'   => $clientId,
+                'provider_id' => $provider->id,
+            ]);
+        });
+
+        $favorite->setRelation('provider', $provider);
+
+        return new ClientFavoriteProviderResource($favorite);
+    }
+
+    //
+
     public function getFavoritedProviderIds(int $clientId): array
     {
         return ClientFavoriteProvider::where('client_id', $clientId)

+ 30 - 0
app/Services/CustomScheduleService.php

@@ -11,9 +11,11 @@ use App\Models\Schedule;
 use App\Models\ScheduleProposal;
 use App\Models\ScheduleRefuse;
 use App\Models\ServicePackage;
+use App\Notifications\Push\Cliente\Agendamento\PrestadorAceitouPush;
 use App\Rules\ScheduleBusinessRules;
 use App\Services\NotificationService;
 use App\Services\DistanceService;
+use App\Services\PushNotificationService;
 use Carbon\Carbon;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
@@ -447,12 +449,40 @@ class CustomScheduleService
             'user_id'     => $schedule->client->user_id,
         ]);
 
+        $this->sendProposalReceivedPush($schedule, $provider->user->name);
+
         return ScheduleProposal::create([
             'schedule_id' => $scheduleId,
             'provider_id' => $providerId,
         ]);
     }
 
+    private function sendProposalReceivedPush(Schedule $schedule, string $providerName): void
+    {
+        $user = $schedule->client->user;
+
+        if (! $user) {
+            Log::warning('Push de proposta ignorada: cliente sem usuário', [
+                'schedule_id' => $schedule->id,
+            ]);
+
+            return;
+        }
+
+        try {
+            app(PushNotificationService::class)->sendToUser(
+                $user,
+                new PrestadorAceitouPush($providerName, isProposal: true)
+            );
+        } catch (\Throwable $exception) {
+            Log::error('Falha ao enviar push de nova proposta sob medida', [
+                'schedule_id' => $schedule->id,
+                'user_id'     => $user->id,
+                'error'       => $exception->getMessage(),
+            ]);
+        }
+    }
+
     public function refuseOpportunity($scheduleId, $providerId)
     {
 

+ 199 - 6
app/Services/DashboardService.php

@@ -414,6 +414,8 @@ class DashboardService
             $item->gender_label = GenderEnum::labelFor($item->gender);
         });
 
+        $this->attachPackageItems($pendingSchedules);
+
         $proposalsDistanceSelect = DistanceService::sqlExpression(
             $providersCloseLatitude,
             $providersCloseLongitude,
@@ -541,7 +543,7 @@ class DashboardService
             'address:district,address,number,source_id,source,id,address_type',
         ])
             ->where('schedules.client_id', $cliente->id)
-            ->whereIn('schedules.status', ['accepted', 'paid', 'started', 'cancelled', 'finished'])
+            ->whereIn('schedules.status', ['paid', 'started', 'finished'])
             ->whereDate('schedules.date', now()->toDateString())
             ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
@@ -813,15 +815,153 @@ class DashboardService
             );
         });
 
-        $providerPhotoUrls = $this->providerPhotoUrls($schedulesProposals->pluck('provider_id'));
+        $packageItemCounts = DB::table('service_package_items as spi_count')
+            ->join('schedules as s_count', function ($join) {
+                $join->on('s_count.id', '=', 'spi_count.schedule_id')
+                    ->whereNotIn('s_count.status', ['cancelled', 'rejected'])
+                    ->where('s_count.schedule_type', 'default');
+            })
+            ->groupBy('spi_count.service_package_id')
+            ->select('spi_count.service_package_id', DB::raw('COUNT(*) AS items_count'));
+
+        $pendingSchedules = Schedule::query()
+            ->where('schedules.client_id', $cliente->id)
+            ->where('schedules.schedule_type', 'default')
+            ->where('schedules.status', 'pending')
+            ->whereDate('schedules.date', '>=', now()->toDateString())
+            ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
+            ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
+            ->leftJoin('addresses as schedule_address', 'schedule_address.id', '=', 'schedules.address_id')
+            ->leftJoin('service_package_items as spi', 'spi.schedule_id', '=', 'schedules.id')
+            ->leftJoinSub(
+                $packageItemCounts,
+                'package_counts',
+                'package_counts.service_package_id',
+                '=',
+                'spi.service_package_id',
+            )
+            ->orderBy('schedules.date', 'asc')
+            ->select([
+                'schedules.id',
+                'schedules.provider_id',
+                'schedules.date',
+                'schedules.start_time',
+                'schedules.end_time',
+                'schedules.status',
+                'provider_user.name as provider_name',
+                'spi.service_package_id',
+
+                DB::raw('COALESCE(package_counts.items_count, 1) AS service_package_items_count'),
 
-        $schedulesProposals->each(function ($item) use ($providerPhotoUrls) {
-            $item->provider_photo = $providerPhotoUrls->get($item->provider_id);
+                DB::raw("
+                CASE
+                    WHEN (NOW() - schedules.created_at) < INTERVAL '1 hour' THEN
+                        CONCAT(ROUND(EXTRACT(EPOCH FROM (NOW() - schedules.created_at)) / 60), 'min')
+                    WHEN (NOW() - schedules.created_at) < INTERVAL '1 day' THEN
+                        CONCAT(ROUND(EXTRACT(EPOCH FROM (NOW() - schedules.created_at)) / 3600), 'h')
+                    ELSE
+                        CONCAT(ROUND(EXTRACT(EPOCH FROM (NOW() - schedules.created_at)) / 86400), 'd')
+                END AS time_since_request
+            "),
+
+                'schedule_address.address as address_address',
+                'schedule_address.number as address_number',
+                'schedule_address.district as address_district',
+            ])
+            ->get()
+            ->unique(fn ($schedule) => $schedule->service_package_id
+                ? 'package-'.$schedule->service_package_id
+                : 'schedule-'.$schedule->id)
+            ->values();
+
+        $this->attachPackageItems($pendingSchedules);
+
+        $pendingSchedules->each(function ($schedule) {
+            $schedule->address = [
+                'address'  => $schedule->address_address,
+                'number'   => $schedule->address_number,
+                'district' => $schedule->address_district,
+            ];
+
+            unset(
+                $schedule->address_address,
+                $schedule->address_number,
+                $schedule->address_district,
+            );
         });
 
+        $pendingServicePackages = ServicePackage::query()
+            ->where('client_id', $cliente->id)
+            ->where('status', ServicePackageStatusEnum::OPEN->value)
+            ->whereHas('items.schedule', fn ($query) => $query->where('status', 'accepted'))
+            ->whereDoesntHave('items.schedule', fn ($query) => $query->where('status', 'pending'))
+            ->with([
+                'provider:id,user_id',
+                'provider.user:id,name',
+                'items:id,service_package_id,schedule_id',
+
+                'items.schedule' => fn ($query) => $query
+                    ->select('id', 'date', 'start_time', 'end_time', 'status', 'total_amount', 'address_id')
+                    ->with('address:id,address,number,district'),
+            ])
+            ->get();
+
+        $providerPhotoUrls = $this->providerPhotoUrls(
+            $schedulesProposals->pluck('provider_id')
+                ->merge($pendingSchedules->pluck('provider_id'))
+                ->merge($pendingServicePackages->pluck('provider_id')),
+        );
+
+        collect([$schedulesProposals, $pendingSchedules])->each(
+            fn (Collection $items) => $items->each(function ($item) use ($providerPhotoUrls) {
+                $item->provider_photo = $providerPhotoUrls->get($item->provider_id);
+            }),
+        );
+
+        $pendingServicePackages = $pendingServicePackages->map(
+            function (ServicePackage $package) use ($providerPhotoUrls) {
+                $schedules = $package->items
+                    ->pluck('schedule')
+                    ->filter()
+                    ->values();
+
+                $activeSchedules = $schedules->reject(
+                    fn ($schedule) => in_array($schedule->status, ['cancelled', 'rejected'], true),
+                );
+
+                return [
+                    'id'                          => $package->id,
+                    'provider_id'                 => $package->provider_id,
+                    'provider_name'               => $package->provider?->user?->name,
+                    'provider_photo'              => $providerPhotoUrls->get($package->provider_id),
+                    'total_amount'                => (float) $activeSchedules->sum(
+                        fn ($schedule) => (float) $schedule->total_amount,
+                    ),
+                    'service_package_items_count' => $activeSchedules->count(),
+
+                    'schedules' => $schedules->map(fn ($schedule) => [
+                        'id'           => $schedule->id,
+                        'status'       => $schedule->status,
+                        'date'         => $schedule->date?->format('Y-m-d'),
+                        'start_time'   => $schedule->start_time,
+                        'end_time'     => $schedule->end_time,
+                        'total_amount' => (float) $schedule->total_amount,
+
+                        'address' => [
+                            'address'  => $schedule->address?->address,
+                            'number'   => $schedule->address?->number,
+                            'district' => $schedule->address?->district,
+                        ],
+                    ])->values(),
+                ];
+            },
+        );
+
         return [
             'schedulesProposals'         => $schedulesProposals,
             'customSchedulesNoProposals' => $customSchedulesNoProposals,
+            'pendingSchedules'           => $pendingSchedules,
+            'pendingServicePackages'     => $pendingServicePackages,
         ];
     }
 
@@ -970,7 +1110,7 @@ class DashboardService
             'address:district,address,number,source_id,source,id',
         ])
             ->where('schedules.provider_id', $provider->id)
-            ->whereIn('schedules.status', ['accepted', 'paid', 'started', 'finished'])
+            ->whereIn('schedules.status', ['paid', 'started', 'finished'])
             ->whereDate('schedules.date', now()->toDateString())
             ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
             ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')
@@ -1210,7 +1350,60 @@ class DashboardService
         ];
     }
 
-    // gera urls apenas para fotos verificadas ou visiveis ao proprio prestador.
+    private function attachPackageItems(Collection $schedules): void
+    {
+        $packageIds    = $schedules->pluck('service_package_id')->filter()->unique();
+        $standaloneIds = $schedules->whereNull('service_package_id')->pluck('id')->filter()->unique();
+
+        if ($packageIds->isEmpty() && $standaloneIds->isEmpty()) {
+            $schedules->each(fn ($schedule) => $schedule->package_items = collect());
+
+            return;
+        }
+
+        $rows = Schedule::query()
+            ->leftJoin('service_package_items as spi', 'spi.schedule_id', '=', 'schedules.id')
+            ->where(function ($query) use ($packageIds, $standaloneIds) {
+                if ($packageIds->isNotEmpty()) {
+                    $query->orWhereIn('spi.service_package_id', $packageIds);
+                }
+
+                if ($standaloneIds->isNotEmpty()) {
+                    $query->orWhereIn('schedules.id', $standaloneIds);
+                }
+            })
+            ->whereNotIn('schedules.status', ['cancelled', 'rejected'])
+            ->orderBy('schedules.date', 'asc')
+            ->orderBy('schedules.start_time', 'asc')
+            ->select([
+                'schedules.id',
+                'schedules.date',
+                'schedules.start_time',
+                'schedules.end_time',
+                'schedules.total_amount',
+                'schedules.offers_meal',
+                'schedules.status',
+                'spi.service_package_id',
+            ])
+            ->get();
+
+        $byPackage = $rows->whereNotNull('service_package_id')->groupBy('service_package_id');
+        $byId      = $rows->keyBy('id');
+
+        $schedules->each(function ($schedule) use ($byPackage, $byId) {
+            if ($schedule->service_package_id) {
+                $schedule->package_items = $byPackage
+                    ->get($schedule->service_package_id, collect())
+                    ->values();
+
+                return;
+            }
+
+            $own = $byId->get($schedule->id);
+
+            $schedule->package_items = $own ? collect([$own]) : collect();
+        });
+    }
 
     private function providerPhotoUrls(iterable $providerIds): Collection
     {

+ 1 - 10
app/Services/Pagarme/PagarmePaymentService.php

@@ -746,17 +746,8 @@ class PagarmePaymentService
             return $payment->idempotency_key;
         }
 
-        $payment->loadMissing(['client', 'provider']);
-
-        $date = $payment->created_at?->format('Y-m-d') ?: now()->format('Y-m-d');
-
-        $providerCode = $payment->provider?->ensureGatewayCode() ?: "provider-{$payment->provider_id}";
-        $clientCode   = $payment->client?->ensureGatewayCode() ?: "client-{$payment->client_id}";
-
         $key = $this->pagarmeIdempotencyKey('order', [
-            $date,
-            $providerCode,
-            $clientCode,
+            $payment->ensureGatewayCode(),
         ]);
 
         $payment->forceFill(['idempotency_key' => $key])->save();

+ 887 - 886
app/Services/PaymentService.php

@@ -22,1012 +22,1013 @@ use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Support\Collection as SupportCollection;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
 
 class PaymentService
 {
-    public function __construct(
-        private readonly PagarmePaymentService $pagarmePaymentService,
-    ) {}
-
-    public function getAll(): Collection
-    {
-        return Payment::query()
-            ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
-            ->orderBy('created_at', 'desc')
-            ->get();
+  public function __construct(
+    private readonly PagarmePaymentService $pagarmePaymentService,
+  ) {}
+
+  public function getAll(): Collection
+  {
+    return Payment::query()
+      ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
+      ->orderBy('created_at', 'desc')
+      ->get();
+  }
+
+  public function findById(int $id): ?Payment
+  {
+    return Payment::query()
+      ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
+      ->find($id);
+  }
+
+  public function create(array $data): Payment
+  {
+    return Payment::create($data);
+  }
+
+  public function update(int $id, array $data): ?Payment
+  {
+    $model = $this->findById($id);
+
+    if (! $model) {
+      return null;
     }
 
-    public function findById(int $id): ?Payment
-    {
-        return Payment::query()
-            ->with(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule'])
-            ->find($id);
-    }
-
-    public function create(array $data): Payment
-    {
-        return Payment::create($data);
-    }
-
-    public function update(int $id, array $data): ?Payment
-    {
-        $model = $this->findById($id);
+    $model->update($data);
 
-        if (! $model) {
-            return null;
-        }
+    return $model->fresh();
+  }
 
-        $model->update($data);
+  public function delete(int $id): bool
+  {
+    $model = $this->findById($id);
 
-        return $model->fresh();
+    if (! $model) {
+      return false;
     }
 
-    public function delete(int $id): bool
-    {
-        $model = $this->findById($id);
+    return $model->delete();
+  }
 
-        if (! $model) {
-            return false;
-        }
+  //
 
-        return $model->delete();
-    }
+  public function platformFees(): array
+  {
+    return $this->pagarmePaymentService->platformFeeRates();
+  }
 
-    //
+  public function payServicePackage(
+    int    $servicePackageId,
+    string $paymentMethod,
+    ?int   $clientPaymentMethodId = null,
+    array  $options               = [],
+  ): Payment {
+    $userId = (int) Auth::id();
 
-    public function platformFees(): array
-    {
-        return $this->pagarmePaymentService->platformFeeRates();
+    if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
+      throw new PaymentException;
     }
+    Log::info('1');
+    $paymentData = DB::transaction(function () use (
+      $servicePackageId,
+      $userId,
+      $paymentMethod,
+      $clientPaymentMethodId,
+      $options,
+    ): array {
+      $servicePackage = ServicePackage::query()
+        ->lockForUpdate()
+        ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType'])
+        ->findOrFail($servicePackageId);
+
+      if ($servicePackage->client?->user_id !== $userId) {
+        throw new AuthorizationException;
+      }
+Log::info('2');
+      $schedules = $this->activePackageSchedules($servicePackage);
+
+      $this->validateServicePackageForPayment($servicePackage, $schedules);
+
+      $existingPayment = Payment::query()
+        ->where('service_package_id', $servicePackage->id)
+        ->whereIn('status', [
+          PaymentStatusEnum::PENDING->value,
+          PaymentStatusEnum::PROCESSING->value,
+          PaymentStatusEnum::AUTHORIZED->value,
+          PaymentStatusEnum::PAID->value,
+        ])
+        ->latest('id')
+        ->first();
+Log::info('3');
+      if ($existingPayment) {
+        if ($this->isExpiredPixPayment($existingPayment)) {
+          $existingPayment->forceFill([
+            'status'          => PaymentStatusEnum::FAILED,
+            'failed_at'       => now(),
+            'failure_message' => 'Pagamento Pix expirado.',
+          ])->save();
 
-    public function payServicePackage(
-        int    $servicePackageId,
-        string $paymentMethod,
-        ?int   $clientPaymentMethodId = null,
-        array  $options               = [],
-    ): Payment {
-        $userId = (int) Auth::id();
-
-        if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
+          PaymentSplit::query()
+            ->where('payment_id', $existingPayment->id)
+            ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+        } elseif ($this->isStaleIncompleteGatewayPayment($existingPayment)) {
+          if ($existingPayment->payment_method !== $paymentMethod) {
             throw new PaymentException;
-        }
+          }
+Log::info('4');
+          [, $cardId] = $this->resolveCard(
+            clientId: $servicePackage->client_id,
+            paymentMethod: $paymentMethod,
+            clientPaymentMethodId: $clientPaymentMethodId ?? $existingPayment->client_payment_method_id,
+            cardId: data_get($options, 'card_id'),
+          );
+
+          $this->servicePackagePaymentTotals($schedules, $paymentMethod);
+Log::info('5');
+          return [
+            'payment'   => $existingPayment,
+            'schedules' => $schedules,
+            'cardId'    => $cardId,
+          ];
+        } else {
+          if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
+            throw new PaymentException;
+          }
 
-        $paymentData = DB::transaction(function () use (
-            $servicePackageId,
-            $userId,
-            $paymentMethod,
-            $clientPaymentMethodId,
-            $options,
-        ): array {
-            $servicePackage = ServicePackage::query()
-                ->lockForUpdate()
-                ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType'])
-                ->findOrFail($servicePackageId);
-
-            if ($servicePackage->client?->user_id !== $userId) {
-                throw new AuthorizationException;
-            }
-
-            $schedules = $this->activePackageSchedules($servicePackage);
-
-            $this->validateServicePackageForPayment($servicePackage, $schedules);
-
-            $existingPayment = Payment::query()
-                ->where('service_package_id', $servicePackage->id)
-                ->whereIn('status', [
-                    PaymentStatusEnum::PENDING->value,
-                    PaymentStatusEnum::PROCESSING->value,
-                    PaymentStatusEnum::AUTHORIZED->value,
-                    PaymentStatusEnum::PAID->value,
-                ])
-                ->latest('id')
-                ->first();
-
-            if ($existingPayment) {
-                if ($this->isExpiredPixPayment($existingPayment)) {
-                    $existingPayment->forceFill([
-                        'status'          => PaymentStatusEnum::FAILED,
-                        'failed_at'       => now(),
-                        'failure_message' => 'Pagamento Pix expirado.',
-                    ])->save();
-
-                    PaymentSplit::query()
-                        ->where('payment_id', $existingPayment->id)
-                        ->update(['status' => PaymentSplitStatusEnum::FAILED]);
-                } elseif ($this->isStaleIncompleteGatewayPayment($existingPayment)) {
-                    if ($existingPayment->payment_method !== $paymentMethod) {
-                        throw new PaymentException;
-                    }
-
-                    [, $cardId] = $this->resolveCard(
-                        clientId:              $servicePackage->client_id,
-                        paymentMethod:         $paymentMethod,
-                        clientPaymentMethodId: $clientPaymentMethodId ?? $existingPayment->client_payment_method_id,
-                        cardId:                data_get($options, 'card_id'),
-                    );
-
-                    $this->servicePackagePaymentTotals($schedules, $paymentMethod);
-
-                    return [
-                        'payment'   => $existingPayment,
-                        'schedules' => $schedules,
-                        'cardId'    => $cardId,
-                    ];
-                } else {
-                    if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
-                        throw new PaymentException;
-                    }
-
-                    return ['existing' => $existingPayment];
-                }
-            }
-
-            if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) {
-                throw new PaymentException;
-            }
-
-            [$clientPaymentMethod, $cardId] = $this->resolveCard(
-                clientId:              $servicePackage->client_id,
-                paymentMethod:         $paymentMethod,
-                clientPaymentMethodId: $clientPaymentMethodId,
-                cardId:                data_get($options, 'card_id'),
-            );
-
-            $totals = $this->servicePackagePaymentTotals($schedules, $paymentMethod);
-
-            $payment = Payment::create([
-                'schedule_id'              => null,
-                'service_package_id'       => $servicePackage->id,
-                'client_id'                => $servicePackage->client_id,
-                'provider_id'              => $servicePackage->provider_id,
-                'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod?->id : null,
-                'gateway_provider'         => 'pagarme',
-                'gateway_code'             => 'payment-'.(string) Str::uuid(),
-                'payment_method'           => $paymentMethod,
-                'status'                   => PaymentStatusEnum::PENDING,
-                'gross_amount'             => data_get($totals, 'gross_amount'),
-                'gateway_fee_amount'       => 0,
-                'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
-                'net_amount'               => data_get($totals, 'gross_amount'),
-                'currency'                 => 'BRL',
-                'installments'             => 1,
-                'expires_at'               => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
-
-                'metadata' => [
-                    'service_package_id' => (string) $servicePackage->id,
-                    'schedule_ids'       => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
-                    'service_amount'     => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
-                    'platform_fee'       => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
-                ],
-            ]);
-
-            PaymentSplit::create([
-                'payment_id'                        => $payment->id,
-                'provider_id'                       => $servicePackage->provider_id,
-                'gateway_provider'                  => 'pagarme',
-                'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id,
-                'gateway_transfer_target_label'     => 'recipient',
-                'status'                            => PaymentSplitStatusEnum::PENDING,
-                'gross_amount'                      => data_get($totals, 'service_amount'),
-                'gateway_fee_amount'                => 0,
-                'net_amount'                        => data_get($totals, 'service_amount'),
-
-                'metadata' => [
-                    'service_package_id' => (string) $servicePackage->id,
-                    'schedule_ids'       => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
-                ],
-            ]);
-
-            return compact('payment', 'schedules', 'cardId');
-        });
-
-        if (data_get($paymentData, 'existing')) {
-            $existingPayment = data_get($paymentData, 'existing');
-
-            $this->syncPaymentTargets($existingPayment);
-
-            return $existingPayment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
+          return ['existing' => $existingPayment];
         }
+      }
+Log::info('6');
+      if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) {
+        throw new PaymentException;
+      }
+
+      [$clientPaymentMethod, $cardId] = $this->resolveCard(
+        clientId: $servicePackage->client_id,
+        paymentMethod: $paymentMethod,
+        clientPaymentMethodId: $clientPaymentMethodId,
+        cardId: data_get($options, 'card_id'),
+      );
+Log::info('7');
+      $totals = $this->servicePackagePaymentTotals($schedules, $paymentMethod);
+
+      $payment = Payment::create([
+        'schedule_id'              => null,
+        'service_package_id'       => $servicePackage->id,
+        'client_id'                => $servicePackage->client_id,
+        'provider_id'              => $servicePackage->provider_id,
+        'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod?->id : null,
+        'gateway_provider'         => 'pagarme',
+        'gateway_code'             => 'payment-' . (string) Str::uuid(),
+        'payment_method'           => $paymentMethod,
+        'status'                   => PaymentStatusEnum::PENDING,
+        'gross_amount'             => data_get($totals, 'gross_amount'),
+        'gateway_fee_amount'       => 0,
+        'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
+        'net_amount'               => data_get($totals, 'gross_amount'),
+        'currency'                 => 'BRL',
+        'installments'             => 1,
+        'expires_at'               => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
+
+        'metadata' => [
+          'service_package_id' => (string) $servicePackage->id,
+          'schedule_ids'       => $schedules->pluck('id')->map(fn($id) => (string) $id)->all(),
+          'service_amount'     => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
+          'platform_fee'       => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
+        ],
+      ]);
+Log::info('8');
+      PaymentSplit::create([
+        'payment_id'                        => $payment->id,
+        'provider_id'                       => $servicePackage->provider_id,
+        'gateway_provider'                  => 'pagarme',
+        'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id,
+        'gateway_transfer_target_label'     => 'recipient',
+        'status'                            => PaymentSplitStatusEnum::PENDING,
+        'gross_amount'                      => data_get($totals, 'service_amount'),
+        'gateway_fee_amount'                => 0,
+        'net_amount'                        => data_get($totals, 'service_amount'),
+
+        'metadata' => [
+          'service_package_id' => (string) $servicePackage->id,
+          'schedule_ids'       => $schedules->pluck('id')->map(fn($id) => (string) $id)->all(),
+        ],
+      ]);
+
+      return compact('payment', 'schedules', 'cardId');
+    });
+Log::info('9');
+    if (data_get($paymentData, 'existing')) {
+      $existingPayment = data_get($paymentData, 'existing');
+Log::info('10');
+      $this->syncPaymentTargets($existingPayment);
+
+      return $existingPayment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
+    }
 
-        /** @var Payment $payment */
-        $payment = data_get($paymentData, 'payment');
-
-        /** @var SupportCollection $schedules */
-        $schedules = data_get($paymentData, 'schedules');
+    /** @var Payment $payment */
+    $payment = data_get($paymentData, 'payment');
 
-        try {
-            $schedules->first()->ensureCustomerPhone(data_get($options, 'phone'));
+    /** @var SupportCollection $schedules */
+    $schedules = data_get($paymentData, 'schedules');
 
-            $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
-                payment:       $payment,
-                schedules:     $schedules,
-                paymentMethod: $paymentMethod,
-                cardId:        data_get($paymentData, 'cardId'),
-                options:       $options,
-            );
-        } catch (\Throwable $e) {
-            $this->failPayment($payment, $e->getMessage());
+    try {
+      $schedules->first()->ensureCustomerPhone(data_get($options, 'phone'));
 
-            throw $e;
-        }
+      $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
+        payment: $payment,
+        schedules: $schedules,
+        paymentMethod: $paymentMethod,
+        cardId: data_get($paymentData, 'cardId'),
+        options: $options,
+      );
+    } catch (\Throwable $e) {
+      $this->failPayment($payment, $e->getMessage());
 
-        $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
+      throw $e;
+    }
 
-        $this->syncPaymentTargets($payment);
+    $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
 
-        if ($payment->status === PaymentStatusEnum::FAILED) {
-            throw new PaymentFailedException;
-        }
+    $this->syncPaymentTargets($payment);
 
-        return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
+    if ($payment->status === PaymentStatusEnum::FAILED) {
+      throw new PaymentFailedException;
     }
 
-    //
+    return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
+  }
 
-    public function getOrCreateServicePackagePixPayment(ServicePackage $servicePackage): Payment
-    {
-        $userId = (int) Auth::id();
+  //
 
-        if ($servicePackage->client?->user_id !== $userId) {
-            throw new AuthorizationException;
-        }
-
-        $existingPayment = Payment::query()
-            ->where('service_package_id', $servicePackage->id)
-            ->where('payment_method', 'pix')
-            ->whereIn('status', [
-                PaymentStatusEnum::PENDING->value,
-                PaymentStatusEnum::PROCESSING->value,
-                PaymentStatusEnum::AUTHORIZED->value,
-                PaymentStatusEnum::PAID->value,
-            ])
-            ->latest('id')
-            ->first();
-
-        if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) {
-            $existingPayment->forceFill([
-                'status'          => PaymentStatusEnum::FAILED,
-                'failed_at'       => Carbon::now(),
-                'failure_message' => 'Pagamento Pix expirado.',
-            ])->save();
-
-            PaymentSplit::query()
-                ->where('payment_id', $existingPayment->id)
-                ->update(['status' => PaymentSplitStatusEnum::FAILED]);
-
-            $existingPayment = null;
-        }
+  public function getOrCreateServicePackagePixPayment(ServicePackage $servicePackage): Payment
+  {
+    $userId = (int) Auth::id();
 
-        if ($existingPayment) {
-            if ($this->isIncompleteGatewayPayment($existingPayment)) {
-                $existingPayment->forceFill([
-                    'status'          => PaymentStatusEnum::FAILED,
-                    'failed_at'       => Carbon::now(),
-                    'failure_message' => 'Pagamento pendente sem retorno do gateway.',
-                ])->save();
-
-                PaymentSplit::query()
-                    ->where('payment_id', $existingPayment->id)
-                    ->update(['status' => PaymentSplitStatusEnum::FAILED]);
-            } else {
-                $this->syncPaymentTargets($existingPayment);
-
-                return $existingPayment;
-            }
-        }
+    if ($servicePackage->client?->user_id !== $userId) {
+      throw new AuthorizationException;
+    }
 
-        $paymentData = DB::transaction(function () use ($servicePackage, $userId): array {
-            $servicePackage = ServicePackage::query()
-                ->lockForUpdate()
-                ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType'])
-                ->findOrFail($servicePackage->id);
-
-            if ($servicePackage->client?->user_id !== $userId) {
-                throw new AuthorizationException;
-            }
-
-            $schedules = $this->activePackageSchedules($servicePackage);
-
-            $this->validateServicePackageForPayment($servicePackage, $schedules);
-
-            if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) {
-                throw new PaymentException;
-            }
-
-            $totals = $this->servicePackagePaymentTotals($schedules, 'pix');
-
-            $payment = Payment::create([
-                'schedule_id'              => null,
-                'service_package_id'       => $servicePackage->id,
-                'client_id'                => $servicePackage->client_id,
-                'provider_id'              => $servicePackage->provider_id,
-                'client_payment_method_id' => null,
-                'gateway_provider'         => 'pagarme',
-                'gateway_code'             => 'payment-'.(string) Str::uuid(),
-                'payment_method'           => 'pix',
-                'status'                   => PaymentStatusEnum::PENDING,
-                'gross_amount'             => data_get($totals, 'gross_amount'),
-                'gateway_fee_amount'       => 0,
-                'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
-                'net_amount'               => data_get($totals, 'gross_amount'),
-                'currency'                 => 'BRL',
-                'installments'             => 1,
-                'expires_at'               => Carbon::now()->addMinutes(30),
-
-                'metadata' => [
-                    'service_package_id' => (string) $servicePackage->id,
-                    'schedule_ids'       => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
-                    'service_amount'     => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
-                    'platform_fee'       => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
-                ],
-            ]);
-
-            PaymentSplit::create([
-                'payment_id'                        => $payment->id,
-                'provider_id'                       => $servicePackage->provider_id,
-                'gateway_provider'                  => 'pagarme',
-                'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id,
-                'gateway_transfer_target_label'     => 'recipient',
-                'status'                            => PaymentSplitStatusEnum::PENDING,
-                'gross_amount'                      => data_get($totals, 'service_amount'),
-                'gateway_fee_amount'                => 0,
-                'net_amount'                        => data_get($totals, 'service_amount'),
-
-                'metadata' => [
-                    'service_package_id' => (string) $servicePackage->id,
-                    'schedule_ids'       => $schedules->pluck('id')->map(fn ($id) => (string) $id)->all(),
-                ],
-            ]);
-
-            return compact('payment', 'schedules');
-        });
-
-        /** @var Payment $payment */
-        $payment = data_get($paymentData, 'payment');
-
-        /** @var SupportCollection $schedules */
-        $schedules = data_get($paymentData, 'schedules');
-
-        try {
-            $schedules->first()->ensureCustomerPhone();
-
-            $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
-                payment:       $payment,
-                schedules:     $schedules,
-                paymentMethod: 'pix',
-                cardId:        null,
-                options:       [],
-            );
-        } catch (\Throwable $e) {
-            $this->failPayment($payment, $e->getMessage());
-
-            throw $e;
-        }
+    $existingPayment = Payment::query()
+      ->where('service_package_id', $servicePackage->id)
+      ->where('payment_method', 'pix')
+      ->whereIn('status', [
+        PaymentStatusEnum::PENDING->value,
+        PaymentStatusEnum::PROCESSING->value,
+        PaymentStatusEnum::AUTHORIZED->value,
+        PaymentStatusEnum::PAID->value,
+      ])
+      ->latest('id')
+      ->first();
+
+    if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) {
+      $existingPayment->forceFill([
+        'status'          => PaymentStatusEnum::FAILED,
+        'failed_at'       => Carbon::now(),
+        'failure_message' => 'Pagamento Pix expirado.',
+      ])->save();
+
+      PaymentSplit::query()
+        ->where('payment_id', $existingPayment->id)
+        ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+
+      $existingPayment = null;
+    }
 
-        $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
+    if ($existingPayment) {
+      if ($this->isIncompleteGatewayPayment($existingPayment)) {
+        $existingPayment->forceFill([
+          'status'          => PaymentStatusEnum::FAILED,
+          'failed_at'       => Carbon::now(),
+          'failure_message' => 'Pagamento pendente sem retorno do gateway.',
+        ])->save();
 
-        $this->syncPaymentTargets($payment);
+        PaymentSplit::query()
+          ->where('payment_id', $existingPayment->id)
+          ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+      } else {
+        $this->syncPaymentTargets($existingPayment);
 
-        return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
+        return $existingPayment;
+      }
     }
 
-    public function payScheduleProposal(
-        int    $proposalId,
-        string $paymentMethod,
-        ?int   $clientPaymentMethodId = null,
-        array  $options               = [],
-    ): Payment {
-        $userId = (int) Auth::id();
-
-        if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
-            throw new PaymentException;
-        }
-
-        $paymentData = DB::transaction(function () use (
-            $proposalId,
-            $userId,
-            $paymentMethod,
-            $clientPaymentMethodId,
-            $options,
-        ): array {
-            $proposal = ScheduleProposal::query()
-                ->lockForUpdate()
-                ->with(['provider', 'schedule.client'])
-                ->findOrFail($proposalId);
-
-            $schedule = $proposal->schedule;
-            $provider = $proposal->provider;
-
-            if ($schedule?->client?->user_id !== $userId) {
-                throw new AuthorizationException;
-            }
-
-            $schedule->setAttribute('total_amount', app(CustomScheduleService::class)->resolveProposalAmount($schedule, $provider));
-
-            $schedules = collect([$schedule]);
-
-            $existingPayment = Payment::query()
-                ->where('metadata->schedule_proposal_id', (string) $proposal->id)
-                ->whereIn('status', [
-                    PaymentStatusEnum::PENDING->value,
-                    PaymentStatusEnum::PROCESSING->value,
-                    PaymentStatusEnum::AUTHORIZED->value,
-                    PaymentStatusEnum::PAID->value,
-                ])
-                ->latest('id')
-                ->first();
-
-            if (! $existingPayment && $schedule->provider_id) {
-                throw new PaymentException;
-            }
-
-            if ($existingPayment) {
-                if ($this->isExpiredPixPayment($existingPayment)) {
-                    $existingPayment->forceFill([
-                        'status'          => PaymentStatusEnum::FAILED,
-                        'failed_at'       => now(),
-                        'failure_message' => 'Pagamento Pix expirado.',
-                    ])->save();
-
-                    PaymentSplit::query()
-                        ->where('payment_id', $existingPayment->id)
-                        ->update(['status' => PaymentSplitStatusEnum::FAILED]);
-                } elseif ($this->isStaleIncompleteGatewayPayment($existingPayment)) {
-                    if ($existingPayment->payment_method !== $paymentMethod) {
-                        throw new PaymentException;
-                    }
-
-                    [, $cardId] = $this->resolveCard(
-                        clientId:              $schedule->client_id,
-                        paymentMethod:         $paymentMethod,
-                        clientPaymentMethodId: $clientPaymentMethodId ?? $existingPayment->client_payment_method_id,
-                        cardId:                data_get($options, 'card_id'),
-                    );
-
-                    $this->servicePackagePaymentTotals($schedules, $paymentMethod);
-
-                    return [
-                        'payment'   => $existingPayment,
-                        'schedules' => $schedules,
-                        'cardId'    => $cardId,
-                    ];
-                } else {
-                    if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
-                        throw new PaymentException;
-                    }
-
-                    return ['existing' => $existingPayment];
-                }
-            }
-
-            [$clientPaymentMethod, $cardId] = $this->resolveCard(
-                clientId:              $schedule->client_id,
-                paymentMethod:         $paymentMethod,
-                clientPaymentMethodId: $clientPaymentMethodId,
-                cardId:                data_get($options, 'card_id'),
-            );
-
-            $totals = $this->servicePackagePaymentTotals($schedules, $paymentMethod);
-
-            $payment = Payment::create([
-                'schedule_id'              => $schedule->id,
-                'service_package_id'       => null,
-                'client_id'                => $schedule->client_id,
-                'provider_id'              => $provider->id,
-                'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod?->id : null,
-                'gateway_provider'         => 'pagarme',
-                'gateway_code'             => 'payment-'.(string) Str::uuid(),
-                'payment_method'           => $paymentMethod,
-                'status'                   => PaymentStatusEnum::PENDING,
-                'gross_amount'             => data_get($totals, 'gross_amount'),
-                'gateway_fee_amount'       => 0,
-                'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
-                'net_amount'               => data_get($totals, 'gross_amount'),
-                'currency'                 => 'BRL',
-                'installments'             => 1,
-                'expires_at'               => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
-
-                'metadata' => [
-                    'schedule_proposal_id' => (string) $proposal->id,
-                    'schedule_id'          => (string) $schedule->id,
-                    'service_amount'       => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
-                    'platform_fee'         => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
-                ],
-            ]);
-
-            PaymentSplit::create([
-                'payment_id'                        => $payment->id,
-                'provider_id'                       => $provider->id,
-                'gateway_provider'                  => 'pagarme',
-                'gateway_transfer_target_reference' => $provider->recipient_id,
-                'gateway_transfer_target_label'     => 'recipient',
-                'status'                            => PaymentSplitStatusEnum::PENDING,
-                'gross_amount'                      => data_get($totals, 'service_amount'),
-                'gateway_fee_amount'                => 0,
-                'net_amount'                        => data_get($totals, 'service_amount'),
-
-                'metadata' => [
-                    'schedule_proposal_id' => (string) $proposal->id,
-                    'schedule_id'          => (string) $schedule->id,
-                ],
-            ]);
-
-            return compact('payment', 'schedules', 'cardId');
-        });
-
-        if (data_get($paymentData, 'existing')) {
-            $existingPayment = data_get($paymentData, 'existing');
-
-            $this->syncPaymentTargets($existingPayment);
-
-            return $existingPayment->fresh(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']);
-        }
+    $paymentData = DB::transaction(function () use ($servicePackage, $userId): array {
+      $servicePackage = ServicePackage::query()
+        ->lockForUpdate()
+        ->with(['client', 'provider', 'items.schedule.client', 'items.schedule.provider', 'items.schedule.customSchedule.serviceType'])
+        ->findOrFail($servicePackage->id);
+
+      if ($servicePackage->client?->user_id !== $userId) {
+        throw new AuthorizationException;
+      }
+
+      $schedules = $this->activePackageSchedules($servicePackage);
+
+      $this->validateServicePackageForPayment($servicePackage, $schedules);
+
+      if ($servicePackage->status !== ServicePackageStatusEnum::OPEN) {
+        throw new PaymentException;
+      }
+
+      $totals = $this->servicePackagePaymentTotals($schedules, 'pix');
+
+      $payment = Payment::create([
+        'schedule_id'              => null,
+        'service_package_id'       => $servicePackage->id,
+        'client_id'                => $servicePackage->client_id,
+        'provider_id'              => $servicePackage->provider_id,
+        'client_payment_method_id' => null,
+        'gateway_provider'         => 'pagarme',
+        'gateway_code'             => 'payment-' . (string) Str::uuid(),
+        'payment_method'           => 'pix',
+        'status'                   => PaymentStatusEnum::PENDING,
+        'gross_amount'             => data_get($totals, 'gross_amount'),
+        'gateway_fee_amount'       => 0,
+        'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
+        'net_amount'               => data_get($totals, 'gross_amount'),
+        'currency'                 => 'BRL',
+        'installments'             => 1,
+        'expires_at'               => Carbon::now()->addMinutes(30),
+
+        'metadata' => [
+          'service_package_id' => (string) $servicePackage->id,
+          'schedule_ids'       => $schedules->pluck('id')->map(fn($id) => (string) $id)->all(),
+          'service_amount'     => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
+          'platform_fee'       => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
+        ],
+      ]);
+
+      PaymentSplit::create([
+        'payment_id'                        => $payment->id,
+        'provider_id'                       => $servicePackage->provider_id,
+        'gateway_provider'                  => 'pagarme',
+        'gateway_transfer_target_reference' => $servicePackage->provider->recipient_id,
+        'gateway_transfer_target_label'     => 'recipient',
+        'status'                            => PaymentSplitStatusEnum::PENDING,
+        'gross_amount'                      => data_get($totals, 'service_amount'),
+        'gateway_fee_amount'                => 0,
+        'net_amount'                        => data_get($totals, 'service_amount'),
+
+        'metadata' => [
+          'service_package_id' => (string) $servicePackage->id,
+          'schedule_ids'       => $schedules->pluck('id')->map(fn($id) => (string) $id)->all(),
+        ],
+      ]);
+
+      return compact('payment', 'schedules');
+    });
+
+    /** @var Payment $payment */
+    $payment = data_get($paymentData, 'payment');
+
+    /** @var SupportCollection $schedules */
+    $schedules = data_get($paymentData, 'schedules');
+
+    try {
+      $schedules->first()->ensureCustomerPhone();
+
+      $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
+        payment: $payment,
+        schedules: $schedules,
+        paymentMethod: 'pix',
+        cardId: null,
+        options: [],
+      );
+    } catch (\Throwable $e) {
+      $this->failPayment($payment, $e->getMessage());
+
+      throw $e;
+    }
 
-        /** @var Payment $payment */
-        $payment = data_get($paymentData, 'payment');
+    $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
 
-        /** @var SupportCollection $schedules */
-        $schedules = data_get($paymentData, 'schedules');
+    $this->syncPaymentTargets($payment);
 
-        try {
-            $schedules->first()->ensureCustomerPhone(data_get($options, 'phone'));
+    return $payment->fresh(['client.user', 'provider.user', 'servicePackage.items.schedule']);
+  }
 
-            $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
-                payment:       $payment,
-                schedules:     $schedules,
-                paymentMethod: $paymentMethod,
-                cardId:        data_get($paymentData, 'cardId'),
-                options:       $options,
-            );
-        } catch (\Throwable $e) {
-            $this->failPayment($payment, $e->getMessage());
+  public function payScheduleProposal(
+    int    $proposalId,
+    string $paymentMethod,
+    ?int   $clientPaymentMethodId = null,
+    array  $options               = [],
+  ): Payment {
+    $userId = (int) Auth::id();
 
-            throw $e;
-        }
+    if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
+      throw new PaymentException;
+    }
 
-        $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
+    $paymentData = DB::transaction(function () use (
+      $proposalId,
+      $userId,
+      $paymentMethod,
+      $clientPaymentMethodId,
+      $options,
+    ): array {
+      $proposal = ScheduleProposal::query()
+        ->lockForUpdate()
+        ->with(['provider', 'schedule.client'])
+        ->findOrFail($proposalId);
+
+      $schedule = $proposal->schedule;
+      $provider = $proposal->provider;
+
+      if ($schedule?->client?->user_id !== $userId) {
+        throw new AuthorizationException;
+      }
+
+      $schedule->setAttribute('total_amount', app(CustomScheduleService::class)->resolveProposalAmount($schedule, $provider));
+
+      $schedules = collect([$schedule]);
+
+      $existingPayment = Payment::query()
+        ->where('metadata->schedule_proposal_id', (string) $proposal->id)
+        ->whereIn('status', [
+          PaymentStatusEnum::PENDING->value,
+          PaymentStatusEnum::PROCESSING->value,
+          PaymentStatusEnum::AUTHORIZED->value,
+          PaymentStatusEnum::PAID->value,
+        ])
+        ->latest('id')
+        ->first();
+
+      if (! $existingPayment && $schedule->provider_id) {
+        throw new PaymentException;
+      }
+
+      if ($existingPayment) {
+        if ($this->isExpiredPixPayment($existingPayment)) {
+          $existingPayment->forceFill([
+            'status'          => PaymentStatusEnum::FAILED,
+            'failed_at'       => now(),
+            'failure_message' => 'Pagamento Pix expirado.',
+          ])->save();
 
-        $this->syncPaymentTargets($payment);
+          PaymentSplit::query()
+            ->where('payment_id', $existingPayment->id)
+            ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+        } elseif ($this->isStaleIncompleteGatewayPayment($existingPayment)) {
+          if ($existingPayment->payment_method !== $paymentMethod) {
+            throw new PaymentException;
+          }
+
+          [, $cardId] = $this->resolveCard(
+            clientId: $schedule->client_id,
+            paymentMethod: $paymentMethod,
+            clientPaymentMethodId: $clientPaymentMethodId ?? $existingPayment->client_payment_method_id,
+            cardId: data_get($options, 'card_id'),
+          );
+
+          $this->servicePackagePaymentTotals($schedules, $paymentMethod);
+
+          return [
+            'payment'   => $existingPayment,
+            'schedules' => $schedules,
+            'cardId'    => $cardId,
+          ];
+        } else {
+          if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
+            throw new PaymentException;
+          }
 
-        if ($payment->status === PaymentStatusEnum::FAILED) {
-            throw new PaymentFailedException;
+          return ['existing' => $existingPayment];
         }
-
-        return $payment->fresh(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']);
+      }
+
+      [$clientPaymentMethod, $cardId] = $this->resolveCard(
+        clientId: $schedule->client_id,
+        paymentMethod: $paymentMethod,
+        clientPaymentMethodId: $clientPaymentMethodId,
+        cardId: data_get($options, 'card_id'),
+      );
+
+      $totals = $this->servicePackagePaymentTotals($schedules, $paymentMethod);
+
+      $payment = Payment::create([
+        'schedule_id'              => $schedule->id,
+        'service_package_id'       => null,
+        'client_id'                => $schedule->client_id,
+        'provider_id'              => $provider->id,
+        'client_payment_method_id' => $paymentMethod === 'credit_card' ? $clientPaymentMethod?->id : null,
+        'gateway_provider'         => 'pagarme',
+        'gateway_code'             => 'payment-' . (string) Str::uuid(),
+        'payment_method'           => $paymentMethod,
+        'status'                   => PaymentStatusEnum::PENDING,
+        'gross_amount'             => data_get($totals, 'gross_amount'),
+        'gateway_fee_amount'       => 0,
+        'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
+        'net_amount'               => data_get($totals, 'gross_amount'),
+        'currency'                 => 'BRL',
+        'installments'             => 1,
+        'expires_at'               => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
+
+        'metadata' => [
+          'schedule_proposal_id' => (string) $proposal->id,
+          'schedule_id'          => (string) $schedule->id,
+          'service_amount'       => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
+          'platform_fee'         => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
+        ],
+      ]);
+
+      PaymentSplit::create([
+        'payment_id'                        => $payment->id,
+        'provider_id'                       => $provider->id,
+        'gateway_provider'                  => 'pagarme',
+        'gateway_transfer_target_reference' => $provider->recipient_id,
+        'gateway_transfer_target_label'     => 'recipient',
+        'status'                            => PaymentSplitStatusEnum::PENDING,
+        'gross_amount'                      => data_get($totals, 'service_amount'),
+        'gateway_fee_amount'                => 0,
+        'net_amount'                        => data_get($totals, 'service_amount'),
+
+        'metadata' => [
+          'schedule_proposal_id' => (string) $proposal->id,
+          'schedule_id'          => (string) $schedule->id,
+        ],
+      ]);
+
+      return compact('payment', 'schedules', 'cardId');
+    });
+
+    if (data_get($paymentData, 'existing')) {
+      $existingPayment = data_get($paymentData, 'existing');
+
+      $this->syncPaymentTargets($existingPayment);
+
+      return $existingPayment->fresh(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']);
     }
 
-    public function getOrCreateScheduleProposalPixPayment(ScheduleProposal $proposal): Payment
-    {
-        $userId = (int) Auth::id();
+    /** @var Payment $payment */
+    $payment = data_get($paymentData, 'payment');
 
-        $proposal->loadMissing('schedule.client');
+    /** @var SupportCollection $schedules */
+    $schedules = data_get($paymentData, 'schedules');
 
-        $schedule = $proposal->schedule;
+    try {
+      $schedules->first()->ensureCustomerPhone(data_get($options, 'phone'));
 
-        if ($schedule?->client?->user_id !== $userId) {
-            throw new AuthorizationException;
-        }
+      $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
+        payment: $payment,
+        schedules: $schedules,
+        paymentMethod: $paymentMethod,
+        cardId: data_get($paymentData, 'cardId'),
+        options: $options,
+      );
+    } catch (\Throwable $e) {
+      $this->failPayment($payment, $e->getMessage());
 
-        $existingPayment = Payment::query()
-            ->where('metadata->schedule_proposal_id', (string) $proposal->id)
-            ->where('payment_method', 'pix')
-            ->whereIn('status', [
-                PaymentStatusEnum::PENDING->value,
-                PaymentStatusEnum::PROCESSING->value,
-                PaymentStatusEnum::AUTHORIZED->value,
-                PaymentStatusEnum::PAID->value,
-            ])
-            ->latest('id')
-            ->first();
-
-        if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) {
-            $existingPayment->forceFill([
-                'status'          => PaymentStatusEnum::FAILED,
-                'failed_at'       => Carbon::now(),
-                'failure_message' => 'Pagamento Pix expirado.',
-            ])->save();
-
-            PaymentSplit::query()
-                ->where('payment_id', $existingPayment->id)
-                ->update(['status' => PaymentSplitStatusEnum::FAILED]);
-
-            $existingPayment = null;
-        }
-
-        if ($existingPayment) {
-            if ($this->isIncompleteGatewayPayment($existingPayment)) {
-                $existingPayment->forceFill([
-                    'status'          => PaymentStatusEnum::FAILED,
-                    'failed_at'       => Carbon::now(),
-                    'failure_message' => 'Pagamento pendente sem retorno do gateway.',
-                ])->save();
-
-                PaymentSplit::query()
-                    ->where('payment_id', $existingPayment->id)
-                    ->update(['status' => PaymentSplitStatusEnum::FAILED]);
-            } else {
-                $this->syncPaymentTargets($existingPayment);
-
-                return $existingPayment;
-            }
-        }
-
-        $paymentData = DB::transaction(function () use ($proposal, $userId): array {
-            $proposal = ScheduleProposal::query()
-                ->lockForUpdate()
-                ->with(['provider', 'schedule.client'])
-                ->findOrFail($proposal->id);
-
-            $schedule = $proposal->schedule;
-            $provider = $proposal->provider;
-
-            if ($schedule?->client?->user_id !== $userId) {
-                throw new AuthorizationException;
-            }
-
-            if ($schedule->provider_id) {
-                throw new PaymentException;
-            }
-
-            $schedule->setAttribute('total_amount', app(CustomScheduleService::class)->resolveProposalAmount($schedule, $provider));
-
-            $schedules = collect([$schedule]);
-
-            $totals = $this->servicePackagePaymentTotals($schedules, 'pix');
-
-            $payment = Payment::create([
-                'schedule_id'              => $schedule->id,
-                'service_package_id'       => null,
-                'client_id'                => $schedule->client_id,
-                'provider_id'              => $provider->id,
-                'client_payment_method_id' => null,
-                'gateway_provider'         => 'pagarme',
-                'gateway_code'             => 'payment-'.(string) Str::uuid(),
-                'payment_method'           => 'pix',
-                'status'                   => PaymentStatusEnum::PENDING,
-                'gross_amount'             => data_get($totals, 'gross_amount'),
-                'gateway_fee_amount'       => 0,
-                'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
-                'net_amount'               => data_get($totals, 'gross_amount'),
-                'currency'                 => 'BRL',
-                'installments'             => 1,
-                'expires_at'               => Carbon::now()->addMinutes(30),
-
-                'metadata' => [
-                    'schedule_proposal_id' => (string) $proposal->id,
-                    'schedule_id'          => (string) $schedule->id,
-                    'service_amount'       => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
-                    'platform_fee'         => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
-                ],
-            ]);
-
-            PaymentSplit::create([
-                'payment_id'                        => $payment->id,
-                'provider_id'                       => $provider->id,
-                'gateway_provider'                  => 'pagarme',
-                'gateway_transfer_target_reference' => $provider->recipient_id,
-                'gateway_transfer_target_label'     => 'recipient',
-                'status'                            => PaymentSplitStatusEnum::PENDING,
-                'gross_amount'                      => data_get($totals, 'service_amount'),
-                'gateway_fee_amount'                => 0,
-                'net_amount'                        => data_get($totals, 'service_amount'),
-
-                'metadata' => [
-                    'schedule_proposal_id' => (string) $proposal->id,
-                    'schedule_id'          => (string) $schedule->id,
-                ],
-            ]);
-
-            return compact('payment', 'schedules');
-        });
-
-        /** @var Payment $payment */
-        $payment = data_get($paymentData, 'payment');
-
-        /** @var SupportCollection $schedules */
-        $schedules = data_get($paymentData, 'schedules');
-
-        try {
-            $schedules->first()->ensureCustomerPhone();
-
-            $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
-                payment:       $payment,
-                schedules:     $schedules,
-                paymentMethod: 'pix',
-                cardId:        null,
-                options:       [],
-            );
-        } catch (\Throwable $e) {
-            $this->failPayment($payment, $e->getMessage());
-
-            throw $e;
-        }
+      throw $e;
+    }
 
-        $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
+    $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
 
-        $this->syncPaymentTargets($payment);
+    $this->syncPaymentTargets($payment);
 
-        return $payment->fresh(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']);
+    if ($payment->status === PaymentStatusEnum::FAILED) {
+      throw new PaymentFailedException;
     }
 
-    //
+    return $payment->fresh(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']);
+  }
 
-    private function isExpiredPixPayment(Payment $payment): bool
-    {
-        if ($payment->payment_method !== 'pix') {
-            return false;
-        }
+  public function getOrCreateScheduleProposalPixPayment(ScheduleProposal $proposal): Payment
+  {
+    $userId = (int) Auth::id();
 
-        if ($payment->status === PaymentStatusEnum::PAID) {
-            return false;
-        }
+    $proposal->loadMissing('schedule.client');
 
-        return $payment->expires_at !== null
-            && $payment->expires_at->isPast();
-    }
+    $schedule = $proposal->schedule;
 
-    private function isIncompleteGatewayPayment(Payment $payment): bool
-    {
-        return $payment->status === PaymentStatusEnum::PENDING
-            && empty($payment->gateway_entity_reference)
-            && empty($payment->gateway_operation_reference)
-            && empty($payment->gateway_payload);
+    if ($schedule?->client?->user_id !== $userId) {
+      throw new AuthorizationException;
     }
 
-    private function isStaleIncompleteGatewayPayment(Payment $payment): bool
-    {
-        return $this->isIncompleteGatewayPayment($payment)
-            && ($payment->created_at?->lte(now()->subMinutes(5)) ?? false);
+    $existingPayment = Payment::query()
+      ->where('metadata->schedule_proposal_id', (string) $proposal->id)
+      ->where('payment_method', 'pix')
+      ->whereIn('status', [
+        PaymentStatusEnum::PENDING->value,
+        PaymentStatusEnum::PROCESSING->value,
+        PaymentStatusEnum::AUTHORIZED->value,
+        PaymentStatusEnum::PAID->value,
+      ])
+      ->latest('id')
+      ->first();
+
+    if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) {
+      $existingPayment->forceFill([
+        'status'          => PaymentStatusEnum::FAILED,
+        'failed_at'       => Carbon::now(),
+        'failure_message' => 'Pagamento Pix expirado.',
+      ])->save();
+
+      PaymentSplit::query()
+        ->where('payment_id', $existingPayment->id)
+        ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+
+      $existingPayment = null;
     }
 
-    public function syncScheduleStatusAfterPayment(Schedule $schedule, Payment $payment): void
-    {
-        if ($payment->status !== PaymentStatusEnum::PAID) {
-            return;
-        }
+    if ($existingPayment) {
+      if ($this->isIncompleteGatewayPayment($existingPayment)) {
+        $existingPayment->forceFill([
+          'status'          => PaymentStatusEnum::FAILED,
+          'failed_at'       => Carbon::now(),
+          'failure_message' => 'Pagamento pendente sem retorno do gateway.',
+        ])->save();
 
-        if ($schedule->status !== 'paid') {
-            $schedule->update(['status' => 'paid']);
-        }
+        PaymentSplit::query()
+          ->where('payment_id', $existingPayment->id)
+          ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+      } else {
+        $this->syncPaymentTargets($existingPayment);
 
-        $this->syncServicePackagesForSchedule($schedule);
+        return $existingPayment;
+      }
     }
 
-    public function syncPaymentTargets(Payment $payment): void
-    {
-        if ($payment->status !== PaymentStatusEnum::PAID) {
-            return;
-        }
+    $paymentData = DB::transaction(function () use ($proposal, $userId): array {
+      $proposal = ScheduleProposal::query()
+        ->lockForUpdate()
+        ->with(['provider', 'schedule.client'])
+        ->findOrFail($proposal->id);
+
+      $schedule = $proposal->schedule;
+      $provider = $proposal->provider;
+
+      if ($schedule?->client?->user_id !== $userId) {
+        throw new AuthorizationException;
+      }
+
+      if ($schedule->provider_id) {
+        throw new PaymentException;
+      }
+
+      $schedule->setAttribute('total_amount', app(CustomScheduleService::class)->resolveProposalAmount($schedule, $provider));
+
+      $schedules = collect([$schedule]);
+
+      $totals = $this->servicePackagePaymentTotals($schedules, 'pix');
+
+      $payment = Payment::create([
+        'schedule_id'              => $schedule->id,
+        'service_package_id'       => null,
+        'client_id'                => $schedule->client_id,
+        'provider_id'              => $provider->id,
+        'client_payment_method_id' => null,
+        'gateway_provider'         => 'pagarme',
+        'gateway_code'             => 'payment-' . (string) Str::uuid(),
+        'payment_method'           => 'pix',
+        'status'                   => PaymentStatusEnum::PENDING,
+        'gross_amount'             => data_get($totals, 'gross_amount'),
+        'gateway_fee_amount'       => 0,
+        'platform_fee_amount'      => data_get($totals, 'platform_fee_amount'),
+        'net_amount'               => data_get($totals, 'gross_amount'),
+        'currency'                 => 'BRL',
+        'installments'             => 1,
+        'expires_at'               => Carbon::now()->addMinutes(30),
+
+        'metadata' => [
+          'schedule_proposal_id' => (string) $proposal->id,
+          'schedule_id'          => (string) $schedule->id,
+          'service_amount'       => number_format(data_get($totals, 'service_amount'), 2, '.', ''),
+          'platform_fee'         => number_format(data_get($totals, 'platform_fee_amount'), 2, '.', ''),
+        ],
+      ]);
+
+      PaymentSplit::create([
+        'payment_id'                        => $payment->id,
+        'provider_id'                       => $provider->id,
+        'gateway_provider'                  => 'pagarme',
+        'gateway_transfer_target_reference' => $provider->recipient_id,
+        'gateway_transfer_target_label'     => 'recipient',
+        'status'                            => PaymentSplitStatusEnum::PENDING,
+        'gross_amount'                      => data_get($totals, 'service_amount'),
+        'gateway_fee_amount'                => 0,
+        'net_amount'                        => data_get($totals, 'service_amount'),
+
+        'metadata' => [
+          'schedule_proposal_id' => (string) $proposal->id,
+          'schedule_id'          => (string) $schedule->id,
+        ],
+      ]);
+
+      return compact('payment', 'schedules');
+    });
+
+    /** @var Payment $payment */
+    $payment = data_get($paymentData, 'payment');
+
+    /** @var SupportCollection $schedules */
+    $schedules = data_get($paymentData, 'schedules');
+
+    try {
+      $schedules->first()->ensureCustomerPhone();
+
+      $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
+        payment: $payment,
+        schedules: $schedules,
+        paymentMethod: 'pix',
+        cardId: null,
+        options: [],
+      );
+    } catch (\Throwable $e) {
+      $this->failPayment($payment, $e->getMessage());
+
+      throw $e;
+    }
 
-        if ($payment->service_package_id) {
-            $paidSchedules = DB::transaction(function () use ($payment): SupportCollection {
-                $servicePackage = ServicePackage::query()->lockForUpdate()->with('items')->find($payment->service_package_id);
+    $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
 
-                if (! $servicePackage) {
-                    return collect();
-                }
+    $this->syncPaymentTargets($payment);
 
-                $scheduleIds = $servicePackage->items->pluck('schedule_id')->filter()->unique();
+    return $payment->fresh(['client.user', 'provider.user', 'schedule', 'servicePackage.items.schedule']);
+  }
 
-                $schedulesToPay = Schedule::query()
-                    ->with('provider')
-                    ->whereIn('id', $scheduleIds)
-                    ->where('status', 'accepted')
-                    ->get();
+  //
 
-                Schedule::query()
-                    ->whereIn('id', $schedulesToPay->pluck('id'))
-                    ->where('status', 'accepted')
-                    ->update(['status' => 'paid']);
+  private function isExpiredPixPayment(Payment $payment): bool
+  {
+    if ($payment->payment_method !== 'pix') {
+      return false;
+    }
 
-                if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
-                    $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
-                }
+    if ($payment->status === PaymentStatusEnum::PAID) {
+      return false;
+    }
 
-                return $schedulesToPay->toBase();
-            });
+    return $payment->expires_at !== null
+      && $payment->expires_at->isPast();
+  }
+
+  private function isIncompleteGatewayPayment(Payment $payment): bool
+  {
+    return $payment->status === PaymentStatusEnum::PENDING
+      && empty($payment->gateway_entity_reference)
+      && empty($payment->gateway_operation_reference)
+      && empty($payment->gateway_payload);
+  }
+
+  private function isStaleIncompleteGatewayPayment(Payment $payment): bool
+  {
+    return $this->isIncompleteGatewayPayment($payment)
+      && ($payment->created_at?->lte(now()->subMinutes(5)) ?? false);
+  }
+
+  public function syncScheduleStatusAfterPayment(Schedule $schedule, Payment $payment): void
+  {
+    if ($payment->status !== PaymentStatusEnum::PAID) {
+      return;
+    }
 
-            $paidSchedules->each(fn (Schedule $schedule) => $this->notifyProviderAndScheduleStart($schedule));
+    if ($schedule->status !== 'paid') {
+      $schedule->update(['status' => 'paid']);
+    }
 
-            return;
-        }
+    $this->syncServicePackagesForSchedule($schedule);
+  }
 
-        $payment->loadMissing('schedule');
+  public function syncPaymentTargets(Payment $payment): void
+  {
+    if ($payment->status !== PaymentStatusEnum::PAID) {
+      return;
+    }
 
-        if (! $payment->schedule) {
-            return;
-        }
+    if ($payment->service_package_id) {
+      $paidSchedules = DB::transaction(function () use ($payment): SupportCollection {
+        $servicePackage = ServicePackage::query()->lockForUpdate()->with('items')->find($payment->service_package_id);
 
-        $proposalId = data_get($payment->metadata, 'schedule_proposal_id');
+        if (! $servicePackage) {
+          return collect();
+        }
 
-        if ($proposalId && ! $payment->schedule->provider_id) {
-            $servicePackage = app(CustomScheduleService::class)->acceptProposal((int) $proposalId);
+        $scheduleIds = $servicePackage->items->pluck('schedule_id')->filter()->unique();
 
-            $payment->update([
-                'schedule_id'        => null,
-                'service_package_id' => $servicePackage->id,
-            ]);
+        $schedulesToPay = Schedule::query()
+          ->with('provider')
+          ->whereIn('id', $scheduleIds)
+          ->where('status', 'accepted')
+          ->get();
 
-            $this->syncPaymentTargets($payment->fresh());
+        Schedule::query()
+          ->whereIn('id', $schedulesToPay->pluck('id'))
+          ->where('status', 'accepted')
+          ->update(['status' => 'paid']);
 
-            return;
+        if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
+          $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
         }
 
-        $this->syncScheduleStatusAfterPayment($payment->schedule, $payment);
-    }
+        return $schedulesToPay->toBase();
+      });
 
-    private function notifyProviderAndScheduleStart(Schedule $schedule): void
-    {
-        if ($schedule->provider_id && $schedule->provider) {
-            app(NotificationService::class)->create([
-                'title'       => __('notifications.payment_confirmed_title'),
-                'description' => __('notifications.payment_confirmed_description'),
-                'origin'      => 'schedule',
-                'origin_id'   => $schedule->id,
-                'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
-                'user_id'     => $schedule->provider->user_id,
-            ]);
-        }
+      $paidSchedules->each(fn(Schedule $schedule) => $this->notifyProviderAndScheduleStart($schedule));
 
-        $dateCleaned = Carbon::parse($schedule->date)->format('Y-m-d');
-
-        StartScheduleJob::dispatch($schedule->id)
-            ->delay(Carbon::parse($dateCleaned . ' ' . $schedule->start_time)->subHour());
+      return;
     }
 
-    private function syncServicePackagesForSchedule(Schedule $schedule): void
-    {
-        ServicePackage::query()
-            ->whereHas('items', fn ($query) => $query->where('schedule_id', $schedule->id))
-            ->with('items')
-            ->get()
-            ->each(fn (ServicePackage $servicePackage) => $this->syncServicePackageStatusAfterPayments($servicePackage));
+    $payment->loadMissing('schedule');
+
+    if (! $payment->schedule) {
+      return;
     }
 
-    private function syncServicePackageStatusAfterPayments(ServicePackage $servicePackage): void
-    {
-        $servicePackage->loadMissing('items');
+    $proposalId = data_get($payment->metadata, 'schedule_proposal_id');
 
-        $scheduleIds = $servicePackage->items
-            ->pluck('schedule_id')
-            ->filter()
-            ->unique()
-            ->values();
+    if ($proposalId && ! $payment->schedule->provider_id) {
+      $servicePackage = app(CustomScheduleService::class)->acceptProposal((int) $proposalId);
 
-        if ($scheduleIds->isEmpty()) {
-            return;
-        }
+      $payment->update([
+        'schedule_id'        => null,
+        'service_package_id' => $servicePackage->id,
+      ]);
 
-        $paidSchedulesCount = Schedule::query()
-            ->whereIn('id', $scheduleIds)
-            ->where('status', 'paid')
-            ->count();
+      $this->syncPaymentTargets($payment->fresh());
 
-        if ($paidSchedulesCount !== $scheduleIds->count()) {
-            return;
-        }
-
-        if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
-            $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
-        }
+      return;
     }
 
-    private function activePackageSchedules(ServicePackage $servicePackage): SupportCollection
-    {
-        return $servicePackage->items
-            ->pluck('schedule')
-            ->filter()
-            ->reject(fn (Schedule $schedule) => in_array($schedule->status, ['cancelled', 'rejected'], true))
-            ->values();
+    $this->syncScheduleStatusAfterPayment($payment->schedule, $payment);
+  }
+
+  private function notifyProviderAndScheduleStart(Schedule $schedule): void
+  {
+    if ($schedule->provider_id && $schedule->provider) {
+      app(NotificationService::class)->create([
+        'title'       => __('notifications.payment_confirmed_title'),
+        'description' => __('notifications.payment_confirmed_description'),
+        'origin'      => 'schedule',
+        'origin_id'   => $schedule->id,
+        'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
+        'user_id'     => $schedule->provider->user_id,
+      ]);
     }
 
-    private function validateServicePackageForPayment(ServicePackage $servicePackage, SupportCollection $schedules): void
-    {
-        if (! in_array($servicePackage->status, [ServicePackageStatusEnum::OPEN, ServicePackageStatusEnum::PAID], true)) {
-            throw new PaymentException;
-        }
-
-        if ($schedules->isEmpty()) {
-            throw new PaymentException;
-        }
+    $dateCleaned = Carbon::parse($schedule->date)->format('Y-m-d');
+
+    StartScheduleJob::dispatch($schedule->id)
+      ->delay(Carbon::parse($dateCleaned . ' ' . $schedule->start_time)->subHour());
+  }
+
+  private function syncServicePackagesForSchedule(Schedule $schedule): void
+  {
+    ServicePackage::query()
+      ->whereHas('items', fn($query) => $query->where('schedule_id', $schedule->id))
+      ->with('items')
+      ->get()
+      ->each(fn(ServicePackage $servicePackage) => $this->syncServicePackageStatusAfterPayments($servicePackage));
+  }
+
+  private function syncServicePackageStatusAfterPayments(ServicePackage $servicePackage): void
+  {
+    $servicePackage->loadMissing('items');
+
+    $scheduleIds = $servicePackage->items
+      ->pluck('schedule_id')
+      ->filter()
+      ->unique()
+      ->values();
+
+    if ($scheduleIds->isEmpty()) {
+      return;
+    }
 
-        $providerIds = $schedules->pluck('provider_id')->filter()->unique()->values();
-        $clientIds   = $schedules->pluck('client_id')->unique()->values();
+    $paidSchedulesCount = Schedule::query()
+      ->whereIn('id', $scheduleIds)
+      ->where('status', 'paid')
+      ->count();
 
-        if (! $servicePackage->provider_id || $providerIds->count() !== 1 || (int) $providerIds->first() !== $servicePackage->provider_id) {
-            throw new PaymentException;
-        }
-
-        if ($clientIds->count() !== 1 || (int) $clientIds->first() !== $servicePackage->client_id) {
-            throw new PaymentException;
-        }
+    if ($paidSchedulesCount !== $scheduleIds->count()) {
+      return;
+    }
 
-        if (empty($servicePackage->provider?->recipient_id)) {
-            throw new PaymentException;
-        }
+    if ($servicePackage->status !== ServicePackageStatusEnum::PAID) {
+      $servicePackage->update(['status' => ServicePackageStatusEnum::PAID->value]);
+    }
+  }
+
+  private function activePackageSchedules(ServicePackage $servicePackage): SupportCollection
+  {
+    return $servicePackage->items
+      ->pluck('schedule')
+      ->filter()
+      ->reject(fn(Schedule $schedule) => in_array($schedule->status, ['cancelled', 'rejected'], true))
+      ->values();
+  }
+
+  private function validateServicePackageForPayment(ServicePackage $servicePackage, SupportCollection $schedules): void
+  {
+    if (! in_array($servicePackage->status, [ServicePackageStatusEnum::OPEN, ServicePackageStatusEnum::PAID], true)) {
+      throw new PaymentException;
+    }
 
-        foreach ($schedules as $schedule) {
-            $expectedStatus = $servicePackage->status === ServicePackageStatusEnum::PAID ? 'paid' : 'accepted';
+    if ($schedules->isEmpty()) {
+      throw new PaymentException;
+    }
 
-            if ($schedule->status !== $expectedStatus) {
-                throw new PaymentException;
-            }
+    $providerIds = $schedules->pluck('provider_id')->filter()->unique()->values();
+    $clientIds   = $schedules->pluck('client_id')->unique()->values();
 
-            if ((float) $schedule->total_amount <= 0) {
-                throw new PaymentException;
-            }
-        }
+    if (! $servicePackage->provider_id || $providerIds->count() !== 1 || (int) $providerIds->first() !== $servicePackage->provider_id) {
+      throw new PaymentException;
     }
 
-    private function resolveCard(
-        int $clientId,
-        string $paymentMethod,
-        ?int $clientPaymentMethodId,
-        ?string $cardId,
-    ): array {
-        if ($paymentMethod !== 'credit_card') {
-            return [null, null];
-        }
-
-        if (! $clientPaymentMethodId && empty($cardId)) {
-            throw new PaymentException;
-        }
+    if ($clientIds->count() !== 1 || (int) $clientIds->first() !== $servicePackage->client_id) {
+      throw new PaymentException;
+    }
 
-        $clientPaymentMethod = $clientPaymentMethodId
-            ? ClientPaymentMethod::query()
-                ->where('client_id', $clientId)
-                ->where('id', $clientPaymentMethodId)
-                ->where('is_active', true)
-                ->first()
-            : null;
+    if (empty($servicePackage->provider?->recipient_id)) {
+      throw new PaymentException;
+    }
 
-        if ($clientPaymentMethodId && ! $clientPaymentMethod) {
-            throw new PaymentException;
-        }
+    foreach ($schedules as $schedule) {
+      $expectedStatus = $servicePackage->status === ServicePackageStatusEnum::PAID ? 'paid' : 'accepted';
 
-        $cardId = $cardId ?: $clientPaymentMethod?->gateway_card_id;
+      if ($schedule->status !== $expectedStatus) {
+        throw new PaymentException;
+      }
 
-        if (empty($cardId)) {
-            throw new PaymentException;
-        }
+      if ((float) $schedule->total_amount <= 0) {
+        throw new PaymentException;
+      }
+    }
+  }
+
+  private function resolveCard(
+    int $clientId,
+    string $paymentMethod,
+    ?int $clientPaymentMethodId,
+    ?string $cardId,
+  ): array {
+    if ($paymentMethod !== 'credit_card') {
+      return [null, null];
+    }
 
-        return [$clientPaymentMethod, $cardId];
+    if (! $clientPaymentMethodId && empty($cardId)) {
+      throw new PaymentException;
     }
 
-    private function servicePackagePaymentTotals(SupportCollection $schedules, string $paymentMethod): array
-    {
-        return $schedules->reduce(function (array $totals, Schedule $schedule) use ($paymentMethod): array {
-            $amounts = $this->pagarmePaymentService->calculatePaymentAmounts(
-                serviceAmount: (float) $schedule->total_amount,
-                paymentMethod: $paymentMethod,
-                schedule:      $schedule,
-            );
-
-            $schedule->setAttribute('payment_gross_amount', data_get($amounts, 'gross_amount'));
-
-            return [
-                'service_amount'      => round(data_get($totals, 'service_amount') + data_get($amounts, 'service_amount'), 2),
-                'platform_fee_amount' => round(data_get($totals, 'platform_fee_amount') + data_get($amounts, 'platform_fee_amount'), 2),
-                'gross_amount'        => round(data_get($totals, 'gross_amount') + data_get($amounts, 'gross_amount'), 2),
-            ];
-        }, [
-            'service_amount'      => 0.0,
-            'platform_fee_amount' => 0.0,
-            'gross_amount'        => 0.0,
-        ]);
+    $clientPaymentMethod = $clientPaymentMethodId
+      ? ClientPaymentMethod::query()
+      ->where('client_id', $clientId)
+      ->where('id', $clientPaymentMethodId)
+      ->where('is_active', true)
+      ->first()
+      : null;
+
+    if ($clientPaymentMethodId && ! $clientPaymentMethod) {
+      throw new PaymentException;
     }
 
-    private function failPayment(Payment $payment, string $message): void
-    {
-        $payment->forceFill([
-            'status'          => PaymentStatusEnum::FAILED,
-            'failed_at'       => now(),
-            'failure_message' => $message,
-        ])->save();
+    $cardId = $cardId ?: $clientPaymentMethod?->gateway_card_id;
 
-        PaymentSplit::query()
-            ->where('payment_id', $payment->id)
-            ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+    if (empty($cardId)) {
+      throw new PaymentException;
     }
+
+    return [$clientPaymentMethod, $cardId];
+  }
+
+  private function servicePackagePaymentTotals(SupportCollection $schedules, string $paymentMethod): array
+  {
+    return $schedules->reduce(function (array $totals, Schedule $schedule) use ($paymentMethod): array {
+      $amounts = $this->pagarmePaymentService->calculatePaymentAmounts(
+        serviceAmount: (float) $schedule->total_amount,
+        paymentMethod: $paymentMethod,
+        schedule: $schedule,
+      );
+
+      $schedule->setAttribute('payment_gross_amount', data_get($amounts, 'gross_amount'));
+
+      return [
+        'service_amount'      => round(data_get($totals, 'service_amount') + data_get($amounts, 'service_amount'), 2),
+        'platform_fee_amount' => round(data_get($totals, 'platform_fee_amount') + data_get($amounts, 'platform_fee_amount'), 2),
+        'gross_amount'        => round(data_get($totals, 'gross_amount') + data_get($amounts, 'gross_amount'), 2),
+      ];
+    }, [
+      'service_amount'      => 0.0,
+      'platform_fee_amount' => 0.0,
+      'gross_amount'        => 0.0,
+    ]);
+  }
+
+  private function failPayment(Payment $payment, string $message): void
+  {
+    $payment->forceFill([
+      'status'          => PaymentStatusEnum::FAILED,
+      'failed_at'       => now(),
+      'failure_message' => $message,
+    ])->save();
+
+    PaymentSplit::query()
+      ->where('payment_id', $payment->id)
+      ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+  }
 }

+ 1 - 1
app/Services/ProviderCalendarService.php

@@ -82,7 +82,7 @@ class ProviderCalendarService
 
         $upcomingSchedules = Schedule::with('address:district,address,number,source_id,source,id')
             ->where('schedules.provider_id', $provider->id)
-            ->whereIn('schedules.status', ['pending', 'accepted', 'paid', 'started'])
+            ->whereIn('schedules.status', ['paid', 'started'])
             ->whereDate('schedules.date', '>=', now()->toDateString())
             ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
             ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')

+ 11 - 2
app/Services/ProviderWithdrawalService.php

@@ -94,11 +94,20 @@ class ProviderWithdrawalService
 
     //
 
-    public function getPaymentSplits(Provider $provider): Collection
+    public function getPaymentSplits(Provider $provider, ?string $paymentStatus = null): Collection
     {
         return PaymentSplit::query()
             ->where('provider_id', $provider->id)
-            ->with(['payment.schedule.client.user', 'payment.servicePackage.items.schedule.client.user'])
+            ->when(
+                $paymentStatus,
+                fn ($query) => $query->whereHas('payment', fn ($q) => $q->where('status', $paymentStatus))
+            )
+            ->with([
+                'payment.schedule.client.user',
+                'payment.schedule.client.profileMedia',
+                'payment.servicePackage.items.schedule.client.user',
+                'payment.servicePackage.items.schedule.client.profileMedia',
+            ])
             ->orderBy('created_at', 'desc')
             ->get();
     }

+ 4 - 0
app/Services/PushNotificationService.php

@@ -22,6 +22,10 @@ class PushNotificationService
      */
     public function sendToUser(User $user, BasePushNotification $notification): void
     {
+        if (! array_key_exists('push_notifications_enabled', $user->getAttributes())) {
+            $user = $user->fresh() ?? $user;
+        }
+        
         if (! $user->push_notifications_enabled) {
             return;
         }

+ 29 - 0
app/Services/ScheduleService.php

@@ -13,6 +13,7 @@ use App\Models\ServicePackage;
 use App\Rules\ScheduleBusinessRules;
 use App\Services\NotificationService;
 use App\Services\PushNotificationService;
+use App\Notifications\Push\Cliente\Agendamento\PrestadorAceitouPush;
 use App\Notifications\Push\Prestador\Agendamento\NewPushRequest;
 use Carbon\Carbon;
 use Illuminate\Support\Facades\Auth;
@@ -202,6 +203,8 @@ class ScheduleService
                                 'user_id'     => $schedule->client->user_id,
                             ]);
 
+                            $this->sendProviderAcceptedPush($schedule);
+
                             break;
 
                         case UserTypeEnum::CLIENT:
@@ -568,6 +571,32 @@ class ScheduleService
 
     //
 
+    private function sendProviderAcceptedPush(Schedule $schedule): void
+    {
+        $user = $schedule->client->user;
+
+        if (! $user) {
+            Log::warning('Push de aceite ignorada: cliente sem usuário', [
+                'schedule_id' => $schedule->id,
+            ]);
+
+            return;
+        }
+
+        try {
+            app(PushNotificationService::class)->sendToUser(
+                $user,
+                new PrestadorAceitouPush($schedule->provider->user->name)
+            );
+        } catch (\Throwable $exception) {
+            Log::error('Falha ao enviar push de aceite do prestador', [
+                'schedule_id' => $schedule->id,
+                'user_id'     => $user->id,
+                'error'       => $exception->getMessage(),
+            ]);
+        }
+    }
+
     private function calculateAmount(Provider $provider, string $periodType): float
     {
         $hourlyRates = [

+ 22 - 0
database/migrations/2026_08_28_141841_add_code_reminder_sent_at_to_schedules_table.php

@@ -0,0 +1,22 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('schedules', function (Blueprint $table) {
+            $table->timestamp('code_reminder_sent_at')->nullable();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('schedules', function (Blueprint $table) {
+            $table->dropColumn('code_reminder_sent_at');
+        });
+    }
+};

+ 42 - 0
database/migrations/2026_08_31_120000_add_share_code_to_providers_table.php

@@ -0,0 +1,42 @@
+<?php
+
+use App\Models\Provider;
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->string('share_code', 6)->nullable()->after('recipient_code');
+
+            $table->unique('share_code');
+        });
+
+        Provider::withTrashed()
+            ->whereNull('share_code')
+            ->orderBy('id')
+            ->chunkById(200, function ($providers) {
+                foreach ($providers as $provider) {
+                    $provider->forceFill(['share_code' => Provider::generateUniqueShareCode()])->saveQuietly();
+                }
+            });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->dropUnique(['share_code']);
+
+            $table->dropColumn('share_code');
+        });
+    }
+};

+ 4 - 0
lang/en/messages.php

@@ -83,4 +83,8 @@ return [
     'service_package_schedule_already_assigned'     => 'The appointment already belongs to another service package.',
     'service_package_schedule_has_active_payment'   => 'The appointment already has a payment and cannot be added to the service package.',
     'gender_only_for_providers'                     => 'Gender can only be changed for provider users.',
+    'favorite_code_added'                            => 'Cleaner added to your favorites!',
+    'favorite_code_not_found'                        => 'Code not found. Check the code and try again.',
+    'favorite_code_already_favorite'                 => 'This cleaner is already in your favorites.',
+    'favorite_code_blocked'                          => 'This cleaner cannot be added to your favorites.',
 ];

+ 2 - 0
lang/en/requests.php

@@ -25,6 +25,8 @@ return [
         'provider_not_found' => 'Provider not found.',
         'already_favorite'   => 'This provider is already a favorite.',
         'notes_max'          => 'The notes may not exceed 1000 characters.',
+        'code_required'      => 'The code is required.',
+        'code_invalid'       => 'Invalid code. The code has 6 characters.',
     ],
 
     'custom_schedule_available' => [

+ 4 - 0
lang/es/messages.php

@@ -83,4 +83,8 @@ return [
     'service_package_schedule_already_assigned'     => 'El servicio programado ya pertenece a otro paquete.',
     'service_package_schedule_has_active_payment'   => 'El servicio programado ya tiene un pago y no puede añadirse al paquete.',
     'gender_only_for_providers'                     => 'El género solo puede modificarse para usuarios prestadores.',
+    'favorite_code_added'                            => '¡Limpiador añadido a tus favoritos!',
+    'favorite_code_not_found'                        => 'Código no encontrado. Verifica el código e inténtalo de nuevo.',
+    'favorite_code_already_favorite'                 => 'Este limpiador ya está en tus favoritos.',
+    'favorite_code_blocked'                          => 'No es posible añadir este limpiador a tus favoritos.',
 ];

+ 2 - 0
lang/es/requests.php

@@ -25,6 +25,8 @@ return [
         'provider_not_found' => 'Prestador no encontrado.',
         'already_favorite'   => 'Este prestador ya está en favoritos.',
         'notes_max'          => 'Las observaciones no pueden superar los 1000 caracteres.',
+        'code_required'      => 'El código es obligatorio.',
+        'code_invalid'       => 'Código inválido. El código tiene 6 caracteres.',
     ],
 
     'custom_schedule_available' => [

+ 4 - 0
lang/pt/messages.php

@@ -84,4 +84,8 @@ return [
     'service_package_schedule_has_active_payment'   => 'O agendamento já possui um pagamento e não pode ser adicionado ao pacote.',
     'gender_only_for_providers'                     => 'O gênero só pode ser alterado para usuários prestadores.',
     'schedule_belongs_to_package_use_package_endpoint' => 'Este agendamento pertence a um pacote de serviços. Use o endpoint de pacotes para aceitá-lo ou recusá-lo.',
+    'favorite_code_added'                            => 'Diarista adicionado aos seus favoritos!',
+    'favorite_code_not_found'                        => 'Código não encontrado. Confira o código e tente novamente.',
+    'favorite_code_already_favorite'                 => 'Este diarista já está nos seus favoritos.',
+    'favorite_code_blocked'                          => 'Não é possível favoritar este diarista.',
 ];

+ 2 - 0
lang/pt/requests.php

@@ -25,6 +25,8 @@ return [
         'provider_not_found' => 'Prestador não encontrado.',
         'already_favorite'   => 'Este prestador já está favoritado.',
         'notes_max'          => 'As observações não podem exceder 1000 caracteres.',
+        'code_required'      => 'O código é obrigatório.',
+        'code_invalid'       => 'Código inválido. O código tem 6 caracteres.',
     ],
 
     'custom_schedule_available' => [

+ 1 - 0
routes/authRoutes/client_favorite_provider.php

@@ -7,5 +7,6 @@ Route::get('/client/favorite-providers/{clientId}',  [ClientFavoriteProviderCont
 Route::get('/client/favorited-providers/{clientId}', [ClientFavoriteProviderController::class, 'getFavoritedProviders'])->middleware('permission:config.client_favorite_provider,view');
 Route::get('/client/favorite-provider/{id}',         [ClientFavoriteProviderController::class, 'show'])->middleware('permission:config.client_favorite_provider,view');
 Route::post('/client/favorite-provider',             [ClientFavoriteProviderController::class, 'store'])->middleware('permission:config.client_favorite_provider,add');
+Route::post('/client/favorite-provider/by-code',     [ClientFavoriteProviderController::class, 'storeByCode'])->middleware(['permission:config.client_favorite_provider,add', 'throttle:10,1']);
 Route::put('/client/favorite-provider/{id}',         [ClientFavoriteProviderController::class, 'update'])->middleware('permission:config.client_favorite_provider,edit');
 Route::delete('/client/favorite-provider/{id}',      [ClientFavoriteProviderController::class, 'destroy'])->middleware('permission:config.client_favorite_provider,delete');