ScheduleService.php 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186
  1. <?php
  2. namespace App\Services;
  3. use App\Broadcasting\RealtimeEvent;
  4. use App\Broadcasting\RealtimeRoom;
  5. use App\Broadcasting\RealtimeService;
  6. use App\Exceptions\ScheduleStatusTransitionException;
  7. use App\Enums\ServicePackageStatusEnum;
  8. use App\Enums\UserTypeEnum;
  9. use App\Enums\NotificationTypeEnum;
  10. use App\Jobs\StartScheduleJob;
  11. use App\Jobs\ScheduleStartingSoonJob;
  12. use App\Models\Provider;
  13. use App\Models\Schedule;
  14. use App\Models\ServicePackage;
  15. use App\Rules\ScheduleBusinessRules;
  16. use App\Services\NotificationService;
  17. use App\Services\PushNotificationService;
  18. use App\Notifications\Push\Cliente\Agendamento\PrestadorAceitouPush;
  19. use App\Notifications\Push\Cliente\Agendamento\PrestadorRecusouPush;
  20. use App\Notifications\Push\Prestador\Agendamento\ClienteAceitouPush;
  21. use App\Notifications\Push\Prestador\Pagamento\ClienteEfetuouPagamentoPush;
  22. use App\Notifications\Push\Cliente\Agendamento\AgendamentoProximoPrestadorPush;
  23. use App\Notifications\Push\Prestador\Agendamento\AgendamentoProximoClientePush;
  24. use App\Notifications\Push\Cliente\Agendamento\PrestadorCancelouPush;
  25. use App\Notifications\Push\Prestador\Agendamento\ClienteCancelouPush;
  26. use App\Notifications\Push\Prestador\Agendamento\PrestadorFaltouPush;
  27. use App\Notifications\Push\Prestador\Agendamento\NewPushRequest;
  28. use App\Enums\BlockedPeriodEnum;
  29. use App\Models\ProviderBlockedDay;
  30. use App\Models\ProviderWorkingDay;
  31. use App\Services\ProviderBlockedDayService;
  32. use Carbon\Carbon;
  33. use Illuminate\Support\Facades\Auth;
  34. use Illuminate\Support\Facades\DB;
  35. use Illuminate\Support\Facades\Log;
  36. class ScheduleService
  37. {
  38. private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];
  39. public function __construct(
  40. private readonly RealtimeService $realtime,
  41. private readonly ProviderBlockedDayService $providerBlockedDayService
  42. ) {}
  43. public function getAll()
  44. {
  45. return Schedule::with(['client.user', 'provider.user', 'address'])
  46. ->where('schedule_type', 'default')
  47. ->orderBy('date', 'desc')
  48. ->orderBy('start_time', 'desc')
  49. ->get();
  50. }
  51. public function getById($id)
  52. {
  53. return Schedule::with(['client.user', 'provider.user', 'address'])->findOrFail($id);
  54. }
  55. public function create(array $data): Schedule
  56. {
  57. return data_get($this->createSingleOrMultiple([], [$data]), 0);
  58. }
  59. public function createSingleOrMultiple(array $baseData, array $schedules)
  60. {
  61. try {
  62. DB::beginTransaction();
  63. $createdSchedules = [];
  64. foreach ($schedules as $schedule) {
  65. $datasMerged = array_merge($baseData, $schedule);
  66. if (data_get($datasMerged, 'schedule_type', 'default') === 'default') {
  67. $provider = Provider::findOrFail(data_get($datasMerged, 'provider_id'));
  68. $datasMerged['total_amount'] = $this->calculateAmount(
  69. $provider,
  70. (string) data_get($datasMerged, 'period_type'),
  71. );
  72. }
  73. $this->validateProviderAvailability($datasMerged, null);
  74. $scheduleData = array_merge($datasMerged, [
  75. 'code' => str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT),
  76. ]);
  77. $newSchedule = Schedule::create($scheduleData);
  78. // NOTIFICAÇÃO PRESTADOR
  79. if ($newSchedule->provider_id) {
  80. $notificationService = app(NotificationService::class);
  81. $notificationService->create([
  82. 'title' => __('notifications.new_schedule_request_title'),
  83. 'description' => __('notifications.new_schedule_request_description'),
  84. 'origin' => 'schedule',
  85. 'origin_id' => $newSchedule->id,
  86. 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_NEW_SOLICITATION->value,
  87. 'user_id' => $newSchedule->provider->user_id,
  88. ]);
  89. // Push Notification
  90. $pushNotificationService = app(PushNotificationService::class);
  91. $pushNotificationService->sendToUser(
  92. $newSchedule->provider->user,
  93. new NewPushRequest($newSchedule->client->user->name)
  94. );
  95. }
  96. $this->realtime->emit(
  97. RealtimeEvent::SCHEDULE_CREATED,
  98. $this->scheduleRooms($newSchedule),
  99. [
  100. 'entity' => 'schedule',
  101. 'id' => $newSchedule->id,
  102. 'status' => $newSchedule->status,
  103. 'schedule_type' => $newSchedule->schedule_type,
  104. ],
  105. );
  106. $createdSchedules[] = $newSchedule;
  107. }
  108. DB::commit();
  109. } catch (\Exception $e) {
  110. DB::rollBack();
  111. throw $e;
  112. }
  113. return $createdSchedules;
  114. }
  115. public function update($id, array $data)
  116. {
  117. unset($data['status']);
  118. $schedule = Schedule::with(['provider.user', 'client.user', 'address'])->findOrFail($id);
  119. if (data_get($data, 'provider_id') !== null || data_get($data, 'period_type') !== null) {
  120. $providerId = data_get($data, 'provider_id', $schedule->provider_id);
  121. $periodType = data_get($data, 'period_type', $schedule->period_type);
  122. $provider = Provider::findOrFail($providerId);
  123. $data['total_amount'] = $this->calculateAmount($provider, $periodType);
  124. }
  125. if (data_get($data, 'date') !== null || data_get($data, 'start_time') !== null || data_get($data, 'provider_id') !== null) {
  126. $validationData = array_merge($schedule->toArray(), $data);
  127. $this->validateProviderAvailability($validationData, $id);
  128. }
  129. $schedule->update($data);
  130. return $schedule->fresh(['client.user', 'provider.user', 'address']);
  131. }
  132. public function delete($id)
  133. {
  134. $schedule = Schedule::findOrFail($id);
  135. $schedule->delete();
  136. return $schedule;
  137. }
  138. //
  139. //
  140. public function updateStatus($id, string $status, bool $fromPackage = false, bool $isProviderAbsence = false)
  141. {
  142. try {
  143. DB::beginTransaction();
  144. $schedule = Schedule::with(['provider.user', 'client.user', 'address'])->findOrFail($id);
  145. if (! $fromPackage && in_array($status, ['accepted', 'rejected']) && Auth::user()?->type === UserTypeEnum::PROVIDER) {
  146. $belongsToServicePackage = DB::table('service_package_items')
  147. ->where('schedule_id', $schedule->id)
  148. ->exists();
  149. if ($belongsToServicePackage) {
  150. throw new \DomainException(__('messages.schedule_belongs_to_package_use_package_endpoint'));
  151. }
  152. }
  153. $allowedTransitions = [
  154. 'pending' => ['accepted', 'rejected', 'paid', 'cancelled'],
  155. 'accepted' => ['paid', 'cancelled'],
  156. 'paid' => ['cancelled', 'started'],
  157. 'started' => ['finished'],
  158. 'rejected' => [],
  159. 'cancelled' => [],
  160. 'finished' => [],
  161. ];
  162. $currentStatus = $schedule->status;
  163. if (
  164. $isProviderAbsence &&
  165. $currentStatus === 'started' &&
  166. $status === 'cancelled'
  167. ) {
  168. $allowedTransitions['started'][] = 'cancelled';
  169. }
  170. if (data_get($allowedTransitions, $currentStatus) === null) {
  171. throw new ScheduleStatusTransitionException;
  172. }
  173. if (! in_array($status, data_get($allowedTransitions, $currentStatus))) {
  174. log::info("Transição de status inválida: {$currentStatus} para {$status}");
  175. throw new ScheduleStatusTransitionException;
  176. }
  177. $schedule->update(['status' => $status]);
  178. $schedule->refresh();
  179. $currentStatus = $schedule->status;
  180. switch ($status) {
  181. case 'pending':
  182. break;
  183. case 'accepted':
  184. $notificationService = app(NotificationService::class);
  185. switch (Auth::user()?->type) {
  186. case UserTypeEnum::PROVIDER:
  187. $notificationService->create([
  188. 'title' => __('notifications.schedule_accepted_title'),
  189. 'description' => __('notifications.provider_accepted_schedule_description', ['provider' => $schedule->provider->user->name]),
  190. 'origin' => 'schedule',
  191. 'origin_id' => $schedule->id,
  192. 'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_ACCEPTED->value,
  193. 'user_id' => $schedule->client->user_id,
  194. ]);
  195. $this->sendProviderAcceptedPush($schedule);
  196. break;
  197. case UserTypeEnum::CLIENT:
  198. if ($schedule->provider_id) {
  199. $notificationService->create([
  200. 'title' => __('notifications.proposal_accepted_title'),
  201. 'description' => __('notifications.proposal_accepted_description'),
  202. 'origin' => 'schedule',
  203. 'origin_id' => $schedule->id,
  204. 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_PROPOSAL_ACCEPTED->value,
  205. 'user_id' => $schedule->provider->user_id,
  206. ]);
  207. }
  208. $this->sendClientAcceptedPush($schedule);
  209. break;
  210. default:
  211. break;
  212. }
  213. break;
  214. //tem que chamar o status cancel por causa da regra de push
  215. case 'cancelled':
  216. $notificationService = app(NotificationService::class);
  217. if ($schedule->cancelled_due_to_provider_absence) {
  218. // Cancelamento por falta do prestador.
  219. // Aqui enviamos a notificação específica para o prestador.
  220. $this->sendProviderAbsencePush($schedule);
  221. break;
  222. }
  223. switch (Auth::user()?->type) {
  224. case UserTypeEnum::CLIENT:
  225. $user = $schedule->provider?->user;
  226. if (!$user) {
  227. break;
  228. }
  229. $notificationService->create([
  230. 'title' => __('notifications.schedule_cancelled_title'),
  231. 'description' => __('notifications.client_cancelled_schedule_description'),
  232. 'origin' => 'schedule',
  233. 'origin_id' => $schedule->id,
  234. 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_CANCELLED->value,
  235. 'user_id' => $user->id,
  236. ]);
  237. $this->sendClientCancelledPush($schedule);
  238. break;
  239. case UserTypeEnum::PROVIDER:
  240. $notificationService->create([
  241. 'title' => __('notifications.schedule_cancelled_title'),
  242. 'description' => __('notifications.provider_cancelled_schedule_description', [
  243. 'provider' => $schedule->provider->user->name
  244. ]),
  245. 'origin' => 'schedule',
  246. 'origin_id' => $schedule->id,
  247. 'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_CANCELLED->value,
  248. 'user_id' => $schedule->client->user_id,
  249. ]);
  250. $this->sendProviderCancelledPush($schedule);
  251. break;
  252. default:
  253. break;
  254. }
  255. break;
  256. case 'started':
  257. $notificationService = app(NotificationService::class);
  258. // CLIENTE
  259. $notificationService->create([
  260. 'title' => __('notifications.provider_on_the_way_title'),
  261. 'description' => __('notifications.provider_on_the_way_description', ['code' => $schedule->code]),
  262. 'origin' => 'schedule',
  263. 'origin_id' => $schedule->id,
  264. 'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_COMING->value,
  265. 'user_id' => $schedule->client->user_id,
  266. ]);
  267. // PRESTADOR
  268. $notificationService->create([
  269. 'title' => __('notifications.service_start_title'),
  270. 'description' => __('notifications.service_start_description'),
  271. 'origin' => 'schedule',
  272. 'origin_id' => $schedule->id,
  273. 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
  274. 'user_id' => $schedule->provider->user_id,
  275. ]);
  276. break;
  277. case 'finished':
  278. $notificationService = app(NotificationService::class);
  279. // CLIENTE
  280. $notificationService->create([
  281. 'title' => __('notifications.service_finished_title'),
  282. 'description' => __('notifications.service_finished_description'),
  283. 'origin' => 'schedule',
  284. 'origin_id' => $schedule->id,
  285. 'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_FINISHED->value,
  286. 'user_id' => $schedule->client->user_id,
  287. ]);
  288. break;
  289. case 'paid':
  290. $notificationService = app(NotificationService::class);
  291. if ($schedule->provider_id) {
  292. $notificationService->create([
  293. 'title' => __('notifications.payment_confirmed_title'),
  294. 'description' => __('notifications.payment_confirmed_description'),
  295. 'origin' => 'schedule',
  296. 'origin_id' => $schedule->id,
  297. 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
  298. 'user_id' => $schedule->provider->user_id,
  299. ]);
  300. }
  301. $this->sendClientPaymentPush($schedule);
  302. $date_cleaned = Carbon::parse($schedule->date)
  303. ->format('Y-m-d');
  304. $start_date_time = Carbon::parse(
  305. $date_cleaned . ' ' . $schedule->start_time
  306. );
  307. // =====================================================
  308. // ScheduleStartingSoonJob
  309. // =====================================================
  310. // TESTE LOCAL: dispara 15 segundos depois do pagamento
  311. // ScheduleStartingSoonJob::dispatch($schedule->id)
  312. // ->delay(now()->addSeconds(15));
  313. // PRODUÇÃO: dispara 1 hora antes do início
  314. $notification_date_time = $start_date_time->copy()->subHour();
  315. ScheduleStartingSoonJob::dispatch($schedule->id)
  316. ->delay($notification_date_time);
  317. // =====================================================
  318. // StartScheduleJob
  319. // =====================================================
  320. // Aqui continua sendo o horário REAL de início
  321. StartScheduleJob::dispatch($schedule->id)
  322. ->delay($start_date_time);
  323. break;
  324. case 'rejected':
  325. $notificationService = app(NotificationService::class);
  326. $notificationService->create([
  327. 'title' => __('notifications.schedule_refused_title'),
  328. 'description' => __('notifications.schedule_refused_description'),
  329. 'origin' => 'schedule',
  330. 'origin_id' => $schedule->id,
  331. 'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_REFUSED->value,
  332. 'user_id' => $schedule->client->user_id,
  333. ]);
  334. $this->sendProviderRefusedPush($schedule);
  335. break;
  336. }
  337. $actor = Auth::user()?->type;
  338. $this->realtime->emit(
  339. RealtimeEvent::SCHEDULE_STATUS_CHANGED,
  340. $this->scheduleRooms($schedule),
  341. [
  342. 'entity' => 'schedule',
  343. 'id' => $schedule->id,
  344. 'status' => $status,
  345. 'schedule_type' => $schedule->schedule_type,
  346. 'actor' => $actor instanceof UserTypeEnum ? strtolower($actor->value) : 'system',
  347. ],
  348. );
  349. DB::commit();
  350. return $schedule->fresh(['client.user', 'provider.user', 'address']);
  351. } catch (ScheduleStatusTransitionException $e) {
  352. DB::rollBack();
  353. throw $e;
  354. } catch (\Exception $e) {
  355. DB::rollBack();
  356. Log::error('Erro ao atualizar status do agendamento: ' . $e->getMessage());
  357. throw $e;
  358. }
  359. }
  360. /**
  361. * @return RealtimeRoom[]
  362. */
  363. private function scheduleRooms(Schedule $schedule): array
  364. {
  365. $rooms = [
  366. RealtimeRoom::schedule($schedule->id),
  367. ];
  368. if ($schedule->client?->user_id) {
  369. $rooms[] = RealtimeRoom::user($schedule->client->user_id);
  370. }
  371. if ($schedule->provider?->user_id) {
  372. $rooms[] = RealtimeRoom::user($schedule->provider->user_id);
  373. }
  374. return $rooms;
  375. }
  376. //
  377. public function getClientProviderBlocks(int $clientId, int $providerId): array
  378. {
  379. $weekStart = Carbon::today()->startOfWeek(Carbon::SUNDAY)->format('Y-m-d');
  380. $schedules = Schedule::where('client_id', $clientId)
  381. ->where('provider_id', $providerId)
  382. ->whereNotIn('status', self::EXCLUDED_STATUSES)
  383. ->whereDate('date', '>=', $weekStart)
  384. ->orderBy('date')
  385. ->orderBy('start_time')
  386. ->get(['id', 'date', 'start_time', 'end_time', 'status']);
  387. $existingSchedules = $schedules->map(function ($schedule) {
  388. return [
  389. 'id' => $schedule->id,
  390. 'date' => Carbon::parse($schedule->date)->format('Y-m-d'),
  391. 'start_time' => $schedule->start_time,
  392. 'end_time' => $schedule->end_time,
  393. 'status' => $schedule->status,
  394. ];
  395. })->values();
  396. $fullyBlockedWeeks = $schedules
  397. ->groupBy(function ($schedule) {
  398. return Carbon::parse($schedule->date)
  399. ->startOfWeek(Carbon::SUNDAY)
  400. ->format('Y-m-d');
  401. })
  402. ->filter(function ($weekSchedules) {
  403. return $weekSchedules->count() >= 2;
  404. })
  405. ->keys()
  406. ->values();
  407. return [
  408. 'existing_schedules' => $existingSchedules,
  409. 'fully_blocked_weeks' => $fullyBlockedWeeks,
  410. ];
  411. }
  412. public function getFinished()
  413. {
  414. return Schedule::with(['client.user', 'provider.user'])
  415. ->where('status', 'finished')
  416. ->orderBy('date', 'desc')
  417. ->orderBy('start_time', 'desc')
  418. ->get();
  419. }
  420. public function getSchedulesDefaultGroupedByClient()
  421. {
  422. $schedules = Schedule::with(['client.user', 'provider.user', 'address', 'reviews.reviewsImprovements.improvementType'])
  423. ->orderBy('id', 'desc')
  424. ->where('schedule_type', 'default')
  425. ->select(
  426. 'schedules.*'
  427. )
  428. ->get();
  429. $grouped = $schedules->groupBy('client_id')->map(function ($clientSchedules) {
  430. $firstSchedule = $clientSchedules->first();
  431. return [
  432. 'client_id' => $firstSchedule->client_id,
  433. 'client_name' => $firstSchedule->client->user->name ?? 'N/A',
  434. 'schedules' => $clientSchedules->map(function ($schedule) {
  435. return [
  436. 'id' => $schedule->id,
  437. 'date' => $schedule->date ? Carbon::parse($schedule->date)->format('d/m/Y') : null,
  438. 'start_time' => $schedule->start_time,
  439. 'end_time' => $schedule->end_time,
  440. 'period_type' => $schedule->period_type,
  441. 'status' => $schedule->status,
  442. 'total_amount' => $schedule->total_amount,
  443. 'code' => $schedule->code,
  444. 'code_verified' => $schedule->code_verified,
  445. 'client_id' => $schedule->client_id,
  446. 'provider_id' => $schedule->provider_id,
  447. 'provider_name' => $schedule->provider->user->name ?? 'N/A',
  448. 'address' => $schedule->address ? [
  449. 'id' => $schedule->address->id,
  450. 'address' => $schedule->address->address,
  451. 'complement' => $schedule->address->complement,
  452. 'zip_code' => $schedule->address->zip_code,
  453. 'city' => $schedule->address->city->name ?? '',
  454. 'state' => $schedule->address->city->state->name ?? '',
  455. ] : null,
  456. 'client_name' => $schedule->client->user->name ?? 'N/A',
  457. 'reviews' => $schedule->reviews->map(function ($review) {
  458. return [
  459. 'id' => $review->id,
  460. 'stars' => $review->stars,
  461. 'comment' => $review->comment,
  462. 'origin' => $review->origin,
  463. 'origin_id' => $review->origin_id,
  464. 'created_at' => Carbon::parse($review->created_at)->format('Y-m-d H:i'),
  465. 'updated_at' => Carbon::parse($review->updated_at)->format('Y-m-d H:i'),
  466. 'improvements' => $review->reviewsImprovements->map(function ($ri) {
  467. return [
  468. 'id' => $ri->id,
  469. 'improvement_type_id' => $ri->improvement_type_id,
  470. 'improvement_type_name' => $ri->improvementType ? $ri->improvementType->description : null,
  471. ];
  472. })->values(),
  473. ];
  474. }),
  475. ];
  476. })->values(),
  477. ];
  478. })->sortBy('id')->values();
  479. return $grouped;
  480. }
  481. //
  482. public function cancelWithReason(int $id, string $cancelText)
  483. {
  484. try {
  485. DB::beginTransaction();
  486. $schedule = Schedule::findOrFail($id);
  487. $allowedStatuses = ['accepted', 'paid', 'pending'];
  488. if (! in_array($schedule->status, $allowedStatuses)) {
  489. throw new ScheduleStatusTransitionException;
  490. }
  491. $cancelled_by = Auth::user()->type;
  492. $schedule->update([
  493. 'cancel_text' => $cancelText,
  494. 'cancelled_by' => $cancelled_by,
  495. ]);
  496. $this->cascadeCancelServicePackages($schedule, $cancelText, $cancelled_by);
  497. $this->updateStatus($id, 'cancelled');
  498. $actor = Auth::user()?->type;
  499. $this->realtime->emit(
  500. RealtimeEvent::SCHEDULE_STATUS_CHANGED,
  501. $this->scheduleRooms($schedule),
  502. [
  503. 'entity' => 'schedule',
  504. 'id' => $schedule->id,
  505. 'status' => 'cancelled',
  506. 'schedule_type' => $schedule->schedule_type,
  507. 'actor' => $actor instanceof UserTypeEnum ? strtolower($actor->value) : 'system',
  508. ],
  509. );
  510. DB::commit();
  511. return $schedule->fresh(['client.user', 'provider.user', 'address']);
  512. } catch (ScheduleStatusTransitionException $e) {
  513. DB::rollBack();
  514. throw $e;
  515. } catch (\Exception $e) {
  516. DB::rollBack();
  517. Log::error('Erro ao cancelar agendamento: ' . $e->getMessage());
  518. throw $e;
  519. }
  520. }
  521. //reporta por falta
  522. public function reportProviderAbsence(
  523. int $scheduleId,
  524. string $cancelText
  525. ): Schedule {
  526. return DB::transaction(function () use ($scheduleId, $cancelText) {
  527. $schedule = Schedule::findOrFail($scheduleId);
  528. // O agendamento precisa ter um prestador vinculado
  529. if (! $schedule->provider_id) {
  530. throw new \Exception(
  531. __('messages.provider_absence_provider_not_assigned')
  532. );
  533. }
  534. // O cliente precisa ser o dono do agendamento
  535. if (Auth::user()->type !== UserTypeEnum::CLIENT) {
  536. throw new \Exception(
  537. __('messages.provider_absence_only_client')
  538. );
  539. }
  540. if (Auth::user()->client?->id !== $schedule->client_id) {
  541. throw new \Exception(
  542. __('messages.provider_absence_not_authorized')
  543. );
  544. }
  545. // O código já foi confirmado: o prestador compareceu
  546. if ($schedule->code_verified) {
  547. throw new \Exception(
  548. __('messages.provider_absence_code_already_verified')
  549. );
  550. }
  551. // O agendamento não pode estar encerrado/cancelado/rejeitado
  552. if (
  553. in_array(
  554. $schedule->status,
  555. ['cancelled', 'rejected', 'finished'],
  556. true
  557. )
  558. ) {
  559. throw new \Exception(
  560. __('messages.provider_absence_invalid_status')
  561. );
  562. }
  563. // Valida se o cliente pode informar a falta neste momento
  564. ScheduleBusinessRules::validateProviderAbsenceWindow($schedule);
  565. // Registra o motivo informado pelo cliente
  566. $schedule->update([
  567. 'cancel_text' => $cancelText,
  568. 'cancelled_by' => Auth::user()->type,
  569. 'cancelled_due_to_provider_absence' => true,
  570. ]);
  571. // Cancela o agendamento
  572. $this->updateStatus(
  573. $schedule->id,
  574. 'cancelled',
  575. false,
  576. true
  577. );
  578. // Aplica a penalidade de bloqueio de 3 dias úteis do prestador.
  579. $this->blockProviderPenaltyDays($schedule);
  580. // TODO: Implementar estorno integral do pagamento.
  581. // Esta etapa será implementada posteriormente por outro responsável.
  582. return $schedule->fresh([
  583. 'client.user',
  584. 'provider.user',
  585. 'address',
  586. ]);
  587. });
  588. }
  589. //penalidade por falta bloqueio de 3 dias validos
  590. // penalidade por falta - bloqueio de 3 dias válidos
  591. private function blockProviderPenaltyDays(Schedule $schedule): void
  592. {
  593. $providerId = $schedule->provider_id;
  594. $date = Carbon::parse($schedule->date)->startOfDay();
  595. $blockedDaysCount = 0;
  596. while ($blockedDaysCount < 3) {
  597. $date->addDay();
  598. $dayOfWeek = $date->dayOfWeek;
  599. // 1. O prestador precisa trabalhar nesse dia da semana.
  600. $worksOnDay = ProviderWorkingDay::query()
  601. ->where('provider_id', $providerId)
  602. ->where('day', $dayOfWeek)
  603. ->exists();
  604. if (! $worksOnDay) {
  605. continue;
  606. }
  607. // 2. Se já existe qualquer ProviderBlockedDay nessa data,
  608. // a data não pode ser utilizada como penalidade.
  609. $alreadyBlocked = ProviderBlockedDay::query()
  610. ->where('provider_id', $providerId)
  611. ->whereDate('date', $date->format('Y-m-d'))
  612. ->exists();
  613. if ($alreadyBlocked) {
  614. continue;
  615. }
  616. // 3. Se existe qualquer agendamento ativo nessa data,
  617. // não podemos bloquear o dia inteiro.
  618. $hasSchedule = Schedule::query()
  619. ->where('provider_id', $providerId)
  620. ->whereDate('date', $date->format('Y-m-d'))
  621. ->whereNotIn('status', self::EXCLUDED_STATUSES)
  622. ->exists();
  623. if ($hasSchedule) {
  624. continue;
  625. }
  626. // 4. Encontramos um dia de trabalho completamente livre.
  627. $this->providerBlockedDayService->create([
  628. 'provider_id' => $providerId,
  629. 'date' => $date->format('Y-m-d'),
  630. 'period' => BlockedPeriodEnum::ALL->value,
  631. 'reason' => 'Bloqueio de 3 dias por falta do prestador.',
  632. 'type' => 'auto_cancel',
  633. 'init_hour' => '07:00',
  634. 'end_hour' => '20:00',
  635. // Identifica que este bloqueio é uma penalidade
  636. // e não pode ser alterado/desbloqueado manualmente.
  637. 'blocked_due_to_provider_absence' => true,
  638. ]);
  639. $blockedDaysCount++;
  640. }
  641. }
  642. private function cascadeCancelServicePackages(Schedule $schedule, string $cancelText, $cancelledBy): void
  643. {
  644. $packageIds = DB::table('service_package_items')
  645. ->where('schedule_id', $schedule->id)
  646. ->pluck('service_package_id');
  647. if ($packageIds->isEmpty()) {
  648. return;
  649. }
  650. $packages = ServicePackage::query()
  651. ->with('items.schedule')
  652. ->whereIn('id', $packageIds)
  653. ->get();
  654. foreach ($packages as $package) {
  655. $siblingSchedules = $package->items->pluck('schedule')->filter();
  656. $siblingSchedules
  657. ->filter(fn(Schedule $sibling) => $sibling->id !== $schedule->id
  658. && in_array($sibling->status, ['pending', 'accepted', 'paid'], true))
  659. ->each(fn(Schedule $sibling) => $sibling->update([
  660. 'status' => 'cancelled',
  661. 'cancel_text' => $cancelText,
  662. 'cancelled_by' => $cancelledBy,
  663. ]));
  664. $hasRealizedSchedule = $siblingSchedules->contains(
  665. fn(Schedule $sibling) => in_array($sibling->status, ['started', 'finished'], true),
  666. );
  667. if (
  668. $package->status === ServicePackageStatusEnum::OPEN
  669. || ($package->status === ServicePackageStatusEnum::PAID && ! $hasRealizedSchedule)
  670. ) {
  671. $package->update(['status' => ServicePackageStatusEnum::CANCELLED->value]);
  672. }
  673. }
  674. }
  675. //Notificações por push do sistema
  676. private function sendProviderAcceptedPush(Schedule $schedule): void
  677. {
  678. $user = $schedule->client->user;
  679. if (! $user) {
  680. Log::warning('Push de aceite ignorada: cliente sem usuário', [
  681. 'schedule_id' => $schedule->id,
  682. ]);
  683. return;
  684. }
  685. try {
  686. app(PushNotificationService::class)->sendToUser(
  687. $user,
  688. new PrestadorAceitouPush($schedule->provider->user->name)
  689. );
  690. } catch (\Throwable $exception) {
  691. Log::error('Falha ao enviar push de aceite do prestador', [
  692. 'schedule_id' => $schedule->id,
  693. 'user_id' => $user->id,
  694. 'error' => $exception->getMessage(),
  695. ]);
  696. }
  697. }
  698. private function sendProviderRefusedPush(Schedule $schedule): void
  699. {
  700. $user = $schedule->client->user;
  701. if (! $user) {
  702. Log::warning('Push de recusa ignorada: cliente sem usuário', [
  703. 'schedule_id' => $schedule->id,
  704. ]);
  705. return;
  706. }
  707. try {
  708. app(PushNotificationService::class)->sendToUser(
  709. $user,
  710. new PrestadorRecusouPush(
  711. $schedule->provider->user->name
  712. )
  713. );
  714. } catch (\Throwable $exception) {
  715. Log::error('Falha ao enviar push de recusa do prestador', [
  716. 'schedule_id' => $schedule->id,
  717. 'user_id' => $user->id,
  718. 'error' => $exception->getMessage(),
  719. ]);
  720. }
  721. }
  722. private function sendClientAcceptedPush(Schedule $schedule): void
  723. {
  724. $user = $schedule->provider?->user;
  725. if (! $user) {
  726. Log::warning('Push de aceite do cliente ignorado: prestador sem usuário', [
  727. 'schedule_id' => $schedule->id,
  728. ]);
  729. return;
  730. }
  731. try {
  732. app(PushNotificationService::class)->sendToUser(
  733. $user,
  734. new ClienteAceitouPush(
  735. $schedule->client->user->name
  736. )
  737. );
  738. } catch (\Throwable $exception) {
  739. Log::error('Falha ao enviar push de aceite do cliente', [
  740. 'schedule_id' => $schedule->id,
  741. 'user_id' => $user->id,
  742. 'error' => $exception->getMessage(),
  743. ]);
  744. }
  745. }
  746. private function sendClientPaymentPush(Schedule $schedule): void
  747. {
  748. $user = $schedule->provider?->user;
  749. if (! $user) {
  750. Log::warning('Push de pagamento ignorado: prestador sem usuário', [
  751. 'schedule_id' => $schedule->id,
  752. ]);
  753. return;
  754. }
  755. try {
  756. app(PushNotificationService::class)->sendToUser(
  757. $user,
  758. new ClienteEfetuouPagamentoPush(
  759. $schedule->client?->user?->name ?? 'Cliente'
  760. )
  761. );
  762. } catch (\Throwable $exception) {
  763. Log::error('Falha ao enviar push de pagamento ao prestador', [
  764. 'schedule_id' => $schedule->id,
  765. 'provider_id' => $schedule->provider_id,
  766. 'user_id' => $user->id,
  767. 'error' => $exception->getMessage(),
  768. ]);
  769. }
  770. }
  771. private function sendClientCancelledPush(Schedule $schedule): void
  772. {
  773. $user = $schedule->provider->user;
  774. if (! $user) {
  775. Log::warning('Push de cancelamento ignorado: prestador sem usuário', [
  776. 'schedule_id' => $schedule->id,
  777. ]);
  778. return;
  779. }
  780. try {
  781. app(PushNotificationService::class)->sendToUser(
  782. $user,
  783. new ClienteCancelouPush(
  784. $schedule->client->user->name
  785. )
  786. );
  787. } catch (\Throwable $exception) {
  788. Log::error('Falha ao enviar push de cancelamento pelo cliente', [
  789. 'schedule_id' => $schedule->id,
  790. 'user_id' => $user->id,
  791. 'error' => $exception->getMessage(),
  792. ]);
  793. }
  794. }
  795. private function sendProviderCancelledPush(Schedule $schedule): void
  796. {
  797. $user = $schedule->client->user;
  798. if (! $user) {
  799. Log::warning('Push de cancelamento ignorado: cliente sem usuário', [
  800. 'schedule_id' => $schedule->id,
  801. ]);
  802. return;
  803. }
  804. try {
  805. app(PushNotificationService::class)->sendToUser(
  806. $user,
  807. new PrestadorCancelouPush(
  808. $schedule->provider->user->name
  809. )
  810. );
  811. } catch (\Throwable $exception) {
  812. Log::error('Falha ao enviar push de cancelamento pelo prestador', [
  813. 'schedule_id' => $schedule->id,
  814. 'user_id' => $user->id,
  815. 'error' => $exception->getMessage(),
  816. ]);
  817. }
  818. }
  819. // cancelou por falta
  820. private function sendProviderAbsencePush(Schedule $schedule): void
  821. {
  822. $user = $schedule->provider?->user;
  823. if (! $user) {
  824. Log::warning('Push de falta ignorada: prestador sem usuário', [
  825. 'schedule_id' => $schedule->id,
  826. ]);
  827. return;
  828. }
  829. try {
  830. app(PushNotificationService::class)->sendToUser(
  831. $user,
  832. new PrestadorFaltouPush()
  833. );
  834. } catch (\Throwable $exception) {
  835. Log::error('Falha ao enviar push de falta do prestador', [
  836. 'schedule_id' => $schedule->id,
  837. 'user_id' => $user->id,
  838. 'error' => $exception->getMessage(),
  839. ]);
  840. }
  841. }
  842. public function sendScheduleStartingSoonPushes(Schedule $schedule): void
  843. {
  844. $pushNotificationService = app(PushNotificationService::class);
  845. $clientUser = $schedule->client?->user;
  846. $providerUser = $schedule->provider?->user;
  847. if ($clientUser) {
  848. try {
  849. $pushNotificationService->sendToUser(
  850. $clientUser,
  851. new AgendamentoProximoPrestadorPush(
  852. $providerUser?->name ?? 'Prestador'
  853. )
  854. );
  855. } catch (\Throwable $exception) {
  856. Log::error('Falha ao enviar push de agendamento próximo para o cliente', [
  857. 'schedule_id' => $schedule->id,
  858. 'user_id' => $clientUser->id,
  859. 'error' => $exception->getMessage(),
  860. ]);
  861. }
  862. }
  863. // PUSH PARA O PRESTADOR
  864. if ($providerUser) {
  865. try {
  866. $pushNotificationService->sendToUser(
  867. $providerUser,
  868. new AgendamentoProximoClientePush(
  869. $clientUser?->name ?? 'Cliente'
  870. )
  871. );
  872. } catch (\Throwable $exception) {
  873. Log::error('Falha ao enviar push de agendamento próximo para o prestador', [
  874. 'schedule_id' => $schedule->id,
  875. 'user_id' => $providerUser->id,
  876. 'error' => $exception->getMessage(),
  877. ]);
  878. }
  879. }
  880. }
  881. //dq pra cima e as notificações
  882. private function calculateAmount(Provider $provider, string $periodType): float
  883. {
  884. $hourlyRates = [
  885. '2' => $provider->daily_price_2h ?? 0,
  886. '4' => $provider->daily_price_4h ?? 0,
  887. '6' => $provider->daily_price_6h ?? 0,
  888. '8' => $provider->daily_price_8h ?? 0,
  889. ];
  890. return data_get($hourlyRates, $periodType, 0);
  891. }
  892. private function validateProviderAvailability(array $data, $excludeScheduleId = null)
  893. {
  894. $provider_id = data_get($data, 'provider_id');
  895. $client_id = data_get($data, 'client_id');
  896. $date = Carbon::parse(data_get($data, 'date'));
  897. $dayOfWeek = $date->dayOfWeek;
  898. $startTime = data_get($data, 'start_time');
  899. $endTime = data_get($data, 'end_time');
  900. $date_ymd = $date->format('Y-m-d');
  901. $period = $startTime < '13:00:00' ? 'morning' : 'afternoon';
  902. ScheduleBusinessRules::validateProviderVisibleToCustomers($provider_id);
  903. // bloqueio 2 schedules por semana para o mesmo client e provider
  904. ScheduleBusinessRules::validateWeeklyScheduleLimit(
  905. $client_id,
  906. $provider_id,
  907. data_get($data, 'date'),
  908. $excludeScheduleId
  909. );
  910. // bloqueio provider trabalha no dia/periodo
  911. ScheduleBusinessRules::validateWorkingDay(
  912. $provider_id,
  913. $dayOfWeek,
  914. $period
  915. );
  916. // bloqueio provider tem blockedday para dia/hora
  917. ScheduleBusinessRules::validateBlockedDay(
  918. $provider_id,
  919. $date->format('Y-m-d'),
  920. $startTime,
  921. $endTime
  922. );
  923. // bloqueio provider tem outro agendamento para dia/hora
  924. ScheduleBusinessRules::validateConflictingSchedule(
  925. $provider_id,
  926. $date->format('Y-m-d'),
  927. $startTime,
  928. $endTime,
  929. $excludeScheduleId
  930. );
  931. // bloqueio provider tem outra proposta na mesma data
  932. ScheduleBusinessRules::validateConflictingProposalSameDate(
  933. $provider_id,
  934. $date_ymd,
  935. $startTime,
  936. $endTime,
  937. null
  938. );
  939. // bloqueio caso o client tenha bloqueado o provider
  940. ScheduleBusinessRules::validateClientNotBlockedByProvider(
  941. $client_id,
  942. $provider_id
  943. );
  944. // bloqueio caso o provider tenha bloqueado o client
  945. ScheduleBusinessRules::validateProviderNotBlockedByClient(
  946. $client_id,
  947. $provider_id
  948. );
  949. return true;
  950. }
  951. }