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