ScheduleService.php 35 KB

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