Your IP : 3.136.17.89


Current Path : /home/bitrix/ext_www/klimatlend.ua/bitrix/modules/sale/lib/
Upload File :
Current File : /home/bitrix/ext_www/klimatlend.ua/bitrix/modules/sale/lib/discount.php

<?php
/**
 * Bitrix Framework
 * @package bitrix
 * @subpackage sale
 * @copyright 2001-2012 Bitrix
 */
namespace Bitrix\Sale;

use Bitrix\Main,
	Bitrix\Main\Localization\Loc,
	Bitrix\Sale\Compatible,
	Bitrix\Sale\Internals,
	Bitrix\Sale\Discount\Context,
	Bitrix\Sale\Discount\RuntimeCache;

Loc::loadMessages(__FILE__);

class Discount
{
	const EVENT_EXTEND_ORDER_DATA = 'onExtendOrderData';

	const APPLY_MODE_ADD = 0x0001;
	const APPLY_MODE_DISABLE = 0x0002;
	const APPLY_MODE_LAST = 0x0004;
	const APPLY_MODE_FULL_DISABLE = 0x0008;
	const APPLY_MODE_FULL_LAST = 0x0010;

	const USE_MODE_FULL = 0x00001;
	const USE_MODE_APPLY = 0x0002;
	const USE_MODE_MIXED = 0x0004;
	const USE_MODE_COUPONS = 0x0008;

	const ROUND_MODE_BASKET_DISCOUNT = 0x0001;
	const ROUND_MODE_SALE_DISCOUNT = 0x0002;
	const ROUND_MODE_FINAL_PRICE = 0x0004;

	const ERROR_ID = 'BX_SALE_DISCOUNT';

	/* Instances */
	/** @var array of Discount */
	private static $instances = array();

	/* Sale objects */
	/** @var Order|null */
	protected $order = null;
	/** @var Basket|null */
	protected $basket = null;
	/** @var null|Shipment $shipment */
	protected $shipment = null;
	/** @var array */
	protected $shipmentIds = array();

	/** @var  Context\BaseContext */
	protected $context;

	/* Calculate data */
	/** @var array|null */
	protected $orderData = null;

	/* Calculate options */
	/** @var bool */
	protected $newOrder = null;
	/** @var bool */
	protected $convertedOrder = null;
	/** @var int */
	protected $useMode = null;
	/** @var bool */
	protected $orderRefresh = false;
	/** @var array */
	protected $saleOptions = array();
	/** @var array */
	protected $executeModuleFilter = array('all', 'sale', 'catalog');

	/* Product discounts for basket items */
	/** @var array */
	protected $basketDiscountList = array();
	/* Various basket items data */
	/** @var array */
	protected $basketItemsData = array();

	/* Sale discount cache on hit */
	/** @var array|null */
	protected $discountIds = null;
	/** @var array */
	protected $discountByUserCache = array();
	/** @var array */
	protected $saleDiscountCache = array();
	/** @var string */
	protected $saleDiscountCacheKey = '';
	/** @var array */
	protected $cacheDiscountModules = array();

	/* Order discounts and coupons, converted to unified format */
	/** @var array */
	protected $discountsCache = array();
	/** @var array */
	protected $couponsCache = array();

	/* Calculation results and applyed flags */
	/** @var array */
	protected $discountResult = array();
	/** @var int */
	protected $discountResultCounter = 0;
	/** @var array */
	protected $applyResult = array();

	/* Contains additional data used to calculate discounts for an existing order */
	protected $discountStoredActionData = array();

	/** @var array */
	protected $loadedModules = array();
	/** @var array */
	protected $entityList = array();
	/** @var array */
	protected $entityResultCache = array();
	/** @var array */
	protected $currentStep = array();

	/** @var array */
	protected $forwardBasketTable = array();
	/** @var array */
	protected $reverseBasketTable = array();

	/* Round mode and data */
	protected $roundApplyMode = self::ROUND_MODE_FINAL_PRICE;
	protected $roundApplyConfig = array();

	/**
	 * Contains list of discounts which pass condition (@see method checkDiscountConditions()).
	 *
	 * @var array
	 */
	protected $fullDiscountList = array();

	/** @var bool  */
	protected $isClone = false;
	/** @var bool  */
	protected $enableCheckingPrediction = false;

	protected function __construct()
	{

	}

	/**
	 * Enables prediction checking instead real condition.
	 * @return void
	 */
	public function enableCheckingPrediction()
	{
		$this->enableCheckingPrediction = true;
		$this->saleOptions = array(
			'APPLY_MODE' => $this::APPLY_MODE_ADD,
		);
	}

	/**
	 * Disables prediction checking instead real condition.
	 * @return void
	 */
	public function disableCheckingPrediction()
	{
		$this->enableCheckingPrediction = false;
	}

	/**
	 * Get discount by fuser and site.
	 *
	 * @param string|int $fuser			Fuser id.
	 * @param string $site				Site id.
	 * @return null|Discount
	 */
	public static function loadByFuser($fuser, $site)
	{
		$instanceIndex = static::getInstanceIndexByFuser((int)$fuser, (string)$site);
		if (isset(self::$instances[$instanceIndex]))
			return self::$instances[$instanceIndex];

		return null;
	}

	/**
	 * Get discount by basket.
	 *
	 * @deprecated deprecated sinse sale 17.0.11
	 * @see Discount::buildFromBasket
	 *
	 * @param BasketBase $basket		Basket object.
	 * @return null|Discount
	 */
	public static function loadByBasket(BasketBase $basket)
	{
		$order = $basket->getOrder();
		if ($order instanceof Order)
		{
			return self::buildFromOrder($order);
		}

		return self::buildFromBasket($basket, new Context\Fuser($basket->getFUserId(true)));
	}

	/**
	 * Get discount by order.
	 *
	 * @deprecated deprecated sinse sale 17.0.11
	 * @see Discount::buildFromOrder
	 *
	 * @param Order $order		Order object.
	 * @return Discount
	 */
	public static function load(Order $order)
	{
		return self::buildFromOrder($order);
	}

	/**
	 * @return Discount
	 */
	protected static function createDiscountObject()
	{
		$registry = Registry::getInstance(Registry::REGISTRY_TYPE_ORDER);
		$discountClassName = $registry->getDiscountClassName();

		return new $discountClassName();
	}

	/**
	 * Builds discounts from order.
	 *
	 * @param Order $order Order object.
	 * @return Discount
	 */
	public static function buildFromOrder(Order $order)
	{
		$instanceIndex = static::getInstanceIndexByOrder($order);
		if (!isset(self::$instances[$instanceIndex]))
		{
			$discount = static::createDiscountObject();
			$discount->order = $order;
			$discount->context = new Context\User($order->getUserId());
			$discount->initInstanceData();
			self::$instances[$instanceIndex] = $discount;
			unset($discount);
		}

		return self::$instances[$instanceIndex];
	}

	/**
	 * Builds discounts from basket. Basket doesn't have to have a order.
	 * Context describes user and user groups which use in
	 *
	 * @param BasketBase $basket Basket.
	 * @param Context\BaseContext $context Context.
	 *
	 * @return mixed|null
	 * @throws Main\InvalidOperationException
	 */
	public static function buildFromBasket(BasketBase $basket, Context\BaseContext $context)
	{
		if ($basket->getOrder())
		{
			throw new Main\InvalidOperationException(
				'Could not build discounts from basket which has the order. You have to use buildFromOrder.'
			);
		}

		if ($basket->count() == 0)
		{
			return null;
		}

		//todo be careful $context may be important in getInstanceIndexByBasket()
		$instanceIndex = static::getInstanceIndexByBasket($basket, $context);
		if (!isset(self::$instances[$instanceIndex]))
		{
			$discount = static::createDiscountObject();
		}
		else
		{
			$discount = self::$instances[$instanceIndex];
		}

		/** @var Discount $discount */
		$discount->basket = $basket;
		$discount->context = $context;
		$discount->initInstanceData();

		self::$instances[$instanceIndex] = $discount;

		return self::$instances[$instanceIndex];
	}

	/**
	 * Get discount by order basket.
	 *
	 * @param Basket $basket		Basket.
	 * @return Discount
	 */
	public static function setOrder(Basket $basket)
	{
		$instanceIndex = static::getInstanceIndexByBasket($basket);
		if (!isset(self::$instances[$instanceIndex]))
			return static::loadByBasket($basket);
		$order = $basket->getOrder();
		if (!($order instanceof Order))
			return self::$instances[$instanceIndex];
		$newInstanceIndex = static::getInstanceIndexByOrder($order);
		if (!isset(self::$instances[$newInstanceIndex]))
		{
			/** @var Discount $discount */
			$discount = self::$instances[$instanceIndex];
			unset(self::$instances[$instanceIndex]);
			$discount->basket = null;
			$discount->order = $order;
			$discount->setNewOrder();
			$discount->loadShipment();
			$discount->fillShipmentData();
			self::$instances[$newInstanceIndex] = $discount;
			unset($discount);
			return self::$instances[$newInstanceIndex];
		}
		else
		{
			unset(self::$instances[$instanceIndex]);
			unset(self::$instances[$newInstanceIndex]);
			return static::load($order);
		}
	}

	/**
	 * Return order.
	 *
	 * @return Order|null
	 */
	public function getOrder()
	{
		return $this->order;
	}

	/**
	 * Return flag is order exists.
	 *
	 * @return bool
	 */
	public function isOrderExists()
	{
		return ($this->order instanceof Order);
	}

	/**
	 * Clone entity.
	 *
	 * @internal
	 * @param \SplObjectStorage $cloneEntity	Clone repository.
	 *
	 * @return Discount
	 */
	public function createClone(\SplObjectStorage $cloneEntity)
	{
		if ($this->isClone() && $cloneEntity->contains($this))
			return $cloneEntity[$this];

		$discountClone = clone $this;
		$discountClone->isClone = true;

		if (!$cloneEntity->contains($this))
			$cloneEntity[$this] = $discountClone;

		if ($this->isOrderExists())
		{
			if ($cloneEntity->contains($this->order))
				$discountClone->order = $cloneEntity[$this->order];
		}
		elseif ($this->isBasketExist())
		{
			if ($cloneEntity->contains($this->basket))
				$discountClone->basket = $cloneEntity[$this->basket];
		}

		if ($this->isShipmentExists())
		{
			if ($cloneEntity->contains($this->shipment))
				$discountClone->shipment = $cloneEntity[$this->shipment];
		}

		return $discountClone;
	}

	/**
	 * Returns true if discount entity is cloned.
	 *
	 * @return bool
	 */
	public function isClone()
	{
		return $this->isClone;
	}
	/**
	 * Return apply mode list.
	 *
	 * @param bool $extendedMode			Get mode list with names.
	 * @return array
	 */
	public static function getApplyModeList($extendedMode = false)
	{
		$extendedMode = ($extendedMode === true);
		if ($extendedMode)
		{
			return array(
				self::APPLY_MODE_ADD => Loc::getMessage('BX_SALE_DISCOUNT_APPLY_MODE_ADD_EXT'),
				self::APPLY_MODE_LAST => Loc::getMessage('BX_SALE_DISCOUNT_APPLY_MODE_LAST_EXT'),
				self::APPLY_MODE_DISABLE => Loc::getMessage('BX_SALE_DISCOUNT_APPLY_MODE_DISABLE_EXT'),
				self::APPLY_MODE_FULL_LAST => Loc::getMessage('BX_SALE_DISCOUNT_APPLY_MODE_FULL_LAST'),
				self::APPLY_MODE_FULL_DISABLE => Loc::getMessage('BX_SALE_DISCOUNT_APPLY_MODE_FULL_DISABLE')
			);
		}
		return array(
			self::APPLY_MODE_ADD,
			self::APPLY_MODE_LAST,
			self::APPLY_MODE_DISABLE,
			self::APPLY_MODE_FULL_LAST,
			self::APPLY_MODE_FULL_DISABLE
		);
	}

	/**
	 * Returns current sale discount apply mode.
	 *
	 * @return int
	 * @throws Main\ArgumentNullException
	 */
	public static function getApplyMode()
	{
		$applyMode = self::APPLY_MODE_ADD;
		if ((string)Main\Config\Option::get('sale', 'use_sale_discount_only') != 'Y')
		{
			$applyMode = (int)Main\Config\Option::get('sale', 'discount_apply_mode');
			if (!in_array($applyMode, self::getApplyModeList(false)))
				$applyMode = self::APPLY_MODE_ADD;
		}
		return $applyMode;
	}

	/**
	 * Set calculate mode.
	 *
	 * @param int $useMode			Calculate mode.
	 * @return void
	 */
	public function setUseMode($useMode)
	{
		$useMode = (int)$useMode;
		if ($useMode <= 0)
			return;
		$this->useMode = $useMode;
	}

	/**
	 * Return calculate mode.
	 *
	 * @return int
	 */
	public function getUseMode()
	{
		return $this->useMode;
	}

	/**
	 * Set full refresh status from edit order form.
	 *
	 * @param bool $state		Refresh or not order.
	 * @return void
	 */
	public function setOrderRefresh($state)
	{
		if ($state !== true && $state !== false)
			return;
		$this->orderRefresh = $state;
	}

	/**
	 * Returns full refresh status value.
	 *
	 * @return bool
	 */
	public function isOrderRefresh()
	{
		return $this->orderRefresh;
	}

	/**
	 * Returns new order flag value.
	 *
	 * @return bool
	 */
	public function isOrderNew()
	{
		return $this->newOrder;
	}

	/**
	 * Sets list of execute module which will be used to filter discount.
	 *
	 * @internal
	 * @param array $moduleList		Allowed execute module list.
	 * @return void
	 */
	public function setExecuteModuleFilter(array $moduleList)
	{
		$this->executeModuleFilter = $moduleList;
	}

	/**
	 * Set calculate shipments.
	 *
	 * @param Shipment $shipment							Current shipment.
	 * @return void
	 */
	public function setCalculateShipments(Shipment $shipment = null)
	{
		$this->shipment = $shipment;
	}

	/**
	 * Return shipment id list for existing order.
	 *
	 * @return array
	 */
	public function getShipmentsIds()
	{
		return $this->shipmentIds;
	}

	/**
	 * Calculate discounts.
	 *
	 * @return Result
	 * @throws Main\ArgumentNullException
	 */
	public function calculate()
	{
		/** @var Result $result */
		$result = new Result;
		$process = true;

		if ($this->stopCalculate())
			return $result;

		$this->discountsCache = array();
		$this->couponsCache = array();

		if (Compatible\DiscountCompatibility::isUsed())
			return $result;

		$this->initUseMode();

		if ($this->isOrderExists() && !$this->isOrderNew())
		{
			if ($this->isOrderRefresh())
			{
				$this->setApplyResult(array());
				DiscountCouponsManager::useSavedCouponsForApply(true);
			}
		}

		$this->orderData = null;
		$orderResult = $this->loadOrderData();

		if (!$orderResult->isSuccess())
		{
			$process = false;
			$result->addErrors($orderResult->getErrors());
		}
		unset($orderResult);

		if ($this->convertedOrder)
			return $result;

		if ($process)
		{
			DiscountCouponsManager::setUseOnlySaleDiscounts($this->useOnlySaleDiscounts());
			$this->resetBasketPrices();
			switch ($this->getUseMode())
			{
				case self::USE_MODE_APPLY:
					$calculateResult = $this->calculateApply();
					break;
				case self::USE_MODE_MIXED:
					$calculateResult = $this->calculateMixed();
					break;
				case self::USE_MODE_FULL:
					$calculateResult = $this->calculateFull();
					break;
				default:
					$calculateResult = new Result;
					$calculateResult->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_BAD_USE_MODE'),
						self::ERROR_ID
					));
					break;
			}
			if (!$calculateResult->isSuccess())
				$result->addErrors($calculateResult->getErrors());
			else
				$result->setData($this->fillDiscountResult());
			unset($calculateResult);
		}

		return $result;
	}

	/* apply result methods */
	/**
	 * Change applied discount list.
	 *
	 * @param array $applyResult		Change apply result.
	 * @return void
	 */
	public function setApplyResult($applyResult)
	{
		if (is_array($applyResult))
			$this->applyResult = $applyResult;

		if (!empty($this->applyResult['DISCOUNT_LIST']))
		{
			if (!empty($this->applyResult['BASKET']) && is_array($this->applyResult['BASKET']))
			{
				foreach ($this->applyResult['BASKET'] as $discountList)
				{
					if (empty($discountList) || !is_array($discountList))
						continue;
					foreach ($discountList as $orderDiscountId => $apply)
					{
						if ($apply == 'Y')
							$this->applyResult['DISCOUNT_LIST'][$orderDiscountId] = 'Y';
					}
					unset($apply, $orderDiscountId);
				}
				unset($discountList);
			}
			if (!empty($this->applyResult['DELIVERY']) && is_array($this->applyResult['DELIVERY']))
			{
				foreach ($this->applyResult['DELIVERY'] as $orderDiscountId => $apply)
				{
					if ($apply == 'Y')
						$this->applyResult['DISCOUNT_LIST'][$orderDiscountId] = 'Y';
				}
				unset($apply, $orderDiscountId);
			}
		}
	}

	/**
	 * Return discount list description.
	 *
	 * @param bool $extMode			Extended mode.
	 * @return array
	 */
	public function getApplyResult($extMode = false)
	{
		if (Compatible\DiscountCompatibility::isUsed())
			return Compatible\DiscountCompatibility::getApplyResult($extMode);

		$extMode = ($extMode === true);

		if (!$this->isOrderNew() && empty($this->orderData))
		{
			$this->initUseMode();
			$this->loadOrderData();
		}

		$this->getApplyDiscounts();
		$this->getApplyPrices();
		$this->getApplyDeliveryList();
		if ($extMode)
			$this->remakingDiscountResult();

		$result = $this->discountResult;
		$result['CONVERTED_ORDER'] = ($this->convertedOrder ? 'Y' : 'N');
		$result['FULL_DISCOUNT_LIST'] = $this->fullDiscountList;

		if ($extMode)
		{
			unset($result['APPLY_BLOCKS']);
		}
		else
		{
			/* for compatibility only */
			if (isset($this->discountResult['APPLY_BLOCKS'][0]['BASKET']))
				$result['BASKET'] = $this->discountResult['APPLY_BLOCKS'][0]['BASKET'];
			if (isset($this->discountResult['APPLY_BLOCKS'][0]['ORDER']))
				$result['ORDER'] = $this->discountResult['APPLY_BLOCKS'][0]['ORDER'];
		}
		return $result;
	}

	/* apply result methods finish */

	/**
	 * Save discount result.
	 *
	 * @return Result
	 */
	public function save()
	{
		$process = true;
		$result = new Result;
		if (!$this->isOrderExists() || !$this->isBasketNotEmpty())
			return $result;
		$orderId = (int)$this->getOrder()->getId();

		if ($this->isUsedDiscountCompatibility())
		{
			if (Compatible\DiscountCompatibility::isRepeatSave())
				return $result;
			$compatibleResult = Compatible\DiscountCompatibility::getResult();
			if ($compatibleResult === false)
				return $result;
			if (empty($compatibleResult))
				return $result;

			$this->setUseMode($compatibleResult['CALCULATE']['USE_MODE']);
			$this->newOrder = $compatibleResult['CALCULATE']['NEW_ORDER'];
			$this->discountsCache = $compatibleResult['DISCOUNT_LIST'];
			$this->couponsCache = $compatibleResult['COUPONS_LIST'];
			$this->discountResult = $compatibleResult['DISCOUNT_RESULT'];
			$this->forwardBasketTable = $compatibleResult['FORWARD_BASKET_TABLE'];
			$this->reverseBasketTable = $compatibleResult['REVERSE_BASKET_TABLE'];
			if ($this->isOrderNew())
			{
				$shipmentResult = $this->loadShipment();
				if (!$shipmentResult->isSuccess())
				{

				}
				unset($shipmentResult);
				$this->fillShipmentData();
			}
			unset($compatibleResult);
		}

		if ($this->getUseMode() === null)
			return $result;

		if ($process)
		{
			switch ($this->getUseMode())
			{
				case self::USE_MODE_FULL:
					$saveResult = $this->saveFull();
					break;
				case self::USE_MODE_APPLY:
					$saveResult = $this->saveApply();
					break;
				case self::USE_MODE_MIXED:
					$saveResult = $this->saveMixed();
					break;
				default:
					$saveResult = new Result;
					$saveResult->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_BAD_USE_MODE'),
						self::ERROR_ID
					));
			}
			if (!$saveResult->isSuccess())
			{
				$result->addErrors($saveResult->getErrors());
			}
			else
			{
				if ($orderId > 0)
					OrderHistory::addLog('DISCOUNT', $orderId, 'DISCOUNT_SAVED', null, null, array(), OrderHistory::SALE_ORDER_HISTORY_LOG_LEVEL_1);
			}
			unset($saveResult);
		}

		if ($orderId > 0)
			OrderHistory::collectEntityFields('DISCOUNT', $orderId);

		return $result;
	}

	/**
	 * Initial instance data.
	 *
	 * @return void
	 */
	protected function initInstanceData()
	{
		$this->orderData = null;
		$this->isClone = false;
		$this->entityResultCache = array();
		$this->setNewOrder();
		$this->fillEmptyDiscountResult();

		if ($this->isOrderExists())
		{
			$orderDiscountConfig = array(
				'SITE_ID' => $this->getOrder()->getSiteId(),
				'CURRENCY' => $this->getOrder()->getCurrency()
			);
		}
		else
		{
			/** @var BasketItem $basketItem */
			$basketItem = $this->getBasket()->rewind();
			$orderDiscountConfig = array(
				'SITE_ID' => $basketItem->getField('LID'),
				'CURRENCY' => $basketItem->getCurrency()
			);
			unset($basketItem);
		}
		OrderDiscountManager::init();
		OrderDiscountManager::setManagerConfig($orderDiscountConfig);
		unset($orderDiscountConfig);
	}

	/**
	 * Return is allow discount calculate.
	 *
	 * @return bool
	 */
	protected function stopCalculate()
	{
		if (!$this->isBasketNotEmpty())
			return true;
		if ($this->isOrderExists() && $this->getOrder()->isExternal())
			return true;
		if ($this->isOrderExists() && !$this->isShipmentExists())
			return false;
		return false;
	}

	/**
	 * Set new order flag.
	 *
	 * @return void
	 */
	protected function setNewOrder()
	{
		if ($this->newOrder !== null)
			return;
		$this->newOrder = true;
		if ($this->isOrderExists())
			$this->newOrder = ((int)$this->getOrder()->getId() <= 0);
	}

	/**
	 * Return true, if only sale discounts is allowed. For new order or refreshed order use sale option, otherwise use order option.
	 *
	 * @return bool
	 */
	protected function useOnlySaleDiscounts()
	{
		if (!$this->isOrderExists() || $this->isOrderNew() || $this->isOrderRefresh())
			return (string)Main\Config\Option::get('sale', 'use_sale_discount_only') == 'Y';
		else
			return (isset($this->saleOptions['SALE_DISCOUNT_ONLY']) && $this->saleOptions['SALE_DISCOUNT_ONLY'] == 'Y');
	}

	/**
	 * Return basket item currency.
	 *
	 * @param string|int $basketCode	Basket item code.
	 * @return string
	 */
	protected function getBasketCurrency($basketCode)
	{
		if ($this->isOrderExists())
			return $this->getOrder()->getCurrency();

		$currency = '';
		/** @var BasketItem $basketItem */
		if ($this->isBasketNotEmpty())
		{
			$basket = $this->getBasket();
			foreach ($basket as $basketItem)
			{
				if ($basketItem->getBasketCode() == $basketCode)
				{
					$currency = $basketItem->getCurrency();
					break;
				}
			}
			unset($basket, $basketItem);
		}
		if ($currency == '')
			$currency = (string)Main\Config\Option::get('sale', 'default_currency');
		return $currency;
	}

	/**
	 * Return current basket.
	 *
	 * @return Basket
	 */
	protected function getBasket()
	{
		if ($this->isOrderExists())
			return $this->getOrder()->getBasket();
		else
			return $this->basket;
	}

	/**
	 * Return exists basket.
	 *
	 * @return bool
	 */
	protected function isBasketExist()
	{
		if ($this->isOrderExists())
			return ($this->getOrder()->getBasket() instanceof Basket);
		else
			return ($this->basket instanceof Basket);
	}

	/**
	 * Returns the existence of a non-empty basket.
	 *
	 * @return bool
	 */
	protected function isBasketNotEmpty()
	{
		if ($this->isOrderExists())
		{
			$basket = $this->getOrder()->getBasket();
			$result = ($basket instanceof Basket && $basket->count() > 0);
			unset($basket);
		}
		else
		{
			$result = ($this->basket instanceof Basket && $this->basket->count() > 0);
		}
		return $result;
	}

	/**
	 * Initialization of the discount calculation mode.
	 *
	 * @return void
	 */
	protected function initUseMode()
	{
		$this->setUseMode(self::USE_MODE_FULL);
		if ($this->isOrderExists() && !$this->isOrderNew())
		{
			if ($this->isOrderRefresh())
				$this->setUseMode(self::USE_MODE_FULL);
			elseif ($this->isMixedBasket())
				$this->setUseMode(self::USE_MODE_MIXED);
			elseif ($this->getOrder()->getCalculateType() == Order::SALE_ORDER_CALC_TYPE_REFRESH)
				$this->setUseMode(self::USE_MODE_FULL);
			else
				$this->setUseMode(self::USE_MODE_APPLY);
		}
	}

	/**
	 * Load order information.
	 *
	 * @return Result
	 */
	protected function loadOrderData()
	{
		$result = new Result;
		$orderId = 0;
		if ($this->isOrderExists())
			$orderId = $this->getOrder()->getId();

		if (empty($this->orderData))
			$this->fillEmptyOrderData();
		$this->shipmentIds = array();

		$basketResult = $this->loadBasket();
		if (!$basketResult->isSuccess())
		{
			$result->addErrors($basketResult->getErrors());
			return $result;
		}
		unset($basketResult);

		if ($this->isOrderExists() && $orderId > 0)
		{
			$basketResult = $this->getBasketTables();
			if (!$basketResult->isSuccess())
			{
				$result->addErrors($basketResult->getErrors());
				return $result;
			}
			unset($basketResult);
		}

		$this->loadOrderConfig();

		$discountResult = $this->loadOrderDiscounts();
		if (!$discountResult->isSuccess())
			$result->addErrors($discountResult->getErrors());
		unset($discountResult);

		$dataResult = $this->loadBasketStoredData();
		if (!$dataResult->isSuccess())
			$result->addErrors($dataResult->getErrors());
		unset($dataResult);

		if (!$this->isShipmentExists())
		{
			$shipmentResult = $this->loadShipment();
			if (!$shipmentResult->isSuccess())
			{
				$result->addErrors($shipmentResult->getErrors());

				return $result;
			}
			unset($shipmentResult);
		}
		$this->fillShipmentData();

		return $result;
	}

	/**
	 * Fill empty order data.
	 *
	 * @return void
	 */
	protected function fillEmptyOrderData()
	{
		/** @var Basket $basket*/
		$basket = $this->getBasket();
		if ($this->isOrderExists())
		{
			$order = $this->getOrder();
			$this->orderData = array(
				'ID' => $order->getId(),
				'USER_ID' => $order->getUserId(),
				'SITE_ID' => $order->getSiteId(),
				'LID' => $order->getSiteId(), // compatibility only
				'ORDER_PRICE' => $order->getPrice(),
				'ORDER_WEIGHT' => $basket->getWeight(),
				'CURRENCY' => $order->getCurrency(),
				'PERSON_TYPE_ID' => $order->getPersonTypeId(),
				'RECURRING_ID' => $order->getField('RECURRING_ID'),
				'BASKET_ITEMS' => array(),
				'PRICE_DELIVERY' => 0,
				'PRICE_DELIVERY_DIFF' => 0,
				'DELIVERY_ID' => 0,
				'CUSTOM_PRICE_DELIVERY' => 'N',
				'SHIPMENT_CODE' => 0,
				'SHIPMENT_ID' => 0,
				'ORDER_PROP' => array()
			);
			$paymentCollection = $order->getPaymentCollection();
			/** @var Payment $payment */
			foreach ($paymentCollection as $payment)
			{
				if ($payment->isInner())
					continue;
				if (!isset($this->orderData['PAY_SYSTEM_ID']))
				{
					$this->orderData['PAY_SYSTEM_ID'] = (int)$payment->getPaymentSystemId();
					break;
				}
			}
			unset($payment, $paymentCollection);
			if (!isset($this->orderData['PAY_SYSTEM_ID']))
				$this->orderData['PAY_SYSTEM_ID'] = 0;

			/** @var \Bitrix\Sale\PropertyValueCollection $shipmentCollection */
			$propertyCollection = $order->getPropertyCollection();
			/** @var \Bitrix\Sale\PropertyValue $orderProperty */
			foreach ($propertyCollection as $orderProperty)
				$this->orderData['ORDER_PROP'][$orderProperty->getPropertyId()] = $orderProperty->getValue();
			unset($orderProperty);
			foreach (static::getOrderPropertyCodes() as $propertyCode => $attribute)
			{
				$this->orderData[$propertyCode] = '';
				$orderProperty = $propertyCollection->getAttribute($attribute);
				if ($orderProperty instanceof PropertyValue)
					$this->orderData[$propertyCode] = $orderProperty->getValue();
				unset($orderProperty);
			}
			unset($propertyCode, $attribute);
			unset($propertyCollection);
		}
		else
		{
			if ($this->isBasketNotEmpty())
			{
				/** @var BasketItem $basketItem */
				$basketItem = $basket->rewind();
				$currency = $basketItem->getCurrency();
				unset($basketItem);
			}
			else
			{
				$currency = Internals\SiteCurrencyTable::getCurrency($basket->getSiteId());
			}

			$this->orderData = array(
				'ID' => 0,
				'USER_ID' => $this->context->getUserId(),
				'SITE_ID' => $basket->getSiteId(),
				'ORDER_PRICE' => $basket->getPrice(),
				'ORDER_WEIGHT' => $basket->getWeight(),
				'CURRENCY' => $currency,
				'PERSON_TYPE_ID' => 0,
				'BASKET_ITEMS' => array(),
				'PRICE_DELIVERY' => 0,
				'PRICE_DELIVERY_DIFF' => 0,
				'DELIVERY_ID' => 0,
				'CUSTOM_PRICE_DELIVERY' => 'N',
				'SHIPMENT_CODE' => 0,
				'SHIPMENT_ID' => 0,
				'PAY_SYSTEM_ID' => 0
			);
			unset($currency);
		}
		unset($basket);
	}

	/**
	 * Get basket data.
	 *
	 * @return Result
	 * @throws Main\ObjectNotFoundException
	 */
	protected function loadBasket()
	{
		$result = new Result;

		$process = true;

		if (!$this->isBasketExist())
			throw new Main\ObjectNotFoundException('Entity "Basket" not found');
		elseif (!$this->isBasketNotEmpty())
			return $result;

		/** @var Basket $basket */
		$basket = $this->getBasket();

		if ($process)
		{
			/** @var BasketItem $basketItem */
			foreach ($basket as $basketItem)
			{
				if (!$basketItem->canBuy())
					continue;
				$code = $basketItem->getBasketCode();
				$this->orderData['BASKET_ITEMS'][$code] = $basketItem->getFieldValues();
				unset($this->orderData['BASKET_ITEMS'][$code]['DATE_INSERT']);
				unset($this->orderData['BASKET_ITEMS'][$code]['DATE_UPDATE']);
				$this->orderData['BASKET_ITEMS'][$code]['PROPERTIES'] = $basketItem->getPropertyCollection()->getPropertyValues();
				if (!isset($this->orderData['BASKET_ITEMS'][$code]['DISCOUNT_PRICE']))
					$this->orderData['BASKET_ITEMS'][$code]['DISCOUNT_PRICE'] = 0;
				$this->orderData['BASKET_ITEMS'][$code]['BASE_PRICE'] = $basketItem->getField('BASE_PRICE');
				if ($this->orderData['BASKET_ITEMS'][$code]['BASE_PRICE'] === null)
				{
					$this->orderData['BASKET_ITEMS'][$code]['BASE_PRICE'] = $this->orderData['BASKET_ITEMS'][$code]['PRICE']
						+ $this->orderData['BASKET_ITEMS'][$code]['DISCOUNT_PRICE'];
				}
				if (empty($this->orderData['BASKET_ITEMS'][$code]['PRICE_TYPE_ID']))
					$this->orderData['BASKET_ITEMS'][$code]['PRICE_TYPE_ID'] = $this->getBasketItemValue($code, 'PRICE_TYPE_ID');
				if ($basketItem->isBundleParent())
				{
					$bundle = $basketItem->getBundleCollection();
					if (empty($bundle))
					{
						$process = false;
						$result->addError(new Main\Entity\EntityError(
							Loc::getMessage('BX_SALE_DISCOUNT_ERR_BASKET_BUNDLE_EMPTY'),
							self::ERROR_ID
						));
						break;
					}
					/** @var BasketItem $bundleItem */
					foreach ($bundle as $bundleItem)
					{
						$code = $bundleItem->getBasketCode();
						$this->orderData['BASKET_ITEMS'][$code] = $bundleItem->getFieldValues();
						$this->orderData['BASKET_ITEMS'][$code]['PROPERTIES'] = $bundleItem->getPropertyCollection()->getPropertyValues();
						$this->orderData['BASKET_ITEMS'][$code]['IN_SET'] = 'Y';
						if (!isset($this->orderData['BASKET_ITEMS'][$code]['DISCOUNT_PRICE']))
							$this->orderData['BASKET_ITEMS'][$code]['DISCOUNT_PRICE'] = 0;
						$this->orderData['BASKET_ITEMS'][$code]['BASE_PRICE'] = $bundleItem->getField('BASE_PRICE');
						if ($this->orderData['BASKET_ITEMS'][$code]['BASE_PRICE'] === null)
						{
							$this->orderData['BASKET_ITEMS'][$code]['BASE_PRICE'] = $this->orderData['BASKET_ITEMS'][$code]['PRICE']
								+ $this->orderData['BASKET_ITEMS'][$code]['DISCOUNT_PRICE'];
						}
					}
					unset($bundle, $bundleItem);
				}
			}
			unset($code, $basketItem);
		}
		unset($basket, $process);

		return $result;
	}

	/**
	 * Return is exists discount shipment.
	 *
	 * @return bool
	 */
	protected function isShipmentExists()
	{
		return ($this->shipment instanceof Shipment);
	}

	/**
	 * Load shipment.
	 *
	 * @return Result
	 */
	protected function loadShipment()
	{
		$result = new Result;
		if (!$this->isOrderExists())
			return $result;
		if (!$this->isShipmentExists())
		{
			$loadDelivery = false;
			$order = $this->getOrder();
			/** @var ShipmentCollection $orderShipmentList */
			$orderShipmentList = $order->getShipmentCollection();
			/** @var Shipment $shipment */
			if ($this->isOrderNew())
			{
				foreach ($orderShipmentList as $shipment)
				{
					if ($shipment->isSystem())
						continue;

					if (!$loadDelivery)
					{
						$this->shipment = $shipment;
						$loadDelivery = true;
						break;
					}
				}
			}
			else
			{
				$shipmentId = false;
				foreach ($orderShipmentList as $shipment)
				{
					if ($shipment->isSystem())
						continue;

					$currentShipmentId = (int)$shipment->getId();
					if ($shipmentId === false || $shipmentId > $currentShipmentId)
						$shipmentId = $currentShipmentId;
				}
				unset($currentShipmentId, $shipment);
				if (!empty($shipmentId))
				{
					$this->shipment = $orderShipmentList->getItemById($shipmentId);
					$loadDelivery = true;
				}
				unset($shipmentId);
			}
			unset($loadDelivery);
		}
		return $result;
	}

	/**
	 * Fill data from shipment.
	 *
	 * @return void
	 */
	protected function fillShipmentData()
	{
		if (!$this->isShipmentExists())
			return;

		$this->orderData['DELIVERY_ID'] = $this->shipment->getDeliveryId();
		$this->orderData['CUSTOM_PRICE_DELIVERY'] = ($this->shipment->isCustomPrice() ? 'Y' : 'N');
		$this->orderData['BASE_PRICE_DELIVERY'] = $this->shipment->getField('BASE_PRICE_DELIVERY');
		$this->orderData['PRICE_DELIVERY'] = $this->orderData['BASE_PRICE_DELIVERY'];
		$this->orderData['SHIPMENT_CODE'] = $this->shipment->getShipmentCode();
		$this->orderData['SHIPMENT_ID'] = (int)$this->shipment->getId();
	}

	/**
	 * Load order config for exists order.
	 *
	 * @return void
	 * @throws Main\ArgumentException
	 * @throws Main\ArgumentNullException
	 */
	protected function loadOrderConfig()
	{
		$this->convertedOrder = false;
		$this->shipment = null;
		$this->saleOptions = array(
			'USE_BASE_PRICE' => Main\Config\Option::get('sale', 'get_discount_percent_from_base_price'),
			'SALE_DISCOUNT_ONLY' => Main\Config\Option::get('sale', 'use_sale_discount_only'),
			'APPLY_MODE' => Main\Config\Option::get('sale', 'discount_apply_mode')
		);

		$this->fillCompatibleOrderFields();

		if (!$this->isOrderExists())
			return;

		$this->convertedOrder = ($this->isOrderNew() === false);
		if ($this->getUseMode() == self::USE_MODE_FULL)
			$this->convertedOrder = false;

		$order = $this->getOrder();
		$orderId = $order->getId();
		if ($this->isOrderNew() || $this->getUseMode() == self::USE_MODE_FULL)
			return;

		$data = Internals\OrderDiscountDataTable::getList(array(
			'select' => array('*'),
			'filter' => array(
				'=ORDER_ID' => $orderId,
				'=ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_ORDER,
				'=ENTITY_ID' => $orderId
			)
		))->fetch();
		if (empty($data))
			return;

		$entityData = &$data['ENTITY_DATA'];
		if (!empty($entityData['DELIVERY']))
		{
			$this->orderData['DELIVERY_ID'] = $entityData['DELIVERY']['DELIVERY_ID'];
			if (isset($entityData['DELIVERY']['CUSTOM_PRICE_DELIVERY']))
				$this->orderData['CUSTOM_PRICE_DELIVERY'] = $entityData['DELIVERY']['CUSTOM_PRICE_DELIVERY'];
			if (isset($entityData['DELIVERY']['SHIPMENT_ID']))
			{
				$entityData['DELIVERY']['SHIPMENT_ID'] = (int)$entityData['DELIVERY']['SHIPMENT_ID'];
				if ($entityData['DELIVERY']['SHIPMENT_ID'] > 0)
				{
					$this->shipmentIds[] = $entityData['DELIVERY']['SHIPMENT_ID'];
					/** @var ShipmentCollection $orderShipmentList */
					$orderShipmentList = $order->getShipmentCollection();
					$this->shipment = $orderShipmentList->getItemById($entityData['DELIVERY']['SHIPMENT_ID']);
					if (empty($this->shipment))
					{
						$this->shipment = null;
						$this->shipmentIds[] = array();
					}
				}
			}
		}
		if (!empty($entityData['OPTIONS']) && is_array($entityData['OPTIONS']))
		{
			foreach (array_keys($this->saleOptions) as $key)
			{
				if (isset($entityData['OPTIONS'][$key]))
					$this->saleOptions[$key] = $entityData['OPTIONS'][$key];
			}
			unset($key);
			$this->fillCompatibleOrderFields();
		}
		if (!isset($entityData['OLD_ORDER']))
			$this->convertedOrder = false;
		unset($entityData);
		unset($data, $orderId, $order);

		$this->loadRoundConfig();
	}

	/**
	 * Load discounrs for exists order.
	 *
	 * @return Result
	 */
	protected function loadOrderDiscounts()
	{
		$result = new Result;
		$this->discountsCache = array();
		$this->couponsCache = array();

		if (!$this->isOrderExists())
			return $result;

		$order = $this->getOrder();
		if ($this->isOrderNew() || ($this->getUseMode() == self::USE_MODE_FULL && !$this->isMixedBasket()))
			return $result;

		$applyResult = OrderDiscountManager::loadResultFromDatabase(
			$order->getId(),
			true,
			$this->reverseBasketTable,
			$this->orderData['BASKET_ITEMS']
		);

		if (!$applyResult->isSuccess())
			$result->addErrors($applyResult->getErrors());

		$applyResultData = $applyResult->getData();

		if (!empty($applyResultData['DISCOUNT_LIST']))
		{
			foreach ($applyResultData['DISCOUNT_LIST'] as $orderDiscountId => $discountData)
			{
				$discountData['ACTIONS_DESCR_DATA'] = false;
				if (!empty($discountData['ACTIONS_DESCR']) && is_array($discountData['ACTIONS_DESCR']))
				{
					$discountData['ACTIONS_DESCR_DATA'] = $discountData['ACTIONS_DESCR'];
					$discountData['ACTIONS_DESCR'] = self::formatDescription($discountData['ACTIONS_DESCR']);
				}
				else
				{
					$discountData['ACTIONS_DESCR'] = false;
				}
				if (empty($discountData['ACTIONS_DESCR']))
				{
					$discountData['ACTIONS_DESCR'] = false;
					$discountData['ACTIONS_DESCR_DATA'] = false;
				}
				$this->discountsCache[$orderDiscountId] = $discountData;

				if (isset($applyResultData['DISCOUNT_MODULES'][$orderDiscountId]))
					$this->cacheDiscountModules[$orderDiscountId] = $applyResultData['DISCOUNT_MODULES'][$orderDiscountId];
			}
			unset($orderDiscountId, $discountData);
		}
		if (!empty($applyResultData['COUPON_LIST']))
			$this->couponsCache = $applyResultData['COUPON_LIST'];

		$this->discountResultCounter = 0;
		$this->discountResult['APPLY_BLOCKS'] = $applyResultData['APPLY_BLOCKS'];
		if (!empty($this->discountResult['APPLY_BLOCKS']))
		{
			foreach ($this->discountResult['APPLY_BLOCKS'] as $counter => $applyBlock)
			{
				if (!empty($applyBlock['BASKET']))
				{
					foreach ($applyBlock['BASKET'] as $discountList)
					{
						foreach ($discountList as $discount)
						{
							if ($discount['COUPON_ID'] == '')
								continue;
							DiscountCouponsManager::setApplyByProduct($discount, array($discount['COUPON_ID']));
						}
					}
					unset($discountList);
				}

				if (!empty($applyBlock['ORDER']))
				{
					foreach ($applyBlock['ORDER'] as $discount)
					{
						if ($discount['COUPON_ID'] != '')
							DiscountCouponsManager::setApply($discount['COUPON_ID'], $discount['RESULT']);
					}
					unset($discount);
				}

				$this->discountResultCounter = $counter + 1;
			}
			unset($counter, $applyBlock);
		}

		if (!empty($applyResultData['DATA']['STORED_ACTION_DATA']) && is_array($applyResultData['DATA']['STORED_ACTION_DATA']))
			$this->discountStoredActionData = $applyResultData['DATA']['STORED_ACTION_DATA'];

		unset($applyResultData, $applyResult);

		return $result;
	}

	/**
	 * Load basket stored data for order.
	 *
	 * @return Result
	 */
	protected function loadBasketStoredData()
	{
		$result = new Result;

		$this->basketItemsData = array();

		if (!$this->isOrderExists())
			return $result;

		$order = $this->getOrder();
		if ($this->isOrderNew() || ($this->getUseMode() == self::USE_MODE_FULL && !$this->isMixedBasket()))
			return $result;

		$basketData = [];

		$iterator = Internals\OrderDiscountDataTable::getList([
			'select' => ['*'],
			'filter' => [
				'=ORDER_ID' => $order->getId(),
				'=ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_BASKET_ITEM
			]
		]);
		while ($row = $iterator->fetch())
		{
			if (empty($row['ENTITY_DATA']) || !is_array($row['ENTITY_DATA']))
				continue;
			$basketData[$row['ENTITY_ID']] = $row['ENTITY_DATA'];
		}
		unset($row, $iterator);

		if (empty($basketData))
			return $result;

		foreach ($this->reverseBasketTable as $basketId => $basketCode)
		{
			if (!isset($basketData[$basketId]) || !is_array($basketData[$basketId]))
				continue;
			$this->basketItemsData[$basketCode] = $basketData[$basketId];
		}
		unset($basketId, $basketCode);
		unset($basketData);
		unset($order);

		return $result;
	}

	/**
	 * Return basket item data value from provider.
	 * @internal
	 *
	 * @param int|string $code				Basket code.
	 * @param string $field					Field name.
	 * @return null|mixed
	 */
	protected function getBasketItemValue($code, $field)
	{
		if (!isset($this->basketItemsData[$code]))
			return null;
		return (isset($this->basketItemsData[$code][$field]) ? $this->basketItemsData[$code][$field] : null);
	}

	/**
	 * Return basket item data from provider.
	 * @internal
	 *
	 * @param int|string $code				Basket code.
	 * @param array $fields					Field names.
	 * @return array|null
	 */
	protected function getBasketItemValueList($code, array $fields)
	{
		if (!isset($this->basketItemsData[$code]) || empty($fields))
			return null;

		$result = array();
		foreach ($fields as $fieldName)
		{
			$result[$fieldName] = (
				isset($this->basketItemsData[$code][$fieldName])
				? $this->basketItemsData[$code][$fieldName]
				: null
			);
		}
		unset($fieldName);
		return $result;
	}

	/**
	 * Calculate discount by new order.
	 *
	 * @return Result
	 */
	protected function calculateFull()
	{
		$result = new Result;
		if (!$this->isBasketNotEmpty())
			return $result;

		$this->discountIds = array();
		Discount\Actions::setUseMode(
			Discount\Actions::MODE_CALCULATE,
			array(
				'USE_BASE_PRICE' => $this->saleOptions['USE_BASE_PRICE'],
				'SITE_ID' => $this->orderData['SITE_ID'],
				'CURRENCY' => $this->orderData['CURRENCY']
			)
		);

		$this->fillEmptyDiscountResult();

		$this->getRoundForBasePrices();

		$basket = $this->getBasket();
		/** @var BasketItem $basketItem */
		foreach ($basket as $basketItem)
		{
			$code = $basketItem->getBasketCode();
			if ($this->isCustomPriceByCode($code))
			{
				if (array_key_exists($code, $this->basketDiscountList))
					unset($this->basketDiscountList[$code]);
			}
			else
			{
				if (!isset($this->basketDiscountList[$code]))
				{
					$this->basketDiscountList[$code] = $basketItem->getField('DISCOUNT_LIST');
					if ($this->basketDiscountList[$code] === null)
						unset($this->basketDiscountList[$code]);
				}
			}
		}
		unset($code, $basketItem, $basket);

		DiscountCouponsManager::clearApply();
		$basketDiscountResult = $this->calculateFullBasketDiscount();
		if (!$basketDiscountResult->isSuccess())
		{
			$result->addErrors($basketDiscountResult->getErrors());
			unset($basketDiscountResult);
			return $result;
		}
		unset($basketDiscountResult);

		if ($this->isRoundMode(self::ROUND_MODE_BASKET_DISCOUNT))
			$this->roundFullBasketPrices();

		if (!$this->isBasketLastDiscount())
		{
			$this->loadDiscountByUserGroups();
			$this->loadDiscountList();
			$executeResult = $this->executeDiscountList();
			if (!$executeResult->isSuccess())
			{
				$result->addErrors($executeResult->getErrors());
				unset($executeResult);
				return $result;
			}
			unset($executeResult);
		}

		if ($this->isRoundMode(self::ROUND_MODE_FINAL_PRICE))
			$this->roundFullBasketPrices();

		return $result;
	}

	/**
	 * Calculate discount by exist order.
	 *
	 * @return Result
	 */
	protected function calculateApply()
	{
		$result = new Result;
		if (!$this->isOrderExists())
			return $result;

		if ($this->convertedOrder)
			return $result;

		if (!empty($this->discountResult['APPLY_BLOCKS']))
		{
			Discount\Actions::setUseMode(
				Discount\Actions::MODE_MANUAL,
				array(
					'USE_BASE_PRICE' => $this->saleOptions['USE_BASE_PRICE'],
					'SITE_ID' => $this->orderData['SITE_ID'],
					'CURRENCY' => $this->orderData['CURRENCY']
				)
			);

			$currentCounter = $this->discountResultCounter;

			foreach (array_keys($this->discountResult['APPLY_BLOCKS']) as $counter)
			{
				$this->discountResultCounter = $counter;
				$blockResult = $this->calculateApplyDiscountBlock();
				if (!$blockResult->isSuccess())
				{
					$result->addErrors($blockResult->getErrors());
					unset($blockResult);

					return $result;
				}
				unset($blockResult);
			}

			$this->discountResultCounter = $currentCounter;
			unset($currentCounter);
		}

		if ($result->isSuccess())
		{
			Discount\Actions::setUseMode(
				Discount\Actions::MODE_CALCULATE,
				array(
					'USE_BASE_PRICE' => $this->saleOptions['USE_BASE_PRICE'],
					'SITE_ID' => $this->orderData['SITE_ID'],
					'CURRENCY' => $this->orderData['CURRENCY']
				)
			);

			$this->clearCurrentApplyBlock();

			$couponsResult = $this->calculateApplyAdditionalCoupons();
			if (!$couponsResult->isSuccess())
			{
				$result->addErrors($couponsResult->getErrors());
				unset($couponsResult);
				return $result;
			}
			unset($couponsResult);

			if ($this->isRoundMode(self::ROUND_MODE_FINAL_PRICE))
				$this->roundChangedBasketPrices();
		}

		return $result;
	}

	/**
	 * Calculate discount by exist order with new items.
	 *
	 * @return Result
	 */
	protected function calculateMixed()
	{
		$result = new Result;

		if (!$this->isOrderExists())
			return $result;

		if ($this->convertedOrder)
			return $result;

		if (!empty($this->discountResult['APPLY_BLOCKS']))
		{
			Discount\Actions::setUseMode(
				Discount\Actions::MODE_MANUAL,
				array(
					'USE_BASE_PRICE' => $this->saleOptions['USE_BASE_PRICE'],
					'SITE_ID' => $this->orderData['SITE_ID'],
					'CURRENCY' => $this->orderData['CURRENCY']
				)
			);

			$currentCounter = $this->discountResultCounter;

			foreach (array_keys($this->discountResult['APPLY_BLOCKS']) as $counter)
			{
				$this->discountResultCounter = $counter;
				$blockResult = $this->calculateApplyDiscountBlock();
				if (!$blockResult->isSuccess())
				{
					$result->addErrors($blockResult->getErrors());
					unset($blockResult);

					return $result;
				}
				unset($blockResult);
			}

			$this->discountResultCounter = $currentCounter;
			unset($currentCounter);
		}

		if ($result->isSuccess())
		{
			Discount\Actions::setUseMode(
				Discount\Actions::MODE_CALCULATE,
				array(
					'USE_BASE_PRICE' => $this->saleOptions['USE_BASE_PRICE'],
					'SITE_ID' => $this->orderData['SITE_ID'],
					'CURRENCY' => $this->orderData['CURRENCY']
				)
			);

			$this->clearCurrentApplyBlock();

			$this->getRoundForBasePrices();

			$basketCodeList = $this->getBasketCodes(true);
			if (!empty($basketCodeList))
			{
				/** @var \Bitrix\Sale\Basket $basket */
				$basket = $this->getBasket();
				foreach ($basketCodeList as $basketCode)
				{
					$basketItem = $basket->getItemByBasketCode($basketCode);
					if ($basketItem instanceof BasketItemBase)
					{
						if (!isset($this->basketDiscountList[$basketCode]))
						{
							$this->basketDiscountList[$basketCode] = $basketItem->getField('DISCOUNT_LIST');
							if ($this->basketDiscountList[$basketCode] === null)
								unset($this->basketDiscountList[$basketCode]);
						}
					}
				}
				unset($basketCode);
				unset($basket);
			}
			unset($basketCodeList);

			$basketDiscountResult = $this->calculateFullBasketDiscount();
			if (!$basketDiscountResult->isSuccess())
			{
				$result->addErrors($basketDiscountResult->getErrors());
				unset($basketDiscountResult);
				return $result;
			}
			unset($basketDiscountResult);

			if ($this->isRoundMode(self::ROUND_MODE_BASKET_DISCOUNT))
				$this->roundFullBasketPrices();

			$couponsResult = $this->calculateApplyAdditionalCoupons();
			if (!$couponsResult->isSuccess())
			{
				$result->addErrors($couponsResult->getErrors());
				unset($couponsResult);
				return $result;
			}
			unset($couponsResult);

			if ($this->isRoundMode(self::ROUND_MODE_FINAL_PRICE))
				$this->roundChangedBasketPrices();
		}

		return $result;
	}

	/**
	 * Save discount result for new order.
	 *
	 * @return Result
	 */
	protected function saveFull()
	{
		$result = new Result;

		$process = true;
		$order = $this->getOrder();
		$orderId = $order->getId();
		$orderCurrency = $order->getCurrency();
		unset($order);

		if (!Compatible\DiscountCompatibility::isUsed() || !Compatible\DiscountCompatibility::isInited())
		{
			$basketResult = $this->getBasketTables();
			if (!$basketResult->isSuccess())
			{
				$process = false;
				$result->addErrors($basketResult->getErrors());
			}
		}

		if ($process)
		{
			DiscountCouponsManager::finalApply();
			DiscountCouponsManager::saveApplied();
			$couponsResult = $this->saveCoupons();
			if (!$couponsResult->isSuccess())
			{
				$process = false;
				$result->addErrors($couponsResult->getErrors());
			}
		}

		if ($process)
		{
			Internals\OrderRulesTable::clearByOrder($orderId);
			Internals\OrderDiscountDataTable::clearByOrder($orderId);

			$lastApplyBlockResult = $this->saveLastApplyBlock();
			if (!$lastApplyBlockResult->isSuccess())
			{
				$process = false;
				$result->addErrors($lastApplyBlockResult->getErrors());
			}
			unset($lastApplyBlockResult);
		}

		if ($process)
		{
			$fields = array(
				'ORDER_ID' => $orderId,
				'ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_ORDER,
				'ENTITY_ID' => $orderId,
				'ENTITY_VALUE' => $orderId,
				'ENTITY_DATA' => array(
					'OPTIONS' => array(
						'USE_BASE_PRICE' => Main\Config\Option::get('sale', 'get_discount_percent_from_base_price'),
						'SALE_DISCOUNT_ONLY' => Main\Config\Option::get('sale', 'use_sale_discount_only'),
						'APPLY_MODE' => Main\Config\Option::get('sale', 'discount_apply_mode')
					),
					'DELIVERY' => array(
						'DELIVERY_ID' => $this->orderData['DELIVERY_ID'],
						'CUSTOM_PRICE_DELIVERY' => $this->orderData['CUSTOM_PRICE_DELIVERY'],
						'SHIPMENT_ID' => 0
					)
				)
			);
			if ($this->shipment instanceof Shipment)
				$fields['ENTITY_DATA']['DELIVERY']['SHIPMENT_ID'] = $this->shipment->getId();
			$dataResult = Internals\OrderDiscountDataTable::add($fields);
			if (!$dataResult->isSuccess())
			{
				$result->addError(new Main\Entity\EntityError(
					Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
					self::ERROR_ID
				));
			}
			unset($dataResult, $fields);
			unset($orderCurrency);

			$fields = array(
				'ORDER_ID' => $orderId,
				'ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_ROUND,
				'ENTITY_ID' => $orderId,
				'ENTITY_VALUE' => $orderId,
				'ENTITY_DATA' => array(
					'MODE' => $this->roundApplyMode,
					'CONFIG' => $this->roundApplyConfig
				)
			);
			$dataResult = Internals\OrderDiscountDataTable::add($fields);
			if (!$dataResult->isSuccess())
			{
				$result->addError(new Main\Entity\EntityError(
					Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
					self::ERROR_ID
				));
			}
			unset($dataResult, $fields);

			if (!empty($this->discountStoredActionData))
			{
				$fields = array(
					'ORDER_ID' => $orderId,
					'ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_DISCOUNT_STORED_DATA,
					'ENTITY_ID' => $orderId,
					'ENTITY_VALUE' => $orderId,
					'ENTITY_DATA' => $this->discountStoredActionData
				);
				$dataResult = Internals\OrderDiscountDataTable::add($fields);
				if (!$dataResult->isSuccess())
				{
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
						self::ERROR_ID
					));
				}
				unset($dataResult, $fields);
			}

			$dataResult = $this->saveBasketStoredData($this->getBasketCodes(true));
			if (!$dataResult->isSuccess())
			{
				$result->addErrors($dataResult->getErrors());
			}
			unset($dataResult);
		}

		if ($process)
		{
			if (DiscountCouponsManager::usedByManager())
				DiscountCouponsManager::clear(true);
		}

		return $result;
	}

	/**
	 * Save basket items stored data.
	 *
	 * @param array $basketCodeList		Code list.
	 * @return Result
	 */
	protected function saveBasketStoredData(array $basketCodeList)
	{
		$result = new Result();
		if (empty($basketCodeList))
			return $result;
		$useMode = $this->getUseMode();
		if ($useMode != self::USE_MODE_FULL && $useMode != self::USE_MODE_MIXED)
			return $result;

		$itemsData = [];
		foreach ($basketCodeList as $basketCode)
		{
			if (!isset($this->basketItemsData[$basketCode]))
				continue;
			$data = $this->prepareBasketItemStoredData($basketCode);
			if ($data === null)
				continue;
			$basketId = $this->forwardBasketTable[$basketCode];
			$itemsData[$basketId] = [
				'ENTITY_ID' => $basketId,
				'ENTITY_VALUE' => $basketId,
				'ENTITY_DATA' => $data
			];
		}
		unset($data, $basketCode);

		$orderId = $this->getOrder()->getId();

		$collection = [];
		$deleteList = [];

		$iterator = Internals\OrderDiscountDataTable::getList([
			'select' => ['*'],
			'filter' => [
				'=ORDER_ID' => $orderId,
				'=ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_BASKET_ITEM,
			]
		]);
		while ($row = $iterator->fetch())
		{
			$index = $row['ENTITY_ID'];
			$collection[$index] = $row;
			$deleteList[$index] = $row['ID'];
		}
		unset($index, $row, $iterator);

		if (!empty($itemsData))
		{
			foreach ($itemsData as $index => $row)
			{
				if (isset($deleteList[$index]))
					unset($deleteList[$index]);
				if (empty($collection[$index]))
				{
					$row['ORDER_ID'] = $orderId;
					$row['ENTITY_TYPE'] = Internals\OrderDiscountDataTable::ENTITY_TYPE_BASKET_ITEM;
					$resultInternal = Internals\OrderDiscountDataTable::add($row);
				}
				else
				{
					$resultInternal = Internals\OrderDiscountDataTable::update(
						$collection[$index]['ID'],
						['ENTITY_DATA' => $row['ENTITY_DATA']]
					);
				}
				if (!$resultInternal->isSuccess())
					$result->addErrors($resultInternal->getErrors());
			}
			unset($resultInternal, $index, $row);
		}
		unset($itemsData);

		if (!empty($deleteList))
		{
			$conn = Main\Application::getConnection();
			$helper = $conn->getSqlHelper();
			$query = 'delete from '.$helper->quote(Internals\OrderDiscountDataTable::getTableName()).' where '.$helper->quote('ID');
			foreach (array_chunk($deleteList, 500) as $row)
			{
				$conn->queryExecute($query.' in ('.implode(', ', $row).')');
			}
			unset($row, $query);
		}

		return $result;
	}

	/**
	 * Save discount result for exist order.
	 *
	 * @return Result
	 */
	protected function saveApply()
	{
		$result = new Result;

		$process = true;
		$orderId = $this->getOrder()->getId();

		if ($this->convertedOrder)
			return $result;

		$basketResult = $this->getBasketTables();
		if (!$basketResult->isSuccess())
		{
			$process = false;
			$result->addErrors($basketResult->getErrors());
		}

		$deleteList = array();
		$rulesIterator = Internals\OrderRulesTable::getList(array(
			'select' => array('ID'),
			'filter' => array('=ORDER_ID' => $orderId)
		));
		while ($rule = $rulesIterator->fetch())
		{
			$rule['ID'] = (int)$rule['ID'];
			$deleteList[$rule['ID']] = $rule['ID'];
		}
		unset($rule, $rulesIterator);

		$deleteRoundList = array();
		$iterator = Internals\OrderRoundTable::getList(array(
			'select' => array('ID'),
			'filter' => array('=ORDER_ID' => $orderId)
		));
		while ($row = $iterator->fetch())
		{
			$row['ID'] = (int)$row['ID'];
			$deleteRoundList[$row['ID']] = $row['ID'];
		}
		unset($row, $iterator);

		$rulesList = array();
		foreach ($this->discountResult['APPLY_BLOCKS'] as $counter => $applyBlock)
		{
			if ($counter == $this->discountResultCounter)
				continue;

			if (!empty($applyBlock['BASKET']))
			{
				foreach ($applyBlock['BASKET'] as $basketCode => $discountList)
				{
					foreach ($discountList as $discount)
					{
						if (!isset($discount['RULE_ID']) || (int)$discount['RULE_ID'] < 0)
						{
							$process = false;
							$result->addError(new Main\Entity\EntityError(
								Loc::getMessage('BX_SALE_DISCOUNT_ERR_EMPTY_RULE_ID_EXT_DISCOUNT'),
								self::ERROR_ID
							));
							continue;
						}
						$orderDiscountId = $discount['DISCOUNT_ID'];
						$ruleData = array(
							'RULE_ID' => $discount['RULE_ID'],
							'APPLY' => $discount['RESULT']['APPLY'],
							'DESCR_ID' => (isset($discount['RULE_DESCR_ID']) ? (int)$discount['RULE_DESCR_ID'] : 0),
							'DESCR' => $discount['RESULT']['DESCR_DATA']['BASKET'],
						);
						if ($ruleData['DESCR_ID'] <= 0)
						{
							$ruleData['DESCR_ID'] = 0;
							$ruleData['MODULE_ID'] = $this->discountsCache[$orderDiscountId]['MODULE_ID'];
							$ruleData['ORDER_DISCOUNT_ID'] = $orderDiscountId;
							$ruleData['ORDER_ID'] = $orderId;
						}
						$rulesList[] = $ruleData;
						unset($ruleData);
					}
					unset($discount);
				}
				unset($basketCode, $discountList);
			}

			if (!empty($applyBlock['ORDER']))
			{
				foreach ($applyBlock['ORDER'] as $discount)
				{
					$orderDiscountId = $discount['DISCOUNT_ID'];
					if (!empty($discount['RESULT']['BASKET']))
					{
						foreach ($discount['RESULT']['BASKET'] as $basketCode => $applyData)
						{
							if (!isset($applyData['RULE_ID']) || (int)$applyData['RULE_ID'] < 0)
							{
								$process = false;
								$result->addError(new Main\Entity\EntityError(
									Loc::getMessage('BX_SALE_DISCOUNT_ERR_EMPTY_RULE_ID_SALE_DISCOUNT'),
									self::ERROR_ID
								));
								continue;
							}
							$ruleData = array(
								'RULE_ID' => $applyData['RULE_ID'],
								'APPLY' => $applyData['APPLY'],
								'DESCR_ID' => (isset($applyData['RULE_DESCR_ID']) ? (int)$applyData['RULE_DESCR_ID'] : 0),
								'DESCR' => $applyData['DESCR_DATA'],
							);
							if ($ruleData['DESCR_ID'] <= 0)
							{
								$ruleData['DESCR_ID'] = 0;
								$ruleData['MODULE_ID'] = $this->discountsCache[$orderDiscountId]['MODULE_ID'];
								$ruleData['ORDER_DISCOUNT_ID'] = $orderDiscountId;
								$ruleData['ORDER_ID'] = $orderId;
							}
							if (!$discount['ACTION_BLOCK_LIST'])
								$ruleData['ACTION_BLOCK_LIST'] = $applyData['ACTION_BLOCK_LIST'];
							$rulesList[] = $ruleData;
							unset($ruleData);
						}
						unset($basketCode, $applyData);
					}
					if (!empty($discount['RESULT']['DELIVERY']))
					{
						if (!isset($discount['RESULT']['DELIVERY']['RULE_ID']) || (int)$discount['RESULT']['DELIVERY']['RULE_ID'] < 0)
						{
							$process = false;
							$result->addError(new Main\Entity\EntityError(
								Loc::getMessage('BX_SALE_DISCOUNT_ERR_EMPTY_RULE_ID_SALE_DISCOUNT'),
								self::ERROR_ID
							));
							continue;
						}
						$ruleData = array(
							'RULE_ID' => $discount['RESULT']['DELIVERY']['RULE_ID'],
							'APPLY' => $discount['RESULT']['DELIVERY']['APPLY'],
							'DESCR_ID' => (isset($discount['RESULT']['DELIVERY']['RULE_DESCR_ID']) ? (int)$discount['RESULT']['DELIVERY']['RULE_DESCR_ID'] : 0),
							'DESCR' => $discount['RESULT']['DELIVERY']['DESCR_DATA']
						);
						if ($ruleData['DESCR_ID'] <= 0)
						{
							$ruleData['DESCR_ID'] = 0;
							$ruleData['MODULE_ID'] = $this->discountsCache[$orderDiscountId]['MODULE_ID'];
							$ruleData['ORDER_DISCOUNT_ID'] = $orderDiscountId;
							$ruleData['ORDER_ID'] = $orderId;
						}
						$rulesList[] = $ruleData;
						unset($ruleData);
					}
				}
				unset($discount);
			}

			if (!empty($applyBlock['BASKET_ROUND']))
			{
				foreach ($applyBlock['BASKET_ROUND'] as $row)
				{
					if (isset($deleteRoundList[$row['RULE_ID']]))
						unset($deleteRoundList[$row['RULE_ID']]);
				}
				unset($row);
			}
		}

		if ($process && !empty($rulesList))
		{
			foreach ($rulesList as $ruleRow)
			{
				$rowUpdate = array('APPLY' => $ruleRow['APPLY']);
				if (isset($ruleRow['ACTION_BLOCK_LIST']))
					$rowUpdate['ACTION_BLOCK_LIST'] = $ruleRow['ACTION_BLOCK_LIST'];
				$ruleResult = Internals\OrderRulesTable::update($ruleRow['RULE_ID'], $rowUpdate);
				unset($rowUpdate);
				if ($ruleResult->isSuccess())
				{
					if (isset($deleteList[$ruleRow['RULE_ID']]))
						unset($deleteList[$ruleRow['RULE_ID']]);
					if ($ruleRow['DESCR_ID'] > 0)
					{
						$descrResult = Internals\OrderRulesDescrTable::update($ruleRow['DESCR_ID'], array('DESCR' => $ruleRow['DESCR']));
					}
					else
					{
						$descrData = array(
							'DESCR' => $ruleRow['DESCR'],
							'MODULE_ID' => $ruleRow['MODULE_ID'],
							'ORDER_DISCOUNT_ID' => $ruleRow['ORDER_DISCOUNT_ID'],
							'ORDER_ID' => $ruleRow['ORDER_ID']
						);
						$descrResult = Internals\OrderRulesDescrTable::add($descrData);
					}
					if (!$descrResult->isSuccess(true))
					{
						$process = false;
						$result->addError(new Main\Entity\EntityError(
							Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
							self::ERROR_ID
						));
						unset($descrResult);
						break;
					}
					unset($descrResult);
				}
				else
				{
					$process = false;
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
						self::ERROR_ID
					));
					unset($ruleResult);
					break;
				}
				unset($ruleResult);
			}
			unset($ruleRow);
		}

		if ($process && (!empty($deleteList) || !empty($deleteRoundList)))
		{
			$conn = Main\Application::getConnection();
			$helper = $conn->getSqlHelper();
			if (!empty($deleteList))
			{
				$mainQuery = 'delete from '.$helper->quote(Internals\OrderRulesTable::getTableName()).' where '.$helper->quote('ID');
				$descrQuery = 'delete from '.$helper->quote(Internals\OrderRulesDescrTable::getTableName()).' where '.$helper->quote('RULE_ID');
				foreach (array_chunk($deleteList, 500) as $row)
				{
					$conn->queryExecute($mainQuery.' in ('.implode(', ', $row).')');
					$conn->queryExecute($descrQuery.' in ('.implode(', ', $row).')');
				}
				unset($row, $descrQuery, $mainQuery);
			}
			if (!empty($deleteRoundList))
			{
				$query = 'delete from '.$helper->quote(Internals\OrderRoundTable::getTableName()).' where '.$helper->quote('ID');
				foreach (array_chunk($deleteRoundList, 500) as $row)
				{
					$conn->queryExecute($query.' in ('.implode(', ', $row).')');
				}
				unset($row, $query);
			}
			unset($helper, $conn);
		}
		unset($deleteRoundList, $deleteList);

		if ($process)
		{
			if (!empty($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]))
			{
				DiscountCouponsManager::finalApply();
				DiscountCouponsManager::saveApplied();
				$couponsResult = $this->saveCoupons();
				if (!$couponsResult->isSuccess())
				{
					$process = false;
					$result->addErrors($couponsResult->getErrors());
				}

				if ($process)
				{
					$lastApplyBlockResult = $this->saveLastApplyBlock();
					if (!$lastApplyBlockResult->isSuccess())
					{
						$process = false;
						$result->addErrors($lastApplyBlockResult->getErrors());
					}
					unset($lastApplyBlockResult);
				}
			}
		}

		if ($process)
		{
			$configId = 0;
			$roundData = Internals\OrderDiscountDataTable::getList(array(
				'select' => array('ID'),
				'filter' => array(
					'=ORDER_ID' => $orderId,
					'=ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_ROUND,
					'=ENTITY_ID' => $orderId
				)
			))->fetch();
			if (!empty($roundData))
				$configId = (int)$roundData['ID'];
			if ($configId > 0)
			{
				$fields = array(
					'ENTITY_DATA' => array(
						'MODE' => $this->roundApplyMode,
						'CONFIG' => $this->roundApplyConfig
					)
				);
				$dataResult = Internals\OrderDiscountDataTable::update($configId, $fields);
				if (!$dataResult->isSuccess())
				{
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
						self::ERROR_ID
					));
				}
				unset($dataResult, $fields);
			}
			else
			{
				$fields = array(
					'ORDER_ID' => $orderId,
					'ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_ROUND,
					'ENTITY_ID' => $orderId,
					'ENTITY_VALUE' => $orderId,
					'ENTITY_DATA' => array(
						'MODE' => $this->roundApplyMode,
						'CONFIG' => $this->roundApplyConfig
					)
				);
				$dataResult = Internals\OrderDiscountDataTable::add($fields);
				if (!$dataResult->isSuccess())
				{
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
						self::ERROR_ID
					));
				}
				unset($dataResult, $fields);
			}

			if (!empty($this->discountStoredActionData))
			{
				$storedDataId = 0;
				$iterator = Internals\OrderDiscountDataTable::getList(array(
					'select' => array('ID'),
					'filter' => array(
						'=ORDER_ID' => $orderId,
						'=ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_DISCOUNT_STORED_DATA,
						'=ENTITY_ID' => $orderId
					)
				));
				$row = $iterator->fetch();
				if (!empty($row))
					$storedDataId = (int)$row['ID'];
				if ($storedDataId > 0)
				{
					$dataResult = Internals\OrderDiscountDataTable::update(
						$storedDataId,
						array(
							'ENTITY_DATA' => $this->discountStoredActionData
						)
					);
				}
				else
				{
					$dataResult = Internals\OrderDiscountDataTable::add(array(
						'ORDER_ID' => $orderId,
						'ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_DISCOUNT_STORED_DATA,
						'ENTITY_ID' => $orderId,
						'ENTITY_VALUE' => $orderId,
						'ENTITY_DATA' => $this->discountStoredActionData
					));
				}
				if (!$dataResult->isSuccess())
				{
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
						self::ERROR_ID
					));
				}
				unset($dataResult);
			}
		}

		if ($process)
		{
			if (DiscountCouponsManager::usedByManager())
				DiscountCouponsManager::clear(true);
		}

		return $result;
	}

	/**
	 * Save discount result for mixed order.
	 *
	 * @return Result
	 */
	protected function saveMixed()
	{
		$result = $this->saveApply();

		if ($result->isSuccess())
		{
			$basketCodeList = array_merge(
				$this->getBasketCodes(false),
				$this->getBasketCodes(true)
			);
			$dataResult = $this->saveBasketStoredData($basketCodeList);
			if (!$dataResult->isSuccess())
			{
				$result->addErrors($dataResult->getErrors());
			}
			unset($dataResult, $basketCodeList);
		}

		return $result;
	}

	/**
	 * Save coupons for order.
	 *
	 * @return Result
	 */
	protected function saveCoupons()
	{
		$result = new Result;
		if (!$this->isOrderExists())
			return $result;
		if (!empty($this->couponsCache))
		{
			$orderId = $this->getOrder()->getId();
			foreach ($this->couponsCache as $orderCouponId => $couponData)
			{
				if ($couponData['ID'] > 0)
					continue;
				$fields = $couponData;
				$fields['ORDER_ID'] = $orderId;
				$couponResult = OrderDiscountManager::saveCoupon($fields);
				if (!$couponResult->isSuccess())
				{
					$result->addErrors($couponResult->getErrors());
					unset($couponResult);
					continue;
				}
				$this->couponsCache[$orderCouponId]['ID'] = $couponResult->getId();
				unset($couponResult);
			}
			unset($orderId);
		}
		return $result;
	}

	public function saveExternalLastApplyblock(BasketItem $basketItem, $orderDiscountId)
	{
		$basket = $basketItem->getCollection();
		$this->order = $basket->getOrder();

		$this->loadOrderData();

		$listItems[$basketItem->getBasketCode()] = array('APPLY'=>'Y', 'ACTION_BLOCK_LIST'=>array(), 'DESCR_DATA'=>array(), 'DESCR'=>array());

		$this->discountsCache[$orderDiscountId]['MODULE_ID'] = 'sale';
		$applyBlock = &$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter];
		$applyBlock['ORDER'][] = array(
				'DISCOUNT_ID'=>$orderDiscountId,
				'COUPON_ID'=>0,
				'RESULT'=>array('BASKET'=>$listItems),
		);

		$this->saveLastApplyBlock();
	}
	/**
	 * Save result last apply block discount.
	 *
	 * @return Result
	 * @throws \Exception
	 */
	protected function saveLastApplyBlock()
	{
		$result = new Result;

		$process = true;

		$orderId = $this->getOrder()->getId();

		$rulesList = array();
		$ruleDescr = array();
		$ruleIndex = 0;

		$roundList = array();
		$roundIndex = 0;

		$applyBlock = &$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter];

		if (!empty($applyBlock['BASKET']))
		{
			foreach ($this->forwardBasketTable as $basketCode => $basketId)
			{
				if (empty($applyBlock['BASKET'][$basketCode]))
					continue;
				$discountList = $applyBlock['BASKET'][$basketCode];
				foreach ($discountList as $discount)
				{
					$orderDiscountId = $discount['DISCOUNT_ID'];
					$rulesList[$ruleIndex] = array(
						'MODULE_ID' => $this->discountsCache[$orderDiscountId]['MODULE_ID'],
						'ORDER_DISCOUNT_ID' => $orderDiscountId,
						'ORDER_ID' => $orderId,
						'ENTITY_TYPE' => Internals\OrderRulesTable::ENTITY_TYPE_BASKET,
						'ENTITY_ID' => $basketId,
						'ENTITY_VALUE' => $basketId,
						'COUPON_ID' => ($discount['COUPON_ID'] != '' ? $this->couponsCache[$discount['COUPON_ID']]['ID'] : 0),
						'APPLY' => $discount['RESULT']['APPLY'],
						'APPLY_BLOCK_COUNTER' => $this->discountResultCounter
					);
					$ruleDescr[$ruleIndex] = array(
						'MODULE_ID' => $this->discountsCache[$orderDiscountId]['MODULE_ID'],
						'ORDER_DISCOUNT_ID' => $orderDiscountId,
						'ORDER_ID' => $orderId,
						'DESCR' => $discount['RESULT']['DESCR_DATA']
					);
					$ruleIndex++;
				}
				unset($discount, $discountList);
			}
			unset($basketCode, $basketId);
		}
		if (!empty($applyBlock['ORDER']))
		{
			foreach ($applyBlock['ORDER'] as $discount)
			{
				$orderDiscountId = $discount['DISCOUNT_ID'];
				if (!empty($discount['RESULT']['BASKET']))
				{
					foreach ($this->forwardBasketTable as $basketCode => $basketId)
					{
						if (empty($discount['RESULT']['BASKET'][$basketCode]))
							continue;
						$applyData = $discount['RESULT']['BASKET'][$basketCode];
						$rulesList[$ruleIndex] = array(
							'MODULE_ID' => $this->discountsCache[$orderDiscountId]['MODULE_ID'],
							'ORDER_DISCOUNT_ID' => $orderDiscountId,
							'ORDER_ID' => $orderId,
							'ENTITY_TYPE' => Internals\OrderRulesTable::ENTITY_TYPE_BASKET,
							'ENTITY_ID' => $basketId,
							'ENTITY_VALUE' => $basketId,
							'COUPON_ID' => ($discount['COUPON_ID'] != '' ? $this->couponsCache[$discount['COUPON_ID']]['ID'] : 0),
							'APPLY' => $applyData['APPLY'],
							'APPLY_BLOCK_COUNTER' => $this->discountResultCounter,
							'ACTION_BLOCK_LIST' => $applyData['ACTION_BLOCK_LIST']
						);
						$ruleDescr[$ruleIndex] = array(
							'MODULE_ID' => $this->discountsCache[$orderDiscountId]['MODULE_ID'],
							'ORDER_DISCOUNT_ID' => $orderDiscountId,
							'ORDER_ID' => $orderId,
							'DESCR' => $applyData['DESCR_DATA']
						);
						$ruleIndex++;
						unset($applyData);
					}
					unset($basketCode, $basketId);
				}
				if (!empty($discount['RESULT']['DELIVERY']))
				{
					$rulesList[$ruleIndex] = array(
						'MODULE_ID' => $this->discountsCache[$orderDiscountId]['MODULE_ID'],
						'ORDER_DISCOUNT_ID' => $orderDiscountId,
						'ORDER_ID' => $orderId,
						'ENTITY_TYPE' => Internals\OrderRulesTable::ENTITY_TYPE_DELIVERY,
						'ENTITY_ID' => (int)$discount['RESULT']['DELIVERY']['DELIVERY_ID'],
						'ENTITY_VALUE' => (string)$discount['RESULT']['DELIVERY']['DELIVERY_ID'],
						'COUPON_ID' => ($discount['COUPON_ID'] != '' ? $this->couponsCache[$discount['COUPON_ID']]['ID'] : 0),
						'APPLY' => $discount['RESULT']['DELIVERY']['APPLY'],
						'APPLY_BLOCK_COUNTER' => $this->discountResultCounter
					);
					$ruleDescr[$ruleIndex] = array(
						'MODULE_ID' => $this->discountsCache[$orderDiscountId]['MODULE_ID'],
						'ORDER_DISCOUNT_ID' => $orderDiscountId,
						'ORDER_ID' => $orderId,
						'DESCR' => $discount['RESULT']['DELIVERY']['DESCR_DATA']
					);
					$ruleIndex++;
				}
			}
			unset($discount);
		}

		if (!empty($applyBlock['BASKET_ROUND']))
		{
			foreach ($applyBlock['BASKET_ROUND'] as $basketCode => $roundData)
			{
				$basketId = $this->forwardBasketTable[$basketCode];
				$roundList[$roundIndex] = array(
					'ORDER_ID' => $orderId,
					'APPLY_BLOCK_COUNTER' => $this->discountResultCounter,
					'ORDER_ROUND' => 'N',
					'ENTITY_TYPE' => Internals\OrderRoundTable::ENTITY_TYPE_BASKET,
					'ENTITY_ID' => $basketId,
					'ENTITY_VALUE' => $basketId,
					'APPLY' => $roundData['APPLY'],
					'ROUND_RULE' => $roundData['ROUND_RULE']
				);
				$roundIndex++;
				unset($basketId);
			}
			unset($roundData, $basketCode);
		}

		unset($applyBlock);

		if (!empty($rulesList))
		{
			foreach ($rulesList as $index => $ruleRow)
			{
				$ruleResult = Internals\OrderRulesTable::add($ruleRow);
				if ($ruleResult->isSuccess())
				{
					$ruleDescr[$index]['RULE_ID'] = $ruleResult->getId();
					$descrResult = Internals\OrderRulesDescrTable::add($ruleDescr[$index]);
					if (!$descrResult->isSuccess())
					{
						$process = false;
						$result->addError(new Main\Entity\EntityError(
							Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
							self::ERROR_ID
						));
						unset($descrResult);
						break;
					}
					unset($descrResult);
				}
				else
				{
					$process = false;
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
						self::ERROR_ID
					));
					unset($ruleResult);
					break;
				}
				unset($ruleResult);
			}
			unset($ruleRow);
		}

		if (!empty($roundList))
		{
			foreach ($roundList as $roundRow)
			{
				$roundResult = Internals\OrderRoundTable::add($roundRow);
				if (!$roundResult->isSuccess())
				{
					$process = false;
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_SAVE_APPLY_RULES'),
						self::ERROR_ID
					));
					unset($roundResult);
					break;
				}
				unset($roundResult);
			}
			unset($roundRow);
		}

		unset($process);

		return $result;
	}

	/**
	 * Check duscount conditions.
	 *
	 * @return bool
	 */
	protected function checkDiscountConditions()
	{
		if (
			!isset($this->currentStep['cacheIndex'])
			|| !isset($this->saleDiscountCache[$this->saleDiscountCacheKey][$this->currentStep['cacheIndex']])
		)
			return false;

		$key = 'UNPACK';
		$executeKey = $key.'_EXECUTE';

		if ($this->enableCheckingPrediction && !empty($this->saleDiscountCache[$this->saleDiscountCacheKey][$this->currentStep['cacheIndex']]['PREDICTIONS_APP']))
		{
			$key = 'PREDICTIONS_APP';
			$executeKey = $key.'_EXECUTE';
		}

		if (empty($this->saleDiscountCache[$this->saleDiscountCacheKey][$this->currentStep['cacheIndex']][$key]))
			return false;

		$discountLink = &$this->saleDiscountCache[$this->saleDiscountCacheKey][$this->currentStep['cacheIndex']];

		if (!array_key_exists($executeKey, $discountLink))
		{
			$checkOrder = null;

			$evalCode = '$checkOrder='.$discountLink[$key].';';
			if (version_compare(PHP_VERSION, '7.0.0', '>='))
			{
				try
				{
					eval($evalCode);
				}
				catch (\ParseError $e)
				{
					$this->showAdminError();
				}
			}
			else
			{
				eval($evalCode);
			}
			unset($evalCode);

			if (!is_callable($checkOrder))
				return false;
			$result = $checkOrder($this->orderData);
			unset($checkOrder);
		}
		else
		{
			if (!is_callable($discountLink[$executeKey]))
				return false;

			$result = $discountLink[$executeKey]($this->orderData);
		}
		unset($discountLink);
		return $result;
	}

	/**
	 * Apply discount rules.
	 *
	 * @return Result
	 */
	protected function applySaleDiscount()
	{
		$result = new Result;

		Discount\Actions::clearApplyCounter();

		$discount = (
			isset($this->currentStep['discountIndex'])
			? $this->discountsCache[$this->currentStep['discountId']]
			: $this->currentStep['discount']
		);
		if (isset($this->currentStep['discountIndex']))
		{
			if (!empty($discount['APPLICATION']) && !$this->loadDiscountModules($this->currentStep['discountId']))
			{
				$discount['APPLICATION'] = null;
				$result->addError(new Main\Entity\EntityError(
					Loc::getMessage('BX_SALE_DISCOUNT_ERR_SALE_DISCOUNT_MODULES_ABSENT'),
					self::ERROR_ID
				));
			}
		}

		if (!empty($discount['APPLICATION']))
		{
			if (!array_key_exists('APPLICATION_EXECUTE', $discount))
			{
				$discount['APPLICATION_EXECUTE'] = null;

				$evalCode = '$discount["APPLICATION_EXECUTE"]='.$discount['APPLICATION'].';';
				if (version_compare(PHP_VERSION, '7.0.0', '>='))
				{
					try
					{
						eval($evalCode);
					}
					catch (\ParseError $e)
					{
						$this->showAdminError();
					}
				}
				else
				{
					eval($evalCode);
				}
				unset($evalCode);
			}
			if (is_callable($discount['APPLICATION_EXECUTE']))
			{
				$currentUseMode = $this->getUseMode();
				$this->currentStep['oldData'] = $this->orderData;
				if (
					$currentUseMode == self::USE_MODE_APPLY
					|| $currentUseMode == self::USE_MODE_MIXED
				)
				{
					$discountStoredActionData = $this->getDiscountStoredActionData($this->currentStep['discountId']);
					if (!empty($discountStoredActionData) && is_array($discountStoredActionData))
						Discount\Actions::setStoredData($discountStoredActionData);
					unset($discountStoredActionData);
				}
				$discount['APPLICATION_EXECUTE']($this->orderData);
				switch ($currentUseMode)
				{
					case self::USE_MODE_COUPONS:
					case self::USE_MODE_FULL:
						$actionsResult = $this->calculateFullSaleDiscountResult();
						break;
					case self::USE_MODE_APPLY:
					case self::USE_MODE_MIXED:
						$actionsResult = $this->calculateApplySaleDiscountResult();
						break;
					default:
						$actionsResult = new Result;

				}
				if (!$actionsResult->isSuccess())
					$result->addErrors($actionsResult->getErrors());
				unset($actionsResult);
				unset($currentUseMode);
			}
		}
		unset($discount);
		Discount\Actions::clearAction();

		return $result;
	}

	/**
	 * Apply basket discount in new order.
	 *
	 * @return Result
	 */
	protected function calculateFullBasketDiscount()
	{
		$result = new Result;

		if ((string)Main\Config\Option::get('sale', 'use_sale_discount_only') == 'Y')
			return $result;
		if (empty($this->basketDiscountList))
			return $result;

		$applyExist = $this->isBasketApplyResultExist();

		$applyBlock = &$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['BASKET'];

		foreach ($this->getBasketCodes(true) as $basketCode)
		{
			if ($this->isOrderNew() && array_key_exists($basketCode, $applyBlock))
				unset($applyBlock[$basketCode]);
			if (empty($this->basketDiscountList[$basketCode]))
				continue;

			$itemData = array(
				'MODULE_ID' => $this->orderData['BASKET_ITEMS'][$basketCode]['MODULE'],
				'PRODUCT_ID' => $this->orderData['BASKET_ITEMS'][$basketCode]['PRODUCT_ID'],
				'BASKET_ID' => $basketCode
			);
			foreach ($this->basketDiscountList[$basketCode] as $index => $discount)
			{
				$discountResult = $this->convertDiscount($discount);
				if (!$discountResult->isSuccess())
				{
					$result->addErrors($discountResult->getErrors());
					unset($discountResult);
					return $result;
				}
				$orderDiscountId = $discountResult->getId();
				$discountData = $discountResult->getData();
				$orderCouponId = '';
				$this->basketDiscountList[$basketCode][$index]['ORDER_DISCOUNT_ID'] = $orderDiscountId;
				if ($discountData['USE_COUPONS'] == 'Y')
				{
					if (empty($discount['COUPON']))
					{
						$result->addError(new Main\Entity\EntityError(
							Loc::getMessage('BX_SALE_DISCOUNT_ERR_DISCOUNT_WITHOUT_COUPON'),
							self::ERROR_ID
						));
						return $result;
					}
					$couponResult = $this->convertCoupon($discount['COUPON'], $orderDiscountId);
					if (!$couponResult->isSuccess())
					{
						$result->addErrors($couponResult->getErrors());
						unset($couponResult);
						return $result;
					}
					$orderCouponId = $couponResult->getId();

					DiscountCouponsManager::setApplyByProduct($itemData, array($orderCouponId));
					unset($couponResult);
				}
				unset($discountData, $discountResult);
				if (!isset($applyBlock[$basketCode]))
					$applyBlock[$basketCode] = array();
				$applyBlock[$basketCode][$index] = array(
					'DISCOUNT_ID' => $orderDiscountId,
					'COUPON_ID' => $orderCouponId,
					'RESULT' => array(
						'APPLY' => 'Y',
						'DESCR' => false,
						'DESCR_DATA' => false
					)
				);

				$currentProduct = $this->orderData['BASKET_ITEMS'][$basketCode];
				$orderApplication = (
					!empty($this->discountsCache[$orderDiscountId]['APPLICATION'])
					? $this->discountsCache[$orderDiscountId]['APPLICATION']
					: null
				);
				if (!empty($orderApplication))
				{
					$this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT'] = (
						!empty($this->discountsCache[$orderDiscountId]['ACTIONS_DESCR_DATA'])
						? $this->discountsCache[$orderDiscountId]['ACTIONS_DESCR_DATA']
						: false
					);

					$applyProduct = null;
					eval('$applyProduct='.$orderApplication.';');
					if (is_callable($applyProduct))
						$applyProduct($this->orderData['BASKET_ITEMS'][$basketCode]);
					unset($applyProduct);

					if (!empty($this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']))
					{
						$applyBlock[$basketCode][$index]['RESULT']['DESCR_DATA'] = $this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']['BASKET'];
						$applyBlock[$basketCode][$index]['RESULT']['DESCR'] = self::formatDescription($this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']);
					}
					unset($this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']);
				}
				unset($orderApplication);

				if ($applyExist && !$this->getStatusApplyBasketDiscount($basketCode, $orderDiscountId, $orderCouponId))
				{
					$this->orderData['BASKET_ITEMS'][$basketCode] = $currentProduct;
					$applyBlock[$basketCode][$index]['RESULT']['APPLY'] = 'N';
				}
				unset($disable, $currentProduct);
			}
			unset($discount, $index);
		}
		unset($basketCode);

		unset($applyBlock);

		return $result;
	}

	/**
	 * Apply basket discount in exist order.
	 *
	 * @return Result
	 */
	protected function calculateApplyBasketDiscount()
	{
		$result = new Result;

		if ($this->useOnlySaleDiscounts())
			return $result;
		if (empty($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['BASKET']))
			return $result;

		$applyExist = $this->isBasketApplyResultExist();

		$applyBlock = &$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['BASKET'];

		foreach ($this->getBasketCodes(false) as $basketCode)
		{
			if ($this->isCustomPriceByCode($basketCode))
			{
				if (isset($applyBlock[$basketCode]))
					unset($applyBlock[$basketCode]);
				continue;
			}
			if (empty($applyBlock[$basketCode]))
				continue;

			foreach ($applyBlock[$basketCode] as $index => $discount)
			{
				$currentProduct = $this->orderData['BASKET_ITEMS'][$basketCode];
				$orderDiscountId = $discount['DISCOUNT_ID'];
				$orderCouponId = $discount['COUPON_ID'];

				$orderApplication = (
					!empty($this->discountsCache[$orderDiscountId]['APPLICATION'])
					? $this->discountsCache[$orderDiscountId]['APPLICATION']
					: null
				);
				if (!empty($orderApplication) && !$this->loadDiscountModules($orderDiscountId))
					$orderApplication = null;

				if (!empty($orderApplication))
				{
					$this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT'] = (
						!empty($this->discountsCache[$orderDiscountId]['ACTIONS_DESCR_DATA'])
						? $this->discountsCache[$orderDiscountId]['ACTIONS_DESCR_DATA']
						: false
					);

					$applyProduct = null;
					eval('$applyProduct='.$orderApplication.';');
					if (is_callable($applyProduct))
						$applyProduct($this->orderData['BASKET_ITEMS'][$basketCode]);
					unset($applyProduct);

					if (!empty($this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']))
					{
						$applyBlock[$basketCode][$index]['RESULT']['DESCR_DATA'] = $this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT'];
						$applyBlock[$basketCode][$index]['RESULT']['DESCR'] = self::formatDescription($this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']);
					}
					unset($this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']);
				}
				unset($orderApplication);

				$disable = ($applyBlock[$basketCode][$index]['RESULT']['APPLY'] == 'N');
				if ($applyExist)
				{
					$applyDisable = !$this->getStatusApplyBasketDiscount($basketCode, $orderDiscountId, $orderCouponId);
					if ($applyDisable != $disable)
						$disable = $applyDisable;
					unset($applyDisable);
				}
				if ($disable)
				{
					$this->orderData['BASKET_ITEMS'][$basketCode] = $currentProduct;
					$applyBlock[$basketCode][$index]['RESULT']['APPLY'] = 'N';
				}
				else
				{
					$applyBlock[$basketCode][$index]['RESULT']['APPLY'] = 'Y';
				}
				unset($disable, $currentProduct);

			}
			unset($index, $discount);
		}
		unset($basketCode);

		unset($applyBlock);

		return $result;
	}

	/**
	 * Calculate discount block for existing order.
	 *
	 * @return Result
	 */
	protected function calculateApplyDiscountBlock()
	{
		$result = new Result;

		$basketDiscountResult = $this->calculateApplyBasketDiscount();
		if (!$basketDiscountResult->isSuccess())
		{
			$result->addErrors($basketDiscountResult->getErrors());
			unset($basketDiscountResult);

			return $result;
		}
		unset($basketDiscountResult);

		$roundApply = false;
		if ($this->isRoundMode(self::ROUND_MODE_BASKET_DISCOUNT))
		{
			$roundApply = true;
			$this->roundApplyBasketPrices();
		}

		if ($this->isRoundMode(self::ROUND_MODE_SALE_DISCOUNT) && !$roundApply)
		{
			$this->roundApplyBasketPricesByIndex(array(
				'DISCOUNT_INDEX' => -1,
				'DISCOUNT_ID' => 0
			));
		}
		if (!empty($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER']))
		{
			$index = -1;
			foreach ($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'] as $indexDiscount => $discount)
			{
				$index++;
				$orderDiscountId = $discount['DISCOUNT_ID'];
				if (!isset($this->discountsCache[$orderDiscountId]))
				{
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_APPLY_WITHOUT_SALE_DISCOUNT'),
						self::ERROR_ID
					));

					return $result;
				}
				Discount\Actions::clearAction();
				if (!empty($discount['RESULT']['BASKET']))
				{
					if ($discount['ACTION_BLOCK_LIST'])
					{
						$applyResultMode = Discount\Actions::APPLY_RESULT_MODE_COUNTER;
						$blockList = array();
						foreach ($discount['RESULT']['BASKET'] as $basketCode => $basketItem)
							$blockList[$basketCode] = $basketItem['ACTION_BLOCK_LIST'];
						unset($basketCode, $basketItem);
					}
					else
					{
						if ($this->discountsCache[$orderDiscountId]['SIMPLE_ACTION'])
						{
							$applyResultMode = Discount\Actions::APPLY_RESULT_MODE_SIMPLE;
							$blockList = array_fill_keys(array_keys($discount['RESULT']['BASKET']), true);
						}
						else
						{
							$applyResultMode = Discount\Actions::APPLY_RESULT_MODE_DESCR;
							$blockList = array();
							foreach ($discount['RESULT']['BASKET'] as $basketCode => $basketItem)
								$blockList[$basketCode] = $basketItem['DESCR_DATA'];
							unset($basketCode, $basketItem);
						}
					}
					Discount\Actions::setApplyResultMode($applyResultMode);
					Discount\Actions::setApplyResult(array('BASKET' => $blockList));
					unset($blockList, $applyResultMode);
				}
				if ($this->isRoundMode(self::ROUND_MODE_SALE_DISCOUNT) && !$roundApply)
				{
					$this->roundApplyBasketPricesByIndex(array(
						'DISCOUNT_INDEX' => $index,
						'DISCOUNT_ID' => $orderDiscountId
					));
				}
				$this->fillCurrentStep(array(
					'discountIndex' => $indexDiscount,
					'discountId' => $orderDiscountId,
				));
				$actionsResult = $this->applySaleDiscount();
				if (!$actionsResult->isSuccess())
				{
					$result->addErrors($actionsResult->getErrors());
					unset($actionsResult);

					return $result;
				}
				unset($orderDiscountId);
			}
			if ($this->isRoundMode(self::ROUND_MODE_SALE_DISCOUNT) && !$roundApply)
			{
				$index++;
				$this->roundApplyBasketPricesByIndex(array(
					'DISCOUNT_INDEX' => $index,
					'DISCOUNT_ID' => 0
				));
				$roundConfig = $this->getRoundIndex('BASKET_ROUND');
				if (is_array($roundConfig))
				{
					if ($roundConfig['DISCOUNT_INDEX'] > $index && $roundConfig['DISCOUNT_ID'] == 0)
					{
						$this->roundApplyBasketPricesByIndex(array(
							'DISCOUNT_INDEX' => $roundConfig['DISCOUNT_INDEX'],
							'DISCOUNT_ID' => 0
						));
					}
				}
				unset($roundConfig);
			}
			unset($discount, $indexDiscount, $currentList);
		}

		if ($this->isRoundMode(self::ROUND_MODE_FINAL_PRICE))
			$this->roundApplyBasketPrices();

		$this->fillEmptyCurrentStep();

		return $result;
	}

	/**
	 * Applyed additional coupons.
	 *
	 * @return Result
	 */
	protected function calculateApplyAdditionalCoupons()
	{
		$result = new Result;

		$useMode = $this->getUseMode();
		if ($useMode != self::USE_MODE_APPLY && $useMode != self::USE_MODE_MIXED)
			return $result;

		if (!$this->useOnlySaleDiscounts())
		{
			$couponList = $this->getAdditionalCoupons(array('!MODULE_ID' => 'sale'));
			if (!empty($couponList))
			{
				$params = array(
					'USE_BASE_PRICE' => $this->saleOptions['USE_BASE_PRICE'],
					'USER_ID' => $this->orderData['USER_ID'],
					'SITE_ID' => $this->orderData['SITE_ID']
				);
				$couponsByModule = array();
				foreach ($couponList as &$coupon)
				{
					if (!isset($couponsByModule[$coupon['MODULE_ID']]))
						$couponsByModule[$coupon['MODULE_ID']] = array();
					$couponsByModule[$coupon['MODULE_ID']][] = array(
						'DISCOUNT_ID' => $coupon['DISCOUNT_ID'],
						'COUPON' => $coupon['COUPON']
					);
				}
				unset($coupon);
				if (!empty($couponsByModule))
				{
					foreach ($couponsByModule as $moduleId => $moduleCoupons)
					{
						if ($useMode == self::USE_MODE_APPLY)
						{
							$currentBasket = $this->orderData['BASKET_ITEMS'];
						}
						else
						{
							$currentBasket = array();
							$basketCodeList = $this->getBasketCodes(false);
							foreach ($basketCodeList as $basketCode)
								$currentBasket[$basketCode] = $this->orderData['BASKET_ITEMS'][$basketCode];
							unset($basketCode, $basketCodeList);
						}
						if (empty($currentBasket))
							continue;
						$couponsApply = OrderDiscountManager::calculateApplyCoupons($moduleId, $moduleCoupons, $currentBasket, $params);
						unset($currentBasket);
						if (!empty($couponsApply))
						{
							$couponsApplyResult = $this->calculateApplyBasketAdditionalCoupons($couponsApply);
							if (!$couponsApplyResult->isSuccess())
								$result->addErrors($couponsApplyResult->getErrors());
							unset($couponsApplyResult);
						}
						unset($couponsApply);
					}
					unset($moduleId, $moduleCoupons);
				}
				unset($couponsByModule, $params);
			}
			unset($couponList);
		}

		$couponList = $this->getAdditionalCoupons(array('MODULE_ID' => 'sale'));
		if (!empty($couponList))
		{
			$couponsApplyResult = $this->calculateApplySaleAdditionalCoupons($couponList);
			if (!$couponsApplyResult->isSuccess())
				$result->addErrors($couponsApplyResult->getErrors());
			unset($couponsApplyResult);
		}
		unset($couponList);

		return $result;
	}

	/**
	 * Calculate step discount result by new order.
	 *
	 * @return Result
	 */
	protected function calculateFullSaleDiscountResult()
	{
		$result = new Result;

		$this->orderData['DISCOUNT_RESULT'] = Discount\Actions::getActionResult();
		$this->orderData['DISCOUNT_DESCR'] = Discount\Actions::getActionDescription();
		if (!empty($this->orderData['DISCOUNT_RESULT']) && is_array($this->orderData['DISCOUNT_RESULT']))
		{
			$stepResult = self::getStepResult($this->orderData);
		}
		else
		{
			$stepResult = self::getStepResultOld($this->orderData, $this->currentStep['oldData']);
			if (!empty($stepResult))
			{
				if (empty($this->orderData['DISCOUNT_DESCR']) || !is_array($this->orderData['DISCOUNT_DESCR']))
					$this->orderData['DISCOUNT_DESCR'] = $this->getSimpleActionDescription($stepResult);
			}
		}

		Discount\Actions::fillCompatibleFields($this->orderData);
		$applied = !empty($stepResult);

		$orderDiscountId = 0;
		$orderCouponId = '';

		if ($applied)
		{
			$this->correctStepResult($stepResult, $this->currentStep['discount']);

			$this->currentStep['discount']['ACTIONS_DESCR'] = $this->orderData['DISCOUNT_DESCR'];
			$discountResult = $this->convertDiscount($this->currentStep['discount']);
			if (!$discountResult->isSuccess())
			{
				$result->addErrors($discountResult->getErrors());
				return $result;
			}
			$orderDiscountId = $discountResult->getId();
			$discountData = $discountResult->getData();

			$this->currentStep['discount']['ORDER_DISCOUNT_ID'] = $orderDiscountId;

			if ($discountData['USE_COUPONS'] == 'Y')
			{
				if (empty($this->currentStep['discount']['COUPON']))
				{
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_DISCOUNT_WITHOUT_COUPON'),
						self::ERROR_ID
					));

					return $result;
				}
				$couponResult = $this->convertCoupon($this->currentStep['discount']['COUPON']['COUPON'], $orderDiscountId);
				if (!$couponResult->isSuccess())
				{
					$result->addErrors($couponResult->getErrors());
					unset($couponResult);

					return $result;
				}
				$orderCouponId = $couponResult->getId();
				DiscountCouponsManager::setApply($orderCouponId, $stepResult);
				unset($couponResult);
			}
			$this->setDiscountStoredActionData($orderDiscountId, Discount\Actions::getStoredData());
		}
		unset($this->orderData['DISCOUNT_DESCR'], $this->orderData['DISCOUNT_RESULT']);

		if ($applied)
		{
			if (
				(
					!empty($this->applyResult['DISCOUNT_LIST'][$orderDiscountId])
					&& $this->applyResult['DISCOUNT_LIST'][$orderDiscountId] == 'N'
				)
				||
				(
					$orderCouponId != ''
					&& !empty($this->applyResult['COUPON_LIST'][$orderCouponId])
					&& $this->applyResult['COUPON_LIST'][$orderCouponId] == 'N'
				)
			)
			{
				$this->orderData = $this->currentStep['oldData'];
				if (!empty($stepResult['BASKET']))
				{
					foreach ($stepResult['BASKET'] as &$basketItem)
						$basketItem['APPLY'] = 'N';
					unset($basketItem);
				}
				if (!empty($stepResult['DELIVERY']))
					$stepResult['DELIVERY']['APPLY'] = 'N';
			}
			else
			{
				if (!empty($this->applyResult['BASKET']) && is_array($this->applyResult['BASKET']))
				{
					foreach ($this->applyResult['BASKET'] as $basketCode => $discountList)
					{
						if (
							is_array($discountList) && !empty($discountList[$orderDiscountId]) && $discountList[$orderDiscountId] == 'N'
						)
						{
							if (empty($stepResult['BASKET'][$basketCode]))
								continue;
							$stepResult['BASKET'][$basketCode]['APPLY'] = 'N';
							$this->orderData['BASKET_ITEMS'][$basketCode] = $this->currentStep['oldData']['BASKET_ITEMS'][$basketCode];
						}
					}
					unset($basketCode, $discountList);
				}
				if (!empty($this->applyResult['DELIVERY']))
				{
					if (
						is_array($this->applyResult['DELIVERY']) && !empty($this->applyResult['DELIVERY'][$orderDiscountId]) && $this->applyResult['DELIVERY'][$orderDiscountId] == 'N'
					)
					{
						$this->orderData['PRICE_DELIVERY'] = $this->currentStep['oldData']['PRICE_DELIVERY'];
						$this->orderData['PRICE_DELIVERY_DIFF'] = $this->currentStep['oldData']['PRICE_DELIVERY_DIFF'];
						$stepResult['DELIVERY']['APPLY'] = 'N';
					}
				}
			}
		}

		if ($applied && $orderCouponId != '')
		{
			$couponApply = DiscountCouponsManager::setApply($this->couponsCache[$orderCouponId]['COUPON'], $stepResult);
			unset($couponApply);
		}

		if ($applied)
		{
			$this->tryToRevertApplyStatusInBlocks($stepResult);

			$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][] = array(
				'DISCOUNT_ID' => $orderDiscountId,
				'COUPON_ID' => $orderCouponId,
				'RESULT' => $stepResult
			);
			if ($this->currentStep['discount']['LAST_DISCOUNT'] == 'Y')
				$this->currentStep['stop'] = true;

			if ($this->currentStep['discount']['LAST_LEVEL_DISCOUNT'] == 'Y')
				$this->currentStep['stopLevel'] = true;
		}

		return $result;
	}

	/**
	 * Tries to revert apply status of discounts.
	 * It depends on current $stepResult. If it has REVERT_APPLY like true, that we have to cancel discounts on basket
	 * items which were affected.
	 * @param array $stepResult
	 * @return void
	 */
	protected function tryToRevertApplyStatusInBlocks(array $stepResult)
	{
		if (empty($stepResult['BASKET']))
		{
			return;
		}

		foreach ($stepResult['BASKET'] as $basketItemId => $item)
		{
			if ($item['APPLY'] !== 'Y')
			{
				continue;
			}

			if (empty($item['DESCR_DATA']))
			{
				continue;
			}

			foreach ($item['DESCR_DATA'] as $rowDescription)
			{
				if (
					!empty($rowDescription['REVERT_APPLY']) &&
					$rowDescription['VALUE_ACTION'] === OrderDiscountManager::DESCR_VALUE_ACTION_CUMULATIVE
				)
				{
					$this->revertApplyBlockForBasketItem($basketItemId);
				}
			}
		}
	}

	/**
	 * Reverts apply flag in blocks for basket items which has for example cumulative discount
	 * which cancels previous discounts on item.
	 * @param int $basketItemId
	 * @return void
	 */
	protected function revertApplyBlockForBasketItem($basketItemId)
	{
		if (empty($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]))
		{
			return;
		}

		$applyBlock = &$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter];
		foreach ($applyBlock['ORDER'] as &$orderBlock)
		{
			foreach ($orderBlock['RESULT']['BASKET'] as $bid => &$basketItem)
			{
				if ($bid != $basketItemId)
				{
					continue;
				}

				$basketItem['APPLY'] = 'N';
			}
		}
	}

	/**
	 * Calculate step discount result by exist order.
	 *
	 * @return Result
	 */
	protected function calculateApplySaleDiscountResult()
	{
		$result = new Result;

		$this->orderData['DISCOUNT_RESULT'] = Discount\Actions::getActionResult();
		if (!empty($this->orderData['DISCOUNT_RESULT']) && is_array($this->orderData['DISCOUNT_RESULT']))
			$stepResult = self::getStepResult($this->orderData);
		else
			$stepResult = self::getStepResultOld($this->orderData, $this->currentStep['oldData']);
		$applied = !empty($stepResult);

		$orderDiscountId = 0;
		$orderCouponId = '';

		if ($applied)
		{
			$this->correctStepResult($stepResult, $this->discountsCache[$this->currentStep['discountId']]);

			$orderDiscountId = $this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$this->currentStep['discountIndex']]['DISCOUNT_ID'];
			$orderCouponId = $this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$this->currentStep['discountIndex']]['COUPON_ID'];
		}

		unset($this->orderData['DISCOUNT_RESULT']);

		if ($applied)
		{
			if (
				(
					!empty($this->applyResult['DISCOUNT_LIST'][$orderDiscountId])
					&& $this->applyResult['DISCOUNT_LIST'][$orderDiscountId] == 'N'
				)
				||
				(
					$orderCouponId != ''
					&& !empty($this->applyResult['COUPON_LIST'][$orderCouponId])
					&& $this->applyResult['COUPON_LIST'][$orderCouponId] == 'N'
				)
			)
			{
				$this->orderData = $this->currentStep['oldData'];
				if (!empty($stepResult['BASKET']))
				{
					foreach ($stepResult['BASKET'] as &$basketItem)
						$basketItem['APPLY'] = 'N';
					unset($basketItem);
				}
				if (!empty($stepResult['DELIVERY']))
					$stepResult['DELIVERY']['APPLY'] = 'N';
			}
			else
			{
				if (!empty($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$this->currentStep['discountIndex']]['RESULT']))
				{
					$existDiscountResult = $this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$this->currentStep['discountIndex']]['RESULT'];
					if (!empty($existDiscountResult['BASKET']))
					{
						$basketCodeList = $this->getBasketCodes(false);
						if (!empty($basketCodeList))
						{
							foreach ($basketCodeList as &$basketCode)
							{
								if ($this->isCustomPriceByCode($basketCode))
									continue;
								if (isset($existDiscountResult['BASKET'][$basketCode]))
								{
									$disable = ($existDiscountResult['BASKET'][$basketCode]['APPLY'] == 'N');
									if (isset($this->applyResult['BASKET'][$basketCode][$orderDiscountId]))
									{
										$applyDisable = ($this->applyResult['BASKET'][$basketCode][$orderDiscountId] == 'N');
										if ($disable != $applyDisable)
											$disable = $applyDisable;
										unset($applyDisable);
									}
									if ($disable)
									{
										$stepResult['BASKET'][$basketCode]['APPLY'] = 'N';
										$this->orderData['BASKET_ITEMS'][$basketCode] = $this->currentStep['oldData']['BASKET_ITEMS'][$basketCode];
									}
									else
									{
										$stepResult['BASKET'][$basketCode]['APPLY'] = 'Y';
									}
								}
							}
							unset($disable, $basketCode);
						}
					}
					if (!empty($existDiscountResult['DELIVERY']))
					{
						$disable = ($existDiscountResult['DELIVERY']['APPLY'] == 'N');
						if (!empty($this->applyResult['DELIVERY'][$orderDiscountId]))
						{
							$applyDisable = ($this->applyResult['DELIVERY'][$orderDiscountId] == 'N');
							if ($disable != $applyDisable)
								$disable = $applyDisable;
							unset($applyDisable);
						}
						if ($disable)
						{
							$this->orderData['PRICE_DELIVERY'] = $this->currentStep['oldData']['PRICE_DELIVERY'];
							$this->orderData['PRICE_DELIVERY_DIFF'] = $this->currentStep['oldData']['PRICE_DELIVERY_DIFF'];
							$stepResult['DELIVERY']['APPLY'] = 'N';
						}
						else
						{
							$stepResult['DELIVERY']['APPLY'] = 'Y';
						}
						unset($disable);
					}
				}
			}
		}

		if ($applied && $orderCouponId != '')
		{
			$couponApply = DiscountCouponsManager::setApply($this->couponsCache[$orderCouponId]['COUPON'], $stepResult);
			unset($couponApply);
		}

		if ($applied)
		{
			$this->mergeDiscountActionResult($this->currentStep['discountIndex'], $stepResult);
		}
		else
		{
			if (!empty($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$this->currentStep['discountIndex']]))
				$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$this->currentStep['discountIndex']]['RESULT'] = array();
		}

		return $result;
	}

	/* rounding tools */

	/**
	 * Return order round apply mode.
	 * @internal
	 *
	 * @return int
	 */
	protected function getRoundMode()
	{
		return $this->roundApplyMode;
	}

	/**
	 * Return true, if selected check round apply mode.
	 * @internal
	 *
	 * @param int $mode		Checked mode.
	 * @return bool
	 */
	protected function isRoundMode($mode)
	{
		return $this->roundApplyMode == $mode;
	}

	/**
	 * Load round apply config for exist order.
	 * @internal
	 *
	 * @return void
	 */
	protected function loadRoundConfig()
	{
		$defaultApplyMode = self::ROUND_MODE_FINAL_PRICE;
		$this->roundApplyMode = $defaultApplyMode;
		$this->roundApplyConfig = array();

		if ($this->isOrderExists() && !$this->isOrderNew() && $this->getUseMode() != self::USE_MODE_FULL)
		{
			$orderId = $this->getOrder()->getId();
			$data = Internals\OrderDiscountDataTable::getList(array(
				'select' => array('*'),
				'filter' => array(
					'=ORDER_ID' => $orderId,
					'=ENTITY_TYPE' => Internals\OrderDiscountDataTable::ENTITY_TYPE_ROUND,
					'=ENTITY_ID' => $orderId
				)
			))->fetch();
			if (empty($data))
				return;

			$entityData = $data['ENTITY_DATA'];
			unset($data, $orderId);

			if (
				is_array($entityData)
				&& isset($entityData['MODE'])
			)
			{
				$this->roundApplyMode = (int)$entityData['MODE'];
				if ($this->isRoundMode(self::ROUND_MODE_SALE_DISCOUNT))
					$this->roundApplyConfig = (isset($entityData['CONFIG']) ? $entityData['CONFIG'] : array());
			}
		}

		if (
			$this->roundApplyMode != self::ROUND_MODE_BASKET_DISCOUNT
			&& $this->roundApplyMode != self::ROUND_MODE_SALE_DISCOUNT
			&& $this->roundApplyMode != self::ROUND_MODE_FINAL_PRICE
		)
			$this->roundApplyMode = null;
		if (!is_array($this->roundApplyConfig))
			$this->roundApplyConfig = array();
		unset($defaultApplyMode);
	}

	/**
	 * Set discount index for use round. Only for sale discount mode.
	 * @internal
	 *
	 * @param string $entity		Entity id.
	 * @param array $index			Index data.
	 * @return void
	 */
	protected function setRoundIndex($entity, array $index)
	{
		if (!$this->isRoundMode(self::ROUND_MODE_SALE_DISCOUNT))
			return;
		if (!isset($index['DISCOUNT_INDEX']) || !isset($index['DISCOUNT_ID']))
			return;
		if (!isset($this->roundApplyConfig[$this->discountResultCounter]))
			$this->roundApplyConfig[$this->discountResultCounter] = array();
		$this->roundApplyConfig[$this->discountResultCounter][$entity] = $index;
	}

	/**
	 * Return index data for use round.
	 * @internal
	 *
	 * @param string $entity			Entity id.
	 * @param null|int $applyCounter	Apply block counter.
	 * @return null|array
	 */
	protected function getRoundIndex($entity, $applyCounter = null)
	{
		if (!$this->isRoundMode(self::ROUND_MODE_SALE_DISCOUNT))
			return null;
		if ($applyCounter === null)
			$applyCounter = $this->discountResultCounter;
		return (isset($this->roundApplyConfig[$applyCounter][$entity]) ? $this->roundApplyConfig[$applyCounter][$entity] : null);
	}

	/**
	 * Round prices.
	 *
	 * @return void
	 */
	protected function roundFullBasketPrices()
	{
		$basketCodeList = $this->getBasketCodes(true);
		if (!empty($basketCodeList))
		{
			$roundBlock = &$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['BASKET_ROUND'];
			$orderData = $this->orderData;
			unset($orderData['BASKET_ITEMS']);
			$basket = array_intersect_key(
				$this->orderData['BASKET_ITEMS'],
				array_fill_keys($basketCodeList, true)
			);
			$result = OrderDiscountManager::roundBasket(
				$basket,
				array(),
				$orderData
			);
			foreach ($result as $basketCode => $roundResult)
			{
				if (empty($roundResult) || !is_array($roundResult))
					continue;
				if (!isset($this->orderData['BASKET_ITEMS'][$basketCode]))
					continue;
				$this->orderData['BASKET_ITEMS'][$basketCode]['PRICE'] = $roundResult['PRICE'];
				$this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_PRICE'] = $roundResult['DISCOUNT_PRICE'];

				$roundBlock[$basketCode] = array(
					'APPLY' => 'Y',
					'ROUND_RULE' => $roundResult['ROUND_RULE']
				);
			}
			unset($basketCode, $roundResult, $result);
			unset($basket, $orderData);
			unset($roundBlock);
		}
		unset($basketCodeList);
	}

	/**
	 * Round prices.
	 *
	 * @return void
	 */
	protected function roundApplyBasketPrices()
	{
		if (empty($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['BASKET_ROUND']))
			return;

		$basketCodeList = $this->getBasketCodes(false);
		if (!empty($basketCodeList))
		{
			$roundBlock = &$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['BASKET_ROUND'];
			$basket = array();
			$roundData = array();
			foreach ($basketCodeList as $basketCode)
			{
				if (empty($roundBlock[$basketCode]))
					continue;
				if ($roundBlock[$basketCode]['APPLY'] != 'Y')
					continue;
				$basket[$basketCode] = $this->orderData['BASKET_ITEMS'][$basketCode];
				$roundData[$basketCode] = $roundBlock[$basketCode]['ROUND_RULE'];
			}
			unset($basketCode);

			if (!empty($basket))
			{
				$orderData = $this->orderData;
				unset($orderData['BASKET_ITEMS']);
				$result = OrderDiscountManager::roundBasket(
					$basket,
					$roundData,
					$orderData
				);
				foreach ($result as $basketCode => $roundResult)
				{
					if (empty($roundResult) || !is_array($roundResult))
						continue;
					if (!isset($this->orderData['BASKET_ITEMS'][$basketCode]))
						continue;
					$this->orderData['BASKET_ITEMS'][$basketCode]['PRICE'] = $roundResult['PRICE'];
					$this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_PRICE'] = $roundResult['DISCOUNT_PRICE'];
				}
				unset($basketCode, $roundResult, $result);
				unset($orderData);
			}
			unset($roundData, $basket);

			unset($roundBlock);
		}
		unset($basketCodeList);
	}

	/**
	 * Round only changed prices.
	 *
	 * @return void
	 */
	protected function roundChangedBasketPrices()
	{
		$basketCodeList = array();
		$applyBlock = $this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter];
		switch ($this->getUseMode())
		{
			case self::USE_MODE_APPLY:
				if (!empty($applyBlock['BASKET']))
				{
					foreach (array_keys($applyBlock['BASKET']) as $basketCode)
					{
						$basketCodeList[$basketCode] = $basketCode;
					}
					unset($basketCode);
				}
				if (!empty($applyBlock['ORDER']))
				{
					foreach ($applyBlock['ORDER'] as $discount)
					{
						if (empty($discount['RESULT']['BASKET']))
							continue;
						foreach (array_keys($discount['RESULT']['BASKET']) as $basketCode)
						{
							$basketCodeList[$basketCode] = $basketCode;
						}
					}
					unset($basketCode, $discount);
				}
				break;
			case self::USE_MODE_MIXED:
				if (!empty($applyBlock['BASKET']))
				{
					foreach (array_keys($applyBlock['BASKET']) as $basketCode)
					{
						$basketCodeList[$basketCode] = $basketCode;
					}
					unset($basketCode);
				}
				if (!empty($applyBlock['ORDER']))
				{
					foreach ($applyBlock['ORDER'] as $discount)
					{
						if (empty($discount['RESULT']['BASKET']))
							continue;
						foreach (array_keys($discount['RESULT']['BASKET']) as $basketCode)
						{
							$basketCodeList[$basketCode] = $basketCode;
						}
					}
					unset($basketCode, $discount);
				}
				foreach ($this->getBasketCodes(true) as $basketCode)
					$basketCodeList[$basketCode] = $basketCode;
				break;
		}

		if (!empty($basketCodeList))
		{
			$roundBlock = &$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['BASKET_ROUND'];
			$orderData = $this->orderData;
			unset($orderData['BASKET_ITEMS']);
			$basket = array_intersect_key(
				$this->orderData['BASKET_ITEMS'],
				array_fill_keys($basketCodeList, true)
			);

			$result = OrderDiscountManager::roundBasket(
				$basket,
				array(),
				$orderData
			);

			foreach ($result as $basketCode => $roundResult)
			{
				if (empty($roundResult) || !is_array($roundResult))
					continue;
				if (!isset($this->orderData['BASKET_ITEMS'][$basketCode]))
					continue;
				$this->orderData['BASKET_ITEMS'][$basketCode]['PRICE'] = $roundResult['PRICE'];
				$this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_PRICE'] = $roundResult['DISCOUNT_PRICE'];

				$roundBlock[$basketCode] = array(
					'APPLY' => 'Y',
					'ROUND_RULE' => $roundResult['ROUND_RULE']
				);
			}
			unset($basketCode, $roundResult, $result);
			unset($storageClassName);
			unset($basket, $orderData);
			unset($roundBlock);
		}
		unset($basketCodeList);
	}

	/**
	 * Round prices in sale discount mode for new order.
	 * @internal
	 *
	 * @param array $index		Index data.
	 * @return void
	 */
	protected function roundFullBasketPriceByIndex(array $index)
	{
		if (!$this->isRoundMode(self::ROUND_MODE_SALE_DISCOUNT))
			return;
		if ($this->getUseMode() != self::USE_MODE_FULL)
			return;

		$this->roundFullBasketPrices();
		if (!empty($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['BASKET_ROUND']))
			$this->setRoundIndex('BASKET_ROUND', $index);
	}

	/**
	 * Round prices in sale discount mode for exist order.
	 * @internal
	 *
	 * @param array $index		Index data.
	 * @return void
	 */
	protected function roundApplyBasketPricesByIndex(array $index)
	{
		if (!isset($index['DISCOUNT_INDEX']))
			return;
		if (!$this->isRoundMode(self::ROUND_MODE_SALE_DISCOUNT))
			return;
		if ($this->getUseMode() != self::USE_MODE_APPLY && $this->getUseMode() != self::USE_MODE_MIXED)
			return;

		$roundConfig = $this->getRoundIndex('BASKET_ROUND');
		if ($roundConfig === null)
			return;
		if ($roundConfig['DISCOUNT_INDEX'] != $index['DISCOUNT_INDEX'])
			return;
		$this->roundApplyBasketPrices();
	}

	/* rounding tools finish */

	/**
	 * Convert discount for saving in order.
	 *
	 * @param array $discount			Raw discount data.
	 * @return Result
	 */
	protected function convertDiscount($discount)
	{
		$result = new Result;

		$discountResult = OrderDiscountManager::saveDiscount($discount, false);
		if (!$discountResult->isSuccess())
		{
			$result->addErrors($discountResult->getErrors());
			unset($discountResult);
			return $result;
		}
		$orderDiscountId = $discountResult->getId();
		$discountData = $discountResult->getData();
		$resultData = array(
			'ORDER_DISCOUNT_ID' => $orderDiscountId,
			'USE_COUPONS' => $discountData['USE_COUPONS'],
			'MODULE_ID' => $discountData['MODULE_ID'],
			'DISCOUNT_ID' => $discountData['DISCOUNT_ID']
		);
		if (!isset($this->discountsCache[$orderDiscountId]))
		{
			$discountData['ACTIONS_DESCR_DATA'] = false;
			if (!empty($discountData['ACTIONS_DESCR']) && is_array($discountData['ACTIONS_DESCR']))
			{
				$discountData['ACTIONS_DESCR_DATA'] = $discountData['ACTIONS_DESCR'];
				$discountData['ACTIONS_DESCR'] = self::formatDescription($discountData['ACTIONS_DESCR']);
			}
			else
			{
				$discountData['ACTIONS_DESCR'] = false;
			}
			if (empty($discountData['ACTIONS_DESCR']))
			{
				$discountData['ACTIONS_DESCR'] = false;
				$discountData['ACTIONS_DESCR_DATA'] = false;
			}
			$this->discountsCache[$orderDiscountId] = $discountData;
		}

		$result->setId($orderDiscountId);
		$result->setData($resultData);
		unset($discountData, $resultData, $orderDiscountId);

		return $result;
	}

	/**
	 * Convert coupon for saving in order.
	 *
	 * @param string|array $coupon			Coupon.
	 * @param int $discount					Order discount id.
	 * @return Result
	 */
	protected function convertCoupon($coupon, $discount)
	{
		$result = new Result;

		if (!is_array($coupon))
		{
			$couponData = DiscountCouponsManager::getEnteredCoupon($coupon, true);
			if (empty($couponData))
			{
				$result->addError(new Main\Entity\EntityError(
					Loc::getMessage('BX_SALE_DISCOUNT_ERR_COUPON_NOT_FOUND'),
					self::ERROR_ID
				));
				return $result;
			}
			$coupon = array(
				'COUPON' => $couponData['COUPON'],
				'TYPE' => $couponData['TYPE'],
				'COUPON_ID' => $couponData['ID'],
				'DATA' => $couponData
			);
			unset($couponData);
		}
		$coupon['ORDER_DISCOUNT_ID'] = $discount;
		$coupon['ID'] = 0;

		$orderCouponId = $coupon['COUPON'];
		if (!isset($this->couponsCache[$orderCouponId]))
			$this->couponsCache[$orderCouponId] = $coupon;
		$result->setId($orderCouponId);
		$result->setData($coupon);
		unset($coupon, $orderCouponId);
		return $result;
	}

	/**
	 * Returns result after one discount.
	 *
	 * @param array $order			Order current data.
	 * @return array
	 */
	protected static function getStepResult($order)
	{
		$result = array();
		$stepResult = &$order['DISCOUNT_RESULT'];
		if (!empty($stepResult['DELIVERY']) && is_array($stepResult['DELIVERY']))
		{
			$result['DELIVERY'] = array(
				'APPLY' => 'Y',
				'DELIVERY_ID' => (isset($order['DELIVERY_ID']) ? $order['DELIVERY_ID'] : false),
				'SHIPMENT_CODE' => (isset($order['SHIPMENT_CODE']) ? $order['SHIPMENT_CODE'] : false),
				'DESCR' => OrderDiscountManager::formatArrayDescription($stepResult['DELIVERY']),
				'DESCR_DATA' => $stepResult['DELIVERY'],
				'ACTION_BLOCK_LIST' => array_keys($stepResult['DELIVERY'])
			);
			if (is_array($result['DELIVERY']['DESCR']))
				$result['DELIVERY']['DESCR'] = implode(', ', $result['DELIVERY']['DESCR']);
		}
		if (!empty($stepResult['BASKET']) && is_array($stepResult['BASKET']))
		{
			if (!isset($result['BASKET']))
				$result['BASKET'] = array();
			foreach ($stepResult['BASKET'] as $basketCode => $basketResult)
			{
				$result['BASKET'][$basketCode] = array(
					'APPLY' => 'Y',
					'DESCR' => OrderDiscountManager::formatArrayDescription($basketResult),
					'DESCR_DATA' => $basketResult,
					'MODULE' => $order['BASKET_ITEMS'][$basketCode]['MODULE'],
					'PRODUCT_ID' => $order['BASKET_ITEMS'][$basketCode]['PRODUCT_ID'],
					'BASKET_ID' => (isset($order['BASKET_ITEMS'][$basketCode]['ID']) ? $order['BASKET_ITEMS'][$basketCode]['ID'] : $basketCode),
					'ACTION_BLOCK_LIST' => array_keys($basketResult)
				);
				if (is_array($result['BASKET'][$basketCode]['DESCR']))
					$result['BASKET'][$basketCode]['DESCR'] = implode(', ', $result['BASKET'][$basketCode]['DESCR']);
			}
			unset($basketCode, $basketResult);
		}
		unset($stepResult);

		return $result;
	}

	/**
	 * Returns result after one discount in old format.
	 *
	 * @param array $currentOrder			Current order data.
	 * @param array $oldOrder				Old order data.
	 * @return array
	 */
	protected static function getStepResultOld($currentOrder, $oldOrder)
	{
		$result = array();
		if (isset($oldOrder['PRICE_DELIVERY']) && isset($currentOrder['PRICE_DELIVERY']))
		{
			if ($oldOrder['PRICE_DELIVERY'] != $currentOrder['PRICE_DELIVERY'])
			{
				$descr = OrderDiscountManager::createSimpleDescription($currentOrder['PRICE_DELIVERY'], $oldOrder['PRICE_DELIVERY'], $oldOrder['CURRENCY']);
				$result['DELIVERY'] = array(
					'APPLY' => 'Y',
					'DELIVERY_ID' => (isset($currentOrder['DELIVERY_ID']) ? $currentOrder['DELIVERY_ID'] : false),
					'SHIPMENT_CODE' => (isset($currentOrder['SHIPMENT_CODE']) ? $currentOrder['SHIPMENT_CODE'] : false),
					'DESCR' => OrderDiscountManager::formatArrayDescription($descr),
					'DESCR_DATA' => $descr
				);
				unset($descr);
				if (is_array($result['DELIVERY']['DESCR']))
					$result['DELIVERY']['DESCR'] = implode(', ', $result['DELIVERY']['DESCR']);
			}
		}
		if (!empty($oldOrder['BASKET_ITEMS']) && !empty($currentOrder['BASKET_ITEMS']))
		{
			foreach ($oldOrder['BASKET_ITEMS'] as $basketCode => $item)
			{
				if (!isset($currentOrder['BASKET_ITEMS'][$basketCode]))
					continue;
				if ($item['PRICE'] != $currentOrder['BASKET_ITEMS'][$basketCode]['PRICE'])
				{
					if (!isset($result['BASKET']))
						$result['BASKET'] = array();
					$descr = OrderDiscountManager::createSimpleDescription($currentOrder['BASKET_ITEMS'][$basketCode]['PRICE'], $item['PRICE'], $oldOrder['CURRENCY']);
					$result['BASKET'][$basketCode] = array(
						'APPLY' => 'Y',
						'DESCR' => OrderDiscountManager::formatArrayDescription($descr),
						'DESCR_DATA' => $descr,
						'MODULE' => $currentOrder['BASKET_ITEMS'][$basketCode]['MODULE'],
						'PRODUCT_ID' => $currentOrder['BASKET_ITEMS'][$basketCode]['PRODUCT_ID'],
						'BASKET_ID' => (isset($currentOrder['BASKET_ITEMS'][$basketCode]['ID']) ? $currentOrder['BASKET_ITEMS'][$basketCode]['ID'] : $basketCode)
					);
					unset($descr);
					if (is_array($result['BASKET'][$basketCode]['DESCR']))
						$result['BASKET'][$basketCode]['DESCR'] = implode(', ', $result['BASKET'][$basketCode]['DESCR']);
				}
			}
		}
		return $result;
	}

	/**
	 * Correct data for exotic coupon.
	 *
	 * @param array &$stepResult			Currenct discount result.
	 * @param array $discount				Discount data.
	 * @return void
	 */
	protected function correctStepResult(&$stepResult, $discount)
	{
		if ($discount['USE_COUPONS'] == 'Y' && !empty($discount['COUPON']))
		{
			if (
				$discount['COUPON']['TYPE'] == Internals\DiscountCouponTable::TYPE_BASKET_ROW &&
				(!empty($stepResult['BASKET']) && count($stepResult['BASKET']) > 1)
			)
			{
				$maxPrice = 0;
				$maxKey = -1;
				$basketKeys = array();
				foreach ($stepResult['BASKET'] as $key => $row)
				{
					$basketKeys[$key] = $key;
					if ($maxPrice < $this->currentStep['oldData']['BASKET_ITEMS'][$key]['PRICE'])
					{
						$maxPrice = $this->currentStep['oldData']['BASKET_ITEMS'][$key]['PRICE'];
						$maxKey = $key;
					}
				}
				unset($basketKeys[$maxKey]);
				foreach ($basketKeys as $key => $row)
				{
					unset($stepResult['BASKET'][$key]);
					$this->orderData['BASKET_ITEMS'][$row] = $this->currentStep['oldData']['BASKET_ITEMS'][$row];
				}
				unset($row, $key);
			}
		}
	}

	/* discount action reference tools */

	/**
	 * Fill additional discount data.
	 *
	 * @param int $orderDiscountId	Converted discount id.
	 * @param array $data Discount	data.
	 *
	 * @return void
	 */
	protected function setDiscountStoredActionData($orderDiscountId, array $data)
	{
		$orderDiscountId = (int)$orderDiscountId;
		if (!isset($this->discountsCache[$orderDiscountId]))
			return;
		if (empty($data))
			return;
		if (!isset($this->discountStoredActionData[$this->discountResultCounter]))
			$this->discountStoredActionData[$this->discountResultCounter] = array();
		$this->discountStoredActionData[$this->discountResultCounter][$orderDiscountId] = $data;
	}

	/**
	 * Returns stored action data for discount.
	 *
	 * @param int $orderDiscountId Converted discount id.
	 * @return array|null
	 */
	protected function getDiscountStoredActionData($orderDiscountId)
	{
		$orderDiscountId = (int)$orderDiscountId;
		if (isset($this->discountStoredActionData[$this->discountResultCounter][$orderDiscountId]))
			return $this->discountStoredActionData[$this->discountResultCounter][$orderDiscountId];
		return null;
	}

	/* discount action reference tools finish */

	/**
	 * Return true, if exist apply result from form for basket.
	 *
	 * @return bool
	 */
	protected function isBasketApplyResultExist()
	{
		return (
			!empty($this->applyResult['DISCOUNT_LIST'])
			|| !empty($this->applyResult['COUPON_LIST'])
			|| !empty($this->applyResult['BASKET'])
		);
	}

	/**
	 * Returns discount and coupon list.
	 *
	 * @return void
	 */
	protected function getApplyDiscounts()
	{
		$discountApply = array();
		$couponApply = array();

		if (!empty($this->discountsCache))
		{
			foreach ($this->discountsCache as $id => $discount)
			{
				$this->discountResult['DISCOUNT_LIST'][$id] = array(
					'ID' => $id,
					'NAME' => $discount['NAME'],
					'MODULE_ID' => $discount['MODULE_ID'],
					'DISCOUNT_ID' => $discount['ID'],
					'REAL_DISCOUNT_ID' => $discount['DISCOUNT_ID'],
					'USE_COUPONS' => $discount['USE_COUPONS'],
					'ACTIONS_DESCR' => $discount['ACTIONS_DESCR'],
					'ACTIONS_DESCR_DATA' => $discount['ACTIONS_DESCR_DATA'],
					'APPLY' => 'N',
					'EDIT_PAGE_URL' => $discount['EDIT_PAGE_URL']
				);
				$discountApply[$id] = &$this->discountResult['DISCOUNT_LIST'][$id];
			}
			unset($id, $discount);
		}

		if (!empty($this->couponsCache))
		{
			foreach ($this->couponsCache as $id => $coupon)
			{
				$this->discountResult['COUPON_LIST'][$id] = $coupon;
				$this->discountResult['COUPON_LIST'][$id]['APPLY'] = 'N';
				$couponApply[$id] = &$this->discountResult['COUPON_LIST'][$id];
			}
			unset($id, $coupon);
		}

		if (empty($this->discountResult['APPLY_BLOCKS']))
		{
			unset($discountApply, $couponApply);
			return;
		}

		foreach ($this->discountResult['APPLY_BLOCKS'] as $counter => $applyBlock)
		{
			if (!empty($applyBlock['BASKET']))
			{
				foreach ($applyBlock['BASKET'] as $basketCode => $discountList)
				{
					foreach ($discountList as $discount)
					{
						if ($discount['RESULT']['APPLY'] == 'Y')
						{
							if (isset($discountApply[$discount['DISCOUNT_ID']]))
								$discountApply[$discount['DISCOUNT_ID']]['APPLY'] = 'Y';
							if (isset($couponApply[$discount['COUPON_ID']]))
								$couponApply[$discount['COUPON_ID']]['APPLY'] = 'Y';
						}
					}
					unset($discount);
				}
				unset($basketCode, $discountList);
			}

			if (!empty($applyBlock['ORDER']))
			{
				foreach ($applyBlock['ORDER'] as $discount)
				{
					if (!empty($discount['RESULT']['BASKET']))
					{
						foreach ($discount['RESULT']['BASKET'] as $basketCode => $applyList)
						{
							if ($applyList['APPLY'] == 'Y')
							{
								if (isset($discountApply[$discount['DISCOUNT_ID']]))
									$discountApply[$discount['DISCOUNT_ID']]['APPLY'] = 'Y';
								if (isset($couponApply[$discount['COUPON_ID']]))
									$couponApply[$discount['COUPON_ID']]['APPLY'] = 'Y';
							}
						}
						unset($basketCode, $applyList);
					}
					if (!empty($discount['RESULT']['DELIVERY']) && $discount['RESULT']['DELIVERY']['APPLY'] == 'Y')
					{
						if (isset($discountApply[$discount['DISCOUNT_ID']]))
							$discountApply[$discount['DISCOUNT_ID']]['APPLY'] = 'Y';
						if (isset($couponApply[$discount['COUPON_ID']]))
							$couponApply[$discount['COUPON_ID']]['APPLY'] = 'Y';
					}
				}
				unset($discount);
			}
		}
		unset($counter, $applyBlock);

		unset($discountApply, $couponApply);
	}

	/**
	 * Fill prices in apply results.
	 *
	 * @return void
	 */
	protected function getApplyPrices()
	{
		$this->normalizeDiscountResult();

		$basket = array();
		if (!empty($this->orderData['BASKET_ITEMS']))
		{
			foreach ($this->orderData['BASKET_ITEMS'] as $basketCode => $basketItem)
			{
				$basket[$basketCode] = array(
					'BASE_PRICE' => $basketItem['BASE_PRICE'],
					'PRICE' => $basketItem['PRICE'],
					'DISCOUNT' => $basketItem['DISCOUNT_PRICE']
				);
			}
			unset($basketCode, $basketItem);
		}

		$this->discountResult['PRICES'] = array(
			'BASKET' => $basket,
			'DELIVERY' => $this->getApplyDeliveryPrice()
		);
		unset($basket);
	}

	/**
	 * Returns delivery price data.
	 *
	 * @return array
	 */
	protected function getApplyDeliveryPrice()
	{
		return array(
			'BASE_PRICE' => $this->orderData['BASE_PRICE_DELIVERY'],
			'PRICE' => $this->orderData['PRICE_DELIVERY'],
			'DISCOUNT' => $this->orderData['PRICE_DELIVERY_DIFF']
		);
	}

	/**
	 * Get discount delivery list.
	 *
	 * @return void
	 */
	protected function getApplyDeliveryList()
	{
		$delivery = array();
		$shipment = array();

		if (!empty($this->discountResult['APPLY_BLOCKS']))
		{
			foreach ($this->discountResult['APPLY_BLOCKS'] as $counter => $applyBlock)
			{
				if (!empty($applyBlock['ORDER']))
				{
					foreach ($applyBlock['ORDER'] as &$discount)
					{
						if (empty($discount['RESULT']['DELIVERY']))
							continue;
						$delivery[$discount['RESULT']['DELIVERY']['DELIVERY_ID']] = $discount['RESULT']['DELIVERY']['DELIVERY_ID'];
					}
					unset($discount);
				}
			}
			unset($counter, $applyBlock);
		}
		if ($this->shipment instanceof Shipment)
			$shipment[] = $this->shipment->getShipmentCode();

		$this->discountResult['DELIVERY_LIST'] = (
			empty($delivery)
			? array()
			: array_values($delivery)
		);

		$this->discountResult['SHIPMENT_LIST'] = $shipment;
	}

	/**
	 * Change result format.
	 *
	 * @return void
	 */
	protected function remakingDiscountResult()
	{
		$basket = array();
		$delivery = array();

		if (!empty($this->discountResult['APPLY_BLOCKS']))
		{
			foreach ($this->discountResult['APPLY_BLOCKS'] as $counter => $applyBlock)
			{
				if (!empty($applyBlock['BASKET']))
				{
					foreach ($applyBlock['BASKET'] as $basketCode => $discountList)
					{
						if (!isset($basket[$basketCode]))
							$basket[$basketCode] = array();
						foreach ($discountList as $discount)
						{
							$basket[$basketCode][] = array(
								'DISCOUNT_ID' => $discount['DISCOUNT_ID'],
								'COUPON_ID' => $discount['COUPON_ID'],
								'APPLY' => $discount['RESULT']['APPLY'],
								'DESCR' => $discount['RESULT']['DESCR']
							);
						}
						unset($discount);
					}
					unset($basketCode, $discountList);
				}

				if (!empty($applyBlock['ORDER']))
				{
					foreach ($applyBlock['ORDER'] as $discount)
					{
						if (!empty($discount['RESULT']['BASKET']))
						{
							foreach ($discount['RESULT']['BASKET'] as $basketCode => $applyList)
							{
								if (!isset($basket[$basketCode]))
									$basket[$basketCode] = array();
								$basket[$basketCode][] = array(
									'DISCOUNT_ID' => $discount['DISCOUNT_ID'],
									'COUPON_ID' => $discount['COUPON_ID'],
									'APPLY' => $applyList['APPLY'],
									'DESCR' => $applyList['DESCR']
								);
							}
							unset($basketCode, $applyList);
						}
						if (!empty($discount['RESULT']['DELIVERY']))
						{
							$delivery[] = array(
								'DISCOUNT_ID' => $discount['DISCOUNT_ID'],
								'COUPON_ID' => $discount['COUPON_ID'],
								'DELIVERY_ID' => $discount['RESULT']['DELIVERY']['DELIVERY_ID'],
								'APPLY' => $discount['RESULT']['DELIVERY']['APPLY'],
								'DESCR' => $discount['RESULT']['DELIVERY']['DESCR']
							);
						}
					}
					unset($discount);
				}
			}
			unset($counter, $applyBlock);
		}

		$this->discountResult['RESULT'] = array(
			'BASKET' => $basket,
			'DELIVERY' => $delivery
		);
	}

	/**
	 * Create correspondence between basket ids and basket codes.
	 *
	 * @return Result
	 */
	protected function getBasketTables()
	{
		$result = new Result;

		$this->forwardBasketTable = array();
		$this->reverseBasketTable = array();

		if (!$this->isBasketNotEmpty())
			return $result;

		$basket = $this->getBasket();
		/** @var BasketItem $basketItem */
		foreach ($basket as $basketItem)
		{
			$code = $basketItem->getBasketCode();
			$id = $basketItem->getField('ID');
			$this->forwardBasketTable[$code] = $id;
			$this->reverseBasketTable[$id] = $code;
			unset($id, $code);

			if ($basketItem->isBundleParent())
			{
				$bundle = $basketItem->getBundleCollection();
				if (empty($bundle))
				{
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_BASKET_BUNDLE_EMPTY'),
						self::ERROR_ID
					));
					break;
				}
				/** @var BasketItem $bundleItem */
				foreach ($bundle as $bundleItem)
				{
					$code = $bundleItem->getBasketCode();
					$id = $bundleItem->getField('ID');
					$this->forwardBasketTable[$code] = $id;
					$this->reverseBasketTable[$id] = $code;
					unset($id, $code);
				}
				unset($bundle, $bundleItem);
			}
		}
		unset($basketItem, $basket);

		return $result;
	}

	/**
	 * Returns exist custom price for basket item code.
	 *
	 * @param int $code			Basket code.
	 * @return bool
	 */
	protected function isCustomPriceByCode($code)
	{
		if (!empty($this->orderData['BASKET_ITEMS'][$code]['CUSTOM_PRICE']) && $this->orderData['BASKET_ITEMS'][$code]['CUSTOM_PRICE'] == 'Y')
			return true;
		return false;
	}

	/**
	 * Returns exist custom price for basket item.
	 *
	 * @param array $item			Basket item.
	 * @return bool
	 */
	protected static function isCustomPrice($item)
	{
		if (!empty($item['CUSTOM_PRICE']) && $item['CUSTOM_PRICE'] == 'Y')
			return true;
		return false;
	}

	/**
	 * Returns check item in set for basket item code.
	 *
	 * @param int $code			Basket code.
	 * @return bool
	 */
	protected function isInSetByCode($code)
	{
		if (!empty($this->orderData['BASKET_ITEMS'][$code]['IN_SET']) && $this->orderData['BASKET_ITEMS'][$code]['IN_SET'] == 'Y')
			return true;
		return false;
	}

	/**
	 * Returns check item in set for basket item.
	 *
	 * @param array $item			Basket item.
	 * @return bool
	 */
	protected static function isInSet($item)
	{
		if (!empty($item['IN_SET']) && $item['IN_SET'] == 'Y')
			return true;
		return false;
	}

	/**
	 * Returns exist new item in basket.
	 *
	 * @return bool
	 */
	protected function isMixedBasket()
	{
		$result = false;
		if (empty($this->orderData['BASKET_ITEMS']))
			return $result;

		foreach ($this->orderData['BASKET_ITEMS'] as $basketItem)
		{
			if (!isset($basketItem['ID']) || (int)$basketItem['ID'] <= 0)
			{
				$result = true;
				break;
			}
		}
		unset($basketItem);

		if (!$result)
		{
			if ($this->isOrderedBasketChanged())
				$result = true;
		}

		return $result;
	}

	/**
	 * Returns check new basket item for basket item code.
	 *
	 * @param int|string $code			Basket code.
	 * @return bool
	 */
	protected function isNewBasketItemByCode($code)
	{
		return (
			$this->getUseMode() == self::USE_MODE_FULL
			|| !isset($this->orderData['BASKET_ITEMS'][$code]['ID'])
			|| $this->orderData['BASKET_ITEMS'][$code]['ID'] <= 0
		);
	}

	/**
	 * Returns check new basket item for basket item.
	 *
	 * @param array $item			Basket item.
	 * @return bool
	 */
	protected static function isNewBasketItem($item)
	{
		return (
			!isset($item['ID'])
			|| $item['ID'] <= 0
		);
	}

	/**
	 * Return true if basket saved order changed (change PRODUCT_ID).
	 *
	 * @return bool
	 */
	protected function isOrderedBasketChanged()
	{
		$result = false;
		if ($this->isOrderExists() && !$this->isOrderNew() && $this->isBasketNotEmpty())
		{
			$basket = $this->getBasket();
			/** @var BasketItem $basketItem */
			foreach ($basket as $basketItem)
			{
				if (!$basketItem->isChanged())
					continue;
				/** @noinspection PhpInternalEntityUsedInspection */
				if (in_array('PRODUCT_ID', $basketItem->getFields()->getChangedKeys()))
				{
					$result = true;
					break;
				}
			}
			unset($basketItem, $basket);
		}
		return $result;
	}

	/**
	 * Return true if ordered basket item changed (change PRODUCT_ID).
	 *
	 * @param int $code			Basket code.
	 * @return bool
	 */
	protected function isBasketItemChanged($code)
	{
		$result = false;
		if ($this->isOrderExists() && !$this->isOrderNew() && $this->isBasketNotEmpty())
		{
			$basketItem = $this->getBasket()->getItemByBasketCode($code);
			if ($basketItem instanceof BasketItem)
			{
				/** @noinspection PhpInternalEntityUsedInspection */
				if (in_array('PRODUCT_ID', $basketItem->getFields()->getChangedKeys()))
					$result = true;
			}
		}
		return $result;
	}

	/**
	 * Returns basket codes for calculate.
	 *
	 * @param bool $full				Full or apply mode.
	 * @return array
	 */
	protected function getBasketCodes($full = true)
	{
		$result = array();
		if (empty($this->orderData['BASKET_ITEMS']))
			return $result;
		switch ($this->getUseMode())
		{
			case self::USE_MODE_FULL:
			case self::USE_MODE_COUPONS:
				foreach ($this->orderData['BASKET_ITEMS'] as $code => $item)
				{
					if ($this->isCustomPrice($item) || $this->isInSet($item))
						continue;
					$result[] = $code;
				}
				unset($code, $item);
				break;
			case self::USE_MODE_APPLY:
				foreach ($this->orderData['BASKET_ITEMS'] as $code => $item)
				{
					if (
						$this->isCustomPrice($item)
						|| $this->isNewBasketItem($item)
						|| $this->isBasketItemChanged($code)
						|| $this->isInSet($item)
					)
						continue;
					$result[] = $code;
				}
				unset($code, $item);
				break;
			case self::USE_MODE_MIXED:
				$full = ($full === true);
				if ($full)
				{
					foreach ($this->orderData['BASKET_ITEMS'] as $code => $item)
					{
						if (
							!$this->isCustomPrice($item)
							&& !$this->isInSet($item)
							&& ($this->isNewBasketItem($item) || $this->isBasketItemChanged($code))
						)
							$result[] = $code;
					}
					unset($code, $item);
				}
				else
				{
					foreach ($this->orderData['BASKET_ITEMS'] as $code => $item)
					{
						if (
							$this->isCustomPrice($item)
							|| $this->isNewBasketItem($item)
							|| $this->isBasketItemChanged($code)
							|| $this->isInSet($item)
						)
							continue;
						$result[] = $code;
					}
					unset($code, $item);
				}
				break;
		}

		return $result;
	}

	/**
	 * Merge discount actions result with old data.
	 *
	 * @param int $index				Discount index.
	 * @param array $stepResult			New result.
	 * @return void
	 */
	protected function mergeDiscountActionResult($index, $stepResult)
	{
		if (!isset($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$index]))
			return;
		if (empty($stepResult) || !is_array($stepResult))
			return;
		$basketKeys = array_keys($this->orderData['BASKET_ITEMS']);
		foreach ($basketKeys as &$basketCode)
		{
			if (!$this->isCustomPriceByCode($basketCode))
				continue;
			if (isset($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$index]['RESULT']['BASKET'][$basketCode]))
				unset($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$index]['RESULT']['BASKET'][$basketCode]);
		}
		unset($basketCode);
		if (isset($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$index]['RESULT']['DESCR_DATA']))
			unset($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$index]['RESULT']['DESCR_DATA']);
		if (isset($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$index]['RESULT']['DESCR']))
			unset($this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$index]['RESULT']['DESCR']);
		self::recursiveMerge($stepResult, $this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$index]['RESULT']);
		$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'][$index]['RESULT'] = $stepResult;
	}

	/**
	 * Fill empty discount result list.
	 *
	 * @return void
	 */
	protected function fillEmptyDiscountResult()
	{
		$this->discountResultCounter = 0;
		$this->discountResult = array(
			'APPLY_BLOCKS' => array(),
			'DISCOUNT_LIST' => array(),
			'COUPON_LIST' => array(),
			'DELIVERY_LIST' => array(),
			'SHIPMENT_LIST' => array()
		);
		$this->clearCurrentApplyBlock();
		$this->discountStoredActionData = array();
	}

	/**
	 * Filtered result order data.
	 *
	 * @return array
	 */
	protected function fillDiscountResult()
	{
		$this->normalizeDiscountResult();
		$orderKeys = array('PRICE_DELIVERY', 'PRICE_DELIVERY_DIFF', 'CURRENCY');
		$basketKeys = array('PRICE', 'DISCOUNT_PRICE', 'VAT_RATE', 'VAT_VALUE', 'CURRENCY');
		$result = array();
		foreach ($orderKeys as $key)
		{
			if (isset($this->orderData[$key]))
				$result[$key] = $this->orderData[$key];
		}
		unset($key);
		$result['DISCOUNT_PRICE'] = $result['PRICE_DELIVERY_DIFF'];
		unset($result['PRICE_DELIVERY_DIFF']);
		$result['BASKET_ITEMS'] = array();
		foreach ($this->orderData['BASKET_ITEMS'] as $index => $basketItem)
		{
			$result['BASKET_ITEMS'][$index] = array();
			foreach ($basketKeys as $key)
			{
				if (isset($basketItem[$key]))
					$result['BASKET_ITEMS'][$index][$key] = $basketItem[$key];
			}
			unset($key);
		}
		unset($index, $basketItem);

		$result['SHIPMENT'] = null;
		if ($this->shipment instanceof Shipment)
			$result['SHIPMENT'] = $this->shipment->getShipmentCode();

		return $result;
	}

	/**
	 * Internal. Fill current apply block empty data.
	 *
	 * @return void
	 */
	protected function clearCurrentApplyBlock()
	{
		$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter] = static::getEmptyApplyBlock();
	}
	/**
	 * Internal. Clear current step data.
	 *
	 * @return void
	 */
	protected function fillEmptyCurrentStep()
	{
		$this->currentStep = array(
			'oldData' => array(),
			'discount' => array(),
			'discountIndex' => null,
			'discountId' => 0,
			'result' => array(),
			'stop' => false,
		);
	}

	/**
	 * Internal. Fill current step data.
	 *
	 * @param array $data			Only not empty keys.
	 * @return void
	 */
	protected function fillCurrentStep($data)
	{
		$this->fillEmptyCurrentStep();
		if (!empty($data) && is_array($data))
		{
			foreach ($data as $key => $value)
				$this->currentStep[$key] = $value;
			unset($value, $key);
		}
	}

	/**
	 * Load from database need modules list for discounts.
	 *
	 * @return void
	 */
	protected function getDiscountModules()
	{
		if (empty($this->discountIds))
			return;
		$loadList = $this->discountIds;
		if (!empty($this->cacheDiscountModules))
		{
			$loadList = array();
			foreach ($this->discountIds as $discount)
			{
				if (!isset($this->cacheDiscountModules['sale'.$discount]))
					$loadList[] = $discount;
			}
			unset($discount);
		}
		if (empty($loadList))
			return;

		foreach ($loadList as &$discount)
			$this->cacheDiscountModules['sale'.$discount] = array();
		unset($discount);

		$moduleList = RuntimeCache\DiscountCache::getInstance()->getDiscountModules($loadList);
		if (!empty($moduleList))
		{
			foreach ($moduleList as $discount => $discountModule)
				$this->cacheDiscountModules['sale'.$discount] = $discountModule;
			unset($discount, $discountModule, $moduleList);
		}
		unset($moduleList);
	}

	/**
	 * Extend order data for discounts.
	 *
	 * @return void
	 */
	protected function extendOrderData()
	{
		if (empty($this->discountIds))
			return;

		$entityCacheKey = md5(serialize($this->discountIds));
		if (!isset($this->entityResultCache[$entityCacheKey]))
		{
			$this->entityResultCache[$entityCacheKey] = array();
			$this->entityList = RuntimeCache\DiscountCache::getInstance()->getDiscountEntities($this->discountIds);
			if (empty($this->entityList))
				return;

			$event = new Main\Event(
				'sale',
				self::EVENT_EXTEND_ORDER_DATA,
				array(
					'ORDER' => $this->orderData,
					'ENTITY' => $this->entityList
				)
			);
			$event->send();
			$this->entityResultCache[$entityCacheKey] = $event->getResults();
		}
		$resultList = $this->entityResultCache[$entityCacheKey];
		if (empty($resultList) || !is_array($resultList))
			return;
		/** @var Main\EventResult $eventResult */
		foreach ($resultList as &$eventResult)
		{
			if ($eventResult->getType() != Main\EventResult::SUCCESS)
				continue;

			$newData = $eventResult->getParameters();
			if (empty($newData) || !is_array($newData))
				continue;

			$this->modifyOrderData($newData);
		}
		unset($newData, $eventResult, $resultList);
	}

	/**
	 * Modify order data from handlers.
	 *
	 * @param array &$newData			New order data from handler.
	 * @return void
	 */
	protected function modifyOrderData(&$newData)
	{
		if (!empty($newData) && is_array($newData))
			self::recursiveMerge($this->orderData, $newData);
	}

	/**
	 * Return formatted discount description.
	 *
	 * @param array $descr				Description.
	 * @return array
	 */
	protected static function formatDescription($descr)
	{
		$result = array();
		if (empty($descr) || !is_array($descr))
			return $result;
		if (isset($descr['DELIVERY']))
		{
			$result['DELIVERY'] = array();
			foreach ($descr['DELIVERY'] as $index => $value)
			{
				$result['DELIVERY'][$index] = OrderDiscountManager::formatDescription($value);
				if ($result['DELIVERY'][$index] == false)
					unset($result['DELIVERY'][$index]);
			}
			unset($value, $index);
			if (!empty($result['DELIVERY']))
				$result['DELIVERY'] = implode(', ', $result['DELIVERY']);
		}
		if (isset($descr['BASKET']))
		{
			$result['BASKET'] = array();
			foreach ($descr['BASKET'] as $index => $value)
			{
				$result['BASKET'][$index] = OrderDiscountManager::formatDescription($value);
				if ($result['BASKET'][$index] == false)
					unset($result['BASKET'][$index]);
			}
			unset($value, $index);
			if (!empty($result['BASKET']))
				$result['BASKET'] = implode(', ', $result['BASKET']);
		}
		return $result;
	}

	/**
	 * Added keys from source array to destination array.
	 *
	 * @param array &$dest			Destination array.
	 * @param array $src			Source array.
	 * @return void
	 */
	protected static function recursiveMerge(&$dest, $src)
	{
		if (!is_array($dest) || !is_array($src))
			return;
		if (empty($dest))
		{
			$dest = $src;
			return;
		}
		foreach ($src as $key => $value)
		{
			if (!isset($dest[$key]))
			{
				$dest[$key] = $value;
				continue;
			}
			if (is_array($dest[$key]))
				self::recursiveMerge($dest[$key], $value);
		}
		unset($value, $key);
	}

	/**
	 * Fill basket prices from base prices.
	 *
	 * @return void
	 */
	protected function resetBasketPrices()
	{
		foreach ($this->orderData['BASKET_ITEMS'] as &$basketItem)
		{
			if (self::isCustomPrice($basketItem) || self::isInSet($basketItem))
				continue;
			$basketItem = self::resetPrice($basketItem);
		}
		unset($basketItem);
	}

	/**
	 * Load from database discount id for user groups.
	 *
	 * @param array $filter			Additional filter.
	 * @return void
	 * @throws Main\ArgumentException
	 */
	protected function loadDiscountByUserGroups(array $filter = array())
	{
		if (!array_key_exists('USER_ID', $this->orderData))
			return;
		$userGroups = $this->context->getUserGroups();
		$filter['@GROUP_ID'] = $userGroups;
		$filter['=ACTIVE'] = 'Y';
		$cacheKey = md5('U'.implode('_', $userGroups).'-F'.serialize($filter));
		if (!isset($this->discountByUserCache[$cacheKey]))
		{
			//RuntimeCache works only with basic filter.
			if(!array_diff_assoc($filter, array(
				'@GROUP_ID' => $userGroups,
				'=ACTIVE' => 'Y',
			)))
			{
				$this->discountByUserCache[$cacheKey] = RuntimeCache\DiscountCache::getInstance()->getDiscountIds($userGroups);
			}
			else
			{
				$discountCache = array();
				$groupDiscountIterator = Internals\DiscountGroupTable::getList(array(
					'select' => array('DISCOUNT_ID'),
					'filter' => $filter,
					'order' => array('DISCOUNT_ID' => 'ASC')
				));
				while ($groupDiscount = $groupDiscountIterator->fetch())
				{
					$groupDiscount['DISCOUNT_ID'] = (int)$groupDiscount['DISCOUNT_ID'];
					if ($groupDiscount['DISCOUNT_ID'] > 0)
						$discountCache[$groupDiscount['DISCOUNT_ID']] = $groupDiscount['DISCOUNT_ID'];
				}
				unset($groupDiscount, $groupDiscountIterator);
				if (!empty($discountCache))
					$this->discountByUserCache[$cacheKey] = $discountCache;
				unset($discountCache);
			}
		}
		$this->discountIds = $this->discountByUserCache[$cacheKey];
		unset($cacheKey, $userGroups);
	}

	/**
	 * Load discount modules.
	 *
	 * @param string|int $discount				Discount key.
	 * @return bool
	 * @throws Main\LoaderException
	 */
	protected function loadDiscountModules($discount)
	{
		$result = true;
		if (empty($this->cacheDiscountModules[$discount]))
			return $result;

		foreach ($this->cacheDiscountModules[$discount] as $moduleID)
		{
			if (!isset($this->loadedModules[$moduleID]))
				$this->loadedModules[$moduleID] = Main\Loader::includeModule($moduleID);
			if (!$this->loadedModules[$moduleID])
			{
				$result = false;
				break;
			}
		}
		unset($moduleID);

		return $result;
	}

	/**
	 * Load sale discount from database
	 *
	 * @return void
	 * @throws Main\ArgumentException
	 */
	protected function loadDiscountList()
	{
		if (empty($this->discountIds))
		{
			$this->discountIds = null;
			return;
		}

		$this->getDiscountModules();

		$couponList = DiscountCouponsManager::getForApply(array('MODULE_ID' => 'sale', 'DISCOUNT_ID' => $this->discountIds), array(), true);

		$this->saleDiscountCacheKey = md5('D'.implode('_', $this->discountIds));
		if (!empty($couponList))
			$this->saleDiscountCacheKey .= '-C'.implode('_', array_keys($couponList));

		$this->saleDiscountCacheKey .= '-MF'.implode('_', $this->executeModuleFilter);

		if (!isset($this->saleDiscountCache[$this->saleDiscountCacheKey]))
		{
			$currentList = RuntimeCache\DiscountCache::getInstance()->getDiscounts(
				$this->discountIds, $this->executeModuleFilter, $this->orderData['SITE_ID'], $couponList?: array()
			);

			if (!empty($currentList))
			{
				$evalCode = '';
				foreach (array_keys($currentList) as $index)
				{
					$discount = $currentList[$index];
					$code = 'sale'.$discount['ID'];
					if (!$this->loadDiscountModules($code))
					{
						unset($currentList[$index]);
						continue;
					}
					if (isset($this->cacheDiscountModules[$code]))
					{
						$currentList[$index]['MODULES'] = $this->cacheDiscountModules[$code];
					}
					if (!$this->enableCheckingPrediction)
					{
						if ($discount['UNPACK'] !== null)
							$evalCode .= '$currentList['.$index.'][\'UNPACK_EXECUTE\'] = '.$discount['UNPACK'].";\n";
					}
					else
					{
						if ($discount['PREDICTIONS_APP'] !== null && $discount['PREDICTIONS_APP'] !== '')
							$evalCode .= '$currentList['.$index.'][\'PREDICTIONS_APP_EXECUTE\'] = '.$discount['PREDICTIONS_APP'].";\n";
					}
					if ($discount['APPLICATION'] !== null)
						$evalCode .= '$currentList['.$index.'][\'APPLICATION_EXECUTE\'] = '.$discount['APPLICATION'].";\n";
				}
				unset($code, $discount, $index);

				if ($evalCode !== '')
				{
					if (version_compare(PHP_VERSION, '7.0.0', '>='))
					{
						try
						{
							eval($evalCode);
						}
						catch (\ParseError $e)
						{
							$this->showAdminError();
						}
					}
					else
					{
						eval($evalCode);
					}
				}
				unset($evalCode);
			}

			$this->saleDiscountCache[$this->saleDiscountCacheKey] = $currentList;
		}
		unset($couponList);
	}

	/**
	 * Execute sale discount list.
	 *
	 * @return Result
	 */
	protected function executeDiscountList()
	{
		$result = new Result;

		$roundApply = true;
		$saleDiscountOnly = $this->useOnlySaleDiscounts();
		$useMode = $this->getUseMode();
		if ($saleDiscountOnly)
		{
			if ($useMode == self::USE_MODE_FULL && $this->isRoundMode(self::ROUND_MODE_SALE_DISCOUNT))
				$roundApply = false;
		}

		$this->discountIds = array();
		if (empty($this->saleDiscountCacheKey) || empty($this->saleDiscountCache[$this->saleDiscountCacheKey]))
		{
			if (!$roundApply)
			{
				$this->roundFullBasketPriceByIndex(array(
					'DISCOUNT_INDEX' => -1,
					'DISCOUNT_ID' => 0
				));
			}
			return $result;
		}

		$currentList = $this->saleDiscountCache[$this->saleDiscountCacheKey];
		$this->discountIds = array_keys($currentList);
		$this->extendOrderData();

		Discount\Actions::clearAction();

		$blackList = array(
			'UNPACK_EXECUTE' => true,
			'APPLICATION_EXECUTE' => true,
			'PREDICTIONS_APP_EXECUTE' => true
		);

		$index = -1;
		$skipPriorityLevel = null;
		foreach ($currentList as $discountIndex => $discount)
		{
			if($skipPriorityLevel == $discount['PRIORITY'])
			{
				continue;
			}
			$skipPriorityLevel = null;

			$this->fillCurrentStep(array(
				'discount' => $discount,
				'cacheIndex' => $discountIndex
			));
			if (!$this->checkDiscountConditions())
				continue;

			$index++;
			if (!$roundApply && $discount['EXECUTE_MODULE'] == 'sale')
			{
				$this->roundFullBasketPriceByIndex(array(
					'DISCOUNT_INDEX' => $index,
					'DISCOUNT_ID' => $discount['ID']
				));
				$roundApply = true;
			}

			if ($useMode == self::USE_MODE_FULL && !isset($this->fullDiscountList[$discount['ID']]))
				$this->fullDiscountList[$discount['ID']] = array_diff_key($discount, $blackList);

			$actionsResult = $this->applySaleDiscount();
			if (!$actionsResult->isSuccess())
			{
				$result->addErrors($actionsResult->getErrors());
				unset($actionsResult);
				return $result;
			}

			if ($this->currentStep['stop'])
				break;

			if ($this->currentStep['stopLevel'])
			{
				$skipPriorityLevel = $discount['PRIORITY'];
			}
		}
		unset($discount, $currentList);
		$this->fillEmptyCurrentStep();

		if (!$roundApply)
		{
			$index++;
			$this->roundFullBasketPriceByIndex(array(
				'DISCOUNT_INDEX' => $index,
				'DISCOUNT_ID' => 0
			));
		}

		return $result;
	}

	/**
	 * Fill last discount flag for basket items. Only for basket or new order or refreshed order.
	 *
	 * @return void
	 */
	protected function fillBasketLastDiscount()
	{
		if ($this->getUseMode() != self::USE_MODE_FULL)
			return;
		$applyMode = self::getApplyMode();
		if ($applyMode == self::APPLY_MODE_ADD)
			return;

		$codeList = array_keys($this->orderData['BASKET_ITEMS']);
		switch ($applyMode)
		{
			case self::APPLY_MODE_DISABLE:
			case self::APPLY_MODE_FULL_DISABLE:
				foreach ($codeList as &$code)
				{
					if (isset($this->basketDiscountList[$code]) && !empty($this->basketDiscountList[$code]))
						$this->orderData['BASKET_ITEMS'][$code]['LAST_DISCOUNT'] = 'Y';
				}
				unset($code);
				break;
			case self::APPLY_MODE_LAST:
			case self::APPLY_MODE_FULL_LAST:
				foreach ($codeList as &$code)
				{
					if (!isset($this->basketDiscountList[$code]) || empty($this->basketDiscountList[$code]))
						continue;
					$lastDiscount = end($this->basketDiscountList[$code]);
					if (!empty($lastDiscount['LAST_DISCOUNT']) && $lastDiscount['LAST_DISCOUNT'] == 'Y')
						$this->orderData['BASKET_ITEMS'][$code]['LAST_DISCOUNT'] = 'Y';
				}
				unset($code);
				break;
		}
		unset($codeList, $applyMode);
	}

	/**
	 * Check last discount flag for basket items. Only for basket or new order or refreshed order.
	 *
	 * @return bool
	 */
	protected function isBasketLastDiscount()
	{
		$result = false;

		if ($this->getUseMode() != self::USE_MODE_FULL)
			return $result;

		$this->fillBasketLastDiscount();
		$applyMode = self::getApplyMode();
		if ($applyMode == self::APPLY_MODE_FULL_LAST || $applyMode == self::APPLY_MODE_FULL_DISABLE)
		{
			foreach ($this->orderData['BASKET_ITEMS'] as $basketItem)
			{
				if (isset($basketItem['LAST_DISCOUNT']) && $basketItem['LAST_DISCOUNT'] == 'Y')
				{
					$result = true;
					break;
				}
			}
			unset($basketItem);
		}
		unset($applyMode);

		return $result;
	}

	/* additional coupons tools */
	/**
	 * Clear coupons from already used discounts.
	 *
	 * @internal
	 * @param array $coupons			Coupons list from \Bitrix\Sale\DiscountCouponsManager::getForApply.
	 * @return array
	 */
	protected function clearAdditionalCoupons(array $coupons)
	{
		if (empty($coupons))
			return array();

		if (empty($this->discountsCache))
			return $coupons;

		$result = array();

		foreach ($coupons as $couponCode => $couponData)
		{
			$found = false;
			foreach ($this->discountsCache as &$discount)
			{
				if (
					$discount['MODULE_ID'] == $couponData['MODULE_ID']
					&& $discount['DISCOUNT_ID'] == $couponData['DISCOUNT_ID']
					&& $discount['USE_COUPONS'] == 'N'
				)
				{
					$found = true;
				}
			}
			unset($discount);

			if (!$found && !empty($this->couponsCache))
			{
				foreach ($this->couponsCache as $existCouponCode => $existCouponData)
				{
					$discount = $this->discountsCache[$existCouponData['ORDER_DISCOUNT_ID']];
					if (
						$discount['MODULE_ID'] != $couponData['MODULE_ID']
						|| $discount['DISCOUNT_ID'] != $couponData['DISCOUNT_ID']
					)
						continue;
					if ($couponCode == $existCouponCode)
					{
						if (
							$existCouponData['ID'] > 0 || $existCouponData['TYPE'] == Internals\DiscountCouponTable::TYPE_BASKET_ROW
						)
							$found = true;
					}
					else
					{
						if (
							$existCouponData['TYPE'] != Internals\DiscountCouponTable::TYPE_BASKET_ROW
							|| $couponData['TYPE'] != Internals\DiscountCouponTable::TYPE_BASKET_ROW
						)
						{
							$found = true;
						}
						else
						{
							if ($discount['MODULE_ID'] == 'sale')
								$found = true;
						}
					}
					unset($discount);
					if ($found)
						break;
				}
				unset($existCouponCode, $existCouponData);
			}

			if (!$found)
				$result[$couponCode] = $couponData;
			unset($found);
		}
		unset($couponCode, $couponData);

		return $result;
	}

	/**
	 * Return additional coupons for exist order.
	 *
	 * @internal
	 * @param array $filter				Coupons filter.
	 * @return array
	 */
	protected function getAdditionalCoupons(array $filter = array())
	{
		if ($this->useOnlySaleDiscounts())
		{
			if (isset($filter['MODULE_ID']) && $filter['MODULE_ID'] != 'sale')
				return array();
			if (isset($filter['!MODULE_ID']) && $filter['!MODULE_ID'] == 'sale')
				return array();
			$filter['MODULE_ID'] = 'sale';
		}

		$useOrderCoupons = DiscountCouponsManager::isUsedOrderCouponsForApply();
		DiscountCouponsManager::useSavedCouponsForApply(false);
		$coupons = DiscountCouponsManager::getForApply($filter, array(), true);
		DiscountCouponsManager::useSavedCouponsForApply($useOrderCoupons);
		unset($useOrderCoupons);

		if (empty($coupons))
			return array();

		return $this->clearAdditionalCoupons($coupons);
	}

	/**
	 * Calculate additional basket coupons.
	 *
	 * @param array $applyCoupons		Apply discount coupons data.
	 * @return Result
	 */
	protected function calculateApplyBasketAdditionalCoupons(array $applyCoupons)
	{
		$result = new Result;

		if ($this->useOnlySaleDiscounts())
			return $result;
		if (empty($applyCoupons))
			return $result;

		$applyBlock = &$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['BASKET'];

		$applyExist = $this->isBasketApplyResultExist();

		$basketCodeList = $this->getBasketCodes(false);
		foreach ($basketCodeList as &$basketCode)
		{
			if (array_key_exists($basketCode, $applyBlock))
				unset($applyBlock[$basketCode]);
			if (empty($applyCoupons[$basketCode]))
				continue;

			$itemData = array(
				'MODULE_ID' => $this->orderData['BASKET_ITEMS'][$basketCode]['MODULE'],
				'PRODUCT_ID' => $this->orderData['BASKET_ITEMS'][$basketCode]['PRODUCT_ID'],
				'BASKET_ID' => $basketCode
			);
			$this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE_TMP'] = $this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE'];
			$this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE'] = $this->orderData['BASKET_ITEMS'][$basketCode]['PRICE'];
			foreach ($applyCoupons[$basketCode] as $index => $discount)
			{
				$discountResult = $this->convertDiscount($discount);
				if (!$discountResult->isSuccess())
				{
					$result->addErrors($discountResult->getErrors());
					unset($discountResult);
					return $result;
				}
				$orderDiscountId = $discountResult->getId();
				$discountData = $discountResult->getData();
				$applyCoupons[$basketCode][$index]['ORDER_DISCOUNT_ID'] = $orderDiscountId;

				if (empty($discount['COUPON']))
				{
					$result->addError(new Main\Entity\EntityError(
						Loc::getMessage('BX_SALE_DISCOUNT_ERR_DISCOUNT_WITHOUT_COUPON'),
						self::ERROR_ID
					));
					return $result;
				}
				$couponResult = $this->convertCoupon($discount['COUPON'], $orderDiscountId);
				if (!$couponResult->isSuccess())
				{
					$result->addErrors($couponResult->getErrors());
					unset($couponResult);
					return $result;
				}
				$orderCouponId = $couponResult->getId();

				DiscountCouponsManager::setApplyByProduct($itemData, array($orderCouponId));
				unset($couponResult);

				unset($discountData, $discountResult);
				if (!isset($applyBlock[$basketCode]))
					$applyBlock[$basketCode] = array();
				$applyBlock[$basketCode][$index] = array(
					'DISCOUNT_ID' => $orderDiscountId,
					'COUPON_ID' => $orderCouponId,
					'RESULT' => array(
						'APPLY' => 'Y',
						'DESCR' => false,
						'DESCR_DATA' => false
					)
				);

				$currentProduct = $this->orderData['BASKET_ITEMS'][$basketCode];
				$orderApplication = (
					!empty($this->discountsCache[$orderDiscountId]['APPLICATION'])
					? $this->discountsCache[$orderDiscountId]['APPLICATION']
					: null
				);
				if (!empty($orderApplication))
				{
					$this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT'] = (
						!empty($this->discountsCache[$orderDiscountId]['ACTIONS_DESCR_DATA'])
						? $this->discountsCache[$orderDiscountId]['ACTIONS_DESCR_DATA']
						: false
					);

					$applyProduct = null;
					eval('$applyProduct='.$orderApplication.';');
					if (is_callable($applyProduct))
						$applyProduct($this->orderData['BASKET_ITEMS'][$basketCode]);
					unset($applyProduct);

					if (!empty($this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']))
					{
						$applyBlock[$basketCode][$index]['RESULT']['DESCR_DATA'] = $this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']['BASKET'];
						$applyBlock[$basketCode][$index]['RESULT']['DESCR'] = self::formatDescription($this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']);
					}
					unset($this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_RESULT']);
				}
				unset($orderApplication);

				if ($applyExist && !$this->getStatusApplyBasketDiscount($basketCode, $orderDiscountId, $orderCouponId))
				{
					$this->orderData['BASKET_ITEMS'][$basketCode] = $currentProduct;
					$applyBlock[$basketCode][$index]['RESULT']['APPLY'] = 'N';
				}
				unset($currentProduct);
			}
			unset($discount, $index);
			$this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE'] = $this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE_TMP'];
			unset($this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE_TMP']);
		}
		unset($basketCode, $basketCodeList);

		unset($applyBlock);

		return $result;
	}

	/**
	 * Calculate additional sale coupons.
	 *
	 * @param array $applyCoupons			Coupons data.
	 * @return Result
	 */
	protected function calculateApplySaleAdditionalCoupons(array $applyCoupons)
	{
		$result = new Result;

		if (empty($applyCoupons))
			return $result;

		$this->discountResult['APPLY_BLOCKS'][$this->discountResultCounter]['ORDER'] = array();

		$discountId = array();
		foreach ($applyCoupons as $coupon)
			$discountId[] = $coupon['DISCOUNT_ID'];
		unset($coupon);

		$currentUseMode = $this->getUseMode();
		$this->setUseMode(self::USE_MODE_COUPONS);

		$this->loadDiscountByUserGroups(array('@DISCOUNT_ID' => $discountId));
		unset($discountId);

		$basketCodeList = $this->getBasketCodes(false);
		foreach ($basketCodeList as $basketCode)
		{
			$this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE_TMP'] = $this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE'];
			$this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE'] = $this->orderData['BASKET_ITEMS'][$basketCode]['PRICE'];
		}
		unset($basketCode);

		$this->loadDiscountList();
		$executeResult = $this->executeDiscountList();
		if (!$executeResult->isSuccess())
			$result->addErrors($executeResult->getErrors());
		unset($executeResult);
		$this->setUseMode($currentUseMode);
		unset($currentUseMode);

		foreach ($basketCodeList as $basketCode)
		{
			$this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE'] = $this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE_TMP'];
			unset($this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE_TMP']);
		}
		unset($basketCode);
		unset($basketCodeList);

		return $result;
	}

	/* additional coupons tools finish */
	/* compatibility tools */

	/**
	 * Fill order fields for deprecated discount classes.
	 *
	 * @return void
	 */
	protected function fillCompatibleOrderFields()
	{
		$this->orderData['USE_BASE_PRICE'] = $this->saleOptions['USE_BASE_PRICE'];
	}

	/* apply flag tools */

	/**
	 * Return apply status for basket discount.
	 *
	 * @internal
	 * @param string|int $basketCode		Basket item code.
	 * @param int $orderDiscountId			Order discount id.
	 * @param string $orderCouponId			Coupon.
	 * @return bool
	 */
	protected function getStatusApplyBasketDiscount($basketCode, $orderDiscountId, $orderCouponId)
	{
		$disable = false;
		if (
			$orderCouponId != ''
			&& !empty($this->applyResult['COUPON_LIST'][$orderCouponId])
			&& $this->applyResult['COUPON_LIST'][$orderCouponId] == 'N'
		)
		{
			$disable = true;
		}
		else
		{
			if (
				!empty($this->applyResult['DISCOUNT_LIST'][$orderDiscountId])
				&& $this->applyResult['DISCOUNT_LIST'][$orderDiscountId] == 'N'
			)
			{
				$disable = true;
			}
			if (!empty($this->applyResult['BASKET'][$basketCode]))
			{
				if (is_string($this->applyResult['BASKET'][$basketCode]))
					$disable = ($this->applyResult['BASKET'][$basketCode] == 'N');
				elseif (!empty($this->applyResult['BASKET'][$basketCode][$orderDiscountId]))
					$disable = ($this->applyResult['BASKET'][$basketCode][$orderDiscountId] == 'N');
			}
		}
		return !$disable;
	}

	/* instance tools */

	/**
	 * Round and correct discount calculation results.
	 * @internal
	 *
	 * @return void
	 */
	protected function normalizeDiscountResult()
	{
		$customPrice = isset($this->orderData['CUSTOM_PRICE_DELIVERY']) && $this->orderData['CUSTOM_PRICE_DELIVERY'] == 'Y';
		/** @noinspection PhpInternalEntityUsedInspection */
		$this->orderData['PRICE_DELIVERY_DIFF'] = (!$customPrice
			? PriceMaths::roundPrecision($this->orderData['PRICE_DELIVERY_DIFF'])
			: 0
		);
		if (!$customPrice)
		{
			if ($this->orderData['PRICE_DELIVERY_DIFF'] > 0)
				$this->orderData['PRICE_DELIVERY'] = $this->orderData['BASE_PRICE_DELIVERY'] - $this->orderData['PRICE_DELIVERY_DIFF'];
			else
				$this->orderData['PRICE_DELIVERY'] = PriceMaths::roundPrecision($this->orderData['PRICE_DELIVERY']);
		}
		unset($customPrice);

		if (!empty($this->orderData['BASKET_ITEMS']))
		{
			foreach (array_keys($this->orderData['BASKET_ITEMS']) as $basketCode)
			{
				$customPrice = $this->isCustomPriceByCode($basketCode);
				$basketItem = $this->orderData['BASKET_ITEMS'][$basketCode];
				$basketItem['DISCOUNT_PRICE'] = (!$customPrice
					? PriceMaths::roundPrecision($basketItem['DISCOUNT_PRICE'])
					: 0
				);
				if (!$customPrice)
				{
					if ($basketItem['DISCOUNT_PRICE'] > 0)
						$basketItem['PRICE'] = $basketItem['BASE_PRICE'] - $basketItem['DISCOUNT_PRICE'];
					else
						$basketItem['PRICE'] = PriceMaths::roundPrecision($basketItem['PRICE']);
				}
				$this->orderData['BASKET_ITEMS'][$basketCode] = $basketItem;
			}
			unset($basketItem, $customPrice, $basketCode);
		}
	}

	/**
	 * Return instance index for order.
	 *
	 * @internal
	 * @param Order $order			Order.
	 * @return string
	 */
	protected static function getInstanceIndexByOrder(Order $order)
	{
		return $order->getInternalId().'|0'.'|'.$order->getSiteId();
	}

	/**
	 * Return instance index for basket.
	 *
	 * @internal
	 *
	 * @param BasketBase $basket Basket.
	 * @param Context\BaseContext|null $context
	 *
	 * @return string
	 */
	protected static function getInstanceIndexByBasket(BasketBase $basket, Context\BaseContext $context = null)
	{
		if (!$context)
			return '0|'.$basket->getFUserId(false).'|'.$basket->getSiteId();
		return '0|-1|'.$basket->getSiteId().'|'.$context->getUserGroupsHash();
	}

	/**
	 * Return instance index for fuser.
	 *
	 * @internal
	 * @param string|int $fuser			Fuser id.
	 * @param string $site				Site id.
	 * @return string
	 */
	protected static function getInstanceIndexByFuser($fuser, $site)
	{
		return '0|'.$fuser.'|'.$site;
	}

	/**
	 * Return order property codes for translate to order fields.
	 *
	 * @return array
	 */
	protected static function getOrderPropertyCodes()
	{
		return array(
			'DELIVERY_LOCATION' => 'IS_LOCATION',
			'USER_EMAIL' => 'IS_EMAIL',
			'PAYER_NAME' => 'IS_PAYER',
			'PROFILE_NAME' => 'IS_PROFILE_NAME',
			'DELIVERY_LOCATION_ZIP' => 'IS_ZIP'
		);
	}

	/**
	 * Return empty apply block
	 *
	 * @return array
	 */
	protected static function getEmptyApplyBlock()
	{
		return array(
			'BASKET' => array(),
			'BASKET_ROUND' => array(),
			'ORDER' => array()
		);
	}

	/**
	 * Check use old api.
	 *
	 * @return bool
	 */
	private function isUsedDiscountCompatibility()
	{
		return (Compatible\DiscountCompatibility::isUsed() && Compatible\DiscountCompatibility::isInited());
	}

	/**
	 * Get description for old actions.
	 *
	 * @param array $stepResult		Action results.
	 * @return array
	 */
	private function getSimpleActionDescription(array $stepResult)
	{
		$result = array();
		if (!empty($stepResult['BASKET']))
		{
			$data = OrderDiscountManager::prepareDiscountDescription(
				OrderDiscountManager::DESCR_TYPE_SIMPLE,
				Loc::getMessage('BX_SALE_DISCOUNT_MESS_SIMPLE_DESCRIPTION_BASKET')
			);
			if ($data->isSuccess())
			{
				$result['BASKET'] = array(
					0 => $data->getData()
				);
			}
			unset($data);
		}
		if (!empty($stepResult['DELIVERY']))
		{
			$data = OrderDiscountManager::prepareDiscountDescription(
				OrderDiscountManager::DESCR_TYPE_SIMPLE,
				Loc::getMessage('BX_SALE_DISCOUNT_MESS_SIMPLE_DESCRIPTION_DELIVERY')
			);
			if ($data->isSuccess())
			{
				$result['DELIVERY'] = array(
					0 => $data->getData()
				);
			}
			unset($data);
		}
		if (empty($result))
		{
			$data = OrderDiscountManager::prepareDiscountDescription(
				OrderDiscountManager::DESCR_TYPE_SIMPLE,
				Loc::getMessage('BX_SALE_DISCOUNT_MESS_SIMPLE_DESCRIPTION_UNKNOWN')
			);
			if ($data->isSuccess())
			{
				$result['BASKET'] = array(
					0 => $data->getData()
				);
			}
			unset($data);
		}

		return $result;
	}

	private function showAdminError()
	{
		$iterator = \CAdminNotify::GetList(
			array(),
			array('MODULE_ID' => 'sale', 'TAG' => self::ERROR_ID)
		);
		$notify = $iterator->Fetch();
		unset($iterator);
		if (empty($notify))
		{
			$defaultLang = '';
			$messages = array();
			$languages = Main\Localization\LanguageTable::getList(array(
				'select' => array('ID', 'DEF'),
				'filter' => array('=ACTIVE' => 'Y')
			));
			while ($row = $languages->fetch())
			{
				if ($row['DEF'] == 'Y')
					$defaultLang = $row['ID'];
				$languageId = $row['ID'];
				Main\Localization\Loc::loadLanguageFile(
					__FILE__,
					$languageId
				);
				$messages[$languageId] = Main\Localization\Loc::getMessage(
					'BX_SALE_DISCOUNT_ERR_PARSE_ERROR',
					array('#LINK#' => '/bitrix/admin/settings.php?lang='.$languageId.'&mid=sale'),
					$languageId
				);
			}
			unset($row, $languages);

			\CAdminNotify::Add(array(
				'MODULE_ID' => 'sale',
				'TAG' => self::ERROR_ID,
				'ENABLE_CLOSE' => 'N',
				'NOTIFY_TYPE' => \CAdminNotify::TYPE_ERROR,
				'MESSAGE' => $messages[$defaultLang],
				'LANG' => $messages
			));
			unset($messages, $defaultLang);
		}
		unset($notify);
	}

	/**
	 * Calculate discount percent for public components.
	 *
	 * @param int|float $basePrice		Base price.
	 * @param int|float $discount		Discount value (for an extra can be negative).
	 * @return float|int|null
	 */
	public static function calculateDiscountPercent($basePrice, $discount)
	{
		$basePrice = (float)$basePrice;
		if ($basePrice <= 0)
			return null;
		$discount = (float)$discount;
		if ($discount > $basePrice)
			return null;

		$result = roundEx(100*$discount/$basePrice, 0);
		if ($result < 0)
			$result = 0;
		return $result;
	}

	/**
	 * Returns show prices for public components.
	 *
	 * @return array
	 */
	public function getShowPrices()
	{
		if (!$this->isOrderNew() && empty($this->orderData))
		{
			$this->initUseMode();
			$this->loadOrderData();
		}

		$result = array(
			'BASKET' => array(),
			'DELIVERY' => $this->getApplyDeliveryPrice()
		);

		$checkRound = true;
		$useMode = $this->getUseMode();
		switch ($useMode)
		{
			case self::USE_MODE_APPLY:
			case self::USE_MODE_MIXED:
				if ($this->convertedOrder)
					$checkRound = false;
				break;
		}

		if (!empty($this->orderData['BASKET_ITEMS']))
		{
			$basket = $this->orderData['BASKET_ITEMS'];
			$order = $this->orderData;
			unset($order['BASKET_ITEMS']);
			switch ($useMode)
			{
				case self::USE_MODE_FULL:
				case self::USE_MODE_APPLY:
					if ($checkRound)
					{
						$basketCodeList = $this->getBasketCodes(true);
						$existRound = array();
						$existRoundRules = array();
						foreach ($basketCodeList as $basketCode)
						{
							if (!empty($this->basketItemsData[$basketCode]['BASE_PRICE_ROUND_RULE']))
							{
								$existRound[$basketCode] = self::resetPrice($basket[$basketCode]);
								$existRoundRules[$basketCode] = $this->basketItemsData[$basketCode]['BASE_PRICE_ROUND_RULE'];
							}
						}
						if (!empty($existRound))
						{
							$roundResult = OrderDiscountManager::roundBasket(
								$existRound,
								$existRoundRules,
								$order
							);
							foreach ($roundResult as $basketCode => $row)
							{
								if (empty($row) || !is_array($row))
									continue;
								if (!isset($existRound[$basketCode]))
									continue;
								$basket[$basketCode]['BASE_PRICE'] = $row['PRICE'];
								$basket[$basketCode]['DISCOUNT_PRICE'] = $basket[$basketCode]['BASE_PRICE'] - $basket[$basketCode]['PRICE'];
							}
						}
						unset($existRoundRules, $existRound);
					}
					break;
				case self::USE_MODE_MIXED:
					if ($checkRound)
					{
						$existRound = array();
						$existRoundRules = array();
						foreach ($basket as $basketCode => $item)
						{
							if ($this->isCustomPrice($item) || $this->isInSet($item))
								continue;
							if (!empty($this->basketItemsData[$basketCode]['BASE_PRICE_ROUND_RULE']))
							{
								$existRound[$basketCode] = self::resetPrice($basket[$basketCode]);
								$existRoundRules[$basketCode] = $this->basketItemsData[$basketCode]['BASE_PRICE_ROUND_RULE'];
							}
						}
						unset($code, $item);
						if (!empty($existRound))
						{
							$roundResult = OrderDiscount::roundBasket(
								$existRound,
								$existRoundRules,
								$order
							);
							foreach ($roundResult as $basketCode => $row)
							{
								if (empty($row) || !is_array($row))
									continue;
								if (!isset($existRound[$basketCode]))
									continue;
								$basket[$basketCode]['BASE_PRICE'] = $row['PRICE'];
								$basket[$basketCode]['DISCOUNT_PRICE'] = $basket[$basketCode]['BASE_PRICE'] - $basket[$basketCode]['PRICE'];
							}
						}
						unset($existRoundRules, $existRound);
					}
					break;
			}

			foreach ($basket as $basketCode => $basketItem)
			{
				$result['BASKET'][$basketCode] = $this->getShowBasketItemPrice($basketCode, $basketItem);
				$result['BASKET'][$basketCode]['REAL_BASE_PRICE'] = $this->orderData['BASKET_ITEMS'][$basketCode]['BASE_PRICE'];
				$result['BASKET'][$basketCode]['REAL_PRICE'] = $this->orderData['BASKET_ITEMS'][$basketCode]['PRICE'];
				$result['BASKET'][$basketCode]['REAL_DISCOUNT'] = $this->orderData['BASKET_ITEMS'][$basketCode]['DISCOUNT_PRICE'];
			}
			unset($basketCode, $basketItem);
		}

		return $result;
	}

	/**
	 * Search round rule for base price.
	 * @internal
	 *
	 * return void
	 */
	private function getRoundForBasePrices()
	{
		$mode = $this->getUseMode();
		if ($mode != self::USE_MODE_FULL && $mode != self::USE_MODE_MIXED)
			return;

		$basketCodeList = $this->getBasketCodes(true);
		if (empty($basketCodeList))
			return;

		$basket = array_intersect_key(
			$this->orderData['BASKET_ITEMS'],
			array_fill_keys($basketCodeList, true)
		);

		if (empty($basket))
			return;

		foreach ($basketCodeList as $basketCode)
		{
			if (!isset($this->basketItemsData[$basketCode]))
				$this->basketItemsData[$basketCode] = array();
			$this->basketItemsData[$basketCode]['BASE_PRICE_ROUND'] = $basket[$basketCode]['BASE_PRICE'];
			$this->basketItemsData[$basketCode]['BASE_PRICE_ROUND_RULE'] = array();

		}
		unset($basketCode);

		foreach ($basket as &$basketItem)
			$basketItem = self::resetPrice($basketItem);
		unset($basketItem);

		$orderData = $this->orderData;
		unset($orderData['BASKET_ITEMS']);

		$result = OrderDiscountManager::roundBasket(
			$basket,
			array(),
			$orderData
		);
		foreach ($basketCodeList as $basketCode)
		{
			if (empty($result[$basketCode]) || !is_array($result[$basketCode]))
				continue;
			$this->basketItemsData[$basketCode]['BASE_PRICE_ROUND'] = $result[$basketCode]['PRICE'];
			$this->basketItemsData[$basketCode]['BASE_PRICE_ROUND_RULE'] = $result[$basketCode]['ROUND_RULE'];
		}
		unset($basketCode, $result);
		unset($storageClassName);
		unset($basket, $orderData);
	}

	/**
	 * Returns basket item price for show in public components (basket, order, etc).
	 *
	 * @param string|int $basketCode	Basket item code.
	 * @param array $item				Basket item.
	 * @return array
	 */
	private function getShowBasketItemPrice($basketCode, array $item)
	{
		if ($this->isCustomPrice($item) || $this->isInSet($item))
		{
			if ($item['BASE_PRICE'] <= $item['PRICE'])
				$result = $this->getShowPriceWithZeroDiscountPercent($item);
			else
				$result = $this->getShowPriceWithDiscountPercent($item);
			return $result;
		}

		if ($item['BASE_PRICE'] <= $item['PRICE'])
			return $this->getShowPriceWithZeroDiscountPercent($item);

		if ($this->isExistBasketItemDiscount($basketCode))
			return $this->getShowPriceWithDiscountPercent($item);

		return $this->getShowPriceWithZeroDiscountPercent($item);
	}

	/**
	 * Returns basket item price with rounded discount percent. Only for show.
	 *
	 * @param array $item	Basket item (price fields).
	 * @return array
	 */
	private function getShowPriceWithDiscountPercent(array $item)
	{
		return [
			'SHOW_BASE_PRICE' => $item['BASE_PRICE'],
			'SHOW_PRICE' => $item['PRICE'],
			'SHOW_DISCOUNT' => $item['DISCOUNT_PRICE'],
			'SHOW_DISCOUNT_PERCENT' => $this->calculateDiscountPercent(
				$item['BASE_PRICE'],
				$item['DISCOUNT_PRICE']
			)
		];
	}

	/**
	 * Returns basket item price without discount. Only for show.
	 *
	 * @param array $item	Basket item (price fields).
	 * @return array
	 */
	private function getShowPriceWithZeroDiscountPercent(array $item)
	{
		return [
			'SHOW_BASE_PRICE' => $item['PRICE'],
			'SHOW_PRICE' => $item['PRICE'],
			'SHOW_DISCOUNT' => 0,
			'SHOW_DISCOUNT_PERCENT' => 0
		];
	}

	/**
	 * Checking the existence of the applied discount on the basket item.
	 *
	 * @param string|int $basketCode	Basket item code.
	 * @return bool
	 */
	private function isExistBasketItemDiscount($basketCode)
	{
		$result = false;

		if (!empty($this->discountResult['APPLY_BLOCKS']))
		{
			foreach ($this->discountResult['APPLY_BLOCKS'] as $counter => $applyBlock)
			{
				if (isset($applyBlock['BASKET'][$basketCode]))
				{
					foreach ($applyBlock['BASKET'][$basketCode] as $discount)
					{
						if ($discount['RESULT']['APPLY'] == 'Y')
							$result = true;
					}
					unset($discount);
				}

				if (!empty($applyBlock['ORDER']))
				{
					foreach ($applyBlock['ORDER'] as $discount)
					{
						if (!empty($discount['RESULT']['BASKET']))
						{
							if (!isset($discount['RESULT']['BASKET'][$basketCode]))
								continue;
							if ($discount['RESULT']['BASKET'][$basketCode]['APPLY'] == 'Y')
								$result = true;
						}
					}
					unset($discount);
				}
			}
			unset($counter, $applyBlock);
		}

		return $result;
	}

	/**
	 * Returns basket item stored data for save.
	 *
	 * @param string|int $basketCode	Basket item code.
	 * @return array|null
	 */
	private function prepareBasketItemStoredData($basketCode)
	{
		$result = [];
		if (isset($this->basketItemsData[$basketCode]))
		{
			if (isset($this->basketItemsData[$basketCode]['BASE_PRICE_ROUND_RULE']))
				$result['BASE_PRICE_ROUND_RULE'] = $this->basketItemsData[$basketCode]['BASE_PRICE_ROUND_RULE'];
		}

		return (!empty($result) ? $result : null);
	}

	/**
	 * Reset product price for discount calculation.
	 *
	 * @param array $item		Basket item data.
	 * @return array
	 */
	private static function resetPrice(array $item)
	{
		$item['PRICE'] = $item['BASE_PRICE'];
		$item['DISCOUNT_PRICE'] = 0;

		return $item;
	}

	/* deprecated methods */

	/**
	 * Set base price for basket item.
	 * @deprecated
	 *
	 * @param int|string $code				Basket code.
	 * @param float $price			Price.
	 * @param string $currency		Currency.
	 * @return void
	 */
	public function setBasketItemBasePrice($code, $price, $currency) {}

	/**
	 * Set base price for all basket items.
	 * @deprecated
	 *
	 * @param array $basket					Basket.
	 * @return void
	 */
	public function setBasketBasePrice($basket) {}

	/**
	 * Get base price for basket item.
	 * @deprecated
	 *
	 * @param int|string $code				Basket code.
	 * @return float|null
	 */
	public function getBasketItemBasePrice($code)
	{
		return (isset($this->orderData[$code]) ? $this->orderData[$code]['BASE_PRICE'] : null);
	}

	/**
	 * Set product discounts for basket item.
	 * @deprecated
	 *
	 * @param int|string $code				Basket code.
	 * @param array $discountList			Discount list.
	 * @return void
	 */
	public function setBasketItemDiscounts($code, $discountList) {}

	/**
	 * Set various basket item data.
	 * @deprecated
	 *
	 * @param int|string $code				Basket code.
	 * @param array $providerData			Product data from provider.
	 * @return void
	 */
	public function setBasketItemData($code, $providerData) {}

	/**
	 * Clear basket item data.
	 * @deprecated
	 *
	 * @param int|string $code				Basket code.
	 * @return void
	 */
	public function clearBasketItemData($code) {}

	/* deprecated methods finish */
}