Files
kulakpos_web/public/restaurant/Modules/RestaurantDelivery/app/Services/Rating/RatingService.php

403 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Services\Rating;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Modules\RestaurantDelivery\Events\RiderRated;
use Modules\RestaurantDelivery\Models\Delivery;
use Modules\RestaurantDelivery\Models\DeliveryRating;
use Modules\RestaurantDelivery\Models\Rider;
class RatingService
{
protected array $config;
public function __construct()
{
$this->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(),
];
}
}