'integer', ]; } /** * @return BelongsTo */ public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo'); } /** * @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * @return HasMany */ 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('variant.product')->get(); return (float) $items->reduce( fn (float $carry, $item): float => $carry + ((float) ($item->variant?->product?->precio ?? 0) * $item->cantidad), 0.0, ); } public function addItem(int $productVariantId, int $quantity): CartItem { if ($quantity <= 0) { throw ValidationException::withMessages([ 'cantidad' => 'La cantidad debe ser mayor a cero.', ]); } return DB::transaction(function () use ($productVariantId, $quantity): CartItem { $variant = $this->resolveScopedVariant($productVariantId, true); if ($variant->stock_tecnico < $quantity) { throw ValidationException::withMessages([ 'cantidad' => "Stock insuficiente para la variante solicitada. Maximo disponible: {$variant->stock_tecnico}.", ]); } /** @var CartItem|null $item */ $item = $this->items() ->where('producto_variante_id', $variant->getKey()) ->lockForUpdate() ->first(); if ($item === null) { $item = $this->items()->create([ 'producto_variante_id' => $variant->getKey(), 'cantidad' => $quantity, ]); } else { $item->cantidad += $quantity; $item->save(); } $variant->incrementReservedStock($quantity); return $item->fresh(); }); } public function updateItem(int $productVariantId, int $quantity): CartItem { if ($quantity <= 0) { throw ValidationException::withMessages([ 'cantidad' => 'La cantidad debe ser mayor a cero.', ]); } return DB::transaction(function () use ($productVariantId, $quantity): CartItem { /** @var CartItem $item */ $item = $this->items() ->where('producto_variante_id', $productVariantId) ->lockForUpdate() ->firstOrFail(); $variant = $this->resolveScopedVariant($productVariantId, true); $delta = $quantity - $item->cantidad; if ($delta > 0 && $variant->stock_tecnico < $delta) { $maxAvailable = $variant->stock_tecnico + $item->cantidad; throw ValidationException::withMessages([ 'cantidad' => "El máximo que se puede agregar es {$maxAvailable}.", ]); } $item->cantidad = $quantity; $item->save(); if ($delta > 0) { $variant->incrementReservedStock($delta); } if ($delta < 0) { $variant->decrementReservedStock(abs($delta)); } return $item->fresh(); }); } public function removeItem(int $productVariantId): void { DB::transaction(function () use ($productVariantId): void { /** @var CartItem $item */ $item = $this->items() ->where('producto_variante_id', $productVariantId) ->lockForUpdate() ->firstOrFail(); $variant = $this->resolveScopedVariant($productVariantId, true); $variant->decrementReservedStock($item->cantidad); $item->delete(); }); } protected function resolveScopedVariant(int $productVariantId, bool $lockForUpdate = false): ProductVariant { $query = ProductVariant::query() ->whereKey($productVariantId) ->whereHas('product', fn ($query) => $query->where('tenant_codigo', $this->tenant_codigo)); if ($lockForUpdate) { $query->lockForUpdate(); } /** @var ProductVariant|null $variant */ $variant = $query->first(); if ($variant === null) { throw new NotFoundHttpException('Product variant not found for tenant.'); } return $variant; } }