shopit-back/app/Domains/Cart/Models/Cart.php

211 lines
6.5 KiB
PHP

<?php
namespace App\Domains\Cart\Models;
use App\Domains\Auth\Models\User;
use App\Domains\Bundle\Models\Bundle;
use App\Domains\Catalog\Models\ProductVariant;
use App\Domains\Shared\Contracts\Buyable;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
#[Fillable([
'tenant_codigo',
'user_id',
'guest_token',
'status',
])]
class Cart extends Model
{
use HasFactory;
use SoftDeletes;
protected $table = 'carritos';
protected function casts(): array
{
return [
'user_id' => 'integer',
];
}
/**
* @return BelongsTo<Tenant, $this>
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
}
/**
* @return BelongsTo<User, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* @return HasMany<CartItem, $this>
*/
public function items(): HasMany
{
return $this->hasMany(CartItem::class, 'cart_id');
}
public function getTotalAmount(): float
{
$items = $this->relationLoaded('items')
? $this->getRelation('items')
: $this->items()->with('buyable')->get();
return (float) $items->reduce(
fn (float $carry, $item): float => $carry + ($item->buyable?->getPrice() * $item->cantidad),
0.0,
);
}
public function addItem(string $buyableType, int $buyableId, int $quantity): CartItem
{
if ($quantity <= 0) {
throw ValidationException::withMessages([
'cantidad' => 'La cantidad debe ser mayor a cero.',
]);
}
return DB::transaction(function () use ($buyableType, $buyableId, $quantity): CartItem {
$buyable = $this->resolveScopedBuyable($buyableType, $buyableId, true);
$canonicalType = $buyable::class;
$availableQuantity = $buyable->availableQuantity();
if ($availableQuantity !== null && $availableQuantity < $quantity) {
throw ValidationException::withMessages([
'cantidad' => "Stock insuficiente para el producto solicitado. Maximo disponible: {$availableQuantity}.",
]);
}
/** @var CartItem|null $item */
$item = $this->items()
->where('buyable_type', $canonicalType)
->where('buyable_id', $buyable->getKey())
->lockForUpdate()
->first();
if ($item === null) {
$item = $this->items()->create([
'buyable_type' => $canonicalType,
'buyable_id' => $buyable->getKey(),
'cantidad' => $quantity,
]);
} else {
$item->cantidad += $quantity;
$item->save();
}
$buyable->reserveStock($quantity);
return $item->fresh();
});
}
public function updateItem(int $cartItemId, int $quantity): CartItem
{
if ($quantity <= 0) {
throw ValidationException::withMessages([
'cantidad' => 'La cantidad debe ser mayor a cero.',
]);
}
return DB::transaction(function () use ($cartItemId, $quantity): CartItem {
/** @var CartItem $item */
$item = $this->items()
->where('id', $cartItemId)
->lockForUpdate()
->firstOrFail();
$buyable = $this->resolveScopedBuyable($item->buyable_type, $item->buyable_id, true);
$delta = $quantity - $item->cantidad;
$availableQuantity = $buyable->availableQuantity();
if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
$maxAvailable = $availableQuantity + $item->cantidad;
throw ValidationException::withMessages([
'cantidad' => "El máximo que se puede agregar es {$maxAvailable}.",
]);
}
$item->cantidad = $quantity;
$item->save();
if ($delta > 0) {
$buyable->reserveStock($delta);
}
if ($delta < 0) {
$buyable->decrementReservedStock(abs($delta));
}
return $item->fresh();
});
}
public function removeItem(int $cartItemId): void
{
DB::transaction(function () use ($cartItemId): void {
/** @var CartItem $item */
$item = $this->items()
->where('id', $cartItemId)
->lockForUpdate()
->firstOrFail();
$buyable = $this->resolveScopedBuyable($item->buyable_type, $item->buyable_id, true);
$buyable->decrementReservedStock($item->cantidad);
$item->delete();
});
}
protected function resolveScopedBuyable(string $buyableType, int $buyableId, bool $lockForUpdate = false): Buyable
{
$buyableClass = $this->resolveBuyableClass($buyableType);
if ($buyableClass === ProductVariant::class) {
$query = ProductVariant::query()
->whereKey($buyableId)
->whereHas('product', fn ($query) => $query->where('tenant_codigo', $this->tenant_codigo));
} else {
$query = Bundle::query()
->whereKey($buyableId)
->where('tenant_codigo', $this->tenant_codigo);
}
if ($lockForUpdate) {
$query->lockForUpdate();
}
$buyable = $query->first();
if ($buyable === null) {
throw new NotFoundHttpException('Buyable not found for tenant.');
}
return $buyable;
}
protected function resolveBuyableClass(string $buyableType): string
{
return match ($buyableType) {
'variant', ProductVariant::class => ProductVariant::class,
'bundle', Bundle::class => Bundle::class,
default => throw new \InvalidArgumentException('Invalid buyable type'),
};
}
}