php-commerce/Models/Inventory.php
Snider a774f4e285 refactor: migrate namespace from Core\Commerce to Core\Mod\Commerce
Align commerce module with the monorepo module structure by updating
all namespaces to use the Core\Mod\Commerce convention. This change
supports the recent monorepo separation and ensures consistency with
other modules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:23:12 +00:00

216 lines
5 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Commerce Inventory - Stock level at a warehouse.
*
* @property int $id
* @property int $product_id
* @property int $warehouse_id
* @property int $quantity
* @property int $reserved_quantity
* @property int $incoming_quantity
* @property int|null $low_stock_threshold
* @property string|null $bin_location
* @property string|null $zone
* @property \Carbon\Carbon|null $last_counted_at
* @property \Carbon\Carbon|null $last_restocked_at
* @property int|null $unit_cost
* @property array|null $metadata
*/
class Inventory extends Model
{
protected $table = 'commerce_inventory';
protected $fillable = [
'product_id',
'warehouse_id',
'quantity',
'reserved_quantity',
'incoming_quantity',
'low_stock_threshold',
'bin_location',
'zone',
'last_counted_at',
'last_restocked_at',
'unit_cost',
'metadata',
];
protected $casts = [
'quantity' => 'integer',
'reserved_quantity' => 'integer',
'incoming_quantity' => 'integer',
'low_stock_threshold' => 'integer',
'unit_cost' => 'integer',
'last_counted_at' => 'datetime',
'last_restocked_at' => 'datetime',
'metadata' => 'array',
];
// Relationships
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
public function warehouse(): BelongsTo
{
return $this->belongsTo(Warehouse::class);
}
public function movements(): HasMany
{
return $this->hasMany(InventoryMovement::class, 'inventory_id');
}
// Quantity helpers
/**
* Get available quantity (not reserved).
*/
public function getAvailableQuantity(): int
{
return max(0, $this->quantity - $this->reserved_quantity);
}
/**
* Get total expected quantity (including incoming).
*/
public function getTotalExpectedQuantity(): int
{
return $this->quantity + $this->incoming_quantity;
}
/**
* Check if low on stock.
*/
public function isLowStock(): bool
{
$threshold = $this->low_stock_threshold
?? $this->product?->low_stock_threshold
?? 5;
return $this->getAvailableQuantity() <= $threshold;
}
/**
* Check if out of stock.
*/
public function isOutOfStock(): bool
{
return $this->getAvailableQuantity() <= 0;
}
// Stock operations
/**
* Reserve stock for an order.
*/
public function reserve(int $quantity): bool
{
if ($this->getAvailableQuantity() < $quantity) {
return false;
}
$this->increment('reserved_quantity', $quantity);
return true;
}
/**
* Release reserved stock.
*/
public function release(int $quantity): void
{
$this->decrement('reserved_quantity', min($quantity, $this->reserved_quantity));
}
/**
* Fulfill reserved stock (convert to sale).
*/
public function fulfill(int $quantity): bool
{
if ($this->reserved_quantity < $quantity) {
return false;
}
$this->decrement('quantity', $quantity);
$this->decrement('reserved_quantity', $quantity);
return true;
}
/**
* Add stock.
*/
public function addStock(int $quantity): void
{
$this->increment('quantity', $quantity);
$this->last_restocked_at = now();
$this->save();
}
/**
* Remove stock.
*/
public function removeStock(int $quantity): bool
{
if ($this->getAvailableQuantity() < $quantity) {
return false;
}
$this->decrement('quantity', $quantity);
return true;
}
/**
* Set stock count (for physical count).
*/
public function setCount(int $quantity): int
{
$difference = $quantity - $this->quantity;
$this->quantity = $quantity;
$this->last_counted_at = now();
$this->save();
return $difference;
}
// Scopes
public function scopeLowStock($query)
{
// Uses a subquery to compare against threshold
return $query->whereRaw('(quantity - reserved_quantity) <= COALESCE(low_stock_threshold, 5)');
}
public function scopeOutOfStock($query)
{
return $query->whereRaw('(quantity - reserved_quantity) <= 0');
}
public function scopeInStock($query)
{
return $query->whereRaw('(quantity - reserved_quantity) > 0');
}
public function scopeForWarehouse($query, int $warehouseId)
{
return $query->where('warehouse_id', $warehouseId);
}
public function scopeForProduct($query, int $productId)
{
return $query->where('product_id', $productId);
}
}