瀏覽代碼

feat: :sparkles: feat(informa falta) Foi implementado a função de informa falta

Foi adicionada a rota para informar falta, criadas as funções de validação e bloqueio de 3 dias úteis, além do fluxo de cancelamento e registro da ausência do prestador.

fase:dev | origin:escopo
kayo henrique 1 天之前
父節點
當前提交
382ff95924

+ 16 - 2
app/Http/Controllers/ScheduleController.php

@@ -37,7 +37,7 @@ class ScheduleController extends Controller
             return $this->successResponse(
                 payload: new ScheduleResource($schedule),
                 message: __('messages.created'),
-                code:    201,
+                code: 201,
             );
         } catch (\Exception $e) {
             return $this->errorResponse($e->getMessage(), 422);
@@ -73,7 +73,7 @@ class ScheduleController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code:    204,
+            code: 204,
         );
     }
 
@@ -151,4 +151,18 @@ class ScheduleController extends Controller
             return $this->errorResponse($e->getMessage(), 422);
         }
     }
+
+    public function reportProviderAbsence(int $id): JsonResponse
+    {
+        try {
+            $schedule = $this->scheduleService->reportProviderAbsence($id);
+
+            return $this->successResponse(
+                payload: new ScheduleResource($schedule),
+                message: __('messages.provider_absence_reported'),
+            );
+        } catch (\Exception $e) {
+            return $this->errorResponse($e->getMessage(), 422);
+        }
+    }
 }

+ 2 - 0
app/Models/Schedule.php

@@ -86,6 +86,7 @@ class Schedule extends Model
         'offers_meal',
         'cancel_text',
         'cancelled_by',
+        'cancelled_due_to_provider_absence',
     ];
 
     protected $casts = [
@@ -94,6 +95,7 @@ class Schedule extends Model
         'code_reminder_sent_at'  => 'datetime',
         'total_amount'           => 'decimal:2',
         'offers_meal'            => 'boolean',
+        'cancelled_due_to_provider_absence' => 'boolean',
     ];
 
     public function client()

+ 33 - 0
app/Rules/ScheduleBusinessRules.php

@@ -389,4 +389,37 @@ class ScheduleBusinessRules
 
         return $withWorkingDay->diff($fullyBlockedIds)->values();
     }
+
+    public static function validateProviderAbsenceWindow(Schedule $schedule): bool
+    {
+        $date = Carbon::parse($schedule->date)->format('Y-m-d');
+
+        $scheduleStart = Carbon::parse(
+            $date . ' ' . $schedule->start_time
+        );
+
+        $scheduleEnd = Carbon::parse(
+            $date . ' ' . $schedule->end_time
+        );
+
+        $now = Carbon::now();
+
+        // O cliente só pode informar a falta após 30 minutos do início.
+        $absenceAvailableAt = $scheduleStart->copy()->addMinutes(30);
+
+        if ($now->lt($absenceAvailableAt)) {
+            throw new \Exception(
+                __('messages.provider_absence_not_available_yet')
+            );
+        }
+
+        // A informação de falta só pode ser feita até o término do serviço.
+        if ($now->gt($scheduleEnd)) {
+            throw new \Exception(
+                __('messages.provider_absence_window_expired')
+            );
+        }
+
+        return true;
+    }
 }

+ 152 - 8
app/Services/ScheduleService.php

@@ -26,6 +26,10 @@ 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;
+use App\Enums\BlockedPeriodEnum;
+use App\Models\ProviderBlockedDay;
+use App\Models\ProviderWorkingDay;
+use App\Services\ProviderBlockedDayService;
 use Carbon\Carbon;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\DB;
@@ -36,7 +40,8 @@ class ScheduleService
     private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];
 
     public function __construct(
-        private readonly RealtimeService $realtime
+        private readonly RealtimeService $realtime,
+        private readonly ProviderBlockedDayService $providerBlockedDayService
     ) {}
 
     public function getAll()
@@ -169,7 +174,7 @@ class ScheduleService
 
     //
     //
-    public function updateStatus($id, string $status, bool $fromPackage = false)
+    public function updateStatus($id, string $status, bool $fromPackage = false, bool $isProviderAbsence = false)
     {
         try {
             DB::beginTransaction();
@@ -198,6 +203,14 @@ class ScheduleService
 
             $currentStatus = $schedule->status;
 
+            if (
+                $isProviderAbsence &&
+                $currentStatus === 'started' &&
+                $status === 'cancelled'
+            ) {
+                $allowedTransitions['started'][] = 'cancelled';
+            }
+
             if (data_get($allowedTransitions, $currentStatus) === null) {
                 throw new ScheduleStatusTransitionException;
             }
@@ -262,9 +275,9 @@ class ScheduleService
                         case UserTypeEnum::CLIENT:
                             $user = $schedule->provider?->user;
 
-                          if(!$user) {
-                            break;
-                          }
+                            if (!$user) {
+                                break;
+                            }
 
                             $notificationService->create([
                                 'title'       => __('notifications.schedule_cancelled_title'),
@@ -371,14 +384,14 @@ class ScheduleService
                     // ScheduleStartingSoonJob::dispatch($schedule->id)
                     //     ->delay(now()->addSeconds(15));
 
-                    
-                        // PRODUÇÃO: dispara 1 hora antes do início
+
+                    // 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
@@ -633,6 +646,137 @@ class ScheduleService
         }
     }
 
+    //reporta por falta 
+
+    public function reportProviderAbsence(int $scheduleId): Schedule
+    {
+        return DB::transaction(function () use ($scheduleId) {
+            $schedule = Schedule::findOrFail($scheduleId);
+
+            // O agendamento precisa ter um prestador vinculado
+            if (! $schedule->provider_id) {
+                throw new \Exception(
+                    __('messages.provider_absence_provider_not_assigned')
+                );
+            }
+
+            // O cliente precisa ser o dono do agendamento
+
+            if (Auth::user()->type !== UserTypeEnum::CLIENT) {
+                throw new \Exception(
+                    __('messages.provider_absence_only_client')
+                );
+            }
+
+            if (Auth::user()->client?->id !== $schedule->client_id) {
+                throw new \Exception(
+                    __('messages.provider_absence_not_authorized')
+                );
+            }
+
+            // O código já foi confirmado: o prestador compareceu
+            if ($schedule->code_verified) {
+                throw new \Exception(
+                    __('messages.provider_absence_code_already_verified')
+                );
+            }
+
+            // O agendamento não pode estar encerrado/cancelado/rejeitado
+            if (in_array($schedule->status, ['cancelled', 'rejected', 'finished'], true)) {
+                throw new \Exception(
+                    __('messages.provider_absence_invalid_status')
+                );
+            }
+
+            // Valida se o cliente pode informar a falta neste momento
+            ScheduleBusinessRules::validateProviderAbsenceWindow($schedule);
+
+            // Motivo utilizado para o cancelamento por falta
+            $cancelText = 'Prestador não compareceu ao serviço.';
+
+            // Registra que o cancelamento ocorreu por falta do prestador
+            $schedule->update([
+                'cancel_text' => $cancelText,
+                'cancelled_by' => Auth::user()->type,
+                'cancelled_due_to_provider_absence' => true,
+            ]);
+
+            $this->updateStatus($schedule->id, 'cancelled', false, true);
+
+            // Aplica a penalidade de bloqueio de 3 dias úteis do prestador.
+            $this->blockProviderPenaltyDays($schedule);
+            // TODO: Implementar estorno integral do pagamento.
+            // Esta etapa será implementada posteriormente por outro responsável.
+
+            return $schedule->fresh([
+                'client.user',
+                'provider.user',
+                'address',
+            ]);
+        });
+    }
+
+    //penalidade por falta bloqueio de 3 dias validos
+    private function blockProviderPenaltyDays(Schedule $schedule): void
+    {
+        $providerId = $schedule->provider_id;
+
+        $date = Carbon::parse($schedule->date)->startOfDay();
+
+        $blockedDaysCount = 0;
+
+        while ($blockedDaysCount < 3) {
+            $date->addDay();
+
+            $dayOfWeek = $date->dayOfWeek;
+
+            // 1. O prestador precisa trabalhar nesse dia da semana.
+            $worksOnDay = ProviderWorkingDay::query()
+                ->where('provider_id', $providerId)
+                ->where('day', $dayOfWeek)
+                ->exists();
+
+            if (! $worksOnDay) {
+                continue;
+            }
+
+            // 2. Se já existe qualquer ProviderBlockedDay nessa data,
+            //    a data não pode ser utilizada como penalidade.
+            $alreadyBlocked = ProviderBlockedDay::query()
+                ->where('provider_id', $providerId)
+                ->whereDate('date', $date->format('Y-m-d'))
+                ->exists();
+
+            if ($alreadyBlocked) {
+                continue;
+            }
+
+            // 3. Se existe qualquer agendamento ativo nessa data,
+            //    não podemos bloquear o dia inteiro.
+            $hasSchedule = Schedule::query()
+                ->where('provider_id', $providerId)
+                ->whereDate('date', $date->format('Y-m-d'))
+                ->whereNotIn('status', self::EXCLUDED_STATUSES)
+                ->exists();
+
+            if ($hasSchedule) {
+                continue;
+            }
+
+            // 4. Encontramos um dia de trabalho completamente livre.
+            $this->providerBlockedDayService->create([
+                'provider_id' => $providerId,
+                'date' => $date->format('Y-m-d'),
+                'period' => BlockedPeriodEnum::ALL->value,
+                'reason' => 'Bloqueio de 3 dias por falta do prestador.',
+                'init_hour' => '07:00',
+                'end_hour' => '20:00',
+            ]);
+
+            $blockedDaysCount++;
+        }
+    }
+
     private function cascadeCancelServicePackages(Schedule $schedule, string $cancelText, $cancelledBy): void
     {
         $packageIds = DB::table('service_package_items')

+ 30 - 0
database/migrations/2026_09_11_093503_add_provider_absence_to_schedules_table.php

@@ -0,0 +1,30 @@
+<?php
+
+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('schedules', function (Blueprint $table) {
+            $table->boolean('cancelled_due_to_provider_absence')
+                ->default(false)
+                ->after('cancelled_by');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('schedules', function (Blueprint $table) {
+            $table->dropColumn('cancelled_due_to_provider_absence');
+        });
+    }
+};

+ 1 - 0
routes/authRoutes/schedule.php

@@ -12,4 +12,5 @@ Route::post('/schedule',                       [ScheduleController::class, 'stor
 Route::put('/schedule/{id}',                   [ScheduleController::class, 'update'])->middleware('permission:config.schedule,edit');
 Route::patch('/schedule/{id}/status',          [ScheduleController::class, 'updateStatus'])->middleware('permission:config.schedule,edit');
 Route::patch('/schedule/{id}/cancel',          [ScheduleController::class, 'cancelWithReason'])->middleware('permission:config.schedule,edit');
+Route::patch('/schedule/{id}/report-absence', [ScheduleController::class, 'reportProviderAbsence'])->middleware('permission:config.schedule,edit');
 Route::delete('/schedule/{id}',                [ScheduleController::class, 'destroy'])->middleware('permission:config.schedule,delete');