1<?php
2/**
3 * 2007-2016 PrestaShop
4 *
5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA
6 * Copyright (C) 2017-2018 thirty bees
7 *
8 * NOTICE OF LICENSE
9 *
10 * This source file is subject to the Open Software License (OSL 3.0)
11 * that is bundled with this package in the file LICENSE.txt.
12 * It is also available through the world-wide-web at this URL:
13 * http://opensource.org/licenses/osl-3.0.php
14 * If you did not receive a copy of the license and are unable to
15 * obtain it through the world-wide-web, please send an email
16 * to license@thirtybees.com so we can send you a copy immediately.
17 *
18 * DISCLAIMER
19 *
20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
21 * versions in the future. If you wish to customize PrestaShop for your
22 * needs please refer to https://www.thirtybees.com for more information.
23 *
24 * @author    thirty bees <contact@thirtybees.com>
25 * @author    PrestaShop SA <contact@prestashop.com>
26 * @copyright 2017-2018 thirty bees
27 * @copyright 2007-2016 PrestaShop SA
28 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
29 *  PrestaShop is an internationally registered trademark & property of PrestaShop SA
30 */
31
32/**
33 * Class CartRuleCore
34 *
35 * @since 1.0.0
36 */
37class CartRuleCore extends ObjectModel
38{
39    /* Filters used when retrieving the cart rules applied to a cart of when calculating the value of a reduction */
40    const FILTER_ACTION_ALL = 1;
41    const FILTER_ACTION_SHIPPING = 2;
42    const FILTER_ACTION_REDUCTION = 3;
43    const FILTER_ACTION_GIFT = 4;
44    const FILTER_ACTION_ALL_NOCAP = 5;
45
46    const BO_ORDER_CODE_PREFIX = 'BO_ORDER_';
47
48    // @codingStandardsIgnoreStart
49    /**
50     * This variable controls that a free gift is offered only once, even when multi-shipping is activated and the same product is delivered in both addresses
51     *
52     * @var array
53     */
54    protected static $onlyOneGift = [];
55    /** @var int $id */
56    public $id;
57    /** @var string $name */
58    public $name;
59    /** @var int $id_customer */
60    public $id_customer;
61    /** @var string $date_from */
62    public $date_from;
63    /** @var string $date_to */
64    public $date_to;
65    /**
66     * @FIXME: with 1.0.x the cart rule cannot register the calculated
67     *       cheapest product in case it is converted into an order.
68     *       The copied cart rule is then injected with this information
69     *       in the `description` field and looks like this:
70     *       {
71     *         "type": "cheapest_product",
72     *         "id_product": "7",
73     *         "id_product_attribute": "0"
74     *       }
75     *
76     *       In the AdminCartRulesController, the field is then disabled to prevent the user from changing it
77     *
78     *       When making an update script for 1.1.x, don't forget to clean this field up and convert it to
79     *       a proper database table.
80     *
81     * @var string $description
82     */
83    public $description;
84    /** @var int $quantity */
85    public $quantity = 1;
86    /** @var int $quantity_per_user */
87    public $quantity_per_user = 1;
88    /** @var int $priority */
89    public $priority = 1;
90    /** @var int $partial_use */
91    public $partial_use = 1;
92    /** @var string $code */
93    public $code;
94    /** @var float $minimum_amount */
95    public $minimum_amount;
96    /** @var bool $minimum_amount_tax */
97    public $minimum_amount_tax;
98    /** @var int $minimum_amount_currency */
99    public $minimum_amount_currency;
100    /** @var bool $minimum_amount_shipping */
101    public $minimum_amount_shipping;
102    /** @var bool $country_restriction */
103    public $country_restriction;
104    /** @var bool $carrier_restriction */
105    public $carrier_restriction;
106    /** @var bool $group_restriction */
107    public $group_restriction;
108    /** @var bool $cart_rule_restriction */
109    public $cart_rule_restriction;
110    /** @var bool $product_restriction */
111    public $product_restriction;
112    /** @var bool $shop_restriction */
113    public $shop_restriction;
114    /** @var bool $free_shipping */
115    public $free_shipping;
116    /** @var float $reduction_percent */
117    public $reduction_percent;
118    /** @var float $reduction_amount */
119    public $reduction_amount;
120    /** @var bool $reduction_tax */
121    public $reduction_tax;
122    /** @var bool $reduction_currency */
123    public $reduction_currency;
124    /** @var int $reduction_product */
125    public $reduction_product;
126    /** @var int $gift_product */
127    public $gift_product;
128    /** @var int $gift_product_attribute */
129    public $gift_product_attribute;
130    /** @var bool $highlight */
131    public $highlight;
132    /** @var int $active */
133    public $active = 1;
134    /** @var string $date_add */
135    public $date_add;
136    /** @var string $date_upd */
137    public $date_upd;
138    // @codingStandardsIgnoreEnd
139
140    /**
141     * @see ObjectModel::$definition
142     */
143    public static $definition = [
144        'table'     => 'cart_rule',
145        'primary'   => 'id_cart_rule',
146        'multilang' => true,
147        'fields'    => [
148            'id_customer'             => ['type' => self::TYPE_INT,    'validate' => 'isUnsignedId'],
149            'date_from'               => ['type' => self::TYPE_DATE,   'validate' => 'isDate', 'required' => true],
150            'date_to'                 => ['type' => self::TYPE_DATE,   'validate' => 'isDate', 'required' => true],
151            'description'             => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 65534],
152            'quantity'                => ['type' => self::TYPE_INT,    'validate' => 'isUnsignedInt'],
153            'quantity_per_user'       => ['type' => self::TYPE_INT,    'validate' => 'isUnsignedInt'],
154            'priority'                => ['type' => self::TYPE_INT,    'validate' => 'isUnsignedInt'],
155            'partial_use'             => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
156            'code'                    => ['type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 254],
157            'minimum_amount'          => ['type' => self::TYPE_PRICE,  'validate' => 'isPrice'],
158            'minimum_amount_tax'      => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
159            'minimum_amount_currency' => ['type' => self::TYPE_INT,    'validate' => 'isInt'],
160            'minimum_amount_shipping' => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
161            'country_restriction'     => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
162            'carrier_restriction'     => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
163            'group_restriction'       => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
164            'cart_rule_restriction'   => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
165            'product_restriction'     => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
166            'shop_restriction'        => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
167            'free_shipping'           => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
168            'reduction_percent'       => ['type' => self::TYPE_FLOAT,  'validate' => 'isPercentage'],
169            'reduction_amount'        => ['type' => self::TYPE_PRICE,  'validate' => 'isPrice'],
170            'reduction_tax'           => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
171            'reduction_currency'      => ['type' => self::TYPE_INT,    'validate' => 'isUnsignedId'],
172            'reduction_product'       => ['type' => self::TYPE_INT,    'validate' => 'isInt'],
173            'gift_product'            => ['type' => self::TYPE_INT,    'validate' => 'isUnsignedId'],
174            'gift_product_attribute'  => ['type' => self::TYPE_INT,    'validate' => 'isUnsignedId'],
175            'highlight'               => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
176            'active'                  => ['type' => self::TYPE_BOOL,   'validate' => 'isBool'],
177            'date_add'                => ['type' => self::TYPE_DATE,   'validate' => 'isDate'],
178            'date_upd'                => ['type' => self::TYPE_DATE,   'validate' => 'isDate'],
179
180            /* Lang fields */
181            'name'                    => ['type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isCleanHtml', 'required' => true, 'size' => 254],
182        ],
183    ];
184
185    /**
186     * Copy conditions from one cart rule to an other
187     *
188     * @param int $idCartRuleSource
189     * @param int $idCartRuleDestination
190     *
191     * @since   1.0.0
192     * @version 1.0.0 Initial version
193     * @throws PrestaShopException
194     */
195    public static function copyConditions($idCartRuleSource, $idCartRuleDestination)
196    {
197        Db::getInstance()->execute(
198            '
199		INSERT INTO `'._DB_PREFIX_.'cart_rule_shop` (`id_cart_rule`, `id_shop`)
200		(SELECT '.(int) $idCartRuleDestination.', id_shop FROM `'._DB_PREFIX_.'cart_rule_shop` WHERE `id_cart_rule` = '.(int) $idCartRuleSource.')'
201        );
202        Db::getInstance()->execute(
203            '
204		INSERT INTO `'._DB_PREFIX_.'cart_rule_carrier` (`id_cart_rule`, `id_carrier`)
205		(SELECT '.(int) $idCartRuleDestination.', id_carrier FROM `'._DB_PREFIX_.'cart_rule_carrier` WHERE `id_cart_rule` = '.(int) $idCartRuleSource.')'
206        );
207        Db::getInstance()->execute(
208            '
209		INSERT INTO `'._DB_PREFIX_.'cart_rule_group` (`id_cart_rule`, `id_group`)
210		(SELECT '.(int) $idCartRuleDestination.', id_group FROM `'._DB_PREFIX_.'cart_rule_group` WHERE `id_cart_rule` = '.(int) $idCartRuleSource.')'
211        );
212        Db::getInstance()->execute(
213            '
214		INSERT INTO `'._DB_PREFIX_.'cart_rule_country` (`id_cart_rule`, `id_country`)
215		(SELECT '.(int) $idCartRuleDestination.', id_country FROM `'._DB_PREFIX_.'cart_rule_country` WHERE `id_cart_rule` = '.(int) $idCartRuleSource.')'
216        );
217        Db::getInstance()->execute(
218            '
219		INSERT INTO `'._DB_PREFIX_.'cart_rule_combination` (`id_cart_rule_1`, `id_cart_rule_2`)
220		(SELECT '.(int) $idCartRuleDestination.', IF(id_cart_rule_1 != '.(int) $idCartRuleSource.', id_cart_rule_1, id_cart_rule_2) FROM `'._DB_PREFIX_.'cart_rule_combination`
221		WHERE `id_cart_rule_1` = '.(int) $idCartRuleSource.' OR `id_cart_rule_2` = '.(int) $idCartRuleSource.')'
222        );
223
224        // Todo : should be changed soon, be must be copied too
225        // Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'cart_rule_product_rule` WHERE `id_cart_rule` = '.(int)$this->id);
226        // Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'cart_rule_product_rule_value` WHERE `id_product_rule` NOT IN (SELECT `id_product_rule` FROM `'._DB_PREFIX_.'cart_rule_product_rule`)');
227
228        // Copy products/category filters
229        $sql = new DbQuery();
230        $sql->select('`id_product_rule_group`, `quantity`');
231        $sql->from('cart_rule_product_rule_group');
232        $sql->where('`id_cart_rule` = '.(int) $idCartRuleSource);
233        $productsRulesGroupSource = Db::getInstance()->ExecuteS($sql);
234
235        foreach ($productsRulesGroupSource as $productRuleGroupSource) {
236            Db::getInstance()->insert(
237                'cart_rule_product_rule_group',
238                [
239                    'id_cart_rule' => (int) $idCartRuleDestination,
240                    'quantity'     => (int) $productRuleGroupSource['quantity'],
241                ]
242            );
243            $idProductRuleGroupDestination = Db::getInstance()->Insert_ID();
244
245            $productsRulesSource = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
246                (new DbQuery())
247                    ->select('`id_product_rule`, `type`')
248                    ->from('cart_rule_product_rule')
249                    ->where('`id_product_rule_group` = '.(int) $productsRulesGroupSource['id_product_rule_group'])
250            );
251
252            foreach ($productsRulesSource as $productRuleSource) {
253                Db::getInstance()->insert(
254                    'cart_rule_product_rule',
255                    [
256                        'id_product_rule_group' => (int) $idProductRuleGroupDestination,
257                        'type'                  => pSQL($productRuleSource['type']),
258                    ]
259                );
260                $idProductRuleDestination = Db::getInstance()->Insert_ID();
261
262                $productsRulesValuesSource = Db::getInstance()->executeS(
263                    (new DbQuery())
264                        ->select('`id_item`')
265                        ->from('cart_rule_product_rule_value')
266                        ->where('`id_product_rule` = '.(int) $productsRulesSource['id_product_rule'])
267                );
268
269                foreach ($productsRulesValuesSource as $productRuleValueSource) {
270                    Db::getInstance()->insert(
271                        'cart_rule_product_rule_value',
272                        [
273                            'id_product_rule' => (int) $idProductRuleDestination,
274                            'id_item'         => (int) $productRuleValueSource['id_item'],
275
276                        ]
277                    );
278                }
279            }
280        }
281    }
282
283    /**
284     * Retrieves the id associated to the given code
285     *
286     * @param string $code
287     *
288     * @return int|bool
289     *
290     * @since   1.0.0
291     * @version 1.0.0 Initial version
292     * @throws PrestaShopException
293     */
294    public static function getIdByCode($code)
295    {
296        if (!Validate::isCleanHtml($code)) {
297            return false;
298        }
299
300        return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
301            (new DbQuery())
302                ->select('`id_cart_rule`')
303                ->from('cart_rule')
304                ->where('`code` = \''.pSQL($code).'\'')
305        );
306    }
307
308    /**
309     * @param int       $idLang
310     * @param int       $idCustomer
311     * @param bool      $active
312     * @param bool      $includeGeneric
313     * @param bool      $inStock
314     * @param Cart|null $cart
315     * @param bool      $freeShippingOnly
316     * @param bool      $highlightOnly
317     *
318     * @return array
319     * @throws PrestaShopDatabaseException
320     *
321     * @since   1.0.0
322     * @version 1.0.0 Initial version
323     * @throws PrestaShopException
324     * @throws PrestaShopException
325     * @throws PrestaShopException
326     * @throws PrestaShopException
327     * @throws PrestaShopException
328     */
329    public static function getCustomerCartRules($idLang, $idCustomer, $active = false, $includeGeneric = true, $inStock = false, Cart $cart = null, $freeShippingOnly = false, $highlightOnly = false)
330    {
331        if (!static::isFeatureActive()) {
332            return [];
333        }
334
335        $sql = (new DbQuery())
336            ->select('*')
337            ->from('cart_rule', 'cr')
338            ->leftJoin('cart_rule_lang', 'crl', 'cr.`id_cart_rule` = crl.`id_cart_rule` AND crl.`id_lang` = '.(int) $idLang)
339            ->where('cr.`date_from` < \''.date('Y-m-d H:i:s').'\'')
340            ->where('cr.`date_to` > \''.date('Y-m-d H:i:s').'\'');
341        if ($active) {
342            $sql->where('cr.`active` = 1');
343        }
344        if ($inStock) {
345            $sql->where('cr.`quantity` > 0');
346        }
347        if ($freeShippingOnly) {
348            $sql->where('`free_shipping` = 1');
349            $sql->where('`carrier_restriction` = 1');
350        }
351        if ($highlightOnly) {
352            $sql->where('`highlight` = 1');
353            $sql->where('`code` NOT LIKE \''.pSQL(static::BO_ORDER_CODE_PREFIX).'%\'');
354        }
355        $sql->where('cr.`id_customer` = '.(int) $idCustomer.' OR cr.`group_restriction` = 1'.(($includeGeneric && (int) $idCustomer !== 0) ? ' OR cr.`id_customer` = 0' : ''));
356
357        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql, true);
358
359        if (empty($result)) {
360            return [];
361        }
362
363        // Remove cart rule that does not match the customer groups
364        $customerGroups = Customer::getGroupsStatic($idCustomer);
365
366        foreach ($result as $key => $cartRule) {
367            if ($cartRule['group_restriction']) {
368                $cartRuleGroups = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
369                    (new DbQuery())
370                        ->select('`id_group`')
371                        ->from('cart_rule_group')
372                        ->where('id_cart_rule = '.(int) $cartRule['id_cart_rule'])
373                );
374                foreach ($cartRuleGroups as $cartRuleGroup) {
375                    if (in_array($cartRuleGroup['id_group'], $customerGroups)) {
376                        continue 2;
377                    }
378                }
379                unset($result[$key]);
380            }
381        }
382
383        foreach ($result as &$cartRule) {
384            if ($cartRule['quantity_per_user']) {
385                $quantityUsed = Order::getDiscountsCustomer((int) $idCustomer, (int) $cartRule['id_cart_rule']);
386                if (isset($cart) && isset($cart->id)) {
387                    $quantityUsed += $cart->getDiscountsCustomer((int) $cartRule['id_cart_rule']);
388                }
389                $cartRule['quantity_for_user'] = $cartRule['quantity_per_user'] - $quantityUsed;
390            } else {
391                $cartRule['quantity_for_user'] = 0;
392            }
393            // Backwards compatibility
394            $cartRule['id_group'] = 0;
395            if ($cartRule['free_shipping']) {
396                $cartRule['id_discount_type'] = 3;
397            } elseif ($cartRule['reduction_percent'] > 0) {
398                $cartRule['id_discount_type'] = 1;
399            } elseif ($cartRule['reduction_amount'] > 0) {
400                $cartRule['id_discount_type'] = 2;
401            }
402            if ($cartRule['reduction_percent'] > 0) {
403                $cartRule['value'] = $cartRule['reduction_percent'];
404            } elseif ($cartRule['reduction_amount'] > 0) {
405                $cartRule['value'] = $cartRule['reduction_amount'];
406            }
407            $cartRule['cumulable'] = $cartRule['cart_rule_restriction'];
408            $cartRule['cumulable_reduction'] = false;
409            $cartRule['minimal'] = $cartRule['minimum_amount'];
410            $cartRule['include_tax'] = $cartRule['reduction_tax'];
411            $cartRule['behavior_not_exhausted'] = $cartRule['partial_use'];
412            $cartRule['cart_display'] = true;
413        }
414        unset($cartRule);
415
416        foreach ($result as $key => $cartRule) {
417            if ($cartRule['shop_restriction']) {
418                $cartRuleShops = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
419                    (new DbQuery())
420                        ->select('`id_shop`')
421                        ->from('cart_rule_shop')
422                        ->where('`id_cart_rule` = '.(int) $cartRule['id_cart_rule'])
423                );
424                foreach ($cartRuleShops as $cartRuleShop) {
425                    if (Shop::isFeatureActive() && ($cartRuleShop['id_shop'] == Context::getContext()->shop->id)) {
426                        continue 2;
427                    }
428                }
429                unset($result[$key]);
430            }
431        }
432
433        if (isset($cart) && isset($cart->id)) {
434            foreach ($result as $key => $cartRule) {
435                if ($cartRule['product_restriction']) {
436                    $cr = new CartRule((int) $cartRule['id_cart_rule']);
437                    $r = $cr->checkProductRestrictions(Context::getContext(), false, false);
438                    if ($r !== false) {
439                        continue;
440                    }
441                    unset($result[$key]);
442                }
443            }
444        }
445
446        $resultBak = $result;
447        $result = [];
448        $countryRestriction = false;
449        foreach ($resultBak as $key => $cartRule) {
450            if ($cartRule['country_restriction']) {
451                $countryRestriction = true;
452                $countries = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
453                    (new DbQuery())
454                        ->select('`id_country`')
455                        ->from('address')
456                        ->where('`id_customer` = '.(int) $idCustomer)
457                        ->where('`deleted` = 0')
458                );
459
460                if (is_array($countries) && !empty($countries)) {
461                    foreach ($countries as $country) {
462                        $idCartRule = (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
463                            (new DbQuery())
464                                ->select('crc.`id_cart_rule`')
465                                ->from('cart_rule_country', 'crc')
466                                ->where('crc.`id_cart_rule` = '.(int) $cartRule['id_cart_rule'])
467                                ->where('crc.`id_country` = '.(int) $country['id_country'])
468                        );
469                        if ($idCartRule) {
470                            $result[] = $resultBak[$key];
471                            break;
472                        }
473                    }
474                }
475            } else {
476                $result[] = $resultBak[$key];
477            }
478        }
479
480        if (!$countryRestriction) {
481            $result = $resultBak;
482        }
483
484        return $result;
485    }
486
487    /**
488     * @return bool
489     *
490     * @since   1.0.0
491     * @version 1.0.0 Initial version
492     * @throws PrestaShopException
493     */
494    public static function isFeatureActive()
495    {
496        static $isFeatureActive = null;
497        if ($isFeatureActive === null) {
498            $isFeatureActive = (bool) Configuration::get('PS_CART_RULE_FEATURE_ACTIVE');
499        }
500
501        return $isFeatureActive;
502    }
503
504    /**
505     * @param Context $context
506     * @param bool    $returnProducts
507     * @param bool    $displayError
508     * @param bool    $alreadyInCart
509     *
510     * @return array|bool|mixed|string
511     *
512     * @throws PrestaShopDatabaseException
513     * @throws PrestaShopException
514     * @since   1.0.0
515     * @version 1.0.0 Initial version
516     */
517    protected function checkProductRestrictions(Context $context, $returnProducts = false, $displayError = true, $alreadyInCart = false)
518    {
519        $selectedProducts = [];
520
521        // Check if the products chosen by the customer are usable with the cart rule
522        if ($this->product_restriction) {
523            $productRuleGroups = $this->getProductRuleGroups();
524            foreach ($productRuleGroups as $idProductRuleGroup => $productRuleGroup) {
525                $eligibleProductsList = [];
526                if (isset($context->cart) && is_object($context->cart) && is_array($products = $context->cart->getProducts())) {
527                    foreach ($products as $product) {
528                        $eligibleProductsList[] = (int) $product['id_product'].'-'.(int) $product['id_product_attribute'];
529                    }
530                }
531                if (!count($eligibleProductsList)) {
532                    return (!$displayError) ? false : Tools::displayError('You cannot use this voucher in an empty cart');
533                }
534
535                $productRules = $this->getProductRules($idProductRuleGroup);
536                foreach ($productRules as $productRule) {
537                    switch ($productRule['type']) {
538                        case 'attributes':
539                            $cartAttributes = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
540                                (new DbQuery())
541                                    ->select('cp.`quantity`, cp.`id_product`, pac.`id_attribute`, cp.`id_product_attribute`')
542                                    ->from('cart_product', 'cp')
543                                    ->leftJoin('product_attribute_combination', 'pac', 'cp.`id_product_attribute` = pac.`id_product_attribute`')
544                                    ->where('cp.`id_cart` = '.(int) $context->cart->id)
545                                    ->where('cp.`id_product` IN ('.implode(',', array_map('intval', $eligibleProductsList)).')')
546                                    ->where('cp.`id_product_attribute` > 0')
547                            );
548                            $countMatchingProducts = 0;
549                            $matchingProductsList = [];
550                            foreach ($cartAttributes as $cartAttribute) {
551                                if (in_array($cartAttribute['id_attribute'], $productRule['values'])) {
552                                    $countMatchingProducts += $cartAttribute['quantity'];
553                                    if ($alreadyInCart && $this->gift_product == $cartAttribute['id_product']
554                                        && $this->gift_product_attribute == $cartAttribute['id_product_attribute']
555                                    ) {
556                                        --$countMatchingProducts;
557                                    }
558                                    $matchingProductsList[] = $cartAttribute['id_product'].'-'.$cartAttribute['id_product_attribute'];
559                                }
560                            }
561                            if ($countMatchingProducts < $productRuleGroup['quantity']) {
562                                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher with these products');
563                            }
564                            $eligibleProductsList = static::array_uintersect($eligibleProductsList, $matchingProductsList);
565                            break;
566                        case 'products':
567                            $cartProducts = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
568                                (new DbQuery())
569                                    ->select('cp.`quantity`, cp.`id_product`')
570                                    ->from('cart_product', 'cp')
571                                    ->where('cp.`id_cart` = '.(int) $context->cart->id)
572                                    ->where('cp.`id_product` IN ('.implode(',', array_map('intval', $eligibleProductsList)).')')
573                            );
574                            $countMatchingProducts = 0;
575                            $matchingProductsList = [];
576                            foreach ($cartProducts as $cartProduct) {
577                                if (in_array($cartProduct['id_product'], $productRule['values'])) {
578                                    $countMatchingProducts += $cartProduct['quantity'];
579                                    if ($alreadyInCart && $this->gift_product == $cartProduct['id_product']) {
580                                        --$countMatchingProducts;
581                                    }
582                                    $matchingProductsList[] = $cartProduct['id_product'].'-0';
583                                }
584                            }
585                            if ($countMatchingProducts < $productRuleGroup['quantity']) {
586                                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher with these products');
587                            }
588                            $eligibleProductsList = static::array_uintersect($eligibleProductsList, $matchingProductsList);
589                            break;
590                        case 'categories':
591                            $cartCategories = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
592                                (new DbQuery())
593                                    ->select('cp.quantity, cp.`id_product`, cp.`id_product_attribute`, catp.`id_category`')
594                                    ->from('cart_product', 'cp')
595                                    ->leftJoin('category_product', 'catp', 'cp.`id_product` = catp.`id_product`')
596                                    ->where('cp.`id_cart` = '.(int) $context->cart->id)
597                                    ->where('cp.`id_product` IN ('.implode(',', array_map('intval', $eligibleProductsList)).')')
598                                    ->where('cp.`id_product` <> '.(int) $this->gift_product)
599                            );
600                            $countMatchingProducts = 0;
601                            $matchingProductsList = [];
602                            foreach ($cartCategories as $cartCategory) {
603                                if (in_array($cartCategory['id_category'], $productRule['values'])
604                                    /**
605                                     * We also check that the product is not already in the matching product list,
606                                     * because there are doubles in the query results (when the product is in multiple categories)
607                                     */
608                                    && !in_array($cartCategory['id_product'].'-'.$cartCategory['id_product_attribute'], $matchingProductsList)
609                                ) {
610                                    $countMatchingProducts += $cartCategory['quantity'];
611                                    $matchingProductsList[] = $cartCategory['id_product'].'-'.$cartCategory['id_product_attribute'];
612                                }
613                            }
614                            if ($countMatchingProducts < $productRuleGroup['quantity']) {
615                                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher with these products');
616                            }
617                            // Attribute id is not important for this filter in the global list, so the ids are replaced by 0
618                            foreach ($matchingProductsList as &$matchingProduct) {
619                                $matchingProduct = preg_replace('/^([0-9]+)-[0-9]+$/', '$1-0', $matchingProduct);
620                            }
621                            $eligibleProductsList = static::array_uintersect($eligibleProductsList, $matchingProductsList);
622                            break;
623                        case 'manufacturers':
624                            $cartManufacturers = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
625                                (new DbQuery())
626                                    ->select('cp.quantity, cp.`id_product`, p.`id_manufacturer`')
627                                    ->from('cart_product', 'cp')
628                                    ->leftJoin('product', 'p', 'cp.`id_product` = p.`id_product`')
629                                    ->where('cp.`id_cart` = '.(int) $context->cart->id)
630                                    ->where('cp.`id_product` IN ('.implode(',', array_map('intval', $eligibleProductsList)).')')
631                            );
632                            $countMatchingProducts = 0;
633                            $matchingProductsList = [];
634                            foreach ($cartManufacturers as $cartManufacturer) {
635                                if (in_array($cartManufacturer['id_manufacturer'], $productRule['values'])) {
636                                    $countMatchingProducts += $cartManufacturer['quantity'];
637                                    $matchingProductsList[] = $cartManufacturer['id_product'].'-0';
638                                }
639                            }
640                            if ($countMatchingProducts < $productRuleGroup['quantity']) {
641                                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher with these products');
642                            }
643                            $eligibleProductsList = static::array_uintersect($eligibleProductsList, $matchingProductsList);
644                            break;
645                        case 'suppliers':
646                            $cartSuppliers = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
647                                (new DbQuery())
648                                    ->select('cp.`quantity`, cp.`id_product`, p.`id_supplier`')
649                                    ->from('cart_product', 'cp')
650                                    ->leftJoin('product', 'p', 'cp.`id_product` = p.`id_product`')
651                                    ->where('cp.`id_cart` = '.(int) $context->cart->id)
652                                    ->where('cp.`id_product` IN ('.implode(',', array_map('intval', $eligibleProductsList)).')')
653                            );
654                            $countMatchingProducts = 0;
655                            $matchingProductsList = [];
656                            foreach ($cartSuppliers as $cartSupplier) {
657                                if (in_array($cartSupplier['id_supplier'], $productRule['values'])) {
658                                    $countMatchingProducts += $cartSupplier['quantity'];
659                                    $matchingProductsList[] = $cartSupplier['id_product'].'-0';
660                                }
661                            }
662                            if ($countMatchingProducts < $productRuleGroup['quantity']) {
663                                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher with these products');
664                            }
665                            $eligibleProductsList = static::array_uintersect($eligibleProductsList, $matchingProductsList);
666                            break;
667                    }
668
669                    if (!count($eligibleProductsList)) {
670                        return (!$displayError) ? false : Tools::displayError('You cannot use this voucher with these products');
671                    }
672                }
673                $selectedProducts = array_merge($selectedProducts, $eligibleProductsList);
674            }
675        }
676
677        if ($returnProducts) {
678            return $selectedProducts;
679        }
680
681        return (!$displayError) ? true : false;
682    }
683
684    /**
685     * @return array
686     *
687     * @throws PrestaShopDatabaseException
688     * @throws PrestaShopException
689     * @since   1.0.0
690     * @version 1.0.0 Initial version
691     */
692    public function getProductRuleGroups()
693    {
694        if (!Validate::isLoadedObject($this) || $this->product_restriction == 0) {
695            return [];
696        }
697
698        $productRuleGroups = [];
699        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
700            (new DbQuery())
701                ->select('*')
702                ->from('cart_rule_product_rule_group')
703                ->where('`id_cart_rule` = '.(int) $this->id)
704        );
705        foreach ($result as $row) {
706            if (!isset($productRuleGroups[$row['id_product_rule_group']])) {
707                $productRuleGroups[$row['id_product_rule_group']] = ['id_product_rule_group' => $row['id_product_rule_group'], 'quantity' => $row['quantity']];
708            }
709            $productRuleGroups[$row['id_product_rule_group']]['product_rules'] = $this->getProductRules($row['id_product_rule_group']);
710        }
711
712        return $productRuleGroups;
713    }
714
715    /**
716     * @param int $idProductRuleGroup
717     *
718     * @return array ('type' => ? , 'values' => ?)
719     * @throws PrestaShopDatabaseException
720     * @throws PrestaShopException
721     */
722    public function getProductRules($idProductRuleGroup)
723    {
724        if (!Validate::isLoadedObject($this) || $this->product_restriction == 0) {
725            return [];
726        }
727
728        $productRules = [];
729        $results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
730            (new DbQuery())
731                ->select('*')
732                ->from('cart_rule_product_rule', 'pr')
733                ->leftJoin('cart_rule_product_rule_value', 'prv', 'pr.`id_product_rule` = prv.`id_product_rule`')
734                ->where('pr.`id_product_rule_group` = '.(int) $idProductRuleGroup)
735        );
736        foreach ($results as $row) {
737            if (!isset($productRules[$row['id_product_rule']])) {
738                $productRules[$row['id_product_rule']] = ['type' => $row['type'], 'values' => []];
739            }
740            $productRules[$row['id_product_rule']]['values'][] = $row['id_item'];
741        }
742
743        return $productRules;
744    }
745
746    /**
747     * @param $array1
748     * @param $array2
749     *
750     * @return array
751     *
752     * @since 1.0.0
753     * @version 1.0.0 Initial version
754     */
755    protected static function array_uintersect($array1, $array2)
756    {
757        $intersection = [];
758        foreach ($array1 as $value1) {
759            foreach ($array2 as $value2) {
760                if (static::array_uintersect_compare($value1, $value2) == 0) {
761                    $intersection[] = $value1;
762                    break 1;
763                }
764            }
765        }
766
767        return $intersection;
768    }
769
770    /**
771     * @param $a
772     * @param $b
773     *
774     * @return int
775     *
776     * @since 1.0.0
777     * @version 1.0.0 Initial version
778     */
779    protected static function array_uintersect_compare($a, $b)
780    {
781        if ($a == $b) {
782            return 0;
783        }
784
785        $asplit = explode('-', $a);
786        $bsplit = explode('-', $b);
787        if ($asplit[0] == $bsplit[0] && (!(int) $asplit[1] || !(int) $bsplit[1])) {
788            return 0;
789        }
790
791        return 1;
792    }
793
794    /**
795     * @param string $name
796     *
797     * @return bool
798     *
799     * @since   1.0.0
800     * @version 1.0.0 Initial version
801     * @throws PrestaShopException
802     */
803    public static function cartRuleExists($name)
804    {
805        if (!static::isFeatureActive()) {
806            return false;
807        }
808
809        return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
810            (new DbQuery())
811                ->select('`id_cart_rule`')
812                ->from('cart_rule')
813                ->where('`code` = \''.pSQL($name).'\'')
814        );
815    }
816
817    /**
818     * @param int $idCustomer
819     *
820     * @return bool
821     *
822     * @since   1.0.0
823     * @version 1.0.0 Initial version
824     * @throws PrestaShopException
825     */
826    public static function deleteByIdCustomer($idCustomer)
827    {
828        $return = true;
829        $cartRules = new PrestaShopCollection('CartRule');
830        $cartRules->where('id_customer', '=', $idCustomer);
831        foreach ($cartRules as $cartRule) {
832            $return &= $cartRule->delete();
833        }
834
835        return $return;
836    }
837
838    /**
839     * Make sure caches are empty
840     * Must be called before calling multiple time getContextualValue()
841     *
842     * @since 1.0.0
843     * @version 1.0.0 Initial version
844     */
845    public static function cleanCache()
846    {
847        static::$onlyOneGift = [];
848    }
849
850    /**
851     * @param null $context
852     *
853     * @return array
854     *
855     * @throws PrestaShopDatabaseException
856     * @throws PrestaShopException
857     * @since   1.0.0
858     * @version 1.0.0 Initial version
859     */
860    public static function autoRemoveFromCart($context = null)
861    {
862        if (!$context) {
863            $context = Context::getContext();
864        }
865        if (!static::isFeatureActive() || !Validate::isLoadedObject($context->cart)) {
866            return [];
867        }
868
869        static $errors = [];
870        foreach ($context->cart->getCartRules() as $cartRule) {
871            /** @var CartRule $cartRuleObject */
872            $cartRuleObject = $cartRule['obj'];
873            if ($error = $cartRuleObject->checkValidity($context, true)) {
874                $context->cart->removeCartRule($cartRuleObject->id);
875                $context->cart->update();
876                $errors[] = $error;
877            }
878        }
879
880        return $errors;
881    }
882
883    /**
884     * @param Context|null $context
885     *
886     * @return mixed
887     *
888     * @since   1.0.0
889     * @version 1.0.0 Initial version
890     * @throws PrestaShopException
891     */
892    public static function autoAddToCart(Context $context = null)
893    {
894        if ($context === null) {
895            $context = Context::getContext();
896        }
897        if (!static::isFeatureActive() || !Validate::isLoadedObject($context->cart)) {
898            return;
899        }
900
901        $sql = '
902		SELECT SQL_NO_CACHE cr.*
903		FROM '._DB_PREFIX_.'cart_rule cr
904		LEFT JOIN '._DB_PREFIX_.'cart_rule_shop crs ON cr.id_cart_rule = crs.id_cart_rule
905		'.(!$context->customer->id && Group::isFeatureActive() ? ' LEFT JOIN '._DB_PREFIX_.'cart_rule_group crg ON cr.id_cart_rule = crg.id_cart_rule' : '').'
906		LEFT JOIN '._DB_PREFIX_.'cart_rule_carrier crca ON cr.id_cart_rule = crca.id_cart_rule
907		'.($context->cart->id_carrier ? 'LEFT JOIN '._DB_PREFIX_.'carrier c ON (c.id_reference = crca.id_carrier AND c.deleted = 0)' : '').'
908		LEFT JOIN '._DB_PREFIX_.'cart_rule_country crco ON cr.id_cart_rule = crco.id_cart_rule
909		WHERE cr.active = 1
910		AND cr.code = ""
911		AND cr.quantity > 0
912		AND cr.date_from < "'.date('Y-m-d H:i:s').'"
913		AND cr.date_to > "'.date('Y-m-d H:i:s').'"
914		AND (
915			cr.id_customer = 0
916			'.($context->customer->id ? 'OR cr.id_customer = '.(int) $context->cart->id_customer : '').'
917		)
918		AND (
919			cr.`carrier_restriction` = 0
920			'.($context->cart->id_carrier ? 'OR c.id_carrier = '.(int) $context->cart->id_carrier : '').'
921		)
922		AND (
923			cr.`shop_restriction` = 0
924			'.((Shop::isFeatureActive() && $context->shop->id) ? 'OR crs.id_shop = '.(int) $context->shop->id : '').'
925		)
926		AND (
927			cr.`group_restriction` = 0
928			'.($context->customer->id ? 'OR EXISTS (
929				SELECT 1
930				FROM `'._DB_PREFIX_.'customer_group` cg
931				INNER JOIN `'._DB_PREFIX_.'cart_rule_group` crg ON cg.id_group = crg.id_group
932				WHERE cr.`id_cart_rule` = crg.`id_cart_rule`
933				AND cg.`id_customer` = '.(int) $context->customer->id.'
934				LIMIT 1
935			)' : (Group::isFeatureActive() ? 'OR crg.`id_group` = '.(int) Configuration::get('PS_UNIDENTIFIED_GROUP') : '')).'
936		)
937		AND (
938			cr.`reduction_product` <= 0
939			OR EXISTS (
940				SELECT 1
941				FROM `'._DB_PREFIX_.'cart_product`
942				WHERE `'._DB_PREFIX_.'cart_product`.`id_product` = cr.`reduction_product` AND `id_cart` = '.(int) $context->cart->id.'
943			)
944		)
945		AND NOT EXISTS (SELECT 1 FROM '._DB_PREFIX_.'cart_cart_rule WHERE cr.id_cart_rule = '._DB_PREFIX_.'cart_cart_rule.id_cart_rule
946																			AND id_cart = '.(int) $context->cart->id.')
947		ORDER BY priority';
948        $result = Db::getInstance()->executeS($sql, true, false);
949        if ($result) {
950            $cartRules = ObjectModel::hydrateCollection('CartRule', $result);
951            if ($cartRules) {
952                foreach ($cartRules as $cartRule) {
953                    /** @var CartRule $cartRule */
954                    if ($cartRule->checkValidity($context, false, false)) {
955                        $context->cart->addCartRule($cartRule->id);
956                    }
957                }
958            }
959        }
960    }
961
962    /**
963     * Check if this cart rule can be applied
964     *
965     * @param Context $context
966     * @param bool    $alreadyInCart Check if the voucher is already on the cart
967     * @param bool    $displayError  Display error
968     *
969     * @return bool|mixed|string
970     *
971     * @since   1.0.0
972     * @version 1.0.0 Initial version
973     * @throws PrestaShopException
974     */
975    public function checkValidity(Context $context, $alreadyInCart = false, $displayError = true, $checkCarrier = true)
976    {
977        if (!static::isFeatureActive()) {
978            return false;
979        }
980
981        if (!$this->active) {
982            return (!$displayError) ? false : Tools::displayError('This voucher is disabled');
983        }
984        if (!$this->quantity) {
985            return (!$displayError) ? false : Tools::displayError('This voucher has already been used');
986        }
987        if (strtotime($this->date_from) > time()) {
988            return (!$displayError) ? false : Tools::displayError('This voucher is not valid yet');
989        }
990        if (strtotime($this->date_to) < time()) {
991            return (!$displayError) ? false : Tools::displayError('This voucher has expired');
992        }
993
994        if ($context->cart->id_customer) {
995            $quantityUsed = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
996                (new DbQuery())
997                    ->select('COUNT(*)')
998                    ->from('orders', 'o')
999                    ->leftJoin('order_cart_rule', 'od', 'od.`id_order` = o.`id_order`')
1000                    ->where('o.`id_customer` = '.(int) $context->cart->id_customer)
1001                    ->where('od.`id_cart_rule` = '.(int) $this->id)
1002                    ->where('o.`current_state` != '.(int) Configuration::get('PS_OS_ERROR'))
1003            );
1004            if ($quantityUsed + 1 > $this->quantity_per_user) {
1005                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher anymore (usage limit reached)');
1006            }
1007        }
1008
1009        // Get an intersection of the customer groups and the cart rule groups (if the customer is not logged in, the default group is Visitors)
1010        if ($this->group_restriction) {
1011            $idCartRule = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
1012                (new DbQuery())
1013                    ->select('crg.`id_cart_rule`')
1014                    ->from('cart_rule_group', 'crg')
1015                    ->where('crg.`id_cart_rule` = '.(int) $this->id)
1016                    ->where('crg.`id_group` '.($context->cart->id_customer ? 'IN (SELECT cg.id_group FROM '._DB_PREFIX_.'customer_group cg WHERE cg.id_customer = '.(int) $context->cart->id_customer.')' : '= '.(int) Configuration::get('PS_UNIDENTIFIED_GROUP')))
1017            );
1018            if (!$idCartRule) {
1019                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher');
1020            }
1021        }
1022
1023        // Check if the customer delivery address is usable with the cart rule
1024        if ($this->country_restriction) {
1025            if (!$context->cart->id_address_delivery) {
1026                return (!$displayError) ? false : Tools::displayError('You must choose a delivery address before applying this voucher to your order');
1027            }
1028            $idCartRule = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
1029                (new DbQuery())
1030                    ->select('crc.`id_cart_rule`')
1031                    ->from('cart_rule_country', 'crc')
1032                    ->where('crc.`id_cart_rule` = '.(int) $this->id)
1033                    ->where('crc.`id_country`  = (SELECT a.id_country FROM '._DB_PREFIX_.'address a WHERE a.id_address = '.(int) $context->cart->id_address_delivery.' LIMIT 1)')
1034            );
1035            if (!$idCartRule) {
1036                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher in your country of delivery');
1037            }
1038        }
1039
1040        // Check if the carrier chosen by the customer is usable with the cart rule
1041        if ($this->carrier_restriction && $checkCarrier) {
1042            if (!$context->cart->id_carrier) {
1043                return (!$displayError) ? false : Tools::displayError('You must choose a carrier before applying this voucher to your order');
1044            }
1045            $idCartRule = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
1046                (new DbQuery())
1047                    ->select('crc.`id_cart_rule`')
1048                    ->from('cart_rule_carrier', 'crc')
1049                    ->innerJoin('carrier', 'c', 'c.`id_reference` = crc.`id_carrier` AND c.`deleted` = 0')
1050                    ->where('crc.`id_cart_rule` = '.(int) $this->id)
1051                    ->where('c.`id_carrier` = '.(int) $context->cart->id_carrier)
1052            );
1053            if (!$idCartRule) {
1054                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher with this carrier');
1055            }
1056        }
1057
1058        // Check if the cart rules appliy to the shop browsed by the customer
1059        if ($this->shop_restriction && $context->shop->id && Shop::isFeatureActive()) {
1060            $idCartRule = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
1061                (new DbQuery())
1062                    ->select('crs.`id_cart_rule`')
1063                    ->from('cart_rule_shop', 'crs')
1064                    ->where('crs.`id_cart_rule` = '.(int) $this->id)
1065                    ->where('crs.`id_shop` = '.(int) $context->shop->id)
1066            );
1067            if (!$idCartRule) {
1068                return (!$displayError) ? false : Tools::displayError('You cannot use this voucher');
1069            }
1070        }
1071
1072        // Check if the products chosen by the customer are usable with the cart rule
1073        if ($this->product_restriction) {
1074            $r = $this->checkProductRestrictions($context, false, $displayError, $alreadyInCart);
1075            if ($r !== false && $displayError) {
1076                return $r;
1077            } elseif (!$r && !$displayError) {
1078                return false;
1079            }
1080        }
1081
1082        // Check if the cart rule is only usable by a specific customer, and if the current customer is the right one
1083        if ($this->id_customer && $context->cart->id_customer != $this->id_customer) {
1084            if (!Context::getContext()->customer->isLogged()) {
1085                return (!$displayError) ? false : (Tools::displayError('You cannot use this voucher').' - '.Tools::displayError('Please log in first'));
1086            }
1087
1088            return (!$displayError) ? false : Tools::displayError('You cannot use this voucher');
1089        }
1090
1091        if ($this->minimum_amount && $checkCarrier) {
1092            // Minimum amount is converted to the contextual currency
1093            $minimumAmount = $this->minimum_amount;
1094            if ($this->minimum_amount_currency != Context::getContext()->currency->id) {
1095                $minimumAmount = Tools::convertPriceFull($minimumAmount, new Currency($this->minimum_amount_currency), Context::getContext()->currency);
1096            }
1097
1098            $cartTotal = $context->cart->getOrderTotal($this->minimum_amount_tax, Cart::ONLY_PRODUCTS);
1099            if ($this->minimum_amount_shipping) {
1100                $cartTotal += $context->cart->getOrderTotal($this->minimum_amount_tax, Cart::ONLY_SHIPPING);
1101            }
1102            $products = $context->cart->getProducts();
1103            $cartRules = $context->cart->getCartRules();
1104
1105            foreach ($cartRules as &$cartRule) {
1106                if ($cartRule['gift_product']) {
1107                    foreach ($products as $key => &$product) {
1108                        if (empty($product['gift'])
1109                            && $product['id_product']
1110                               == $cartRule['gift_product']
1111                            && $product['id_product_attribute']
1112                               == $cartRule['gift_product_attribute']) {
1113                            if ($this->minimum_amount_tax) {
1114                                $cartTotal = $cartTotal - $product['price_wt'];
1115                            } else {
1116                                $cartTotal = $cartTotal - $product['price'];
1117                            }
1118                        }
1119                    }
1120                }
1121            }
1122
1123            if ($cartTotal < $minimumAmount) {
1124                return (!$displayError) ? false : Tools::displayError('You have not reached the minimum amount required to use this voucher');
1125            }
1126        }
1127
1128        /* This loop checks:
1129            - if the voucher is already in the cart
1130            - if a non compatible voucher is in the cart
1131            - if there are products in the cart (gifts excluded)
1132            Important note: this MUST be the last check, because if the tested cart rule has priority over a non combinable one in the cart, we will switch them
1133        */
1134        $nbProducts = Cart::getNbProducts($context->cart->id);
1135        $otherCartRules = [];
1136        if ($checkCarrier) {
1137            $otherCartRules = $context->cart->getCartRules();
1138        }
1139        if (count($otherCartRules)) {
1140            foreach ($otherCartRules as $otherCartRule) {
1141                if ($otherCartRule['id_cart_rule'] == $this->id && !$alreadyInCart) {
1142                    return (!$displayError) ? false : Tools::displayError('This voucher is already in your cart');
1143                }
1144                if ($otherCartRule['gift_product']) {
1145                    --$nbProducts;
1146                }
1147
1148                if ($this->cart_rule_restriction && $otherCartRule['cart_rule_restriction'] && $otherCartRule['id_cart_rule'] != $this->id) {
1149                    $combinable = Db::getInstance()->getValue(
1150                        '
1151					SELECT id_cart_rule_1
1152					FROM '._DB_PREFIX_.'cart_rule_combination
1153					WHERE (id_cart_rule_1 = '.(int) $this->id.' AND id_cart_rule_2 = '.(int) $otherCartRule['id_cart_rule'].')
1154					OR (id_cart_rule_2 = '.(int) $this->id.' AND id_cart_rule_1 = '.(int) $otherCartRule['id_cart_rule'].')'
1155                    );
1156                    if (!$combinable) {
1157                        $cartRule = new CartRule((int) $otherCartRule['id_cart_rule'], $context->cart->id_lang);
1158                        // The cart rules are not combinable and the cart rule currently in the cart has priority over the one tested
1159                        if ($cartRule->priority <= $this->priority) {
1160                            return (!$displayError) ? false : Tools::displayError('This voucher is not combinable with an other voucher already in your cart:').' '.$cartRule->name;
1161                        } // But if the cart rule that is tested has priority over the one in the cart, we remove the one in the cart and keep this new one
1162                        else {
1163                            $context->cart->removeCartRule($cartRule->id);
1164                        }
1165                    }
1166                }
1167            }
1168        }
1169
1170        if (!$nbProducts) {
1171            return (!$displayError) ? false : Tools::displayError('Cart is empty');
1172        }
1173
1174        if (!$displayError) {
1175            return true;
1176        }
1177    }
1178
1179    /**
1180     * @param $type
1181     * @param $list
1182     *
1183     * @return bool
1184     *
1185     * @since   1.0.0
1186     * @version 1.0.0 Initial version
1187     * @throws PrestaShopException
1188     * @throws PrestaShopDatabaseException
1189     */
1190    public static function cleanProductRuleIntegrity($type, $list)
1191    {
1192        // Type must be available in the 'type' enum of the table cart_rule_product_rule
1193        if (!in_array($type, ['products', 'categories', 'attributes', 'manufacturers', 'suppliers'])) {
1194            return false;
1195        }
1196
1197        // This check must not be removed because this var is used a few lines below
1198        $list = (is_array($list) ? implode(',', array_map('intval', $list)) : (int) $list);
1199        if (!preg_match('/^[0-9,]+$/', $list)) {
1200            return false;
1201        }
1202
1203        // Delete associated restrictions on cart rules
1204        Db::getInstance()->execute(
1205            '
1206		DELETE crprv
1207		FROM `'._DB_PREFIX_.'cart_rule_product_rule` crpr
1208		LEFT JOIN `'._DB_PREFIX_.'cart_rule_product_rule_value` crprv ON crpr.`id_product_rule` = crprv.`id_product_rule`
1209		WHERE crpr.`type` = "'.pSQL($type).'"
1210		AND crprv.`id_item` IN ('.$list.')'
1211        ); // $list is checked a few lines above
1212
1213        // Delete the product rules that does not have any values
1214        if (Db::getInstance()->Affected_Rows() > 0) {
1215            Db::getInstance()->delete(
1216                'cart_rule_product_rule', 'NOT EXISTS (SELECT 1 FROM `'._DB_PREFIX_.'cart_rule_product_rule_value`
1217																							WHERE `'._DB_PREFIX_.'cart_rule_product_rule`.`id_product_rule` = `'._DB_PREFIX_.'cart_rule_product_rule_value`.`id_product_rule`)'
1218            );
1219        }
1220        // If the product rules were the only conditions of a product rule group, delete the product rule group
1221        if (Db::getInstance()->Affected_Rows() > 0) {
1222            Db::getInstance()->delete(
1223                'cart_rule_product_rule_group', 'NOT EXISTS (SELECT 1 FROM `'._DB_PREFIX_.'cart_rule_product_rule`
1224																						WHERE `'._DB_PREFIX_.'cart_rule_product_rule`.`id_product_rule_group` = `'._DB_PREFIX_.'cart_rule_product_rule_group`.`id_product_rule_group`)'
1225            );
1226        }
1227
1228        // If the product rule group were the only restrictions of a cart rule, update de cart rule restriction cache
1229        if (Db::getInstance()->Affected_Rows() > 0) {
1230            Db::getInstance()->execute(
1231                '
1232				UPDATE `'._DB_PREFIX_.'cart_rule` cr
1233				LEFT JOIN `'._DB_PREFIX_.'cart_rule_product_rule_group` crprg ON cr.id_cart_rule = crprg.id_cart_rule
1234				SET product_restriction = IF(crprg.id_product_rule_group IS NULL, 0, 1)'
1235            );
1236        }
1237
1238        return true;
1239    }
1240
1241    /**
1242     * @param string $name
1243     * @param int    $idLang
1244     *
1245     * @param bool   $extended
1246     *
1247     * @return array
1248     *
1249     * @throws PrestaShopDatabaseException
1250     * @throws PrestaShopException
1251     * @since   1.0.0
1252     * @version 1.0.0 Initial version
1253     */
1254    public static function getCartsRuleByCode($name, $idLang, $extended = false)
1255    {
1256        $sqlBase = 'SELECT cr.*, crl.*
1257						FROM '._DB_PREFIX_.'cart_rule cr
1258						LEFT JOIN '._DB_PREFIX_.'cart_rule_lang crl ON (cr.id_cart_rule = crl.id_cart_rule AND crl.id_lang = '.(int) $idLang.')';
1259        if ($extended) {
1260            return Db::getInstance()->executeS('('.$sqlBase.' WHERE code LIKE \'%'.pSQL($name).'%\') UNION ('.$sqlBase.' WHERE name LIKE \'%'.pSQL($name).'%\')');
1261        } else {
1262            return Db::getInstance()->executeS($sqlBase.' WHERE code LIKE \'%'.pSQL($name).'%\'');
1263        }
1264    }
1265
1266    /**
1267     * @see     ObjectModel::add()
1268     *
1269     * @since   1.0.0
1270     * @version 1.0.0 Initial version
1271     * @throws PrestaShopException
1272     */
1273    public function add($autoDate = true, $nullValues = false)
1274    {
1275        if (!$this->reduction_currency) {
1276            $this->reduction_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT');
1277        }
1278
1279        if (!parent::add($autoDate, $nullValues)) {
1280            return false;
1281        }
1282
1283        Configuration::updateGlobalValue('PS_CART_RULE_FEATURE_ACTIVE', '1');
1284
1285        return true;
1286    }
1287
1288    /**
1289     * @param bool $nullValues
1290     *
1291     * @return bool
1292     *
1293     * @since   1.0.0
1294     * @version 1.0.0 Initial version
1295     * @throws PrestaShopException
1296     */
1297    public function update($nullValues = false)
1298    {
1299        Cache::clean('getContextualValue_'.$this->id.'_*');
1300
1301        if (!$this->reduction_currency) {
1302            $this->reduction_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT');
1303        }
1304
1305        return parent::update($nullValues);
1306    }
1307
1308    /**
1309     * @see     ObjectModel::delete()
1310     *
1311     * @since   1.0.0
1312     * @version 1.0.0 Initial version
1313     * @throws PrestaShopException
1314     */
1315    public function delete()
1316    {
1317        if (!parent::delete()) {
1318            return false;
1319        }
1320
1321        Configuration::updateGlobalValue('PS_CART_RULE_FEATURE_ACTIVE', static::isCurrentlyUsed($this->def['table'], true));
1322
1323        $r = Db::getInstance()->delete('cart_cart_rule', '`id_cart_rule` = '.(int) $this->id);
1324        $r &= Db::getInstance()->delete('cart_rule_carrier', '`id_cart_rule` = '.(int) $this->id);
1325        $r &= Db::getInstance()->delete('cart_rule_shop', '`id_cart_rule` = '.(int) $this->id);
1326        $r &= Db::getInstance()->delete('cart_rule_group', '`id_cart_rule` = '.(int) $this->id);
1327        $r &= Db::getInstance()->delete('cart_rule_country', '`id_cart_rule` = '.(int) $this->id);
1328        $r &= Db::getInstance()->delete('cart_rule_combination', '`id_cart_rule_1` = '.(int) $this->id.' OR `id_cart_rule_2` = '.(int) $this->id);
1329        $r &= Db::getInstance()->delete('cart_rule_product_rule_group', '`id_cart_rule` = '.(int) $this->id);
1330        $r &= Db::getInstance()->delete(
1331            'cart_rule_product_rule', 'NOT EXISTS (SELECT 1 FROM `'._DB_PREFIX_.'cart_rule_product_rule_group`
1332			WHERE `'._DB_PREFIX_.'cart_rule_product_rule`.`id_product_rule_group` = `'._DB_PREFIX_.'cart_rule_product_rule_group`.`id_product_rule_group`)'
1333        );
1334        $r &= Db::getInstance()->delete(
1335            'cart_rule_product_rule_value', 'NOT EXISTS (SELECT 1 FROM `'._DB_PREFIX_.'cart_rule_product_rule`
1336			WHERE `'._DB_PREFIX_.'cart_rule_product_rule_value`.`id_product_rule` = `'._DB_PREFIX_.'cart_rule_product_rule`.`id_product_rule`)'
1337        );
1338
1339        return $r;
1340    }
1341
1342    /**
1343     * @param int $idCustomer
1344     *
1345     * @return bool
1346     *
1347     * @since   1.0.0
1348     * @version 1.0.0 Initial version
1349     * @throws PrestaShopException
1350     */
1351    public function usedByCustomer($idCustomer)
1352    {
1353        return (bool) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
1354            (new DbQuery())
1355                ->select('`id_cart_rule`')
1356                ->from('order_cart_rule', 'ocr')
1357                ->leftJoin('orders', 'o', 'ocr.`id_order` = o.`id_order`')
1358                ->where('ocr.`id_cart_rule` = '.(int) $this->id)
1359                ->where('o.`id_customer` = '.(int) $idCustomer)
1360        );
1361    }
1362
1363    /**
1364     * The reduction value is POSITIVE
1365     *
1366     * @param bool    $useTax
1367     * @param Context $context
1368     * @param null    $filter
1369     * @param null    $package
1370     * @param bool    $useCache Allow using cache to avoid multiple free gift using multishipping
1371     *
1372     * @return float|int|string
1373     * @throws Adapter_Exception
1374     * @throws PrestaShopDatabaseException
1375     * @throws PrestaShopException
1376     * @since   1.0.0
1377     * @version 1.0.0 Initial version
1378     */
1379    public function getContextualValue($useTax, Context $context = null, $filter = null, $package = null, $useCache = true)
1380    {
1381        if (!static::isFeatureActive()) {
1382            return 0;
1383        }
1384        if (!$context) {
1385            $context = Context::getContext();
1386        }
1387        if (!$filter) {
1388            $filter = static::FILTER_ACTION_ALL;
1389        }
1390        $roundType = (int) Configuration::get('PS_ROUND_TYPE');
1391        $displayDecimals = 0;
1392        if ($context->currency->decimals) {
1393            $displayDecimals = Configuration::get('PS_PRICE_DISPLAY_PRECISION');
1394        }
1395
1396        $allProducts = $context->cart->getProducts();
1397        $packageProducts = (is_null($package) ? $allProducts : $package['products']);
1398
1399        $reductionValue = 0;
1400
1401        $cacheId = 'getContextualValue_'.(int) $this->id.'_'.(int) $useTax.'_'.(int) $context->cart->id.'_'.(int) $filter;
1402        foreach ($packageProducts as $product) {
1403            $cacheId .= '_'.(int) $product['id_product'].'_'.(int) $product['id_product_attribute'].(isset($product['in_stock']) ? '_'.(int) $product['in_stock'] : '');
1404        }
1405
1406        if (Cache::isStored($cacheId)) {
1407            return Cache::retrieve($cacheId);
1408        }
1409
1410        $allCartRulesIds = $context->cart->getOrderedCartRulesIds();
1411
1412        $cartAmountTaxIncluded = $context->cart->getOrderTotal(true, Cart::ONLY_PRODUCTS);
1413        $cartAmountTaxExcluded = $context->cart->getOrderTotal(false, Cart::ONLY_PRODUCTS);
1414
1415        // Free shipping on selected carriers
1416        if ($this->free_shipping && in_array($filter, [static::FILTER_ACTION_ALL, static::FILTER_ACTION_ALL_NOCAP, static::FILTER_ACTION_SHIPPING])) {
1417            if (!$this->carrier_restriction) {
1418                $reductionValue += $context->cart->getOrderTotal($useTax, Cart::ONLY_SHIPPING, is_null($package) ? null : $package['products'], is_null($package) ? null : $package['id_carrier']);
1419            } else {
1420                $data = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
1421                    (new DbQuery())
1422                        ->select('crc.`id_cart_rule`, c.`id_carrier`')
1423                        ->from('cart_rule_carrier', 'crc')
1424                        ->innerJoin('carrier', 'c', 'c.`id_reference` = crc.`id_carrier` AND c.`deleted` = 0')
1425                        ->where('crc.`id_cart_rule` = '.(int) $this->id)
1426                        ->where('c.`id_carrier` = '.(int) $context->cart->id_carrier)
1427                );
1428
1429                if ($data) {
1430                    foreach ($data as $cartRule) {
1431                        $reductionValue += $context->cart->getCarrierCost((int) $cartRule['id_carrier'], $useTax, $context->country);
1432                    }
1433                }
1434            }
1435        }
1436
1437        if (in_array($filter, [static::FILTER_ACTION_ALL, static::FILTER_ACTION_ALL_NOCAP, static::FILTER_ACTION_REDUCTION])) {
1438            // Discount (%) on the whole order
1439            if ($this->reduction_percent && $this->reduction_product == 0) {
1440                // Do not give a reduction on free products!
1441                $orderTotal = $context->cart->getOrderTotal($useTax, Cart::ONLY_PRODUCTS, $packageProducts);
1442                foreach ($context->cart->getCartRules(static::FILTER_ACTION_GIFT) as $cartRule) {
1443                    $reduction = $cartRule['obj']->getContextualValue(
1444                        $useTax, $context,
1445                        static::FILTER_ACTION_GIFT, $package
1446                    );
1447                    if ($roundType === Order::ROUND_ITEM) {
1448                        $reduction = round($reduction, $displayDecimals);
1449                    }
1450                    $orderTotal -= $reduction;
1451                }
1452
1453                $reductionValue += round(
1454                    $orderTotal * $this->reduction_percent / 100,
1455                    _TB_PRICE_DATABASE_PRECISION_
1456                );
1457            }
1458
1459            // Discount (%) on a specific product
1460            if ($this->reduction_percent && $this->reduction_product > 0) {
1461                foreach ($packageProducts as $product) {
1462                    if ($product['id_product'] == $this->reduction_product) {
1463                        if ($useTax == true) {
1464                            $reduction = $product['total_wt'];
1465                        } else {
1466                            $reduction = $product['total'];
1467                        }
1468                        $reductionValue += round(
1469                            $reduction * $this->reduction_percent / 100,
1470                            _TB_PRICE_DATABASE_PRECISION_
1471                        );
1472                    }
1473                }
1474            }
1475
1476            // Discount (%) on the cheapest product
1477            if ($this->reduction_percent && $this->reduction_product == -1) {
1478                $minPrice = false;
1479                $cheapestProduct = null;
1480                $selectedProducts = $this->checkProductRestrictions($context, true);
1481                foreach ($allProducts as $product) {
1482                    if (!is_array($selectedProducts) ||
1483                        (!in_array($product['id_product'].'-'.$product['id_product_attribute'], $selectedProducts) && !in_array($product['id_product'].'-0', $selectedProducts))
1484                    ) {
1485                        continue;
1486                    }
1487
1488                    $price = $product['price'];
1489                    if ($useTax == true) {
1490                        $price = round(
1491                            $price * (1 + $product['rate'] / 100),
1492                            _TB_PRICE_DATABASE_PRECISION_
1493                        );
1494                    }
1495
1496                    if ($price > 0 && ($minPrice === false || $minPrice > $price)) {
1497                        $minPrice = $price;
1498                        $cheapestProduct = $product['id_product'].'-'.$product['id_product_attribute'];
1499                    }
1500                }
1501
1502                // Check if the cheapest product is in the package
1503                $inPackage = false;
1504                foreach ($packageProducts as $product) {
1505                    if ($product['id_product'].'-'.$product['id_product_attribute'] == $cheapestProduct || $product['id_product'].'-0' == $cheapestProduct) {
1506                        $inPackage = true;
1507                    }
1508                }
1509                if ($inPackage) {
1510                    $reductionValue += round(
1511                        $minPrice * $this->reduction_percent / 100,
1512                        _TB_PRICE_DATABASE_PRECISION_
1513                    );
1514                }
1515            }
1516
1517            // Discount (%) on the selection of products
1518            if ($this->reduction_percent && $this->reduction_product == -2) {
1519                $selectedProductsReduction = 0;
1520                $selectedProducts = $this->checkProductRestrictions($context, true);
1521                if (is_array($selectedProducts)) {
1522                    foreach ($packageProducts as $product) {
1523                        if (in_array($product['id_product'].'-'.$product['id_product_attribute'], $selectedProducts)
1524                            || in_array($product['id_product'].'-0', $selectedProducts)
1525                        ) {
1526                            $price = $product['price'];
1527                            if ($useTax == true) {
1528                                $price = round(
1529                                    $price * (1 + $product['rate'] / 100),
1530                                    _TB_PRICE_DATABASE_PRECISION_
1531                                );
1532                            }
1533
1534                            $selectedProductsReduction += $price * $product['cart_quantity'];
1535                        }
1536                    }
1537                }
1538                $reductionValue += round(
1539                    $selectedProductsReduction * $this->reduction_percent / 100,
1540                    _TB_PRICE_DATABASE_PRECISION_
1541                );
1542            }
1543
1544            // Discount (¤)
1545            if ($this->reduction_amount > 0) {
1546                $prorata = 1;
1547                if (!is_null($package) && count($allProducts)) {
1548                    $totalProducts = $context->cart->getOrderTotal($useTax, Cart::ONLY_PRODUCTS);
1549                    if ($totalProducts) {
1550                        $prorata = $context->cart->getOrderTotal($useTax, Cart::ONLY_PRODUCTS, $package['products']) / $totalProducts;
1551                    }
1552                }
1553
1554                $reductionAmount = $this->reduction_amount;
1555                $voucherCurrency = new Currency($this->reduction_currency);
1556                // First we convert the voucher value to the default currency.
1557                $reductionAmount = Tools::convertPrice(
1558                    $reductionAmount,
1559                    $voucherCurrency,
1560                    false
1561                );
1562                // Then we convert the voucher value to the cart currency.
1563                $reductionAmount = Tools::convertPrice(
1564                    $reductionAmount,
1565                    $context->currency,
1566                    true
1567                );
1568
1569                // If it has the same tax application that you need, then it's the right value, whatever the product!
1570                if ($this->reduction_tax == $useTax) {
1571                    // The reduction cannot exceed the products total, except when we do not want it to be limited (for the partial use calculation)
1572                    if ($filter != static::FILTER_ACTION_ALL_NOCAP) {
1573                        $cartAmount = $context->cart->getOrderTotal($useTax, Cart::ONLY_PRODUCTS);
1574                        $reductionAmount = min($reductionAmount, $cartAmount);
1575                    }
1576                    $reductionValue += $prorata * $reductionAmount;
1577                } else {
1578                    if ($this->reduction_product > 0) {
1579                        foreach ($context->cart->getProducts() as $product) {
1580                            if ($product['id_product'] == $this->reduction_product) {
1581                                $productPriceTaxIncluded = $product['price_wt'];
1582                                $productPriceTaxExcluded = $product['price'];
1583                                $productVatAmount = $productPriceTaxIncluded - $productPriceTaxExcluded;
1584
1585                                if ($productVatAmount == 0 || $productPriceTaxExcluded == 0) {
1586                                    $productVatRate = 0;
1587                                } else {
1588                                    $productVatRate = $productVatAmount / $productPriceTaxExcluded;
1589                                }
1590
1591                                if ($this->reduction_tax && !$useTax) {
1592                                    $reductionValue += round(
1593                                        $prorata * $reductionAmount
1594                                        / (1 + $productVatRate),
1595                                        _TB_PRICE_DATABASE_PRECISION_
1596                                    );
1597                                } elseif (!$this->reduction_tax && $useTax) {
1598                                    $reductionValue += round(
1599                                        $prorata * $reductionAmount
1600                                        * (1 + $productVatRate),
1601                                        _TB_PRICE_DATABASE_PRECISION_
1602                                    );
1603                                }
1604                            }
1605                        }
1606                    } // Discount (¤) on the whole order
1607                    elseif ($this->reduction_product == 0) {
1608                        $cartAmountTaxExcluded = null;
1609                        $cartAmountTaxIncluded = null;
1610                        $cartAverageVatRate = $context->cart->getAverageProductsTaxRate($cartAmountTaxExcluded, $cartAmountTaxIncluded);
1611
1612                        // The reduction cannot exceed the products total, except when we do not want it to be limited (for the partial use calculation)
1613                        if ($filter != static::FILTER_ACTION_ALL_NOCAP) {
1614                            $reductionAmount = min($reductionAmount, $this->reduction_tax ? $cartAmountTaxIncluded : $cartAmountTaxExcluded);
1615                        }
1616
1617                        if ($this->reduction_tax && !$useTax) {
1618                            $reductionValue += round(
1619                                $prorata * $reductionAmount
1620                                / (1 + $cartAverageVatRate),
1621                                _TB_PRICE_DATABASE_PRECISION_
1622                            );
1623                        } elseif (!$this->reduction_tax && $useTax) {
1624                            $reductionValue += round(
1625                                $prorata * $reductionAmount
1626                                * (1 + $cartAverageVatRate),
1627                                _TB_PRICE_DATABASE_PRECISION_
1628                            );
1629                        }
1630                    }
1631                    /*
1632                     * Reduction on the cheapest or on the selection is not really meaningful and has been disabled in the backend
1633                     * Please keep this code, so it won't be considered as a bug
1634                     * elseif ($this->reduction_product == -1)
1635                     * elseif ($this->reduction_product == -2)
1636                    */
1637                }
1638
1639                // Take care of the other cart rules values if the filter allow it
1640                if ($filter != static::FILTER_ACTION_ALL_NOCAP) {
1641                    // Cart values
1642                    $cart = Context::getContext()->cart;
1643
1644                    if (!Validate::isLoadedObject($cart)) {
1645                        $cart = new Cart();
1646                    }
1647
1648                    $cartAverageVatRate = $cart->getAverageProductsTaxRate();
1649                    $currentCartAmount = $useTax ? $cartAmountTaxIncluded : $cartAmountTaxExcluded;
1650
1651                    foreach ($allCartRulesIds as $currentCartRuleId) {
1652                        if ((int) $currentCartRuleId['id_cart_rule'] == (int) $this->id) {
1653                            break;
1654                        }
1655
1656                        $previousCartRule = new CartRule((int) $currentCartRuleId['id_cart_rule']);
1657                        $previousReductionAmount = $previousCartRule->reduction_amount;
1658
1659                        if ($previousCartRule->reduction_tax && !$useTax) {
1660                            $previousReductionAmount = round(
1661                                $previousReductionAmount
1662                                * $prorata
1663                                / (1 + $cartAverageVatRate),
1664                                _TB_PRICE_DATABASE_PRECISION_
1665                            );
1666                        } elseif (!$previousCartRule->reduction_tax && $useTax) {
1667                            $previousReductionAmount = round(
1668                                $previousReductionAmount
1669                                * $prorata
1670                                * (1 + $cartAverageVatRate),
1671                                _TB_PRICE_DATABASE_PRECISION_
1672                            );
1673                        }
1674
1675                        $currentCartAmount = max($currentCartAmount - (float) $previousReductionAmount, 0);
1676                    }
1677
1678                    $reductionValue = min($reductionValue, $currentCartAmount);
1679                }
1680            }
1681
1682            if ($roundType === Order::ROUND_LINE) {
1683                $reductionValue = Tools::ps_round(
1684                    $reductionValue,
1685                    $displayDecimals
1686                );
1687            }
1688        }
1689
1690        // Free gift
1691        if ((int) $this->gift_product && in_array($filter, [static::FILTER_ACTION_ALL, static::FILTER_ACTION_ALL_NOCAP, static::FILTER_ACTION_GIFT])) {
1692            $idAddress = (is_null($package) ? 0 : $package['id_address']);
1693            foreach ($packageProducts as $product) {
1694                if ($product['id_product'] == $this->gift_product && ($product['id_product_attribute'] == $this->gift_product_attribute || !(int) $this->gift_product_attribute)) {
1695                    // The free gift coupon must be applied to one product only (needed for multi-shipping which manage multiple product lists)
1696                    if (!isset(static::$onlyOneGift[$this->id.'-'.$this->gift_product])
1697                        || static::$onlyOneGift[$this->id.'-'.$this->gift_product] == $idAddress
1698                        || static::$onlyOneGift[$this->id.'-'.$this->gift_product] == 0
1699                        || $idAddress == 0
1700                        || !$useCache
1701                    ) {
1702                        $reductionValue += ($useTax ? $product['price_wt'] : $product['price']);
1703                        if ($useCache && (!isset(static::$onlyOneGift[$this->id.'-'.$this->gift_product]) || static::$onlyOneGift[$this->id.'-'.$this->gift_product] == 0)) {
1704                            static::$onlyOneGift[$this->id.'-'.$this->gift_product] = $idAddress;
1705                        }
1706                        break;
1707                    }
1708                }
1709            }
1710        }
1711
1712        Cache::store($cacheId, $reductionValue);
1713
1714        return $reductionValue;
1715    }
1716
1717    /* When an entity associated to a product rule (product, category, attribute, supplier, manufacturer...) is deleted, the product rules must be updated */
1718
1719    /**
1720     * @param string $type
1721     * @param bool   $activeOnly
1722     * @param bool   $i18n
1723     * @param int    $offset
1724     * @param int    $limit
1725     * @param string $searchCartRuleName
1726     *
1727     * @return array|bool
1728     * @throws PrestaShopDatabaseException
1729     *
1730     * @since   1.0.0
1731     * @version 1.0.0 Initial version
1732     * @throws PrestaShopException
1733     * @throws PrestaShopException
1734     */
1735    public function getAssociatedRestrictions($type, $activeOnly, $i18n, $offset = null, $limit = null, $searchCartRuleName = '')
1736    {
1737        $array = ['selected' => [], 'unselected' => []];
1738
1739        if (!in_array($type, ['country', 'carrier', 'group', 'cart_rule', 'shop'])) {
1740            return false;
1741        }
1742
1743        $shopList = '';
1744        if ($type == 'shop') {
1745            $shops = Context::getContext()->employee->getAssociatedShops();
1746            if (count($shops)) {
1747                $shopList = ' AND t.id_shop IN ('.implode(array_map('intval', $shops), ',').') ';
1748            }
1749        }
1750
1751        if ($offset !== null && $limit !== null) {
1752            $sqlLimit = ' LIMIT '.(int) $offset.', '.(int) ($limit + 1);
1753        } else {
1754            $sqlLimit = '';
1755        }
1756
1757        if (!Validate::isLoadedObject($this) || $this->{$type.'_restriction'} == 0) {
1758            $array['selected'] = Db::getInstance()->executeS(
1759                '
1760			SELECT t.*'.($i18n ? ', tl.*' : '').', 1 as selected
1761			FROM `'._DB_PREFIX_.$type.'` t
1762			'.($i18n ? 'LEFT JOIN `'._DB_PREFIX_.$type.'_lang` tl ON (t.id_'.$type.' = tl.id_'.$type.' AND tl.id_lang = '.(int) Context::getContext()->language->id.')' : '').'
1763			WHERE 1
1764			'.($activeOnly ? 'AND t.active = 1' : '').'
1765			'.(in_array($type, ['carrier', 'shop']) ? ' AND t.deleted = 0' : '').'
1766			'.($type == 'cart_rule' ? 'AND t.id_cart_rule != '.(int) $this->id : '').
1767                $shopList.
1768                (in_array($type, ['carrier', 'shop']) ? ' ORDER BY t.name ASC ' : '').
1769                (in_array($type, ['country', 'group', 'cart_rule']) && $i18n ? ' ORDER BY tl.name ASC ' : '').
1770                $sqlLimit
1771            );
1772        } else {
1773            if ($type == 'cart_rule') {
1774                $array = $this->getCartRuleCombinations($offset, $limit, $searchCartRuleName);
1775            } else {
1776                $resource = Db::getInstance()->executeS(
1777                    '
1778				SELECT t.*'.($i18n ? ', tl.*' : '').', IF(crt.id_'.$type.' IS NULL, 0, 1) as selected
1779				FROM `'._DB_PREFIX_.$type.'` t
1780				'.($i18n ? 'LEFT JOIN `'._DB_PREFIX_.$type.'_lang` tl ON (t.id_'.$type.' = tl.id_'.$type.' AND tl.id_lang = '.(int) Context::getContext()->language->id.')' : '').'
1781				LEFT JOIN (SELECT id_'.$type.' FROM `'._DB_PREFIX_.'cart_rule_'.$type.'` WHERE id_cart_rule = '.(int) $this->id.') crt ON t.id_'.($type == 'carrier' ? 'reference' : $type).' = crt.id_'.$type.'
1782				WHERE 1 '.($activeOnly ? ' AND t.active = 1' : '').
1783                    $shopList
1784                    .(in_array($type, ['carrier', 'shop']) ? ' AND t.deleted = 0' : '').
1785                    (in_array($type, ['carrier', 'shop']) ? ' ORDER BY t.name ASC ' : '').
1786                    (in_array($type, ['country', 'group', 'cart_rule']) && $i18n ? ' ORDER BY tl.name ASC ' : '').
1787                    $sqlLimit,
1788                    false
1789                );
1790                foreach ($resource as $row) {
1791                    $array[($row['selected'] || $this->{$type.'_restriction'} == 0) ? 'selected' : 'unselected'][] = $row;
1792                }
1793            }
1794        }
1795
1796        return $array;
1797    }
1798
1799    /**
1800     * Find the cheapest product
1801     *
1802     * @param array $package
1803     *
1804     * @return null|string
1805     *
1806     * @throws PrestaShopDatabaseException
1807     * @throws PrestaShopException
1808     * @since 1.0.2
1809     */
1810    public function findCheapestProduct($package)
1811    {
1812        $context = Context::getContext();
1813        $cheapestProduct = null;
1814        $allProducts = $package['products'];
1815
1816        if ($this->reduction_percent && $this->reduction_product == -1) {
1817            $minPrice = false;
1818            $selectedProducts = $this->checkProductRestrictions($context, true);
1819            foreach ($allProducts as $product) {
1820                if (!is_array($selectedProducts) ||
1821                    (!in_array($product['id_product'].'-'.$product['id_product_attribute'], $selectedProducts) && !in_array($product['id_product'].'-0', $selectedProducts))
1822                ) {
1823                    continue;
1824                }
1825
1826                $price = $product['price'];
1827                if ($price > 0 && ($minPrice === false || $minPrice > $price)) {
1828                    $minPrice = $price;
1829                    $cheapestProduct = $product['id_product'].'-'.$product['id_product_attribute'];
1830                }
1831            }
1832        }
1833
1834        return $cheapestProduct;
1835    }
1836
1837    /**
1838     * @param int    $offset
1839     * @param int    $limit
1840     * @param string $search
1841     *
1842     * @return array
1843     *
1844     * @throws PrestaShopDatabaseException
1845     * @throws PrestaShopException
1846     * @since   1.0.0
1847     * @version 1.0.0 Initial version
1848     */
1849    protected function getCartRuleCombinations($offset = null, $limit = null, $search = '')
1850    {
1851        $array = [];
1852        if ($offset !== null && $limit !== null) {
1853            $sqlLimit = ' LIMIT '.(int) $offset.', '.(int) ($limit + 1);
1854        } else {
1855            $sqlLimit = '';
1856        }
1857
1858        $array['selected'] = Db::getInstance()->executeS(
1859            '
1860		SELECT cr.*, crl.*, 1 AS selected
1861		FROM '._DB_PREFIX_.'cart_rule cr
1862		LEFT JOIN '._DB_PREFIX_.'cart_rule_lang crl ON (cr.id_cart_rule = crl.id_cart_rule AND crl.id_lang = '.(int) Context::getContext()->language->id.')
1863		WHERE cr.id_cart_rule != '.(int) $this->id.($search ? ' AND crl.name LIKE "%'.pSQL($search).'%"' : '').'
1864		AND (
1865			cr.cart_rule_restriction = 0
1866			OR EXISTS (
1867				SELECT 1
1868				FROM '._DB_PREFIX_.'cart_rule_combination
1869				WHERE cr.id_cart_rule = '._DB_PREFIX_.'cart_rule_combination.id_cart_rule_1 AND '.(int) $this->id.' = id_cart_rule_2
1870			)
1871			OR EXISTS (
1872				SELECT 1
1873				FROM '._DB_PREFIX_.'cart_rule_combination
1874				WHERE cr.id_cart_rule = '._DB_PREFIX_.'cart_rule_combination.id_cart_rule_2 AND '.(int) $this->id.' = id_cart_rule_1
1875			)
1876		) ORDER BY cr.id_cart_rule'.$sqlLimit
1877        );
1878
1879        $array['unselected'] = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
1880            (new DbQuery())
1881                ->select('cr.*, crl.*, 1 AS `selected`')
1882                ->from('cart_rule', 'cr')
1883                ->innerJoin('cart_rule_lang', 'crl', 'cr.`id_cart_rule` = crl.`id_cart_rule` AND crl.`id_lang` = '.(int) Context::getContext()->language->id)
1884                ->leftJoin('cart_rule_combination', 'crc1', 'cr.`id_cart_rule` = crc1.`id_cart_rule_1` AND crc1.`id_cart_rule_2` = '.(int) $this->id)
1885                ->leftJoin('cart_rule_combination', 'crc2', 'cr.`id_cart_rule` = crc2.`id_cart_rule_2` AND crc2.`id_cart_rule_1` = '.(int) $this->id)
1886                ->where('cr.`cart_rule_restriction` = 1')
1887                ->where('cr.`id_cart_rule` != '.(int) $this->id)
1888                ->where($search ? 'crl.`name` LIKE "%'.pSQL($search).'%"' : '')
1889                ->where('crc1.`id_cart_rule_1` IS NULL')
1890                ->where('crc2.`id_cart_rule_1` IS NULL')
1891                ->orderBy('cr.`id_cart_rule`')
1892                ->limit($limit, $offset)
1893        );
1894
1895        return $array;
1896    }
1897}
1898