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\Core\Cart; 28 29use Cart; 30 31class CartRuleCalculator 32{ 33 /** 34 * @var Calculator 35 */ 36 protected $calculator; 37 38 /** 39 * @var CartRowCollection 40 */ 41 protected $cartRows; 42 43 /** 44 * @var CartRuleCollection 45 */ 46 protected $cartRules; 47 48 /** 49 * @var Fees 50 */ 51 protected $fees; 52 53 /** 54 * process cartrules calculation 55 */ 56 public function applyCartRules() 57 { 58 foreach ($this->cartRules as $cartRule) { 59 $this->applyCartRule($cartRule); 60 } 61 } 62 63 /** 64 * process cartrules calculation, excluding free-shipping processing 65 */ 66 public function applyCartRulesWithoutFreeShipping() 67 { 68 foreach ($this->cartRules as $cartRule) { 69 $this->applyCartRule($cartRule, false); 70 } 71 } 72 73 /** 74 * @param \PrestaShop\PrestaShop\Core\Cart\CartRuleCollection $cartRules 75 * 76 * @return CartRuleCalculator 77 */ 78 public function setCartRules($cartRules) 79 { 80 $this->cartRules = $cartRules; 81 82 return $this; 83 } 84 85 /** 86 * @param CartRuleData $cartRuleData 87 * @param bool $withFreeShipping used to calculate free shipping discount (avoid loop on shipping calculation) 88 * 89 * @throws \PrestaShopDatabaseException 90 */ 91 protected function applyCartRule(CartRuleData $cartRuleData, $withFreeShipping = true) 92 { 93 $cartRule = $cartRuleData->getCartRule(); 94 $cart = $this->calculator->getCart(); 95 96 if (!\CartRule::isFeatureActive()) { 97 return; 98 } 99 100 // Free shipping on selected carriers 101 if ($cartRule->free_shipping && $withFreeShipping) { 102 $initialShippingFees = $this->calculator->getFees()->getInitialShippingFees(); 103 $this->calculator->getFees()->subDiscountValueShipping($initialShippingFees); 104 $cartRuleData->addDiscountApplied($initialShippingFees); 105 } 106 107 // Free gift 108 if ((int) $cartRule->gift_product) { 109 foreach ($this->cartRows as $cartRow) { 110 $product = $cartRow->getRowData(); 111 if ($product['id_product'] == $cartRule->gift_product 112 && ($product['id_product_attribute'] == $cartRule->gift_product_attribute 113 || !(int) $cartRule->gift_product_attribute) 114 ) { 115 $cartRuleData->addDiscountApplied($cartRow->getInitialUnitPrice()); 116 $cartRow->applyFlatDiscount($cartRow->getInitialUnitPrice()); 117 } 118 } 119 } 120 121 // Percentage discount 122 if ((float) $cartRule->reduction_percent > 0) { 123 // Discount (%) on the whole order 124 if ($cartRule->reduction_product == 0) { 125 foreach ($this->cartRows as $cartRow) { 126 $product = $cartRow->getRowData(); 127 if ( 128 array_key_exists('product_quantity', $product) && 129 0 === (int) $product['product_quantity'] 130 ) { 131 $cartRuleData->addDiscountApplied(new AmountImmutable(0.0, 0.0)); 132 } elseif ((($cartRule->reduction_exclude_special && !$product['reduction_applies']) 133 || !$cartRule->reduction_exclude_special)) { 134 $amount = $cartRow->applyPercentageDiscount($cartRule->reduction_percent); 135 $cartRuleData->addDiscountApplied($amount); 136 } 137 } 138 } 139 140 // Discount (%) on a specific product 141 if ($cartRule->reduction_product > 0) { 142 foreach ($this->cartRows as $cartRow) { 143 if ($cartRow->getRowData()['id_product'] == $cartRule->reduction_product) { 144 $amount = $cartRow->applyPercentageDiscount($cartRule->reduction_percent); 145 $cartRuleData->addDiscountApplied($amount); 146 } 147 } 148 } 149 150 // Discount (%) on the cheapest product 151 if ($cartRule->reduction_product == -1) { 152 /** @var CartRow|null $cartRowCheapest */ 153 $cartRowCheapest = null; 154 foreach ($this->cartRows as $cartRow) { 155 $product = $cartRow->getRowData(); 156 if (((($cartRule->reduction_exclude_special && !$product['reduction_applies']) 157 || !$cartRule->reduction_exclude_special)) && ($cartRowCheapest === null 158 || $cartRowCheapest->getInitialUnitPrice()->getTaxIncluded() > $cartRow->getInitialUnitPrice() 159 ->getTaxIncluded()) 160 ) { 161 $cartRowCheapest = $cartRow; 162 } 163 } 164 if ($cartRowCheapest !== null) { 165 // apply only on one product of the cheapest row 166 $discountTaxIncluded = $cartRowCheapest->getInitialUnitPrice()->getTaxIncluded() 167 * $cartRule->reduction_percent / 100; 168 $discountTaxExcluded = $cartRowCheapest->getInitialUnitPrice()->getTaxExcluded() 169 * $cartRule->reduction_percent / 100; 170 $amount = new AmountImmutable($discountTaxIncluded, $discountTaxExcluded); 171 $cartRowCheapest->applyFlatDiscount($amount); 172 $cartRuleData->addDiscountApplied($amount); 173 } 174 } 175 176 // Discount (%) on the selection of products 177 if ($cartRule->reduction_product == -2) { 178 $selected_products = $cartRule->checkProductRestrictionsFromCart($cart, true); 179 if (is_array($selected_products)) { 180 foreach ($this->cartRows as $cartRow) { 181 $product = $cartRow->getRowData(); 182 if ((in_array($product['id_product'] . '-' . $product['id_product_attribute'], $selected_products) 183 || in_array($product['id_product'] . '-0', $selected_products)) 184 && (($cartRule->reduction_exclude_special && !$product['reduction_applies']) 185 || !$cartRule->reduction_exclude_special)) { 186 $amount = $cartRow->applyPercentageDiscount($cartRule->reduction_percent); 187 $cartRuleData->addDiscountApplied($amount); 188 } 189 } 190 } 191 } 192 } 193 194 // Amount discount (¤) : weighted calculation on all concerned rows 195 // weight factor got from price with same tax (incl/excl) as voucher 196 if ((float) $cartRule->reduction_amount > 0) { 197 $concernedRows = new CartRowCollection(); 198 if ($cartRule->reduction_product > 0) { 199 // discount on single product 200 foreach ($this->cartRows as $cartRow) { 201 if ($cartRow->getRowData()['id_product'] == $cartRule->reduction_product) { 202 $concernedRows->addCartRow($cartRow); 203 } 204 } 205 } elseif ($cartRule->reduction_product == 0) { 206 // Discount (¤) on the whole order 207 $concernedRows = $this->cartRows; 208 } 209 /* 210 * Reduction on the cheapest or on the selection is not really meaningful and has been disabled in the backend 211 * Please keep this code, so it won't be considered as a bug 212 * elseif ($this->reduction_product == -1) 213 * elseif ($this->reduction_product == -2) 214 */ 215 216 // currency conversion 217 $discountConverted = $this->convertAmountBetweenCurrencies( 218 $cartRule->reduction_amount, 219 new \Currency($cartRule->reduction_currency), 220 new \Currency($cart->id_currency) 221 ); 222 223 // get total of concerned rows 224 $totalTaxIncl = $totalTaxExcl = 0; 225 foreach ($concernedRows as $concernedRow) { 226 $totalTaxIncl += $concernedRow->getFinalTotalPrice()->getTaxIncluded(); 227 $totalTaxExcl += $concernedRow->getFinalTotalPrice()->getTaxExcluded(); 228 } 229 230 // The reduction cannot exceed the products total, except when we do not want it to be limited (for the partial use calculation) 231 $discountConverted = min($discountConverted, $cartRule->reduction_tax ? $totalTaxIncl : $totalTaxExcl); 232 233 // apply weighted discount : 234 // on each line we apply a part of the discount corresponding to discount*rowWeight/total 235 foreach ($concernedRows as $concernedRow) { 236 // get current line tax rate 237 $taxRate = 0; 238 if ($concernedRow->getFinalTotalPrice()->getTaxExcluded() != 0) { 239 $taxRate = ($concernedRow->getFinalTotalPrice()->getTaxIncluded() 240 - $concernedRow->getFinalTotalPrice()->getTaxExcluded()) 241 / $concernedRow->getFinalTotalPrice()->getTaxExcluded(); 242 } 243 $weightFactor = 0; 244 if ($cartRule->reduction_tax) { 245 // if cart rule amount is set tax included : calculate weight tax included 246 if ($totalTaxIncl != 0) { 247 $weightFactor = $concernedRow->getFinalTotalPrice()->getTaxIncluded() / $totalTaxIncl; 248 } 249 $discountAmountTaxIncl = $discountConverted * $weightFactor; 250 // recalculate tax included 251 $discountAmountTaxExcl = $discountAmountTaxIncl / (1 + $taxRate); 252 } else { 253 // if cart rule amount is set tax excluded : calculate weight tax excluded 254 if ($totalTaxExcl != 0) { 255 $weightFactor = $concernedRow->getFinalTotalPrice()->getTaxExcluded() / $totalTaxExcl; 256 } 257 $discountAmountTaxExcl = $discountConverted * $weightFactor; 258 // recalculate tax excluded 259 $discountAmountTaxIncl = $discountAmountTaxExcl * (1 + $taxRate); 260 } 261 $amount = new AmountImmutable($discountAmountTaxIncl, $discountAmountTaxExcl); 262 $concernedRow->applyFlatDiscount($amount); 263 $cartRuleData->addDiscountApplied($amount); 264 } 265 } 266 } 267 268 /** 269 * @param \PrestaShop\PrestaShop\Core\Cart\Calculator $calculator 270 * 271 * @return CartRuleCalculator 272 */ 273 public function setCalculator($calculator) 274 { 275 $this->calculator = $calculator; 276 277 return $this; 278 } 279 280 protected function convertAmountBetweenCurrencies($amount, \Currency $currencyFrom, \Currency $currencyTo) 281 { 282 if ($amount == 0 || $currencyFrom->conversion_rate == 0) { 283 return 0; 284 } 285 286 // convert to default currency 287 $amount /= $currencyFrom->conversion_rate; 288 // convert to destination currency 289 $amount *= $currencyTo->conversion_rate; 290 291 return $amount; 292 } 293 294 /** 295 * @param \PrestaShop\PrestaShop\Core\Cart\CartRowCollection $cartRows 296 * 297 * @return CartRuleCalculator 298 */ 299 public function setCartRows($cartRows) 300 { 301 $this->cartRows = $cartRows; 302 303 return $this; 304 } 305 306 /** 307 * @return CartRuleCollection 308 */ 309 public function getCartRulesData() 310 { 311 return $this->cartRules; 312 } 313} 314