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
27declare(strict_types=1);
28
29namespace PrestaShop\PrestaShop\Adapter\Order\Refund;
30
31use Cart;
32use CartRule;
33use Db;
34use Order;
35use OrderCartRule;
36use OrderDetail;
37use PrestaShop\PrestaShop\Adapter\Cart\Comparator\CartProductsComparator;
38use PrestaShop\PrestaShop\Adapter\Cart\Comparator\CartProductUpdate;
39use PrestaShop\PrestaShop\Core\Domain\Order\Exception\DeleteCustomizedProductFromOrderException;
40use PrestaShop\PrestaShop\Core\Domain\Order\Exception\DeleteProductFromOrderException;
41use Psr\Log\LoggerInterface;
42use SpecificPrice;
43use Symfony\Component\Translation\TranslatorInterface;
44
45class OrderProductRemover
46{
47    /**
48     * @var LoggerInterface
49     */
50    private $logger;
51
52    /**
53     * @var TranslatorInterface
54     */
55    private $translator;
56
57    /**
58     * @var CartProductsComparator
59     */
60    private $cartProductComparator;
61
62    /**
63     * OrderProductRemover constructor.
64     *
65     * @param LoggerInterface $logger
66     */
67    public function __construct(LoggerInterface $logger, TranslatorInterface $translator)
68    {
69        $this->logger = $logger;
70        $this->translator = $translator;
71    }
72
73    /**
74     * @param Order $order
75     * @param OrderDetail $orderDetail
76     * @param bool $updateCart Used when you don't want to update the cart (CartRule removal for example)
77     *
78     * @return CartProductsComparator
79     */
80    public function deleteProductFromOrder(
81        Order $order,
82        OrderDetail $orderDetail,
83        bool $updateCart = true
84    ): CartProductsComparator {
85        $cart = new Cart($order->id_cart);
86
87        // Important to remove order cart rule before the product is removed, so that cart rule can detect if it's applied on it
88        $this->deleteOrderCartRule($order, $orderDetail, $cart);
89
90        if ((int) $orderDetail->id_customization > 0) {
91            $this->deleteCustomization($order, $orderDetail);
92        }
93
94        $this->cartProductComparator = new CartProductsComparator($cart);
95        if ($updateCart) {
96            $this->updateCart($cart, $orderDetail);
97        }
98
99        $this->deleteSpecificPrice($order, $orderDetail, $cart);
100
101        if ((int) $orderDetail->id_customization > 0) {
102            $this->deleteCustomization($order, $orderDetail);
103        }
104
105        $this->deleteOrderDetail(
106            $order,
107            $orderDetail
108        );
109
110        return $this->cartProductComparator;
111    }
112
113    /**
114     * @param Cart $cart
115     * @param OrderDetail $orderDetail
116     */
117    private function updateCart(
118        Cart $cart,
119        OrderDetail $orderDetail
120    ): void {
121        $knownUpdates = [
122            new CartProductUpdate(
123                (int) $orderDetail->product_id,
124                (int) $orderDetail->product_attribute_id,
125                -$orderDetail->product_quantity,
126                false,
127                (int) $orderDetail->id_customization
128            ),
129        ];
130        $this->cartProductComparator->setKnownUpdates($knownUpdates);
131
132        $cart->updateQty(
133            $orderDetail->product_quantity,
134            $orderDetail->product_id,
135            $orderDetail->product_attribute_id,
136            $orderDetail->id_customization,
137            'down',
138            0,
139            null,
140            true,
141            false,
142            false // Do not preserve gift removal
143        );
144    }
145
146    /**
147     * @param Order $order
148     * @param OrderDetail $orderDetail
149     *
150     * @throws DeleteProductFromOrderException
151     * @throws \PrestaShopDatabaseException
152     * @throws \PrestaShopException
153     */
154    private function deleteOrderDetail(
155        Order $order,
156        OrderDetail $orderDetail
157    ) {
158        if (!$orderDetail->delete()) {
159            throw new DeleteProductFromOrderException('Could not delete order detail');
160        }
161
162        $order->update();
163    }
164
165    /**
166     * @param Order $order
167     * @param OrderDetail $orderDetail
168     */
169    private function deleteCustomization(Order $order, OrderDetail $orderDetail)
170    {
171        if (!(int) $order->getCurrentState()) {
172            throw new DeleteCustomizedProductFromOrderException('Could not get a valid Order state before deletion');
173        }
174        if (!Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_customization` = ' . (int) $orderDetail->id_customization . ' AND `id_cart` = ' . (int) $order->id_cart . ' AND `id_product` = ' . (int) $orderDetail->product_id)) {
175            throw new DeleteCustomizedProductFromOrderException('Could not delete customization from database.');
176        }
177    }
178
179    /**
180     * The deleted OrderCartRule are ignored by CartRule:autoAdd and CartRule:autoRemove so it is not able to clean
181     * them when the product is removed, hence the discount could never be re applied. So we manually check and remove
182     * them.
183     *
184     * @param Order $order
185     * @param OrderDetail $orderDetail
186     * @param Cart $cart
187     */
188    private function deleteOrderCartRule(
189        Order $order,
190        OrderDetail $orderDetail,
191        Cart $cart
192    ): void {
193        $orderCartRules = $order->getDeletedCartRules();
194        if (empty($orderCartRules)) {
195            return;
196        }
197
198        $removedOrderCartRules = [];
199        foreach ($orderCartRules as $orderCartRule) {
200            $cartRule = new CartRule($orderCartRule['id_cart_rule']);
201            $discountedProducts = $cartRule->checkProductRestrictionsFromCart($cart, true, false, true);
202            if (!is_array($discountedProducts)) {
203                continue;
204            }
205            foreach ($discountedProducts as $discountedProduct) {
206                // The return value is the concatenation of productId and attributeId, but the attributeId is always replaced by 0
207                if ($discountedProduct === $orderDetail->product_id . '-0') {
208                    if (!in_array($orderCartRule['id_order_cart_rule'], $removedOrderCartRules)) {
209                        $removedOrderCartRules[] = $orderCartRule['id_order_cart_rule'];
210                    }
211                }
212            }
213        }
214
215        foreach ($removedOrderCartRules as $removedOrderCartRuleId) {
216            $orderCartRule = new OrderCartRule($removedOrderCartRuleId);
217            $orderCartRule->delete();
218        }
219    }
220
221    /**
222     * @param Order $order
223     * @param OrderDetail $orderDetail
224     * @param Cart $cart
225     */
226    private function deleteSpecificPrice(
227        Order $order,
228        OrderDetail $orderDetail,
229        Cart $cart
230    ): void {
231        $productQuantity = $cart->getProductQuantity($orderDetail->product_id, $orderDetail->product_attribute_id);
232        if (!isset($productQuantity['quantity']) || (int) $productQuantity['quantity'] > 0) {
233            return;
234        }
235
236        // WARNING: DO NOT use SpecificPrice::getSpecificPrice as it filters out fields that are not in database
237        // hence it ignores the customer or cart restriction and results are biased
238        $existingSpecificPriceId = SpecificPrice::exists(
239            (int) $orderDetail->product_id,
240            (int) $orderDetail->product_attribute_id,
241            0,
242            0,
243            0,
244            $order->id_currency,
245            $order->id_customer,
246            SpecificPrice::ORDER_DEFAULT_FROM_QUANTITY,
247            SpecificPrice::ORDER_DEFAULT_DATE,
248            SpecificPrice::ORDER_DEFAULT_DATE,
249            false,
250            $order->id_cart
251        );
252        if (!empty($existingSpecificPriceId)) {
253            $specificPrice = new SpecificPrice($existingSpecificPriceId);
254            $specificPrice->delete();
255        }
256    }
257}
258