* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ namespace PrestaShop\PrestaShop\Adapter\Cart\CommandHandler; use Cart; use Context; use Customer; use Pack; use PrestaShop\PrestaShop\Adapter\Cart\AbstractCartHandler; use PrestaShop\PrestaShop\Adapter\ContextStateManager; use PrestaShop\PrestaShop\Core\Domain\Cart\Command\UpdateProductQuantityInCartCommand; use PrestaShop\PrestaShop\Core\Domain\Cart\CommandHandler\UpdateProductQuantityInCartHandlerInterface; use PrestaShop\PrestaShop\Core\Domain\Cart\Exception\CartConstraintException; use PrestaShop\PrestaShop\Core\Domain\Cart\Exception\CartException; use PrestaShop\PrestaShop\Core\Domain\Cart\Exception\MinimalQuantityException; use PrestaShop\PrestaShop\Core\Domain\Product\Exception\PackOutOfStockException; use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductCustomizationNotFoundException; use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductException; use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException; use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductOutOfStockException; use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId; use Product; use ProductAttribute; use Shop; /** * @internal */ final class UpdateProductQuantityInCartHandler extends AbstractCartHandler implements UpdateProductQuantityInCartHandlerInterface { /** * @var ContextStateManager */ private $contextStateManager; /** * @param ContextStateManager $contextStateManager */ public function __construct(ContextStateManager $contextStateManager) { $this->contextStateManager = $contextStateManager; } /** * {@inheritdoc} */ public function handle(UpdateProductQuantityInCartCommand $command) { $cart = $this->getCart($command->getCartId()); $this->contextStateManager ->setCart($cart) ->setShop(new Shop($cart->id_shop)) ; try { $this->updateProductQuantityInCart($cart, $command); } finally { $this->contextStateManager->restorePreviousContext(); } } /** * @param Cart $cart * @param UpdateProductQuantityInCartCommand $command * * @throws CartConstraintException * @throws CartException * @throws ProductException * @throws ProductNotFoundException * @throws ProductOutOfStockException */ private function updateProductQuantityInCart(Cart $cart, UpdateProductQuantityInCartCommand $command): void { $previousQty = $this->findPreviousQuantityInCart($cart, $command); $qtyDiff = abs($command->getNewQuantity() - $previousQty); if ($qtyDiff === 0) { throw new CartConstraintException(sprintf('Cart quantity is already %d', $command->getNewQuantity()), CartConstraintException::UNCHANGED_QUANTITY); } // $cart::updateQty needs customer context $customer = new Customer($cart->id_customer); Context::getContext()->customer = $customer; $this->assertOrderDoesNotExistForCart($cart); $product = $this->getProductObject($command->getProductId()); $combinationIdValue = $command->getCombinationId() ? $command->getCombinationId()->getValue() : 0; $customizationId = $command->getCustomizationId(); $this->assertProductIsInStock($product, $command); $this->assertProductCustomization($product, $command); if ($previousQty < $command->getNewQuantity()) { $action = 'up'; } else { $action = 'down'; } $updateResult = $cart->updateQty( $qtyDiff, $command->getProductId()->getValue(), $combinationIdValue, $customizationId ? $customizationId->getValue() : false, $action ); if (!$updateResult) { throw new CartException('Failed to update product quantity in cart'); } // It seems that $updateResult can be -1, // when adding product with less quantity than minimum required. if ($updateResult < 0) { $minQuantity = $combinationIdValue ? ProductAttribute::getAttributeMinimalQty($combinationIdValue) : $product->minimal_quantity; throw new MinimalQuantityException('Minimum quantity of %d must be added to cart.', $minQuantity); } } /** * @param Cart $cart * * @throws CartException */ private function assertOrderDoesNotExistForCart(Cart $cart) { if ($cart->orderExists()) { throw new CartException(sprintf('Order for cart with id "%s" already exists.', $cart->id)); } } /** * @param ProductId $productId * * @return Product * * @throws ProductNotFoundException */ private function getProductObject(ProductId $productId) { $product = new Product($productId->getValue(), true); if ($product->id !== $productId->getValue()) { throw new ProductNotFoundException(sprintf('Product with id "%s" was not found', $productId->getValue())); } return $product; } /** * @param Product $product * @param UpdateProductQuantityInCartCommand $command * * @throws ProductOutOfStockException * @throws PackOutOfStockException */ private function assertProductIsInStock(Product $product, UpdateProductQuantityInCartCommand $command): void { $isAvailableWhenOutOfStock = Product::isAvailableWhenOutOfStock($product->out_of_stock); if (null !== $command->getCombinationId()) { $isEnoughQuantity = ProductAttribute::checkAttributeQty( $command->getCombinationId()->getValue(), $command->getNewQuantity() ); if (!$isAvailableWhenOutOfStock && !$isEnoughQuantity) { throw new ProductOutOfStockException( sprintf('Product with id "%s" is out of stock, thus cannot be added to cart', $product->id) ); } return; } elseif (Pack::isPack($product->id)) { $hasPackEnoughQuantity = Pack::isInStock($product->id, $command->getNewQuantity()); if (!$isAvailableWhenOutOfStock && !$hasPackEnoughQuantity) { throw new PackOutOfStockException( sprintf('Product with id "%s" is out of stock, thus cannot be added to cart', $product->id) ); } return; } if (!$product->checkQty($command->getNewQuantity())) { throw new ProductOutOfStockException(sprintf('Product with id "%s" is out of stock, thus cannot be added to cart', $product->id)); } } /** * If product is customizable and customization is not provided, * then exception is thrown. * * @param Product $product * @param UpdateProductQuantityInCartCommand $command * * @throws ProductCustomizationNotFoundException */ private function assertProductCustomization(Product $product, UpdateProductQuantityInCartCommand $command) { if (null === $command->getCustomizationId() && !$product->hasAllRequiredCustomizableFields()) { throw new ProductCustomizationNotFoundException(sprintf( 'Missing customization for product with id "%s"', $product->id )); } } /** * @param Cart $cart * @param UpdateProductQuantityInCartCommand $command * * @return int */ private function findPreviousQuantityInCart(Cart $cart, UpdateProductQuantityInCartCommand $command): int { $isCombination = ($command->getCombinationId() !== null); $isCustomization = ($command->getCustomizationId() !== null); foreach ($cart->getProducts() as $cartProduct) { $equalProductId = (int) $cartProduct['id_product'] === $command->getProductId()->getValue(); if ($isCombination) { if ($equalProductId && (int) $cartProduct['id_product_attribute'] === $command->getCombinationId()->getValue()) { return (int) $cartProduct['quantity']; } } elseif ($isCustomization) { if ($equalProductId && (int) $cartProduct['id_customization'] === $command->getCustomizationId()->getValue()) { return (int) $cartProduct['quantity']; } } elseif ($equalProductId) { return (int) $cartProduct['quantity']; } } return 0; } }