ScheduleBusinessRules.php 14 KB

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