251 lines
6.4 KiB
PHP
251 lines
6.4 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\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Modules\RestaurantDelivery\Traits\HasRestaurant;
|
|
use Modules\RestaurantDelivery\Traits\HasUuid;
|
|
|
|
class DeliveryZone extends Model
|
|
{
|
|
use HasFactory, HasRestaurant, HasUuid, SoftDeletes;
|
|
|
|
public const TABLE_NAME = 'restaurant_delivery_zones';
|
|
|
|
protected $table = self::TABLE_NAME;
|
|
|
|
protected $fillable = [
|
|
'id',
|
|
'uuid',
|
|
'restaurant_id',
|
|
'name',
|
|
'slug',
|
|
'description',
|
|
'color',
|
|
'coordinates',
|
|
'min_lat',
|
|
'max_lat',
|
|
'min_lng',
|
|
'max_lng',
|
|
'priority',
|
|
'is_active',
|
|
'is_default',
|
|
'max_delivery_distance',
|
|
'operating_hours',
|
|
];
|
|
|
|
protected $casts = [
|
|
'coordinates' => 'array',
|
|
'operating_hours' => 'array',
|
|
'is_active' => 'boolean',
|
|
'is_default' => 'boolean',
|
|
'min_lat' => 'decimal:7',
|
|
'max_lat' => 'decimal:7',
|
|
'min_lng' => 'decimal:7',
|
|
'max_lng' => 'decimal:7',
|
|
'max_delivery_distance' => 'decimal:2',
|
|
];
|
|
|
|
protected static function boot()
|
|
{
|
|
parent::boot();
|
|
|
|
static::saving(function ($zone) {
|
|
if (! empty($zone->coordinates)) {
|
|
$zone->calculateBoundingBox();
|
|
}
|
|
});
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Relationships
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
public function pricingRules(): HasMany
|
|
{
|
|
return $this->hasMany(ZonePricingRule::class, 'zone_id')
|
|
->orderBy('priority');
|
|
}
|
|
|
|
public function activePricingRule(): HasMany
|
|
{
|
|
return $this->hasMany(ZonePricingRule::class, 'zone_id')
|
|
->where('is_active', true)
|
|
->orderBy('priority')
|
|
->limit(1);
|
|
}
|
|
|
|
public function deliveries(): HasMany
|
|
{
|
|
return $this->hasMany(Delivery::class, 'zone_id');
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Scopes
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
public function scopeDefault($query)
|
|
{
|
|
return $query->where('is_default', true);
|
|
}
|
|
|
|
public function scopeByPriority($query)
|
|
{
|
|
return $query->orderByDesc('priority');
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Methods
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
|
|
public function calculateBoundingBox(): void
|
|
{
|
|
if (empty($this->coordinates)) {
|
|
return;
|
|
}
|
|
|
|
$lats = array_column($this->coordinates, 0);
|
|
$lngs = array_column($this->coordinates, 1);
|
|
|
|
$this->min_lat = min($lats);
|
|
$this->max_lat = max($lats);
|
|
$this->min_lng = min($lngs);
|
|
$this->max_lng = max($lngs);
|
|
}
|
|
|
|
public function containsPoint(float $latitude, float $longitude): bool
|
|
{
|
|
// Quick bounding box check first
|
|
if (
|
|
$latitude < $this->min_lat ||
|
|
$latitude > $this->max_lat ||
|
|
$longitude < $this->min_lng ||
|
|
$longitude > $this->max_lng
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// Ray casting algorithm for polygon containment
|
|
return $this->pointInPolygon($latitude, $longitude, $this->coordinates);
|
|
}
|
|
|
|
protected function pointInPolygon(float $latitude, float $longitude, array $polygon): bool
|
|
{
|
|
$n = count($polygon);
|
|
$inside = false;
|
|
|
|
$x = $longitude;
|
|
$y = $latitude;
|
|
|
|
$p1x = $polygon[0][1];
|
|
$p1y = $polygon[0][0];
|
|
|
|
for ($i = 1; $i <= $n; $i++) {
|
|
$p2x = $polygon[$i % $n][1];
|
|
$p2y = $polygon[$i % $n][0];
|
|
|
|
if ($y > min($p1y, $p2y)) {
|
|
if ($y <= max($p1y, $p2y)) {
|
|
if ($x <= max($p1x, $p2x)) {
|
|
if ($p1y != $p2y) {
|
|
$xinters = ($y - $p1y) * ($p2x - $p1x) / ($p2y - $p1y) + $p1x;
|
|
}
|
|
if ($p1x == $p2x || $x <= $xinters) {
|
|
$inside = ! $inside;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$p1x = $p2x;
|
|
$p1y = $p2y;
|
|
}
|
|
|
|
return $inside;
|
|
}
|
|
|
|
public function isOperatingNow(): bool
|
|
{
|
|
if (empty($this->operating_hours)) {
|
|
return true; // No restrictions
|
|
}
|
|
|
|
$now = now();
|
|
$dayName = strtolower($now->format('l'));
|
|
|
|
if (! isset($this->operating_hours[$dayName])) {
|
|
return false;
|
|
}
|
|
|
|
$hours = $this->operating_hours[$dayName];
|
|
|
|
if (isset($hours['closed']) && $hours['closed']) {
|
|
return false;
|
|
}
|
|
|
|
$openTime = \Carbon\Carbon::createFromTimeString($hours['open']);
|
|
$closeTime = \Carbon\Carbon::createFromTimeString($hours['close']);
|
|
|
|
// Handle overnight hours
|
|
if ($closeTime->lt($openTime)) {
|
|
return $now->gte($openTime) || $now->lte($closeTime);
|
|
}
|
|
|
|
return $now->between($openTime, $closeTime);
|
|
}
|
|
|
|
public function getActivePricingRule(): ?ZonePricingRule
|
|
{
|
|
return $this->pricingRules()
|
|
->where('is_active', true)
|
|
->orderBy('priority')
|
|
->first();
|
|
}
|
|
|
|
public function canDeliverTo(float $latitude, float $longitude): bool
|
|
{
|
|
if (! $this->is_active) {
|
|
return false;
|
|
}
|
|
|
|
if (! $this->containsPoint($latitude, $longitude)) {
|
|
return false;
|
|
}
|
|
|
|
if (! $this->isOperatingNow()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static function findForPoint(float $latitude, float $longitude): ?self
|
|
{
|
|
return static::active()
|
|
->byPriority()
|
|
->get()
|
|
->first(fn ($zone) => $zone->containsPoint($latitude, $longitude));
|
|
}
|
|
|
|
public static function getDefault(): ?self
|
|
{
|
|
return static::active()->default()->first();
|
|
}
|
|
}
|