ScheduleBusinessRules.php 13 KB

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