4 Коммиты 9db741f703 ... 3f8c02469a

Автор SHA1 Сообщение Дата
  Gustavo Zanatta 3f8c02469a fix prestadores para nao aparecer oportunidades se estao sem conta bancaria 3 дней назад
  zntt 1edd6ae96f Merge branch 'fix/diaria-kay-correções-backend' of Softpar/sfp_api_laravel_diarista into development 3 дней назад
  kayo henrique 35f60bda93 fix: :bug: fix(correções backend) Foi criado o disparo da push notification referente a 1 hora antes do iniciio da diaria 3 дней назад
  kayo henrique 3cfcfacd28 fix: :bug: fix(correções backend) Foi ajustado o envio de notificação por push quando e cancelado a diaria 4 дней назад

+ 52 - 0
app/Jobs/ScheduleStartingSoonJob.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\Schedule;
+use App\Services\ScheduleService;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
+
+class ScheduleStartingSoonJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public function __construct(
+        public int $scheduleId
+    ) {}
+
+    public function handle(): void
+    {
+        try {
+            $schedule = Schedule::with([
+                'client.user',
+                'provider.user',
+            ])->find($this->scheduleId);
+
+            if (! $schedule) {
+                return;
+            }
+
+            // Só envia a notificação se o agendamento ainda estiver pago.
+            if ($schedule->status !== 'paid') {
+                return;
+            }
+
+            app(ScheduleService::class)
+                ->sendScheduleStartingSoonPushes($schedule);
+
+        } catch (\Throwable $e) {
+            Log::error(
+                'Erro ao enviar push de agendamento próximo.',
+                [
+                    'schedule_id' => $this->scheduleId,
+                    'error'       => $e->getMessage(),
+                ]
+            );
+        }
+    }
+}

+ 55 - 0
app/Notifications/Push/Cliente/Agendamento/AgendamentoProximoPrestadorPush.php

@@ -0,0 +1,55 @@
+<?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;
+
+class AgendamentoProximoPrestadorPush extends BasePushNotification
+{
+    public function __construct(
+        private readonly string $providerName,
+    ) {}
+
+    public function label(): string
+    {
+        return 'schedule_starting_soon';
+    }
+
+    public function title(): string
+    {
+        return 'Agendamento próximo!';
+    }
+
+    public function body(): string
+    {
+        return "Seu agendamento com {$this->providerName} começa em 1 hora.";
+    }
+
+    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;
+    }
+}

+ 55 - 0
app/Notifications/Push/Prestador/Agendamento/AgendamentoProximoClientePush.php

@@ -0,0 +1,55 @@
+<?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;
+
+class AgendamentoProximoClientePush extends BasePushNotification
+{
+    public function __construct(
+        private readonly string $clientName,
+    ) {}
+
+    public function label(): string
+    {
+        return 'schedule_starting_soon';
+    }
+
+    public function title(): string
+    {
+        return 'Agendamento próximo!';
+    }
+
+    public function body(): string
+    {
+        return "Seu agendamento com {$this->clientName} começa em 1 hora.";
+    }
+
+    public function target(): PushNotificationTargetEnum
+    {
+        return PushNotificationTargetEnum::PRESTADOR;
+    }
+
+    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;
+    }
+}

+ 17 - 0
app/Services/CustomScheduleService.php

@@ -271,6 +271,10 @@ class CustomScheduleService
     {
         $provider = Provider::find($providerId);
 
+        if (! $provider || ! self::providerHasActivePrimaryBankAccount($providerId)) {
+            return collect();
+        }
+
         $providerAddress = Address::where('source', 'provider')
             ->where('source_id', $providerId)
             ->orderBy('is_primary', 'desc')
@@ -458,6 +462,10 @@ class CustomScheduleService
     {
         $schedule = Schedule::findOrFail($scheduleId);
 
+        if (! self::providerHasActivePrimaryBankAccount($providerId)) {
+            throw new \Exception(__('messages.provider_missing_bank_account'));
+        }
+
         if ($schedule->provider_id) {
             throw new \Exception(__('validation.custom.opportunity.already_assigned'));
         }
@@ -899,6 +907,14 @@ class CustomScheduleService
     /**
      * @return Collection<int, int>
      */
+    private static function providerHasActivePrimaryBankAccount($providerId): bool
+    {
+        return Provider::query()
+            ->where('providers.id', $providerId)
+            ->whereExists(Provider::hasActivePrimaryBankAccount())
+            ->exists();
+    }
+
     private function getCandidateProviderIdsForOpportunity(Schedule $schedule): Collection
     {
         $address = Address::find($schedule->address_id);
@@ -959,6 +975,7 @@ class CustomScheduleService
         return Provider::query()
             ->join($providerAddressSubquery, 'provider_address.source_id', '=', 'providers.id')
             ->where('providers.approval_status', ApprovalStatusEnum::ACCEPTED->value)
+            ->whereExists(Provider::hasActivePrimaryBankAccount())
 
             ->where(function ($query) use ($cityId, $lat, $lng) {
                 if ($cityId !== null) {

+ 117 - 45
app/Services/ScheduleService.php

@@ -10,6 +10,7 @@ use App\Enums\ServicePackageStatusEnum;
 use App\Enums\UserTypeEnum;
 use App\Enums\NotificationTypeEnum;
 use App\Jobs\StartScheduleJob;
+use App\Jobs\ScheduleStartingSoonJob;
 use App\Models\Provider;
 use App\Models\Schedule;
 use App\Models\ServicePackage;
@@ -20,6 +21,8 @@ use App\Notifications\Push\Cliente\Agendamento\PrestadorAceitouPush;
 use App\Notifications\Push\Cliente\Agendamento\PrestadorRecusouPush;
 use App\Notifications\Push\Prestador\Agendamento\ClienteAceitouPush;
 use App\Notifications\Push\Prestador\Pagamento\ClienteEfetuouPagamentoPush;
+use App\Notifications\Push\Cliente\Agendamento\AgendamentoProximoPrestadorPush;
+use App\Notifications\Push\Prestador\Agendamento\AgendamentoProximoClientePush;
 use App\Notifications\Push\Cliente\Agendamento\PrestadorCancelouPush;
 use App\Notifications\Push\Prestador\Agendamento\ClienteCancelouPush;
 use App\Notifications\Push\Prestador\Agendamento\NewPushRequest;
@@ -165,7 +168,7 @@ class ScheduleService
     }
 
     //
-
+    //
     public function updateStatus($id, string $status, bool $fromPackage = false)
     {
         try {
@@ -251,7 +254,7 @@ class ScheduleService
                     }
 
                     break;
-
+                //tem que chamar o status cancel por causa da regra de push
                 case 'cancelled':
                     $notificationService = app(NotificationService::class);
 
@@ -265,7 +268,7 @@ class ScheduleService
                                 'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_CANCELLED->value,
                                 'user_id'     => $schedule->provider->user_id,
                             ]);
-                            $this->sendProviderCancelledPush($schedule);
+                            $this->sendClientCancelledPush($schedule);
 
                             break;
 
@@ -278,7 +281,8 @@ class ScheduleService
                                 'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_CANCELLED->value,
                                 'user_id'     => $schedule->client->user_id,
                             ]);
-                            $this->sendClientCancelledPush($schedule);
+
+                            $this->sendProviderCancelledPush($schedule);
 
                             break;
 
@@ -334,7 +338,7 @@ class ScheduleService
                 case 'paid':
                     $notificationService = app(NotificationService::class);
                     if ($schedule->provider_id) {
-                
+
                         $notificationService->create([
                             'title'       => __('notifications.payment_confirmed_title'),
                             'description' => __('notifications.payment_confirmed_description'),
@@ -349,12 +353,34 @@ class ScheduleService
                     $date_cleaned = Carbon::parse($schedule->date)
                         ->format('Y-m-d');
 
-                    $date_time_dispatch = Carbon::parse(
+                    $start_date_time = Carbon::parse(
                         $date_cleaned . ' ' . $schedule->start_time
-                    )->subHour();
+                    );
+
+                    // =====================================================
+                    // ScheduleStartingSoonJob
+                    // =====================================================
+
+                    // TESTE LOCAL: dispara 15 segundos depois do pagamento
+                    ScheduleStartingSoonJob::dispatch($schedule->id)
+                        ->delay(now()->addSeconds(15));
+
+                    /*
+                        PRODUÇÃO: dispara 1 hora antes do início
+
+                        $notification_date_time = $start_date_time->copy()->subHour();
 
+                        ScheduleStartingSoonJob::dispatch($schedule->id)
+                            ->delay($notification_date_time);
+                        */
+
+                    // =====================================================
+                    // StartScheduleJob
+                    // =====================================================
+
+                    // Aqui continua sendo o horário REAL de início
                     StartScheduleJob::dispatch($schedule->id)
-                        ->delay($date_time_dispatch);
+                        ->delay($start_date_time);
 
                     break;
 
@@ -429,42 +455,42 @@ class ScheduleService
 
     public function getClientProviderBlocks(int $clientId, int $providerId): array
     {
-      $weekStart = Carbon::today()->startOfWeek(Carbon::SUNDAY)->format('Y-m-d');
-
-      $schedules = Schedule::where('client_id', $clientId)
-          ->where('provider_id', $providerId)
-          ->whereNotIn('status', self::EXCLUDED_STATUSES)
-          ->whereDate('date', '>=', $weekStart)
-          ->orderBy('date')
-          ->orderBy('start_time')
-          ->get(['id', 'date', 'start_time', 'end_time', 'status']);
-
-      $existingSchedules = $schedules->map(function ($schedule) {
-          return [
-              'id'         => $schedule->id,
-              'date'       => Carbon::parse($schedule->date)->format('Y-m-d'),
-              'start_time' => $schedule->start_time,
-              'end_time'   => $schedule->end_time,
-              'status'     => $schedule->status,
-          ];
-      })->values();
-
-      $fullyBlockedWeeks = $schedules
-          ->groupBy(function ($schedule) {
-              return Carbon::parse($schedule->date)
-                  ->startOfWeek(Carbon::SUNDAY)
-                  ->format('Y-m-d');
-          })
-          ->filter(function ($weekSchedules) {
-              return $weekSchedules->count() >= 2;
-          })
-          ->keys()
-          ->values();
-
-      return [
-          'existing_schedules'  => $existingSchedules,
-          'fully_blocked_weeks' => $fullyBlockedWeeks,
-      ];
+        $weekStart = Carbon::today()->startOfWeek(Carbon::SUNDAY)->format('Y-m-d');
+
+        $schedules = Schedule::where('client_id', $clientId)
+            ->where('provider_id', $providerId)
+            ->whereNotIn('status', self::EXCLUDED_STATUSES)
+            ->whereDate('date', '>=', $weekStart)
+            ->orderBy('date')
+            ->orderBy('start_time')
+            ->get(['id', 'date', 'start_time', 'end_time', 'status']);
+
+        $existingSchedules = $schedules->map(function ($schedule) {
+            return [
+                'id'         => $schedule->id,
+                'date'       => Carbon::parse($schedule->date)->format('Y-m-d'),
+                'start_time' => $schedule->start_time,
+                'end_time'   => $schedule->end_time,
+                'status'     => $schedule->status,
+            ];
+        })->values();
+
+        $fullyBlockedWeeks = $schedules
+            ->groupBy(function ($schedule) {
+                return Carbon::parse($schedule->date)
+                    ->startOfWeek(Carbon::SUNDAY)
+                    ->format('Y-m-d');
+            })
+            ->filter(function ($weekSchedules) {
+                return $weekSchedules->count() >= 2;
+            })
+            ->keys()
+            ->values();
+
+        return [
+            'existing_schedules'  => $existingSchedules,
+            'fully_blocked_weeks' => $fullyBlockedWeeks,
+        ];
     }
 
     public function getFinished()
@@ -564,12 +590,12 @@ class ScheduleService
             $cancelled_by = Auth::user()->type;
 
             $schedule->update([
-                'status'       => 'cancelled',
                 'cancel_text'  => $cancelText,
                 'cancelled_by' => $cancelled_by,
             ]);
 
             $this->cascadeCancelServicePackages($schedule, $cancelText, $cancelled_by);
+            $this->updateStatus($id, 'cancelled');
 
             $actor = Auth::user()?->type;
 
@@ -810,6 +836,52 @@ class ScheduleService
         }
     }
 
+    public function sendScheduleStartingSoonPushes(Schedule $schedule): void
+    {
+        $pushNotificationService = app(PushNotificationService::class);
+
+        $clientUser = $schedule->client?->user;
+        $providerUser = $schedule->provider?->user;
+
+        if ($clientUser) {
+            try {
+                $pushNotificationService->sendToUser(
+                    $clientUser,
+                    new AgendamentoProximoPrestadorPush(
+                        $providerUser?->name ?? 'Prestador'
+                    )
+                );
+            } catch (\Throwable $exception) {
+                Log::error('Falha ao enviar push de agendamento próximo para o cliente', [
+                    'schedule_id' => $schedule->id,
+                    'user_id'     => $clientUser->id,
+                    'error'       => $exception->getMessage(),
+                ]);
+            }
+        }
+
+        // PUSH PARA O PRESTADOR
+        if ($providerUser) {
+            try {
+                $pushNotificationService->sendToUser(
+                    $providerUser,
+                    new AgendamentoProximoClientePush(
+                        $clientUser?->name ?? 'Cliente'
+                    )
+                );
+            } catch (\Throwable $exception) {
+                Log::error('Falha ao enviar push de agendamento próximo para o prestador', [
+                    'schedule_id' => $schedule->id,
+                    'user_id'     => $providerUser->id,
+                    'error'       => $exception->getMessage(),
+                ]);
+            }
+        }
+    }
+
+
+    //dq pra cima e as notificações
+
     private function calculateAmount(Provider $provider, string $periodType): float
     {
         $hourlyRates = [

+ 1 - 0
lang/en/messages.php

@@ -54,6 +54,7 @@ return [
     'only_clients_allowed'                         => 'Only clients may access this resource.',
     'only_providers_allowed'                       => 'Only providers may access this resource.',
     'provider_unavailable_for_schedule'            => 'Provider unavailable for this appointment.',
+    'provider_missing_bank_account'                => 'Register your primary bank account to accept services.',
     'invalid_current_status'                       => 'The current status is invalid.',
     'status_transition_not_allowed'                => 'Status transition not allowed.',
     'schedule_status_update_failed'                => 'The appointment status could not be updated.',

+ 1 - 0
lang/es/messages.php

@@ -54,6 +54,7 @@ return [
     'only_clients_allowed'                         => 'Solo los clientes pueden acceder a este recurso.',
     'only_providers_allowed'                       => 'Solo los prestadores pueden acceder a este recurso.',
     'provider_unavailable_for_schedule'            => 'Prestador no disponible para el servicio programado.',
+    'provider_missing_bank_account'                => 'Registra tu cuenta bancaria principal para aceptar servicios.',
     'invalid_current_status'                       => 'El estado actual es inválido.',
     'status_transition_not_allowed'                => 'Transición de estado no permitida.',
     'schedule_status_update_failed'                => 'No fue posible actualizar el estado del servicio programado.',

+ 1 - 0
lang/pt/messages.php

@@ -54,6 +54,7 @@ return [
     'only_clients_allowed'                         => 'Apenas clientes podem acessar este recurso.',
     'only_providers_allowed'                       => 'Apenas prestadores podem acessar este recurso.',
     'provider_unavailable_for_schedule'            => 'Prestador indisponível para agendamento.',
+    'provider_missing_bank_account'                => 'Cadastre sua conta bancária principal para aceitar serviços.',
     'invalid_current_status'                       => 'Status atual inválido.',
     'status_transition_not_allowed'                => 'Transição de status não permitida.',
     'schedule_status_update_failed'                => 'Não foi possível atualizar o status do agendamento.',