ScheduleBusinessRules.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. <?php
  2. namespace App\Rules;
  3. use App\Models\ClientProviderBlock;
  4. use App\Models\Provider;
  5. use App\Models\ProviderBlockedDay;
  6. use App\Models\ProviderClientBlock;
  7. use App\Models\ProviderWorkingDay;
  8. use App\Models\Schedule;
  9. use App\Models\ScheduleProposal;
  10. use Carbon\Carbon;
  11. use Illuminate\Support\Collection;
  12. class ScheduleBusinessRules
  13. {
  14. // Status que devem ser ignorados na validação de limite por semana
  15. private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];
  16. public static function validateProviderVisibleToCustomers($providerId): bool
  17. {
  18. $provider = Provider::query()
  19. ->visibleToCustomers()
  20. ->where('providers.id', $providerId)
  21. ->exists();
  22. if (! $provider) {
  23. throw new \Exception(__('messages.provider_unavailable_for_schedule'));
  24. }
  25. return true;
  26. }
  27. /**
  28. * Valida se o prestador pode ter mais um agendamento com o cliente na semana
  29. * Limite: 2 agendamentos por semana (domingo a sábado)
  30. *
  31. * @param int $clientId
  32. * @param int $providerId
  33. * @param string $date (Y-m-d)
  34. * @param int|null $excludeScheduleId
  35. * @return bool
  36. *
  37. * @throws \Exception
  38. */
  39. public static function validateWeeklyScheduleLimit($clientId, $providerId, $date, $excludeScheduleId = null)
  40. {
  41. $date = Carbon::parse($date);
  42. $weekStart = $date->copy()->startOfWeek(Carbon::SUNDAY);
  43. $weekEnd = $date->copy()->endOfWeek(Carbon::SATURDAY);
  44. $weeklySchedulesCount = Schedule::where('client_id', $clientId)
  45. ->where('provider_id', $providerId)
  46. ->whereNotIn('status', self::EXCLUDED_STATUSES)
  47. ->whereBetween('date', [$weekStart->format('Y-m-d'), $weekEnd->format('Y-m-d')])
  48. ->when($excludeScheduleId, function ($query) use ($excludeScheduleId) {
  49. $query->where('id', '!=', $excludeScheduleId);
  50. })
  51. ->count();
  52. if ($weeklySchedulesCount >= 2) {
  53. throw new \Exception(__('validation.custom.schedule.weekly_limit_exceeded'));
  54. }
  55. return true;
  56. }
  57. /**
  58. * Valida se o prestador tem horário de trabalho cadastrado para o dia da semana e período
  59. *
  60. * @param int $provider_id
  61. * @param int $day_of_week (0 - domingo, 6 - sábado)
  62. * @param string $period ('morning' ou 'afternoon')
  63. * @return bool
  64. *
  65. * @throws \Exception
  66. */
  67. public static function validateWorkingDay($provider_id, $day_of_week, $period)
  68. {
  69. $workingDay = ProviderWorkingDay::where('provider_id', $provider_id)
  70. ->where('day', $day_of_week)
  71. ->where('period', $period)
  72. ->first();
  73. if (! $workingDay) {
  74. throw new \Exception(__('validation.custom.schedule.provider_not_working'));
  75. }
  76. return true;
  77. }
  78. /**
  79. * Valida se o prestador tem bloqueio cadastrado para o dia e horário
  80. *
  81. * @param int $provider_id
  82. * @param string $date_ymd (Y-m-d)
  83. * @param string $start_time (H:i:s)
  84. * @param string $end_time (H:i:s)
  85. * @return bool
  86. *
  87. * @throws \Exception
  88. */
  89. public static function validateBlockedDay($provider_id, $date_ymd, $start_time, $end_time)
  90. {
  91. $blockedDay = ProviderBlockedDay::where('provider_id', $provider_id)
  92. ->where('date', $date_ymd)
  93. ->where(function ($query) use ($start_time, $end_time) {
  94. $query->where('period', 'full')
  95. ->orWhere(function ($q) use ($start_time, $end_time) {
  96. $q->where('period', 'partial')
  97. ->where(function ($q2) use ($start_time, $end_time) {
  98. $q2->whereBetween('init_hour', [$start_time, $end_time])
  99. ->orWhereBetween('end_hour', [$start_time, $end_time])
  100. ->orWhere(function ($q3) use ($start_time, $end_time) {
  101. $q3->where('init_hour', '<=', $start_time)
  102. ->where('end_hour', '>=', $end_time);
  103. });
  104. });
  105. });
  106. })
  107. ->first();
  108. if ($blockedDay) {
  109. throw new \Exception(__('validation.custom.schedule.provider_blocked'));
  110. }
  111. return true;
  112. }
  113. // apenas para custom_schedules
  114. public static function validatePricePeriod($provider_id, $min_price, $max_price, $period_type)
  115. {
  116. if ($min_price < 0 || $max_price < 0) {
  117. throw new \Exception(__('validation.custom.schedule.invalid_price'));
  118. }
  119. if ($min_price > $max_price) {
  120. throw new \Exception(__('validation.custom.schedule.invalid_price_range'));
  121. }
  122. $provider = Provider::find($provider_id);
  123. $min_price_proportional = 0;
  124. $max_price_proportional = 0;
  125. $provider_price_period = 0;
  126. switch ($period_type) {
  127. case '2': // 2 horas
  128. $provider_price_period = $provider->daily_price_2h;
  129. $min_price_proportional = $min_price * 0.30;
  130. $max_price_proportional = $max_price * 0.30;
  131. break;
  132. case '4': // 4 horas
  133. $provider_price_period = $provider->daily_price_4h;
  134. $min_price_proportional = $min_price * 0.55;
  135. $max_price_proportional = $max_price * 0.55;
  136. break;
  137. case '6': // 6 horas
  138. $provider_price_period = $provider->daily_price_6h;
  139. $min_price_proportional = $min_price * 0.85;
  140. $max_price_proportional = $max_price * 0.85;
  141. break;
  142. case '8': // 8 horas
  143. $provider_price_period = $provider->daily_price_8h;
  144. $min_price_proportional = $min_price;
  145. $max_price_proportional = $max_price;
  146. break;
  147. default:
  148. throw new \Exception(__('validation.custom.schedule.invalid_period_type'));
  149. }
  150. if ($provider_price_period < $min_price_proportional || $provider_price_period > $max_price_proportional) {
  151. throw new \Exception(__('validation.custom.schedule.price_not_in_range'));
  152. }
  153. return true;
  154. }
  155. /**
  156. * Valida se o prestador tem outro agendamento no mesmo dia e horário
  157. *
  158. * @param int $provider_id
  159. * @param string $date_ymd (Y-m-d)
  160. * @param string $start_time (H:i:s)
  161. * @param string $end_time (H:i:s)
  162. * @param int|null $exclude_schedule_id (id do agendamento a ser excluído da validação, usado para edição de agendamento)
  163. * @return bool
  164. *
  165. * @throws \Exception
  166. */
  167. public static function validateConflictingSchedule($provider_id, $date_ymd, $start_time, $end_time, $exclude_schedule_id = null)
  168. {
  169. $conflictingSchedule = Schedule::where('provider_id', $provider_id)
  170. ->where('date', $date_ymd)
  171. ->whereIn('status', ['pending', 'accepted', 'paid', 'started'])
  172. ->where(function ($query) use ($start_time, $end_time) {
  173. $query->whereBetween('start_time', [$start_time, $end_time])
  174. ->orWhereBetween('end_time', [$start_time, $end_time])
  175. ->orWhere(function ($q) use ($start_time, $end_time) {
  176. $q->where('start_time', '<=', $start_time)
  177. ->where('end_time', '>=', $end_time);
  178. });
  179. })
  180. ->when($exclude_schedule_id, function ($query) use ($exclude_schedule_id) {
  181. $query->where('id', '!=', $exclude_schedule_id);
  182. })
  183. ->first();
  184. if ($conflictingSchedule) {
  185. throw new \Exception(__('validation.custom.schedule.provider_conflicting_schedule'));
  186. }
  187. return true;
  188. }
  189. /**
  190. * Valida se o prestador tem outro agendamento com o mesmo cliente no mesmo dia e horário
  191. *
  192. * @param int $provider_id
  193. * @param string $date_ymd (Y-m-d)
  194. * @param string $start_time (H:i:s)
  195. * @param string $end_time (H:i:s)
  196. * @param int|null $exclude_schedule_id (id do agendamento a ser excluído da validação, usado para edição de agendamento)
  197. * @return bool
  198. *
  199. * @throws \Exception
  200. */
  201. public static function validateConflictingSameProposal($provider_id, $schedule_id)
  202. {
  203. $conflictingSameProposal = ScheduleProposal::where('schedule_proposals.provider_id', $provider_id)
  204. ->where('schedule_proposals.schedule_id', $schedule_id)
  205. ->leftJoin('schedules', 'schedule_proposals.schedule_id', '=', 'schedules.id')
  206. ->whereNotIn('schedules.status', self::EXCLUDED_STATUSES)
  207. ->first();
  208. if ($conflictingSameProposal) {
  209. throw new \Exception(__('validation.custom.schedule.provider_conflicting_same_proposal'));
  210. }
  211. return true;
  212. }
  213. /**
  214. * Valida se o prestador tem outro agendamento com o mesmo cliente no mesmo dia e horário, ignorando o horário
  215. *
  216. * @param int $provider_id
  217. * @param string $date_ymd (Y-m-d)
  218. * @param string $start_time (H:i:s)
  219. * @param string $end_time (H:i:s)
  220. * @param int|null $exclude_schedule_id (id do agendamento a ser excluído da validação, usado para edição de agendamento)
  221. * @return bool
  222. *
  223. * @throws \Exception
  224. */
  225. public static function validateConflictingProposalSameDate($provider_id, $date_ymd, $start_time, $end_time, $exclude_schedule_id = null)
  226. {
  227. $conflictingProposalSameDate = ScheduleProposal::where('schedule_proposals.provider_id', $provider_id)
  228. ->leftJoin('schedules', 'schedule_proposals.schedule_id', '=', 'schedules.id')
  229. ->where('schedules.date', $date_ymd)
  230. ->where(function ($query) use ($start_time, $end_time) {
  231. $query->whereBetween('schedules.start_time', [$start_time, $end_time])
  232. ->orWhereBetween('schedules.end_time', [$start_time, $end_time])
  233. ->orWhere(function ($q) use ($start_time, $end_time) {
  234. $q->where('schedules.start_time', '<=', $start_time)
  235. ->where('schedules.end_time', '>=', $end_time);
  236. });
  237. })
  238. ->whereNotIn('status', self::EXCLUDED_STATUSES)
  239. ->when($exclude_schedule_id, function ($query) use ($exclude_schedule_id) {
  240. $query->where('schedules.id', '!=', $exclude_schedule_id);
  241. })
  242. ->first();
  243. if ($conflictingProposalSameDate) {
  244. throw new \Exception(__('validation.custom.schedule.provider_conflicting_proposal_same_date'));
  245. }
  246. return true;
  247. }
  248. /**
  249. * Valida se o cliente tem bloqueio cadastrado para o prestador
  250. *
  251. * @param int $client_id
  252. * @param int $provider_id
  253. * @return bool
  254. *
  255. * @throws \Exception
  256. */
  257. public static function validateClientNotBlockedByProvider($client_id, $provider_id)
  258. {
  259. $provider_client_block = ProviderClientBlock::where('provider_id', $provider_id)
  260. ->where('client_id', $client_id)
  261. ->first();
  262. if ($provider_client_block) {
  263. throw new \Exception(__('validation.custom.schedule.client_blocked_by_provider'));
  264. }
  265. return true;
  266. }
  267. /**
  268. * Valida se o prestador tem bloqueio cadastrado para o cliente
  269. *
  270. * @param int $client_id
  271. * @param int $provider_id
  272. * @return bool
  273. *
  274. * @throws \Exception
  275. */
  276. public static function validateProviderNotBlockedByClient($client_id, $provider_id)
  277. {
  278. $client_provider_block = ClientProviderBlock::where('provider_id', $provider_id)
  279. ->where('client_id', $client_id)
  280. ->first();
  281. if ($client_provider_block) {
  282. throw new \Exception(__('validation.custom.schedule.provider_blocked_by_client'));
  283. }
  284. return true;
  285. }
  286. // -------------------------------------------------------------------------
  287. // Métodos de consulta em batch — usados para filtragem em listagens.
  288. // Não lançam exceção: retornam coleções de IDs para uso em whereIn/whereNotIn.
  289. // -------------------------------------------------------------------------
  290. /**
  291. * Retorna os IDs de prestadores que bloquearam o cliente OU foram bloqueados por ele.
  292. * Centraliza ambas as direções de bloqueio em um único método para uso em listagens.
  293. */
  294. public static function getBlockedProviderIdsForClient(int $client_id): Collection
  295. {
  296. // Prestadores que bloquearam este cliente (ProviderClientBlock)
  297. $blockedByProvider = ProviderClientBlock::where('client_id', $client_id)
  298. ->pluck('provider_id');
  299. // Prestadores que este cliente bloqueou (ClientProviderBlock)
  300. $blockedByClient = ClientProviderBlock::where('client_id', $client_id)
  301. ->pluck('provider_id');
  302. return $blockedByProvider->merge($blockedByClient)->unique()->values();
  303. }
  304. /**
  305. * Retorna os IDs de prestadores que possuem pelo menos um dia de trabalho cadastrado.
  306. * Garante que apenas prestadores ativos na plataforma apareçam em listagens.
  307. */
  308. public static function getProviderIdsWithWorkingDays(): Collection
  309. {
  310. return ProviderWorkingDay::select('provider_id')
  311. ->distinct()
  312. ->pluck('provider_id');
  313. }
  314. /**
  315. * Retorna os IDs de prestadores disponíveis em uma data específica,
  316. * dentro de um conjunto pré-filtrado de IDs.
  317. *
  318. * Regras (em batch, sem iteração PHP):
  319. * 1. Prestador tem pelo menos um ProviderWorkingDay para o day_of_week da data.
  320. * 2. Prestador NÃO tem ProviderBlockedDay com period = 'all' nessa data.
  321. * (period = 'morning' ou 'afternoon' = bloqueio parcial → ainda disponível)
  322. *
  323. * @param string $date_ymd Y-m-d
  324. * @param Collection $providerIds conjunto de IDs a filtrar
  325. */
  326. public static function getAvailableProviderIdsForDate(string $date_ymd, Collection $providerIds): Collection
  327. {
  328. $dayOfWeek = Carbon::parse($date_ymd)->dayOfWeek;
  329. $withWorkingDay = ProviderWorkingDay::whereIn('provider_id', $providerIds)
  330. ->where('day', $dayOfWeek)
  331. ->pluck('provider_id')
  332. ->unique();
  333. $fullyBlockedIds = ProviderBlockedDay::whereIn('provider_id', $withWorkingDay)
  334. ->where('date', $date_ymd)
  335. ->where('period', 'all')
  336. ->pluck('provider_id')
  337. ->unique();
  338. return $withWorkingDay->diff($fullyBlockedIds)->values();
  339. }
  340. }