DeliveryStatus::class, // <-- cast status to enum 'pickup_latitude' => 'decimal:7', 'pickup_longitude' => 'decimal:7', 'drop_latitude' => 'decimal:7', 'drop_longitude' => 'decimal:7', 'distance' => 'decimal:2', 'base_fare' => 'decimal:2', 'distance_charge' => 'decimal:2', 'surge_charge' => 'decimal:2', 'surge_multiplier' => 'decimal:2', 'peak_hour_charge' => 'decimal:2', 'late_night_charge' => 'decimal:2', 'small_order_fee' => 'decimal:2', 'total_delivery_charge' => 'decimal:2', 'tip_amount' => 'decimal:2', 'order_value' => 'decimal:2', 'status_changed_at' => 'datetime', 'estimated_pickup_time' => 'datetime', 'estimated_delivery_time' => 'datetime', 'food_ready_at' => 'datetime', 'rider_assigned_at' => 'datetime', 'rider_accepted_at' => 'datetime', 'rider_at_restaurant_at' => 'datetime', 'picked_up_at' => 'datetime', 'on_the_way_at' => 'datetime', 'arrived_at' => 'datetime', 'delivered_at' => 'datetime', 'cancelled_at' => 'datetime', 'failed_at' => 'datetime', 'tip_paid_at' => 'datetime', 'scheduled_for' => 'datetime', 'is_priority' => 'boolean', 'is_scheduled' => 'boolean', 'customer_notified' => 'boolean', 'charge_breakdown' => 'array', 'route_data' => 'array', 'assignment_history' => 'array', 'notification_log' => 'array', 'meta' => 'array', ]; protected static function boot() { parent::boot(); static::creating(function ($delivery) { if (empty($delivery->tracking_code)) { $delivery->tracking_code = static::generateTrackingCode(); } }); static::created(function ($delivery) { event(new DeliveryCreated($delivery)); }); } /* |-------------------------------------------------------------------------- | Relationships |-------------------------------------------------------------------------- */ public function orderable(): MorphTo { return $this->morphTo(); } public function rider(): BelongsTo { return $this->belongsTo(Rider::class, 'rider_id'); } public function zone(): BelongsTo { return $this->belongsTo(DeliveryZone::class, 'zone_id'); } public function rating(): HasOne { return $this->hasOne(DeliveryRating::class, 'delivery_id') ->where('is_restaurant_rating', false); } public function restaurantRating(): HasOne { return $this->hasOne(DeliveryRating::class, 'delivery_id') ->where('is_restaurant_rating', true); } public function ratings(): HasMany { return $this->hasMany(DeliveryRating::class, 'delivery_id'); } public function tip(): HasOne { return $this->hasOne(DeliveryTip::class, 'delivery_id'); } public function assignments(): HasMany { return $this->hasMany(DeliveryAssignment::class, 'delivery_id'); } public function statusHistory(): HasMany { return $this->hasMany(DeliveryStatusHistory::class, 'delivery_id') ->orderBy('changed_at', 'desc'); } public function locationLogs(): HasMany { return $this->hasMany(LocationLog::class, 'delivery_id') ->orderBy('recorded_at', 'desc'); } public function riderEarning(): HasOne { return $this->hasOne(RiderEarning::class, 'delivery_id') ->where('type', 'delivery'); } /* |-------------------------------------------------------------------------- | Scopes |-------------------------------------------------------------------------- */ public function scopeActive($query) { return $query->whereIn('status', array_map( fn ($s) => $s->value, DeliveryStatus::activeStatuses() )); } public function scopeCompleted($query) { return $query->where('status', DeliveryStatus::DELIVERED); } public function scopeCancelled($query) { return $query->where('status', DeliveryStatus::CANCELLED); } public function scopeFailed($query) { return $query->where('status', DeliveryStatus::FAILED); } public function scopeForRider($query, int $riderId) { return $query->where('rider_id', $riderId); } public function scopeForRestaurant($query, int $restaurantId) { return $query->where('restaurant_id', $restaurantId); } public function scopeNeedsRider($query) { return $query->where('status', DeliveryStatus::READY_FOR_PICKUP) ->whereNull('rider_id'); } public function scopeScheduledFor($query, $date) { return $query->where('is_scheduled', true) ->whereDate('scheduled_for', $date); } /* |-------------------------------------------------------------------------- | Status Methods |-------------------------------------------------------------------------- */ public function updateStatus(string|DeliveryStatus $newStatus, ?array $metadata = null): bool { if (is_string($newStatus)) { $newStatus = DeliveryStatus::from($newStatus); } if (! $this->status->canTransitionTo($newStatus)) { return false; } $previousStatus = $this->status; $this->previous_status = $previousStatus->value; $this->status = $newStatus; $this->status_changed_at = now(); // Set specific timestamps $this->setStatusTimestamp($newStatus); $this->save(); // Record status history DeliveryStatusHistory::create([ 'delivery_id' => $this->id, 'rider_id' => $this->rider_id, 'from_status' => $previousStatus->value, 'to_status' => $newStatus->value, 'changed_by_type' => $metadata['changed_by_type'] ?? 'system', 'changed_by_id' => $metadata['changed_by_id'] ?? null, 'latitude' => $metadata['latitude'] ?? null, 'longitude' => $metadata['longitude'] ?? null, 'notes' => $metadata['notes'] ?? null, 'meta' => $metadata['meta'] ?? null, 'changed_at' => now(), ]); event(new DeliveryStatusChanged($this, $previousStatus, $metadata)); return true; } protected function setStatusTimestamp(DeliveryStatus $status): void { $timestampMap = [ DeliveryStatus::READY_FOR_PICKUP->value => 'food_ready_at', DeliveryStatus::RIDER_ASSIGNED->value => 'rider_assigned_at', DeliveryStatus::RIDER_AT_RESTAURANT->value => 'rider_at_restaurant_at', DeliveryStatus::PICKED_UP->value => 'picked_up_at', DeliveryStatus::ON_THE_WAY->value => 'on_the_way_at', DeliveryStatus::ARRIVED->value => 'arrived_at', DeliveryStatus::DELIVERED->value => 'delivered_at', DeliveryStatus::CANCELLED->value => 'cancelled_at', DeliveryStatus::FAILED->value => 'failed_at', ]; if (isset($timestampMap[$status->value])) { $this->{$timestampMap[$status->value]} = now(); } } public function confirm(): bool { return $this->updateStatus(DeliveryStatus::CONFIRMED); } public function markPreparing(): bool { return $this->updateStatus(DeliveryStatus::PREPARING); } public function markReadyForPickup(): bool { return $this->updateStatus(DeliveryStatus::READY_FOR_PICKUP); } public function assignRider(Rider $rider): bool { $this->rider_id = $rider->id; $this->rider_assigned_at = now(); $this->save(); return $this->updateStatus(DeliveryStatus::RIDER_ASSIGNED); } public function markRiderAtRestaurant(): bool { return $this->updateStatus(DeliveryStatus::RIDER_AT_RESTAURANT); } public function markPickedUp(): bool { return $this->updateStatus(DeliveryStatus::PICKED_UP); } public function markOnTheWay(): bool { return $this->updateStatus(DeliveryStatus::ON_THE_WAY); } public function markArrived(): bool { return $this->updateStatus(DeliveryStatus::ARRIVED); } public function markDelivered(array $proofData = []): bool { if (! empty($proofData)) { $this->fill([ 'delivery_photo' => $proofData['photo'] ?? null, 'signature' => $proofData['signature'] ?? null, 'recipient_name' => $proofData['recipient_name'] ?? null, ]); } return $this->updateStatus(DeliveryStatus::DELIVERED); } public function cancel(string $reason, string $cancelledBy, ?string $notes = null): bool { $this->cancellation_reason = $reason; $this->cancelled_by = $cancelledBy; $this->cancellation_notes = $notes; return $this->updateStatus(DeliveryStatus::CANCELLED, [ 'changed_by_type' => $cancelledBy, 'notes' => $notes, ]); } public function markFailed(string $reason, ?string $notes = null): bool { $this->failure_reason = $reason; $this->failure_notes = $notes; return $this->updateStatus(DeliveryStatus::FAILED, [ 'notes' => $notes, ]); } /* |-------------------------------------------------------------------------- | Helper Methods |-------------------------------------------------------------------------- */ public static function generateTrackingCode(): string { do { $code = 'RD'.strtoupper(Str::random(8)); } while (static::where('tracking_code', $code)->exists()); return $code; } public function isActive(): bool { return $this->status->isActive(); } public function isCompleted(): bool { return true; } public function isTrackable(): bool { return $this->status->isTrackable() && $this->rider_id !== null; } public function hasRider(): bool { return $this->rider_id !== null; } public function canBeRated(): bool { if (! $this->isCompleted() || ! $this->delivered_at) { return false; } $window = config('restaurant-delivery.rating.rating_window', 72); return $this->delivered_at->diffInHours(now()) <= $window ?? null; } public function canReceiveTip(): bool { if (! $this->hasRider()) { return false; } // Check if post-delivery tip is allowed if ($this->isCompleted() && $this->delivered_at) { $window = config('restaurant-delivery.tip.post_delivery_window', 24); return $this->delivered_at->diffInHours(now()) <= $window; } // Pre-delivery tip is allowed for active deliveries return config('restaurant-delivery.tip.allow_pre_delivery', true) && $this->isActive(); } public function getTotalChargeWithTip(): float { return $this->total_delivery_charge + $this->tip_amount; } public function getActualDuration(): ?int { if (! $this->delivered_at) { return null; } return $this->created_at->diffInMinutes($this->delivered_at); } public function wasDelayed(): bool { if (! $this->estimated_delivery_time || ! $this->delivered_at) { return false; } return $this->delivered_at->gt($this->estimated_delivery_time); } public function getDelayMinutes(): ?int { if (! $this->wasDelayed()) { return null; } return $this->estimated_delivery_time->diffInMinutes($this->delivered_at); } public function addAssignmentToHistory(array $assignmentData): void { $history = $this->assignment_history ?? []; $history[] = array_merge($assignmentData, ['timestamp' => now()->toIso8601String()]); $this->update([ 'assignment_history' => $history, 'assignment_attempts' => count($history), ]); } public function logNotification(string $type, array $data = []): void { $log = $this->notification_log ?? []; $log[] = [ 'type' => $type, 'data' => $data, 'sent_at' => now()->toIso8601String(), ]; $this->update(['notification_log' => $log]); } }