| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCart\App\Models; |
| 4 |
|
| 5 |
|
| 6 |
use FluentCart\App\Helpers\AddressHelper; |
| 7 |
use FluentCart\App\Models\Concerns\CanSearch; |
| 8 |
use FluentCart\Framework\Database\Orm\Relations\BelongsTo; |
| 9 |
|
| 10 |
/** |
| 11 |
* Shipping Method Model - DB Model for Shipping Methods |
| 12 |
* |
| 13 |
* @package FluentCart\App\Models |
| 14 |
* @version 1.0.0 |
| 15 |
*/ |
| 16 |
class ShippingMethod extends Model |
| 17 |
{ |
| 18 |
use CanSearch; |
| 19 |
|
| 20 |
protected $table = 'fct_shipping_methods'; |
| 21 |
|
| 22 |
protected $appends = ['formatted_states']; |
| 23 |
|
| 24 |
/** |
| 25 |
* The attributes that are mass assignable. |
| 26 |
* |
| 27 |
* @var array |
| 28 |
*/ |
| 29 |
protected $fillable = [ |
| 30 |
'zone_id', |
| 31 |
'title', |
| 32 |
'type', |
| 33 |
'settings', |
| 34 |
'amount', |
| 35 |
'is_enabled', |
| 36 |
'order', |
| 37 |
'states', |
| 38 |
'meta' |
| 39 |
]; |
| 40 |
|
| 41 |
protected $attributes = [ |
| 42 |
'states' => '[]', |
| 43 |
]; |
| 44 |
|
| 45 |
/** |
| 46 |
* The attributes that should be cast. |
| 47 |
* |
| 48 |
* @var array |
| 49 |
*/ |
| 50 |
protected $casts = [ |
| 51 |
'settings' => 'array', |
| 52 |
'states' => 'array', |
| 53 |
'is_enabled' => 'boolean' |
| 54 |
]; |
| 55 |
|
| 56 |
/** |
| 57 |
* Get the zone that this method belongs to. |
| 58 |
*/ |
| 59 |
public function zone(): BelongsTo |
| 60 |
{ |
| 61 |
return $this->belongsTo(ShippingZone::class, 'zone_id', 'id'); |
| 62 |
} |
| 63 |
|
| 64 |
public function scopeApplicableToCountry($query, $country, $state) |
| 65 |
{ |
| 66 |
|
| 67 |
$query = $query->whereHas('zone', function ($query) use ($country) { |
| 68 |
$query->where(function ($q) use ($country) { |
| 69 |
$q->whereIn('region', [$country, 'all']) |
| 70 |
->orWhere('region', 'selection'); |
| 71 |
}); |
| 72 |
$query->where(function ($q) { |
| 73 |
$q->whereNull('shipping_class_id') |
| 74 |
->orWhere('shipping_class_id', 0); |
| 75 |
}); |
| 76 |
}); |
| 77 |
|
| 78 |
// SQLite-compatible substring extraction |
| 79 |
$isSqlite = defined('DB_ENGINE') && DB_ENGINE === 'sqlite'; |
| 80 |
// SQLite: Use simple string search instead of JSON functions |
| 81 |
if ($isSqlite) { |
| 82 |
$query = $query->where(function ($q) use ($state) { |
| 83 |
// Handle empty states |
| 84 |
$q->where('states', '[]') |
| 85 |
->orWhereNull('states'); // fallback if states column is NULL |
| 86 |
|
| 87 |
if ($state) { |
| 88 |
// Check if the state exists in the array (simple string search) |
| 89 |
// Escape LIKE wildcards to prevent wildcard injection on public endpoint |
| 90 |
$escapedState = str_replace(['%', '_'], ['\\%', '\\_'], $state); |
| 91 |
$q->orWhere('states', 'LIKE', '%"' . $escapedState . '"%'); |
| 92 |
} |
| 93 |
}); |
| 94 |
} else { |
| 95 |
// MySQL: Use JSON functions for filtering |
| 96 |
$query = $query->where(function ($q) use ($state) { |
| 97 |
$q->whereJsonLength('states', 0); |
| 98 |
|
| 99 |
if ($state) { |
| 100 |
// Shipping methods containing the state |
| 101 |
$q->orWhereJsonContains('states', $state); |
| 102 |
} |
| 103 |
}); |
| 104 |
} |
| 105 |
$query->orderBy('amount', 'DESC'); |
| 106 |
|
| 107 |
return $query->where('is_enabled', 1); |
| 108 |
} |
| 109 |
|
| 110 |
/** |
| 111 |
* Get shipping methods applicable to a country, with post-filtering for multi-country selection zones. |
| 112 |
* |
| 113 |
* @param string $country |
| 114 |
* @param string|null $state |
| 115 |
* @return \FluentCart\Framework\Database\Orm\Collection |
| 116 |
*/ |
| 117 |
public static function getApplicableForCountry(string $country, $state = null) |
| 118 |
{ |
| 119 |
$methods = static::applicableToCountry($country, $state) |
| 120 |
->with('zone') |
| 121 |
->get(); |
| 122 |
|
| 123 |
// Post-filter 'selection' zones that don't actually match this country |
| 124 |
return $methods->filter(function ($method) use ($country) { |
| 125 |
if (!$method->zone || $method->zone->region !== 'selection') { |
| 126 |
return true; |
| 127 |
} |
| 128 |
return $method->zone->appliesToCountry($country); |
| 129 |
}); |
| 130 |
} |
| 131 |
|
| 132 |
public function getFormattedStatesAttribute() |
| 133 |
{ |
| 134 |
|
| 135 |
if (is_array($this->states)) { |
| 136 |
$states = array_map(function ($region) { |
| 137 |
return AddressHelper::getStateNameByCode($region, $this->zone->region); |
| 138 |
}, $this->states); |
| 139 |
return $states; |
| 140 |
} |
| 141 |
return []; |
| 142 |
} |
| 143 |
|
| 144 |
public function setMetaAttribute($value) |
| 145 |
{ |
| 146 |
|
| 147 |
if ($value) { |
| 148 |
$decoded = \json_encode($value); |
| 149 |
if (!($decoded)) { |
| 150 |
$decoded = '[]'; |
| 151 |
} |
| 152 |
} else { |
| 153 |
$decoded = '[]'; |
| 154 |
} |
| 155 |
|
| 156 |
$this->attributes['meta'] = $decoded; |
| 157 |
} |
| 158 |
|
| 159 |
public function getMetaAttribute($value) |
| 160 |
{ |
| 161 |
if (!$value) { |
| 162 |
return []; |
| 163 |
} |
| 164 |
|
| 165 |
return \json_decode($value, true); |
| 166 |
} |
| 167 |
} |
| 168 |
|