'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(['catalogItem', 'variant'])->get(); return (float) $items->reduce( fn (float $carry, CartItem $item): float => $carry + (($item->selectedItem()?->getPrice() ?? 0) * $item->cantidad), 0.0, ); } public function addItem(int $catalogItemId, ?int $variantId, int $quantity): CartItem { if ($quantity <= 0) { throw ValidationException::withMessages([ 'cantidad' => 'La cantidad debe ser mayor a cero.', ]); } return DB::transaction(function () use ($catalogItemId, $variantId, $quantity): CartItem { self::query()->whereKey($this->getKey())->lockForUpdate()->firstOrFail(); $selectedItem = $this->resolveScopedItem($catalogItemId, $variantId, true); $inventoryService = app(CatalogInventoryService::class); $availableQuantity = $inventoryService->availableQuantity($selectedItem); 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('catalog_item_id', $catalogItemId) ->where('variant_id', $variantId) ->lockForUpdate() ->first(); if ($item === null) { $item = $this->items()->create([ 'catalog_item_id' => $catalogItemId, 'variant_id' => $variantId, 'cantidad' => $quantity, ]); } else { $item->cantidad += $quantity; $item->save(); } $inventoryService->reserve($selectedItem, $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(); $selectedItem = $this->resolveScopedItem( $item->catalog_item_id, $item->variant_id, true, ); $inventoryService = app(CatalogInventoryService::class); $delta = $quantity - $item->cantidad; $availableQuantity = $inventoryService->availableQuantity($selectedItem); 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) { $inventoryService->reserve($selectedItem, $delta); } if ($delta < 0) { $inventoryService->release($selectedItem, 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(); $selectedItem = $this->resolveScopedItem( $item->catalog_item_id, $item->variant_id, true, ); app(CatalogInventoryService::class)->release( $selectedItem, $item->cantidad, ); $item->delete(); }); } protected function resolveScopedItem( int $catalogItemId, ?int $variantId, bool $lockForUpdate = false, ): CatalogItem|Variant { $catalogItemQuery = CatalogItem::query() ->whereKey($catalogItemId) ->where('tenant_code', $this->tenant_codigo); if ($lockForUpdate) { $catalogItemQuery->lockForUpdate(); } $catalogItem = $catalogItemQuery->first(); if ($catalogItem === null) { throw new NotFoundHttpException('Catalog item not found for tenant.'); } if ($catalogItem->isBundle()) { if ($variantId !== null) { throw ValidationException::withMessages([ 'variant_id' => 'Un bundle no admite una variante.', ]); } if (! $catalogItem->bundleComponents()->exists()) { throw ValidationException::withMessages([ 'catalog_item_id' => 'El bundle no tiene componentes.', ]); } return $catalogItem; } if ($variantId === null) { if ($catalogItem->inventory_id === null) { throw ValidationException::withMessages([ 'variant_id' => 'Debe seleccionar una variante para este ítem.', ]); } $inventory = $this->resolveInventory($catalogItem->inventory_id, $lockForUpdate); $catalogItem->setRelation('inventory', $inventory); return $catalogItem; } $variantQuery = Variant::query() ->whereKey($variantId) ->where('catalog_item_id', $catalogItem->id); if ($lockForUpdate) { $variantQuery->lockForUpdate(); } $variant = $variantQuery->first(); if ($variant === null) { throw new NotFoundHttpException('Variant not found for catalog item.'); } $inventory = $this->resolveInventory($variant->inventory_id, $lockForUpdate); $variant->setRelation('catalogItem', $catalogItem); $variant->setRelation('inventory', $inventory); return $variant; } protected function resolveInventory(int $inventoryId, bool $lockForUpdate): Inventory { $query = Inventory::query()->whereKey($inventoryId); if ($lockForUpdate) { $query->lockForUpdate(); } return $query->firstOrFail(); } }