|
|
@@ -2,6 +2,9 @@
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
+use App\Broadcasting\RealtimeEvent;
|
|
|
+use App\Broadcasting\RealtimeRoom;
|
|
|
+use App\Broadcasting\RealtimeService;
|
|
|
use App\Exceptions\ScheduleStatusTransitionException;
|
|
|
use App\Enums\ServicePackageStatusEnum;
|
|
|
use App\Enums\UserTypeEnum;
|
|
|
@@ -14,6 +17,11 @@ use App\Rules\ScheduleBusinessRules;
|
|
|
use App\Services\NotificationService;
|
|
|
use App\Services\PushNotificationService;
|
|
|
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\PrestadorCancelouPush;
|
|
|
+use App\Notifications\Push\Prestador\Agendamento\ClienteCancelouPush;
|
|
|
use App\Notifications\Push\Prestador\Agendamento\NewPushRequest;
|
|
|
use Carbon\Carbon;
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
@@ -24,6 +32,10 @@ class ScheduleService
|
|
|
{
|
|
|
private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];
|
|
|
|
|
|
+ public function __construct(
|
|
|
+ private readonly RealtimeService $realtime
|
|
|
+ ) {}
|
|
|
+
|
|
|
public function getAll()
|
|
|
{
|
|
|
return Schedule::with(['client.user', 'provider.user', 'address'])
|
|
|
@@ -93,6 +105,17 @@ class ScheduleService
|
|
|
}
|
|
|
|
|
|
|
|
|
+ $this->realtime->emit(
|
|
|
+ RealtimeEvent::SCHEDULE_CREATED,
|
|
|
+ $this->scheduleRooms($newSchedule),
|
|
|
+ [
|
|
|
+ 'entity' => 'schedule',
|
|
|
+ 'id' => $newSchedule->id,
|
|
|
+ 'status' => $newSchedule->status,
|
|
|
+ 'schedule_type' => $newSchedule->schedule_type,
|
|
|
+ ],
|
|
|
+ );
|
|
|
+
|
|
|
$createdSchedules[] = $newSchedule;
|
|
|
}
|
|
|
|
|
|
@@ -161,7 +184,7 @@ class ScheduleService
|
|
|
}
|
|
|
|
|
|
$allowedTransitions = [
|
|
|
- 'pending' => ['accepted', 'rejected', 'cancelled'],
|
|
|
+ 'pending' => ['accepted', 'rejected', 'paid', 'cancelled'],
|
|
|
'accepted' => ['paid', 'cancelled'],
|
|
|
'paid' => ['cancelled', 'started'],
|
|
|
'started' => ['finished'],
|
|
|
@@ -177,6 +200,7 @@ class ScheduleService
|
|
|
}
|
|
|
|
|
|
if (! in_array($status, data_get($allowedTransitions, $currentStatus))) {
|
|
|
+ log::info("Transição de status inválida: {$currentStatus} para {$status}");
|
|
|
throw new ScheduleStatusTransitionException;
|
|
|
}
|
|
|
|
|
|
@@ -191,7 +215,6 @@ class ScheduleService
|
|
|
|
|
|
case 'accepted':
|
|
|
$notificationService = app(NotificationService::class);
|
|
|
-
|
|
|
switch (Auth::user()?->type) {
|
|
|
case UserTypeEnum::PROVIDER:
|
|
|
$notificationService->create([
|
|
|
@@ -219,6 +242,8 @@ class ScheduleService
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
+ $this->sendClientAcceptedPush($schedule);
|
|
|
+
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
@@ -240,6 +265,7 @@ class ScheduleService
|
|
|
'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_CANCELLED->value,
|
|
|
'user_id' => $schedule->provider->user_id,
|
|
|
]);
|
|
|
+ $this->sendProviderCancelledPush($schedule);
|
|
|
|
|
|
break;
|
|
|
|
|
|
@@ -252,6 +278,7 @@ class ScheduleService
|
|
|
'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_CANCELLED->value,
|
|
|
'user_id' => $schedule->client->user_id,
|
|
|
]);
|
|
|
+ $this->sendClientCancelledPush($schedule);
|
|
|
|
|
|
break;
|
|
|
|
|
|
@@ -306,25 +333,18 @@ class ScheduleService
|
|
|
|
|
|
case 'paid':
|
|
|
$notificationService = app(NotificationService::class);
|
|
|
-
|
|
|
- switch (Auth::user()?->type) {
|
|
|
- case UserTypeEnum::CLIENT:
|
|
|
- if ($schedule->provider_id) {
|
|
|
- $notificationService->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,
|
|
|
- ]);
|
|
|
- }
|
|
|
-
|
|
|
- break;
|
|
|
-
|
|
|
- default:
|
|
|
- break;
|
|
|
+ if ($schedule->provider_id) {
|
|
|
+
|
|
|
+ $notificationService->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,
|
|
|
+ ]);
|
|
|
}
|
|
|
+ $this->sendClientPaymentPush($schedule);
|
|
|
|
|
|
$date_cleaned = Carbon::parse($schedule->date)
|
|
|
->format('Y-m-d');
|
|
|
@@ -350,9 +370,25 @@ class ScheduleService
|
|
|
'user_id' => $schedule->client->user_id,
|
|
|
]);
|
|
|
|
|
|
+ $this->sendProviderRefusedPush($schedule);
|
|
|
+
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
+ $actor = Auth::user()?->type;
|
|
|
+
|
|
|
+ $this->realtime->emit(
|
|
|
+ RealtimeEvent::SCHEDULE_STATUS_CHANGED,
|
|
|
+ $this->scheduleRooms($schedule),
|
|
|
+ [
|
|
|
+ 'entity' => 'schedule',
|
|
|
+ 'id' => $schedule->id,
|
|
|
+ 'status' => $status,
|
|
|
+ 'schedule_type' => $schedule->schedule_type,
|
|
|
+ 'actor' => $actor instanceof UserTypeEnum ? strtolower($actor->value) : 'system',
|
|
|
+ ],
|
|
|
+ );
|
|
|
+
|
|
|
DB::commit();
|
|
|
|
|
|
return $schedule->fresh(['client.user', 'provider.user', 'address']);
|
|
|
@@ -369,46 +405,66 @@ class ScheduleService
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * @return RealtimeRoom[]
|
|
|
+ */
|
|
|
+ private function scheduleRooms(Schedule $schedule): array
|
|
|
+ {
|
|
|
+ $rooms = [
|
|
|
+ RealtimeRoom::schedule($schedule->id),
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($schedule->client?->user_id) {
|
|
|
+ $rooms[] = RealtimeRoom::user($schedule->client->user_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($schedule->provider?->user_id) {
|
|
|
+ $rooms[] = RealtimeRoom::user($schedule->provider->user_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $rooms;
|
|
|
+ }
|
|
|
+
|
|
|
//
|
|
|
|
|
|
public function getClientProviderBlocks(int $clientId, int $providerId): array
|
|
|
{
|
|
|
- $today = Carbon::today()->format('Y-m-d');
|
|
|
-
|
|
|
- $schedules = Schedule::where('client_id', $clientId)
|
|
|
- ->where('provider_id', $providerId)
|
|
|
- ->whereNotIn('status', self::EXCLUDED_STATUSES)
|
|
|
- ->whereDate('date', '>=', $today)
|
|
|
- ->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()
|
|
|
@@ -515,6 +571,20 @@ class ScheduleService
|
|
|
|
|
|
$this->cascadeCancelServicePackages($schedule, $cancelText, $cancelled_by);
|
|
|
|
|
|
+ $actor = Auth::user()?->type;
|
|
|
+
|
|
|
+ $this->realtime->emit(
|
|
|
+ RealtimeEvent::SCHEDULE_STATUS_CHANGED,
|
|
|
+ $this->scheduleRooms($schedule),
|
|
|
+ [
|
|
|
+ 'entity' => 'schedule',
|
|
|
+ 'id' => $schedule->id,
|
|
|
+ 'status' => 'cancelled',
|
|
|
+ 'schedule_type' => $schedule->schedule_type,
|
|
|
+ 'actor' => $actor instanceof UserTypeEnum ? strtolower($actor->value) : 'system',
|
|
|
+ ],
|
|
|
+ );
|
|
|
+
|
|
|
DB::commit();
|
|
|
|
|
|
return $schedule->fresh(['client.user', 'provider.user', 'address']);
|
|
|
@@ -550,26 +620,28 @@ class ScheduleService
|
|
|
$siblingSchedules = $package->items->pluck('schedule')->filter();
|
|
|
|
|
|
$siblingSchedules
|
|
|
- ->filter(fn (Schedule $sibling) => $sibling->id !== $schedule->id
|
|
|
+ ->filter(fn(Schedule $sibling) => $sibling->id !== $schedule->id
|
|
|
&& in_array($sibling->status, ['pending', 'accepted', 'paid'], true))
|
|
|
- ->each(fn (Schedule $sibling) => $sibling->update([
|
|
|
+ ->each(fn(Schedule $sibling) => $sibling->update([
|
|
|
'status' => 'cancelled',
|
|
|
'cancel_text' => $cancelText,
|
|
|
'cancelled_by' => $cancelledBy,
|
|
|
]));
|
|
|
|
|
|
$hasRealizedSchedule = $siblingSchedules->contains(
|
|
|
- fn (Schedule $sibling) => in_array($sibling->status, ['started', 'finished'], true),
|
|
|
+ fn(Schedule $sibling) => in_array($sibling->status, ['started', 'finished'], true),
|
|
|
);
|
|
|
|
|
|
- if ($package->status === ServicePackageStatusEnum::OPEN
|
|
|
- || ($package->status === ServicePackageStatusEnum::PAID && ! $hasRealizedSchedule)) {
|
|
|
+ if (
|
|
|
+ $package->status === ServicePackageStatusEnum::OPEN
|
|
|
+ || ($package->status === ServicePackageStatusEnum::PAID && ! $hasRealizedSchedule)
|
|
|
+ ) {
|
|
|
$package->update(['status' => ServicePackageStatusEnum::CANCELLED->value]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- //
|
|
|
+ //Notificações por push do sistema
|
|
|
|
|
|
private function sendProviderAcceptedPush(Schedule $schedule): void
|
|
|
{
|
|
|
@@ -597,6 +669,147 @@ class ScheduleService
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private function sendProviderRefusedPush(Schedule $schedule): void
|
|
|
+ {
|
|
|
+ $user = $schedule->client->user;
|
|
|
+
|
|
|
+ if (! $user) {
|
|
|
+ Log::warning('Push de recusa ignorada: cliente sem usuário', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ app(PushNotificationService::class)->sendToUser(
|
|
|
+ $user,
|
|
|
+ new PrestadorRecusouPush(
|
|
|
+ $schedule->provider->user->name
|
|
|
+ )
|
|
|
+ );
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
+ Log::error('Falha ao enviar push de recusa do prestador', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ 'user_id' => $user->id,
|
|
|
+ 'error' => $exception->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function sendClientAcceptedPush(Schedule $schedule): void
|
|
|
+ {
|
|
|
+ $user = $schedule->provider?->user;
|
|
|
+
|
|
|
+ if (! $user) {
|
|
|
+ Log::warning('Push de aceite do cliente ignorado: prestador sem usuário', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ app(PushNotificationService::class)->sendToUser(
|
|
|
+ $user,
|
|
|
+ new ClienteAceitouPush(
|
|
|
+ $schedule->client->user->name
|
|
|
+ )
|
|
|
+ );
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
+ Log::error('Falha ao enviar push de aceite do cliente', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ 'user_id' => $user->id,
|
|
|
+ 'error' => $exception->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function sendClientPaymentPush(Schedule $schedule): void
|
|
|
+ {
|
|
|
+ $user = $schedule->provider?->user;
|
|
|
+ if (! $user) {
|
|
|
+ Log::warning('Push de pagamento ignorado: prestador sem usuário', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ app(PushNotificationService::class)->sendToUser(
|
|
|
+ $user,
|
|
|
+ new ClienteEfetuouPagamentoPush(
|
|
|
+ $schedule->client?->user?->name ?? 'Cliente'
|
|
|
+ )
|
|
|
+
|
|
|
+ );
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
+ Log::error('Falha ao enviar push de pagamento ao prestador', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ 'provider_id' => $schedule->provider_id,
|
|
|
+ 'user_id' => $user->id,
|
|
|
+ 'error' => $exception->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function sendClientCancelledPush(Schedule $schedule): void
|
|
|
+ {
|
|
|
+ $user = $schedule->provider->user;
|
|
|
+
|
|
|
+ if (! $user) {
|
|
|
+ Log::warning('Push de cancelamento ignorado: prestador sem usuário', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ app(PushNotificationService::class)->sendToUser(
|
|
|
+ $user,
|
|
|
+ new ClienteCancelouPush(
|
|
|
+ $schedule->client->user->name
|
|
|
+ )
|
|
|
+ );
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
+ Log::error('Falha ao enviar push de cancelamento pelo cliente', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ 'user_id' => $user->id,
|
|
|
+ 'error' => $exception->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function sendProviderCancelledPush(Schedule $schedule): void
|
|
|
+ {
|
|
|
+ $user = $schedule->client->user;
|
|
|
+
|
|
|
+ if (! $user) {
|
|
|
+ Log::warning('Push de cancelamento ignorado: cliente sem usuário', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ app(PushNotificationService::class)->sendToUser(
|
|
|
+ $user,
|
|
|
+ new PrestadorCancelouPush(
|
|
|
+ $schedule->provider->user->name
|
|
|
+ )
|
|
|
+ );
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
+ Log::error('Falha ao enviar push de cancelamento pelo prestador', [
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
+ 'user_id' => $user->id,
|
|
|
+ 'error' => $exception->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private function calculateAmount(Provider $provider, string $periodType): float
|
|
|
{
|
|
|
$hourlyRates = [
|