Files
kulakpos_web/public/restaurant/Modules/RestaurantDelivery/app/Models/DeliveryZone.php

251 lines
6.4 KiB
PHP
Raw Normal View History

2026-03-15 17:08:23 +07:00
<?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();
}
}