|
@@ -26,6 +26,10 @@ use App\Notifications\Push\Prestador\Agendamento\AgendamentoProximoClientePush;
|
|
|
use App\Notifications\Push\Cliente\Agendamento\PrestadorCancelouPush;
|
|
use App\Notifications\Push\Cliente\Agendamento\PrestadorCancelouPush;
|
|
|
use App\Notifications\Push\Prestador\Agendamento\ClienteCancelouPush;
|
|
use App\Notifications\Push\Prestador\Agendamento\ClienteCancelouPush;
|
|
|
use App\Notifications\Push\Prestador\Agendamento\NewPushRequest;
|
|
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 Carbon\Carbon;
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\DB;
|
|
@@ -36,7 +40,8 @@ class ScheduleService
|
|
|
private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];
|
|
private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];
|
|
|
|
|
|
|
|
public function __construct(
|
|
public function __construct(
|
|
|
- private readonly RealtimeService $realtime
|
|
|
|
|
|
|
+ private readonly RealtimeService $realtime,
|
|
|
|
|
+ private readonly ProviderBlockedDayService $providerBlockedDayService
|
|
|
) {}
|
|
) {}
|
|
|
|
|
|
|
|
public function getAll()
|
|
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 {
|
|
try {
|
|
|
DB::beginTransaction();
|
|
DB::beginTransaction();
|
|
@@ -198,6 +203,14 @@ class ScheduleService
|
|
|
|
|
|
|
|
$currentStatus = $schedule->status;
|
|
$currentStatus = $schedule->status;
|
|
|
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ $isProviderAbsence &&
|
|
|
|
|
+ $currentStatus === 'started' &&
|
|
|
|
|
+ $status === 'cancelled'
|
|
|
|
|
+ ) {
|
|
|
|
|
+ $allowedTransitions['started'][] = 'cancelled';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (data_get($allowedTransitions, $currentStatus) === null) {
|
|
if (data_get($allowedTransitions, $currentStatus) === null) {
|
|
|
throw new ScheduleStatusTransitionException;
|
|
throw new ScheduleStatusTransitionException;
|
|
|
}
|
|
}
|
|
@@ -262,9 +275,9 @@ class ScheduleService
|
|
|
case UserTypeEnum::CLIENT:
|
|
case UserTypeEnum::CLIENT:
|
|
|
$user = $schedule->provider?->user;
|
|
$user = $schedule->provider?->user;
|
|
|
|
|
|
|
|
- if(!$user) {
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!$user) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
$notificationService->create([
|
|
$notificationService->create([
|
|
|
'title' => __('notifications.schedule_cancelled_title'),
|
|
'title' => __('notifications.schedule_cancelled_title'),
|
|
@@ -371,14 +384,14 @@ class ScheduleService
|
|
|
// ScheduleStartingSoonJob::dispatch($schedule->id)
|
|
// ScheduleStartingSoonJob::dispatch($schedule->id)
|
|
|
// ->delay(now()->addSeconds(15));
|
|
// ->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();
|
|
$notification_date_time = $start_date_time->copy()->subHour();
|
|
|
|
|
|
|
|
ScheduleStartingSoonJob::dispatch($schedule->id)
|
|
ScheduleStartingSoonJob::dispatch($schedule->id)
|
|
|
->delay($notification_date_time);
|
|
->delay($notification_date_time);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
|
|
|
// =====================================================
|
|
// =====================================================
|
|
|
// StartScheduleJob
|
|
// 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
|
|
private function cascadeCancelServicePackages(Schedule $schedule, string $cancelText, $cancelledBy): void
|
|
|
{
|
|
{
|
|
|
$packageIds = DB::table('service_package_items')
|
|
$packageIds = DB::table('service_package_items')
|