SearchService.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\GenderEnum;
  4. use App\Models\Address;
  5. use App\Models\Client;
  6. use App\Models\Provider;
  7. use App\Models\ProviderServicesType;
  8. use App\Rules\ScheduleBusinessRules;
  9. use Illuminate\Support\Collection;
  10. use Illuminate\Support\Facades\Auth;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Storage;
  13. class SearchService
  14. {
  15. private const NEARBY_RADIUS_KM = 20.0;
  16. public function __construct(
  17. private readonly ZipCodeCoordinatesService $zipCodeCoordinatesService,
  18. ) {}
  19. /**
  20. * @return array{providers: array, has_location: bool}
  21. */
  22. public function buscaPrestadores(?string $name = null, ?string $date = null): array
  23. {
  24. $user = Auth::user();
  25. $cliente = Client::where('user_id', $user->id)->first();
  26. $blockedProviderIds = ScheduleBusinessRules::getBlockedProviderIdsForClient($cliente->id);
  27. $providersWithWorkingDays = ScheduleBusinessRules::getProviderIdsWithWorkingDays();
  28. $clientPrimaryAddress = Address::where('source', 'client')
  29. ->where('source_id', $cliente->id)
  30. ->orderByDesc('is_primary')
  31. ->orderByDesc('id')
  32. ->first();
  33. $cityId = $clientPrimaryAddress?->city_id;
  34. $clientLat = $clientPrimaryAddress?->latitude !== null ? (float) $clientPrimaryAddress->latitude : null;
  35. $clientLng = $clientPrimaryAddress?->longitude !== null ? (float) $clientPrimaryAddress->longitude : null;
  36. $hasLocation = $clientLat !== null && $clientLng !== null;
  37. if (! $hasLocation) {
  38. return [
  39. 'providers' => [],
  40. 'has_location' => false,
  41. ];
  42. }
  43. $distanceSelect = $this->distanceSelect($clientLat, $clientLng);
  44. $baseQuery = Provider::leftJoin(
  45. 'users as provider_user',
  46. 'provider_user.id',
  47. '=',
  48. 'providers.user_id'
  49. )
  50. ->visibleToCustomers()
  51. ->leftJoin(
  52. DB::raw("
  53. (
  54. SELECT DISTINCT ON (source_id)
  55. *
  56. FROM addresses
  57. WHERE source = 'provider'
  58. AND deleted_at IS NULL
  59. ORDER BY
  60. source_id,
  61. (latitude IS NOT NULL AND longitude IS NOT NULL) DESC,
  62. is_primary DESC,
  63. id DESC
  64. ) AS provider_address
  65. "),
  66. 'provider_address.source_id',
  67. '=',
  68. 'providers.id'
  69. )
  70. ->whereNotNull('provider_address.id')
  71. ->whereNotIn('providers.id', $blockedProviderIds)
  72. ->whereIn('providers.id', $providersWithWorkingDays)
  73. ->whereNull('providers.deleted_at')
  74. ->whereNotNull('providers.daily_price_8h')
  75. ->whereNotNull('providers.daily_price_6h')
  76. ->whereNotNull('providers.daily_price_4h')
  77. ->whereNotNull('providers.daily_price_2h')
  78. ->when($name, fn ($q) => $q->where('provider_user.name', 'ILIKE', "%{$name}%"))
  79. ->select(
  80. 'providers.id',
  81. 'providers.id as provider_id',
  82. 'providers.profile_media_id',
  83. 'provider_user.name as provider_name',
  84. 'providers.gender',
  85. 'provider_address.zip_code as provider_zip_code',
  86. 'provider_address.district',
  87. 'provider_address.latitude as provider_latitude',
  88. 'provider_address.longitude as provider_longitude',
  89. 'providers.average_rating',
  90. 'providers.total_services',
  91. 'providers.daily_price_8h',
  92. 'providers.daily_price_6h',
  93. 'providers.daily_price_4h',
  94. 'providers.daily_price_2h',
  95. 'providers.created_at',
  96. DB::raw("
  97. (
  98. SELECT COUNT(*)
  99. FROM reviews
  100. LEFT JOIN schedules
  101. ON schedules.id = reviews.schedule_id
  102. WHERE reviews.origin = 'provider'
  103. AND schedules.provider_id = providers.id
  104. ) AS total_reviews
  105. "),
  106. $distanceSelect,
  107. )
  108. ->where(function ($query) use ($cityId, $clientLat, $clientLng) {
  109. if ($cityId !== null) {
  110. $query->orWhere('provider_address.city_id', $cityId);
  111. }
  112. if ($clientLat !== null && $clientLng !== null) {
  113. $query->orWhereRaw(
  114. DistanceService::withinRadiusSqlCondition(
  115. (float) $clientLat,
  116. (float) $clientLng,
  117. self::NEARBY_RADIUS_KM,
  118. )
  119. );
  120. }
  121. })
  122. ->orderByRaw('distance_km ASC NULLS LAST');
  123. $providers = $baseQuery->get();
  124. $this->zipCodeCoordinatesService->preload(
  125. $providers->whereNull('distance_km')->pluck('provider_zip_code')
  126. );
  127. $providers->each(function ($provider) use ($clientPrimaryAddress) {
  128. if ($provider->distance_km !== null) {
  129. return;
  130. }
  131. $provider->distance_km = $this->zipCodeCoordinatesService->calculateDistance(
  132. $clientPrimaryAddress?->latitude !== null ? (float) $clientPrimaryAddress->latitude : null,
  133. $clientPrimaryAddress?->longitude !== null ? (float) $clientPrimaryAddress->longitude : null,
  134. $clientPrimaryAddress?->zip_code,
  135. $provider->provider_latitude !== null ? (float) $provider->provider_latitude : null,
  136. $provider->provider_longitude !== null ? (float) $provider->provider_longitude : null,
  137. $provider->provider_zip_code,
  138. );
  139. });
  140. $providers = $providers
  141. ->sortBy(fn ($provider) => $provider->distance_km ?? PHP_FLOAT_MAX)
  142. ->values();
  143. $filtered = $providers->when(
  144. $date,
  145. fn ($collection) => $collection->whereIn(
  146. 'provider_id',
  147. ScheduleBusinessRules::getAvailableProviderIdsForDate(
  148. $date,
  149. $collection->pluck('provider_id')
  150. )->toArray()
  151. )->values()
  152. );
  153. $filtered->load('profileMedia');
  154. return [
  155. 'providers' => $this->mapProviders($filtered),
  156. 'has_location' => true,
  157. ];
  158. }
  159. private function mapProviders(Collection $filtered): array
  160. {
  161. return $filtered->map(function ($item) {
  162. $arr = is_array($item) ? $item : $item->toArray();
  163. $arr['profile_media_url'] = $item->profileMedia?->path
  164. ? Storage::temporaryUrl($item->profileMedia->path, now()->addMinutes(60))
  165. : null;
  166. $arr['gender_label'] = GenderEnum::labelFor(data_get($arr, 'gender'));
  167. $arr['daily_price_8h_base'] = data_get($arr, 'daily_price_8h');
  168. $arr['daily_price_6h_base'] = data_get($arr, 'daily_price_6h');
  169. $arr['daily_price_4h_base'] = data_get($arr, 'daily_price_4h');
  170. $arr['daily_price_2h_base'] = data_get($arr, 'daily_price_2h');
  171. $arr['daily_price_8h'] = $this->applyCreditCardFee(data_get($arr, 'daily_price_8h'));
  172. $arr['daily_price_6h'] = $this->applyCreditCardFee(data_get($arr, 'daily_price_6h'));
  173. $arr['daily_price_4h'] = $this->applyCreditCardFee(data_get($arr, 'daily_price_4h'));
  174. $arr['daily_price_2h'] = $this->applyCreditCardFee(data_get($arr, 'daily_price_2h'));
  175. unset(
  176. $arr['id'],
  177. $arr['profile_media_id'],
  178. $arr['profile_media'],
  179. $arr['provider_zip_code'],
  180. );
  181. return $arr;
  182. })->values()->toArray();
  183. }
  184. public function perfilPrestador(int $providerId): array
  185. {
  186. $provider = Provider::query()
  187. ->visibleToCustomers()
  188. ->whereNull('providers.deleted_at')
  189. ->with(['user:id,name', 'profileMedia'])
  190. ->findOrFail($providerId);
  191. $address = Address::where('source', 'provider')
  192. ->where('source_id', $provider->id)
  193. ->orderByDesc('is_primary')
  194. ->orderByDesc('id')
  195. ->first();
  196. $serviceTypes = ProviderServicesType::query()
  197. ->join('service_types', 'service_types.id', '=', 'provider_services_types.service_type_id')
  198. ->where('provider_services_types.provider_id', $provider->id)
  199. ->whereNull('provider_services_types.deleted_at')
  200. ->whereNull('service_types.deleted_at')
  201. ->where('service_types.is_active', true)
  202. ->orderBy('service_types.description')
  203. ->get([
  204. 'service_types.id',
  205. 'service_types.description',
  206. ])
  207. ->toArray();
  208. $totalReviews = DB::table('reviews')
  209. ->leftJoin('schedules', 'schedules.id', '=', 'reviews.schedule_id')
  210. ->where('reviews.origin', 'provider')
  211. ->where('schedules.provider_id', $provider->id)
  212. ->count();
  213. return [
  214. 'provider_id' => $provider->id,
  215. 'provider_name' => $provider->user?->name,
  216. 'district' => $address?->district,
  217. 'age' => $provider->birth_date?->age,
  218. 'gender' => $provider->gender,
  219. 'gender_label' => GenderEnum::labelFor($provider->gender),
  220. 'average_rating' => $provider->average_rating,
  221. 'total_services' => $provider->total_services,
  222. 'total_reviews' => $totalReviews,
  223. 'profile_media_url' => $provider->profileMedia?->path
  224. ? Storage::temporaryUrl($provider->profileMedia->path, now()->addMinutes(60))
  225. : null,
  226. 'service_types' => $serviceTypes,
  227. 'daily_price_8h_base' => $provider->daily_price_8h,
  228. 'daily_price_6h_base' => $provider->daily_price_6h,
  229. 'daily_price_4h_base' => $provider->daily_price_4h,
  230. 'daily_price_2h_base' => $provider->daily_price_2h,
  231. 'daily_price_8h' => $this->applyCreditCardFee($provider->daily_price_8h),
  232. 'daily_price_6h' => $this->applyCreditCardFee($provider->daily_price_6h),
  233. 'daily_price_4h' => $this->applyCreditCardFee($provider->daily_price_4h),
  234. 'daily_price_2h' => $this->applyCreditCardFee($provider->daily_price_2h),
  235. ];
  236. }
  237. //
  238. private function applyCreditCardFee(?float $price): ?float
  239. {
  240. if ($price === null) {
  241. return null;
  242. }
  243. $rate = $this->platformCreditCardFeeRate();
  244. return round($price * (1 + $rate), 2);
  245. }
  246. private function platformCreditCardFeeRate(): float
  247. {
  248. $rate = config('services.pagarme.platform_credit_card_fee_rate', 0.16);
  249. if (is_string($rate)) {
  250. $rate = str_replace(',', '.', trim($rate));
  251. }
  252. $rate = (float) $rate;
  253. return $rate > 1 ? $rate / 100 : $rate;
  254. }
  255. private function distanceSelect(?float $clientLatitude, ?float $clientLongitude): \Illuminate\Contracts\Database\Query\Expression
  256. {
  257. return DistanceService::sqlExpression($clientLatitude, $clientLongitude);
  258. }
  259. }