2025-11-21 10:49:41 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Blax\Shop\Models;
|
|
|
|
|
|
|
|
|
|
use Blax\Shop\Models\Product;
|
2025-11-23 14:07:12 +00:00
|
|
|
use Blax\Workkit\Traits\HasExpiration;
|
2025-11-21 10:49:41 +00:00
|
|
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
class ProductStock extends Model
|
|
|
|
|
{
|
2025-11-23 14:07:12 +00:00
|
|
|
use HasUuids, HasExpiration;
|
2025-11-21 10:49:41 +00:00
|
|
|
|
|
|
|
|
protected $fillable = [
|
|
|
|
|
'product_id',
|
|
|
|
|
'quantity',
|
|
|
|
|
'type',
|
|
|
|
|
'status',
|
|
|
|
|
'reference_type',
|
|
|
|
|
'reference_id',
|
|
|
|
|
'expires_at',
|
|
|
|
|
'note',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
protected $casts = [
|
|
|
|
|
'quantity' => 'integer',
|
|
|
|
|
'expires_at' => 'datetime',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public function __construct(array $attributes = [])
|
|
|
|
|
{
|
|
|
|
|
parent::__construct($attributes);
|
|
|
|
|
$this->setTable(config('shop.tables.product_stocks', 'product_stocks'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static function booted()
|
|
|
|
|
{
|
|
|
|
|
static::created(function ($model) {
|
|
|
|
|
$model->logStockChange();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
static::updated(function ($model) {
|
|
|
|
|
if ($model->wasChanged('status') && $model->status === 'completed') {
|
|
|
|
|
$model->releaseStock();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function product(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(config('shop.models.product', Product::class));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function reference(): MorphTo
|
|
|
|
|
{
|
|
|
|
|
return $this->morphTo();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scopePending($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('status', 'pending');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scopeReleased($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('status', 'completed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scopeTemporary($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->whereNotNull('expires_at');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scopePermanent($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->whereNull('expires_at');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Backward compatibility accessors
|
|
|
|
|
public function getReleasedAtAttribute()
|
|
|
|
|
{
|
|
|
|
|
return $this->status === 'completed' ? $this->updated_at : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getUntilAtAttribute()
|
|
|
|
|
{
|
|
|
|
|
return $this->expires_at;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function reserve(
|
|
|
|
|
Product $product,
|
|
|
|
|
int $quantity,
|
|
|
|
|
$reference = null,
|
|
|
|
|
?\DateTimeInterface $until = null,
|
|
|
|
|
?string $note = null
|
|
|
|
|
): ?self {
|
2025-11-25 11:33:42 +00:00
|
|
|
return DB::transaction(function () use ($product, $quantity, $reference, $until, $note) {
|
2025-11-21 10:49:41 +00:00
|
|
|
if (!$product->decreaseStock($quantity)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self::create([
|
|
|
|
|
'product_id' => $product->id,
|
|
|
|
|
'quantity' => $quantity,
|
2025-11-25 11:33:42 +00:00
|
|
|
'type' => 'reservation',
|
2025-11-21 10:49:41 +00:00
|
|
|
'status' => 'pending',
|
|
|
|
|
'reference_type' => $reference ? get_class($reference) : null,
|
|
|
|
|
'reference_id' => $reference?->id,
|
|
|
|
|
'expires_at' => $until,
|
|
|
|
|
'note' => $note,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function release(): bool
|
|
|
|
|
{
|
|
|
|
|
if ($this->status !== 'pending') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () {
|
|
|
|
|
$this->status = 'completed';
|
|
|
|
|
$this->save();
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isPermanent(): bool
|
|
|
|
|
{
|
|
|
|
|
return is_null($this->expires_at);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isTemporary(): bool
|
|
|
|
|
{
|
|
|
|
|
return !is_null($this->expires_at);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isExpired(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->isTemporary()
|
|
|
|
|
&& $this->status === 'pending'
|
|
|
|
|
&& $this->expires_at->isPast();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isActive(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->status === 'pending';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function logStockChange(): void
|
|
|
|
|
{
|
|
|
|
|
if (!config('shop.stock.log_changes', true)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::table('product_stock_logs')->insert([
|
|
|
|
|
'product_id' => $this->product_id,
|
|
|
|
|
'quantity_change' => -$this->quantity,
|
|
|
|
|
'quantity_after' => $this->product->stock_quantity,
|
|
|
|
|
'type' => $this->type,
|
|
|
|
|
'note' => $this->note,
|
|
|
|
|
'reference_type' => $this->reference_type,
|
|
|
|
|
'reference_id' => $this->reference_id,
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function releaseStock(): void
|
|
|
|
|
{
|
|
|
|
|
if (!config('shop.stock.log_changes', true)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::table('product_stock_logs')->insert([
|
|
|
|
|
'product_id' => $this->product_id,
|
|
|
|
|
'quantity_change' => $this->quantity,
|
|
|
|
|
'quantity_after' => $this->product->stock_quantity,
|
|
|
|
|
'type' => 'release',
|
|
|
|
|
'note' => 'Stock released from reservation',
|
|
|
|
|
'reference_type' => $this->reference_type,
|
|
|
|
|
'reference_id' => $this->reference_id,
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function releaseExpired(): int
|
|
|
|
|
{
|
|
|
|
|
$expired = self::expired()->get();
|
|
|
|
|
$count = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($expired as $stock) {
|
|
|
|
|
if ($stock->release()) {
|
|
|
|
|
$count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $count;
|
|
|
|
|
}
|
2025-11-23 14:07:12 +00:00
|
|
|
|
|
|
|
|
public static function scopeAvailable($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('status', 'completed');
|
|
|
|
|
}
|
2025-11-25 11:33:42 +00:00
|
|
|
|
|
|
|
|
public static function scopeAvailableReservations($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('type', 'reservation')->where('status', 'pending');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function reservations()
|
|
|
|
|
{
|
|
|
|
|
return self::availableReservations();
|
|
|
|
|
}
|
2025-11-21 10:49:41 +00:00
|
|
|
}
|