ScheduleService.php 41 KB

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