Files

460 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Modules\Authentication\Models\User;
use Modules\RestaurantDelivery\Enums\CommissionType;
use Modules\RestaurantDelivery\Enums\RiderStatus;
use Modules\RestaurantDelivery\Enums\RiderType;
use Modules\RestaurantDelivery\Enums\VehicleType;
use Modules\RestaurantDelivery\Traits\HasRestaurant;
use Modules\RestaurantDelivery\Traits\HasUuid;
class Rider extends Model
{
use HasFactory, HasRestaurant, HasUuid, SoftDeletes;
public const TABLE_NAME = 'restaurant_riders';
protected $table = self::TABLE_NAME;
protected $fillable = [
'restaurant_id',
'user_id',
'first_name',
'last_name',
'phone',
'email',
'photo',
'date_of_birth',
'national_id',
'emergency_contact',
'type',
'status',
'vehicle_type',
'vehicle_number',
'vehicle_model',
'vehicle_color',
'license_number',
'license_expiry',
'is_verified',
'verified_at',
'verified_by',
'verification_documents',
'commission_type',
'commission_rate',
'base_commission',
'per_km_rate',
'current_latitude',
'current_longitude',
'last_location_update',
'rating',
'rating_count',
'total_deliveries',
'successful_deliveries',
'cancelled_deliveries',
'failed_deliveries',
'acceptance_rate',
'completion_rate',
'is_online',
'last_online_at',
'went_offline_at',
'fcm_token',
'device_id',
'bank_name',
'bank_account_number',
'bank_account_name',
'mobile_wallet_number',
'mobile_wallet_provider',
'assigned_zones',
'meta',
];
protected $casts = [
'date_of_birth' => '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);
}
}