config = config('restaurant-delivery.rating'); } /* |-------------------------------------------------------------------------- | Create Rating |-------------------------------------------------------------------------- */ /** * Create a rating for a delivery */ public function createRating( Delivery $delivery, int $overallRating, array $categoryRatings = [], ?string $review = null, array $tags = [], bool $isAnonymous = false, ?int $customerId = null, bool $isRestaurantRating = false ): ?DeliveryRating { // Validate can rate if (! $this->canRate($delivery, $isRestaurantRating)) { Log::warning('Cannot rate delivery', [ 'delivery_id' => $delivery->id, 'reason' => 'Rating window expired or already rated', ]); return null; } // Validate rating value if (! $this->isValidRating($overallRating)) { return null; } // Validate review length if ($review && ! $this->isValidReview($review)) { return null; } try { $rating = DB::transaction(function () use ( $delivery, $overallRating, $categoryRatings, $review, $tags, $isAnonymous, $customerId, $isRestaurantRating ) { $rating = DeliveryRating::create([ 'delivery_id' => $delivery->id, 'rider_id' => $delivery->rider_id, 'customer_id' => $customerId, 'restaurant_id' => $delivery->restaurant_id, 'overall_rating' => $overallRating, 'speed_rating' => $categoryRatings['speed'] ?? null, 'communication_rating' => $categoryRatings['communication'] ?? null, 'food_condition_rating' => $categoryRatings['food_condition'] ?? null, 'professionalism_rating' => $categoryRatings['professionalism'] ?? null, 'review' => $review, 'is_anonymous' => $isAnonymous, 'tags' => array_filter($tags), 'is_restaurant_rating' => $isRestaurantRating, 'is_approved' => $this->shouldAutoApprove($overallRating, $review), 'moderation_status' => $this->shouldAutoApprove($overallRating, $review) ? 'approved' : 'pending', ]); // Update rider's rating $delivery->rider->updateRating($overallRating); // Check if rider needs warning or suspension $this->checkRatingThresholds($delivery->rider); return $rating; }); // Dispatch event Event::dispatch(new RiderRated($rating)); return $rating; } catch (\Exception $e) { Log::error('Failed to create rating', [ 'delivery_id' => $delivery->id, 'error' => $e->getMessage(), ]); return null; } } /** * Check if delivery can be rated */ public function canRate(Delivery $delivery, bool $isRestaurantRating = false): bool { if (! $this->config['enabled']) { return false; } if (! $delivery->isCompleted()) { return false; } if (! $delivery->hasRider()) { return false; } // Check rating window $window = $this->config['rating_window']; // If delivered_at is null, rating is not allowed if (! $delivery->delivered_at) { return false; } if ($delivery->delivered_at->diffInHours(now()) > $window) { return false; } // Check if already rated $existingRating = DeliveryRating::where('delivery_id', $delivery->id) ->where('is_restaurant_rating', $isRestaurantRating) ->exists(); return ! $existingRating; } /* |-------------------------------------------------------------------------- | Validation |-------------------------------------------------------------------------- */ public function isValidRating(int $rating): bool { return $rating >= $this->config['min_rating'] && $rating <= $this->config['max_rating']; } public function isValidReview(?string $review): bool { if (! $this->config['allow_review']) { return $review === null; } if ($review === null) { return true; } return strlen($review) <= $this->config['review_max_length']; } protected function shouldAutoApprove(int $rating, ?string $review): bool { // Auto-approve high ratings without review if ($rating >= 4 && ! $review) { return true; } // Auto-approve high ratings with short reviews if ($rating >= 4 && $review && strlen($review) < 100) { return true; } // Manual review for low ratings or long reviews return false; } /* |-------------------------------------------------------------------------- | Rating Thresholds |-------------------------------------------------------------------------- */ protected function checkRatingThresholds(Rider $rider): void { $thresholds = $this->config['thresholds']; // Need minimum ratings before thresholds apply if ($rider->rating_count < $thresholds['minimum_ratings_for_threshold']) { return; } // Check suspension threshold if ($rider->rating <= $thresholds['suspension_threshold']) { $this->suspendRider($rider); return; } // Check warning threshold if ($rider->rating <= $thresholds['warning_threshold']) { $this->warnRider($rider); } } protected function suspendRider(Rider $rider): void { $rider->update(['status' => 'suspended']); Log::info('Rider suspended due to low rating', [ 'rider_id' => $rider->id, 'rating' => $rider->rating, ]); // TODO: Send notification to rider } protected function warnRider(Rider $rider): void { Log::info('Rider warned due to low rating', [ 'rider_id' => $rider->id, 'rating' => $rider->rating, ]); // TODO: Send warning notification to rider } /* |-------------------------------------------------------------------------- | Rider Response |-------------------------------------------------------------------------- */ /** * Add rider's response to a rating */ public function addRiderResponse(DeliveryRating $rating, string $response): bool { if ($rating->rider_response) { return false; // Already responded } $rating->addRiderResponse($response); return true; } /* |-------------------------------------------------------------------------- | Moderation |-------------------------------------------------------------------------- */ /** * Approve a rating */ public function approveRating(DeliveryRating $rating): void { $rating->approve(); } /** * Reject a rating */ public function rejectRating(DeliveryRating $rating, string $reason): void { $rating->reject($reason); // Recalculate rider's rating without this one $rating->rider->recalculateRating(); } /** * Feature a rating */ public function featureRating(DeliveryRating $rating): void { $rating->feature(); } /* |-------------------------------------------------------------------------- | Statistics |-------------------------------------------------------------------------- */ /** * Get rider's rating statistics */ public function getRiderStats(Rider $rider): array { $ratings = $rider->ratings()->approved()->get(); if ($ratings->isEmpty()) { return [ 'average_rating' => null, 'total_ratings' => 0, 'rating_distribution' => [1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0], 'category_averages' => null, 'recent_reviews' => [], 'positive_tags' => [], 'negative_tags' => [], ]; } // Rating distribution $distribution = [1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0]; foreach ($ratings as $rating) { $distribution[$rating->overall_rating]++; } // Category averages $categoryAverages = [ 'speed' => $ratings->whereNotNull('speed_rating')->avg('speed_rating'), 'communication' => $ratings->whereNotNull('communication_rating')->avg('communication_rating'), 'food_condition' => $ratings->whereNotNull('food_condition_rating')->avg('food_condition_rating'), 'professionalism' => $ratings->whereNotNull('professionalism_rating')->avg('professionalism_rating'), ]; // Tag counts $allTags = $ratings->pluck('tags')->flatten()->filter()->countBy(); $positiveTags = $allTags->only(DeliveryRating::getPositiveTags())->toArray(); $negativeTags = $allTags->only(DeliveryRating::getNegativeTags())->toArray(); // Recent reviews $recentReviews = $rider->ratings() ->approved() ->withReview() ->recent($this->config['display']['show_recent_reviews']) ->get() ->map(fn ($r) => [ 'rating' => $r->overall_rating, 'review' => $r->review, 'created_at' => $r->created_at->diffForHumans(), 'is_anonymous' => $r->is_anonymous, 'rider_response' => $r->rider_response, ]); return [ 'average_rating' => round($rider->rating, 2), 'total_ratings' => $ratings->count(), 'rating_distribution' => $distribution, 'category_averages' => array_map(fn ($v) => $v ? round($v, 2) : null, $categoryAverages), 'recent_reviews' => $recentReviews->toArray(), 'positive_tags' => $positiveTags, 'negative_tags' => $negativeTags, ]; } /** * Get ratings for display on tracking page */ public function getRiderDisplayInfo(Rider $rider): array { if (! $this->config['display']['show_on_tracking']) { return []; } $data = [ 'rating' => round($rider->rating, 1), 'star_display' => str_repeat('★', (int) round($rider->rating)).str_repeat('☆', 5 - (int) round($rider->rating)), ]; if ($this->config['display']['show_review_count']) { $data['review_count'] = $rider->rating_count; } return $data; } /** * Get available rating categories */ public function getCategories(): array { return $this->config['categories']; } /** * Get positive and negative tags */ public function getTags(): array { return [ 'positive' => DeliveryRating::getPositiveTags(), 'negative' => DeliveryRating::getNegativeTags(), ]; } }