AppointmentService.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\AppointmentStatusEnum;
  4. use App\Enums\AppointmentTypeEnum;
  5. use App\Enums\NotificationRecipientEnum;
  6. use App\Enums\PartnerAgreementTypeEnum;
  7. use App\Enums\UserStatusEnum;
  8. use App\Models\Appointment;
  9. use App\Models\PartnerAgreementService;
  10. use App\Models\User;
  11. use Carbon\Carbon;
  12. use Illuminate\Database\Eloquent\Collection;
  13. use Illuminate\Pagination\LengthAwarePaginator;
  14. use Illuminate\Support\Facades\DB;
  15. use Illuminate\Support\Str;
  16. class AppointmentService
  17. {
  18. private const RELATIONS = [
  19. 'user',
  20. 'userDependent',
  21. 'partnerAgreement',
  22. 'partnerAgreementService',
  23. 'exams.partnerAgreementService',
  24. ];
  25. public function __construct(protected NotificationService $notificationService) {}
  26. public function getAll(): Collection
  27. {
  28. return Appointment::with(self::RELATIONS)
  29. ->orderBy('date', 'desc')
  30. ->get();
  31. }
  32. public function getAllByUser(int $userId): Collection
  33. {
  34. return Appointment::with(self::RELATIONS)
  35. ->where('user_id', $userId)
  36. ->orderByRaw('COALESCE(appointments.requested_at, appointments.created_at) DESC')
  37. ->orderBy('id', 'desc')
  38. ->get();
  39. }
  40. public function getAllByPartnerUser(int $userId): Collection
  41. {
  42. return Appointment::with(self::RELATIONS)
  43. ->whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))
  44. ->orderBy('date', 'desc')
  45. ->get();
  46. }
  47. public function findById(int $id): ?Appointment
  48. {
  49. return Appointment::with(self::RELATIONS)->find($id);
  50. }
  51. public function findByIdForPartnerUser(int $id, int $userId): ?Appointment
  52. {
  53. return Appointment::with(self::RELATIONS)
  54. ->whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))
  55. ->find($id);
  56. }
  57. public function create(array $data): Appointment
  58. {
  59. $data['order_number'] = $this->generateOrderNumber();
  60. $data['requested_at'] = now();
  61. return Appointment::create($data)->load(self::RELATIONS);
  62. }
  63. public function createConsulta(array $data, User $authUser): Appointment
  64. {
  65. $creatingForOther = isset($data['user_id']) && (int) $data['user_id'] !== $authUser->id;
  66. $data['type'] = AppointmentTypeEnum::CONSULTA;
  67. if ($creatingForOther || $authUser->status === UserStatusEnum::ACTIVE) {
  68. $data['status'] = AppointmentStatusEnum::CONFIRMADO;
  69. $data['auto_approved'] = true;
  70. } else {
  71. $data['status'] = AppointmentStatusEnum::PENDENTE;
  72. $data['auto_approved'] = false;
  73. }
  74. $appointment = $this->create($data);
  75. if ($creatingForOther) {
  76. $this->notifyCreation($appointment);
  77. }
  78. return $appointment;
  79. }
  80. public function notifyCreation(Appointment $model): void
  81. {
  82. $dateStr = $model->date ? Carbon::parse($model->date)->format('d/m/Y') : null;
  83. $dependent = $model->userDependent?->name;
  84. $target = $dependent ? "para o dependente {$dependent}" : 'para você';
  85. $message = $dateStr
  86. ? "Um agendamento #{$model->order_number} foi criado {$target}" . ($model->time ? " para {$dateStr} às {$model->time}" : " em {$dateStr}") . "."
  87. : "Um agendamento #{$model->order_number} foi criado {$target}.";
  88. $this->notificationService->createAutoForUser([
  89. 'title' => 'Novo agendamento',
  90. 'message' => $message,
  91. 'recipient' => NotificationRecipientEnum::ASSOCIADO,
  92. 'source' => 'appointment',
  93. 'source_id' => $model->id,
  94. ], $model->user_id);
  95. }
  96. public function update(int $id, array $data, ?int $approvingUserId = null): ?Appointment
  97. {
  98. $model = Appointment::find($id);
  99. if (!$model) {
  100. return null;
  101. }
  102. if (
  103. isset($data['status'])
  104. && $data['status'] === AppointmentStatusEnum::CONFIRMADO->value
  105. && $model->status !== AppointmentStatusEnum::CONFIRMADO
  106. ) {
  107. $data['auto_approved'] = false;
  108. $data['approved_by_user_id'] = $approvingUserId;
  109. }
  110. $model->update($data);
  111. return $model->fresh(self::RELATIONS);
  112. }
  113. public function delete(int $id): bool
  114. {
  115. $model = Appointment::find($id);
  116. if (!$model) {
  117. return false;
  118. }
  119. return $model->delete();
  120. }
  121. public function isFrozen(Appointment $appointment): bool
  122. {
  123. if ($appointment->status === AppointmentStatusEnum::RECUSADO) {
  124. return true;
  125. }
  126. return $appointment->isExame() && $appointment->accepted_at !== null;
  127. }
  128. public function getAdminCounters(): array
  129. {
  130. return [
  131. 'pendentes' => Appointment::where('status', AppointmentStatusEnum::PENDENTE)->count(),
  132. 'aguardando_aceite' => Appointment::where('status', AppointmentStatusEnum::AGUARDANDO_ACEITE)->count(),
  133. 'aprovados' => Appointment::where('status', AppointmentStatusEnum::CONFIRMADO)->count(),
  134. 'recusados' => Appointment::where('status', AppointmentStatusEnum::RECUSADO)->count(),
  135. ];
  136. }
  137. public function getAllPaginated(array $filters = [], int $perPage = 10): LengthAwarePaginator
  138. {
  139. $query = Appointment::with(self::RELATIONS)
  140. ->orderBy('requested_at', 'desc');
  141. if (!empty($filters['status'])) {
  142. $query->where('status', $filters['status']);
  143. }
  144. if (!empty($filters['type'])) {
  145. $query->where('type', $filters['type']);
  146. }
  147. if (!empty($filters['search'])) {
  148. $term = '%' . mb_strtolower($filters['search']) . '%';
  149. $query->where(function ($q) use ($term) {
  150. $q->whereHas('user', function ($uq) use ($term) {
  151. $uq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]);
  152. })->orWhereHas('userDependent', function ($dq) use ($term) {
  153. $dq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]);
  154. })->orWhereHas('partnerAgreement', function ($pq) use ($term) {
  155. $pq->whereRaw('UNACCENT(LOWER(company_name)) LIKE UNACCENT(?)', [$term]);
  156. })->orWhereHas('partnerAgreementService', function ($sq) use ($term) {
  157. $sq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]);
  158. })->orWhereHas('exams.partnerAgreementService', function ($eq) use ($term) {
  159. $eq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]);
  160. })->orWhereRaw('UNACCENT(LOWER(order_number)) LIKE UNACCENT(?)', [$term]);
  161. });
  162. }
  163. return $query->paginate($perPage);
  164. }
  165. public function approve(int $id, string $date, string $time, ?int $approvedByUserId = null): ?Appointment
  166. {
  167. $model = Appointment::find($id);
  168. if (!$model) return null;
  169. $model->update([
  170. 'status' => AppointmentStatusEnum::CONFIRMADO,
  171. 'date' => $date,
  172. 'time' => $time,
  173. 'auto_approved' => false,
  174. 'approved_by_user_id' => $approvedByUserId,
  175. ]);
  176. $this->notificationService->createAutoForUser([
  177. 'title' => 'Agendamento confirmado',
  178. 'message' => "Seu agendamento #{$model->order_number} foi confirmado para " . Carbon::parse($date)->format('d/m/Y') . " às {$time}.",
  179. 'recipient' => NotificationRecipientEnum::ASSOCIADO,
  180. 'source' => 'appointment',
  181. 'source_id' => $model->id,
  182. ], $model->user_id);
  183. return $model->fresh(self::RELATIONS);
  184. }
  185. public function reject(int $id): ?Appointment
  186. {
  187. $model = Appointment::find($id);
  188. if (!$model) return null;
  189. $model->update(['status' => AppointmentStatusEnum::RECUSADO]);
  190. $this->notificationService->createAutoForUser([
  191. 'title' => 'Agendamento recusado',
  192. 'message' => "Seu agendamento #{$model->order_number} foi recusado.",
  193. 'recipient' => NotificationRecipientEnum::ASSOCIADO,
  194. 'source' => 'appointment',
  195. 'source_id' => $model->id,
  196. ], $model->user_id);
  197. return $model->fresh(self::RELATIONS);
  198. }
  199. public function approveByPartner(int $id, int $userId, string $date, string $time): ?Appointment
  200. {
  201. $model = Appointment::whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))->find($id);
  202. if (!$model) return null;
  203. $model->update([
  204. 'status' => AppointmentStatusEnum::CONFIRMADO,
  205. 'date' => $date,
  206. 'time' => $time,
  207. 'auto_approved' => false,
  208. 'approved_by_user_id' => $userId,
  209. ]);
  210. $this->notificationService->createAutoForUser([
  211. 'title' => 'Agendamento confirmado',
  212. 'message' => "Seu agendamento #{$model->order_number} foi confirmado para " . Carbon::parse($date)->format('d/m/Y') . " às {$time}.",
  213. 'recipient' => NotificationRecipientEnum::ASSOCIADO,
  214. 'source' => 'appointment',
  215. 'source_id' => $model->id,
  216. ], $model->user_id);
  217. return $model->fresh(self::RELATIONS);
  218. }
  219. public function rejectByPartner(int $id, int $userId): ?Appointment
  220. {
  221. $model = Appointment::whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))->find($id);
  222. if (!$model) return null;
  223. $model->update(['status' => AppointmentStatusEnum::RECUSADO]);
  224. $this->notificationService->createAutoForUser([
  225. 'title' => 'Agendamento recusado',
  226. 'message' => "Seu agendamento #{$model->order_number} foi recusado.",
  227. 'recipient' => NotificationRecipientEnum::ASSOCIADO,
  228. 'source' => 'appointment',
  229. 'source_id' => $model->id,
  230. ], $model->user_id);
  231. return $model->fresh(self::RELATIONS);
  232. }
  233. // ------------------------------------------------------------------
  234. // Exames
  235. // ------------------------------------------------------------------
  236. public function getExamsByPartnerUser(int $userId): Collection
  237. {
  238. return $this->examsForPartnerQuery($userId)
  239. ->orderBy('requested_at', 'desc')
  240. ->get();
  241. }
  242. public function findExamForPartnerUser(int $id, int $userId): ?Appointment
  243. {
  244. return $this->examsForPartnerQuery($userId)->find($id);
  245. }
  246. private function examsForPartnerQuery(int $userId)
  247. {
  248. return Appointment::with(self::RELATIONS)
  249. ->where('type', AppointmentTypeEnum::EXAME)
  250. ->whereHas('partnerAgreement', fn($q) => $q
  251. ->where('user_id', $userId)
  252. ->where('type', PartnerAgreementTypeEnum::AGREEMENT));
  253. }
  254. public function createExam(array $data): Appointment
  255. {
  256. $serviceIds = $data['service_ids'];
  257. unset($data['service_ids']);
  258. return DB::transaction(function () use ($data, $serviceIds) {
  259. $services = PartnerAgreementService::whereIn('id', $serviceIds)->get();
  260. $data['type'] = AppointmentTypeEnum::EXAME;
  261. $data['status'] = AppointmentStatusEnum::AGUARDANDO_ACEITE;
  262. $data['auto_approved'] = false;
  263. $data['partner_agreement_service_id'] = null;
  264. $data['service_price'] = $services->sum(fn($s) => (float) $this->examPrice($s));
  265. $appointment = $this->create($data);
  266. foreach ($services as $service) {
  267. $appointment->exams()->create([
  268. 'partner_agreement_service_id' => $service->id,
  269. 'service_price' => $this->examPrice($service),
  270. ]);
  271. }
  272. $this->notifyExamIssued($appointment, $services->count());
  273. return $appointment->fresh(self::RELATIONS);
  274. });
  275. }
  276. public function acceptExam(int $id, int $userId): ?Appointment
  277. {
  278. $model = $this->pendingExamForUser($id, $userId);
  279. if (!$model) {
  280. return null;
  281. }
  282. $model->update([
  283. 'status' => AppointmentStatusEnum::CONFIRMADO,
  284. 'accepted_at' => now(),
  285. ]);
  286. $this->notifyPartnerExamDecision($model, accepted: true);
  287. return $model->fresh(self::RELATIONS);
  288. }
  289. public function refuseExam(int $id, int $userId, ?string $reason = null): ?Appointment
  290. {
  291. $model = $this->pendingExamForUser($id, $userId);
  292. if (!$model) {
  293. return null;
  294. }
  295. $model->update([
  296. 'status' => AppointmentStatusEnum::RECUSADO,
  297. 'refused_at' => now(),
  298. 'refused_by_user_id' => $userId,
  299. 'refusal_reason' => $reason,
  300. ]);
  301. $this->notifyPartnerExamDecision($model, accepted: false);
  302. return $model->fresh(self::RELATIONS);
  303. }
  304. private function pendingExamForUser(int $id, int $userId): ?Appointment
  305. {
  306. return Appointment::with(self::RELATIONS)
  307. ->where('user_id', $userId)
  308. ->where('type', AppointmentTypeEnum::EXAME)
  309. ->where('status', AppointmentStatusEnum::AGUARDANDO_ACEITE)
  310. ->find($id);
  311. }
  312. private function examPrice(PartnerAgreementService $service): ?string
  313. {
  314. return $service->associate_price ?? $service->price;
  315. }
  316. private function notifyExamIssued(Appointment $model, int $examCount): void
  317. {
  318. $clinic = $model->partnerAgreement?->company_name;
  319. $dependent = $model->userDependent?->name;
  320. $target = $dependent ? "para o dependente {$dependent}" : 'para você';
  321. $plural = $examCount === 1 ? 'exame' : 'exames';
  322. $this->notificationService->createAutoForUser([
  323. 'title' => 'Guia de exames para aprovação',
  324. 'message' => "{$clinic} gerou uma guia com {$examCount} {$plural} {$target}. Acesse seus agendamentos para aceitar ou recusar a guia #{$model->order_number}.",
  325. 'recipient' => NotificationRecipientEnum::ASSOCIADO,
  326. 'source' => 'appointment',
  327. 'source_id' => $model->id,
  328. ], $model->user_id);
  329. }
  330. private function notifyPartnerExamDecision(Appointment $model, bool $accepted): void
  331. {
  332. $partnerUserId = $model->partnerAgreement?->user_id;
  333. if (!$partnerUserId) {
  334. return;
  335. }
  336. $associate = $model->user?->name;
  337. $message = $accepted
  338. ? "{$associate} aceitou a guia de exames #{$model->order_number}."
  339. : "{$associate} recusou a guia de exames #{$model->order_number}. Gere uma nova guia com os ajustes necessários."
  340. . ($model->refusal_reason ? " Motivo: {$model->refusal_reason}" : '');
  341. $this->notificationService->createAutoForUser([
  342. 'title' => $accepted ? 'Guia de exames aceita' : 'Guia de exames recusada',
  343. 'message' => $message,
  344. 'recipient' => NotificationRecipientEnum::PARCEIRO,
  345. 'source' => 'appointment',
  346. 'source_id' => $model->id,
  347. ], $partnerUserId);
  348. }
  349. private function generateOrderNumber(): string
  350. {
  351. do {
  352. $number = 'AGD-' . strtoupper(Str::random(8));
  353. } while (Appointment::where('order_number', $number)->exists());
  354. return $number;
  355. }
  356. }