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 : |
<?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; } }