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