migrate to gtea from bistbucket
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user