'date', 'license_expiry' => 'date', 'is_verified' => 'boolean', 'verified_at' => 'datetime', 'is_online' => 'boolean', 'last_online_at' => 'datetime', 'went_offline_at' => 'datetime', 'last_location_update' => 'datetime', 'current_latitude' => 'decimal:7', 'current_longitude' => 'decimal:7', 'rating' => 'decimal:2', 'acceptance_rate' => 'decimal:2', 'completion_rate' => 'decimal:2', 'commission_rate' => 'decimal:2', 'base_commission' => 'decimal:2', 'per_km_rate' => 'decimal:2', 'verification_documents' => 'array', 'assigned_zones' => 'array', 'meta' => 'array', ]; protected $appends = [ 'full_name', 'photo_url', ]; /* |-------------------------------------------------------------------------- | Accessors |-------------------------------------------------------------------------- */ public function getFullNameAttribute(): string { return trim("{$this->first_name} {$this->last_name}"); } public function getPhotoUrlAttribute(): ?string { if (! $this->photo) { return null; } if (filter_var($this->photo, FILTER_VALIDATE_URL)) { return $this->photo; } return asset('storage/'.$this->photo); } /* |-------------------------------------------------------------------------- | Relationships |-------------------------------------------------------------------------- */ public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } public function deliveries(): HasMany { return $this->hasMany(Delivery::class, 'rider_id'); } public function activeDeliveries(): HasMany { return $this->hasMany(Delivery::class, 'rider_id') ->whereIn('status', array_map( fn ($s) => $s->value, \Modules\RestaurantDelivery\Enums\DeliveryStatus::riderActiveStatuses() )); } public function completedDeliveries(): HasMany { return $this->hasMany(Delivery::class, 'rider_id') ->where('status', 'delivered'); } public function ratings(): HasMany { return $this->hasMany(DeliveryRating::class, 'rider_id'); } public function earnings(): HasMany { return $this->hasMany(RiderEarning::class, 'rider_id'); } public function tips(): HasMany { return $this->hasMany(DeliveryTip::class, 'rider_id'); } public function payouts(): HasMany { return $this->hasMany(RiderPayout::class, 'rider_id'); } public function assignments(): HasMany { return $this->hasMany(DeliveryAssignment::class, 'rider_id'); } public function locationLogs(): HasMany { return $this->hasMany(LocationLog::class, 'rider_id'); } /* |-------------------------------------------------------------------------- | Scopes |-------------------------------------------------------------------------- */ // ------------------------------- // Enum Accessors / Mutators // ------------------------------- public function getTypeAttribute($value): ?RiderType { return $value ? RiderType::tryFrom($value) : null; } // After (works with DB strings) public function setTypeAttribute($type): void { if ($type instanceof RiderType) { $this->attributes['type'] = $type->value; } elseif (is_string($type)) { $this->attributes['type'] = $type; // assume DB value is valid } else { $this->attributes['type'] = null; } } public function getStatusAttribute($value): ?RiderStatus { return $value ? RiderStatus::tryFrom($value) : null; } public function setStatusAttribute($status): void { if ($status instanceof RiderStatus) { $this->attributes['status'] = $status->value; } elseif (is_string($status)) { $this->attributes['status'] = $status; } else { $this->attributes['status'] = null; } } public function getVehicleTypeAttribute($value): ?VehicleType { return $value ? VehicleType::tryFrom($value) : null; } public function setVehicleTypeAttribute($vehicleType): void { if ($vehicleType instanceof VehicleType) { $this->attributes['vehicle_type'] = $vehicleType->value; } elseif (is_string($vehicleType)) { $this->attributes['vehicle_type'] = $vehicleType; } else { $this->attributes['vehicle_type'] = null; } } public function getCommissionTypeAttribute($value): ?CommissionType { return $value ? CommissionType::tryFrom($value) : null; } public function setCommissionTypeAttribute($type): void { if ($type instanceof CommissionType) { $this->attributes['commission_type'] = $type->value; } elseif (is_string($type)) { $this->attributes['commission_type'] = $type; } else { $this->attributes['commission_type'] = null; } } public function scopeActive($query) { return $query->where('status', RiderStatus::ACTIVE); } public function scopeOnline($query) { return $query->where('is_online', true); } public function scopeVerified($query) { return $query->where('is_verified', true); } public function scopeAvailable($query) { return $query->active() ->online() ->verified() ->whereDoesntHave('activeDeliveries', function ($q) { $maxConcurrent = config('restaurant-delivery.assignment.max_concurrent_orders', 3); $q->havingRaw('COUNT(*) >= ?', [$maxConcurrent]); }); } public function scopeNearby($query, float $latitude, float $longitude, float $radiusKm = 5) { $earthRadius = config('restaurant-delivery.distance.earth_radius_km', 6371); return $query->selectRaw(" *, ({$earthRadius} * acos( cos(radians(?)) * cos(radians(current_latitude)) * cos(radians(current_longitude) - radians(?)) + sin(radians(?)) * sin(radians(current_latitude)) )) AS distance ", [$latitude, $longitude, $latitude]) ->having('distance', '<=', $radiusKm) ->orderBy('distance'); } public function scopeWithMinRating($query, float $minRating) { return $query->where('rating', '>=', $minRating); } /* |-------------------------------------------------------------------------- | Methods |-------------------------------------------------------------------------- */ public function canAcceptOrders(): bool { return $this->status->canAcceptOrders() && $this->is_verified && $this->is_online; } public function getCurrentOrderCount(): int { return $this->activeDeliveries()->count(); } public function canTakeMoreOrders(): bool { $maxConcurrent = config('restaurant-delivery.assignment.max_concurrent_orders', 3); return $this->getCurrentOrderCount() < $maxConcurrent; } public function goOnline(): void { $this->update([ 'is_online' => true, 'last_online_at' => now(), 'went_offline_at' => null, ]); } public function goOffline(): void { $this->update([ 'is_online' => false, 'went_offline_at' => now(), ]); } public function updateLocation(float $latitude, float $longitude): void { $this->update([ 'current_latitude' => $latitude, 'current_longitude' => $longitude, 'last_location_update' => now(), ]); } public function updateRating(float $newRating): void { $totalRating = ($this->rating * $this->rating_count) + $newRating; $newCount = $this->rating_count + 1; $this->update([ 'rating' => round($totalRating / $newCount, 2), 'rating_count' => $newCount, ]); } public function recalculateRating(): void { $stats = $this->ratings() ->selectRaw('AVG(overall_rating) as avg_rating, COUNT(*) as count') ->first(); $avgRating = (float) ($stats->avg_rating ?? 5.00); $count = (int) ($stats->count ?? 0); $this->update([ 'rating' => round($avgRating, 2), 'rating_count' => $count, ]); } public function incrementDeliveryStats(bool $successful = true): void { $this->increment('total_deliveries'); if ($successful) { $this->increment('successful_deliveries'); } else { $this->increment('failed_deliveries'); } $this->updateCompletionRate(); } public function incrementCancelledDeliveries(): void { $this->increment('cancelled_deliveries'); $this->updateAcceptanceRate(); } protected function updateCompletionRate(): void { if ($this->total_deliveries > 0) { $rate = ($this->successful_deliveries / $this->total_deliveries) * 100; $this->update(['completion_rate' => round($rate, 2)]); } } protected function updateAcceptanceRate(): void { $totalAssignments = $this->assignments()->count(); $acceptedAssignments = $this->assignments()->where('status', 'accepted')->count(); if ($totalAssignments > 0) { $rate = ($acceptedAssignments / $totalAssignments) * 100; $this->update(['acceptance_rate' => round($rate, 2)]); } } public function calculateEarningForDelivery(Delivery $delivery): float { $deliveryCharge = $delivery->total_delivery_charge; return match ($this->commission_type) { CommissionType::FIXED => $this->commission_rate, CommissionType::PERCENTAGE => ($deliveryCharge * $this->commission_rate) / 100, CommissionType::PER_KM => $delivery->distance * $this->commission_rate, CommissionType::HYBRID => $this->base_commission + ($delivery->distance * ($this->per_km_rate ?? 0)), }; } public function getDistanceTo(float $latitude, float $longitude): float { if (! $this->current_latitude || ! $this->current_longitude) { return PHP_FLOAT_MAX; } $earthRadius = config('restaurant-delivery.distance.earth_radius_km', 6371); $dLat = deg2rad($latitude - $this->current_latitude); $dLng = deg2rad($longitude - $this->current_longitude); $a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($this->current_latitude)) * cos(deg2rad($latitude)) * sin($dLng / 2) * sin($dLng / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); return round($earthRadius * $c, 2); } }