Your IP : 18.190.239.5


Current Path : /var/www/axolotl/data/www/axolotl.ru/www/bitrix/components/yandexpay.pay/purchase/
Upload File :
Current File : /var/www/axolotl/data/www/axolotl.ru/www/bitrix/components/yandexpay.pay/purchase/class.php

<?php

namespace YandexPay\Pay\Components;

use Bitrix\Main;
use Bitrix\Sale;
use Bitrix\Main\Localization\Loc;
use YandexPay\Pay;
use YandexPay\Pay\Exceptions;
use YandexPay\Pay\Reference\Assert;
use YandexPay\Pay\Trading\Action as TradingAction;
use YandexPay\Pay\Trading\Settings as TradingSettings;
use YandexPay\Pay\Trading\Setup as TradingSetup;
use YandexPay\Pay\Trading\Entity\Reference as EntityReference;
use YandexPay\Pay\Trading\Entity\Registry as EntityRegistry;
use YandexPay\Pay\Trading\Entity\Sale as EntitySale;
use YandexPay\Pay\Utils;

if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) { die(); }

Loc::loadMessages(__FILE__);

class Purchase extends \CBitrixComponent
{
	/** @var EntityReference\Environment */
	protected $environment;
	/** @var TradingSettings\Options */
	protected $options;
	/** @var TradingSetup\Model */
	protected $setup;
	/** @var array<int, string> $productIndex => $basketCode  */
	protected $basketMap = [];
	protected $filledProperties = [];
	protected $relatedProperties = [];

	public function executeComponent(): void
	{
		try
		{
			$this->loadModules();
			$this->parseRequest();
			$this->bootstrap();

			$action = $this->resolveAction();
			$this->callAction($action);
		}
		catch (Main\SystemException $exception)
		{
			$this->sendResponse([
				'error' => [
					'code' => (string)$exception->getCode(),
					'message' => $exception->getMessage(),
				],
			]);

			// todo show error, may be loggers
		}
	}

	protected function parseRequest() : void
	{
		$this->request->addFilter(new Utils\JsonBodyFilter());
	}

	protected function makeRequest($className) : TradingAction\Incoming\Common
	{
		Assert::isSubclassOf($className, Pay\Reference\Common\Model::class);

		$data = $this->request->getPostList()->toArray();

		return new $className($data);
	}

	protected function resolveAction() : string
	{
		$request = $this->request->get('yapayAction');

		Assert::notNull($request, 'yapayAction');

		return (string)$request;
	}

	protected function callAction(string $action) : void
	{
		$action = trim($action);
		$method = $action . 'Action';

		if (!method_exists($this, $method)) { throw new Main\ArgumentException('unknown action'); }

		$this->{$method}();
	}

	protected function sendResponse(array $response) : void
	{
		global $APPLICATION;

		$APPLICATION->RestartBuffer();

		$json = Main\Web\Json::encode($response);
		\CMain::FinalActions($json);
	}

	protected function getProductsAction() : void
	{
		$userId = $this->searchUser();
		$order = $this->getOrder($userId);

		/** @var TradingAction\Incoming\Product $request */
		$request = $this->makeRequest(TradingAction\Incoming\Product::class);

		$order->initialize();

		$this->fillPlatform($order);
		$this->fillPersonType($order);
		$this->wakeUpBasket($order, $request);

		$order->finalize();

		// collect response

		$result['amount'] = (string)$order->getOrderPrice();

		foreach ($order->getOrderableItems() as $basketCode)
		{
			$itemResult = $order->getBasketItemData($basketCode);
			$itemData = $itemResult->getData();

			Exceptions\Facade::handleResult($itemResult);
			Assert::notNull($itemData['QUANTITY'], '$itemData[QUANTITY]');

			if ($itemData['QUANTITY'] <= 0) { continue; }

			$result['items'][] = $this->collectBasketItem($itemData);
		}

		$this->sendResponse($result);
	}

	protected function collectBasketItem(array $basketItem) : array
	{
		return [
			'id' => $basketItem['PRODUCT_ID'],
			'count' => (string)$basketItem['QUANTITY'],
			'label' => (string)$basketItem['NAME'],
			'amount' => (string)($basketItem['PRICE'] * $basketItem['QUANTITY']),
			'basketId' => $basketItem['BASKET_ID'] ?? null,
			'props' => $basketItem['PROPS'],
		];
	}

	protected function deliveryOptionsAction() : void
	{
		$result = [];

		$userId = $this->searchUser();
		$order = $this->getOrder($userId);

		/** @var TradingAction\Incoming\DeliveryOptions $request */
		$request = $this->makeRequest(TradingAction\Incoming\DeliveryOptions::class);

		$order->initialize();

		$this->fillPlatform($order);
		$this->fillPersonType($order);
		$this->fillLocation($order, $request->getAddress());
		$this->fillBasket($order, $request->getItems());
		$order->createPayment($request->getPaySystemId());

		$order->finalize();

		$deliveries = $this->restrictedDeliveries($order);
		$deliveries = $this->filterDeliveryByType($deliveries, EntitySale\Delivery::COURIER_TYPE);

		foreach ($deliveries as $deliveryId)
		{
			if (!$this->isDeliveryCompatible($order, $deliveryId)) { continue; }

			$calculationResult = $this->calculateDelivery($order, $deliveryId);

			if (!$calculationResult->isSuccess()) { continue; }

			$result[] = $this->collectDeliveryOption($calculationResult);
		}

		$this->sendResponse($result);
	}

	protected function collectDeliveryOption(EntityReference\Delivery\CalculationResult $calculationResult) : array
	{
		return [
			'id'        => (string)$calculationResult->getDeliveryId(),
			'label'     => $calculationResult->getServiceName(),
			'amount'    => (string)$calculationResult->getPrice(),
			'provider'  => 'custom', //todo
			'category'  => $calculationResult->getCategory(),
			'date'      => $calculationResult->getDateFrom()->getTimestamp(),
		];
	}

	protected function restrictedDeliveries(EntityReference\Order $order) : array
	{
		$result = [];
		$deliveryService = $this->environment->getDelivery();
		$compatibleIds = $deliveryService->getRestricted($order);

		if (empty($compatibleIds)) { return $result; }

		if ($this->options->isDeliveryStrict())
		{
			$deliveryOptions = $this->options->getDeliveryOptions();
			$configuredIds = $deliveryOptions->getServiceIds();

			$result = array_intersect($compatibleIds, $configuredIds);
		}
		else
		{
			$result = $compatibleIds;
		}

		return $result;
	}

	protected function isDeliveryCompatible(EntityReference\Order $order, int $deliveryId) : bool
	{
		return $this->environment->getDelivery()->isCompatible($deliveryId, $order);
	}

	protected function calculateDelivery(EntityReference\Order $order, int $deliveryId) : EntityReference\Delivery\CalculationResult
	{
		return $this->environment->getDelivery()->calculate($deliveryId, $order);
	}

	protected function getPickupStores(EntityReference\Order $order, int $deliveryId, array $bounds = null) : array
	{
		return $this->environment->getDelivery()->getPickupStores($deliveryId, $order, $bounds);
	}

	protected function filterDeliveryByType(array $deliveryIds, string $type) : array
	{
		if (empty($deliveryIds)) { return []; }

		$result = [];

		$deliveryOptions = $this->options->getDeliveryOptions();
		$deliveryService = $this->environment->getDelivery();

		foreach ($deliveryIds as $deliveryId)
		{
			$service = $deliveryOptions->getItemByServiceId($deliveryId);

			if ($service !== null)
			{
				$typeOption = $service->getType();
			}
			else
			{
				$typeOption = $deliveryService->suggestDeliveryType($deliveryId);
			}

			if ($typeOption !== $type) { continue; }

			$result[] = $deliveryId;
		}

		return $result;
	}

	protected function pickupOptionsAction() : void
	{
		$result = [];

		$userId = $this->searchUser();
		$order = $this->getOrder($userId);

		/** @var TradingAction\Incoming\PickupOptions $request */
		$request = $this->makeRequest(TradingAction\Incoming\PickupOptions::class);

		$order->initialize();

		$this->fillPlatform($order);
		$this->fillPersonType($order);
		$this->fillBasket($order, $request->getItems());
		$order->createPayment($request->getPaySystemId());

		$order->finalize();

		$deliveries = $this->restrictedDeliveries($order);
		$deliveries = $this->filterDeliveryByType($deliveries, EntitySale\Delivery::PICKUP_TYPE);
		$bounds = $request->getBounds()->getFields();

		foreach ($deliveries as $deliveryId)
		{
			$allStores = $this->getPickupStores($order, $deliveryId, $bounds);

			foreach ($allStores as $locationId => $stores)
			{
				$this->clearCalculatable($order);

				$this->setLocation($order, $locationId);

				if (!$this->isDeliveryCompatible($order, $deliveryId)) { continue; }

				foreach ($stores as $store)
				{
					$result[] = $this->collectPickupOption($deliveryId, $store, $locationId);
				}
			}
		}

		$this->sendResponse($result);
	}

	protected function pickupDetailAction() : void
	{
		$userId = $this->searchUser();
		$order = $this->getOrder($userId);

		/** @var TradingAction\Incoming\PickupDetail $request */
		$request = $this->makeRequest(TradingAction\Incoming\PickupDetail::class);

		$order->initialize();

		$this->fillPlatform($order);
		$this->fillPersonType($order);
		$this->fillBasket($order, $request->getItems());
		$order->createPayment($request->getPaySystemId());

		$order->finalize();

		$deliveryEnv = $this->environment->getDelivery();
		$deliveryService = $deliveryEnv->getDeliveryService($request->getId());
		$deliveryIntegration = $deliveryEnv->deliveryIntegration($deliveryService, EntitySale\Delivery::PICKUP_TYPE);
		$deliveryIntegration->prepareCalculatePickup(
			$order->getCalculatable(),
			$request->getId(),
			$request->getStoreId(),
			$request->getLocationId(),
			$request->getZip()
		);

		$this->clearCalculatable($order);
		$this->setLocation($order, $request->getLocationId());
		$calculationResult = $this->calculateDelivery($order, $request->getId());

		if (!$calculationResult->isSuccess())
		{
			throw new Main\SystemException(implode(', ', $calculationResult->getErrorMessages()));
		}

		$store = $deliveryIntegration->getDetailPickup($request->getStoreId());

		$result = $this->collectPickupDetail($calculationResult, $store, $request->getLocationId());

		$this->sendResponse($result);
	}

	protected function collectPickupDetail(EntityReference\Delivery\CalculationResult $calculationResult, array $store, int $locationId = null) : array
	{
		return [
			'id' => Main\Web\Json::encode([
				'deliveryId' => $calculationResult->getDeliveryId(),
				'storeId' => (string)$store['ID'],
				'locationId' => $locationId,
				'zip' => $store['ZIP'],
			]),
			'label'     => $store['TITLE'],
			'provider'  => $store['PROVIDER'] ?? 'pickpoint', //todo
			'address'   => $store['ADDRESS'],
			'deliveryDate' => $calculationResult->getDateFrom()->getTimestamp(),
			'amount'    => (string)$calculationResult->getPrice(),
			'info' => [
				'contacts' => [$store['PHONE']],
				'tripDescription' => $store['DESCRIPTION'] . PHP_EOL . $store['SCHEDULE'],
			],
			'coordinates' =>  [
				'latitude' => (float)$store['GPS_N'],
				'longitude' => (float)$store['GPS_S'],
			],
		];
	}

	protected function clearCalculatable(EntityReference\Order $order) : void
	{
		$order->clearCalculatable();
	}

	protected function collectPickupOption(int $deliveryId, array $store, int $locationId = null) : array
	{
		return [
			'id' => Main\Web\Json::encode([
				'deliveryId' => $deliveryId,
				'storeId' => (string)$store['ID'],
				'locationId' => $locationId,
				'zip' => $store['ZIP'],
			]),
			'label'     => $store['TITLE'],
			'provider'  => $store['PROVIDER'] ?? 'pickpoint', //todo
			'address'   => $store['ADDRESS'],
			'coordinates' =>  [
				'latitude' => (float)$store['GPS_N'],
				'longitude' => (float)$store['GPS_S'],
			],
		];
	}

	protected function couponAction() : void
	{
		$order = $this->getOrder();
		//$this->fillBasket($order);
		$this->fillCoupon($order);
	}

	protected function orderAcceptAction() : void
	{
		/** @var TradingAction\Incoming\OrderAccept $request */
		$request = $this->makeRequest(TradingAction\Incoming\OrderAccept::class);

		$userId = $this->createUser($request);
		$order = $this->getOrder($userId);

		$order->initialize();

		$this->fillPlatform($order);
		$this->fillPersonType($order);
		$this->fillStatus($order);
		$this->fillProperties($order, $request);

		if ($request->getDeliveryType() === EntitySale\Delivery::PICKUP_TYPE)
		{
			$this->fillPickupLocation($order, $request->getPickup()->getLocationId());
			$this->fillBasket($order, $request->getItems());
			$this->fillPickup($order, $request->getPickup());
		}
		else
		{
			$this->fillLocation($order, $request->getAddress());
			$this->fillBasket($order, $request->getItems());
			$this->fillDelivery($order, $request->getDelivery(), $request->getAddress());
		}

		$this->fillPaySystem($order, $request);
		$this->fillRelatedProperties($order);

		$order->finalize();

		$this->check($order, $request);

		$saveOrderData = $this->addOrder($order);

		$redirect = $this->getRedirectUrl($saveOrderData['ID'], $saveOrderData['PAY_SYSTEM_ID']);

		$result = [
			'externalId' => $saveOrderData['PAYMENT_ID'],
			'paySystemId' => $saveOrderData['PAY_SYSTEM_ID'],
			'redirect' => $redirect,
		];

		$this->sendResponse($result);
	}

	protected function check(EntityReference\Order $order, TradingAction\Incoming\OrderAccept $request) : void
	{
		$this->checkBasket($order, $request->getItems());
		$this->checkDelivery($order, $request);
		$this->checkTotal($order, $request);
	}

	protected function checkTotal(EntityReference\Order $order, TradingAction\Incoming\OrderAccept $request) : void
	{
		$orderPrice = $order->getOrderPrice();
		$requestPrice = $request->getOrderAmount();

		if (Pay\Data\Price::round($orderPrice) !== Pay\Data\Price::round($requestPrice))
		{
			$message = $this->getLang('ORDER_PRICE_NOT_MATCH', [
				'#REQUEST_PRICE#' => $requestPrice,
				'#ORDER_PRICE#' => $orderPrice
			]);
			throw new Main\SystemException($message);
		}
	}

	protected function checkDelivery(EntityReference\Order $order, TradingAction\Incoming\OrderAccept $request) : void
	{
		$delivery = $request->getDeliveryType() === EntitySale\Delivery::PICKUP_TYPE ? $request->getPickup() : $request->getDelivery();
		$shipmentPrice = $order->getShipmentPrice($delivery->getId());

		Assert::notNull($shipmentPrice, '$shipmentPrice');

		if (Pay\Data\Price::round($shipmentPrice) !== Pay\Data\Price::round($delivery->getAmount()))
		{
			$priceResult = $order->setShipmentPrice($delivery->getId(), $delivery->getAmount());
			Exceptions\Facade::handleResult($priceResult);
		}
	}

	protected function checkBasket(EntityReference\Order $order, TradingAction\Incoming\Items $items) : void
	{
		/** @var TradingAction\Incoming\Item $item */
		foreach ($items as $index => $item)
		{
			$basketCode = $this->basketMap[$index] ?? null;

			Assert::notNull($basketCode, '$basketMap[$index]');

			$basketResult = $order->getBasketItemData($basketCode);
			$basketData = $basketResult->getData();

			Exceptions\Facade::handleResult($basketResult);

			Assert::notNull($basketData['QUANTITY'], '$basketData[QUANTITY]');
			Assert::notNull($basketData['PRICE'], '$basketData[PRICE]');

			$productPrice = $basketData['PRICE'] * $basketData['QUANTITY'];

			if (Pay\Data\Price::round($productPrice) !== Pay\Data\Price::round($item->getAmount()))
			{
				$priceResult = $order->setBasketItemPrice($basketCode, $item->getAmount());
				Exceptions\Facade::handleResult($priceResult);
			}

			if (Pay\Data\Quantity::round($basketData['QUANTITY']) !== Pay\Data\Quantity::round($item->getQuantity()))
			{
				$quantityResult = $order->setBasketItemQuantity($basketCode, $item->getQuantity());
				Exceptions\Facade::handleResult($quantityResult);
			}
		}
	}

	protected function fillPickup(EntityReference\Order $order, TradingAction\Incoming\OrderAccept\Pickup $pickup) : void
	{
		$deliveryId = $pickup->getId();
		$price = $pickup->getAmount();

		if ((string)$deliveryId === '')
		{
			$deliveryId = $this->environment->getDelivery()->getEmptyDeliveryId();
		}

		$order->createShipment($deliveryId, $price);
		$order->recalculateShipment();
		$order->fillPropertiesPickup($pickup->getStoreId(), $pickup->getAddress());
	}

	protected function wakeUpBasket(EntityReference\Order $order, TradingAction\Incoming\Product $request) : void
	{
		$mode = $request->getMode();

		if ($mode === Pay\Injection\Behavior\Registry::ELEMENT)
		{
			$productId = $request->getProductId();

			$order->initEmptyBasket();

			$basketData = $this->getProductBasketData($productId);
			$quantity = $basketData['RATIO'] ?? 1;
			$addResult = $order->addProduct($productId, $quantity, $basketData);
		}
		elseif (
			$mode === Pay\Injection\Behavior\Registry::BASKET
			|| $mode === Pay\Injection\Behavior\Registry::ORDER
		)
		{
			$addResult = $order->initUserBasket();
		}
		else
		{
			throw new Main\ArgumentException('not found mode');
		}

		Exceptions\Facade::handleResult($addResult);
	}

	protected function getProductBasketData(int $productId) : array
	{
		$environmentProduct = $this->environment->getProduct();
		$fewData = $environmentProduct->getBasketData([$productId]);
		$itemData = $fewData[$productId] ?? null;

		Assert::notNull($itemData, '$enviromentProduct->getBasketData()');

		if (isset($itemData['ERROR']))
		{
			throw new Main\SystemException($itemData['ERROR']);
		}

		return $itemData;
	}

	protected function fillBasket(EntityReference\Order $order, TradingAction\Incoming\Items $products) : void
	{
		[$exists, $new] = $this->splitBasketAlreadyExists($products);

		if (!empty($exists))
		{
			$order->initUserBasket();

			[$notFound, $needDelete] = $this->syncBasketExistProducts($order, $exists);

			$this->deleteBasketProducts($order, $needDelete);
			$this->addBasketNewProducts($order, $notFound);
		}
		else
		{
			$order->initEmptyBasket();
		}

		$this->addBasketNewProducts($order, $new);
	}

	protected function splitBasketAlreadyExists(TradingAction\Incoming\Items $products) : array
	{
		$exists = [];
		$new = [];

		/** @var TradingAction\Incoming\Item $product */
		foreach ($products as $index => $product)
		{
			if ((int)$product->getBasketId() > 0)
			{
				$exists[$index] = $product;
			}
			else
			{
				$new[$index] = $product;
			}
		}

		return [$exists, $new];
	}

	protected function syncBasketExistProducts(EntityReference\Order $order, array $products) : array
	{
		$productCodes = array_map(static function(TradingAction\Incoming\Item $item) { return $item->getBasketId(); }, $products);
		$existsCodes = $order->getOrderableItems();
		$existsMap = array_flip($existsCodes);
		$notFound = [];
		$needDelete = array_diff($existsCodes, $productCodes);

		/** @var TradingAction\Incoming\Item $product */
		foreach ($products as $index => $product)
		{
			$basketCode = $product->getBasketId();

			if (!isset($existsMap[$basketCode]))
			{
				$notFound[$index] = $product;
				continue;
			}

			$quantityResult = $order->setBasketItemQuantity($basketCode, $product->getQuantity());

			Exceptions\Facade::handleResult($quantityResult);

			$this->basketMap[$index] = $basketCode;
		}

		return [$notFound, $needDelete];
	}

	protected function deleteBasketProducts(EntityReference\Order $order, array $basketCodes) : void
	{
		foreach ($basketCodes as $basketCode)
		{
			$result = $order->deleteBasketItem($basketCode);
			Exceptions\Facade::handleResult($result);
		}
	}

	protected function addBasketNewProducts(EntityReference\Order $order, array $products) : void
	{
		/** @var TradingAction\Incoming\Item $product */
		foreach ($products as $index => $product)
		{
			$productId = $product->getId();
			$quantity = $product->getQuantity();
			$data = [
				'PROPS' => $product->getProps(),
			];

			$addResult = $order->addProduct($productId, $quantity, $data);
			$addData = $addResult->getData();

			Exceptions\Facade::handleResult($addResult);
			Assert::notNull($addData['BASKET_CODE'], '$addData[BASKET_CODE]');

			$this->basketMap[$index] = $addData['BASKET_CODE'];
		}
	}

	protected function fillCoupon(EntityReference\Order $order) : void
	{
		/** @var \YandexPay\Pay\Trading\Action\Request\Coupon $requestCoupon */
		$requestCoupon = $this->getRequestCoupon();
		$coupon = $requestCoupon->getCoupon();// ?? 'SL-HO25W-JD3ASTQ';

		if ($coupon !== null)
		{
			$couponResult = $order->applyCoupon($coupon);
			Exceptions\Facade::handleResult($couponResult);
		}
	}

	protected function addOrder(EntityReference\Order $order) : array
	{
		global $USER;

		$externalId = $order->getId();
		$saveResult = $order->add($externalId);

		Exceptions\Facade::handleResult($saveResult);

		$saveData = $saveResult->getData();

		if (!isset($saveData['ID']))
		{
			$errorMessage = $this->getLang('ORDER_ACCEPT_SAVE_RESULT_ID_NOT_SET');
			throw new Main\SystemException($errorMessage);
		}

		if (!$USER->IsAuthorized())
		{
			if (!is_array($_SESSION['SALE_ORDER_ID']))
			{
				$_SESSION['SALE_ORDER_ID'] = [];
			}

			$_SESSION['SALE_ORDER_ID'][] = $saveData['INTERNAL_ID'];
		}

		return $saveData;
	}

	protected function getRedirectUrl(string $orderId, int $paySystemId) : ?string
	{
		$result = null;

		$options = $this->options;
		$isCashPaySystem = ($options->getPaymentCash() === $paySystemId);
		$url = Utils\Url::absolutizePath($this->options->getSuccessUrl()) . '?ORDER_ID=' . $orderId;

		if ($isCashPaySystem)
		{
			$result = $url;
		}
		else
		{
			unset($_SESSION['yabackurl'], $_SESSION['yabehaviorbackurl']);
			$_SESSION['yabehaviorbackurl'] = $url;
		}

		return $result;
	}

	protected function fillPaySystem(EntityReference\Order $order, TradingAction\Incoming\OrderAccept $request) : void
	{
		$paySystemId = $request->getPaySystemId();
		$price = $request->getOrderAmount();

		if ($paySystemId > 0)
		{
			$order->createPayment($paySystemId);
		}
	}

	protected function fillDelivery(
		EntityReference\Order $order,
		TradingAction\Incoming\OrderAccept\Delivery $delivery,
		TradingAction\Incoming\Address $address
	) : void
	{
		$deliveryId = $delivery->getId();

		if ((string)$deliveryId === '')
		{
			$deliveryId = $this->environment->getDelivery()->getEmptyDeliveryId();
		}

		$order->createShipment($deliveryId);
		$order->recalculateShipment();
		$order->fillPropertiesCourier($address->getMeaningfulAddress(), $address->getMeaningfulZip());
	}

	protected function fillStatus(EntityReference\Order $order) : void
	{
		$statusResult = $order->setStatus(Sale\OrderStatus::getInitialStatus());

		Exceptions\Facade::handleResult($statusResult);
	}

	protected function searchUser() : ?int
	{
		global $USER;

		return $USER->IsAuthorized() ? (int)$USER->GetID() : null;
	}

	protected function createUser(TradingAction\Incoming\OrderAccept $request) : int
	{
		global $USER;

		if ($USER->IsAuthorized())
		{
			return (int)$USER->GetID();
		}

		$userData = $request->getUser();

		$user = $this->environment->getUserRegistry()->getUser([
			'EMAIL' => $userData->getEmail(),
			'PHONE' => $userData->getPhone(),
			'FIRST_NAME' => $userData->getFirstName(),
			'LAST_NAME' => $userData->getLastName(),
			'SECOND_NAME' => $userData->getSecondName(),
			'SITE_ID' => $this->setup->getSiteId(),
		]);

		$userId = $user->getId(); //todo allowAppendOrder options

		if ($userId !== null)
		{
			return $userId;
		}

		/** @var Main\ORM\Data\AddResult $installResult */
		$installResult = $user->install();

		Exceptions\Facade::handleResult($installResult);

		$userId = $installResult->getId();
		$USER->Authorize($userId);

		return $userId;
	}

	protected function getOrder(int $userId = null) : EntityReference\Order
	{
		return $this->environment->getOrderRegistry()->createOrder(
			$this->setup->getSiteId(),
			$userId,
			$this->request->get('currency')
		);
	}

	protected function fillLocation(EntityReference\Order $order, TradingAction\Incoming\Address $address) : void
	{
		$locationService = $this->environment->getLocation();
		$locationId = $locationService->getLocation($address->getFields());
		$meaningfulValues = $locationService->getMeaningfulValues($locationId);

		$this->setLocation($order, $locationId);

		if (!empty($meaningfulValues))
		{
			$this->setMeaningfulPropertyValues($order, $meaningfulValues);
		}
	}

	protected function fillPickupLocation(EntityReference\Order $order, int $locationId) : void
	{
		$locationService = $this->environment->getLocation();
		$meaningfulValues = $locationService->getMeaningfulValues($locationId);

		$this->setLocation($order, $locationId);

		if (!empty($meaningfulValues))
		{
			$this->setMeaningfulPropertyValues($order, $meaningfulValues);
		}
	}

	protected function setLocation(EntityReference\Order $order, $locationId) : void
	{
		$orderResult = $order->setLocation($locationId);

		Exceptions\Facade::handleResult($orderResult);
	}

	protected function fillProperties(EntityReference\Order $order, TradingAction\Incoming\OrderAccept $request) : void
	{
		$this->fillBuyerProperties($order, $request);

		$deliveryType = $request->getDeliveryType();

		if ($deliveryType !== EntitySale\Delivery::PICKUP_TYPE)
		{
			$this->fillAddress($order, $request);
			$this->fillComment($order, $request);
		}
	}

	protected function fillRelatedProperties(EntityReference\Order $order) : void
	{
		if (!empty($this->relatedProperties))
		{
			$order->fillProperties($this->relatedProperties);

			$this->filledProperties += $this->relatedProperties;
			$this->relatedProperties = [];
		}
	}

	protected function fillComment(EntityReference\Order $order, TradingAction\Incoming\OrderAccept $request): void
	{
		$address = $request->getAddress();
		$comment = $address->getComment();

		if ((string)$comment !== '')
		{
			$order->setComment($comment);
		}
	}

	protected function fillBuyerProperties(EntityReference\Order $order, TradingAction\Incoming\OrderAccept $request) : void
	{
		$buyer = $request->getUser();
		$values = $buyer->getMeaningfulValues();
		$this->setMeaningfulPropertyValues($order, $values);
	}

	protected function fillAddress(EntityReference\Order $order, TradingAction\Incoming\OrderAccept $request) : void
	{
		$address = $request->getAddress();
		$propertyValues = $this->getAddressProperties($address);
		$this->setMeaningfulPropertyValues($order, $propertyValues);
	}

	protected function fillPersonType(EntityReference\Order $order) : void
	{
		Assert::notNull($this->setup->getPersonTypeId(), 'personal type');

		$personTypeResult = $order->setPersonType($this->setup->getPersonTypeId());

		Exceptions\Facade::handleResult($personTypeResult);
	}

	protected function fillPlatform(EntityReference\Order $order) : void
	{
		$platform = $this->environment->getPlatform();
		$order->fillTradingSetup($platform);
	}

	protected function setMeaningfulPropertyValues(EntityReference\Order $order, $values) : void
	{
		$formattedValues = $values;
		$propertyValues = $this->combineMeaningfulPropertyValues($formattedValues);

		if (!empty($propertyValues))
		{
			$fillResult = $order->fillProperties($propertyValues);
			Exceptions\Facade::handleResult($fillResult);

			$fillData = $fillResult->getData();

			if (isset($fillData['FILLED']))
			{
				$filledMap = array_fill_keys((array)$fillData['FILLED'], true);

				if (isset($this->filledProperties))
				{
					$this->filledProperties += array_intersect_key($propertyValues, $filledMap);
				}

				if (isset($this->relatedProperties))
				{
					$this->relatedProperties += array_diff_key($propertyValues, $filledMap);
				}
			}
		}
	}

	protected function combineMeaningfulPropertyValues($values) : array
	{
		$options = $this->options;
		$propertyValues = [];

		foreach ($values as $name => $value)
		{
			$propertyId = (string)$options->getProperty($name);

			if ($propertyId === '') { continue; }

			if (!isset($propertyValues[$propertyId]))
			{
				$propertyValues[$propertyId] = $value;
			}
			else
			{
				if (!is_array($propertyValues[$propertyId]))
				{
					$propertyValues[$propertyId] = [
						$propertyValues[$propertyId],
					];
				}

				if (is_array($value))
				{
					$propertyValues[$propertyId] = array_merge($propertyValues[$propertyId], $value);
				}
				else
				{
					$propertyValues[$propertyId][] = $value;
				}
			}
		}

		return $propertyValues;
	}

	protected function getAddressProperties(TradingAction\Incoming\Address $address) : array
	{
		return [
			'ZIP' => $address->getMeaningfulZip(),
			'CITY' => $address->getMeaningfulCity(),
			'ADDRESS' => $address->getMeaningfulAddress(),
			'LAT' => $address->getLat(),
			'LON' => $address->getLon(),
		];
	}

	protected function getRequestCoupon() : \YandexPay\Pay\Reference\Common\Model
	{
		$coupon = $this->request->get('coupon');

		return TradingAction\Request\Coupon::initialize(['coupon' => $coupon]);
	}

	protected function loadModules(): void
	{
		$requiredModules = $this->getRequiredModules();

		foreach ($requiredModules as $requiredModule)
		{
			if (!Main\Loader::includeModule($requiredModule))
			{
				$message = $this->getLang('MODULE_NOT_INSTALLED', [ '#MODULE_ID#' => $requiredModule ]);

				throw new Main\SystemException($message);
			}
		}
	}

	protected function getRequiredModules(): array
	{
		return [
			'yandexpay.pay',
			'sale',
		];
	}

	protected function bootstrap() : void
	{
		$this->environment = EntityRegistry::getEnvironment();

		$this->setup = $this->loadSetup();
		$this->setup->fill();

		$this->options = $this->setup->wakeupOptions();
	}

	protected function loadSetup() : TradingSetup\Model
	{
		return TradingSetup\Model::wakeUp(['ID' => $this->request->get('setupId')]);
	}

	protected function getLang(string $code, $replace = null, $language = null): string
	{
		return Main\Localization\Loc::getMessage('YANDEX_PAY_TRADING_CART_' . $code, $replace, $language);
	}
}