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