403Webshell
Server IP : 80.87.202.40  /  Your IP : 216.73.216.169
Web Server : Apache
System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64
User : bitrix ( 600)
PHP Version : 8.2.27
Disable Function : NONE
MySQL : OFF |  cURL : ON |  WGET : ON |  Perl : ON |  Python : OFF |  Sudo : ON |  Pkexec : ON
Directory :  /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/yandex.market/lib/trading/entity/sale/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/yandex.market/lib/trading/entity/sale/order.php
<?php

namespace Yandex\Market\Trading\Entity\Sale;

use Yandex\Market;
use Yandex\Market\Trading\Entity\Reference as EntityReference;
use Yandex\Market\Trading\Entity\Operation as EntityOperation;
use Bitrix\Main;
use Bitrix\Sale;
use Bitrix\Catalog;

class Order extends Market\Trading\Entity\Reference\Order
{
	use Market\Reference\Concerns\HasLang;

	const PAYMENT_SUBSIDY_PREFIX = 'MARKET_SUBSIDY_';

	/** @var int|null */
	protected $eventProcessing;
	/** @var array */
	protected $initialChangedValues;
	/** @var bool|null*/
	protected $isStartField;
	/** @var Environment */
	protected $environment;
	/** @var Sale\OrderBase */
	protected $internalOrder;
	/** @var Sale\OrderBase */
	protected $calculatable;
	/** @var Internals\BasketDataPreserver */
	protected $basketDataPreserver;
	/** @var Internals\AccountNumberSetter */
	protected $accountNumberSetter;
	/** @var int */
	protected $tradingCampaignId;

	protected static function includeMessages()
	{
		Main\Localization\Loc::loadMessages(__FILE__);
	}

	public function __construct(Environment $environment, Sale\OrderBase $internalOrder, $eventProcessing = null)
	{
		parent::__construct($environment, $internalOrder);

		$this->eventProcessing = $eventProcessing;
		$this->initialChangedValues = $eventProcessing !== null ? $internalOrder->getFields()->getChangedValues() : [];
	}

	public function getAdminEditUrl()
	{
		return Market\Ui\Admin\Path::getPageUrl('sale_order_view', [
			'ID' => $this->getId(),
			'lang' => LANGUAGE_ID,
			'sale_order_view_active_tab' => 'YANDEX_MARKET_TRADING_ORDER_VIEW',
		]);
	}

	public function hasAccess($userId, $operation)
	{
		switch ($operation)
		{
			case Market\Trading\Entity\Operation\Order::VIEW:
				$result = $this->hasStatusRights($userId, 'view');
			break;

			case Market\Trading\Entity\Operation\Order::BOX:
			case Market\Trading\Entity\Operation\Order::CIS:
			case Market\Trading\Entity\Operation\Order::DIGITAL:
				$result =
					$this->hasStatusRights($userId, 'update')
					|| $this->hasShipmentRights($userId, ['update', 'delivery', 'deduction']);
			break;

			case Market\Trading\Entity\Operation\Order::ITEM:
				$result = $this->hasStatusRights($userId, 'update');
			break;

			default:
				throw new Main\ArgumentException('unknown operation');
		}

		return $result;
	}

	protected function hasStatusRights($userId, $saleActions)
	{
		/** @var OrderRegistry $orderRegistry */
		$orderRegistry = $this->environment->getOrderRegistry();
		$statusClass = $orderRegistry::getOrderStatusClassName();
		$status = $this->internalOrder->getField('STATUS_ID');
		$result = false;

		foreach ((array)$saleActions as $saleAction)
		{
			$allowedStatuses = $statusClass::getStatusesUserCanDoOperations($userId, [ $saleAction ]);

			if (in_array($status, $allowedStatuses, true))
			{
				$result = true;
				break;
			}
		}

		return $result;
	}

	protected function hasShipmentRights($userId, $saleActions)
	{
		/** @var OrderRegistry $orderRegistry */
		$shipment = $this->getNotSystemShipment();

		if ($shipment !== null)
		{
			$orderRegistry = $this->environment->getOrderRegistry();
			$statusClass = $orderRegistry::getDeliveryStatusClassName();
			$status = $shipment->getField('STATUS_ID');
			$result = false;

			foreach ((array)$saleActions as $saleAction)
			{
				$allowedStatuses = $statusClass::getStatusesUserCanDoOperations($userId, [ $saleAction ]);

				if (in_array($status, $allowedStatuses, true))
				{
					$result = true;
					break;
				}
			}
		}
		else
		{
			$result = false;
		}

		return $result;
	}

	public function getId()
	{
		return $this->internalOrder->getId();
	}

	public function getUserId()
	{
		$userId = (int)$this->internalOrder->getUserId();

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

	public function getAccountNumber()
	{
		return OrderRegistry::getOrderAccountNumber($this->internalOrder);
	}

	public function getCreationDate()
	{
		return $this->internalOrder->getDateInsert();
	}

	public function getCurrency()
	{
		return $this->internalOrder->getCurrency();
	}

	public function getReasonCanceled()
	{
		return (string)$this->internalOrder->getField('REASON_CANCELED');
	}

	public function getProfileName()
	{
		$property = $this->internalOrder->getPropertyCollection()->getProfileName();

		return $property !== null ? $property->getValue() : null;
	}

	public function getPropertyValues()
	{
		$propertyCollection = $this->internalOrder->getPropertyCollection();
		$result = [];

		/** @var Sale\PropertyValue $property */
		foreach ($propertyCollection as $property)
		{
			$propertyId = $property->getField('ORDER_PROPS_ID');

			$result[$propertyId] = $property->getValue();
		}

		return $result;
	}

	public function getPropertyValue($propertyId)
	{
		$propertyCollection = $this->internalOrder->getPropertyCollection();
		$property = $propertyCollection->getItemByOrderPropertyId($propertyId);

		return $property !== null ? $property->getValue() : null;
	}

	public function getSiteId()
	{
		return $this->internalOrder->getSiteId();
	}

	public function setUserId($userId)
	{
		$userId = (int)$userId;
		$result = new Main\Result();

		if ($userId <= 0)
		{
			$result->addError(new Main\Error('cant set empty userId'));
		}
		else
		{
			$basket = $this->internalOrder->getBasket();

			$this->internalOrder->setFieldNoDemand('USER_ID', $userId);

			if ($basket && $this->internalOrder->isNew())
			{
				$fuserId = Sale\Fuser::getIdByUserId($userId);
				$basket->setFUserId($fuserId);
			}
		}

		return $result;
	}

	public function getPersonType()
	{
		return $this->internalOrder->getPersonTypeId();
	}

	public function setPersonType($personType)
	{
		return $this->internalOrder->setPersonTypeId($personType);
	}

	public function initialize()
	{
		Sale\DiscountCouponsManager::init(Sale\DiscountCouponsManager::MODE_EXTERNAL);

		$this->freeze();
		$this->getBasket(); // initialize basket (fix clear shipmentCollection)
	}

	public function fillAccountNumber($accountNumber)
	{
		$this->accountNumberSetter = new Internals\AccountNumberSetter($this->internalOrder, $accountNumber);
		$this->accountNumberSetter->install();
	}

	public function fillXmlId($externalId, $campaignId, EntityReference\Platform $platform)
	{
		$xmlId = $platform->getOrderXmlId($externalId, $campaignId);
		$this->tradingCampaignId = $campaignId;

		$this->internalOrder->setField('XML_ID', $xmlId);
		$this->updateTradeBinding($externalId, $campaignId, $platform);
	}

	public function fillProperties(array $values, $onlyEmpty = false)
	{
		$propertyCollection = $this->internalOrder->getPropertyCollection();
		$result = new Main\Result();
		$changes = [];
		$filled = [];

		/** @var Sale\PropertyValue $property*/
		foreach ($propertyCollection as $property)
		{
			$propertyId = $property->getPropertyId();

			if ($propertyId === null || !array_key_exists($propertyId, $values)) { continue; }
			if ($onlyEmpty && !Market\Utils\Value::isEmpty($property->getValue())) { continue; }

			$value = $values[$propertyId];
			$sanitizedValue = $this->sanitizePropertyValue($property, $value);

			$property->setValue($sanitizedValue);

			if ($property->isChanged())
			{
				$changes[] = $propertyId;

				Listener::addInternalChange(
					$this->internalOrder->getId(),
					sprintf('PROPERTY_%s.VALUE', $propertyId),
					$property->getValue()
				);
			}

			$filled[] = $propertyId;
		}

		$result->setData([
			'CHANGES' => $changes,
			'FILLED' => $filled,
		]);

		return $result;
	}

	protected function sanitizePropertyValue(Sale\PropertyValue $property, $value)
	{
		$value = $this->sanitizePropertyValueByType($property, $value);
		$value = $this->sanitizePropertyValueMultiple($property, $value);
		$value = $this->sanitizePropertyValueOptions($property, $value);

		return $value;
	}

	protected function sanitizePropertyValueByType(Sale\PropertyValue $property, $value)
	{
		$isValueMultiple = is_array($value);
		$sanitizeValues = $isValueMultiple ? $value : [ $value ];
		$propertyFields = $property->getProperty();
		$propertyType = isset($propertyFields['TYPE']) ? $propertyFields['TYPE'] : null;

		foreach ($sanitizeValues as &$sanitizeValue)
		{
			// value

			if (
				$sanitizeValue instanceof Main\Type\DateTime
				&& (
					$propertyType !== 'DATE'
					|| !isset($propertyFields['TIME'])
					|| $propertyFields['TIME'] !== 'N'
				)
			)
			{
				$sanitizeValue = ConvertTimeStamp($sanitizeValue->getTimestamp(), 'FULL', $this->internalOrder->getSiteId());
			}
			else if ($sanitizeValue instanceof Main\Type\Date)
			{
				$sanitizeValue = ConvertTimeStamp($sanitizeValue->getTimestamp(), 'SHORT', $this->internalOrder->getSiteId());
			}
			else if ($value instanceof Market\Data\Type\EnumValue)
			{
				$propertyData = $property->getProperty();
				$propertyType = isset($propertyData['TYPE']) ? $propertyData['TYPE'] : 'STRING';

				$sanitizeValue = $propertyType === 'ENUM' ? $value->code : (string)$value;
			}

			// property

			if ($propertyType === 'NUMBER' && is_string($sanitizeValue) && preg_match('/^\s*(\d+)(?:[.,](\d+))?(\s|$)/', $sanitizeValue, $matches))
			{
				$sanitizeValue = (float)($matches[1] . (isset($matches[2]) && $matches[2] !== '' ? '.' . $matches[2] : ''));
			}
		}
		unset($sanitizeValue);

		return $isValueMultiple ? $sanitizeValues : reset($sanitizeValues);
	}

	protected function sanitizePropertyValueMultiple(Sale\PropertyValue $property, $value)
	{
		$propertyRow = $property->getProperty();
		$isPropertyMultiple = (isset($propertyRow['MULTIPLE']) && $propertyRow['MULTIPLE'] === 'Y');
		$isValueMultiple = is_array($value);

		if ($isPropertyMultiple === $isValueMultiple)
		{
			$result = $value;
		}
		else if ($isValueMultiple)
		{
			$result = $this->environment->getProperty()->joinPropertyMultipleValue($property, $value);
		}
		else if (!Market\Utils\Value::isEmpty($value))
		{
			$result = [ $value ];
		}
		else
		{
			$result = null;
		}

		return $result;
	}

	protected function sanitizePropertyValueOptions(Sale\PropertyValue $property, $value)
	{
		$propertyRow = $property->getProperty();

		if (!isset($propertyRow['TYPE']) || $propertyRow['TYPE'] !== 'ENUM') { return $value; }

		$options = $this->getPropertyValueOptions($property);

		if (isset($options[$value]))
		{
			$result = $value;
		}
		else
		{
			$key = array_search($value, $options, true);
			$result = $key !== false ? $key : null;
		}

		return $result;
	}

	protected function getPropertyValueOptions(Sale\PropertyValue $property)
	{
		$propertyRow = $property->getProperty();

		return method_exists($property, 'getPropertyObject')
			? $property->getPropertyObject()->getOptions()
			: Sale\PropertyValue::loadOptions($propertyRow['ID']);
	}

	public function resetLocation()
	{
		return $this->setLocationPropertyValue(null);
	}

	public function setLocation($locationId)
	{
		$locationCode = \CSaleLocation::getLocationCODEbyID($locationId);

		return $this->setLocationPropertyValue($locationCode);
	}

	protected function setLocationPropertyValue($locationCode = null)
	{
		$propertyCollection = $this->internalOrder->getPropertyCollection();
		$locationProperty = $propertyCollection->getDeliveryLocation();
		$result = new Main\Result();

		if ($locationProperty === null)
		{
			$errorMessage = static::getLang('TRADING_ENTITY_SALE_ORDER_HASNT_LOCATION_PROPERTY');
			$result->addError(new Main\Error($errorMessage));

			return $result;
		}

		$locationProperty->setValue($locationCode);

		return $result;
	}

	public function addProduct($productId, $count = 1, array $data = null)
	{
		$basket = $this->getBasket();
		$basketFields = $this->getProductBasketFields($productId, $count, $data);

		$result = $this->createBasketItem($basket, $basketFields);
		$addData = $result->getData();

		if (isset($addData['BASKET_ITEM']))
		{
			/** @var Sale\BasketItemBase $basketItem */
			$basketItem = $addData['BASKET_ITEM'];
			$basketCode = $basketItem->getBasketCode();
			$addData['BASKET_CODE'] = $basketCode;

			$result->setData($addData);

			if (!empty($data))
			{
				$basketHandler = $this->getBasketDataPreserver();
				$basketHandler->preserve($basketCode, $data);
			}
		}

		return $result;
	}

	protected function getProductBasketFields($productId, $count = 1, array $data = null)
	{
		$result = [
			'PRODUCT_ID' => $productId,
			'QUANTITY' => $count,
			'CURRENCY' => $this->internalOrder->getCurrency(), // required for bitrix17
			'MODULE' => 'catalog',
			'PRODUCT_PROVIDER_CLASS' => $this->getProductDefaultProvider(),
		];

		if ($data !== null)
		{
			$result = $data + $result; // user data priority
		}

		return $result;
	}

	protected function getProductDefaultProvider()
	{
		if (method_exists(Catalog\Product\Basket::class, 'getDefaultProviderName'))
		{
			$result = Catalog\Product\Basket::getDefaultProviderName();
		}
		else
		{
			$result = 'CCatalogProductProvider';
		}

		return $result;
	}

	protected function createBasketItem(Sale\BasketBase $basket, $basketFields)
	{
		/** @var Sale\BasketItemBase $basketItem */
		$result = new Main\Result();
		$basketItem = $basket->createItem($basketFields['MODULE'], $basketFields['PRODUCT_ID']);
		$settableFieldsMap = array_flip($basketItem::getSettableFields());
		$alreadySetFields = [
			'MODULE' => true,
			'PRODUCT_ID' => true,
		];

		// apply limits

		if (isset($basketFields['AVAILABLE_QUANTITY']))
		{
			$siblingsQuantity = $this->getBasketProductSiblingsFilledQuantity($basketItem);

			if ($siblingsQuantity > 0)
			{
				$basketFields['AVAILABLE_QUANTITY'] -= $siblingsQuantity;
			}

			if ($basketFields['AVAILABLE_QUANTITY'] < $basketFields['QUANTITY'])
			{
				$basketFields['QUANTITY'] = $basketFields['AVAILABLE_QUANTITY'];

				$result->addError(new Main\Error(static::getLang('TRADING_ENTITY_SALE_ENTITY_ORDER_BASKET_ITEM_INSUFFICIENT_QUANTITY', [
					'#REQUIRED#' => $basketFields['QUANTITY'],
					'#AVAILABLE#' => $basketFields['AVAILABLE_QUANTITY'],
				])));
			}
		}

		// properties

		$propertyCollection = $basketItem->getPropertyCollection();

		if (!empty($basketFields['PROPS']) && $propertyCollection)
		{
			$propertyCollection->setProperty($basketFields['PROPS']);
		}

		$alreadySetFields += [
			'PROPS' => true,
		];

		// set presets

		$presetsFields = [
			'PRODUCT_PROVIDER_CLASS' => true,
			'CALLBACK_FUNC' => true,
			'PAY_CALLBACK_FUNC' => true,
			'SUBSCRIBE' => true,
		];
		$presets = array_intersect_key($basketFields, $presetsFields);
		$presets = array_intersect_key($presets, $settableFieldsMap);

		$basketItem->setFields($presets);
		$alreadySetFields += $presetsFields;

		// get provider data

		$providerResult = $this->getBasketItemProviderData($basket, $basketItem, $basketFields);

		if ($providerResult->isSuccess())
		{
			$providerData = (array)$providerResult->getData();

			if (
				isset($providerData['AVAILABLE_QUANTITY'], $basketFields['AVAILABLE_QUANTITY'])
				&& $providerData['AVAILABLE_QUANTITY'] < $basketFields['AVAILABLE_QUANTITY']
			)
			{
				$basketFields['AVAILABLE_QUANTITY'] = $providerData['AVAILABLE_QUANTITY'];
			}

			$basketFields += $providerData;
		}
		else
		{
			$result->addErrors($providerResult->getErrors());
		}

		// set name

		if (isset($basketFields['NAME']))
		{
			$basketItem->setField('NAME', $basketFields['NAME']);

			if ($this->useBasketItemNameFromProvider())
			{
				$alreadySetFields['NAME'] = true;
			}
		}

		// set quantity

		$setQuantityResult = $basketItem->setField('QUANTITY', $basketFields['QUANTITY']);

		if (!$setQuantityResult->isSuccess())
		{
			$this->fillBasketItemAvailableQuantity($basketItem, $basketFields);

			$result->addErrors($setQuantityResult->getErrors());
		}

		$alreadySetFields += [
			'QUANTITY' => true,
		];

		// set left fields

		$leftFields = array_diff_key($basketFields, $alreadySetFields);
		$leftFields = array_intersect_key($leftFields, $settableFieldsMap);

		$setLeftFields = $basketItem->setFields($leftFields);

		if (!$setLeftFields->isSuccess())
		{
			$result->addErrors($setLeftFields->getErrors());
		}

		$result->setData([
			'BASKET_ITEM' => $basketItem,
		]);

		return $result;
	}

	protected function useBasketItemNameFromProvider()
	{
		return Market\Config::getOption('trading_basket_name_original') === 'Y';
	}

	protected function getBasketItemProviderData(Sale\BasketBase $basket, Sale\BasketItemBase $basketItem, $basketFields)
	{
		$result = new Main\Result();
		$initialQuantity = $basketItem->getField('QUANTITY');

		$basketItem->setFieldNoDemand('QUANTITY', $basketFields['QUANTITY']); // required for get available quantity

		$providerData = Sale\Provider::getProductData($basket, ['PRICE', 'AVAILABLE_QUANTITY'], $basketItem);
		$basketCode = $basketItem->getBasketCode();

		if (empty($providerData[$basketCode]))
		{
			$errorMessage = static::getLang('TRADING_ENTITY_SALE_ORDER_PRODUCT_NO_PROVIDER_DATA');
			$result->addError(new Main\Error($errorMessage));
		}
		else
		{
			// -- cache provider data to discount

			$discount = $basket->getOrder()->getDiscount();

			if ($discount instanceof Sale\Discount)
			{
				$basketFieldsDiscountData = array_intersect_key($basketFields, [
					'BASE_PRICE' => true,
					'DISCOUNT_PRICE' => true,
					'PRICE' => true,
					'CURRENCY' => true,
					'DISCOUNT_LIST' => true,
				]);
				$discountBasketData = $basketFieldsDiscountData + $providerData[$basketCode];

				$discount->setBasketItemData($basketCode, $discountBasketData);
			}

			// -- export data

			$result->setData($providerData[$basketCode]);
		}

		$basketItem->setFieldNoDemand('QUANTITY', $initialQuantity); // reset initial quantity

		return $result;
	}

	protected function fillBasketItemAvailableQuantity(Sale\BasketItemBase $basketItem, $fields)
	{
		$currentQuantity = (float)$basketItem->getQuantity();
		$requestedQuantity = (float)$fields['QUANTITY'];

		if ($currentQuantity < $requestedQuantity && isset($fields['AVAILABLE_QUANTITY']))
		{
			$siblingsQuantity = $this->getBasketProductSiblingsFilledQuantity($basketItem);
			$availableQuantity = (float)$fields['AVAILABLE_QUANTITY'] - $siblingsQuantity;

			if ($availableQuantity > $currentQuantity && $availableQuantity < $requestedQuantity)
			{
				$basketItem->setField('QUANTITY', $availableQuantity);
			}
		}
	}

	protected function getBasketProductSiblingsFilledQuantity(Sale\BasketItemBase $basketItem)
	{
		$result = 0;
		$searchProductId = (string)$basketItem->getProductId();

		if ($searchProductId !== '')
		{
			$basket = $basketItem->getCollection();

			/** @var Sale\BasketItemBase $basketItem*/
			foreach ($basket as $siblingItem)
			{
				if (
					$siblingItem !== $basketItem
					&& (string)$siblingItem->getProductId() === $searchProductId
					&& $siblingItem->canBuy()
				)
				{
					$result += $siblingItem->getQuantity();
				}
			}
		}

		return $result;
	}

	public function fillMarking(array $basketMarkings)
	{
		$result = new Main\Result();
		$allChanges = [];

		foreach ($basketMarkings as $basketCode => $itemMarkings)
		{
			$basketItem = $this->getBasket()->getItemByBasketCode($basketCode);

			if ($basketItem === null) { continue; }
			if (!method_exists($basketItem, 'isSupportedMarkingCode') || !$basketItem->isSupportedMarkingCode()) { continue; }

			$type = $this->basketItemMarkingType($basketItem) ?: Market\Data\Trading\MarkingRegistry::CIS;
			$filled = array_column($this->collectBasketItemInstances($basketItem), $type);
			$filled = array_filter($filled);
			$new = $type === Market\Data\Trading\MarkingRegistry::UIN
				? Market\Data\Trading\Uin::diff($itemMarkings, $filled)
				: Market\Data\Trading\Cis::diff($itemMarkings, $filled);
			$itemChanges = [];

			if (empty($new)) { continue; }

			/** @var Sale\Shipment $shipment */
			foreach ($this->internalOrder->getShipmentCollection() as $shipment)
			{
				if ($shipment->isSystem()) { continue; }

				$shipmentItem = $shipment->getShipmentItemCollection()->getItemByBasketCode($basketItem->getBasketCode());

				if ($shipmentItem === null) { continue; }

				$storeItemCollection = $shipmentItem->getShipmentItemStoreCollection();

				if ($storeItemCollection === null) { continue; }

				/** @var Sale\ShipmentItemStore $shipment */
				foreach ($storeItemCollection as $storeItem)
				{
					if (!method_exists($storeItem, 'getMarkingCode')) { continue; }

					$storedCode = (string)$storeItem->getMarkingCode();

					if ($storedCode !== '') { continue; }

					$code = array_shift($new);

					$storeItem->setField('MARKING_CODE', $code);
					$itemChanges[] = $code;

					if (empty($new)) { break; }
				}

				foreach ($new as $code)
				{
					if ($storeItemCollection->count() >= $shipmentItem->getQuantity()) { break; }

					$itemChanges[] = $code;
					$shipmentItemStore = $storeItemCollection->createItem($basketItem);
					$shipmentItemStore->setFields([
						'MARKING_CODE' => $code,
						'QUANTITY' => 1,
					]);
				}
			}

			if (!empty($itemChanges))
			{
				$allChanges[$basketCode] = $itemChanges;
			}
		}

		if (!empty($allChanges))
		{
			Listener::addInternalChange(
				$this->internalOrder->getId(),
				'SHIPMENT.ITEM.STORE.MARKING_CODE',
				Listener::STRICT_INTERNAL_CHANGE
			);
		}

		$result->setData([
			'CHANGES' => $allChanges,
		]);

		return $result;
	}

	public function getExistsBasketItemCodes()
	{
		$basket = $this->getBasket();
		$result = [];

		/** @var Sale\BasketItem $basketItem */
		foreach ($basket as $basketItem)
		{
			$result[] = $basketItem->getBasketCode();
		}

		return $result;
	}

	public function getBasketItemCode($value, $field = 'PRODUCT_ID')
	{
		$basket = $this->getBasket();
		$result = null;

		/** @var Sale\BasketItem $basketItem */
		foreach ($basket as $basketItem)
		{
			$itemValue = (string)$basketItem->getField($field);

			if ($field === 'XML_ID' && $itemValue !== '')
			{
				$hashPosition = Market\Data\TextString::getPosition($itemValue, '_R');

				if ($hashPosition > 0)
				{
					$itemValue = Market\Data\TextString::getSubstring($itemValue, 0, $hashPosition);
				}
			}

			if ($itemValue === (string)$value)
			{
				$result = $basketItem->getBasketCode();
				break;
			}
		}

		return $result;
	}

	public function debugBasketItem($basketCode, array $expected = [])
	{
		$basketItem = $this->getBasket()->getItemByBasketCode($basketCode);

		if ($basketItem === null) { return []; }

		$result = [];
		$current = $basketItem->getFields()->getValues();
		$expected += [
			'PRODUCT_PROVIDER_CLASS' => $this->getProductDefaultProvider(),
		];

		foreach ($expected as $name => $expectedValue)
		{
			if (!isset($current[$name]) && !array_key_exists($name, $current)) { continue; }

			$currentValue = $current[$name];

			if ($name === 'QUANTITY')
			{
				$isEqual = Market\Data\Quantity::equal($expectedValue, $current['QUANTITY']);
			}
			else
			{
				$expectedSanitized = Market\Data\TextString::toLower($expectedValue);
				$currentSanitized = Market\Data\TextString::toLower($currentValue);

				if ($name === 'PRODUCT_PROVIDER_CLASS')
				{
					$expectedSanitized = ltrim($expectedSanitized, '\\');
					$currentSanitized = ltrim($currentSanitized, '\\');
				}

				$isEqual = ($expectedSanitized === $currentSanitized);
			}

			if (!$isEqual)
			{
				$result[$name] = $currentValue;
			}
		}

		return $result;
	}

	public function getBasketItemData($basketCode)
	{
		$result = new Main\Result();
		$basket = $this->getBasket();
		$basketItem = $basket->getItemByBasketCode($basketCode);

		if ($basketItem === null)
		{
			$errorMessage = static::getLang('TRADING_ENTITY_SALE_ENTITY_ORDER_BASKET_ITEM_NOT_FOUND');
			$result->addError(new Main\Error($errorMessage));
		}
		else
		{
			$result->setData([
				'PRODUCT_ID' => $basketItem->getField('PRODUCT_ID'),
				'NAME' => $basketItem->getField('NAME'),
				'PRICE' => $basketItem->getPrice(),
				'DISCOUNT_PRICE' => $basketItem->getDiscountPrice(),
				'QUANTITY' => $basketItem->canBuy() ? $basketItem->getQuantity() : 0,
				'XML_ID' => $basketItem->getField('XML_ID'),
				'MEASURE_NAME' => $basketItem->getField('MEASURE_NAME'),
				'DETAIL_PAGE_URL' => $basketItem->getField('DETAIL_PAGE_URL'),
				'VAT_RATE' => $basketItem->getVatRate() * 100,
				'INSTANCES' => $this->collectBasketItemInstances($basketItem),
				'MARKING_GROUP' => method_exists($basketItem, 'getMarkingCodeGroup')
					? $basketItem->getMarkingCodeGroup()
					: null,
				'MARKING_TYPE' => $this->basketItemMarkingType($basketItem),
			]);
		}

		return $result;
	}

	protected function collectBasketItemInstances(Sale\BasketItemBase $basketItem)
	{
		$basketItemCode = $basketItem->getBasketCode();
		$markingType = $this->basketItemMarkingType($basketItem) ?: Market\Data\Trading\MarkingRegistry::CIS;
		$result = [];

		/** @var Sale\Shipment $shipment */
		foreach ($this->internalOrder->getShipmentCollection() as $shipment)
		{
			if ($shipment->isSystem()) { continue; }

			$shipmentItem = $shipment->getShipmentItemCollection()->getItemByBasketCode($basketItemCode);

			if ($shipmentItem === null) { continue; }

			/** @var Sale\ShipmentItemStore $storeItem */
			foreach ($shipmentItem->getShipmentItemStoreCollection() as $storeItem)
			{
				$markingCode = method_exists($storeItem, 'getMarkingCode')
					? (string)$storeItem->getMarkingCode()
					: '';

				$result[] = [
					$markingType => $markingCode !== '' ? $markingCode : null,
				];
			}
		}

		return $result;
	}

	protected function basketItemMarkingType(Sale\BasketItemBase $basketItem)
	{
		if (!method_exists($basketItem, 'getMarkingCodeGroup')) { return null; }

		return $this->environment->getProduct()->getMarkingGroupType(
			$basketItem->getMarkingCodeGroup()
		);
	}

	public function setBasketItemPrice($basketCode, $price)
	{
		$result = new Main\Result();
		$basket = $this->getBasket();
		$basketItem = $basket->getItemByBasketCode($basketCode);

		if ($basketItem === null)
		{
			$errorMessage = static::getLang('TRADING_ENTITY_SALE_BASKET_ITEM_NOT_FOUND');
			$result->addError(new Main\Error($errorMessage));
		}
		else
		{
			$setResult = $basketItem->setFields([
				'CUSTOM_PRICE' => 'Y',
				'PRICE' => $price
			]);

			if (!$setResult->isSuccess())
			{
				$result->addErrors($setResult->getErrors());
			}
			else if ((float)$basketItem->getField('BASE_PRICE') < $price)
			{
				$basketItem->setField('BASE_PRICE', $price);
			}
		}

		return $result;
	}

	public function setBasketItemQuantity($basketCode, $quantity, $silent = false)
	{
		$result = new Main\Result();
		$basket = $this->getBasket();
		$basketItem = $basket->getItemByBasketCode($basketCode);

		if ($basketItem === null)
		{
			$errorMessage = static::getLang('TRADING_ENTITY_SALE_BASKET_ITEM_NOT_FOUND');
			$result->addError(new Main\Error($errorMessage));
		}
		else if (Market\Data\Quantity::round($quantity) === Market\Data\Quantity::round($basketItem->getQuantity()))
		{
			// nothing
		}
		else if ($silent)
		{
			$basketItem->setFieldNoDemand('QUANTITY', $quantity);

			Listener::addInternalChange(
				$this->internalOrder->getId(),
				sprintf('BASKET.%s.QUANTITY', $basketCode),
				$quantity
			);

			if ($this->supportsShipmentItemOverheadQuantity())
			{
				$this->syncShipmentItemQuantity($basketItem, true);
			}
		}
		else
		{
			$setResult = $basketItem->setField('QUANTITY', $quantity);

			if ($setResult->isSuccess())
			{
				Listener::addInternalChange(
					$this->internalOrder->getId(),
					sprintf('BASKET.%s.QUANTITY', $basketCode),
					$quantity
				);

				$this->syncShipmentItemQuantity($basketItem);
			}
			else
			{
				$result->addErrors($setResult->getErrors());
			}
		}

		return $result;
	}

	public function setBasketStore(array $stores)
	{
		$result = new Main\Result();
		$storeId = reset($stores);

		if (!is_numeric($storeId) || count($stores) !== 1 || !Sale\Configuration::useStoreControl())
		{
			return $result;
		}

		/** @var Sale\BasketItem $basketItem */
		foreach ($this->getBasket() as $basketItem)
		{
			$itemResult = $this->setBasketItemStore($basketItem->getBasketCode(), $storeId);

			if (!$itemResult->isSuccess())
			{
				$result->addErrors($itemResult->getErrors());
			}
		}

		return $result;
	}

	public function setBasketItemStore($basketCode, $storeId)
	{
		$result = new Main\Result();

		try
		{
			$basketItem = $this->getBasket()->getItemByBasketCode($basketCode);

			if ($basketItem === null)
			{
				throw new Main\SystemException(static::getLang('TRADING_ENTITY_SALE_ENTITY_ORDER_BASKET_ITEM_NOT_FOUND'));
			}

			$shipment = $this->resolveBasketItemStoreShipment($basketItem, $storeId);

			if ($shipment === null)
			{
				throw new Main\SystemException(static::getLang('TRADING_ENTITY_SALE_ENTITY_ORDER_BASKET_ITEM_SHIPMENT_MISSING'));
			}

			$basketItemCode = $basketItem->getBasketCode();
			$shipmentItem = $shipment->getShipmentItemCollection()->getItemByBasketCode($basketItemCode);

			if ($shipmentItem === null)
			{
				$shipmentItem = $this->moveBasketItemToShipment($basketItem, $shipment);
			}

			if ($shipmentItem === null)
			{
				throw new Main\SystemException(static::getLang('TRADING_ENTITY_SALE_ENTITY_ORDER_BASKET_ITEM_SHIPMENT_ITEM_MISSING'));
			}

			if (Sale\Configuration::useStoreControl())
			{
				if (method_exists($basketItem, 'isSupportedMarkingCode') && $basketItem->isSupportedMarkingCode())
				{
					foreach (range(1, $basketItem->getQuantity()) as $unused)
					{
						$storeItem = $shipmentItem->getShipmentItemStoreCollection()->createItem($basketItem);

						$storeItem->setField('STORE_ID', $storeId);
						$storeItem->setField('QUANTITY', 1);
					}
				}
				else
				{
					$storeItem = $shipmentItem->getShipmentItemStoreCollection()->createItem($basketItem);

					$storeItem->setField('STORE_ID', $storeId);
					$storeItem->setField('QUANTITY', $basketItem->getQuantity());
				}
			}
		}
		catch (Main\SystemException $exception)
		{
			$result->addError(new Main\Error($exception->getMessage()));
		}

		return $result;
	}

	protected function resolveBasketItemStoreShipment(Sale\BasketItemBase $basketItem, $storeId)
	{
		$basketItemShipment = $this->getBasketItemShipment($basketItem);

		if ($basketItemShipment === null)
		{
			$result = $this->getStoreShipment($storeId) ?: $this->makeStoreShipment($storeId);
		}
		else if ($basketItemShipment->getStoreId() <= 0)
		{
			$basketItemShipment->setStoreId((int)$storeId);
			$result = $basketItemShipment;
		}
		else if ($basketItemShipment->getStoreId() !== (int)$storeId)
		{
			$result = $this->getStoreShipment($storeId) ?: $this->makeStoreShipment($storeId);
		}
		else
		{
			$result = $basketItemShipment;
		}

		if ($result !== $basketItemShipment)
		{
			$this->moveBasketItemToShipment($basketItem, $result, $basketItemShipment);
		}

		return $result;
	}

	protected function getBasketItemShipment(Sale\BasketItemBase $basketItem)
	{
		if (!($this->internalOrder instanceof Sale\Order)) { return null; }

		$result = null;
		$basketCode = $basketItem->getBasketCode();

		/** @var Sale\Shipment $shipment */
		foreach ($this->internalOrder->getShipmentCollection() as $shipment)
		{
			if ($shipment->isSystem()) { continue; }

			$shipmentItem = $shipment->getShipmentItemCollection()->getItemByBasketCode($basketCode);

			if ($shipmentItem !== null)
			{
				$result = $shipment;
				break;
			}
		}

		return $result;
	}

	protected function getStoreShipment($storeId)
	{
		if (!($this->internalOrder instanceof Sale\Order)) { return null; }

		$storeId = (int)$storeId;
		$result = null;

		/** @var Sale\Shipment $shipment */
		foreach ($this->internalOrder->getShipmentCollection() as $shipment)
		{
			if ($shipment->isSystem()) { continue; }

			if ($shipment->getStoreId() === $storeId)
			{
				$result = $shipment;
				break;
			}
		}

		return $result;
	}

	protected function makeStoreShipment($storeId)
	{
		$originalShipment = $this->getNotSystemShipment();

		if ($originalShipment === null) { return null; }

		if ($originalShipment->getStoreId() > 0)
		{
			/** @var Sale\ShipmentCollection $shipmentCollection */
			$shipmentCollection = $originalShipment->getCollection();
			$result = $shipmentCollection->createItem();
			$originalValues = $originalShipment->getFields()->getValues();
			$originalValues = array_intersect_key(
				$originalValues,
				array_flip(Sale\Shipment::getAvailableFields())
			);

			$result->setFields($originalValues);
		}
		else
		{
			$result = $originalShipment;
		}

		$result->setStoreId((int)$storeId);

		return $result;
	}

	protected function moveBasketItemToShipment(Sale\BasketItemBase $basketItem, Sale\Shipment $to, Sale\Shipment $from = null)
	{
		if (!($basketItem instanceof Sale\BasketItem)) { return null; }

		$basketCode = $basketItem->getBasketCode();

		// remove from

		if ($from !== null)
		{
			$fromShipmentItem = $from->getShipmentItemCollection()->getItemByBasketCode($basketCode);

			if ($fromShipmentItem !== null)
			{
				$fromShipmentItem->setField('QUANTITY', 0);
				$fromShipmentItem->delete();
			}
		}

		// add to

		$toShipmentCollection = $to->getShipmentItemCollection();
		$shipmentItem = $toShipmentCollection->createItem($basketItem);

		if ($shipmentItem === null) { return null; }

		$shipmentItem->setField('QUANTITY', $basketItem->getQuantity());

		return $shipmentItem;
	}

	public function deleteBasketItem($basketCode)
	{
		$result = new Main\Result();
		$basket = $this->getBasket();
		$basketItem = $basket->getItemByBasketCode($basketCode);

		if ($basketItem === null)
		{
			$errorMessage = static::getLang('TRADING_ENTITY_SALE_BASKET_ITEM_NOT_FOUND');
			$result->addError(new Main\Error($errorMessage));
		}
		else
		{
			$deleteResult = $basketItem->delete();

			if ($deleteResult->isSuccess())
			{
				Listener::addInternalChange($this->internalOrder->getId(), 'BASKET.DELETE', $basketCode);
			}
			else
			{
				$result->addErrors($deleteResult->getErrors());
			}
		}

		return $result;
	}

	public function getBasketPrice()
	{
		return $this->getBasket()->getPrice();
	}

	/** @return Sale\OrderBase */
	public function getInternal()
	{
		return $this->internalOrder;
	}

	/** @return Sale\OrderBase */
	public function getCalculatable()
	{
		if ($this->calculatable === null)
		{
			$this->calculatable = $this->createCalculatable();
		}

		return $this->calculatable;
	}

	protected function createCalculatable()
	{
		$order = method_exists($this->internalOrder, 'createClone')
			? $this->internalOrder->createClone()
			: $this->internalOrder;

		$order->isStartField();

		$shipment = $this->getNotSystemShipment($order) ?: $this->initOrderShipment($order);

		if ($shipment !== null)
		{
			$shipment->setField('CUSTOM_PRICE_DELIVERY', 'N');
		}

		return $order;
	}

	public function getDeliveryId()
	{
		$shipment = $this->getNotSystemShipment();

		return $shipment !== null ? $shipment->getDeliveryId() : null;
	}

	public function createShipment($deliveryId, $price = null, array $data = null)
	{
		$shipmentCollection = $this->internalOrder->getShipmentCollection();

		$this->clearOrderShipment($shipmentCollection);
		$shipment = $this->buildOrderShipment($shipmentCollection, $deliveryId, $data);

		$this->fillShipmentPrice($shipment, $price);
		$this->fillShipmentBasket($shipment);

		return new Main\Result();
	}

	protected function initOrderShipment(Sale\OrderBase $order = null)
	{
		if ($order === null) { $order = $this->internalOrder; }

		$shipmentCollection = $order->getShipmentCollection();
		$shipment = $shipmentCollection->createItem();
		$shipment->setField('CURRENCY', $order->getCurrency());

		$this->fillShipmentBasket($shipment);

		return $shipment;
	}

	protected function clearOrderShipment(Sale\ShipmentCollection $shipmentCollection)
	{
		/** @var Sale\Shipment $shipment */
		foreach ($shipmentCollection as $shipment)
		{
			if (!$shipment->isSystem())
			{
				$shipment->delete();
			}
		}
	}

	protected function buildOrderShipment(Sale\ShipmentCollection $shipmentCollection, $deliveryId, array $data = null)
	{
		$shipment = $shipmentCollection->createItem();

		if ((int)$deliveryId > 0)
		{
			$delivery = Sale\Delivery\Services\Manager::getObjectById($deliveryId);

			if ($delivery !== null)
			{
				$deliveryName = $delivery->getNameWithParent();
			}
			else
			{
				$deliveryName = 'Not found (' . $deliveryId . ')';
			}

			$shipment->setField('DELIVERY_ID', $deliveryId);
			$shipment->setField('DELIVERY_NAME', $deliveryName);
		}

		if ($data === null) { $data = []; }

		$data += [
			'CURRENCY' => $this->internalOrder->getCurrency(),
		];
		$settableFields = array_flip($shipment::getAvailableFields());
		$settableData = array_intersect_key($data, $settableFields);

		if (!empty($settableData))
		{
			$shipment->setFields($settableData);
		}

		return $shipment;
	}

	protected function fillShipmentPrice(Sale\Shipment $shipment, $price = null)
	{
		if ($price !== null)
		{
			$result = $shipment->setFields([
				'CUSTOM_PRICE_DELIVERY' => 'Y',
				'BASE_PRICE_DELIVERY' => $price,
				'PRICE_DELIVERY' => $price,
			]);
		}
		else
		{
			$result = new Sale\Result();
		}

		return $result;
	}

	protected function fillShipmentBasket(Sale\Shipment $shipment)
	{
		/** @var Sale\BasketItem $basketItem */
		/** @var Sale\ShipmentItem $shipmentItem */
		$basket = $this->getBasket();
		$shipmentItemCollection = $shipment->getShipmentItemCollection();

		foreach ($basket as $basketItem)
		{
			$shipmentItem = $shipmentItemCollection->createItem($basketItem);

			if ($shipmentItem)
			{
				$shipmentItem->setQuantity($basketItem->getQuantity());
			}
		}
	}

	public function getShipmentPrice($deliveryId)
	{
		$shipment = $this->getDeliveryShipment($deliveryId);

		return $shipment !== null ? $shipment->getPrice() : null;
	}

	public function setShipmentPrice($deliveryId, $price)
	{
		$shipment = $this->getDeliveryShipment($deliveryId);

		if ($shipment === null)
		{
			$result = new Main\Result();
			$result->addError(new Main\Error(
				sprintf('cant find shipment with delivery service %s', $deliveryId)
			));
		}
		else
		{
			$result = $this->fillShipmentPrice($shipment, $price);
		}

		return $result;
	}

	public function setShipmentStore($deliveryId, $storeId)
	{
		$shipment = $this->getDeliveryShipment($deliveryId);
		$result = new Main\Result();

		if ($shipment === null)
		{
			$result->addError(new Main\Error(
				sprintf('cant find shipment with delivery service %s', $deliveryId)
			));

			return $result;
		}

		// always set for event handlers

		$shipment->setStoreId((int)$storeId);

		// test delivery support

		$storeExtraService = Sale\Delivery\ExtraServices\Manager::getStoresFields($shipment->getDeliveryId());

		if (empty($storeExtraService))
		{
			$result->addError(new Main\Error(
				sprintf('delivery service %s hasn\'t support for store selection', $deliveryId)
			));
		}

		return $result;
	}

	protected function getDeliveryShipment($deliveryId)
	{
		$deliveryId = (int)$deliveryId;
		$result = null;

		/** @var Sale\Shipment $shipment */
		foreach ($this->internalOrder->getShipmentCollection() as $shipment)
		{
			if ($shipment->isSystem()) { continue; }

			if ((int)$shipment->getDeliveryId() === $deliveryId)
			{
				$result = $shipment;
				break;
			}
		}

		return $result;
	}

	protected function supportsShipmentItemOverheadQuantity()
	{
		$saleVersion = Main\ModuleManager::getVersion('sale');

		return $saleVersion !== false && CheckVersion($saleVersion, '17.0.0');
	}

	protected function syncShipmentItemQuantity(Sale\BasketItemBase $basketItem, $silent = false)
	{
		if (!($basketItem instanceof Sale\BasketItem)) { return; }

		$shipment = $this->getBasketItemShipment($basketItem) ?: $this->getNotSystemShipment();

		if ($shipment === null) { return; }

		$shipmentItemCollection = $shipment->getShipmentItemCollection();
		$shipmentItem = $shipmentItemCollection->createItem($basketItem);

		if ($shipmentItem === null) { return; }

		if ($silent)
		{
			$shipmentItem->setFieldNoDemand('QUANTITY', $basketItem->getQuantity());
		}
		else
		{
			$shipmentItem->setField('QUANTITY', $basketItem->getQuantity());
		}
	}

	protected function getNotSystemShipment(Sale\OrderBase $order = null)
	{
		if ($order === null) { $order = $this->internalOrder; }

		$shipmentCollection = $order->getShipmentCollection();
		$result = null;

		/** @var \Bitrix\Sale\Shipment $shipment */
		foreach ($shipmentCollection as $shipment)
		{
			if (!$shipment->isSystem())
			{
				$result = $shipment;
				break;
			}
		}

		return $result;
	}

	public function getPaySystemId()
	{
		$paymentCollection = $this->internalOrder->getPaymentCollection();
		$payments = $this->filterPaymentCollection($paymentCollection, [
			'SUBSIDY' => false,
		]);

		if (empty($payments)) { return null; }

		$firstPayment = reset($payments);

		return $firstPayment->getPaymentSystemId();
	}

	public function createPayment($paySystemId, $price = null, array $data = null)
	{
		$paymentCollection = $this->internalOrder->getPaymentCollection();

		$payment = $this->buildOrderPayment($paymentCollection, $paySystemId, $data);
		$this->fillPaymentPrice($payment, $price);

		return new Main\Result();
	}

	protected function clearOrderPayment(Sale\PaymentCollection $paymentCollection)
	{
		/** @var Sale\Payment $payment*/
		foreach ($paymentCollection as $payment)
		{
			if ($payment->isPaid())
			{
				$payment->setPaid('N');
			}

			$payment->delete();
		}
	}

	protected function buildOrderPayment(Sale\PaymentCollection $paymentCollection, $paySystemId, array $data = null)
	{
		$payment = $paymentCollection->createItem();

		if ((int)$paySystemId > 0)
		{
			$paySystem = Sale\PaySystem\Manager::getById($paySystemId);

			if ($paySystem !== false)
			{
				$paySystemName = $paySystem['NAME'];
			}
			else
			{
				$paySystemName = 'Not found (' . $paySystemId . ')';
			}

			$payment->setField('PAY_SYSTEM_ID', $paySystemId);
			$payment->setField('PAY_SYSTEM_NAME', $paySystemName);
		}

		if (!empty($data))
		{
			$settableFields = array_flip($payment::getAvailableFields());
			$settableData = array_intersect_key($data, $settableFields);

			$payment->setFields($settableData);
		}

		if (isset($data['SUBSIDY']) && $data['SUBSIDY'] === true)
		{
			$xmlId = $this->makeSubsidyPaymentXmlId($data);

			$payment->setField('XML_ID', $xmlId);
		}

		return $payment;
	}

	protected function fillPaymentPrice(Sale\Payment $payment, $price = null)
	{
		if ($price === null)
		{
			$orderPrice = $this->internalOrder->getPrice();
			$paymentsSum = $this->internalOrder->getPaymentCollection()->getSum();
			$selfSum = $payment->getSum();

			$price = $orderPrice - ($paymentsSum - $selfSum);
		}

		$payment->setField('SUM', $price);
	}

	protected function isSubsidyPayment(Sale\Payment $payment)
	{
		$xmlId = (string)$payment->getField('XML_ID');

		return (Market\Data\TextString::getPosition($xmlId, static::PAYMENT_SUBSIDY_PREFIX) === 0);
	}

	protected function makeSubsidyPaymentXmlId(array $data)
	{
		$orderId = isset($data['ORDER_ID']) ? $data['ORDER_ID'] : 'CART';

		return static::PAYMENT_SUBSIDY_PREFIX . $orderId;
	}

	public function setNotes($notes)
	{
		return $this->internalOrder->setField('USER_DESCRIPTION', $notes);
	}

	public function finalize()
	{
		$basket = $this->getBasket();
		$basketHandler = $this->getBasketDataPreserver();
		$basketHandler->install();

		$this->internalOrder->setMathActionOnly(false);
		$result = $basket->refreshData();

		$basketHandler->release();

		if ($result->isSuccess())
		{
			$unfreezeResult = $this->unfreeze();

			if (!$unfreezeResult->isSuccess())
			{
				$result->addErrors($unfreezeResult->getErrors());
			}
		}

		return $result;
	}

	public function freeze()
	{
		$this->isStartField = $this->internalOrder->isStartField();
		$this->internalOrder->setMathActionOnly(true);
	}

	public function unfreeze()
	{
		$result = new Main\Result();
		$needCalculate = (
			$this->needCalculate(EntityOperation\PriceCalculation::PRODUCT)
			|| $this->needCalculate(EntityOperation\PriceCalculation::DELIVERY)
		);

		$this->internalOrder->setMathActionOnly(false);

		if ($this->isStartField && $needCalculate)
		{
			$hasMeaningfulFields = $this->internalOrder->hasMeaningfulField();
			$finalActionResult = $this->internalOrder->doFinalAction($hasMeaningfulFields);

			if (!$finalActionResult->isSuccess())
			{
				$result->addErrors($finalActionResult->getErrors());
			}
		}

		return $result;
	}

	public function isExistMarker($code, $condition = null)
	{
		$marker = $this->environment->getMarker();

		if ($this->internalOrder->getField('MARKED') !== 'Y')
		{
			$result = false;
		}
		else if ($marker->hasExternalEntity())
		{
			$result = ($marker->getMarkerId($this->getId(), $code, $condition) !== null);
		}
		else
		{
			$result = true;
		}

		return $result;
	}

	public function getMarkers($codePrefix)
	{
		if ($this->internalOrder->getField('MARKED') !== 'Y') { return []; }

		$marker = $this->environment->getMarker();
		$result = [];

		if ($marker->hasExternalEntity())
		{
			foreach ($marker->getActive($this->internalOrder->getId()) as $row)
			{
				if (Market\Data\TextString::getPosition($row['CODE'], $codePrefix) !== 0) { continue; }

				$result[] = $row['MESSAGE'];
			}
		}
		else
		{
			$result[] = $this->internalOrder->getField('REASON_MARKED');
		}

		return $result;
	}

	public function addMarker($message, $code)
	{
		$message = $this->truncateMarkerText($message);
		$reason = $message;
		$marker = $this->environment->getMarker();

		if ($marker->hasExternalEntity())
		{
			if (!$marker->hasSameMarker($this->internalOrder, $message))
			{
				$marker->addMarker($this->internalOrder, $this->internalOrder, $message, $code);
			}
		}
		else
		{
			$previousReason = (string)$this->internalOrder->getField('REASON_MARKED');

			if ($previousReason !== '')
			{
				$reason = $previousReason . '<br>' . $reason;
				$reason = $this->truncateMarkerText($reason);
			}
		}

		return $this->internalOrder->setFields([
			'MARKED' => 'Y',
			'REASON_MARKED' => $reason,
		]);
	}

	protected function truncateMarkerText($text)
	{
		$result = $text;
		$maxLength = 250;

		if (Market\Data\TextString::getLength($result) > $maxLength)
		{
			$dots = '...';
			$dotsLength = Market\Data\TextString::getLength($dots);

			$result =
				Market\Data\TextString::getSubstring($result, 0, $maxLength - $dotsLength)
				. $dots;
		}

		return $result;
	}

	public function removeMarker($code)
	{
		$result = new Main\Result();
		$marker = $this->environment->getMarker();
		$hasLeftMarkers = false;

		if ($marker->hasExternalEntity())
		{
			$markerId = $marker->getMarkerId($this->getId(), $code);

			if ($markerId !== null)
			{
				$deleteResult = $marker->delete($markerId);

				if ($deleteResult->isSuccess())
				{
					$hasLeftMarkers = $marker->hasMarkers($this->getId());
				}
				else
				{
					$result->addErrors($deleteResult->getErrors());
				}
			}
			else
			{
				$hasLeftMarkers = $marker->hasMarkers($this->getId());
			}
		}

		if (!$hasLeftMarkers)
		{
			$this->internalOrder->setFields([
				'MARKED' => 'N',
				'REASON_MARKED' => '',
			]);
		}

		return $result;
	}

	public function getStatuses()
	{
		$result = [
			(string)$this->internalOrder->getField('STATUS_ID'),
		];

		if ($this->internalOrder->isCanceled())
		{
			$result[] = Status::STATUS_CANCELED;
		}

		if ($this->internalOrder->isPaid())
		{
			$result[] = Status::STATUS_PAYED;
		}

		if ($this->internalOrder->isAllowDelivery())
		{
			$result[] = Status::STATUS_ALLOW_DELIVERY;
		}

		if ($this->internalOrder->getField('DEDUCTED') === 'Y')
		{
			$result[] = Status::STATUS_DEDUCTED;
		}

		return $result;
	}

	public function setStatus($status, $payload = null)
	{
		switch ($status)
		{
			case Status::STATUS_CANCELED:
				$result = $this->cancelOrder($payload);
			break;

			case Status::STATUS_SUBSIDY:
				$result = $this->setPaid(true, [ 'SUBSIDY' => true ]);
			break;

			case Status::STATUS_PAYED:
				$result = $this->setPaid(true, $payload);
			break;

			case Status::STATUS_ALLOW_DELIVERY:
				$result = $this->allowDelivery();
			break;

			case Status::STATUS_DEDUCTED:
				$result = $this->deduct();
			break;

			default:
				$result = $this->fillStatus($status, $payload);
			break;
		}

		return $result;
	}

	protected function allowDelivery()
	{
		$shipmentCollection = $this->internalOrder->getShipmentCollection();
		$result = new Main\Result();
		$changes = [];

		/** @var Sale\Shipment $shipment */
		foreach ($shipmentCollection as $shipment)
		{
			if ($shipment->isSystem() || $shipment->isAllowDelivery()) { continue; }

			$allowResult = $shipment->allowDelivery();

			if ($allowResult->isSuccess())
			{
				$changes[] = $shipment->getId();
			}
			else
			{
				$result->addErrors($allowResult->getErrors());
			}
		}

		$result->setData([
			'CHANGES' => $changes,
		]);

		return $result;
	}

	protected function deduct()
	{
		$shipmentCollection = $this->internalOrder->getShipmentCollection();
		$result = new Main\Result();
		$changes = [];

		/** @var Sale\Shipment $shipment */
		foreach ($shipmentCollection as $shipment)
		{
			if ($shipment->isSystem() || $shipment->isShipped()) { continue; }

			$deductResult = $shipment->setField('DEDUCTED', 'Y');

			if ($deductResult->isSuccess())
			{
				$changes[] = $shipment->getId();
			}
			else
			{
				$result->addErrors($deductResult->getErrors());
			}
		}

		$result->setData([
			'CHANGES' => $changes,
		]);

		return $result;
	}

	protected function cancelOrder($reason = null)
	{
		$result = new Main\Result();

		if ($this->internalOrder->isCanceled()) { return $result; }

		$this->cancelShipment($this->internalOrder);
		$this->cancelPayment($this->internalOrder);

		$setResult = $this->internalOrder->setField('CANCELED', 'Y');

		if ($setResult->isSuccess())
		{
			$changes = [
				'CANCELED',
			];
			
			Listener::addInternalChange($this->internalOrder->getId(), 'ORDER.CANCELED', 'Y');

			if ((string)$reason !== '')
			{
				$changes[] = 'REASON_CANCELED';
				$this->internalOrder->setField('REASON_CANCELED', $reason);
			}

			$result->setData([
				'CHANGES' => $changes,
			]);
		}
		else
		{
			$result->addErrors($setResult->getErrors());
		}

		return $result;
	}

	protected function cancelShipment(Sale\OrderBase $order)
	{
		/** @var Sale\Shipment $shipment */
		foreach ($order->getShipmentCollection() as $shipment)
		{
			if ($shipment->isShipped())
			{
				if ($shipment->isSystem())
				{
					$shipment->setFieldNoDemand('DEDUCTED', 'N');
				}
				else
				{
					$shipment->setField('DEDUCTED', 'N');
				}
			}

			if ($shipment->isAllowDelivery())
			{
				if ($shipment->isSystem())
				{
					$shipment->setFieldNoDemand('ALLOW_DELIVERY', 'N');
				}
				else
				{
					$shipment->disallowDelivery();
				}
			}
		}
	}

	protected function cancelPayment(Sale\OrderBase $order)
	{
		/** @var Sale\Payment $payment */
		foreach ($order->getPaymentCollection() as $payment)
		{
			if (!$payment->isPaid()) { continue; }

			$payment->setPaid('N');
		}
	}

	protected function setPaid($value, $payload = null)
	{
		$paymentCollection = $this->internalOrder->getPaymentCollection();
		$result = new Sale\Result();
		$value = (bool)$value;
		$changes = [];
		$needSyncPayment = false;

		foreach ($this->filterPaymentCollection($paymentCollection, $payload) as $payment)
		{
			if ((bool)$payment->isPaid() === $value) { continue; }

			$paymentValue = $value ? 'Y' : 'N';

			if ($paymentValue === 'Y' && $this->isPaymentSystemInner($payment))
			{
				$needSyncPayment = true;
				$payment->setFieldNoDemand('PAID', $paymentValue);
				$paymentResult = new Sale\Result();
			}
			else
			{
				$paymentResult = $payment->setPaid($paymentValue);
			}

			if ($paymentResult->isSuccess())
			{
				$changes[] = $payment->getId();
			}
			else
			{
				$result->addErrors($paymentResult->getErrors());
			}
		}

		if ($needSyncPayment)
		{
			$this->syncOrderPaidSum();
		}

		$result->setData([
			'CHANGES' => $changes,
		]);

		return $result;
	}

	/**
	 * @param Sale\PaymentCollection $paymentCollection
	 * @param array|float|null  $payload
	 *
	 * @return Sale\Payment[]
	 */
	protected function filterPaymentCollection(Sale\PaymentCollection $paymentCollection, $payload)
	{
		$payments = iterator_to_array($paymentCollection->getIterator());
		$payload = $this->sanitizePaymentCollectionPayload($payload);

		$payments = $this->filterPaymentCollectionExclude($payments, $payload['EXCLUDE']);
		$payments = $this->filterPaymentCollectionSubsidy($payments, $payload['SUBSIDY']);
		$payments = $this->filterPaymentCollectionPaySystem($payments, $payload['PAY_SYSTEM_ID']);
		$payments = $this->filterPaymentCollectionSum($payments, $payload['SUM']);

		return $payments;
	}

	protected function sanitizePaymentCollectionPayload($payload)
	{
		$result = [
			'SUM' => null,
			'PAY_SYSTEM_ID' => null,
			'SUBSIDY' => null,
			'EXCLUDE' => null,
		];

		if (is_array($payload))
		{
			if (isset($payload['SUM']) && is_numeric($payload['SUM']))
			{
				$result['SUM'] = (float)$payload['SUM'];
			}

			if (isset($payload['PAY_SYSTEM_ID']) && (string)$payload['PAY_SYSTEM_ID'] !== '')
			{
				$result['PAY_SYSTEM_ID'] = (int)$payload['PAY_SYSTEM_ID'];
			}

			if (isset($payload['SUBSIDY']))
			{
				$result['SUBSIDY'] = (bool)$payload['SUBSIDY'];
			}

			if (isset($payload['EXCLUDE']))
			{
				$exclude = $this->sanitizePaymentCollectionPayload($payload['EXCLUDE']);
				$excludeFilled = array_filter($exclude, static function($value) { return $value !== null; });

				if (!empty($excludeFilled))
				{
					$result['EXCLUDE'] = $exclude;
				}
			}
		}
		else if ($payload !== null && is_numeric($payload))
		{
			$result['SUM'] = (float)$payload;
		}

		return $result;
	}

	protected function filterPaymentCollectionExclude(array $payments, $payloadExclude)
	{
		if ($payloadExclude === null) { return $payments; }

		$exclude = $this->filterPaymentCollectionSubsidy($payments, $payloadExclude['SUBSIDY']);
		$exclude = $this->filterPaymentCollectionPaySystem($exclude, $payloadExclude['PAY_SYSTEM_ID']);
		$exclude = $this->filterPaymentCollectionSum($exclude, $payloadExclude['SUM']);

		foreach ($exclude as $excludePayment)
		{
			$index = array_search($excludePayment, $payments, true);

			if ($index !== false)
			{
				array_splice($payments, $index, 1);
			}
		}

		return $payments;
	}

	protected function filterPaymentCollectionSubsidy(array $payments, $payloadSubsidy)
	{
		if ($payloadSubsidy === null) { return $payments; }

		return array_filter($payments, function(Sale\Payment $payment) use ($payloadSubsidy) {
			return ($this->isSubsidyPayment($payment) === $payloadSubsidy);
		});
	}

	protected function filterPaymentCollectionPaySystem(array $payments, $payloadPaySystem)
	{
		if ($payloadPaySystem === null) { return $payments; }

		$result = [];

		/** @var Sale\Payment $payment */
		foreach ($payments as $payment)
		{
			$paySystemId = (int)$payment->getPaymentSystemId();

			if ($paySystemId === $payloadPaySystem)
			{
				$result[] = $payment;
			}
		}

		return $result;
	}

	protected function filterPaymentCollectionSum(array $payments, $payloadSum)
	{
		if ($payloadSum === null) { return $payments; }

		$threshold = 1;
		$result = [];

		foreach ($payments as $payment)
		{
			$paymentDiff = abs($payment->getSum() - $payloadSum);

			if ($paymentDiff < $threshold)
			{
				$result[] = $payment;
				break;
			}
		}

		return $result;
	}

	protected function isPaymentSystemInner(Sale\Payment $payment)
	{
		$paySystem = $payment->getPaySystem();

		return ($paySystem !== null && $paySystem->getField('ACTION_FILE') === 'inner');
	}

	protected function fillStatus($status, $payload = null)
	{
		$result = new Sale\Result();

		if ($this->internalOrder->getField('STATUS_ID') === $status) { return $result; }

		$setResult = $this->internalOrder->setField('STATUS_ID', $status);

		if ($setResult->isSuccess())
		{
			$result->setData([
				'CHANGES' => [ 'STATUS_ID' ],
			]);
		}
		else
		{
			$result->addErrors($setResult->getErrors());
		}

		return $result;
	}

	public function resetCashbox()
	{
		if (!class_exists(Sale\Cashbox\Internals\Pool::class)) { return; }

		$internalId = $this->internalOrder->getInternalId();

		// immediate reset for new bitrix

		Sale\Cashbox\Internals\Pool::resetDocs($internalId);

		// delayed reset for old bitrix

		if (!CheckVersion(Main\ModuleManager::getVersion('sale'), '20.0.675'))
		{
			Main\EventManager::getInstance()->addEventHandler('sale', 'OnSalePaymentEntitySaved', static function(Main\Event $event) use ($internalId) {
				/** @var Sale\Payment $payment */
				/** @var Sale\PaymentCollection $paymentCollection */
				$payment = $event->getParameter('ENTITY');
				$paymentCollection = $payment->getCollection();
				$order = $paymentCollection ? $paymentCollection->getOrder() : null;

				if (!$order || $order->getInternalId() !== $internalId) { return; }

				Sale\Cashbox\Internals\Pool::resetDocs($internalId);
			});
		}
	}

	public function add($externalId, EntityReference\Platform $platform)
	{
		$result = new Main\Result();

		$this->syncOrderPrice();
		$this->syncOrderPaymentSum();
        $this->forceFillBasketItemFUserId();

		$orderResult = $this->internalOrder->save();

		if (!$orderResult->isSuccess())
		{
			$result->addErrors($orderResult->getErrors());
		}
		else
		{
			if ($this->accountNumberSetter !== null)
			{
				$this->accountNumberSetter->release();
			}

			$orderId = $orderResult->getId();
			$tradingResult = $this->addTradingTable($orderId, $externalId, $platform);

			if (!$tradingResult->isSuccess())
			{
				$result->addErrors($tradingResult->getErrors());
			}
			else
			{
				$orderExportId = $orderId;
				$orderAccountNumber = (string)$this->internalOrder->getField('ACCOUNT_NUMBER');

				if ($orderAccountNumber !== '' && OrderRegistry::useAccountNumber())
				{
					$orderExportId = $orderAccountNumber;
				}

				$result->setData([
					'ID' => $orderExportId
				]);
			}
		}

		return $result;
	}

	protected function syncOrderPaidSum()
	{
		$paymentCollection = $this->internalOrder->getPaymentCollection();

		if ($paymentCollection === null) { return; }

		$calculated = $paymentCollection->getPaidSum();
		$current = $this->internalOrder->getSumPaid();

		if (Market\Data\Price::round($calculated) !== Market\Data\Price::round($current))
		{
			$this->internalOrder->setFieldNoDemand('SUM_PAID', $calculated);
		}
	}

	protected function syncOrderPrice()
	{
		$currentPrice = $this->internalOrder->getPrice();
		$calculatedPrice = $this->getCalculatedOrderPrice();

		if (Market\Data\Price::round($currentPrice) !== Market\Data\Price::round($calculatedPrice))
		{
			$this->internalOrder->setField('PRICE', $calculatedPrice);
		}
	}

	protected function syncOrderPaymentSum()
	{
		$paymentCollection = $this->internalOrder->getPaymentCollection();

		if ($paymentCollection)
		{
			$lastPayment = null;
			$orderSum = $this->internalOrder->getPrice();
			$paymentSum = 0;

			/** @var Sale\Payment $payment*/
			foreach ($paymentCollection as $payment)
			{
				$paymentSum += $payment->getSum();

				if (!$payment->isPaid() && !$this->isPaymentSystemInner($payment) && !$this->isSubsidyPayment($payment))
				{
					$lastPayment = $payment;
				}
			}

			if (
				$lastPayment !== null
				&& Market\Data\Price::round($orderSum) !== Market\Data\Price::round($paymentSum)
			)
			{
				$newPaymentSum = $orderSum - ($paymentSum - $lastPayment->getSum());

				$payment->setField('SUM', $newPaymentSum);
			}
		}
	}

	protected function getCalculatedOrderPrice()
	{
		$result = 0;
		$basket = $this->internalOrder->getBasket();
		$shipmentCollection = $this->internalOrder->getShipmentCollection();

		if ($basket)
		{
			$result += $basket->getPrice();
		}

		if (!$this->internalOrder->isUsedVat())
		{
			$result += (float)$this->internalOrder->getField('TAX_PRICE');
		}

		if ($shipmentCollection)
		{
			$result += $shipmentCollection->getPriceDelivery();
		}

		return $result;
	}

    /** @noinspection PhpInternalEntityUsedInspection */
    protected function forceFillBasketItemFUserId()
    {
        $fuserId = (int)$this->internalOrder->getBasket()->getFUserId(true);

        if ($fuserId <= 0) { return; }

        /** @var Sale\BasketItem $basketItem */
        foreach ($this->internalOrder->getBasket() as $basketItem)
        {
            $basketItem->setFieldNoDemand('FUSER_ID', $fuserId);
        }
    }

	public function update()
	{
		if ($this->eventProcessing === Listener::STATE_PROCESSING) // will be stored in handlers
		{
			$result = new Main\Result();
		}
		else if ($this->eventProcessing === Listener::STATE_SAVING) // on after order row saved
		{
			$result = Market\Result\Facade::merge([
				$this->updateOrderRow(),
				$this->updateOrderProperties(),
			]);
		}
		else
		{
			$result = $this->internalOrder->save();
		}

		return $result;
	}

	protected function updateOrderRow()
	{
		if (!($this->internalOrder instanceof Sale\Order)) { return new Main\Result(); }

		$changedValues = $this->internalOrder->getFields()->getChangedValues();
		$diffValues = array_diff($changedValues, $this->initialChangedValues);
		$saveValues = array_intersect_key(
			$diffValues,
			Sale\Internals\OrderTable::getEntity()->getFields()
		);

		if (empty($saveValues)) { return new Main\Result(); }

		return Sale\OrderTable::update($this->internalOrder->getId(), $saveValues);
	}

	protected function updateOrderProperties()
	{
		$propertyCollection = $this->internalOrder->getPropertyCollection();

		if (!$propertyCollection->isChanged()) { return new Main\Result(); }

		return $propertyCollection->save();
	}

	public function getTradingInfo()
	{
		return $this->getTradingInfoFromTradeBinding() ?: $this->getTradingInfoFromXmlId();
	}

	protected function getTradingInfoFromTradeBinding()
	{
		if (!method_exists($this->internalOrder, 'getTradeBindingCollection')) { return null; }

		/** @var Sale\TradeBindingEntity $binding */
		foreach ($this->internalOrder->getTradeBindingCollection() as $binding)
		{
			$platformId = (int)$binding->getField('TRADING_PLATFORM_ID');

			if (!Internals\Platform::isOur($platformId)) { continue; }

			/** @var array $params */
			$params = $binding->getField('PARAMS');

			return [
				'PLATFORM_ID' => $platformId, // old behavior
				'TRADING_PLATFORM_ID' => $platformId,
				'ORDER_ID' => (string)$binding->getField('EXTERNAL_ORDER_ID'),
				'CAMPAIGN_ID' => isset($params['CAMPAIGN_ID']) ? (int)$params['CAMPAIGN_ID'] : null,
				'SETUP_ID' => isset($params['SETUP_ID']) ? (int)$params['SETUP_ID'] : null,
			];
		}

		return null;
	}

	protected function getTradingInfoFromXmlId()
	{
		return Platform::parseOrderXmlId((string)$this->internalOrder->getField('XML_ID'));
	}

	protected function supportsTradeBinding(EntityReference\Platform $platform)
	{
		return (
			method_exists($this->internalOrder, 'getTradeBindingCollection')
			&& $platform instanceof Platform
			&& $platform->isInstalled()
		);
	}

	protected function updateTradeBinding($externalId, $campaignId, EntityReference\Platform $platform)
	{
		if (!$this->supportsTradeBinding($platform)) { return; }

		$binding =
			$this->searchTradeBinding($platform)
			?: $this->createTradeBinding($platform);

		if ($externalId !== null)
		{
			$binding->setField('EXTERNAL_ORDER_ID', $externalId);
		}

		$existParams = $binding->getField('PARAMS');

		if (!is_array($existParams)) { $existParams = []; }

		$binding->setField('PARAMS', $existParams + [
			'SETUP_ID' => $platform->getSetupId(),
			'CAMPAIGN_ID' => $campaignId,
		]);
	}

	protected function searchTradeBinding(EntityReference\Platform $platform)
	{
		if (!$this->supportsTradeBinding($platform)) { return null; }

		$platformId = (int)$platform->getId();
		$bindingCollection = $this->internalOrder->getTradeBindingCollection();
		$result = null;

		/** @var Sale\TradeBindingEntity $binding */
		foreach ($bindingCollection as $binding)
		{
			$bindingPlatformId = (int)$binding->getField('TRADING_PLATFORM_ID');

			if ($platformId === $bindingPlatformId)
			{
				$result = $binding;
				break;
			}
		}

		return $result;
	}

	protected function createTradeBinding(EntityReference\Platform $platform)
	{
		if (!$this->supportsTradeBinding($platform)) { return null; }

		$salePlatform = $platform->getSalePlatform();
		$bindingCollection = $this->internalOrder->getTradeBindingCollection();

		return $bindingCollection->createItem($salePlatform);
	}

	protected function addTradingTable($orderId, $externalId, Market\Trading\Entity\Reference\Platform $platform)
	{
		if ($this->supportsTradeBinding($platform) && $this->searchTradeBinding($platform) !== null)
		{
			return new Main\Entity\AddResult(); // save handled inside binding entity
		}

		return Sale\TradingPlatform\OrderTable::add([
			'ORDER_ID' => $orderId,
			'EXTERNAL_ORDER_ID' => $externalId,
			'TRADING_PLATFORM_ID' => $platform->getId(),
			'PARAMS' => [
				'SETUP_ID' => $platform->getSetupId(),
				'CAMPAIGN_ID' => $this->tradingCampaignId,
			],
		]);
	}

	protected function getBasket()
	{
		$order = $this->internalOrder;
		$basket = $order->getBasket();

		if ($basket === null || $basket === false)
		{
			$basket = $this->createBasket($order);
			$order->setBasket($basket);
		}

		return $basket;
	}

	protected function createBasket(Sale\OrderBase $order)
	{
		$siteId = $order->getSiteId();
		$userId = $order->getUserId();
		$fUserId = null;

		if ($userId > 0)
		{
			$fUserId = Sale\Fuser::getIdByUserId($userId);
		}

		$basketClassName = OrderRegistry::getBasketClassName();
		$basket = $basketClassName::create($siteId);
		$basket->setFUserId($fUserId);

		return $basket;
	}

	protected function getBasketDataPreserver()
	{
		if ($this->basketDataPreserver === null)
		{
			$this->basketDataPreserver = new Internals\BasketDataPreserver();
		}

		return $this->basketDataPreserver;
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit