onQueue(config('restaurant-delivery.queue.queues.assignment', 'restaurant-delivery-assignment')); } /** * Execute the job. */ public function handle(FirebaseService $firebase): void { // Skip if already assigned if ($this->delivery->rider_id) { Log::info('Delivery already has a rider assigned', [ 'delivery_id' => $this->delivery->id, 'rider_id' => $this->delivery->rider_id, ]); return; } // Get assignment config $config = config('restaurant-delivery.assignment'); // Find nearby available riders $riders = $this->findAvailableRiders(); if ($riders->isEmpty()) { Log::warning('No available riders found for delivery', [ 'delivery_id' => $this->delivery->id, ]); // Retry after a delay if max attempts not reached if ($this->attempts() < $this->tries) { $this->release(60); // Release back to queue after 1 minute } return; } // Score and rank riders $scoredRiders = $this->scoreRiders($riders, $config['scoring']); // Use broadcast or direct assignment if ($config['broadcast']['enabled']) { $this->broadcastToRiders($scoredRiders->take($config['broadcast']['max_riders']), $firebase); } else { $this->directAssign($scoredRiders->first()); } } /** * Find available riders within the assignment radius. */ protected function findAvailableRiders() { $radius = config('restaurant-delivery.assignment.assignment_radius', 5); $maxConcurrent = config('restaurant-delivery.assignment.max_concurrent_orders', 3); $query = Rider::query() ->where('status', 'available') ->where('is_online', true) ->where('is_verified', true) ->whereNotNull('current_latitude') ->whereNotNull('current_longitude'); // Filter by restaurant for SaaS multi-tenant if ($this->delivery->restaurant_id) { $query->where('restaurant_id', $this->delivery->restaurant_id); } // Filter riders with less than max concurrent orders $query->withCount(['deliveries' => function ($q) { $q->whereIn('status', ['rider_assigned', 'rider_at_restaurant', 'picked_up', 'on_the_way', 'arrived']); }])->having('deliveries_count', '<', $maxConcurrent); // Calculate distance and filter by radius // Using Haversine formula in raw query for efficiency $lat = $this->delivery->pickup_latitude; $lng = $this->delivery->pickup_longitude; $query->selectRaw(' *, (6371 * acos( cos(radians(?)) * cos(radians(current_latitude)) * cos(radians(current_longitude) - radians(?)) + sin(radians(?)) * sin(radians(current_latitude)) )) AS distance ', [$lat, $lng, $lat]) ->having('distance', '<=', $radius) ->orderBy('distance'); return $query->get(); } /** * Score riders based on various criteria. */ protected function scoreRiders($riders, array $weights) { $maxDistance = config('restaurant-delivery.assignment.assignment_radius', 5); return $riders->map(function ($rider) use ($weights, $maxDistance) { $score = 0; // Distance score (closer is better) $distanceScore = (1 - ($rider->distance / $maxDistance)) * $weights['distance_weight']; $score += $distanceScore; // Rating score $ratingScore = ($rider->rating / 5) * $weights['rating_weight']; $score += $ratingScore; // Acceptance rate score $acceptanceScore = ($rider->acceptance_rate / 100) * $weights['acceptance_rate_weight']; $score += $acceptanceScore; // Current orders score (fewer is better) $ordersScore = (1 - ($rider->deliveries_count / config('restaurant-delivery.assignment.max_concurrent_orders', 3))) * $weights['current_orders_weight']; $score += $ordersScore; // Experience score (normalized by max 1000 deliveries) $experienceScore = min($rider->total_deliveries / 1000, 1) * $weights['experience_weight']; $score += $experienceScore; $rider->assignment_score = $score; return $rider; })->sortByDesc('assignment_score'); } /** * Broadcast assignment request to multiple riders. */ protected function broadcastToRiders($riders, FirebaseService $firebase): void { $timeout = config('restaurant-delivery.assignment.broadcast.accept_timeout', 30); foreach ($riders as $rider) { // Record assignment attempt $this->delivery->addAssignmentToHistory([ 'rider_id' => $rider->id, 'type' => 'broadcast', 'score' => $rider->assignment_score, 'distance' => $rider->distance, ]); // Send push notification to rider if ($rider->fcm_token) { $firebase->sendPushNotification( $rider->fcm_token, 'New Delivery Request', "New delivery from {$this->delivery->restaurant_name}. Distance: ".round($rider->distance, 1).' km', [ 'type' => 'delivery_request', 'delivery_id' => (string) $this->delivery->id, 'tracking_code' => $this->delivery->tracking_code, 'restaurant_name' => $this->delivery->restaurant_name, 'pickup_address' => $this->delivery->pickup_address, 'drop_address' => $this->delivery->drop_address, 'distance' => (string) $this->delivery->distance, 'timeout' => (string) $timeout, ] ); } } Log::info('Delivery request broadcasted to riders', [ 'delivery_id' => $this->delivery->id, 'rider_count' => $riders->count(), ]); // Schedule timeout check dispatch(new CheckAssignmentTimeoutJob($this->delivery)) ->delay(now()->addSeconds($timeout + 5)); } /** * Directly assign to the best scoring rider. */ protected function directAssign(Rider $rider): void { DB::transaction(function () use ($rider) { $this->delivery->assignRider($rider); // Record assignment $this->delivery->addAssignmentToHistory([ 'rider_id' => $rider->id, 'type' => 'direct', 'score' => $rider->assignment_score ?? 0, 'distance' => $rider->distance ?? 0, 'assigned_at' => now()->toIso8601String(), ]); }); Log::info('Rider directly assigned to delivery', [ 'delivery_id' => $this->delivery->id, 'rider_id' => $rider->id, ]); } /** * Handle a job failure. */ public function failed(\Throwable $exception): void { Log::error('Failed to assign rider to delivery', [ 'delivery_id' => $this->delivery->id, 'error' => $exception->getMessage(), ]); } }