1<?php 2/** 3 * Copyright since 2007 PrestaShop SA and Contributors 4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA 5 * 6 * NOTICE OF LICENSE 7 * 8 * This source file is subject to the Open Software License (OSL 3.0) 9 * that is bundled with this package in the file LICENSE.md. 10 * It is also available through the world-wide-web at this URL: 11 * https://opensource.org/licenses/OSL-3.0 12 * If you did not receive a copy of the license and are unable to 13 * obtain it through the world-wide-web, please send an email 14 * to license@prestashop.com so we can send you a copy immediately. 15 * 16 * DISCLAIMER 17 * 18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer 19 * versions in the future. If you wish to customize PrestaShop for your 20 * needs please refer to https://devdocs.prestashop.com/ for more information. 21 * 22 * @author PrestaShop SA and Contributors <contact@prestashop.com> 23 * @copyright Since 2007 PrestaShop SA and Contributors 24 * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) 25 */ 26 27namespace PrestaShop\PrestaShop\Adapter\Cart\CommandHandler; 28 29use Attribute; 30use Cart; 31use Context; 32use Customer; 33use Pack; 34use PrestaShop\PrestaShop\Adapter\Cart\AbstractCartHandler; 35use PrestaShop\PrestaShop\Adapter\ContextStateManager; 36use PrestaShop\PrestaShop\Core\Domain\Cart\Command\UpdateProductQuantityInCartCommand; 37use PrestaShop\PrestaShop\Core\Domain\Cart\CommandHandler\UpdateProductQuantityInCartHandlerInterface; 38use PrestaShop\PrestaShop\Core\Domain\Cart\Exception\CartConstraintException; 39use PrestaShop\PrestaShop\Core\Domain\Cart\Exception\CartException; 40use PrestaShop\PrestaShop\Core\Domain\Cart\Exception\MinimalQuantityException; 41use PrestaShop\PrestaShop\Core\Domain\Product\Exception\PackOutOfStockException; 42use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductCustomizationNotFoundException; 43use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductException; 44use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException; 45use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductOutOfStockException; 46use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId; 47use Product; 48use Shop; 49 50/** 51 * @internal 52 */ 53final class UpdateProductQuantityInCartHandler extends AbstractCartHandler implements UpdateProductQuantityInCartHandlerInterface 54{ 55 /** 56 * @var ContextStateManager 57 */ 58 private $contextStateManager; 59 60 /** 61 * @param ContextStateManager $contextStateManager 62 */ 63 public function __construct(ContextStateManager $contextStateManager) 64 { 65 $this->contextStateManager = $contextStateManager; 66 } 67 68 /** 69 * {@inheritdoc} 70 */ 71 public function handle(UpdateProductQuantityInCartCommand $command) 72 { 73 $cart = $this->getCart($command->getCartId()); 74 $this->contextStateManager 75 ->setCart($cart) 76 ->setShop(new Shop($cart->id_shop)) 77 ; 78 79 try { 80 $this->updateProductQuantityInCart($cart, $command); 81 } finally { 82 $this->contextStateManager->restorePreviousContext(); 83 } 84 } 85 86 /** 87 * @param Cart $cart 88 * @param UpdateProductQuantityInCartCommand $command 89 * 90 * @throws CartConstraintException 91 * @throws CartException 92 * @throws ProductException 93 * @throws ProductNotFoundException 94 * @throws ProductOutOfStockException 95 */ 96 private function updateProductQuantityInCart(Cart $cart, UpdateProductQuantityInCartCommand $command): void 97 { 98 $previousQty = $this->findPreviousQuantityInCart($cart, $command); 99 $qtyDiff = abs($command->getNewQuantity() - $previousQty); 100 101 if ($qtyDiff === 0) { 102 throw new CartConstraintException(sprintf('Cart quantity is already %d', $command->getNewQuantity()), CartConstraintException::UNCHANGED_QUANTITY); 103 } 104 105 // $cart::updateQty needs customer context 106 $customer = new Customer($cart->id_customer); 107 Context::getContext()->customer = $customer; 108 109 $this->assertOrderDoesNotExistForCart($cart); 110 111 $product = $this->getProductObject($command->getProductId()); 112 $combinationIdValue = $command->getCombinationId() ? $command->getCombinationId()->getValue() : 0; 113 $customizationId = $command->getCustomizationId(); 114 115 $this->assertProductIsInStock($product, $command); 116 $this->assertProductCustomization($product, $command); 117 118 if ($previousQty < $command->getNewQuantity()) { 119 $action = 'up'; 120 } else { 121 $action = 'down'; 122 } 123 124 $updateResult = $cart->updateQty( 125 $qtyDiff, 126 $command->getProductId()->getValue(), 127 $combinationIdValue, 128 $customizationId ? $customizationId->getValue() : false, 129 $action 130 ); 131 132 if (!$updateResult) { 133 throw new CartException('Failed to update product quantity in cart'); 134 } 135 136 // It seems that $updateResult can be -1, 137 // when adding product with less quantity than minimum required. 138 if ($updateResult < 0) { 139 $minQuantity = $combinationIdValue ? 140 Attribute::getAttributeMinimalQty($combinationIdValue) : 141 $product->minimal_quantity; 142 143 throw new MinimalQuantityException('Minimum quantity of %d must be added to cart.', $minQuantity); 144 } 145 } 146 147 /** 148 * @param Cart $cart 149 * 150 * @throws CartException 151 */ 152 private function assertOrderDoesNotExistForCart(Cart $cart) 153 { 154 if ($cart->orderExists()) { 155 throw new CartException(sprintf('Order for cart with id "%s" already exists.', $cart->id)); 156 } 157 } 158 159 /** 160 * @param ProductId $productId 161 * 162 * @return Product 163 * 164 * @throws ProductNotFoundException 165 */ 166 private function getProductObject(ProductId $productId) 167 { 168 $product = new Product($productId->getValue(), true); 169 170 if ($product->id !== $productId->getValue()) { 171 throw new ProductNotFoundException(sprintf('Product with id "%s" was not found', $productId->getValue())); 172 } 173 174 return $product; 175 } 176 177 /** 178 * @param Product $product 179 * @param UpdateProductQuantityInCartCommand $command 180 * 181 * @throws ProductOutOfStockException 182 * @throws PackOutOfStockException 183 */ 184 private function assertProductIsInStock(Product $product, UpdateProductQuantityInCartCommand $command): void 185 { 186 $isAvailableWhenOutOfStock = Product::isAvailableWhenOutOfStock($product->out_of_stock); 187 if (null !== $command->getCombinationId()) { 188 $isEnoughQuantity = Attribute::checkAttributeQty( 189 $command->getCombinationId()->getValue(), 190 $command->getNewQuantity() 191 ); 192 193 if (!$isAvailableWhenOutOfStock && !$isEnoughQuantity) { 194 throw new ProductOutOfStockException( 195 sprintf('Product with id "%s" is out of stock, thus cannot be added to cart', $product->id) 196 ); 197 } 198 199 return; 200 } elseif (Pack::isPack($product->id)) { 201 $hasPackEnoughQuantity = Pack::isInStock($product->id, $command->getNewQuantity()); 202 203 if (!$isAvailableWhenOutOfStock && !$hasPackEnoughQuantity) { 204 throw new PackOutOfStockException( 205 sprintf('Product with id "%s" is out of stock, thus cannot be added to cart', $product->id) 206 ); 207 } 208 209 return; 210 } 211 212 if (!$product->checkQty($command->getNewQuantity())) { 213 throw new ProductOutOfStockException(sprintf('Product with id "%s" is out of stock, thus cannot be added to cart', $product->id)); 214 } 215 } 216 217 /** 218 * If product is customizable and customization is not provided, 219 * then exception is thrown. 220 * 221 * @param Product $product 222 * @param UpdateProductQuantityInCartCommand $command 223 * 224 * @throws ProductCustomizationNotFoundException 225 */ 226 private function assertProductCustomization(Product $product, UpdateProductQuantityInCartCommand $command) 227 { 228 if (null === $command->getCustomizationId() && !$product->hasAllRequiredCustomizableFields()) { 229 throw new ProductCustomizationNotFoundException(sprintf( 230 'Missing customization for product with id "%s"', 231 $product->id 232 )); 233 } 234 } 235 236 /** 237 * @param Cart $cart 238 * @param UpdateProductQuantityInCartCommand $command 239 * 240 * @return int 241 */ 242 private function findPreviousQuantityInCart(Cart $cart, UpdateProductQuantityInCartCommand $command): int 243 { 244 $isCombination = ($command->getCombinationId() !== null); 245 $isCustomization = ($command->getCustomizationId() !== null); 246 247 foreach ($cart->getProducts() as $cartProduct) { 248 $equalProductId = (int) $cartProduct['id_product'] === $command->getProductId()->getValue(); 249 if ($isCombination) { 250 if ($equalProductId && (int) $cartProduct['id_product_attribute'] === $command->getCombinationId()->getValue()) { 251 return (int) $cartProduct['quantity']; 252 } 253 } elseif ($isCustomization) { 254 if ($equalProductId && (int) $cartProduct['id_customization'] === $command->getCustomizationId()->getValue()) { 255 return (int) $cartProduct['quantity']; 256 } 257 } elseif ($equalProductId) { 258 return (int) $cartProduct['quantity']; 259 } 260 } 261 262 return 0; 263 } 264} 265