migrate to gtea from bistbucket
This commit is contained in:
@@ -0,0 +1,540 @@
|
||||
<?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\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\RestaurantDelivery\Enums\DeliveryStatus;
|
||||
use Modules\RestaurantDelivery\Events\DeliveryCreated;
|
||||
use Modules\RestaurantDelivery\Events\DeliveryStatusChanged;
|
||||
use Modules\RestaurantDelivery\Traits\HasRestaurant;
|
||||
use Modules\RestaurantDelivery\Traits\HasUuid;
|
||||
|
||||
class Delivery extends Model
|
||||
{
|
||||
use HasFactory, HasRestaurant, HasUuid, SoftDeletes;
|
||||
|
||||
public const TABLE_NAME = 'restaurant_deliveries';
|
||||
|
||||
protected $table = self::TABLE_NAME;
|
||||
|
||||
protected $fillable = [
|
||||
'tracking_code',
|
||||
'restaurant_id',
|
||||
'rider_id',
|
||||
'zone_id',
|
||||
'orderable_type',
|
||||
'orderable_id',
|
||||
'status',
|
||||
'previous_status',
|
||||
'status_changed_at',
|
||||
'restaurant_name',
|
||||
'pickup_address',
|
||||
'pickup_latitude',
|
||||
'pickup_longitude',
|
||||
'pickup_contact_name',
|
||||
'pickup_contact_phone',
|
||||
'pickup_instructions',
|
||||
'customer_name',
|
||||
'drop_address',
|
||||
'drop_latitude',
|
||||
'drop_longitude',
|
||||
'drop_contact_name',
|
||||
'drop_contact_phone',
|
||||
'drop_instructions',
|
||||
'drop_floor',
|
||||
'drop_apartment',
|
||||
'distance',
|
||||
'distance_unit',
|
||||
'estimated_duration',
|
||||
'estimated_pickup_time',
|
||||
'estimated_delivery_time',
|
||||
'food_ready_at',
|
||||
'rider_assigned_at',
|
||||
'rider_accepted_at',
|
||||
'rider_at_restaurant_at',
|
||||
'picked_up_at',
|
||||
'on_the_way_at',
|
||||
'arrived_at',
|
||||
'delivered_at',
|
||||
'cancelled_at',
|
||||
'failed_at',
|
||||
'base_fare',
|
||||
'distance_charge',
|
||||
'surge_charge',
|
||||
'surge_multiplier',
|
||||
'peak_hour_charge',
|
||||
'late_night_charge',
|
||||
'small_order_fee',
|
||||
'total_delivery_charge',
|
||||
'charge_breakdown',
|
||||
'tip_amount',
|
||||
'tip_type',
|
||||
'tip_paid_at',
|
||||
'order_value',
|
||||
'cancellation_reason',
|
||||
'cancelled_by',
|
||||
'cancellation_notes',
|
||||
'failure_reason',
|
||||
'failure_notes',
|
||||
'route_polyline',
|
||||
'route_data',
|
||||
'assignment_attempts',
|
||||
'reassignment_count',
|
||||
'assignment_history',
|
||||
'delivery_photo',
|
||||
'signature',
|
||||
'recipient_name',
|
||||
'customer_notified',
|
||||
'notification_log',
|
||||
'is_priority',
|
||||
'is_scheduled',
|
||||
'scheduled_for',
|
||||
'meta',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => 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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user