4 Commits aa0503d88f ... 631d31a5d6

Autor SHA1 Mensagem Data
  Gustavo Zanatta 631d31a5d6 padronizando schedulebusinessrules para centralizar as regras + ajustes no schedule e custom_schedule para consumir esse arquivo nas validacoes 5 dias atrás
  Gustavo Zanatta 1c59dcc27b criando jobs de dispatch start e finish schedule + logs diarios dos dispatchs 5 dias atrás
  Gustavo Zanatta 1715910711 fix 5 dias atrás
  Gustavo Zanatta 7c951ec9ee formatando horarios para retirar os segundos 1 semana atrás

+ 4 - 21
app/Http/Controllers/ScheduleController.php

@@ -30,29 +30,12 @@ class ScheduleController extends Controller
     try {
       $validated = $request->validated();
 
-      if (isset($validated['schedules']) && is_array($validated['schedules']) && count($validated['schedules']) > 0) {
-        $baseData = [
-          'client_id' => $validated['client_id'],
-          'provider_id' => $validated['provider_id'],
-          'address_id' => $validated['address_id'],
-          'schedule_type' => $validated['schedule_type'] ?? 'default',
-          'status' => $validated['status'] ?? 'pending',
-        ];
-
-        $schedules = $this->scheduleService->createMultiple($baseData, $validated['schedules']);
-        return $this->successResponse(
-          payload: ScheduleResource::collection($schedules),
-          message: count($schedules) . " " . __("schedules.schedules_created"),
-          code: 201,
-        );
-      }
-
-      $schedule = $this->scheduleService->create($validated);
+      $schedules = $this->scheduleService->createSingleOrMultiple($validated, $validated['schedules']);
       return $this->successResponse(
-        payload: new ScheduleResource($schedule),
-        message: __("messages.created"),
+        payload: ScheduleResource::collection($schedules),
+        message: count($schedules) . " " . __("schedules.schedules_created"),
         code: 201,
-      );
+        );
     } catch (\Exception $e) {
       return $this->errorResponse($e->getMessage(), 422);
     }

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

@@ -32,8 +32,8 @@ class CustomScheduleRequest extends FormRequest
             'offers_meal' => 'nullable|boolean',
             'date' => 'required|date|after_or_equal:today',
             'period_type' => 'required|in:2,4,6,8',
-            'start_time' => 'required|date_format:H:i:s',
-            'end_time' => 'required|date_format:H:i:s|after:start_time',
+            'start_time' => 'required|date_format:H:i',
+            'end_time' => 'required|date_format:H:i|after:start_time',
             'quantity' => 'nullable|integer|min:1|max:10',
             'speciality_ids' => 'nullable|array',
             'speciality_ids.*' => 'exists:specialities,id',

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

@@ -28,8 +28,8 @@ class ScheduleRequest extends FormRequest
             'schedules.*.total_amount' => 'required|numeric|min:0',
             'period_type' => 'sometimes|required|in:2,4,6,8',
             'schedule_type' => 'sometimes|in:default,custom',
-            'start_time' => 'sometimes|required|date_format:H:i:s',
-            'end_time' => 'sometimes|required|date_format:H:i:s|after:start_time',
+            'start_time' => 'sometimes|required|date_format:H:i',
+            'end_time' => 'sometimes|required|date_format:H:i|after:start_time',
             'status' => 'sometimes|in:pending,accepted,rejected,paid,cancelled,started,finished',
             'code_verified' => 'sometimes|boolean',
         ];

+ 2 - 2
app/Http/Resources/CityResource.php

@@ -20,8 +20,8 @@ class CityResource extends JsonResource
             'state' => $this->state,
             'country' => $this->country,
             'status' => $this->status,
-            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
-            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
+            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i'),
+            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i'),
         ];
     }
 

+ 2 - 2
app/Http/Resources/ClientFavoriteProviderResource.php

@@ -15,8 +15,8 @@ class ClientFavoriteProviderResource extends JsonResource
             'provider_id' => $this->provider_id,
             'provider_name' => $this->provider->user->name ?? null,
             'notes' => $this->notes,
-            'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
-            'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
+            'created_at' => $this->created_at?->format('Y-m-d H:i'),
+            'updated_at' => $this->updated_at?->format('Y-m-d H:i'),
         ];
     }
 }

+ 2 - 2
app/Http/Resources/ClientPaymentMethodResource.php

@@ -20,8 +20,8 @@ class ClientPaymentMethodResource extends JsonResource
             'cvv' => $this->cvv,
             'card_number' => '**** **** **** ' . $this->last_four_digits,
             'is_active' => $this->is_active,
-            'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
-            'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
+            'created_at' => $this->created_at?->format('Y-m-d H:i'),
+            'updated_at' => $this->updated_at?->format('Y-m-d H:i'),
         ];
     }
 }

+ 2 - 6
app/Http/Resources/CountryResource.php

@@ -17,12 +17,8 @@ class CountryResource extends JsonResource
             "name" => $this->name,
             "code" => $this->code,
             "status" => $this->status,
-            "created_at" => Carbon::parse($this->created_at)->format(
-                "Y-m-d H:i:s",
-            ),
-            "updated_at" => Carbon::parse($this->updated_at)->format(
-                "Y-m-d H:i:s",
-            ),
+            "created_at" => Carbon::parse($this->created_at)->format("Y-m-d H:i"),
+            "updated_at" => Carbon::parse($this->updated_at)->format("Y-m-d H:i"),
         ];
     }
 

+ 2 - 2
app/Http/Resources/CustomScheduleResource.php

@@ -26,8 +26,8 @@ class CustomScheduleResource extends JsonResource
             'min_price' => number_format($this->min_price, 2, '.', ''),
             'max_price' => number_format($this->max_price, 2, '.', ''),
             'offers_meal' => $this->offers_meal,
-            'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
-            'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
+            'created_at' => $this->created_at?->format('Y-m-d H:i'),
+            'updated_at' => $this->updated_at?->format('Y-m-d H:i'),
             
             'schedule' => $this->whenLoaded('schedule', function () {
                 return [

+ 2 - 2
app/Http/Resources/MediaResource.php

@@ -21,8 +21,8 @@ class MediaResource extends JsonResource
             'url' => $this->url,
             'user_id' => $this->user_id,
             'user' => $this->user,
-            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
-            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
+            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i'),
+            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i'),
         ];
     }
 

+ 2 - 2
app/Http/Resources/PermissionResource.php

@@ -22,8 +22,8 @@ class PermissionResource extends JsonResource
             'description' => $this->description,
             'bits' => $this->bits,
             'parent_id' => $this->parent_id,
-            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
-            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
+            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i'),
+            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i'),
         ];
     }
     public static function collection($resource): AnonymousResourceCollection

+ 2 - 2
app/Http/Resources/ProviderResource.php

@@ -30,8 +30,8 @@ class ProviderResource extends JsonResource
             'daily_price_2h' => $this->daily_price_2h,
             'profile_media_id' => $this->profile_media_id,
             'profile_media' => $this->profileMedia,
-            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
-            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
+            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i'),
+            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i'),
         ];
     }
 

+ 2 - 6
app/Http/Resources/StateResource.php

@@ -19,12 +19,8 @@ class StateResource extends JsonResource
             "country_id" => $this->country_id,
             "country" => $this->country,
             "status" => $this->status,
-            "created_at" => Carbon::parse($this->created_at)->format(
-                "Y-m-d H:i:s",
-            ),
-            "updated_at" => Carbon::parse($this->updated_at)->format(
-                "Y-m-d H:i:s",
-            ),
+            "created_at" => Carbon::parse($this->created_at)->format("Y-m-d H:i"),
+            "updated_at" => Carbon::parse($this->updated_at)->format("Y-m-d H:i"),
         ];
     }
 

+ 2 - 2
app/Http/Resources/UserResource.php

@@ -22,8 +22,8 @@ class UserResource extends JsonResource
             'email' => $this->email,
             'language' => $this->language,
             'type' => $this->type,
-            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
-            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
+            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i'),
+            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i'),
         ];
     }
 

+ 68 - 0
app/Jobs/FinishScheduleJob.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\Schedule;
+use Carbon\Carbon;
+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 FinishScheduleJob implements ShouldQueue
+{
+  use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+  public function __construct(
+    public int $scheduleId
+  ) {}
+
+  public function handle()
+  {
+    try {
+      $schedule = Schedule::find($this->scheduleId);
+      $date_cleaned = Carbon::parse($schedule->date)->format('Y-m-d');
+  
+      if (!$schedule) {
+        return;
+      }
+  
+      Log::channel('schedule_end_jobs')->info('Verificando status do agendamento id: ' . $schedule->id);
+      Log::channel('schedule_end_jobs')->info('Status do agendamento: ' . $schedule->status);
+  
+      if ($schedule->status !== 'started') {
+        return;
+      }
+  
+      Log::channel('schedule_end_jobs')->info('Verificando data');
+      Log::channel('schedule_end_jobs')->info('Data do agendamento: ' . $date_cleaned);
+      Log::channel('schedule_end_jobs')->info('Data atual: ' . now()->toDateString());
+      
+
+      if ($date_cleaned > now()->toDateString()) {
+        return;
+      }
+
+      Log::channel('schedule_end_jobs')->info('Verificando horário');
+      Log::channel('schedule_end_jobs')->info('Horário do agendamento: ' . $schedule->end_time);
+      Log::channel('schedule_end_jobs')->info('Horário atual: ' . now()->toTimeString());
+  
+      $end_date_time = Carbon::parse($date_cleaned . ' ' . $schedule->end_time);
+      
+      if ($end_date_time > now()) {
+        return;
+      }
+  
+      Log::channel('schedule_end_jobs')->info('Validado com sucesso, atualizado agendamento id: ' . $schedule->id . ' para status finalizado');
+  
+      $schedule->update([
+        'status' => 'finished'
+      ]);
+    } catch (\Exception $e) {
+      Log::channel('schedule_end_jobs')->error('Erro ao finalizar agendamento id: ' . $this->scheduleId . '. Erro: ' . $e->getMessage());
+      return;
+    }
+  }
+}

+ 72 - 0
app/Jobs/StartScheduleJob.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\Schedule;
+use Carbon\Carbon;
+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 StartScheduleJob implements ShouldQueue
+{
+  use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+  public function __construct(
+    public int $scheduleId
+  ) {}
+
+  public function handle()
+  {
+    try {
+      $schedule = Schedule::find($this->scheduleId);
+      $date_cleaned = Carbon::parse($schedule->date)->format('Y-m-d');
+  
+      if (!$schedule) {
+        return;
+      }
+  
+      Log::channel('schedule_start_jobs')->info('Verificando status do agendamento id: ' . $schedule->id);
+      Log::channel('schedule_start_jobs')->info('Status do agendamento: ' . $schedule->status);
+  
+      if ($schedule->status !== 'paid') {
+        return;
+      }
+      
+      Log::channel('schedule_start_jobs')->info('Verificando data');
+      Log::channel('schedule_start_jobs')->info('Data do agendamento: ' . $date_cleaned);
+      Log::channel('schedule_start_jobs')->info('Data atual: ' . now()->toDateString());
+      
+      if ($date_cleaned > now()->toDateString()) {
+        return;
+      
+      }
+      Log::channel('schedule_start_jobs')->info('Verificando horário');
+      Log::channel('schedule_start_jobs')->info('Horário do agendamento: ' . $schedule->start_time);
+      Log::channel('schedule_start_jobs')->info('Horário atual: ' . now()->toTimeString());
+
+      $start_date_time = Carbon::parse($date_cleaned . ' ' . $schedule->start_time);
+      
+      if ($start_date_time > now()) {
+        return;
+      }
+  
+      Log::channel('schedule_start_jobs')->info('Validado com sucesso, atualizado agendamento id: ' . $schedule->id . ' para status iniciado');
+  
+      $schedule->update([
+        'status' => 'started'
+      ]);
+      $date_time_dispatch = Carbon::parse($date_cleaned . ' ' . $schedule->end_time);
+      FinishScheduleJob::dispatch($schedule->id)->delay($date_time_dispatch); 
+
+      // dispatch de teste em local 
+      // FinishScheduleJob::dispatch($schedule->id)->delay(now()->addSeconds(15)); 
+    } catch (\Exception $e) {
+      Log::channel('schedule_start_jobs')->error('Erro ao iniciar agendamento id: ' . $this->scheduleId . '. Erro: ' . $e->getMessage());
+      return;
+    }
+  }
+}

+ 204 - 10
app/Rules/ScheduleBusinessRules.php

@@ -2,8 +2,13 @@
 
 namespace App\Rules;
 
+use App\Models\Provider;
+use App\Models\ProviderBlockedDay;
+use App\Models\ProviderWorkingDay;
 use App\Models\Schedule;
+use App\Models\ScheduleProposal;
 use Carbon\Carbon;
+use Illuminate\Support\Facades\Log;
 
 class ScheduleBusinessRules
 {
@@ -11,16 +16,15 @@ class ScheduleBusinessRules
     private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];
 
     /**
-     * Valida se o prestador pode ter mais um agendamento com o cliente na semana
-     * Limite: 2 agendamentos por semana (domingo a sábado)
-     * 
-     * @param int $clientId
-     * @param int $providerId
-     * @param string $date (Y-m-d)
-     * @param int|null $excludeScheduleId
-     * @return bool
-     * @throws \Exception
-     */
+      * Valida se o prestador pode ter mais um agendamento com o cliente na semana
+      * Limite: 2 agendamentos por semana (domingo a sábado)
+      * @param int $clientId
+      * @param int $providerId
+      * @param string $date (Y-m-d)
+      * @param int|null $excludeScheduleId
+      * @return bool
+      * @throws \Exception
+    */
     public static function validateWeeklyScheduleLimit($clientId, $providerId, $date, $excludeScheduleId = null)
     {
         $date = Carbon::parse($date);
@@ -43,4 +47,194 @@ class ScheduleBusinessRules
 
         return true;
     }
+    
+    /**
+      * Valida se o prestador tem horário de trabalho cadastrado para o dia da semana e período
+      * @param int $provider_id
+      * @param int $day_of_week (0 - domingo, 6 - sábado)
+      * @param string $period ('morning' ou 'afternoon')
+      * @return bool
+      * @throws \Exception
+    */
+    public static function validateWorkingDay($provider_id, $day_of_week, $period)
+    {
+      $workingDay = ProviderWorkingDay::where('provider_id', $provider_id)
+        ->where('day', $day_of_week)
+        ->where('period', $period)
+        ->first();
+
+      if(!$workingDay) {
+        throw new \Exception(__('validation.custom.schedule.provider_not_working'));
+      }
+
+      return true;
+    }
+
+    /**
+      * Valida se o prestador tem bloqueio cadastrado para o dia e horário
+      * @param int $provider_id
+      * @param string $date_ymd (Y-m-d)
+      * @param string $start_time (H:i:s)
+      * @param string $end_time (H:i:s)
+      * @return bool
+      * @throws \Exception
+    */
+    public static function validateBlockedDay($provider_id, $date_ymd, $start_time, $end_time)
+    {
+      $blockedDay = ProviderBlockedDay::where('provider_id', $provider_id)
+        ->where('date', $date_ymd)
+        ->where(function ($query) use ($start_time, $end_time) {
+          $query->where('period', 'full')
+            ->orWhere(function ($q) use ($start_time, $end_time) {
+              $q->where('period', 'partial')
+                ->where(function ($q2) use ($start_time, $end_time) {
+                  $q2->whereBetween('init_hour', [$start_time, $end_time])
+                    ->orWhereBetween('end_hour', [$start_time, $end_time])
+                    ->orWhere(function ($q3) use ($start_time, $end_time) {
+                      $q3->where('init_hour', '<=', $start_time)
+                        ->where('end_hour', '>=', $end_time);
+                    });
+                });
+            });
+        })
+        ->first();
+
+      if ($blockedDay) {
+        throw new \Exception(__('validation.custom.schedule.provider_blocked'));
+      }
+
+      return true;
+    }
+
+    public static function validatePricePeriod($provider_id, $min_price, $max_price, $period_type)
+    {
+      if ($min_price < 0 || $max_price < 0) {
+        throw new \Exception(__('validation.custom.schedule.invalid_price'));
+      }
+
+      if ($min_price > $max_price) {
+        throw new \Exception(__('validation.custom.schedule.invalid_price_range'));
+      }
+
+      $provider = Provider::find($provider_id);
+      $provider_price_period = 0;
+      switch ($period_type):
+        case '2': //2 horas
+          $provider_price_period = $provider->daily_price_2h;
+          break;
+        case '4': //4 horas
+          $provider_price_period = $provider->daily_price_4h;
+          break;
+        case '6': //6 horas
+          $provider_price_period = $provider->daily_price_6h;
+          break;
+        case '8': //8 horas
+          $provider_price_period = $provider->daily_price_8h;
+          break;
+        default:
+          throw new \Exception(__('validation.custom.schedule.invalid_period_type'));
+        endswitch;
+
+      if ($provider_price_period < $min_price || $provider_price_period > $max_price) {
+        throw new \Exception(__('validation.custom.schedule.price_not_in_range'));
+      }
+
+      return true;
+    }
+
+    /**
+      * Valida se o prestador tem outro agendamento no mesmo dia e horário
+      * @param int $provider_id
+      * @param string $date_ymd (Y-m-d)
+      * @param string $start_time (H:i:s)
+      * @param string $end_time (H:i:s)
+      * @param int|null $exclude_schedule_id (id do agendamento a ser excluído da validação, usado para edição de agendamento)
+      * @return bool
+      * @throws \Exception
+    */
+    public static function validateConflictingSchedule($provider_id, $date_ymd, $start_time, $end_time, $exclude_schedule_id = null)
+    {
+      $conflictingSchedule = Schedule::where('provider_id', $provider_id)
+        ->where('date', $date_ymd)
+        ->whereIn('status', ['pending', 'accepted', 'paid', 'started'])
+        ->where(function ($query) use ($start_time, $end_time) {
+          $query->whereBetween('start_time', [$start_time, $end_time])
+            ->orWhereBetween('end_time', [$start_time, $end_time])
+            ->orWhere(function ($q) use ($start_time, $end_time) {
+              $q->where('start_time', '<=', $start_time)
+                ->where('end_time', '>=', $end_time);
+            });
+        })
+        ->when($exclude_schedule_id, function ($query) use ($exclude_schedule_id) {
+          $query->where('id', '!=', $exclude_schedule_id);
+        })
+        ->first();
+
+      if ($conflictingSchedule) {
+        throw new \Exception(__('validation.custom.schedule.provider_conflicting_schedule'));
+      }
+
+      return true;
+    }
+
+    /**
+      * Valida se o prestador tem outro agendamento com o mesmo cliente no mesmo dia e horário
+      * @param int $provider_id
+      * @param string $date_ymd (Y-m-d)
+      * @param string $start_time (H:i:s)
+      * @param string $end_time (H:i:s)
+      * @param int|null $exclude_schedule_id (id do agendamento a ser excluído da validação, usado para edição de agendamento)
+      * @return bool
+      * @throws \Exception
+    */
+    public static function validateConflictingSameProposal($provider_id, $schedule_id)
+    {
+      $conflictingSameProposal = ScheduleProposal::where('schedule_proposals.provider_id', $provider_id)
+        ->where('schedule_proposals.schedule_id', $schedule_id)
+        ->leftJoin('schedules', 'schedule_proposals.schedule_id', '=', 'schedules.id')
+        ->whereNotIn('schedules.status', self::EXCLUDED_STATUSES)
+        ->first();
+
+      if ($conflictingSameProposal) {
+        throw new \Exception(__('validation.custom.schedule.provider_conflicting_same_proposal'));
+      }
+
+      return true;
+    }
+
+    /**
+      * Valida se o prestador tem outro agendamento com o mesmo cliente no mesmo dia e horário, ignorando o horário
+      * @param int $provider_id
+      * @param string $date_ymd (Y-m-d)
+      * @param string $start_time (H:i:s)
+      * @param string $end_time (H:i:s)
+      * @param int|null $exclude_schedule_id (id do agendamento a ser excluído da validação, usado para edição de agendamento)
+      * @return bool
+      * @throws \Exception
+    */
+    public static function validateConflictingProposalSameDate($provider_id, $date_ymd, $start_time, $end_time, $exclude_schedule_id = null)
+    {
+      $conflictingProposalSameDate = ScheduleProposal::where('schedule_proposals.provider_id', $provider_id)
+        ->leftJoin('schedules', 'schedule_proposals.schedule_id', '=', 'schedules.id')
+        ->where('schedules.date', $date_ymd)
+        ->where(function ($query) use ($start_time, $end_time) {
+          $query->whereBetween('schedules.start_time', [$start_time, $end_time])
+            ->orWhereBetween('schedules.end_time', [$start_time, $end_time])
+            ->orWhere(function ($q) use ($start_time, $end_time) {
+              $q->where('schedules.start_time', '<=', $start_time)
+                ->where('schedules.end_time', '>=', $end_time);
+            });
+        })
+        ->whereNotIn('status', self::EXCLUDED_STATUSES)
+        ->when($exclude_schedule_id, function ($query) use ($exclude_schedule_id) {
+          $query->where('schedules.id', '!=', $exclude_schedule_id);
+        })
+        ->first();
+
+      if ($conflictingProposalSameDate) {
+        throw new \Exception(__('validation.custom.schedule.provider_conflicting_proposal_same_date'));
+      }
+
+      return true;
+    }
 }

+ 51 - 64
app/Services/CustomScheduleService.php

@@ -365,77 +365,65 @@ class CustomScheduleService
   private function checkProviderAvailability($providerId, $schedule)
   {
     $date = Carbon::parse($schedule->date);
+    $provider_id = $providerId;
+    $client_id = $schedule->client_id;
     $startTime = $schedule->start_time;
     $endTime = $schedule->end_time;
-    $dayOfWeek = $date->dayOfWeek;
+    $dayOfWeek = $date->dayOfWeek;//0-6
+    $date_ymd = $date->format('Y-m-d');
+    $period = $startTime < '13:00:00' ? 'morning' : 'afternoon';
+    $period_type = $schedule->period_type;//2,4,6,8
 
+    // bloqueio 2 schedules por semana para o mesmo client e provider
     ScheduleBusinessRules::validateWeeklyScheduleLimit(
-      $schedule->client_id,
-      $providerId,
-      $schedule->date
+      $client_id,
+      $provider_id,
+      $date_ymd
+    );
+    // bloqueio provider trabalha no dia/periodo
+    ScheduleBusinessRules::validateWorkingDay(
+      $provider_id,
+      $dayOfWeek,
+      $period
+    );
+    // bloqueio provider tem blockedday para dia/hora
+    ScheduleBusinessRules::validateBlockedDay(
+      $provider_id,
+      $date_ymd,
+      $startTime,
+      $endTime
     );
 
-    $startHour = (int) substr($startTime, 0, 2);
-    $endHour = (int) substr($endTime, 0, 2);
-
-    $periods = [];
-    if ($startHour < 13) {
-      $periods[] = 'morning';
-    }
-    if ($endHour >= 13 || ($startHour < 13 && $endHour > 12)) {
-      $periods[] = 'afternoon';
-    }
-
-    foreach ($periods as $period) {
-      $workingDay = ProviderWorkingDay::where('provider_id', $providerId)
-        ->where('day', $dayOfWeek)
-        ->where('period', $period)
-        ->first();
-
-      if (!$workingDay) {
-        throw new \Exception(__('validation.custom.opportunity.provider_not_working'));
-      }
-    }
-
-    $blockedDay = ProviderBlockedDay::where('provider_id', $providerId)
-      ->where('date', $date->format('Y-m-d'))
-      ->where(function ($query) use ($startTime, $endTime) {
-        $query->where('period', 'full')
-          ->orWhere(function ($q) use ($startTime, $endTime) {
-            $q->where('period', 'partial')
-              ->where(function ($q2) use ($startTime, $endTime) {
-                $q2->whereBetween('init_hour', [$startTime, $endTime])
-                  ->orWhereBetween('end_hour', [$startTime, $endTime])
-                  ->orWhere(function ($q3) use ($startTime, $endTime) {
-                    $q3->where('init_hour', '<=', $startTime)
-                      ->where('end_hour', '>=', $endTime);
-                  });
-              });
-          });
-      })
-      ->first();
-
-    if ($blockedDay) {
-      throw new \Exception(__('validation.custom.opportunity.provider_blocked'));
-    }
-    $excluded_status = ['cancelled', 'rejected'];
-    $conflictingSchedule = Schedule::where('provider_id', $providerId)
-      ->where('date', $date->format('Y-m-d'))
-      ->whereNotIn('status', $excluded_status)
-      ->where(function ($query) use ($startTime, $endTime) {
-        $query->whereBetween('start_time', [$startTime, $endTime])
-          ->orWhereBetween('end_time', [$startTime, $endTime])
-          ->orWhere(function ($q) use ($startTime, $endTime) {
-            $q->where('start_time', '<=', $startTime)
-              ->where('end_time', '>=', $endTime);
-          });
-      })
-      ->first();
+    // bloqueio daily_price do provider esta fora do range min_price e max_price
+    ScheduleBusinessRules::validatePricePeriod(
+      $provider_id,
+      $schedule->customSchedule->min_price,
+      $schedule->customSchedule->max_price,
+      $period_type
+    );
 
-    if ($conflictingSchedule) {
-      throw new \Exception(__('validation.custom.opportunity.schedule_conflict'));
-    }
+    // bloqueio provider tem outro agendamento para dia/hora
+    ScheduleBusinessRules::validateConflictingSchedule(
+      $provider_id,
+      $date_ymd,
+      $startTime,
+      $endTime
+    );
+    // bloqueio provider tem outra proposta para o mesmo agendamento
+    ScheduleBusinessRules::validateConflictingSameProposal(
+      $provider_id,
+      $schedule->id
+    );
 
+    // bloqueio provider tem outra proposta na mesma data
+    ScheduleBusinessRules::validateConflictingProposalSameDate(
+      $provider_id,
+      $date_ymd,
+      $startTime,
+      $endTime,
+      $schedule->id
+    );
+    
     return true;
   }
 
@@ -518,7 +506,6 @@ class CustomScheduleService
 
     $schedule->update([
       'code_verified' => true,
-      'status' => 'started',
     ]);
 
     return $schedule;

+ 105 - 121
app/Services/ScheduleService.php

@@ -2,6 +2,8 @@
 
 namespace App\Services;
 
+use App\Jobs\FinishScheduleJob;
+use App\Jobs\StartScheduleJob;
 use App\Models\Schedule;
 use App\Models\Provider;
 use App\Models\ProviderBlockedDay;
@@ -27,41 +29,23 @@ class ScheduleService
     return Schedule::with(['client.user', 'provider.user', 'address'])->findOrFail($id);
   }
 
-  public function create(array $data)
-  {
-    $data['code'] = str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT);
-
-    $provider = Provider::findOrFail($data['provider_id']);
-    $data['total_amount'] = $this->calculateAmount($provider, $data['period_type']);
-
-    $this->validateProviderAvailability($data);
-    $this->validateWeeklyScheduleLimit($data);
-
-    return Schedule::create($data);
-  }
-
-  public function createMultiple(array $baseData, array $schedules)
+  public function createSingleOrMultiple(array $baseData, array $schedules)
   {
+    $createdSchedules = [];
     foreach ($schedules as $schedule) {
-      $validationData = array_merge($baseData, $schedule);
       try {
-        $this->validateProviderAvailability($validationData);
-        $this->validateWeeklyScheduleLimit($validationData);
+        $datasMerged = array_merge($baseData, $schedule);
+        $this->validateProviderAvailability($datasMerged, null);
+        $scheduleData = array_merge($datasMerged, [
+          'code' => str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT),
+        ]);
+        $createdSchedules[] = Schedule::create($scheduleData);
+
       } catch (\Exception $e) {
-        throw new \Exception("Prestador não disponível para a data " . Carbon::parse($schedule['date'])->format('d/m/Y') . ": " . $e->getMessage());
+        throw new \Exception(__("validation.provider_unavailable"));
       }
     }
 
-    $createdSchedules = [];
-
-    foreach ($schedules as $schedule) {
-      $scheduleData = array_merge($baseData, $schedule, [
-        'code' => str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT),
-      ]);
-
-      $createdSchedules[] = Schedule::create($scheduleData);
-    }
-
     return $createdSchedules;
   }
 
@@ -106,81 +90,48 @@ class ScheduleService
 
   private function validateProviderAvailability(array $data, $excludeScheduleId = null)
   {
-    $provider = Provider::findOrFail($data['provider_id']);
+    $provider_id = $data['provider_id'];
+    $client_id = $data['client_id'];
+
     $date = Carbon::parse($data['date']);
+    $dayOfWeek = $date->dayOfWeek;
     $startTime = $data['start_time'];
     $endTime = $data['end_time'];
+    $period = $startTime < '13:00:00' ? 'morning' : 'afternoon';
+
+    // bloqueio 2 schedules por semana para o mesmo client e provider
+    ScheduleBusinessRules::validateWeeklyScheduleLimit(
+      $client_id,
+      $provider_id,
+      $data['date'],
+      $excludeScheduleId
+    );
 
-    $dayOfWeek = $date->dayOfWeek;
-
-    $startHour = (int) substr($startTime, 0, 2);
-    $period = $startHour < 12 ? 'morning' : 'afternoon';
-
-    $workingDay = ProviderWorkingDay::where('provider_id', $data['provider_id'])
-      ->where('day', $dayOfWeek)
-      ->where('period', $period)
-      ->first();
-
-    if (!$workingDay) {
-      throw new \Exception("Prestador não trabalha neste dia/período.");
-    }
-
-    $blockedDay = ProviderBlockedDay::where('provider_id', $data['provider_id'])
-      ->where('date', $date->format('Y-m-d'))
-      ->where(function ($query) use ($startTime, $endTime) {
-        $query->where('period', 'full')
-          ->orWhere(function ($q) use ($startTime, $endTime) {
-            $q->where('period', 'partial')
-              ->where(function ($q2) use ($startTime, $endTime) {
-                $q2->whereBetween('init_hour', [$startTime, $endTime])
-                  ->orWhereBetween('end_hour', [$startTime, $endTime])
-                  ->orWhere(function ($q3) use ($startTime, $endTime) {
-                    $q3->where('init_hour', '<=', $startTime)
-                      ->where('end_hour', '>=', $endTime);
-                  });
-              });
-          });
-      })
-      ->first();
-
-    if ($blockedDay) {
-      throw new \Exception("Prestador possui bloqueio neste dia/horário.");
-    }
-
-    $conflictingSchedule = Schedule::where('provider_id', $data['provider_id'])
-      ->where('date', $date->format('Y-m-d'))
-      ->whereIn('status', ['pending', 'accepted', 'paid', 'started'])
-      ->where(function ($query) use ($startTime, $endTime) {
-        $query->whereBetween('start_time', [$startTime, $endTime])
-          ->orWhereBetween('end_time', [$startTime, $endTime])
-          ->orWhere(function ($q) use ($startTime, $endTime) {
-            $q->where('start_time', '<=', $startTime)
-              ->where('end_time', '>=', $endTime);
-          });
-      })
-      ->when($excludeScheduleId, function ($query) use ($excludeScheduleId) {
-        $query->where('id', '!=', $excludeScheduleId);
-      })
-      ->first();
-
-    if ($conflictingSchedule) {
-      throw new \Exception("Prestador já possui agendamento neste horário.");
-    }
-
-    return true;
-  }
-
-  private function validateWeeklyScheduleLimit(array $data)
-  {
-    if (isset($data['schedule_type']) && $data['schedule_type'] !== 'default') {
-      return true;
-    }
-
-    return ScheduleBusinessRules::validateWeeklyScheduleLimit(
-      $data['client_id'],
-      $data['provider_id'],
-      $data['date']
+    // bloqueio provider trabalha no dia/periodo
+    ScheduleBusinessRules::validateWorkingDay(
+      $provider_id,
+      $dayOfWeek,
+      $period
+    );
+    
+    // bloqueio provider tem blockedday para dia/hora
+    ScheduleBusinessRules::validateBlockedDay(
+      $provider_id,
+      $date->format('Y-m-d'),
+      $startTime,
+      $endTime
     );
+    
+    // bloqueio provider tem outro agendamento para dia/hora
+    ScheduleBusinessRules::validateConflictingSchedule(
+      $provider_id,
+      $date->format('Y-m-d'),
+      $startTime,
+      $endTime,
+      $excludeScheduleId
+    );
+    
+    return true;
   }
 
   public function getSchedulesDefaultGroupedByClient()
@@ -231,29 +182,62 @@ class ScheduleService
 
   public function updateStatus($id, string $status)
   {
-    $schedule = Schedule::findOrFail($id);
-
-    $allowedTransitions = [
-      'pending' => ['accepted', 'rejected'],
-      'accepted' => ['paid', 'cancelled'],
-      'paid' => ['cancelled', 'started'],
-      'started' => ['finished'],
-      'rejected' => [],
-      'cancelled' => [],
-      'finished' => [],
-    ];
-
-    $currentStatus = $schedule->status;
-
-    if (!isset($allowedTransitions[$currentStatus])) {
-      throw new \Exception("Status atual inválido.");
-    }
-
-    if (!in_array($status, $allowedTransitions[$currentStatus])) {
-      throw new \Exception("Transição de status não permitida: {$currentStatus} → {$status}");
+    try {
+      DB::beginTransaction();
+      $schedule = Schedule::findOrFail($id);
+  
+      $allowedTransitions = [
+        'pending' => ['accepted', 'rejected'],
+        'accepted' => ['paid', 'cancelled'],
+        'paid' => ['cancelled', 'started'],
+        'started' => ['finished'],
+        'rejected' => [],
+        'cancelled' => [],
+        'finished' => [],
+      ];
+  
+      $currentStatus = $schedule->status;
+  
+      if (!isset($allowedTransitions[$currentStatus])) {
+        throw new \Exception("Status atual inválido.");
+      }
+  
+      if (!in_array($status, $allowedTransitions[$currentStatus])) {
+        throw new \Exception("Transição de status não permitida: {$currentStatus} → {$status}");
+      }
+  
+      $schedule->update(['status' => $status]);
+  
+      switch ($status) {
+        case 'pending':
+          break;
+        case 'accepted':
+          break;
+        case 'rejected':
+          break;
+        case 'paid':
+          $date_cleaned = Carbon::parse($schedule->date)->format('Y-m-d');
+          $date_time_dispatch = Carbon::parse($date_cleaned . ' ' . $schedule->start_time);
+
+          StartScheduleJob::dispatch($schedule->id)->delay($date_time_dispatch);
+
+          // dispatch de teste em local
+          // StartScheduleJob::dispatch($schedule->id)->delay(now()->addSeconds(15));
+          break;
+        case 'cancelled':
+          break;
+        case 'started':
+          break;
+        case 'finished':
+          break;
+      }
+      
+      DB::commit();
+      return $schedule->fresh(['client.user', 'provider.user', 'address']);
+    } catch (\Exception $e) {
+      DB::rollBack();
+      Log::error("Erro ao atualizar status do agendamento: " . $e->getMessage());
+      throw new \Exception("Não foi possível atualizar o status do agendamento.");
     }
-
-    $schedule->update(['status' => $status]);
-    return $schedule->fresh(['client.user', 'provider.user', 'address']);
   }
 }

+ 12 - 0
config/logging.php

@@ -133,6 +133,18 @@ return [
             'level' => 'info',
             'days' => 7,
         ],
+        'schedule_start_jobs' => [
+            'driver' => 'daily',
+            'path' => storage_path('logs/schedules/start_jobs.log'),
+            'level' => 'info',
+            'days' => 7,
+        ],
+        'schedule_end_jobs' => [
+            'driver' => 'daily',
+            'path' => storage_path('logs/schedules/end_jobs.log'),
+            'level' => 'info',
+            'days' => 7,
+        ],
 
     ],
 

+ 1 - 0
lang/en/validation.php

@@ -136,6 +136,7 @@ return [
     'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
     'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
     'prohibits' => 'The :attribute field prohibits :other from being present.',
+    'provider_unavailable' => 'The provider is unavailable for the selected date and time.',
     'refund_value_greater_than_payment_value' => 'The refund value cannot be greater than the payment value.',
     'regex' => 'The :attribute field format is invalid.',
     'required' => 'The :attribute field is required.',

+ 1 - 0
lang/es/validation.php

@@ -136,6 +136,7 @@ return [
     'prohibited_if' => 'El campo :attribute está prohibido cuando :other es :value.',
     'prohibited_unless' => 'El campo :attribute está prohibido a menos que :other esté en :values.',
     'prohibits' => 'El campo :attribute prohíbe que :other esté presente.',
+    'provider_unavailable' => 'El proveedor no está disponible para la fecha y hora seleccionadas.',
     'refund_value_greater_than_payment_value' => 'El valor del reembolso no puede ser mayor que el valor del pago.',
     'regex' => 'El formato del campo :attribute no es válido.',
     'required' => 'El campo :attribute es obligatorio.',

+ 1 - 0
lang/pt/validation.php

@@ -137,6 +137,7 @@ return [
     'prohibited_if' => 'O campo :attribute é proibido quando :other é :value.',
     'prohibited_unless' => 'O campo :attribute é proibido a menos que :other esteja em :values.',
     'prohibits' => 'O campo :attribute proíbe :other de estar presente.',
+    'provider_unavailable' => 'O prestador não está disponível para a data e horário selecionados.',
     'refund_value_greater_than_payment_value' => 'O valor do reembolso não pode ser maior que o valor do pagamento.',
     'regex' => 'O formato do campo :attribute é inválido.',
     'required' => 'O campo :attribute é obrigatório.',