feat: Implement inventory policy for product variants

- Introduced InventoryPolicy enum to manage tracked and unlimited inventory types.
- Updated ProductVariant model to include inventory_policy and cantidad_vendida attributes.
- Modified addItem and updateItem methods in Cart to check available quantity based on inventory policy.
- Added migration to include inventory_policy and cantidad_vendida in productos_variantes table.
- Enhanced tests to validate inventory behavior for both tracked and unlimited policies.
- Updated various request and resource classes to handle new inventory fields.
This commit is contained in:
2026-07-16 08:36:59 -03:00
parent 716d8e447c
commit 10157d7c63
16 changed files with 547 additions and 73 deletions

View File

@@ -3,17 +3,18 @@
namespace App\Domains\Catalog\Models;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Catalog\Enums\InventoryPolicy;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Casts\Attribute;
#[Fillable([
'producto_id',
'inventory_policy',
'stock_real',
'stock_reservado',
'stock',
@@ -30,9 +31,20 @@ class ProductVariant extends Model
protected $appends = ['stock_tecnico'];
protected $attributes = [
'inventory_policy' => 'tracked',
'stock_real' => 0,
'stock_reservado' => 0,
'cantidad_vendida' => 0,
];
protected static function booted(): void
{
static::saving(function (ProductVariant $variant) {
if ($variant->exists && $variant->isDirty('inventory_policy')) {
throw new \InvalidArgumentException('La politica de inventario no puede modificarse.');
}
$variant->validateStock();
});
}
@@ -47,34 +59,44 @@ class ProductVariant extends Model
throw new \InvalidArgumentException('El stock reservado no puede ser negativo.');
}
if ($this->stock_reservado > $this->stock_real) {
if ($this->cantidad_vendida < 0) {
throw new \InvalidArgumentException('La cantidad vendida no puede ser negativa.');
}
if ($this->tracksInventory() && $this->stock_reservado > $this->stock_real) {
throw new \InvalidArgumentException('El stock reservado no puede ser mayor que el stock real.');
}
}
public function incrementRealStock(int $amount): void
public function tracksInventory(): bool
{
return $this->inventory_policy === InventoryPolicy::Tracked;
}
public function availableQuantity(): ?int
{
if (! $this->tracksInventory()) {
return null;
}
return $this->stock_real - $this->stock_reservado;
}
public function isAvailableForSale(): bool
{
return ! $this->tracksInventory() || $this->availableQuantity() > 0;
}
public function reserveStock(int $amount): void
{
if ($amount < 0) {
throw new \InvalidArgumentException('El monto a incrementar debe ser positivo.');
}
$this->stock_real += $amount;
$this->save();
}
public function decrementRealStock(int $amount): void
{
if ($amount < 0) {
throw new \InvalidArgumentException('El monto a decrementar debe ser positivo.');
if ($this->tracksInventory() && $this->availableQuantity() < $amount) {
throw new \InvalidArgumentException('No hay suficiente stock disponible para reservar.');
}
$this->stock_real -= $amount;
$this->save();
}
public function incrementReservedStock(int $amount): void
{
if ($amount < 0) {
throw new \InvalidArgumentException('El monto a incrementar debe ser positivo.');
}
$this->stock_reservado += $amount;
$this->save();
}
@@ -88,13 +110,13 @@ class ProductVariant extends Model
$this->save();
}
public function confirmReservedStock(int $amount): void
public function buy(int $amount): void
{
if ($amount < 0) {
throw new \InvalidArgumentException('El monto a confirmar debe ser positivo.');
}
if ($this->stock_real < $amount) {
if ($this->tracksInventory() && $this->stock_real < $amount) {
throw new \InvalidArgumentException('No hay suficiente stock real para confirmar la reserva.');
}
@@ -102,14 +124,18 @@ class ProductVariant extends Model
throw new \InvalidArgumentException('No hay suficiente stock reservado para confirmar la reserva.');
}
$this->stock_real -= $amount;
if ($this->tracksInventory()) {
$this->stock_real -= $amount;
}
$this->stock_reservado -= $amount;
$this->cantidad_vendida += $amount;
$this->save();
}
protected function stockTecnico(): Attribute
{
return Attribute::get(fn () => $this->stock_real - $this->stock_reservado);
return Attribute::get(fn (): ?int => $this->availableQuantity());
}
protected function stock(): Attribute
@@ -126,8 +152,10 @@ class ProductVariant extends Model
{
return [
'producto_id' => 'integer',
'inventory_policy' => InventoryPolicy::class,
'stock_real' => 'integer',
'stock_reservado' => 'integer',
'cantidad_vendida' => 'integer',
'is_placeholder' => 'boolean',
'has_tickets' => 'boolean',
'minimum_use_date' => 'datetime',