Your IP : 18.117.187.15


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

<?php
namespace Bitrix\Catalog\Discount;

use Bitrix\Main,
	Bitrix\Main\Localization\Loc,
	Bitrix\Main\Loader,
	Bitrix\Main\Type\Collection,
	Bitrix\Catalog,
	Bitrix\Iblock,
	Bitrix\Sale;

Loc::loadMessages(__FILE__);

class DiscountManager
{
	protected static $discountCache = array();
	protected static $typeCache = array();
	protected static $editUrlTemplate = array();
	protected static $saleIncluded = null;
	protected static $preloadedPriceData = array();
	protected static $preloadedProductsData = array();
	protected static $productProperties = array();

	/**
	 * Return methods for prepare discount.
	 *
	 * @param Main\Event $event					Event data from discount manager.
	 * @return Main\EventResult
	 */
	public static function catalogDiscountManager(/** @noinspection PhpUnusedParameterInspection */Main\Event $event)
	{
		$result = new Main\EventResult(
			Main\EventResult::SUCCESS,
			array(
				'prepareData' => array(__CLASS__, 'prepareData'),
				'getEditUrl' => array(__CLASS__, 'getEditUrl'),
				'calculateApplyCoupons' => array(__CLASS__, 'calculateApplyCoupons'),
				'roundPrice' => array(__CLASS__, 'roundPrice'),
				'roundBasket' => array(__CLASS__, 'roundBasket')
			),
			'catalog'
		);
		return $result;
	}

	/**
	 * Prepare discount before saving.
	 *
	 * @param array $discount				Discount data.
	 * @param array $params					Params.
	 * @return array|bool
	 */
	public static function prepareData($discount, $params = array())
	{
		if (empty($discount) || empty($discount['ID']))
			return false;

		$discountId = (int)$discount['ID'];
		if ($discountId <= 0)
			return false;
		if (!isset(self::$discountCache[$discountId]))
		{
			self::$discountCache[$discountId] = false;

			$loadData = self::loadFromDatabase($discountId, $discount);
			if (!empty($loadData))
			{
				$loadData['LAST_LEVEL_DISCOUNT'] = 'N';
				if ($loadData['CURRENCY'] != $params['CURRENCY'])
					Catalog\DiscountTable::convertCurrency($loadData, $params['CURRENCY']);
				self::createSaleAction($loadData, $params);
				$loadData['EDIT_PAGE_URL'] = self::getEditUrl(array('ID' => $discountId, 'TYPE' => $loadData['TYPE']));
				self::$discountCache[$discountId] = $loadData;
			}
		}
		$result = self::$discountCache[$discountId];
		if (empty($result))
			return $result;
		if ($result['USE_COUPONS'] == 'Y')
		{
			if (isset($discount['COUPON']))
				$result['COUPON'] = $discount['COUPON'];
		}

		return $result;
	}

	/**
	 * Return url for edit discount.
	 *
	 * @param array $discount			Discount data.
	 * @return string
	 */
	public static function getEditUrl($discount)
	{
		if (empty(self::$editUrlTemplate))
		{
			self::$editUrlTemplate = array(
				Catalog\DiscountTable::TYPE_DISCOUNT => '/bitrix/admin/cat_discount_edit.php?lang='.LANGUAGE_ID.'&ID=',
				Catalog\DiscountTable::TYPE_DISCOUNT_SAVE => '/bitrix/admin/cat_discsave_edit.php?lang='.LANGUAGE_ID.'&ID='
			);
		}
		$result = '';
		if (empty($discount['ID']) || (int)$discount['ID'] <= 0)
			return $result;

		$id = (int)$discount['ID'];
		$type = -1;
		if (isset($discount['TYPE']))
			$type = (int)$discount['TYPE'];

		if ($type != Catalog\DiscountTable::TYPE_DISCOUNT && $type != Catalog\DiscountTable::TYPE_DISCOUNT_SAVE)
		{
			if (isset(self::$typeCache[$id]))
			{
				$type = self::$typeCache[$id];
			}
			else
			{
				$discountIterator = Catalog\DiscountTable::getList(array(
					'select' => array('ID', 'TYPE'),
					'filter' => array('=ID' => $id)
				));
				$data = $discountIterator->fetch();
				if (!empty($data))
				{
					$type = (int)$data['TYPE'];
					self::$typeCache[$id] = $type;
				}
				unset($data, $discountIterator);
			}
		}
		if (isset(self::$editUrlTemplate[$type]))
			$result = self::$editUrlTemplate[$type].$id;
		unset($type, $id);
		return $result;
	}

	/**
	 * Check apply coupons.
	 *
	 * @param array $couponsList		Coupons.
	 * @param array $basket				Basket data.
	 * @param array $params				Calculate params.
	 * @return array
	 * @throws Main\ArgumentException
	 */
	public static function calculateApplyCoupons(array $couponsList, array $basket, array $params)
	{
		$result = array();

		if (empty($couponsList))
			return $result;
		if (empty($basket))
			return $result;
		$filteredBasket = array_filter($basket, '\Bitrix\Catalog\Discount\DiscountManager::basketFilter');
		if (empty($filteredBasket))
			return $result;
		$filteredBasket = array_filter($filteredBasket, '\Bitrix\Catalog\Discount\DiscountManager::lastDiscountFilter');
		if (empty($filteredBasket))
			return $result;

		$filteredCoupons = array();
		foreach ($couponsList as $coupon)
		{
			if (!isset($coupon['COUPON']) || $coupon['COUPON'] == '')
				continue;
			if (!isset($coupon['DISCOUNT_ID']) || (int)$coupon['DISCOUNT_ID'] <= 0)
				continue;
			$filteredCoupons[] = $coupon['COUPON'];
		}
		unset($coupon);
		if (empty($filteredCoupons))
			return $result;

		$discountIds = array();
		$discountCoupons = array();
		$oneRowCoupons = array();
		$couponsIterator = Catalog\DiscountCouponTable::getList(array(
			'select' => array('ID', 'COUPON', 'DISCOUNT_ID', 'TYPE'),
			'filter' => array('@COUPON' => $filteredCoupons, 'ACTIVE' => 'Y')
		));
		while ($coupon = $couponsIterator->fetch())
		{
			$discountIds[$coupon['DISCOUNT_ID']] = true;
			$discountCoupons[$coupon['COUPON']] = $coupon['COUPON'];
			if ($coupon['TYPE'] == Catalog\DiscountCouponTable::TYPE_ONE_ROW)
				$oneRowCoupons[$coupon['COUPON']] = true;
		}
		unset($coupon, $couponsIterator);
		if (empty($discountCoupons))
			return $result;

		$userId = (isset($params['USER_ID']) ? (int)$params['USER_ID'] : 0);
		if ($userId <= 0)
			return $result;
		$userGroups = Main\UserTable::getUserGroupIds($userId);
		$userGroups[] = -1;

		$iblockList = array();
		$product2Iblock = array();
		$itemIds = array();
		foreach ($filteredBasket as $basketItem)
		{
			$productId = (int)$basketItem['PRODUCT_ID'];
			$itemIds[$productId] = $productId;
		}
		unset($basketItem);

		$itemIterator = Iblock\ElementTable::getList(array(
			'select' => array('ID', 'IBLOCK_ID'),
			'filter' => array('@ID' => $itemIds, 'ACTIVE' => 'Y')
		));
		while ($item = $itemIterator->fetch())
		{
			$id = (int)$item['ID'];
			$iblockId = (int)$item['IBLOCK_ID'];
			if (!isset($iblockList[$iblockId]))
				$iblockList[$iblockId] = array();
			$iblockList[$iblockId][$id] = $id;
			$product2Iblock[$id] = $iblockId;
			unset($iblockId, $id);
		}
		unset($item, $itemIterator);
		unset($itemIds);

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

		foreach($iblockList as $iblockId => $elements)
		{
			/** @noinspection PhpMethodOrClassCallIsNotCaseSensitiveInspection */
			\CCatalogDiscount::setProductSectionsCache($elements);
			/** @noinspection PhpMethodOrClassCallIsNotCaseSensitiveInspection */
			\CCatalogDiscount::setDiscountProductCache($elements, array('IBLOCK_ID' => $iblockId, 'GET_BY_ID' => 'Y'));
		}
		unset($iblockId, $elements);

		$discountPercentMode = \CCatalogDiscount::getUseBasePrice();
		if (isset($params['USE_BASE_PRICE']))
			\CCatalogDiscount::setUseBasePrice($params['USE_BASE_PRICE'] == 'Y');

		Main\Type\Collection::sortByColumn($filteredBasket, array('PRICE' => SORT_DESC), '', null, true);
		foreach ($filteredBasket as $basketCode => $basketItem)
		{
			$productId = (int)$basketItem['PRODUCT_ID'];
			if (!isset($product2Iblock[$productId]))
				continue;
			if (empty($discountCoupons))
				break;

			/** @noinspection PhpMethodOrClassCallIsNotCaseSensitiveInspection */
			$discountList = \CCatalogDiscount::getDiscount(
				$productId,
				$product2Iblock[$productId],
				array(-1),
				$userGroups,
				'N',
				$params['SITE_ID'],
				$discountCoupons
			);

			if (empty($discountList))
				continue;

			$itemDiscounts = array();
			foreach ($discountList as $discount)
			{
				if (!isset($discountIds[$discount['ID']]))
					continue;
				$itemDiscounts[] = $discount;
			}
			unset($discount, $discountList);
			if (empty($itemDiscounts))
				continue;

			$itemsDiscountResult = \CCatalogDiscount::applyDiscountList($basketItem['PRICE'], $basketItem['CURRENCY'], $itemDiscounts);
			unset($itemDiscounts);
			if (!empty($itemsDiscountResult['DISCOUNT_LIST']))
			{
				$result[$basketCode] = array();
				foreach ($itemsDiscountResult['DISCOUNT_LIST'] as $discount)
				{
					$result[$basketCode][] = \CCatalogDiscount::getDiscountDescription($discount);
					if (!empty($discount['COUPON']) && isset($oneRowCoupons[$discount['COUPON']]))
						unset($discountCoupons[$discount['COUPON']]);
				}
				unset($discount);
			}
			unset($itemsDiscountResult);
		}
		unset($basketCode, $basketItem, $basketItem);

		\CCatalogDiscount::setUseBasePrice($discountPercentMode);
		unset($discountPercentMode);

		return $result;
	}

	/**
	 * Round basket item price.
	 *
	 * @param array $basketItem		Basket item data.
	 * @param array $roundData		Round rule.
	 * @return array
	 */
	public static function roundPrice(array $basketItem, array $roundData = array())
	{
		if (empty($basketItem))
			return array();
		if (empty($roundData))
		{
			$priceTypeId = 0;
			if (isset($basketItem['PRICE_TYPE_ID']))
				$priceTypeId = (int)$basketItem['PRICE_TYPE_ID'];
			if ($priceTypeId <= 0 && isset($basketItem['CATALOG_GROUP_ID']))
				$priceTypeId = (int)$basketItem['CATALOG_GROUP_ID'];
			if ($priceTypeId <= 0 && isset($basketItem['PRODUCT_PRICE_ID']))
			{
				$priceId = (int)$basketItem['PRODUCT_PRICE_ID'];
				if ($priceId > 0)
				{
					$row = self::getPriceDataByPriceId($priceId)?: Catalog\PriceTable::getList(array(
						'select' => array('ID', 'CATALOG_GROUP_ID'),
						'filter' => array('=ID' => $priceId)
					))->fetch();
					if (!empty($row))
						$priceTypeId = (int)$row['CATALOG_GROUP_ID'];
					unset($row);
				}
				unset($priceId);
			}
			if ($priceTypeId > 0)
				$roundData = Catalog\Product\Price::searchRoundRule($priceTypeId, $basketItem['PRICE'], $basketItem['CURRENCY']);
			unset($priceTypeId);
		}
		if (empty($roundData))
			return array();
		return self::getRoundResult($basketItem, $roundData);
	}

	/**
	 * Round basket prices.
	 *
	 * @param array $basket             Basket.
	 * @param array $basketRoundData    Round rules.
	 * @param array $orderData          Order (without basket, can be absent).
	 * @return array
	 */
	public static function roundBasket(
		array $basket,
		array $basketRoundData = array(),
		/** @noinspection PhpUnusedParameterInspection */array $orderData
	)
	{
		if (empty($basket))
			return array();

		$result = array();
		$basket = array_filter($basket, '\Bitrix\Catalog\Discount\DiscountManager::basketFilter');
		if (!empty($basket))
		{
			$priceTypes = array();
			$loadPriceId = array();
			$loadBasketCodes = array();
			foreach ($basket as $basketCode => $basketItem)
			{
				if (!empty($basketRoundData[$basketCode]))
					continue;
				$priceTypeId = 0;
				if (isset($basketItem['PRICE_TYPE_ID']))
					$priceTypeId = (int)$basketItem['PRICE_TYPE_ID'];
				if ($priceTypeId <= 0 && isset($basketItem['CATALOG_GROUP_ID']))
					$priceTypeId = (int)$basketItem['CATALOG_GROUP_ID'];
				if ($priceTypeId <= 0 && isset($basketItem['PRODUCT_PRICE_ID']))
				{
					$priceId = (int)$basketItem['PRODUCT_PRICE_ID'];
					if ($priceId > 0)
					{
						$cachedPrice = self::getPriceDataByPriceId($priceId);
						if (!empty($cachedPrice))
							$priceTypeId = (int)$cachedPrice['CATALOG_GROUP_ID'];
						if ($priceTypeId <= 0)
						{
							$loadPriceId[] = $priceId;
							$loadBasketCodes[$priceId] = $basketCode;
						}
					}
				}

				$basket[$basketCode]['PRICE_TYPE_ID'] = $priceTypeId;
				if ($priceTypeId > 0)
					$priceTypes[$priceTypeId] = $priceTypeId;

			}
			unset($priceId, $priceTypeId, $basketCode, $basketItem);

			if (!empty($loadPriceId))
			{
				sort($loadPriceId);
				foreach (array_chunk($loadPriceId, 500) as $pageIds)
				{
					$iterator = Catalog\PriceTable::getList(array(
						'select' => array('ID', 'CATALOG_GROUP_ID'),
						'filter' => array('@ID' => $pageIds)
					));
					while ($row = $iterator->fetch())
					{
						$id = (int)$row['ID'];
						$priceTypeId = (int)$row['CATALOG_GROUP_ID'];
						if (!isset($loadBasketCodes[$id]))
							continue;
						$basket[$loadBasketCodes[$id]]['PRICE_TYPE_ID'] = $priceTypeId;
						$priceTypes[$priceTypeId] = $priceTypeId;
					}
					unset($priceTypeId, $id, $row, $iterator);
				}
			}
			unset($loadBasketCodes, $loadPriceId);

			if (!empty($priceTypes))
				Catalog\Product\Price::loadRoundRules($priceTypes);
			unset($priceTypes);

			foreach ($basket as $basketCode => $basketItem)
			{
				if (!empty($basketRoundData[$basketCode]))
					$roundData = $basketRoundData[$basketCode];
				else
					$roundData = Catalog\Product\Price::searchRoundRule(
						$basketItem['PRICE_TYPE_ID'],
						$basketItem['PRICE'],
						$basketItem['CURRENCY']
					);
				if (empty($roundData))
					continue;
				$result[$basketCode] = self::getRoundResult($basketItem, $roundData);
			}
			unset($roundData, $basketCode, $basketItem, $basketRoundData);
		}
		unset($basket);

		return $result;
	}

	/**
	 * Apply catalog discount by basket item.
	 *
	 * @param array &$product			Product data.
	 * @param array $discount			Discount data.
	 * @return void
	 */
	public static function applyDiscount(&$product, $discount)
	{
		if (empty($product) || !is_array($product))
			return;
		if (empty($discount) || empty($discount['TYPE']))
			return;
		if (isset($discount['CURRENCY']) && $discount['CURRENCY'] != $product['CURRENCY'])
			return;
		if (!isset($product['DISCOUNT_PRICE']))
			$product['DISCOUNT_PRICE'] = 0;
		$getPercentFromBasePrice = (isset($discount['USE_BASE_PRICE']) && $discount['USE_BASE_PRICE'] == 'Y');
		$basePrice = (float)(
			isset($product['BASE_PRICE'])
			? $product['BASE_PRICE']
			: $product['PRICE'] + $product['DISCOUNT_PRICE']
		);

		switch ($discount['TYPE'])
		{
			case Catalog\DiscountTable::VALUE_TYPE_PERCENT:
				$discount['VALUE'] = -$discount['VALUE'];
				$discountValue = self::roundValue(
					((
						$getPercentFromBasePrice
							? $basePrice
							: $product['PRICE']
						)*$discount['VALUE'])/100,
					$product['CURRENCY']
				);
				if (isset($discount['MAX_VALUE']) && $discount['MAX_VALUE'] > 0)
				{
					if ($discountValue + $discount['MAX_VALUE'] <= 0)
						$discountValue = -$discount['MAX_VALUE'];
				}
				$product['PRICE'] += $discountValue;
				$product['DISCOUNT_PRICE'] -= $discountValue;
				if (!empty($product['DISCOUNT_RESULT']))
				{
					$product['DISCOUNT_RESULT']['BASKET'][0]['RESULT_VALUE'] = (string)abs($discountValue);
					$product['DISCOUNT_RESULT']['BASKET'][0]['RESULT_UNIT'] = $product['CURRENCY'];
				}
				unset($discountValue);
				break;
			case Catalog\DiscountTable::VALUE_TYPE_FIX:
				$discount['VALUE'] = self::roundValue($discount['VALUE'], $product['CURRENCY']);
				$product['PRICE'] -= $discount['VALUE'];
				$product['DISCOUNT_PRICE'] += $discount['VALUE'];
				break;
			case Catalog\DiscountTable::VALUE_TYPE_SALE:
				$discount['VALUE'] = self::roundValue($discount['VALUE'], $product['CURRENCY']);
				$product['DISCOUNT_PRICE'] += ($product['PRICE'] - $discount['VALUE']);
				$product['PRICE'] = $discount['VALUE'];
				break;
		}
	}

	/**
	 * Returns price for product which has catalog group.
	 *
	 * @param int $productId		Product id.
	 * @param int $catalogGroupId	Catalog group.
	 * @return null|array
	 */
	public static function getPriceDataByProductId($productId, $catalogGroupId)
	{
		if (!isset(self::$preloadedPriceData[$productId.'-'.$catalogGroupId]))
		{
			self::$preloadedPriceData[$productId.'-'.$catalogGroupId] = null;
			self::preloadPriceData(array($productId), array($catalogGroupId));
		}
		return self::$preloadedPriceData[$productId.'-'.$catalogGroupId];
	}

	/**
	 * Set property values cache for product.
	 *
	 * @param int $productId		Product id.
	 * @param array $props			Property values.
	 * @return void
	 */
	public static function setProductPropertiesCache($productId, $props)
	{
		if (!is_array($props))
			return;

		self::$productProperties[$productId] = $props;
	}

	/**
	 * Clear property values cache.
	 *
	 * @return void
	 */
	public static function clearProductPropertiesCache()
	{
		self::$productProperties = array();
	}

	/**
	 * Clear products cache.
	 *
	 * @return void
	 */
	public static function clearProductsCache()
	{
		self::$preloadedProductsData = array();
	}

	/**
	 * Clear product prices cache.
	 *
	 * @return void
	 */
	public static function clearProductPricesCache()
	{
		self::$preloadedPriceData = array();
	}

	/**
	 * Preloads prices for products with catalog groups.
	 *
	 * @param array $productIds		List of product ids.
	 * @param array $catalogGroups	Catalog groups.
	 * @return void
	 */
	public static function preloadPriceData(array $productIds, array $catalogGroups)
	{
		if (empty($productIds) || empty($catalogGroups))
			return;
		Collection::normalizeArrayValuesByInt($productIds);
		if (empty($productIds))
			return;
		Collection::normalizeArrayValuesByInt($catalogGroups);
		if (empty($catalogGroups))
			return;

		$productIds = self::extendProductIdsToOffer($productIds);

		foreach($productIds as $i => $productId)
		{
			if(isset(self::$preloadedPriceData[$productId]))
			{
				unset($productIds[$i]);
			}
		}

		if(empty($productIds))
		{
			return;
		}

		$dbPrice = Catalog\PriceTable::getList(array(
			'select' => array('*'),
			'filter' => array('@PRODUCT_ID' => $productIds, '@CATALOG_GROUP_ID' => $catalogGroups)
		));
		while($priceRow = $dbPrice->fetch())
		{
			self::$preloadedPriceData[$priceRow['PRODUCT_ID'] . '-' . $priceRow['CATALOG_GROUP_ID']] = $priceRow;
		}
	}

	private static function fillByPreloadedPrices(array &$productData, array $priceList)
	{
		foreach ($productData as $productId => $product)
		{
			foreach (self::$preloadedPriceData as $priceData)
			{
				if ($priceData['PRODUCT_ID'] != $productId)
				{
					continue;
				}

				if(!in_array($priceData['ID'], $priceList))
				{
					continue;
				}

				$productData[$productId]['CATALOG_GROUP_ID'] = $priceData['CATALOG_GROUP_ID'];
			}
		}
	}

	/**
	 * Load product data for calculate discounts.
	 *
	 * @param array $productIds		Product id list.
	 * @param array $userGroups		User group list.
	 * @return void
	 */
	public static function preloadProductDataToExtendOrder(array $productIds, array $userGroups)
	{
		if (empty($productIds) || empty($userGroups))
			return;
		Collection::normalizeArrayValuesByInt($productIds);
		if (empty($productIds))
			return;
		Collection::normalizeArrayValuesByInt($userGroups);
		if (empty($userGroups))
			return;

		if(self::$saleIncluded === null)
			self::$saleIncluded = Loader::includeModule('sale');

		if(!self::$saleIncluded)
			return;

		$discountCache = Sale\Discount\RuntimeCache\DiscountCache::getInstance();

		$discountIds = $discountCache->getDiscountIds($userGroups);
		if(!$discountIds)
		{
			return;
		}

		Collection::normalizeArrayValuesByInt($discountIds);

		$entityList = $discountCache->getDiscountEntities($discountIds);
		if(!$entityList || empty($entityList['catalog']))
		{
			return;
		}

		$entityData = self::prepareEntity($entityList);
		if(!$entityData)
		{
			return;
		}

		$productIds = self::extendProductIdsToOffer($productIds);

		$iblockData = self::getProductIblocks($productIds);
		self::fillProductPropertyList($entityData, $iblockData);

		$productData = array_fill_keys($productIds, array());
		if(empty($iblockData['iblockElement']))
		{
			return;
		}

		self::getProductData($productData, $entityData, $iblockData);

		$cacheKeyForEntityList = self::getCacheKeyForEntityList($entityList);
		if(!isset(self::$preloadedProductsData[$cacheKeyForEntityList]))
		{
			self::$preloadedProductsData[$cacheKeyForEntityList] = array();
		}

		foreach($productData as $productId => $data)
		{
			self::$preloadedProductsData[$cacheKeyForEntityList][$productId] = $data;
		}
	}

	/**
	 * Extend basket data.
	 *
	 * @param Main\Event $event			Event.
	 * @return Main\EventResult
	 */
	public static function extendOrderData(Main\Event $event)
	{
		$process = true;
		$resultData = array();
		$orderData = $event->getParameter('ORDER');
		$entityList = $event->getParameter('ENTITY');
		$cacheKeyForEntityList = self::getCacheKeyForEntityList($entityList);

		if (empty($orderData) || !is_array($orderData))
		{
			$process = false;
		}
		else
		{
			if (!isset($orderData['BASKET_ITEMS']) || !is_array($orderData['BASKET_ITEMS']))
				$process = false;
		}

		$entityData = false;
		$iblockData = false;
		if (
			$process
			&& !empty($orderData['BASKET_ITEMS'])
		)
		{
			$entityData = self::prepareEntity($entityList);
			if (empty($entityData))
				$process = false;
		}
		if ($process)
		{
			$productMap = array();
			$productList = array();
			$productData = array();
			$priceList = array();

			$basket = array_filter($orderData['BASKET_ITEMS'], '\Bitrix\Catalog\Discount\DiscountManager::basketFilter');
			if (!empty($basket))
			{
				foreach ($basket as $basketCode => $basketItem)
				{
					$basketItem['PRODUCT_ID'] = (int)$basketItem['PRODUCT_ID'];
					$productList[] = $basketItem['PRODUCT_ID'];
					if (!isset($productMap[$basketItem['PRODUCT_ID']]))
						$productMap[$basketItem['PRODUCT_ID']] = array();
					$productMap[$basketItem['PRODUCT_ID']][] = &$basket[$basketCode];
					$priceList[] = $basketItem['PRODUCT_PRICE_ID'];
				}
				unset($basketItem, $basketCode);

				if(isset(self::$preloadedProductsData[$cacheKeyForEntityList]))
				{
					$preloadedProductIds = array_keys(self::$preloadedProductsData[$cacheKeyForEntityList]);
					$loadedProductIds = array_intersect($productList, $preloadedProductIds);

					$productList = array_diff($productList, $preloadedProductIds);
				}

				$productData = array_fill_keys($productList, array());

				if($productData)
				{
					$iblockData = self::getProductIblocks($productList);
					self::fillProductPropertyList($entityData, $iblockData);
					self::fillProductPriceList($entityData, $priceList);
				}
			}

			if (!empty($iblockData['iblockElement']))
			{
				self::getProductData($productData, $entityData, $iblockData);
			}

			if(!empty($loadedProductIds))
			{
				foreach($loadedProductIds as $loadedProductId)
				{
					$productData[$loadedProductId] = self::$preloadedProductsData[$cacheKeyForEntityList][$loadedProductId];
				}

				if(!empty($entityData['priceFields']))
				{
					self::fillByPreloadedPrices($productData, $priceList);
				}
			}

			if($productData)
			{
				foreach ($productData as $product => $data)
				{
					if (empty($productMap[$product]))
						continue;
					foreach ($productMap[$product] as &$basketItem)
						$basketItem['CATALOG'] = $data;
					unset($basketItem);
				}
				unset($product, $data);

				$resultData['BASKET_ITEMS'] = $basket;
			}
			unset($basket, $productData, $productMap, $productList);
		}

		if ($process)
			$result = new Main\EventResult(Main\EventResult::SUCCESS, $resultData, 'catalog');
		else
			$result = new Main\EventResult(Main\EventResult::ERROR, null, 'catalog');
		unset($process, $resultData);

		return $result;
	}

	protected static function getCacheKeyForEntityList(array $entityList)
	{
		return md5(serialize($entityList));
	}

	protected static function extendProductIdsToOffer(array $productIds)
	{
		static $cache = array();

		Collection::normalizeArrayValuesByInt($productIds);
		if (empty($productIds))
			return array();
		$key = md5(implode('|', $productIds));

		if(!isset($cache[$key]))
		{
			$extendedList = array_combine($productIds, $productIds);
			foreach(\CCatalogSku::getOffersList($productIds) as $mainProduct)
			{
				foreach(array_keys($mainProduct) as $offerId)
				{
					if(!isset($extendedList[$offerId]))
					{
						$extendedList[$offerId] = $offerId;
					}
				}
			}

			$cache[$key] = $extendedList;
		}

		return $cache[$key];
	}

	/**
	 * Filter for catalog basket items.
	 *
	 * @param array $basketItem			Basket item data.
	 * @return bool
	 */
	protected static function basketFilter($basketItem)
	{
		return (
			(
				(isset($basketItem['MODULE']) && $basketItem['MODULE'] == 'catalog')
				|| (isset($basketItem['MODULE_ID']) && $basketItem['MODULE_ID'] == 'catalog')
			)
			&& (isset($basketItem['PRODUCT_ID']) && (int)$basketItem['PRODUCT_ID'] > 0)
		);
	}

	/**
	 * Filter for stop discount calculate for basket item.
	 *
	 * @param array $basketItem			Basket item data.
	 * @return bool
	 */
	protected static function lastDiscountFilter($basketItem)
	{
		return (
			!isset($basketItem['LAST_DISCOUNT'])
			|| $basketItem['LAST_DISCOUNT'] != 'Y'
		);
	}

	/**
	 * Load discount data from db.
	 * @param int $id					Discount id.
	 * @param array $discount			Exist discount data.
	 * @return bool|array
	 */
	protected static function loadFromDatabase($id, $discount)
	{
		$select = array();
		if (!isset($discount['NAME']))
			$select['NAME'] = true;
		if (empty($discount['CONDITIONS']))
			$select['CONDITIONS_LIST'] = true;
		if (empty($discount['UNPACK']))
			$select['UNPACK'] = true;
		if (empty($discount['USE_COUPONS']))
			$discount['USE_COUPONS'] = (!empty($discount['COUPON']) ? 'Y' : 'N');
		if (!isset($discount['SORT']))
			$select['SORT'] = true;
		if (!isset($discount['PRIORITY']))
			$select['PRIORITY'] = true;
		if (!isset($discount['LAST_DISCOUNT']))
			$select['LAST_DISCOUNT'] = true;

		if (
			!isset($discount['TYPE'])
			|| ($discount['TYPE'] != Catalog\DiscountTable::TYPE_DISCOUNT && $discount['TYPE'] != Catalog\DiscountTable::TYPE_DISCOUNT_SAVE)
		)
			$select['TYPE'] = true;
		if (!isset($discount['VALUE_TYPE']))
		{
			$select['VALUE_TYPE'] = true;
			$select['VALUE'] = true;
			$select['MAX_DISCOUNT'] = true;
			$select['CURRENCY'] = true;
		}
		else
		{
			if (!isset($discount['VALUE']))
				$select['VALUE'] = true;
			if (!isset($discount['CURRENCY']))
				$select['CURRENCY'] = true;
			if ($discount['VALUE_TYPE'] == Catalog\DiscountTable::VALUE_TYPE_PERCENT && !isset($discount['MAX_VALUE']))
				$select['MAX_DISCOUNT'] = true;
		}
		$selectKeys = array_keys($select);

		if (!empty($select))
		{
			$discountIterator = Catalog\DiscountTable::getList(array(
				'select' => $selectKeys,
				'filter' => array('=ID' => $id)
			));
			$loadData = $discountIterator->fetch();
			if (empty($loadData))
				return false;
			$discount = array_merge($loadData, $discount);
			if (isset($discount['CONDITIONS_LIST']))
			{
				$discount['CONDITIONS'] = $discount['CONDITIONS_LIST'];
				unset($discount['CONDITIONS_LIST']);
			}
			if (isset($discount['MAX_DISCOUNT']))
			{
				$discount['MAX_VALUE'] = $discount['MAX_DISCOUNT'];
				unset($discount['MAX_DISCOUNT']);
			}
			unset($loadData, $discountIterator);
		}
		$discount['DISCOUNT_ID'] = $id;
		if (empty($discount['MODULE_ID']))
			$discount['MODULE_ID'] = 'catalog';
		if (array_key_exists('HANDLERS', $discount))
		{
			if (!empty($discount['HANDLERS']['MODULES']) && empty($discount['MODULES']))
				$discount['MODULES'] = $discount['HANDLERS']['MODULES'];
			unset($discount['HANDLERS']);
		}
		if (empty($discount['MODULES']))
		{
			$discount['MODULES'] = array();

			$conn = Main\Application::getConnection();
			$helper = $conn->getSqlHelper();
			/** @noinspection SqlResolve */
			$moduleIterator = $conn->query(
				'select MODULE_ID from '.$helper->quote('b_catalog_discount_module').' where '.$helper->quote('DISCOUNT_ID').' = '.$id
			);
			while ($module = $moduleIterator->fetch())
				$discount['MODULES'][] = $module['MODULE_ID'];
			unset($module, $moduleIterator, $helper, $conn);
			if (!in_array('catalog', $discount['MODULES']))
				$discount['MODULES'][] = 'catalog';
		}
		self::$typeCache[$id] = $discount['TYPE'];

		return $discount;
	}

	/**
	 * Prepare entity to iblock and catalog fields.
	 *
	 * @param array $entityList			Entity list.
	 * @return array|bool
	 */
	protected static function prepareEntity($entityList)
	{
		$result = array(
			'iblockFields' => array(),
			'sections' => false,
			'iblockProperties' => array(),
			'iblockPropertiesMap' => array(),
			'catalogFields' => array(),
			'priceFields' => array()
		);

		if (!is_array($entityList))
			return false;

		if (empty($entityList['catalog']))
			return $result;

		if (!empty($entityList['catalog']))
		{
			if (!empty($entityList['catalog']['ELEMENT']) && is_array($entityList['catalog']['ELEMENT']))
			{
				foreach ($entityList['catalog']['ELEMENT'] as $entity)
				{
					if ($entity['FIELD_ENTITY'] == 'SECTION_ID')
					{
						$result['sections'] = true;
						continue;
					}
					$result['iblockFields'][$entity['FIELD_TABLE']] = $entity['FIELD_ENTITY'];
				}
				unset($entity);
			}
			if (!empty($entityList['catalog']['ELEMENT_PROPERTY']) && is_array($entityList['catalog']['ELEMENT_PROPERTY']))
			{
				foreach ($entityList['catalog']['ELEMENT_PROPERTY'] as $entity)
				{
					$propertyData = explode(':', $entity['FIELD_TABLE']);
					if (!is_array($propertyData) || count($propertyData) != 2)
						continue;
					$iblock = (int)$propertyData[0];
					$property = (int)$propertyData[1];
					unset($propertyData);
					if (!isset($result['iblockProperties'][$iblock]))
						$result['iblockProperties'][$iblock] = array();
					$result['iblockProperties'][$iblock][] = $property;
					if (!isset($result['iblockPropertiesMap'][$iblock]))
						$result['iblockPropertiesMap'][$iblock] = array();
					$result['iblockPropertiesMap'][$iblock][$property] = $entity['FIELD_ENTITY'];
				}
				unset($iblock, $property, $entity);
			}

			if (!empty($entityList['catalog']['PRODUCT']) && is_array($entityList['catalog']['PRODUCT']))
			{
				foreach ($entityList['catalog']['PRODUCT'] as $entity)
					$result['catalogFields'][$entity['FIELD_TABLE']] = $entity['FIELD_ENTITY'];
				unset($entity);
			}

			if (!empty($entityList['catalog']['PRICE']) && is_array($entityList['catalog']['PRICE']))
			{
				foreach ($entityList['catalog']['PRICE'] as $entity)
					$result['priceFields'][$entity['FIELD_TABLE']] = $entity['FIELD_ENTITY'];
				unset($entity);
			}
		}

		return $result;
	}

	/**
	 * Returns product separate by iblocks.
	 *
	 * @param array $productList		Product id list.
	 * @return array
	 */
	protected static function getProductIblocks($productList)
	{
		$result = array(
			'iblockElement' => array(),
			'iblockList' => array(),
			'skuIblockList' => array()
		);

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

		$elementIterator = Iblock\ElementTable::getList(array(
			'select' => array('ID', 'IBLOCK_ID'),
			'filter' => array('@ID' => $productList)
		));
		while ($element = $elementIterator->fetch())
		{
			$element['ID'] = (int)$element['ID'];
			$element['IBLOCK_ID'] = (int)$element['IBLOCK_ID'];
			if (!isset($result['iblockElement'][$element['IBLOCK_ID']]))
				$result['iblockElement'][$element['IBLOCK_ID']] = array();
			$result['iblockElement'][$element['IBLOCK_ID']][] = $element['ID'];
		}
		unset($element, $elementIterator);
		if (!empty($result['iblockElement']))
		{
			$result['iblockList'] = array_keys($result['iblockElement']);

			$skuIterator = Catalog\CatalogIblockTable::getList(array(
				'select' => array('IBLOCK_ID', 'PRODUCT_IBLOCK_ID', 'SKU_PROPERTY_ID'),
				'filter' => array('@IBLOCK_ID' => $result['iblockList'], '!=PRODUCT_IBLOCK_ID' => 0)
			));
			while ($sku = $skuIterator->fetch())
			{
				$sku['IBLOCK_ID'] = (int)$sku['IBLOCK_ID'];
				$sku['PRODUCT_IBLOCK_ID'] = (int)$sku['PRODUCT_IBLOCK_ID'];
				$sku['SKU_PROPERTY_ID'] = (int)$sku['SKU_PROPERTY_ID'];
				$result['skuIblockList'][$sku['IBLOCK_ID']] = $sku;
			}
			unset($sku, $skuIterator);
		}

		return $result;
	}

	/**
	 * Create property list for discounts.
	 *
	 * @param array &$entityData			Entity data.
	 * @param array $iblockData				Iblock data.
	 * @return void
	 */
	protected static function fillProductPropertyList(&$entityData, $iblockData)
	{
		$entityData['needProperties'] = array();
		if (!empty($entityData['iblockProperties']) && !empty($iblockData['iblockList']))
		{
			foreach ($iblockData['iblockList'] as $iblock)
			{
				if (!empty($entityData['iblockProperties'][$iblock]))
					$entityData['needProperties'][$iblock] = $entityData['iblockProperties'][$iblock];
			}
			unset($iblock);
		}
		if (!empty($iblockData['skuIblockList']))
		{
			foreach ($iblockData['skuIblockList'] as $skuData)
			{
				if (!isset($entityData['needProperties'][$skuData['IBLOCK_ID']]))
					$entityData['needProperties'][$skuData['IBLOCK_ID']] = array();
				$entityData['needProperties'][$skuData['IBLOCK_ID']][] = $skuData['SKU_PROPERTY_ID'];
				$entityData['iblockPropertiesMap'][$skuData['IBLOCK_ID']][$skuData['SKU_PROPERTY_ID']] = 'PARENT_ID';
				if (!empty($entityData['iblockProperties'][$skuData['PRODUCT_IBLOCK_ID']]))
					$entityData['needProperties'][$skuData['PRODUCT_IBLOCK_ID']] = $entityData['iblockProperties'][$skuData['PRODUCT_IBLOCK_ID']];
			}
			unset($skuData);
		}
	}

	/**
	 * Convert properties values to discount format.
	 *
	 * @param array &$productData			Product data.
	 * @param array $propertyValues			Product properties.
	 * @param array $entityData				Entity data.
	 * @param array $iblockData				Iblock data.
	 * @return void
	 */
	protected static function convertProperties(&$productData, $propertyValues, $entityData, $iblockData)
	{
		if (empty($productData) || !is_array($productData))
			return;
		if (empty($propertyValues) || !is_array($propertyValues))
			return;
		if (empty($entityData) || !is_array($entityData))
			return;
		if (empty($iblockData) || !is_array($iblockData))
			return;

		if (empty($entityData['needProperties']) || !is_array($entityData['needProperties']))
			return;
		$propertyIblocks = array_keys($entityData['needProperties']);
		foreach ($propertyIblocks as &$iblock)
		{
			if (empty($iblockData['iblockElement'][$iblock]))
				continue;
			$propertyMap = $entityData['iblockPropertiesMap'][$iblock];
			foreach ($iblockData['iblockElement'][$iblock] as $element)
			{
				if (empty($propertyValues[$element]))
					continue;
				foreach ($propertyValues[$element] as $property)
				{
					if (empty($property) || empty($property['ID']))
						continue;
					if ($property['PROPERTY_TYPE'] == Iblock\PropertyTable::TYPE_FILE)
						continue;
					$property['ID'] = (int)$property['ID'];
					if (empty($propertyMap[$property['ID']]))
						continue;
					$propertyKey = $propertyMap[$property['ID']];
					$value = '';

					$check = false;
					if ($property['MULTIPLE'] == 'N')
					{
						if (!empty($property['USER_TYPE']))
						{
							switch($property['USER_TYPE'])
							{
								case 'DateTime':
								case 'Date':
									$property['VALUE'] = (string)$property['VALUE'];
									if ($property['VALUE'] != '')
									{
										$propertyFormat = false;
										if ($property['USER_TYPE'] == 'DateTime')
										{
											if (defined('FORMAT_DATETIME'))
												$propertyFormat = FORMAT_DATETIME;
										}
										else
										{
											if (defined('FORMAT_DATE'))
												$propertyFormat = FORMAT_DATE;
										}
										$intStackTimestamp = (int)$property['VALUE'];
										$property['VALUE'] = (
										$intStackTimestamp.'!' != $property['VALUE'].'!'
											? (int)MakeTimeStamp($property['VALUE'], $propertyFormat)
											: $intStackTimestamp
										);
									}
									$value = $property['VALUE'];
									$check = true;
									break;
							}
						}
						if (!$check)
						{
							switch ($property['PROPERTY_TYPE'])
							{
								case Iblock\PropertyTable::TYPE_LIST:
									$property['VALUE_ENUM_ID'] = (int)$property['VALUE_ENUM_ID'];
									$value = ($property['VALUE_ENUM_ID'] > 0 ? $property['VALUE_ENUM_ID'] : -1);
									break;
								case Iblock\PropertyTable::TYPE_ELEMENT:
								case Iblock\PropertyTable::TYPE_SECTION:
									$property['VALUE'] = (int)$property['VALUE'];
									$value = ($property['VALUE'] > 0 ? $property['VALUE'] : -1);
									break;
								default:
									$value = $property['VALUE'];
									break;
							}
						}
					}
					else
					{
						$value = array();
						if (!empty($property['USER_TYPE']))
						{
							switch($property['USER_TYPE'])
							{
								case 'DateTime':
								case 'Date':
									if (!empty($property['VALUE']) && is_array($property['VALUE']))
									{
										$propertyFormat = false;
										if ($property['USER_TYPE'] == 'DateTime')
										{
											if (defined('FORMAT_DATETIME'))
												$propertyFormat = FORMAT_DATETIME;
										}
										else
										{
											if (defined('FORMAT_DATE'))
												$propertyFormat = FORMAT_DATE;
										}
										foreach ($property['VALUE'] as &$oneValue)
										{
											$oneValue = (string)$oneValue;
											if ('' != $oneValue)
											{
												$intStackTimestamp = (int)$oneValue;
												if ($intStackTimestamp.'!' != $oneValue.'!')
													$oneValue = (int)MakeTimeStamp($oneValue, $propertyFormat);
												else
													$oneValue = $intStackTimestamp;
											}
											$value[] = $oneValue;
										}
										unset($oneValue, $propertyFormat);
									}
									$check = true;
									break;
							}
						}
						if (!$check)
						{
							switch ($property['PROPERTY_TYPE'])
							{
								case Iblock\PropertyTable::TYPE_LIST:
									if (!empty($property['VALUE_ENUM_ID']) && is_array($property['VALUE_ENUM_ID']))
									{
										foreach ($property['VALUE_ENUM_ID'] as &$oneValue)
										{
											$oneValue = (int)$oneValue;
											if ($oneValue > 0)
												$value[] = $oneValue;
										}
										unset($oneValue);
									}
									if (empty($value))
										$value = array(-1);
									break;
								case Iblock\PropertyTable::TYPE_ELEMENT:
								case Iblock\PropertyTable::TYPE_SECTION:
									if (!empty($property['VALUE']) && is_array($property['VALUE']))
									{
										foreach ($property['VALUE'] as &$oneValue)
										{
											$oneValue = (int)$oneValue;
											if ($oneValue > 0)
												$value[] = $oneValue;
										}
										unset($oneValue);
									}
									if (empty($value))
										$value = array(-1);
									break;
								default:
									$value = $property['VALUE'];
									break;
							}
						}
					}
					$productData[$element][$propertyKey] = (is_array($value) ? $value : array($value));
				}
			}
			unset($element);
		}
		unset($iblock);
	}

	/**
	 * Returns parent product data.
	 *
	 * @param array &$productData			Product data.
	 * @param array $entityData				Entity data.
	 * @param array $iblockData				Iblock data.
	 * @return void
	 */
	protected static function getParentProducts(&$productData, $entityData, $iblockData)
	{
		if (empty($iblockData['skuIblockList']))
			return;
		if (empty($productData) || !is_array($productData))
			return;
		$parentMap = array();
		$parentData = array();
		$parentIblockData = array(
			'iblockElement' => array(),
			'iblockList' => array()
		);
		if (!empty($entityData['iblockFields']))
		{
			foreach ($entityData['iblockFields'] as &$value)
				$value = 'PARENT_'.$value;
		}
		if (array_key_exists('catalogFields', $entityData))
			unset($entityData['catalogFields']);
		foreach ($iblockData['skuIblockList'] as $skuData)
		{
			if (empty($iblockData['iblockElement'][$skuData['IBLOCK_ID']]))
				continue;
			foreach ($iblockData['iblockElement'][$skuData['IBLOCK_ID']] as $element)
			{
				if (empty($productData[$element]['PARENT_ID']))
					continue;
				$parentId = (int)(
				is_array($productData[$element]['PARENT_ID'])
					? current($productData[$element]['PARENT_ID'])
					: $productData[$element]['PARENT_ID']
				);
				if ($parentId <= 0)
					continue;
				if (!isset($parentMap[$parentId]))
					$parentMap[$parentId] = array();
				$parentMap[$parentId][] = $element;
				$parentData[$parentId] = array();
				if (!isset($parentIblockData['iblockElement'][$skuData['PRODUCT_IBLOCK_ID']]))
					$parentIblockData['iblockElement'][$skuData['PRODUCT_IBLOCK_ID']] = array();
				$parentIblockData['iblockElement'][$skuData['PRODUCT_IBLOCK_ID']][] = $parentId;
			}
			unset($parentId, $element);
		}
		unset($skuData);
		if (empty($parentIblockData['iblockElement']))
			return;
		$parentIblockData['iblockList'] = array_keys($parentIblockData['iblockElement']);

		self::getProductData($parentData, $entityData, $parentIblockData);

		foreach ($parentData as $parentId => $data)
		{
			$parentSections = array();
			if ($entityData['sections'])
			{
				$parentSections = $data['SECTION_ID'];
				unset($data['SECTION_ID']);
			}
			if(!isset($parentMap[$parentId]))
			{
				continue;
			}
			foreach ($parentMap[$parentId] as $element)
			{
				$productData[$element] = array_merge($productData[$element], $data);
				if ($entityData['sections'])
				{
					$productData[$element]['SECTION_ID'] = (
						empty($productData['SECTION_ID'])
						? $parentSections
						: array_merge($productData[$element]['SECTION_ID'], $parentSections)
					);
				}
			}
			unset($element, $parentSections);
		}
		unset($parentId, $data);
	}

	protected static function loadIblockFields(array $productIds, array $fields)
	{
		if (isset($fields['DATE_ACTIVE_FROM']))
		{
			$fields['ACTIVE_FROM'] = $fields['DATE_ACTIVE_FROM'];
			unset($fields['DATE_ACTIVE_FROM']);
		}
		if (isset($fields['DATE_ACTIVE_TO']))
		{
			$fields['ACTIVE_TO'] = $fields['DATE_ACTIVE_TO'];
			unset($fields['DATE_ACTIVE_TO']);
		}

		$productData = array();

		\CTimeZone::Disable();
		$elementIterator = Iblock\ElementTable::getList(array(
			'select' => array_merge(array('ID'), array_keys($fields)),
			'filter' => array('@ID' => $productIds)
		));
		while ($element = $elementIterator->fetch())
		{
			$element['ID'] = (int)$element['ID'];
			foreach ($fields as $key => $alias)
			{
				if ($element[$key] instanceof Main\Type\DateTime)
					$productData[$element['ID']][$alias] = $element[$key]->getTimestamp();
				else
					$productData[$element['ID']][$alias] = $element[$key];
			}
		}
		\CTimeZone::Enable();

		return $productData;
	}

	protected static function loadSections(array $productIds)
	{
		$productSection = array_fill_keys($productIds, array());
		$elementSectionIterator = Iblock\SectionElementTable::getList(array(
			'select' => array('*'),
			'filter' => array('@IBLOCK_ELEMENT_ID' => $productIds)
		));
		while ($elementSection = $elementSectionIterator->fetch())
		{
			$elementSection['IBLOCK_ELEMENT_ID'] = (int)$elementSection['IBLOCK_ELEMENT_ID'];
			$elementSection['IBLOCK_SECTION_ID'] = (int)$elementSection['IBLOCK_SECTION_ID'];
			$elementSection['ADDITIONAL_PROPERTY_ID'] = (int)$elementSection['ADDITIONAL_PROPERTY_ID'];
			if ($elementSection['ADDITIONAL_PROPERTY_ID'] > 0)
				continue;
			$productSection[$elementSection['IBLOCK_ELEMENT_ID']][$elementSection['IBLOCK_SECTION_ID']] = true;
			$parentSectionIterator = \CIBlockSection::GetNavChain(0, $elementSection['IBLOCK_SECTION_ID'], array('ID'));
			while ($parentSection = $parentSectionIterator->fetch())
			{
				$parentSection['ID'] = (int)$parentSection['ID'];
				$productSection[$elementSection['IBLOCK_ELEMENT_ID']][$parentSection['ID']] = true;
			}
			unset($parentSection, $parentSectionIterator);
		}
		unset($elementSection, $elementSectionIterator);

		return $productSection;
	}

	protected static function loadCatalogFields(array $productIds, array $fields)
	{
		$productData = array();

		$productIterator = Catalog\ProductTable::getList(array(
			'select' => array_merge(array('ID'), array_keys($fields)),
			'filter' => array('@ID' => $productIds)
		));
		while ($product = $productIterator->fetch())
		{
			$product['ID'] = (int)$product['ID'];
			foreach ($fields as $key => $alias)
			{
				$productData[$product['ID']][$alias] = $product[$key];
			}
		}

		return $productData;
	}

	protected static function fillProperties(array &$productData, array $productIds, array $iblockData, array $entityData)
	{
		$propertyValues = array_fill_keys($productIds, array());
		foreach ($entityData['needProperties'] as $iblock => $propertyList)
		{
			if (empty($iblockData['iblockElement'][$iblock]))
			{
				continue;
			}

			$needToLoad = array_fill_keys($iblockData['iblockElement'][$iblock], true);
			if(self::$productProperties)
			{
				foreach ($iblockData['iblockElement'][$iblock] as $productId)
				{
					$allExist = true;
					foreach ($propertyList as $prop)
					{
						$propData = self::getCachedProductProperty($productId, $prop);
						if (!empty($propData))
						{
							$propertyValues[$productId][$propData['ID']] = $propData;
						}
						else
						{
							$allExist = false;
							break;
						}
					}
					unset($prop);
					if (!$allExist)
					{
						// if property value is not exist
						$propertyValues[$productId] = array();
					}
					else
					{
						unset($needToLoad[$productId]);
					}
					unset($allExist);
				}
			}

			if(!empty($needToLoad))
			{
				$iblockPropertyValues = array_fill_keys(array_keys($needToLoad), array());

				$filter = array(
					'ID' => $iblockData['iblockElement'][$iblock],
					'IBLOCK_ID' => $iblock
				);

				\CTimeZone::Disable();
				\CIBlockElement::GetPropertyValuesArray(
					$iblockPropertyValues,
					$iblock,
					$filter,
					array('ID' => $propertyList),
					array(
						'USE_PROPERTY_ID' => 'Y',
						'PROPERTY_FIELDS' => array('ID', 'PROPERTY_TYPE', 'MULTIPLE', 'USER_TYPE')
					)
				);
				\CTimeZone::Enable();

				foreach ($iblockPropertyValues as $productId => $data)
					$propertyValues[$productId] = $data;
				unset($productId, $data, $iblockPropertyValues);
			}
		}

		self::convertProperties($productData, $propertyValues, $entityData, $iblockData);
	}

	/**
	 * Returns product data.
	 *
	 * @param array &$productData			Product data.
	 * @param array $entityData				Entity data.
	 * @param array $iblockData				Iblock list data.
	 * @return void
	 */
	protected static function getProductData(&$productData, $entityData, $iblockData)
	{
		if (!empty($iblockData['iblockElement']))
		{
			$productList = array_keys($productData);
			if (!empty($entityData['iblockFields']))
			{
				foreach(self::loadIblockFields($productList, $entityData['iblockFields']) as $productId => $fields)
				{
					$productData[$productId] = (
						empty($productData[$productId])
						? $fields
						: array_merge($productData[$productId], $fields)
					);
				}
				unset($fields);
			}
			if ($entityData['sections'])
			{
				foreach(self::loadSections($productList) as $element => $sections)
				{
					$productData[$element]['SECTION_ID'] = array_keys($sections);
				}
			}
			if (!empty($entityData['needProperties']))
			{
				self::fillProperties($productData, $productList, $iblockData, $entityData);
			}
			if (!empty($entityData['catalogFields']))
			{
				foreach(self::loadCatalogFields($productList, $entityData['catalogFields']) as $productId => $fields)
				{
					$productData[$productId] = (
						empty($productData[$productId])
						? $fields
						: array_merge($productData[$productId], $fields)
					);
				}
				unset($fields);
			}
			if (!empty($entityData['priceFields']) && !empty($entityData['priceData']))
			{
				foreach($entityData['priceData'] as $productId => $priceId)
				{
					$productData[$productId]['CATALOG_GROUP_ID'] = $priceId;
				}
				unset($product, $productIterator);
			}

			if (!empty($iblockData['skuIblockList']))
				self::getParentProducts($productData, $entityData, $iblockData);
		}
	}

	/**
	 * Create sale action.
	 *
	 * @param array &$discount			Discount data.
	 * @param array $params				Manager parameters.
	 * @return void
	 */
	protected static function createSaleAction(&$discount, $params)
	{
		$data = array(
			'TYPE' => $discount['VALUE_TYPE'],
			'VALUE' => $discount['VALUE'],
			'CURRENCY' => $discount['CURRENCY'],
			'USE_BASE_PRICE' => $params['USE_BASE_PRICE']
		);
		if ($discount['TYPE'] == Catalog\DiscountTable::VALUE_TYPE_PERCENT)
			$data['MAX_VALUE'] = $discount['MAX_VALUE'];

		$action = '\Bitrix\Catalog\Discount\DiscountManager::applyDiscount('.$params['BASKET_ITEM'].', '.var_export($data, true).');';
		$discount['APPLICATION'] = 'function (&'.$params['BASKET_ITEM'].'){'.$action.'};';
		$discount['ACTIONS'] = $data;
		unset($action, $data);

		if (self::$saleIncluded === null)
			self::$saleIncluded = Loader::includeModule('sale');
		if (!self::$saleIncluded)
			return;

		$type = '';
		$descr = array(
			'VALUE_ACTION' => (
				$discount['TYPE'] == Catalog\DiscountTable::TYPE_DISCOUNT_SAVE
				? Sale\OrderDiscountManager::DESCR_VALUE_ACTION_ACCUMULATE
				: Sale\OrderDiscountManager::DESCR_VALUE_ACTION_DISCOUNT
			),
			'VALUE' => $discount['VALUE']
		);
		switch ($discount['VALUE_TYPE'])
		{
			case Catalog\DiscountTable::VALUE_TYPE_PERCENT:
				$type = (
					$discount['MAX_VALUE'] > 0
					? Sale\OrderDiscountManager::DESCR_TYPE_LIMIT_VALUE
					: Sale\OrderDiscountManager::DESCR_TYPE_VALUE
				);
				$descr['VALUE_TYPE'] = Sale\OrderDiscountManager::DESCR_VALUE_TYPE_PERCENT;
				if ($discount['MAX_VALUE'] > 0)
				{
					$descr['LIMIT_TYPE'] = Sale\OrderDiscountManager::DESCR_LIMIT_MAX;
					$descr['LIMIT_UNIT'] = $discount['CURRENCY'];
					$descr['LIMIT_VALUE'] = $discount['MAX_VALUE'];
				}
				break;
			case Catalog\DiscountTable::VALUE_TYPE_FIX:
				$type = Sale\OrderDiscountManager::DESCR_TYPE_VALUE;
				$descr['VALUE_TYPE'] = Sale\OrderDiscountManager::DESCR_VALUE_TYPE_CURRENCY;
				$descr['VALUE_UNIT'] = $discount['CURRENCY'];
				break;
			case Catalog\DiscountTable::VALUE_TYPE_SALE:
				$type = Sale\OrderDiscountManager::DESCR_TYPE_FIXED;
				$descr['VALUE_UNIT'] = $discount['CURRENCY'];
				break;
		}
		$descrResult = Sale\OrderDiscountManager::prepareDiscountDescription($type, $descr);
		if ($descrResult->isSuccess())
		{
			$discount['ACTIONS_DESCR'] = array(
				'BASKET' => array(
					0 => $descrResult->getData()
				)
			);
		}
		unset($descrResult, $descr, $type);
	}

	protected static function fillProductPriceList(&$entityData, $priceIds)
	{
		$entityData['priceData'] = array();
		if(empty($entityData['priceFields']) || empty($priceIds))
		{
			return;
		}

		$priceData = array();
		$priceList = Catalog\PriceTable::getList(array(
			'select' => array(
				'PRODUCT_ID',
				'CATALOG_GROUP_ID',
			),
			'filter' => array('@ID' => $priceIds),
		));
		while($price = $priceList->fetch())
		{
			if(!isset($priceData[$price['PRODUCT_ID']]))
			{
				$priceData[$price['PRODUCT_ID']] = array();
			}
			$priceData[$price['PRODUCT_ID']] = $price['CATALOG_GROUP_ID'];
		}

		$entityData['priceData'] = $priceData;
	}

	/**
	 * Rounded catalog discount value.
	 *
	 * @param float|int $value Value.
	 * @param string $currency Currency.
	 * @return float
	 */
	protected static function roundValue($value, $currency)
	{
		if (self::$saleIncluded === null)
			self::$saleIncluded = Loader::includeModule('sale');
		if (self::$saleIncluded)
			return Sale\Discount\Actions::roundValue($value, $currency);
		else
			return roundEx($value, CATALOG_VALUE_PRECISION);
	}

	/**
	 * Returns data after price rounding.
	 * @internal
	 *
	 * @param array $basketItem     Basket row data.
	 * @param array $roundData      Round rule.
	 * @return array
	 */
	private static function getRoundResult(array $basketItem, array $roundData)
	{
		$result = array(
			'ROUND_RULE' => $roundData
		);
		$result['PRICE'] = Catalog\Product\Price::roundValue(
			$basketItem['PRICE'],
			$roundData['ROUND_PRECISION'],
			$roundData['ROUND_TYPE']
		);

		if (isset($basketItem['BASE_PRICE']))
		{
			$result['DISCOUNT_PRICE'] = $basketItem['BASE_PRICE'] - $result['PRICE'];
		}
		else
		{
			if (!isset($result['DISCOUNT_PRICE']))
				$result['DISCOUNT_PRICE'] = 0;
			$result['DISCOUNT_PRICE'] += ($basketItem['PRICE'] - $result['PRICE']);
		}

		return $result;
	}

	private static function getPriceDataByPriceId($priceId)
	{
		foreach(self::$preloadedPriceData as $priceData)
		{
			if($priceData['ID'] == $priceId)
			{
				return $priceData;
			}
		}

		return null;
	}

	private static function getCachedProductProperty($productId, $propertyId)
	{
		if(!isset(self::$productProperties[$productId]))
		{
			return null;
		}

		foreach(self::$productProperties[$productId] as $props)
		{
			if($props['ID'] == $propertyId)
			{
				return $props;
			}
		}

		return null;
	}

	private static function getProduct($productId, array $fieldsData, array $entityList = array())
	{
		$product = array();
		if(isset(self::$preloadedProductsData[$productId]))
		{
			$product = self::$preloadedProductsData[$productId];
		}

		if(!empty($fieldsData['iblockFields']))
		{
			$aliases = array_fill_keys(
				array_values($fieldsData['iblockFields']),
				true
			);
			$needleFields = array_diff_key($aliases, $product);
			if($needleFields)
			{
				foreach(self::loadIblockFields(array($productId), $needleFields) as $pId => $fields)
				{
					if($pId != $productId)
					{
						continue;
					}

					$product = array_merge($product, $fields);
				}
			}
		}
		if(!empty($fieldsData['catalogFields']))
		{
			$aliases = array_fill_keys(
				array_values($fieldsData['catalogFields']),
				true
			);
			$needleFields = array_diff_key($aliases, $product);
			if($needleFields)
			{
				foreach(self::loadCatalogFields(array($productId), $needleFields) as $pId => $fields)
				{
					if($pId != $productId)
					{
						continue;
					}

					$product = array_merge($product, $fields);
				}
			}
		}
		if(!empty($fieldsData['sections']) && !is_array($product['SECTION_ID']))
		{
			foreach(self::loadSections(array($productId)) as $pId => $sections)
			{
				if($pId != $productId)
				{
					continue;
				}
				$product['SECTION_ID'] = array_keys($sections);
			}
		}
		if(!empty($fieldsData['elementProperties']) && $entityList)
		{
		}
	}
}