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