|
@@ -5,7 +5,9 @@ namespace App\Services;
|
|
|
use App\Broadcasting\RealtimeEvent;
|
|
use App\Broadcasting\RealtimeEvent;
|
|
|
use App\Broadcasting\RealtimeRoom;
|
|
use App\Broadcasting\RealtimeRoom;
|
|
|
use App\Broadcasting\RealtimeService;
|
|
use App\Broadcasting\RealtimeService;
|
|
|
|
|
+use App\Enums\ApprovalStatusEnum;
|
|
|
use App\Enums\NotificationTypeEnum;
|
|
use App\Enums\NotificationTypeEnum;
|
|
|
|
|
+use App\Jobs\NotifyProvidersOfNewOpportunityJob;
|
|
|
use App\Models\Address;
|
|
use App\Models\Address;
|
|
|
use App\Models\CustomSchedule;
|
|
use App\Models\CustomSchedule;
|
|
|
use App\Models\CustomScheduleSpeciality;
|
|
use App\Models\CustomScheduleSpeciality;
|
|
@@ -23,6 +25,7 @@ use App\Rules\ScheduleBusinessRules;
|
|
|
use App\Services\NotificationService;
|
|
use App\Services\NotificationService;
|
|
|
use App\Services\DistanceService;
|
|
use App\Services\DistanceService;
|
|
|
use Carbon\Carbon;
|
|
use Carbon\Carbon;
|
|
|
|
|
+use Illuminate\Support\Collection;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\Storage;
|
|
@@ -123,6 +126,8 @@ class CustomScheduleService
|
|
|
|
|
|
|
|
DB::commit();
|
|
DB::commit();
|
|
|
|
|
|
|
|
|
|
+ $this->dispatchOpportunityNotification($createdCustomSchedules);
|
|
|
|
|
+
|
|
|
return $createdCustomSchedules;
|
|
return $createdCustomSchedules;
|
|
|
} catch (\Exception $e) {
|
|
} catch (\Exception $e) {
|
|
|
DB::rollBack();
|
|
DB::rollBack();
|
|
@@ -371,6 +376,28 @@ class CustomScheduleService
|
|
|
return $availableOpportunities->values();
|
|
return $availableOpportunities->values();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @return Collection<int, int>
|
|
|
|
|
+ */
|
|
|
|
|
+ public function getAvailableProvidersForOpportunity(Schedule $schedule): Collection
|
|
|
|
|
+ {
|
|
|
|
|
+ $schedule->loadMissing(['customSchedule', 'address']);
|
|
|
|
|
+
|
|
|
|
|
+ $candidateIds = $this->getCandidateProviderIdsForOpportunity($schedule);
|
|
|
|
|
+
|
|
|
|
|
+ if ($candidateIds->isEmpty()) {
|
|
|
|
|
+ return $candidateIds;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $candidateIds->filter(function ($providerId) use ($schedule) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return $this->checkProviderAvailability($providerId, $schedule);
|
|
|
|
|
+ } catch (\Exception $e) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ })->values();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public function getOpportunityProposals($scheduleId)
|
|
public function getOpportunityProposals($scheduleId)
|
|
|
{
|
|
{
|
|
|
return ScheduleProposal::with(['provider.user'])
|
|
return ScheduleProposal::with(['provider.user'])
|
|
@@ -875,6 +902,132 @@ class CustomScheduleService
|
|
|
|
|
|
|
|
//
|
|
//
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @return Collection<int, int>
|
|
|
|
|
+ */
|
|
|
|
|
+ private function getCandidateProviderIdsForOpportunity(Schedule $schedule): Collection
|
|
|
|
|
+ {
|
|
|
|
|
+ $address = Address::find($schedule->address_id);
|
|
|
|
|
+
|
|
|
|
|
+ $cityId = $address?->city_id;
|
|
|
|
|
+ $lat = $address?->latitude !== null ? (float) $address->latitude : null;
|
|
|
|
|
+ $lng = $address?->longitude !== null ? (float) $address->longitude : null;
|
|
|
|
|
+
|
|
|
|
|
+ if ($cityId === null && ($lat === null || $lng === null)) {
|
|
|
|
|
+ Log::warning('Oportunidade sem geolocalizacao; nenhum prestador elegivel', [
|
|
|
|
|
+ 'schedule_id' => $schedule->id,
|
|
|
|
|
+ 'address_id' => $schedule->address_id,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return collect();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $periodType = (string) $schedule->period_type;
|
|
|
|
|
+
|
|
|
|
|
+ $factor = match ($periodType) {
|
|
|
|
|
+ '2' => 0.30,
|
|
|
|
|
+ '4' => 0.55,
|
|
|
|
|
+ '6' => 0.85,
|
|
|
|
|
+ '8' => 1.00,
|
|
|
|
|
+ default => null,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if ($factor === null) {
|
|
|
|
|
+ return collect();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $priceColumn = "providers.daily_price_{$periodType}h";
|
|
|
|
|
+
|
|
|
|
|
+ $date = Carbon::parse($schedule->date);
|
|
|
|
|
+ $dayOfWeek = $date->dayOfWeek;
|
|
|
|
|
+
|
|
|
|
|
+ $period = $schedule->start_time < '13:00:00' ? 'morning' : 'afternoon';
|
|
|
|
|
+
|
|
|
|
|
+ $minProportional = (float) $schedule->customSchedule->min_price * $factor;
|
|
|
|
|
+ $maxProportional = (float) $schedule->customSchedule->max_price * $factor;
|
|
|
|
|
+
|
|
|
|
|
+ $providerAddressSubquery = DB::raw("
|
|
|
|
|
+ (
|
|
|
|
|
+ SELECT DISTINCT ON (source_id)
|
|
|
|
|
+ *
|
|
|
|
|
+ FROM addresses
|
|
|
|
|
+ WHERE
|
|
|
|
|
+ source = 'provider'
|
|
|
|
|
+ AND deleted_at IS NULL
|
|
|
|
|
+ ORDER BY
|
|
|
|
|
+ source_id,
|
|
|
|
|
+ (latitude IS NOT NULL AND longitude IS NOT NULL) DESC,
|
|
|
|
|
+ is_primary DESC,
|
|
|
|
|
+ id DESC
|
|
|
|
|
+ ) AS provider_address
|
|
|
|
|
+ ");
|
|
|
|
|
+
|
|
|
|
|
+ return Provider::query()
|
|
|
|
|
+ ->join($providerAddressSubquery, 'provider_address.source_id', '=', 'providers.id')
|
|
|
|
|
+ ->where('providers.approval_status', ApprovalStatusEnum::ACCEPTED->value)
|
|
|
|
|
+
|
|
|
|
|
+ ->where(function ($query) use ($cityId, $lat, $lng) {
|
|
|
|
|
+ if ($cityId !== null) {
|
|
|
|
|
+ $query->orWhere('provider_address.city_id', $cityId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($lat !== null && $lng !== null) {
|
|
|
|
|
+ $query->orWhereRaw(
|
|
|
|
|
+ DistanceService::withinRadiusSqlCondition(
|
|
|
|
|
+ $lat,
|
|
|
|
|
+ $lng,
|
|
|
|
|
+ self::NEARBY_RADIUS_KM,
|
|
|
|
|
+ 'provider_address.latitude',
|
|
|
|
|
+ 'provider_address.longitude',
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ ->whereExists(function ($query) use ($dayOfWeek, $period) {
|
|
|
|
|
+ $query->select(DB::raw(1))
|
|
|
|
|
+ ->from('provider_working_days')
|
|
|
|
|
+ ->whereColumn('provider_working_days.provider_id', 'providers.id')
|
|
|
|
|
+ ->where('provider_working_days.day', $dayOfWeek)
|
|
|
|
|
+ ->where('provider_working_days.period', $period)
|
|
|
|
|
+ ->whereNull('provider_working_days.deleted_at');
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ ->whereNotNull($priceColumn)
|
|
|
|
|
+ ->whereBetween($priceColumn, [$minProportional, $maxProportional])
|
|
|
|
|
+
|
|
|
|
|
+ ->whereNotIn(
|
|
|
|
|
+ 'providers.id',
|
|
|
|
|
+ ScheduleBusinessRules::getBlockedProviderIdsForClient($schedule->client_id)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ ->pluck('providers.id');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function dispatchOpportunityNotification(array $customSchedules): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $scheduleIds = [];
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ $scheduleIds = collect($customSchedules)
|
|
|
|
|
+ ->pluck('schedule_id')
|
|
|
|
|
+ ->filter()
|
|
|
|
|
+ ->values()
|
|
|
|
|
+ ->all();
|
|
|
|
|
+
|
|
|
|
|
+ if (empty($scheduleIds)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ NotifyProvidersOfNewOpportunityJob::dispatch($scheduleIds);
|
|
|
|
|
+ } catch (\Throwable $exception) {
|
|
|
|
|
+ Log::error('Falha ao enfileirar notificacao de nova oportunidade', [
|
|
|
|
|
+ 'schedule_ids' => $scheduleIds,
|
|
|
|
|
+ 'error' => $exception->getMessage(),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private function checkProviderAvailability($providerId, $schedule)
|
|
private function checkProviderAvailability($providerId, $schedule)
|
|
|
{
|
|
{
|
|
|
$client_id = $schedule->client_id;
|
|
$client_id = $schedule->client_id;
|