AnonSec Shell
Server IP : 85.193.89.191  /  Your IP : 18.218.8.152
Web Server : Apache
System : Linux 956367-cx40159.tmweb.ru 3.10.0-1160.105.1.el7.x86_64 #1 SMP Thu Dec 7 15:39:45 UTC 2023 x86_64
User : bitrix ( 600)
PHP Version : 8.1.27
Disable Function : NONE
MySQL : OFF  |  cURL : OFF  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : ON  |  Pkexec : ON
Directory :  /home/bitrix/www/bitrix/modules/sale/lib/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ HOME ]     

Current File : /home/bitrix/www/bitrix/modules/sale/lib/discountcouponsmanagerbase.php
<?php

namespace Bitrix\Sale;

use Bitrix\Main;
use Bitrix\Main\Application;
use Bitrix\Main\Config\Option;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Session\Session;
use Bitrix\Sale;

class DiscountCouponsManagerBase
{
	public const MODE_CLIENT = 0x0001;
	public const MODE_MANAGER = 0x0002;
	public const MODE_ORDER = 0x0004;
	public const MODE_SYSTEM = 0x0008;
	public const MODE_EXTERNAL = 0x0010;

	public const STATUS_NOT_FOUND = 0x0001;
	public const STATUS_ENTERED = 0x0002;
	public const STATUS_APPLYED = 0x0004;
	public const STATUS_NOT_APPLYED = 0x0008;
	public const STATUS_FREEZE = 0x0010;

	public const COUPON_CHECK_OK = 0x0000;
	public const COUPON_CHECK_NOT_FOUND = 0x0001;
	public const COUPON_CHECK_NO_ACTIVE = 0x0002;
	public const COUPON_CHECK_RANGE_ACTIVE_FROM = 0x0004;
	public const COUPON_CHECK_RANGE_ACTIVE_TO = 0x0008;
	public const COUPON_CHECK_NO_ACTIVE_DISCOUNT = 0x0010;
	public const COUPON_CHECK_RANGE_ACTIVE_FROM_DISCOUNT = 0x0020;
	public const COUPON_CHECK_RANGE_ACTIVE_TO_DISCOUNT = 0x0040;
	public const COUPON_CHECK_BAD_USER_ID = 0x0080;
	public const COUPON_CHECK_ALREADY_MAX_USED = 0x0100;
	public const COUPON_CHECK_UNKNOWN_TYPE = 0x0200;
	public const COUPON_CHECK_CORRUPT_DATA = 0x0400;
	public const COUPON_CHECK_NOT_APPLIED = 0x0800;

	public const COUPON_MODE_SIMPLE = 0x0001;
	public const COUPON_MODE_FULL = 0x0002;

	public const EVENT_ON_BUILD_COUPON_PROVIDES = 'onBuildCouponProviders';
	public const EVENT_ON_SAVE_APPLIED_COUPONS = 'onManagerSaveApplied';
	public const EVENT_ON_COUPON_ADD = 'onManagerCouponAdd';
	public const EVENT_ON_COUPON_DELETE = 'onManagerCouponDelete';
	public const EVENT_ON_COUPON_APPLY_PRODUCT = 'onManagerCouponApplyByProduct';
	public const EVENT_ON_COUPON_APPLY = 'onManagerCouponApply';

	public const STORAGE_MANAGER_COUPONS = 'CATALOG_MANAGE_COUPONS';
	public const STORAGE_CLIENT_COUPONS = 'CATALOG_USER_COUPONS';

	protected static array $coupons = [];
	protected static bool $init = false;
	protected static int $useMode = self::MODE_CLIENT;
	protected static array $errors = [];
	protected static ?bool $onlySaleDiscount = null;
	protected static ?int $userId = null;
	protected static array $couponProviders = [];
	protected static array $couponTypes = [];
	protected static int $couponIndex = 0;
	protected static ?int $orderId = null;
	protected static bool $allowedSave = false;
	protected static bool $checkActivity = true;
	protected static bool $useOrderCoupons = true;

	protected static array $clearFields = [
		'STATUS',
		'CHECK_CODE',
		'DISCOUNT_NAME',
		'DISCOUNT_ACTIVE',
		'SAVED',
		'BASKET',
		'DELIVERY',
	];
	protected static array $timeFields = [
		'DISCOUNT_ACTIVE_FROM',
		'DISCOUNT_ACTIVE_TO',
		'ACTIVE_FROM',
		'ACTIVE_TO',
	];

	protected static int $allowCouponStorage = 0;

	protected static array $lockedCoupons = [];

	/**
	 * @throws Main\NotImplementedException
	 * @return string
	 */
	public static function getRegistryType()
	{
		throw new Main\NotImplementedException();
	}

	/**
	 * Init use mode and user id.
	 *
	 * @param int $mode			Discount manager mode.
	 * @param array $params		Initial params (userId, orderId, oldUserId)
	 * 		keys are case-sensitive:
	 * 			<ul>
	 * 			<li>int userId		Order owner (for MODE_MANAGER or MODE_ORDER only)
	 * 			<li>int orderId		Edit order id (for MODE_ORDER only(!))
	 * 			<li>int oldUserId	Old order owner (for MODE_MANAGER or MODE_ORDER only)
	 * 			</ul>.
	 * @return void
	 */
	public static function initUseMode($mode = self::MODE_CLIENT, $params = [])
	{
		$mode = (int)$mode;
		if (!is_array($params))
		{
			$params = [];
		}
		self::$checkActivity = true;
		self::$userId = null;
		self::$orderId = null;
		self::$allowedSave = false;
		self::$useOrderCoupons = true;

		self::$useMode = self::MODE_SYSTEM;
		switch ($mode)
		{
			case self::MODE_MANAGER:
				if (!isset($params['userId']) || (int)$params['userId'] < 0)
				{
					self::$errors[] = Loc::getMessage('BX_SALE_DCM_ERR_BAD_USER_ID');
				}
				if (isset($params['orderId']))
				{
					self::$errors[] = Loc::getMessage('BX_SALE_DCM_ERR_ORDER_ID_EXIST');
				}
				if (empty(self::$errors))
				{
					self::$userId = (int)$params['userId'];
					self::$orderId = null;
					self::$allowedSave = true;
					self::$useMode = self::MODE_MANAGER;
					if (isset($params['oldUserId']))
					{
						self::migrateStorage($params['oldUserId']);
					}
				}
				break;
			case self::MODE_ORDER:
				if (!isset($params['userId']) || (int)$params['userId'] < 0)
				{
					self::$errors[] = Loc::getMessage('BX_SALE_DCM_ERR_BAD_USER_ID');
				}
				if (!isset($params['orderId']) || (int)$params['orderId'] <= 0)
				{
					self::$errors[] = Loc::getMessage('BX_SALE_DCM_ERR_ORDER_ID_ABSENT');
				}
				if (empty(self::$errors))
				{
					self::$userId = (int)$params['userId'];
					self::$orderId = (int)$params['orderId'];
					self::$allowedSave = true;
					self::$useMode = self::MODE_ORDER;
					if (isset($params['oldUserId']))
					{
						self::migrateStorage($params['oldUserId']);
					}

				}
				break;
			case self::MODE_CLIENT:
				self::$useMode = self::MODE_CLIENT;
				if (isset($params['userId']) && (int)$params['userId'] >= 0)
				{
					self::$userId = (int)$params['userId'];
				}
				else
				{
					self::initUserId();
				}
				if (self::isSuccess())
				{
					self::$allowedSave = true;
				}
				break;
			case self::MODE_EXTERNAL:
				self::$useMode = self::MODE_EXTERNAL;
				self::$userId = (
					isset($params['userId']) && (int)$params['userId'] >= 0
						? (int)$params['userId']
						: \CSaleUser::GetAnonymousUserID()
				);
				break;
			case self::MODE_SYSTEM:
				break;
			default:
				self::$errors[] = Loc::getMessage('BX_SALE_DCM_ERR_BAD_MODE');
				break;
		}
	}

	/**
	 * Returns use mode.
	 *
	 * @return int
	 */
	public static function getUseMode()
	{
		return self::$useMode;
	}

	/**
	 * Verifies that the client mode being used.
	 *
	 * @return bool
	 */
	public static function usedByClient()
	{
		return (self::$useMode === self::MODE_CLIENT);
	}

	/**
	 * Verifies that the manager mode being used.
	 *
	 * @return bool
	 */
	public static function usedByManager()
	{
		return (self::$useMode === self::MODE_MANAGER || self::$useMode === self::MODE_ORDER);
	}

	/**
	 * Verifies that the external order mode being used.
	 *
	 * @return bool
	 */
	public static function usedByExternal()
	{
		return (self::$useMode === self::MODE_EXTERNAL);
	}

	/**
	 * Returns user id.
	 *
	 * @return null|int
	 */
	public static function getUserId()
	{
		if ((self::$userId === null || self::$userId === 0) && self::usedByClient())
		{
			self::$userId = null;
			self::initUserId();
		}

		return self::$userId;
	}

	/**
	 * Returns order id, if current use mode self::MODE_ORDER.
	 *
	 * @return null|int
	 */
	public static function getOrderId()
	{
		return self::$orderId;
	}

	/**
	 * Session object.
	 *
	 * If session is not accessible, returns null and add error.
	 *
	 * @return Session|null
	 */
	protected static function getSession(): ?Session
	{
		/** @var Session $session */
		$session = Application::getInstance()->getSession();
		if (!$session->isAccessible())
		{
			self::$errors[] = Loc::getMessage('BX_SALE_DCM_ERR_SESSION_NOT_ACCESSIBLE');

			return null;
		}

		return $session;
	}

	/**
	 * Returns a sign of success.
	 *
	 * @return bool
	 */
	public static function isSuccess()
	{
		return empty(self::$errors);
	}

	/**
	 * Returns error list.
	 *
	 * @return array
	 */
	public static function getErrors()
	{
		return self::$errors;
	}

	/**
	 * Clear errors list.
	 *
	 * @return void
	 */
	public static function clearErrors()
	{
		self::$errors = [];
	}

	/**
	 * Returns coupon status list.
	 *
	 * @param bool $extendedMode	Get status Ids or Ids with description.
	 * @return array
	 */
	public static function getStatusList($extendedMode = false)
	{
		$extendedMode = ($extendedMode === true);
		if ($extendedMode)
		{
			return [
				self::STATUS_NOT_FOUND => Loc::getMessage('BX_SALE_DCM_STATUS_NOT_FOUND'),
				self::STATUS_ENTERED => Loc::getMessage('BX_SALE_DCM_STATUS_ENTERED'),
				self::STATUS_NOT_APPLYED => Loc::getMessage('BX_SALE_DCM_STATUS_NOT_APPLYED'),
				self::STATUS_APPLYED => Loc::getMessage('BX_SALE_DCM_STATUS_APPLYED'),
				self::STATUS_FREEZE => Loc::getMessage('BX_SALE_DCM_STATUS_FREEZE'),
			];
		}

		return [
			self::STATUS_NOT_FOUND,
			self::STATUS_ENTERED,
			self::STATUS_NOT_APPLYED,
			self::STATUS_APPLYED,
			self::STATUS_FREEZE,
		];
	}

	/**
	 * Returns check code list.
	 *
	 * @param bool $extendedMode	Get codes or codes with description.
	 * @return array
	 */
	public static function getCheckCodeList($extendedMode = false)
	{
		$extendedMode = ($extendedMode === true);
		if ($extendedMode)
		{
			return [
				self::COUPON_CHECK_OK => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_OK'),
				self::COUPON_CHECK_NOT_FOUND => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_NOT_FOUND'),
				self::COUPON_CHECK_NO_ACTIVE => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_NO_ACTIVE'),
				self::COUPON_CHECK_RANGE_ACTIVE_FROM => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_RANGE_ACTIVE_FROM'),
				self::COUPON_CHECK_RANGE_ACTIVE_TO => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_RANGE_ACTIVE_TO'),
				self::COUPON_CHECK_NO_ACTIVE_DISCOUNT => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_NO_ACTIVE_DISCOUNT'),
				self::COUPON_CHECK_RANGE_ACTIVE_FROM_DISCOUNT => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_RANGE_ACTIVE_FROM_DISCOUNT'),
				self::COUPON_CHECK_RANGE_ACTIVE_TO_DISCOUNT => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_RANGE_ACTIVE_TO_DISCOUNT'),
				self::COUPON_CHECK_BAD_USER_ID => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_BAD_USER_ID'),
				self::COUPON_CHECK_ALREADY_MAX_USED => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_ALREADY_MAX_USED'),
				self::COUPON_CHECK_UNKNOWN_TYPE => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_UNKNOWN_TYPE'),
				self::COUPON_CHECK_CORRUPT_DATA => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_CORRUPT_DATA'),
				self::COUPON_CHECK_NOT_APPLIED => Loc::getMessage('BX_SALE_DCM_COUPON_CHECK_NOT_APPLIED'),
			];
		}

		return [
			self::COUPON_CHECK_OK,
			self::COUPON_CHECK_NOT_FOUND,
			self::COUPON_CHECK_NO_ACTIVE,
			self::COUPON_CHECK_RANGE_ACTIVE_FROM,
			self::COUPON_CHECK_RANGE_ACTIVE_TO,
			self::COUPON_CHECK_NO_ACTIVE_DISCOUNT,
			self::COUPON_CHECK_RANGE_ACTIVE_FROM_DISCOUNT,
			self::COUPON_CHECK_RANGE_ACTIVE_TO_DISCOUNT,
			self::COUPON_CHECK_BAD_USER_ID,
			self::COUPON_CHECK_ALREADY_MAX_USED,
			self::COUPON_CHECK_UNKNOWN_TYPE,
			self::COUPON_CHECK_CORRUPT_DATA,
			self::COUPON_CHECK_NOT_APPLIED,
		];
	}

	/**
	 * Returns description of check code.
	 * @param int $code Code value.
	 *
	 * @return string|null
	 */
	public static function getCheckCodeMessage($code)
	{
		$codes = self::getCheckCodeList(true);
		if (isset($codes[$code]))
		{
			return $codes[$code];
		}

		return null;
	}

	/**
	 * Set use ordered coupons for apply.
	 *
	 * @param bool $state		Use state.
	 * @return void
	 */
	public static function useSavedCouponsForApply($state)
	{
		if ($state !== true && $state !== false)
		{
			return;
		}
		self::$useOrderCoupons = $state;
	}

	/**
	 * Returns use ordered coupons for apply.
	 *
	 * @return bool
	 */
	public static function isUsedOrderCouponsForApply()
	{
		return self::$useOrderCoupons;
	}

	/**
	 * Enable get coupons for calculate discounts.
	 *
	 * @return void
	 */
	public static function unFreezeCouponStorage()
	{
		self::$allowCouponStorage++;
	}

	/**
	 * Disable get coupons for calculate discounts.
	 *
	 * @return void
	 */
	public static function freezeCouponStorage()
	{
		self::$allowCouponStorage--;
	}

	/**
	 * Returns true, if disallow get coupons for calculate discounts.
	 *
	 * @return bool
	 */
	public static function isFrozenCouponStorage()
	{
		return (self::$allowCouponStorage < 0);
	}

	/**
	 * Initialization coupon manager.
	 *
	 * @param int $mode				Discount manager mode.
	 * @param array $params			Initial params (userId, orderId, oldUserId)
	 * 		keys are case-sensitive:
	 * 			<ul>
	 * 			<li>int userId		Order owner (for MODE_MANAGER or MODE_ORDER only)
	 * 			<li>int orderId		Edit order id (for MODE_ORDER only(!))
	 * 			<li>int oldUserId	Old order owner (for MODE_MANAGER or MODE_ORDER only)
	 * 			</ul>.
	 * @param bool $clearStorage	Clear coupon session storage.
	 * @return void
	 */
	public static function init($mode = self::MODE_CLIENT, $params = [], $clearStorage = false)
	{
		if (self::$init)
		{
			return;
		}
		self::$onlySaleDiscount = null;
		self::$couponTypes = Internals\DiscountCouponTable::getCouponTypes(true);
		self::$couponIndex = 0;
		self::$lockedCoupons = [];
		self::clearErrors();
		self::initUseMode($mode, $params);
		self::initUseDiscount();
		if (!self::isSuccess())
		{
			return;
		}
		if (self::$useMode !== self::MODE_SYSTEM)
		{
			$session = self::getSession();
			if (!$session)
			{
				return;
			}

			self::clear($clearStorage);
			$couponsList = [];
			switch (self::$useMode)
			{
				case self::MODE_CLIENT:
				case self::MODE_EXTERNAL:
					if (
						!empty($session[self::STORAGE_CLIENT_COUPONS])
						&& is_array($session[self::STORAGE_CLIENT_COUPONS])
					)
					{
						$couponsList = $session[self::STORAGE_CLIENT_COUPONS];
					}
					break;
				case self::MODE_MANAGER:
					if (
						!empty($session[self::STORAGE_MANAGER_COUPONS])
						&& !empty($session[self::STORAGE_MANAGER_COUPONS][self::$userId])
						&& is_array($session[self::STORAGE_MANAGER_COUPONS][self::$userId])
					)
					{
						$couponsList = $session[self::STORAGE_MANAGER_COUPONS][self::$userId];
					}
					break;
				case self::MODE_ORDER:
					self::load();
					if (
						!empty($session[self::STORAGE_MANAGER_COUPONS])
						&& !empty($session[self::STORAGE_MANAGER_COUPONS][self::$userId])
						&& is_array($session[self::STORAGE_MANAGER_COUPONS][self::$userId])
					)
					{
						$couponsList = $session[self::STORAGE_MANAGER_COUPONS][self::$userId];
					}
					break;
			}
			if (!empty($couponsList))
			{
				self::setCoupons($couponsList);
			}
			unset($couponsList);
			if (self::$useMode === self::MODE_ORDER)
			{
				self::saveToStorage();
			}
		}
		self::$init = true;
	}

	/**
	 * Unconditional reinitialization coupon manager.
	 *
	 * @param int $mode				Discount manager mode.
	 * @param array $params			Initial params (userId, orderId, oldUserId)
	 * 		keys are case-sensitive:
	 * 			<ul>
	 * 			<li>int userId		Order owner (for MODE_MANAGER or MODE_ORDER only)
	 * 			<li>int orderId		Edit order id (for MODE_ORDER only(!))
	 * 			<li>int oldUserId	Old order owner (for MODE_MANAGER or MODE_ORDER only)
	 * 			</ul>.
	 * @param bool $clearStorage	Clear coupon session storage.
	 * @return void
	 */
	public static function reInit($mode = self::MODE_CLIENT, $params = [], $clearStorage = false)
	{
		if (self::isFrozenCouponStorage())
		{
			return;
		}
		self::$init = false;
		self::init($mode, $params, $clearStorage);
	}

	/**
	 * Returns true, if coupons were are entered.
	 *
	 * @return bool
	 */
	public static function isEntered()
	{
		return !empty(self::$coupons);
	}

	/**
	 * Add coupon in manager.
	 *
	 * @param string $coupon	Added coupon.
	 * @return bool
	 */
	public static function add($coupon)
	{
		if (!self::$init)
		{
			self::init();
		}
		if (self::$useMode === self::MODE_SYSTEM || !self::isSuccess())
		{
			return false;
		}

		$coupon = trim((string)$coupon);
		if ($coupon === '')
		{
			return false;
		}
		if (!isset(self::$coupons[$coupon]))
		{
			$couponData = self::getData($coupon);
			if (!isset(self::$coupons[$couponData['COUPON']]))
			{
				$couponData['SORT'] = self::$couponIndex;
				self::createApplyFields($couponData);
				self::$coupons[$couponData['COUPON']] = $couponData;
				self::$couponIndex++;
				self::saveToStorage();
				$event = new Main\Event('sale', self::EVENT_ON_COUPON_ADD, $couponData);
				$event->send();
			}
			if (self::$coupons[$couponData['COUPON']]['MODE'] === self::COUPON_MODE_FULL)
			{
				return (self::$coupons[$couponData['COUPON']]['STATUS'] != self::STATUS_NOT_FOUND);
			}
			else
			{
				return (
					self::$coupons[$couponData['COUPON']]['STATUS'] != self::STATUS_NOT_FOUND
					&& self::$coupons[$couponData['COUPON']]['STATUS'] != self::STATUS_FREEZE
				);
			}
		}
		else
		{
			if (self::$coupons[$coupon]['MODE'] === self::COUPON_MODE_FULL)
			{
				return (self::$coupons[$coupon]['STATUS'] != self::STATUS_NOT_FOUND);
			}
			else
			{
				return (
					self::$coupons[$coupon]['STATUS'] != self::STATUS_NOT_FOUND
					&& self::$coupons[$coupon]['STATUS'] != self::STATUS_FREEZE
				);
			}
		}
	}

	/**
	 * Delete coupon from manager.
	 *
	 * @param string $coupon	Deleted coupon.
	 * @return bool
	 */
	public static function delete($coupon)
	{
		if (!self::$init)
		{
			self::init();
		}
		if (self::$useMode === self::MODE_SYSTEM || !self::isSuccess())
		{
			return false;
		}

		$coupon = trim((string)$coupon);
		if ($coupon === '')
		{
			return false;
		}
		$founded = false;
		if (isset(self::$coupons[$coupon]))
		{
			$couponData = self::$coupons[$coupon];
			unset(self::$coupons[$coupon]);
			$founded = true;
		}
		else
		{
			$couponData = self::getData($coupon, false);
			if (isset(self::$coupons[$couponData['COUPON']]))
			{
				unset(self::$coupons[$couponData['COUPON']]);
				$founded = true;
			}
		}
		if ($founded)
		{
			self::saveToStorage();
			$event = new Main\Event('sale', self::EVENT_ON_COUPON_DELETE, $couponData);
			$event->send();

			return true;
		}

		return false;
	}

	/**
	 * Clear coupon storage.
	 *
	 * @param bool $clearStorage		Clear coupon session storage.
	 * @return bool
	 */
	public static function clear($clearStorage = false)
	{
		if (self::$useMode === self::MODE_SYSTEM || !self::isSuccess())
		{
			return false;
		}

		$clearStorage = ($clearStorage === true);
		self::$coupons = [];
		if ($clearStorage)
		{
			self::saveToStorage();
		}

		return true;
	}

	/**
	 * Clear coupon storage for order.
	 *
	 * @param int $order			Order id.
	 * @return bool
	 */
	public static function clearByOrder($order)
	{
		if (!self::isSuccess())
		{
			return false;
		}
		$order = (int)$order;
		if ($order <= 0)
		{
			return false;
		}
		$userId = 0;

		$registry = Registry::getInstance(static::getRegistryType());

		/** @var OrderBase $orderClassName */
		$orderClassName = $registry->getOrderClassName();

		$orderIterator = $orderClassName::getList([
			'select' => ['ID', 'USER_ID'],
			'filter' => ['=ID' => $order],
		]);
		if ($orderData = $orderIterator->fetch())
		{
			$userId = (int)$orderData['USER_ID'];
		}
		unset($orderData, $orderIterator);
		if ($userId <= 0)
		{
			return false;
		}
		self::initUseMode(
			self::MODE_ORDER,
			[
				'userId' => $userId,
				'orderId' => $order,
			]
		);
		if (!self::isSuccess())
		{
			return false;
		}
		self::$coupons = [];
		self::saveToStorage();

		return true;
	}

	/**
	 * Change coupons owner in manager or order mode.
	 *
	 * @param int $oldUser				Old user id.
	 * @return void
	 */
	public static function migrateStorage($oldUser)
	{
		if (self::$useMode !== self::MODE_MANAGER && self::$useMode !== self::MODE_ORDER || self::$userId === null)
		{
			return;
		}

		$oldUser = (int)$oldUser;
		if ($oldUser < 0)
		{
			return;
		}

		$session = self::getSession();
		if (!$session)
		{
			return;
		}

		if (empty($session[self::STORAGE_MANAGER_COUPONS]))
		{
			$session[self::STORAGE_MANAGER_COUPONS] = [];
		}

		if (
			empty($session[self::STORAGE_MANAGER_COUPONS][self::$userId])
			|| !is_array($session[self::STORAGE_MANAGER_COUPONS][self::$userId])
		)
		{
			$session[self::STORAGE_MANAGER_COUPONS][self::$userId] = [];
		}

		if (!empty($session[self::STORAGE_MANAGER_COUPONS][$oldUser]))
		{
			if (is_array($session[self::STORAGE_MANAGER_COUPONS][$oldUser]))
			{
				$session[self::STORAGE_MANAGER_COUPONS][self::$userId] = $session[self::STORAGE_MANAGER_COUPONS][$oldUser];
			}
			unset($session[self::STORAGE_MANAGER_COUPONS][$oldUser]);
		}
	}

	/**
	 * Load coupons for existing order.
	 *
	 * @return void
	 */
	public static function load()
	{
		if (self::$useMode !== self::MODE_ORDER)
		{
			return;
		}

		self::$checkActivity = false;
		$couponsList = [];

		$registry = Registry::getInstance(static::getRegistryType());

		/** @var OrderDiscountBase $storageClassName */
		$storageClassName = $registry->getOrderDiscountClassName();

		$couponIterator = $storageClassName::getOrderCouponIterator([
			'select' => [
				'*',
				'MODULE_ID' => 'ORDER_DISCOUNT.MODULE_ID',
				'DISCOUNT_ID' => 'ORDER_DISCOUNT.DISCOUNT_ID',
				'DISCOUNT_NAME' => 'ORDER_DISCOUNT.NAME',
			],
			'filter' => ['=ORDER_ID' => self::$orderId],
			'order' => ['ID' => 'ASC'],
		]);
		while ($coupon = $couponIterator->fetch())
		{
			$couponData = $coupon['DATA'];
			$couponData['COUPON'] = $coupon['COUPON'];
			$couponData['STATUS'] = self::STATUS_ENTERED;
			$couponData['CHECK_CODE'] = self::COUPON_CHECK_OK;
			$couponData['MODULE'] = $coupon['MODULE_ID'];
			$couponData['MODULE_ID'] = $coupon['MODULE_ID'];
			$couponData['ID'] = $coupon['COUPON_ID'];
			$couponData['DISCOUNT_ID'] = $coupon['DISCOUNT_ID'];
			$couponData['DISCOUNT_NAME'] = (string)$coupon['DISCOUNT_NAME'];
			$couponData['DISCOUNT_ACTIVE'] = 'Y';
			$couponData['TYPE'] = $coupon['TYPE'];
			$couponData['ACTIVE'] = 'Y';
			$couponData['SAVED'] = 'Y';
			foreach (self::$timeFields as $fieldName)
			{
				if (isset($couponData[$fieldName]))
				{
					$couponData[$fieldName] = Main\Type\DateTime::createFromTimestamp($couponData[$fieldName]);
				}
			}
			unset($fieldName);
			if (empty($couponData['USER_INFO']) && $couponData['MODE'] === self::COUPON_MODE_FULL)
			{
				$couponData['USER_INFO'] = [
					'USER_ID' => 0,
					'MAX_USE' => 0,
					'USE_COUNT' => 0,
					'ACTIVE_FROM' => null,
					'ACTIVE_TO' => null,
				];
			}
			if (!empty($couponData['USER_INFO']))
			{
				foreach (self::$timeFields as $fieldName)
				{
					if (isset($couponData['USER_INFO'][$fieldName]))
					{
						$couponData['USER_INFO'][$fieldName] = Main\Type\DateTime::createFromTimestamp($couponData['USER_INFO'][$fieldName]);
					}
				}
				unset($fieldName);
				foreach ($couponData['USER_INFO'] as $fieldName => $fieldValue)
				{
					$couponData[$fieldName] = $fieldValue;
				}
			}
			$couponsList[$couponData['COUPON']] = $couponData;
		}
		unset($coupon, $couponIterator);

		if (!empty($couponsList))
		{
			self::setCoupons($couponsList, false);
		}

		self::$checkActivity = true;
	}

	/**
	 * Get coupons list.
	 *
	 * @param bool $extMode			Get full information or coupons only.
	 * @param array $filter			Coupons filter.
	 * @param bool $show			Get for show or apply.
	 * @param bool $final			Change status ENTERED to NOT_APPLIED.
	 * @return array|bool
	 */
	public static function get($extMode = true, $filter = [], $show = false, $final = false)
	{
		if (self::$useMode === self::MODE_SYSTEM)
		{
			return false;
		}
		$extMode = ($extMode === true);
		if (!is_array($filter))
		{
			$filter = [];
		}
		static::convertOldFilterFields($filter);
		$show = ($show === true);
		if (!self::$init)
		{
			self::init();
		}
		if (!self::isSuccess())
		{
			return false;
		}

		if (self::isFrozenCouponStorage() || !self::isEntered())
		{
			return [];
		}

		$final = ($final === true);
		if ($final)
		{
			self::finalApply();
		}
		$validCoupons = (
			$show
				? self::$coupons
				: array_filter(self::$coupons, '\Bitrix\Sale\DiscountCouponsManager::filterFreezeCoupons')
		);
		if (empty($validCoupons))
		{
			return [];
		}
		if (!empty($filter))
		{
			self::filterArrayCoupons($validCoupons, $filter);
		}
		if (!empty($validCoupons))
		{
			self::clearSystemData($validCoupons);
		}
		if ($show && !empty($validCoupons))
		{
			self::fillCouponHints($validCoupons);
		}

		return ($extMode ? $validCoupons : array_keys($validCoupons));
	}

	/**
	 * Get coupons list for apply.
	 *
	 * @param array $filter					Coupons filter.
	 * @param array $product				Product description.
	 * @param bool $uniqueDiscount			Get one coupon for discount.
	 * @return array|bool
	 */
	public static function getForApply($filter, $product = [], $uniqueDiscount = false)
	{
		if (self::$useMode === self::MODE_SYSTEM)
		{
			return [];
		}
		if (self::$useMode === self::MODE_ORDER && static::isUsedOrderCouponsForApply())
		{
			$filter['SAVED'] = ['Y', 'N'];
		}
		else
		{
			$filter['SAVED'] = 'N';
		}

		$couponsList = self::get(true, $filter, false);
		if ($couponsList === false)
		{
			return [];
		}
		if (!empty($couponsList))
		{
			$uniqueDiscount = ($uniqueDiscount === true);
			if ($uniqueDiscount)
			{
				self::filterUniqueDiscount($couponsList);
			}
			if (!empty($product))
			{
				$hash = self::getProductHash($product);
				if ($hash !== '')
				{
					$productCoupons = [];
					foreach ($couponsList as $id => $data)
					{
						if (self::filterOneRowCoupons($data, $hash))
						{
							$productCoupons[$id] = $data;
						}
					}
					$couponsList = $productCoupons;
					unset($productCoupons);
				}
				else
				{
					$couponsList = [];
				}
			}
		}

		static::filterLockedCoupons($couponsList);

		return $couponsList;
	}

	/**
	 * Returns coupons for current order.
	 *
	 * @param bool $extMode					Get full information or coupons only.
	 * @param array $filter					Coupons filter.
	 * @return array
	 */
	public static function getOrderedCoupons($extMode = true, $filter = [])
	{
		$extMode = ($extMode === true);
		$result = [];
		if (self::$useMode !== self::MODE_ORDER)
		{
			return $result;
		}
		if (!self::isSuccess())
		{
			return $result;
		}

		if (!self::isEntered())
		{
			return $result;
		}

		$result = array_filter(self::$coupons, '\Bitrix\Sale\DiscountCouponsManager::filterFreezeCoupons');
		if (empty($result))
		{
			return $result;
		}
		$result = array_filter($result, '\Bitrix\Sale\DiscountCouponsManager::filterFreezeOrderedCoupons');
		if (empty($result))
		{
			return $result;
		}

		$filter['SAVED'] = 'Y';
		static::filterArrayCoupons($result, $filter);
		if (!empty($result))
		{
			static::clearSystemData($result);
		}

		return ($extMode ? $result : array_keys($result));
	}

	/**
	 * Verifies the current status of new applied coupons. Used before order save.
	 *
	 * @return Result
	 */
	public static function verifyApplied()
	{
		$result = new Sale\Result();

		if (
			self::$useMode === self::MODE_SYSTEM
			|| !self::isEntered()
		)
		{
			return $result;
		}

		$appliedCoupons = self::filterCoupons(['STATUS' => self::STATUS_APPLYED, 'SAVED' => 'N'], true);
		if (!empty($appliedCoupons))
		{
			$badCoupons = [];
			$appliedCoupons = array_keys($appliedCoupons);
			foreach ($appliedCoupons as $coupon)
			{
				$row = self::getData($coupon, true);
				if ($row['STATUS'] == self::STATUS_NOT_FOUND || $row['STATUS'] == self::STATUS_FREEZE)
				{
					$badCoupons[$coupon] = $row;
				}
			}
			unset($row, $coupon);
			if (!empty($badCoupons))
			{
				self::fillCouponHints($badCoupons);
				$errorData = [];
				foreach ($badCoupons as $row)
				{
					$errorData[$row['COUPON']] = implode(', ', $row['CHECK_CODE_TEXT']);
				}
				unset($row);
				$result->addError(new Main\Error(
					Loc::getMessage('BX_SALE_DCM_COUPONS_VERIFY_ERR'),
					'COUPON',
					$errorData
				));
				unset($errorData);
			}
			unset($badCoupons);
		}
		unset($appliedCoupons);

		return $result;
	}

	/**
	 * Save applied coupons.
	 *
	 * @return Main\Result
	 */
	public static function saveApplied(): Main\Result
	{
		$commonResult = new Main\Result();
		if (
			self::$useMode === self::MODE_SYSTEM
			|| !self::isEntered()
			|| !self::$allowedSave
		)
		{
			return $commonResult;
		}

		$result = [];
		$currentTime = new Main\Type\DateTime();
		$userId = self::getUserId();

		$appliedCoupons = self::filterCoupons(
			[
				'STATUS' => self::STATUS_APPLYED,
				'MODULE_ID' => 'sale',
				'SAVED' => 'N',
			],
			true
		);

		if (!empty($appliedCoupons))
		{
			$result['sale'] = [
				'COUPONS' => $appliedCoupons,
			];
			$saveResult = Internals\DiscountCouponTable::saveApplied($appliedCoupons, $userId, $currentTime);

			if ($saveResult === false)
			{
				$result['sale']['ERROR'] = true;
			}
			else
			{
				if ($saveResult['STATUS'])
				{
					$result['sale']['DEACTIVATE'] = $saveResult['DEACTIVATE'];
					$result['sale']['LIMITED'] = $saveResult['LIMITED'];
					$result['sale']['INCREMENT'] = $saveResult['INCREMENT'];
					self::eraseAppliedCoupons($result['sale']);
				}
				else
				{
					$commonResult->addError(new Main\Error(
						Loc::getMessage('BX_SALE_DCM_ERR_SAVE_APPLIED'),
						'sale',
						$saveResult['ERROR']
					));

					return $commonResult;
				}
			}
		}
		if (!self::$onlySaleDiscount && !empty(self::$couponProviders))
		{
			foreach (self::$couponProviders as $provider)
			{
				$appliedCoupons = self::filterCoupons(
					[
						'STATUS' => self::STATUS_APPLYED,
						'MODULE_ID' => $provider['module'],
						'SAVED' => 'N',
					],
					true
				);
				if (empty($appliedCoupons))
				{
					continue;
				}
				$result[$provider['module']] = [
					'COUPONS' => $appliedCoupons,
				];
				$saveResult = call_user_func_array(
					$provider['saveApplied'],
					[
						$appliedCoupons,
						$userId,
						$currentTime,
					]
				);
				if (empty($saveResult) || !is_array($saveResult))
				{
					$result[$provider['module']]['ERROR'] = true;
				}
				else
				{
					if (!isset($saveResult['STATUS']) || $saveResult['STATUS'])
					{
						$result[$provider['module']]['DEACTIVATE'] = ($saveResult['DEACTIVATE'] ?? []);
						$result[$provider['module']]['LIMITED'] = ($saveResult['LIMITED'] ?? []);
						$result[$provider['module']]['INCREMENT'] = ($saveResult['INCREMENT'] ?? []);
						self::eraseAppliedCoupons($result[$provider['module']]);
					}
					else
					{
						$commonResult->addError(new Main\Error(
							Loc::getMessage('BX_SALE_DCM_ERR_SAVE_APPLIED'),
							$provider['module'],
							$saveResult['ERROR']
						));

						return $commonResult;
					}
				}
			}
		}
		self::saveToStorage();
		self::$allowedSave = false;
		$event = new Main\Event('sale', self::EVENT_ON_SAVE_APPLIED_COUPONS, $result);
		$event->send();

		return $commonResult;
	}

	/**
	 * Set applied information for product.
	 *
	 * @param array $product		Product description.
	 * @param array $couponsList	Coupons for product.
	 * @param bool $oldMode			Compatibility mode with old custom providers.
	 * @return bool
	 */
	public static function setApplyByProduct($product, $couponsList, $oldMode = false)
	{
		static $count = null;
		if ($count === null)
		{
			$count = 0;
		}
		if (self::$useMode === self::MODE_SYSTEM)
		{
			return false;
		}
		if (empty($couponsList) || empty($product))
		{
			return false;
		}
		$oldMode = ($oldMode === true);
		if ($oldMode)
		{
			if (!isset($product['BASKET_ID']))
			{
				$product['BASKET_ID'] = 'c'.$count;
			}
			$count++;
		}
		$hash = ($oldMode ? self::getCatalogProductHash($product) : self::getProductHash($product));

		if ($hash === '')
		{
			return false;
		}
		$applyed = false;
		$applyList = [];
		foreach ($couponsList as $coupon)
		{
			$coupon = trim((string)$coupon);
			if ($coupon === '' || !isset(self::$coupons[$coupon]))
			{
				continue;
			}
			if (
				self::$coupons[$coupon]['STATUS'] == self::STATUS_NOT_FOUND
				|| self::$coupons[$coupon]['STATUS'] == self::STATUS_FREEZE
			)
			{
				continue;
			}
			if (
				self::$coupons[$coupon]['TYPE'] == Internals\DiscountCouponTable::TYPE_BASKET_ROW
				&& !empty(self::$coupons[$coupon]['BASKET'])
			)
			{
				continue;
			}
			self::$coupons[$coupon]['BASKET'][$hash] = true;
			self::$coupons[$coupon]['STATUS'] = self::STATUS_APPLYED;
			$applyed = true;
			$applyList[$coupon] = self::$coupons[$coupon];
		}
		unset($coupon);
		if ($applyed)
		{
			$event = new Main\Event(
				'sale',
				self::EVENT_ON_COUPON_APPLY_PRODUCT,
				[
					'PRODUCT' => $product,
					'COUPONS' => $applyList,
				]
			);
			$event->send();
		}
		unset($applyList);

		return $applyed;
	}

	/**
	 * Set applied information for basket.
	 *
	 * @param string $coupon		Coupon.
	 * @param array $data				Apply data (basket, delivery).
	 * @return bool
	 */
	public static function setApply($coupon, $data)
	{
		if (self::$useMode === self::MODE_SYSTEM)
		{
			return false;
		}
		$coupon = trim((string)$coupon);
		if ($coupon === '' || empty($data) || !is_array($data))
		{
			return false;
		}
		if (!isset(self::$coupons[$coupon]))
		{
			return false;
		}
		if (
			self::$coupons[$coupon]['STATUS'] == self::STATUS_NOT_FOUND
			|| self::$coupons[$coupon]['STATUS'] == self::STATUS_FREEZE
		)
		{
			return false;
		}
		$result = [];
		if ((!empty($data['BASKET']) && is_array($data['BASKET'])) || !empty($data['DELIVERY']))
		{
			if (!empty($data['BASKET']) && is_array($data['BASKET']))
			{
				if (self::$coupons[$coupon]['TYPE'] == Internals\DiscountCouponTable::TYPE_BASKET_ROW && count($data['BASKET']) > 1)
				{
					return false;
				}
				foreach ($data['BASKET'] as $product)
				{
					if (empty($product))
					{
						continue;
					}
					$hash = self::getProductHash($product);
					if ($hash === '')
					{
						continue;
					}
					if (
						self::$coupons[$coupon]['TYPE'] == Internals\DiscountCouponTable::TYPE_BASKET_ROW
						&& !empty(self::$coupons[$coupon]['BASKET'])
					)
					{
						continue;
					}
					self::$coupons[$coupon]['BASKET'][$hash] = true;
					self::$coupons[$coupon]['STATUS'] = self::STATUS_APPLYED;
					$result['COUPON'] = self::$coupons[$coupon];
					if (!isset($result['BASKET']))
					{
						$result['BASKET'] = [];
					}
					$result['BASKET'][] = $product;
				}
				unset($product);
			}
			if (!empty($data['DELIVERY']))
			{
				self::$coupons[$coupon]['DELIVERY'] = $data['DELIVERY'];
				self::$coupons[$coupon]['STATUS'] = self::STATUS_APPLYED;
				$result['COUPON'] = self::$coupons[$coupon];
				$result['DELIVERY'] = self::$coupons[$coupon]['DELIVERY'];
			}
			$event = new Main\Event('sale', self::EVENT_ON_COUPON_APPLY, $result);
			unset($result);
			$event->send();

			return true;
		}

		return false;
	}

	/**
	 * Clear applied information for product.
	 *
	 * @param array $product		Product description.
	 * @return bool
	 */
	public static function deleteApplyByProduct($product)
	{
		if (self::$useMode === self::MODE_SYSTEM || empty($product))
		{
			return false;
		}
		$hash = self::getProductHash($product);
		if ($hash === '')
		{
			return false;
		}
		$success = false;
		foreach (self::$coupons as &$oneCoupon)
		{
			if ($oneCoupon['STATUS'] == self::STATUS_NOT_FOUND || $oneCoupon['STATUS'] == self::STATUS_FREEZE)
			{
				continue;
			}
			if ($oneCoupon['SAVED'] === 'Y')
			{
				continue;
			}
			if (isset($oneCoupon['BASKET'][$hash]))
			{
				unset($oneCoupon['BASKET'][$hash]);
				if (empty($oneCoupon['BASKET']) && empty($oneCoupon['DELIVERY']))
				{
					$oneCoupon['STATUS'] = self::STATUS_NOT_APPLYED;
				}
				$success = true;
			}
		}
		unset($oneCoupon);

		return $success;
	}

	/**
	 * Change status coupons for save.
	 *
	 * @return void
	 */
	public static function finalApply()
	{
		if (self::$useMode === self::MODE_SYSTEM || !self::isSuccess() || empty(self::$coupons))
		{
			return;
		}

		foreach (self::$coupons as &$oneCoupon)
		{
			if ($oneCoupon['STATUS'] == self::STATUS_ENTERED)
			{
				$oneCoupon['STATUS'] = self::STATUS_NOT_APPLYED;
				if ($oneCoupon['CHECK_CODE'] == self::COUPON_CHECK_OK)
				{
					$oneCoupon['CHECK_CODE'] = self::COUPON_CHECK_NOT_APPLIED;
				}
			}
		}
		unset($oneCoupon);
	}

	/**
	 * Clear applied data for coupon.
	 *
	 * @param string $coupon			Coupon.
	 * @return bool
	 */
	public static function clearApplyCoupon($coupon)
	{
		if (self::$useMode === self::MODE_SYSTEM || !self::isSuccess())
		{
			return false;
		}
		if (empty(self::$coupons))
		{
			return true;
		}
		$coupon = trim((string)$coupon);
		if ($coupon === '')
		{
			return false;
		}
		if (!isset(self::$coupons[$coupon]))
		{
			return false;
		}
		if (
			self::$coupons[$coupon]['STATUS'] == self::STATUS_NOT_FOUND
			|| self::$coupons[$coupon]['STATUS'] == self::STATUS_FREEZE
		)
		{
			return false;
		}
		self::$coupons[$coupon]['STATUS'] = self::STATUS_ENTERED;
		self::createApplyFields(self::$coupons[$coupon]);

		return true;
	}

	/**
	 * Clear applied data for coupons.
	 *
	 * @param bool $all					Clear for coupons or not saved.
	 * @return bool
	 */
	public static function clearApply($all = true)
	{
		if (self::$useMode === self::MODE_SYSTEM || !self::isSuccess())
		{
			return false;
		}
		if (self::isFrozenCouponStorage())
		{
			return false;
		}
		if (empty(self::$coupons))
		{
			return true;
		}
		$all = ($all !== false);
		foreach (self::$coupons as &$coupon)
		{
			if (
				$coupon['STATUS'] == self::STATUS_NOT_FOUND
				|| $coupon['STATUS'] == self::STATUS_FREEZE
			)
			{
				continue;
			}
			if (!$all && $coupon['SAVED'] === 'Y')
			{
				continue;
			}
			$coupon['STATUS'] = self::STATUS_ENTERED;
			self::createApplyFields($coupon);
		}
		unset($coupon);

		return true;
	}

	/**
	 * Returns information about coupon.
	 *
	 * @param string $coupon			Coupon for search.
	 * @param bool $checkCoupon			Check coupon data.
	 * @return array|false
	 */
	public static function getData($coupon, $checkCoupon = true)
	{
		$currentTime = new Main\Type\DateTime();
		$currentTimestamp = $currentTime->getTimestamp();
		self::initUseDiscount();
		$coupon = trim((string)$coupon);
		if ($coupon === '')
		{
			return false;
		}
		$checkCoupon = ($checkCoupon === true);

		$result = static::getEmptyCouponFields($coupon);

		$resultKeyList = [
			'ID',
			'COUPON',
			'DISCOUNT_ID',
			'TYPE',
			'ACTIVE',
			'DISCOUNT_NAME',
			'DISCOUNT_ACTIVE',
			'DISCOUNT_ACTIVE_FROM',
			'DISCOUNT_ACTIVE_TO',
		];

		$couponIterator = Internals\DiscountCouponTable::getList([
			'select' => [
				'ID',
				'COUPON',
				'DISCOUNT_ID',
				'TYPE',
				'ACTIVE',
				'USER_ID',
				'MAX_USE',
				'USE_COUNT',
				'ACTIVE_FROM',
				'ACTIVE_TO',
				'DISCOUNT_NAME' => 'DISCOUNT.NAME',
				'DISCOUNT_ACTIVE' => 'DISCOUNT.ACTIVE',
				'DISCOUNT_ACTIVE_FROM' => 'DISCOUNT.ACTIVE_FROM',
				'DISCOUNT_ACTIVE_TO' => 'DISCOUNT.ACTIVE_TO',
			],
			'filter' => [
				'=COUPON' => $coupon,
			],
		]);
		if ($existCoupon = $couponIterator->fetch())
		{
			$result['MODE'] = self::COUPON_MODE_FULL;
			$result['MODULE'] = 'sale';
			$result['MODULE_ID'] = 'sale';
			$checkCode = self::checkBaseData($existCoupon, self::COUPON_CHECK_OK);
			foreach ($resultKeyList as $resultKey)
			{
				$result[$resultKey] = $existCoupon[$resultKey];
			}
			unset($resultKey);

			if ($checkCoupon)
			{
				$checkCode = self::checkFullData($existCoupon, $result['MODE'], $checkCode, $currentTimestamp);
				self::fillUserInfo($result, $existCoupon, $checkCode);
			}
			$result['STATUS'] = ($checkCode == self::COUPON_CHECK_OK ? self::STATUS_ENTERED : self::STATUS_FREEZE);
			$result['CHECK_CODE'] = $checkCode;
			unset($checkCode);
		}
		elseif (!self::$onlySaleDiscount && !empty(self::$couponProviders))
		{
			foreach (self::$couponProviders as $provider)
			{
				$existCoupon = call_user_func_array(
					$provider['getData'],
					[
						$coupon,
					]
				);
				if (!empty($existCoupon) && is_array($existCoupon))
				{
					$result['MODE'] = (int)$provider['mode'];
					$result['MODULE'] = $provider['module'];
					$result['MODULE_ID'] = $provider['module'];
					$checkCode = self::checkBaseData($existCoupon, self::COUPON_CHECK_OK);
					foreach ($resultKeyList as $resultKey)
					{
						$result[$resultKey] = $existCoupon[$resultKey];
					}
					unset($resultKey);

					if ($checkCoupon)
					{
						$checkCode = self::checkFullData($existCoupon, $result['MODE'], $checkCode, $currentTimestamp);
						self::fillUserInfo($result, $existCoupon, $checkCode);
					}
					$result['STATUS'] = ($checkCode == self::COUPON_CHECK_OK ? self::STATUS_ENTERED : self::STATUS_FREEZE);
					$result['CHECK_CODE'] = $checkCode;
					unset($checkCode);
					break;
				}
			}
			unset($provider);
		}

		return $result;
	}

	/**
	 * Checks if a coupon exists.
	 *
	 * @param string $coupon		Coupon for check.
	 * @return array|bool
	 */
	public static function isExist($coupon)
	{
		$coupon = trim((string)$coupon);
		if ($coupon === '')
		{
			return false;
		}

		self::initUseDiscount();
		$couponIterator = Internals\DiscountCouponTable::getList([
			'select' => [
				'ID',
				'COUPON',
			],
			'filter' => [
				'=COUPON' => $coupon,
			],
		]);
		if ($existCoupon = $couponIterator->fetch())
		{
			return [
				'ID' => $existCoupon['ID'],
				'COUPON' => $existCoupon['COUPON'],
				'MODULE' => 'sale',
				'MODULE_ID' => 'sale',
			];
		}
		else
		{
			if (!self::$onlySaleDiscount && !empty(self::$couponProviders))
			{
				foreach (self::$couponProviders as $provider)
				{
					$existCoupon = call_user_func_array(
						$provider['isExist'],
						[
							$coupon,
						]
					);
					if (!empty($existCoupon) && is_array($existCoupon))
					{
						if (!isset($existCoupon['ID']) || !isset($existCoupon['COUPON']))
						{
							continue;
						}
						return [
							'ID' => $existCoupon['ID'],
							'COUPON' => $existCoupon['COUPON'],
							'MODULE' => $provider['module'],
							'MODULE_ID' => $provider['module'],
						];
					}
				}
				unset($provider);
			}
		}

		return false;
	}

	/**
	 * Returns entered coupon data.
	 *
	 * @param string $coupon			Coupon code.
	 * @param bool $clearData			Clear data for save order coupon.
	 * @return bool|array
	 */
	public static function getEnteredCoupon($coupon, $clearData = false)
	{
		if (!self::$init)
		{
			self::init();
		}
		$result = false;
		if (self::$useMode === self::MODE_SYSTEM || !self::isSuccess())
		{
			return false;
		}

		$clearData = ($clearData === true);
		$coupon = trim((string)$coupon);
		if ($coupon === '')
		{
			return false;
		}
		if (!isset(self::$coupons[$coupon]))
		{
			$couponData = self::getData($coupon);
			if (isset(self::$coupons[$couponData['COUPON']]))
			{
				$result = self::$coupons[$couponData['COUPON']];
			}
		}
		else
		{
			$result = self::$coupons[$coupon];
		}
		if (!empty($result))
		{
			if ($result['MODE'] === self::COUPON_MODE_FULL)
			{
				$result['USER_INFO'] = $result['SYSTEM_DATA'];
				unset($result['SYSTEM_DATA']);
			}
			if ($clearData)
			{
				foreach (self::$clearFields as $fieldName)
				{
					unset($result[$fieldName]);
				}
				unset($fieldName);
				foreach (self::$timeFields as $fieldName)
				{
					if (isset($result[$fieldName]) && $result[$fieldName] instanceof Main\Type\DateTime)
					{
						$result[$fieldName] = $result[$fieldName]->getTimestamp();
					}
				}
				unset($fieldName);

				if (!empty($result['USER_INFO']))
				{
					foreach (self::$timeFields as $fieldName)
					{
						if (isset($result['USER_INFO'][$fieldName]) && $result['USER_INFO'][$fieldName] instanceof Main\Type\DateTime)
						{
							$result['USER_INFO'][$fieldName] = $result['USER_INFO'][$fieldName]->getTimestamp();
						}
					}
					unset($fieldName);
				}
			}
		}

		return $result;
	}

	/**
	 * Clear coupons storage with logout from public.
	 *
	 * @return void
	 */
	public static function logout()
	{
		if (!self::$init)
		{
			self::init();
		}
		if (self::$useMode !== self::MODE_CLIENT)
		{
			return;
		}
		if (self::isSuccess())
		{
			self::clear(true);
		}
	}

	/**
	 * Returns true if the coupon was used in the order and saved.
	 *
	 * @param array $coupon			Coupon data.
	 * @return bool
	 */
	public static function filterOrderCoupons($coupon)
	{
		return (isset($coupon['SAVED']) && $coupon['SAVED'] === 'Y');
	}

	/**
	 * Reload discount coupons providers.
	 * @internal
	 *
	 * @param bool $mode true, if you need use only sale discounts.
	 * @return void
	 */
	public static function setUseOnlySaleDiscounts($mode)
	{
		if (!is_bool($mode))
		{
			return;
		}
		if (self::getUseMode() != self::MODE_ORDER)
		{
			return;
		}
		self::$onlySaleDiscount = $mode;
		self::loadCouponProviders();
	}

	/**
	 * Checks the basic coupon fields.
	 *
	 * @param array &$data			Coupon data.
	 * @param int $checkCode		Start status.
	 * @return int
	 */
	protected static function checkBaseData(&$data, $checkCode = self::COUPON_CHECK_OK)
	{
		if (empty(self::$couponTypes))
		{
			self::$couponTypes = Internals\DiscountCouponTable::getCouponTypes(true);
		}

		if (!isset($data['ID']))
		{
			$data['ID'] = 0;
			$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
		}
		else
		{
			$data['ID'] = (int)$data['ID'];
			if ($data['ID'] <= 0 && self::$checkActivity)
			{
				$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
			}
		}
		if (!isset($data['DISCOUNT_ID']))
		{
			$data['DISCOUNT_ID'] = 0;
			$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
		}
		else
		{
			$data['DISCOUNT_ID'] = (int)$data['DISCOUNT_ID'];
			if ($data['DISCOUNT_ID'] <= 0 && self::$checkActivity)
			{
				$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
			}
		}
		if (!isset($data['TYPE']))
		{
			$data['TYPE'] = Internals\DiscountCouponTable::TYPE_UNKNOWN;
			$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
		}
		else
		{
			$data['TYPE'] = (int)$data['TYPE'];
			if (!isset(self::$couponTypes[$data['TYPE']]) && $data['TYPE'] != Internals\DiscountCouponTable::TYPE_ARCHIVED)
			{
				$data['TYPE'] = Internals\DiscountCouponTable::TYPE_UNKNOWN;
				$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
			}
		}
		if (!isset($data['ACTIVE']))
		{
			$data['ACTIVE'] = '';
			$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
		}
		else
		{
			$data['ACTIVE'] = (string)$data['ACTIVE'];
			if ($data['ACTIVE'] !== 'Y' && $data['ACTIVE'] !== 'N')
			{
				$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
			}
		}
		if (isset($data['ACTIVE_FROM']) && !($data['ACTIVE_FROM'] instanceof Main\Type\DateTime))
		{
			$data['ACTIVE_FROM'] = null;
			$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
		}
		if (isset($data['ACTIVE_TO']) && !($data['ACTIVE_TO'] instanceof Main\Type\DateTime))
		{
			$data['ACTIVE_TO'] = null;
			$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
		}
		$data['DISCOUNT_NAME'] = (isset($data['DISCOUNT_NAME']) ? (string)$data['DISCOUNT_NAME'] : '');
		if (!isset($data['DISCOUNT_ACTIVE']))
		{
			$data['DISCOUNT_ACTIVE'] = '';
			$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
		}
		else
		{
			$data['DISCOUNT_ACTIVE'] = (string)$data['DISCOUNT_ACTIVE'];
			if ($data['DISCOUNT_ACTIVE'] !== 'Y' && $data['DISCOUNT_ACTIVE'] !== 'N')
			{
				$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
			}
		}
		if (isset($data['DISCOUNT_ACTIVE_FROM']) && !($data['DISCOUNT_ACTIVE_FROM'] instanceof Main\Type\DateTime))
		{
			$data['DISCOUNT_ACTIVE_FROM'] = null;
			$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
		}
		if (isset($data['DISCOUNT_ACTIVE_TO']) && !($data['DISCOUNT_ACTIVE_TO'] instanceof Main\Type\DateTime))
		{
			$data['DISCOUNT_ACTIVE_TO'] = null;
			$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
		}

		return $checkCode;
	}

	/**
	 * Checks the extended coupon fields.
	 *
	 * @param array &$data			Coupon data.
	 * @param int $mode				Coupon mode (full or simple).
	 * @param int $checkCode		Start check status.
	 * @param int $currentTimestamp		Current time.
	 * @return int
	 */
	protected static function checkFullData(&$data, $mode, $checkCode, $currentTimestamp)
	{
		$mode = ((int)$mode !== self::COUPON_MODE_SIMPLE ? self::COUPON_MODE_FULL : self::COUPON_MODE_SIMPLE);

		if (self::$checkActivity)
		{
			if ($data['ACTIVE'] !== 'Y')
			{
				$checkCode |= self::COUPON_CHECK_NO_ACTIVE;
			}
			if ($data['DISCOUNT_ACTIVE'] !== 'Y')
			{
				$checkCode |= self::COUPON_CHECK_NO_ACTIVE_DISCOUNT;
			}
			if ($data['DISCOUNT_ACTIVE_FROM'] instanceof Main\Type\DateTime && $data['DISCOUNT_ACTIVE_FROM']->getTimestamp() > $currentTimestamp)
			{
				$checkCode |= self::COUPON_CHECK_RANGE_ACTIVE_FROM_DISCOUNT;
			}
			if ($data['DISCOUNT_ACTIVE_TO'] instanceof Main\Type\DateTime && $data['DISCOUNT_ACTIVE_TO']->getTimestamp() < $currentTimestamp)
			{
				$checkCode |= self::COUPON_CHECK_RANGE_ACTIVE_TO_DISCOUNT;
			}
		}

		if ($mode === self::COUPON_MODE_FULL)
		{
			if (self::$checkActivity)
			{
				if ($data['ACTIVE_FROM'] instanceof Main\Type\DateTime && $data['ACTIVE_FROM']->getTimestamp() > $currentTimestamp)
				{
					$checkCode |= self::COUPON_CHECK_RANGE_ACTIVE_FROM;
				}
				if ($data['ACTIVE_TO'] instanceof Main\Type\DateTime && $data['ACTIVE_TO']->getTimestamp() < $currentTimestamp)
				{
					$checkCode |= self::COUPON_CHECK_RANGE_ACTIVE_TO;
				}
			}
			if (!isset($data['USER_ID']))
			{
				$data['USER_ID'] = 0;
				$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
			}
			else
			{
				$data['USER_ID'] = (int)$data['USER_ID'];
				if ($data['USER_ID'] > 0 && $data['USER_ID'] != self::$userId)
				{
					$checkCode |= self::COUPON_CHECK_BAD_USER_ID;
				}
			}
			if (!isset($data['MAX_USE']))
			{
				$data['MAX_USE'] = 0;
				$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
			}
			else
			{
				$data['MAX_USE'] = (int)$data['MAX_USE'];
			}
			if (!isset($data['USE_COUNT']))
			{
				$data['USE_COUNT'] = 0;
				$checkCode |= self::COUPON_CHECK_CORRUPT_DATA;
			}
			else
			{
				if (self::$checkActivity)
				{
					$data['USE_COUNT'] = (int)$data['USE_COUNT'];
					if ($data['MAX_USE'] > 0 && $data['USE_COUNT'] >= $data['MAX_USE'])
					{
						$checkCode |= self::COUPON_CHECK_ALREADY_MAX_USED;
					}
				}
			}
		}

		return $checkCode;
	}

	/**
	 * Fill client information.
	 *
	 * @param array &$result			Coupon data.
	 * @param array $existCoupon	User data.
	 * @param int $checkCode		Checked result.
	 * @return void
	 */
	protected static function fillUserInfo(&$result, $existCoupon, $checkCode)
	{
		if ($checkCode == self::COUPON_CHECK_OK && $result['MODE'] === self::COUPON_MODE_FULL)
		{
			$result['SYSTEM_DATA'] = [
				'USER_ID' => $existCoupon['USER_ID'],
				'MAX_USE' => $existCoupon['MAX_USE'],
				'USE_COUNT' => $existCoupon['USE_COUNT'],
				'ACTIVE_FROM' => $existCoupon['ACTIVE_FROM'],
				'ACTIVE_TO' => $existCoupon['ACTIVE_TO'],
			];
			if (self::usedByManager() || ($existCoupon['USER_ID'] > 0 && $existCoupon['USER_ID'] == self::$userId))
			{
				$result['USER_INFO'] = $result['SYSTEM_DATA'];
			}
		}
	}

	/**
	 * Get user by fuser id.
	 *
	 * @return void
	 */
	protected static function initUserId(): void
	{
		if (
			self::isSuccess()
			&& self::$useMode === self::MODE_CLIENT
			&& self::$userId === null
		)
		{
			$currentUserId = self::getCurrentUserId();
			if ($currentUserId === null)
			{
				$fuserId = Fuser::getId(true);
				if ($fuserId !== null)
				{
					// TODO: replace this code after refactoring Fuser::getUserIdById
					$row = Internals\FuserTable::getRow([
						'select' => [
							'ID',
							'USER_ID',
						],
						'filter' => [
							'=ID' => $fuserId,
						],
						'order' => [
							'ID' => 'DESC',
						],
					]);
					if ($row !== null)
					{
						$currentUserId = (int)$row['USER_ID'];
					}
					// end TODO
				}
				unset($fuserId);
			}
			if ($currentUserId === null)
			{
				self::$errors[] = Loc::getMessage('BX_SALE_DCM_ERR_BAD_FUSER_ID');
			}
			else
			{
				self::$userId = $currentUserId;
			}
			unset($currentUserId);
		}
	}

	/**
	 * Save current coupons to session storage.
	 *
	 * @return void
	 */
	protected static function saveToStorage()
	{
		if (self::isSuccess())
		{
			$session = self::getSession();
			if (!$session)
			{
				return;
			}

			$couponsList = [];
			if (!empty(self::$coupons))
			{
				$couponsList = array_filter(self::$coupons, '\Bitrix\Sale\DiscountCouponsManager::clearSavedCoupons');
				if (!empty($couponsList))
				{
					$couponsList = array_keys($couponsList);
				}
			}

			if (self::usedByManager())
			{
				if (!isset($session[self::STORAGE_MANAGER_COUPONS]) || !is_array($session[self::STORAGE_MANAGER_COUPONS]))
				{
					$session[self::STORAGE_MANAGER_COUPONS] = [];
				}
				$session[self::STORAGE_MANAGER_COUPONS][self::$userId] = $couponsList;
			}
			else
			{
				$session[self::STORAGE_CLIENT_COUPONS] = $couponsList;
			}
			unset($couponsList);
		}
	}

	/**
	 * Clear applied coupons.
	 *
	 * @param array $result		Applied coupons.
	 * @return void
	 */
	protected static function eraseAppliedCoupons($result)
	{
		if (!empty($result['DEACTIVATE']) || !empty($result['LIMITED']))
		{
			$clear = array_keys(array_merge($result['DEACTIVATE'], $result['LIMITED']));
			foreach ($clear as $coupon)
			{
				if (isset(self::$coupons[$coupon]))
				{
					unset(self::$coupons[$coupon]);
				}
				if (isset(self::$lockedCoupons[$coupon]))
				{
					unset(self::$lockedCoupons[$coupon]);
				}
			}
			unset($coupon, $clear);
		}
	}

	/**
	 * Create applied fields.
	 *
	 * @param array &$couponData	Coupon data.
	 * @return void
	 */
	protected static function createApplyFields(&$couponData)
	{
		$couponData['BASKET'] = [];
		$couponData['DELIVERY'] = [];
	}

	/**
	 * Load coupon providers from modules.
	 *
	 * @return void
	 */
	protected static function loadCouponProviders()
	{
		self::$couponProviders = [];
		if (!self::$onlySaleDiscount)
		{
			$eventData = [
				'COUPON_UNKNOWN' => Internals\DiscountCouponTable::TYPE_UNKNOWN,
				'COUPON_TYPES' => Internals\DiscountCouponTable::getCouponTypes(false),
			];
			$event = new Main\Event('sale', self::EVENT_ON_BUILD_COUPON_PROVIDES, $eventData);
			$event->send();
			$resultList = $event->getResults();
			if (empty($resultList) || !is_array($resultList))
				return;
			/** @var Main\EventResult $eventResult */
			foreach ($resultList as $eventResult)
			{
				if ($eventResult->getType() != Main\EventResult::SUCCESS)
				{
					continue;
				}
				$module = (string)$eventResult->getModuleId();
				$provider = $eventResult->getParameters();
				if (empty($provider) || !is_array($provider))
				{
					continue;
				}
				if (empty($provider['getData']) || empty($provider['isExist']) || empty($provider['saveApplied']))
				{
					continue;
				}
				self::$couponProviders[] = [
					'module' => $module,
					'getData' => $provider['getData'],
					'isExist' => $provider['isExist'],
					'saveApplied' => $provider['saveApplied'],
					'mode' => (
						(int)($provider['mode'] ?? self::COUPON_MODE_SIMPLE) === self::COUPON_MODE_FULL
							? self::COUPON_MODE_FULL
							: self::COUPON_MODE_SIMPLE
					),
				];
			}
			unset($provider, $module, $eventResult, $resultList, $event, $eventData);
		}
	}

	/**
	 * Initialization coupons providers.
	 *
	 * @return void
	 */
	protected static function initUseDiscount()
	{
		if (self::$onlySaleDiscount !== null)
		{
			return;
		}

		self::$onlySaleDiscount = Option::get('sale', 'use_sale_discount_only') === 'Y';
		self::loadCouponProviders();
	}

	/**
	 * Filter for remove unknown coupons.
	 *
	 * @param array $coupon		Coupon data.
	 * @return bool
	 */
	protected static function filterUnknownCoupons($coupon)
	{
		if (empty(self::$couponTypes))
		{
			self::$couponTypes = Internals\DiscountCouponTable::getCouponTypes(true);
		}

		return (isset($coupon['TYPE']) && isset(self::$couponTypes[$coupon['TYPE']]));
	}

	/**
	 * Filter for remove freeze coupons.
	 *
	 * @param array $coupon		Coupon data.
	 * @return bool
	 */
	protected static function filterFreezeCoupons($coupon)
	{
		if (empty(self::$couponTypes))
		{
			self::$couponTypes = Internals\DiscountCouponTable::getCouponTypes(true);
		}

		return (
			isset($coupon['TYPE'])
			&& isset(self::$couponTypes[$coupon['TYPE']])
			&& $coupon['STATUS'] != self::STATUS_FREEZE
		);
	}

	/**
	 * Filter for remove freeze ordered coupons.
	 *
	 * @param array $coupon		Coupon data.
	 * @return bool
	 */
	protected static function filterFreezeOrderedCoupons($coupon)
	{
		static $currentTimeStamp = null;
		if ($currentTimeStamp === null)
		{
			$currentTimeStamp = time();
		}
		if (!isset($coupon['SAVED']) || $coupon['SAVED'] !== 'Y')
		{
			return true;
		}
		if (isset($coupon['MODE']) && $coupon['MODE'] === self::COUPON_MODE_FULL)
		{
			if (
				isset($coupon['ACTIVE_FROM']) && $coupon['ACTIVE_FROM'] instanceof Main\Type\DateTime
				&& $coupon['ACTIVE_FROM']->getTimestamp() > $currentTimeStamp
			)
			{
				return false;
			}
			if (
				isset($coupon['ACTIVE_TO']) && $coupon['ACTIVE_TO'] instanceof Main\Type\DateTime
				&& $coupon['ACTIVE_TO']->getTimestamp() < $currentTimeStamp
			)
			{
				return false;
			}
		}

		return true;
	}

	/**
	 * Clear one row coupons.
	 *
	 * @param array $coupon		Coupon data.
	 * @param string $hash		Product hash.
	 * @return bool
	 */
	protected static function filterOneRowCoupons($coupon, $hash)
	{
		return (
			$coupon['TYPE'] != Internals\DiscountCouponTable::TYPE_BASKET_ROW
			|| empty($coupon['BASKET'])
			|| (count($coupon['BASKET']) === 1 && isset($coupon['BASKET'][$hash]))
		);
	}

	/**
	 * Returns one coupon for one discount.
	 *
	 * @param array &$coupons		Coupons list.
	 * @return void
	 */
	protected static function filterUniqueDiscount(&$coupons)
	{
		$existDiscount = [];
		$hash = '';
		foreach ($coupons as $key => $oneCoupon)
		{
			$hash = $oneCoupon['MODULE_ID'].':'.$oneCoupon['DISCOUNT_ID'];
			if (
				isset($existDiscount[$hash])
				&& (
					$oneCoupon['TYPE'] == Internals\DiscountCouponTable::TYPE_ONE_ORDER
					|| $oneCoupon['TYPE'] == Internals\DiscountCouponTable::TYPE_MULTI_ORDER
				)
			)
			{
				unset($coupons[$key]);
			}
			else
			{
				$existDiscount[$hash] = true;
			}
		}
		unset($hash, $existDiscount);
	}

	protected static function filterLockedCoupons(array &$couponList): void
	{
		if (empty($couponList) || !static::usedByClient())
		{
			return;
		}

		$locker = Sale\Discount\CouponLocker::getInstance();
		foreach (array_keys($couponList) as $coupon)
		{
			if (!static::needLockCoupon($coupon))
			{
				continue;
			}
			if (isset(self::$lockedCoupons[$coupon]))
			{
				continue;
			}
			$locker->lock($coupon);
			if ($locker->isLocked($coupon))
			{
				self::$lockedCoupons[$coupon] = true;
			}
			else
			{
				unset($couponList[$coupon]);
			}
		}
		unset($locker);
	}

	/**
	 * Filter manager coupons list.
	 *
	 * @param array $filter			Filter for coupons.
	 * @param bool $getId			Resturn Id or full data.
	 * @return array
	 */
	protected static function filterCoupons($filter, $getId = false)
	{
		$getId = ($getId === true);
		$result = [];
		if (empty(self::$coupons) || empty($filter) || !is_array($filter))
		{
			return $result;
		}

		foreach (self::$coupons as $id => $data)
		{
			$copy = true;
			foreach ($filter as $filterKey => $filterValue)
			{
				if (is_array($filterValue) && isset($filterValue['LOGIC']))
				{
					$logic = mb_strtolower($filterValue['LOGIC']);
					if ($logic !== 'and' && $logic !== 'or')
					{
						break 2;
					}
					unset($filterValue['LOGIC']);
					if (empty($filterValue))
					{
						break 2;
					}
					$subresult = [];
					foreach ($filterValue as $subfilterKey => $subfilterValue)
					{
						$invert = strncmp($subfilterKey, '!', 1) === 0;
						$fieldName = ($invert? mb_substr($subfilterKey, 1) : $subfilterKey);
						if (!isset($data[$fieldName]))
						{
							break 3;
						}
						else
						{
							$compare = (
								is_array($subfilterValue)
									? in_array($data[$fieldName], $subfilterValue)
									: $data[$fieldName] == $subfilterValue
							);
							if ($invert)
							{
								$compare = !$compare;
							}
							$subresult[] = $compare;
						}
					}
					$compare = (
						$logic === 'and'
						? !in_array(false, $subresult, true)
						: in_array(true, $subresult, true)
					);
					if (!$compare)
					{
						$copy = false;
						break;
					}
				}
				else
				{
					$invert = strncmp($filterKey, '!', 1) === 0;
					$fieldName = ($invert? mb_substr($filterKey, 1) : $filterKey);
					if (!isset($data[$fieldName]))
					{
						break 2;
					}
					else
					{
						$compare = (
							is_array($filterValue)
								? in_array($data[$fieldName], $filterValue)
								: $data[$fieldName] == $filterValue
						);
						if ($invert)
						{
							$compare = !$compare;
						}
						if (!$compare)
						{
							$copy = false;
							break;
						}
					}
				}
			}
			if ($copy)
			{
				$result[$id] = ($getId ? $data['ID'] : $data);
			}
		}

		return $result;
	}

	/**
	 * Filter coupons list.
	 *
	 * @param array &$coupons		Coupons list.
	 * @param array $filter			Coupon filter.
	 * @return void
	 */
	protected static function filterArrayCoupons(&$coupons, $filter)
	{
		if (empty($coupons) || !is_array($coupons) || empty($filter) || !is_array($filter))
		{
			return;
		}
		$result = [];
		foreach ($coupons as $id => $data)
		{
			$copy = true;
			foreach ($filter as $filterKey => $filterValue)
			{
				if (is_array($filterValue) && isset($filterValue['LOGIC']))
				{
					$logic = mb_strtolower($filterValue['LOGIC']);
					if ($logic !== 'and' && $logic !== 'or')
					{
						break 2;
					}
					unset($filterValue['LOGIC']);
					if (empty($filterValue))
					{
						break 2;
					}
					$subresult = [];
					foreach ($filterValue as $subfilterKey => $subfilterValue)
					{
						$invert = strncmp($subfilterKey, '!', 1) === 0;
						$fieldName = ($invert? mb_substr($subfilterKey, 1) : $subfilterKey);
						if (!isset($data[$fieldName]))
						{
							break 3;
						}
						else
						{
							$compare = (
								is_array($subfilterValue)
									? in_array($data[$fieldName], $subfilterValue)
									: $data[$fieldName] == $subfilterValue
							);
							if ($invert)
							{
								$compare = !$compare;
							}
							$subresult[] = $compare;
						}
					}
					$compare = (
						$logic === 'and'
							? !in_array(false, $subresult, true)
							: in_array(true, $subresult, true)
					);
					if (!$compare)
					{
						$copy = false;
						break;
					}
				}
				else
				{
					$invert = strncmp($filterKey, '!', 1) === 0;
					$fieldName = ($invert? mb_substr($filterKey, 1) : $filterKey);
					if (!isset($data[$fieldName]))
					{
						break 2;
					}
					else
					{
						$compare = (
							is_array($filterValue)
								? in_array($data[$fieldName], $filterValue)
								: $data[$fieldName] == $filterValue
						);
						if ($invert)
						{
							$compare = !$compare;
						}
						if (!$compare)
						{
							$copy = false;
							break;
						}
					}
				}
			}
			if ($copy)
			{
				$result[$id] = $data;
			}
		}
		$coupons = $result;
		unset($result);
	}

	/**
	 * Create product hash.
	 *
	 * @param array $product		Product description.
	 * @return string
	 */
	protected static function getProductHash($product)
	{
		$hash = '';
		if (!empty($product) && is_array($product))
		{
			$module = '';
			if (isset($product['MODULE_ID']))
			{
				$module = trim((string)$product['MODULE_ID']);
			}
			elseif (isset($product['MODULE']))
			{
				$module = trim((string)$product['MODULE']);
			}
			$productId = (isset($product['PRODUCT_ID']) ? (int)$product['PRODUCT_ID'] : 0);
			$basketId = (isset($product['BASKET_ID']) ? trim((string)$product['BASKET_ID']) : '0');
			if ($productId > 0 && $basketId !== '')
			{
				$hash = $module.':'.$productId.':'.$basketId;
			}
		}

		return $hash;
	}

	/**
	 * Create catalog product hash for old custom providers.
	 *
	 * @param array $product		Product description.
	 * @return string
	 */
	protected static function getCatalogProductHash($product)
	{
		$hash = '';
		$module = 'catalog';
		$productId = 0;
		$basketId = '';
		if (!empty($product) && is_array($product))
		{
			if (isset($product['MODULE_ID']))
			{
				$module = trim((string)$product['MODULE_ID']);
			}
			elseif (isset($product['MODULE']))
			{
				$module = trim((string)$product['MODULE']);
			}
			if (isset($product['PRODUCT_ID']))
			{
				$productId = (int)$product['PRODUCT_ID'];
			}
			$basketId = (isset($product['BASKET_ID']) ? trim((string)$product['BASKET_ID']) : '0');
		}
		if ($productId >= 0 && $basketId !== '')
		{
			$hash = $module.':'.$productId.':'.$basketId;
		}

		return $hash;
	}
	/**
	 * Fill coupon hints.
	 *
	 * @param array &$coupons			Coupons list.
	 * @return void
	 */
	protected static function fillCouponHints(&$coupons)
	{
		$statusList = self::getStatusList(true);
		$checkCode = self::getCheckCodeList(true);
		foreach ($coupons as &$oneCoupon)
		{
			$oneCoupon['STATUS_TEXT'] = $statusList[$oneCoupon['STATUS']];
			if ($oneCoupon['CHECK_CODE'] == self::COUPON_CHECK_OK || $oneCoupon['CHECK_CODE'] == self::COUPON_CHECK_NOT_APPLIED)
			{
				if ($oneCoupon['CHECK_CODE'] == self::COUPON_CHECK_OK)
				{
					$oneCoupon['CHECK_CODE_TEXT'] = (
						$oneCoupon['STATUS'] == self::STATUS_APPLYED
							? [$statusList[$oneCoupon['STATUS']]]
							: [$checkCode[self::COUPON_CHECK_OK]]
					);
				}
				else
				{
					$oneCoupon['CHECK_CODE_TEXT'] = [$checkCode[self::COUPON_CHECK_NOT_APPLIED]];
				}
			}
			else
			{
				$oneCoupon['CHECK_CODE_TEXT'] = [];
				foreach ($checkCode as $code => $text)
				{
					if ($code == self::COUPON_CHECK_OK)
					{
						continue;
					}
					if (($oneCoupon['CHECK_CODE'] & $code) == $code)
					{
						$oneCoupon['CHECK_CODE_TEXT'][] = $checkCode[$code];
					}
				}
			}
		}
		unset($oneCoupon);
	}

	/**
	 * Set coupons list.
	 *
	 * @param array $couponsList			Coupons list.
	 * @param bool $checkCoupons			Find coupons.
	 * @return void
	 */
	protected static function setCoupons($couponsList, $checkCoupons = true)
	{
		if (empty($couponsList) || !is_array($couponsList))
		{
			return;
		}

		$checkCoupons = ($checkCoupons !== false);
		if ($checkCoupons)
		{
			foreach ($couponsList as $coupon)
			{
				$coupon = trim((string)$coupon);
				if ($coupon === '')
				{
					continue;
				}
				$couponData = self::getData($coupon);
				if (!isset(self::$coupons[$couponData['COUPON']]))
				{
					$couponData['SORT'] = self::$couponIndex;
					self::createApplyFields($couponData);
					self::$coupons[$couponData['COUPON']] = $couponData;
					self::$couponIndex++;
				}
			}
			unset($couponData, $coupon);
		}
		else
		{
			$currentTime = new Main\Type\DateTime();
			$currentTimestamp = $currentTime->getTimestamp();
			unset($currentTime);
			foreach ($couponsList as $coupon)
			{
				if (empty($coupon) || !is_array($coupon))
				{
					continue;
				}
				$checkCode = self::checkBaseData($coupon, self::COUPON_CHECK_OK);
				$checkCode = self::checkFullData($coupon, $coupon['MODE'], $checkCode, $currentTimestamp);
				$coupon['STATUS'] = ($checkCode == self::COUPON_CHECK_OK ? self::STATUS_ENTERED : self::STATUS_FREEZE);
				$coupon['CHECK_CODE'] = $checkCode;
				unset($checkCode);
				if (!isset(self::$coupons[$coupon['COUPON']]))
				{
					$coupon['SORT'] = self::$couponIndex;
					self::createApplyFields($coupon);
					self::$coupons[$coupon['COUPON']] = $coupon;
					self::$couponIndex++;
				}
			}
			unset($coupon, $currentTimestamp);
		}
	}

	/**
	 * Clear order saved coupons.
	 *
	 * @internal
	 * @param array $coupon		Coupon data.
	 * @return bool
	 */
	protected static function clearSavedCoupons($coupon)
	{
		return (!isset($coupon['SAVED']) || $coupon['SAVED'] !== 'Y');
	}

	/**
	 * Clear system data.
	 *
	 * @internal
	 * @param array &$coupons			Coupons.
	 * @return void
	 */
	protected static function clearSystemData(&$coupons)
	{
		$result = [];
		foreach ($coupons as $couponIndex => $couponData)
		{
			if (array_key_exists('SYSTEM_DATA', $couponData))
			{
				unset($couponData['SYSTEM_DATA']);
			}
			$result[$couponIndex] = $couponData;
		}
		unset($couponIndex, $couponData);
		$coupons = $result;
	}

	/**
	 * Convert old filter fields.
	 *
	 * @internal
	 * @param array &$filter		Coupons filter.
	 * @return void
	 */
	protected static function convertOldFilterFields(array &$filter)
	{
		if (array_key_exists('MODULE', $filter))
		{
			if (!isset($filter['MODULE_ID']))
			{
				$filter['MODULE_ID'] = $filter['MODULE'];
			}
			unset($filter['MODULE']);
		}
		if (array_key_exists('!MODULE', $filter))
		{
			if (!isset($filter['!MODULE_ID']))
			{
				$filter['!MODULE_ID'] = $filter['!MODULE'];
			}
			unset($filter['!MODULE']);
		}
	}

	/**
	 * Returns empty coupon (default field values).
	 *
	 * @internal
	 * @param string $coupon		Coupon code.
	 * @return array
	 */
	protected static function getEmptyCouponFields($coupon)
	{
		/* field MODULE - unused, for compatibility only */
		return [
			'COUPON' => $coupon,
			'MODE' => self::COUPON_MODE_SIMPLE,
			'STATUS' => self::STATUS_NOT_FOUND,
			'CHECK_CODE' => self::COUPON_CHECK_NOT_FOUND,
			'MODULE' => '',
			'MODULE_ID' => '',
			'ID' => 0,
			'DISCOUNT_ID' => 0,
			'DISCOUNT_NAME' => '',
			'TYPE' => Internals\DiscountCouponTable::TYPE_UNKNOWN,
			'ACTIVE' => '',
			'USER_INFO' => [],
			'SAVED' => 'N',
		];
	}

	private static function getCurrentUserId(): ?int
	{
		global $USER;

		if (!(
			isset($USER)
			&& $USER instanceof \CUser
		))
		{
			return null;
		}

		$userId = (int)$USER->GetID();

		return $userId > 0 ? $userId : null;
	}

	protected static function needLockCoupon(string $coupon): bool
	{
		$type = self::$coupons[$coupon]['TYPE'] ?? Internals\DiscountCouponTable::TYPE_UNKNOWN;

		return (
			$type === Internals\DiscountCouponTable::TYPE_BASKET_ROW
			|| $type === Internals\DiscountCouponTable::TYPE_ONE_ORDER
		);
	}
}

Anon7 - 2022
AnonSec Team