1247 lines
44 KiB
PHP
1247 lines
44 KiB
PHP
<?php
|
||
|
||
/**
|
||
* Plugin Name: Монета | PayAnyWay
|
||
* Description: PayAnyWay - Приём платежей для бизнеса на сайтах, в соцсетях и приложениях
|
||
* Version: 1.5.0
|
||
* Author: PayAnyWay
|
||
* Author URI: https://payanyway.ru
|
||
* Requires Plugins: woocommerce
|
||
*/
|
||
|
||
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
|
||
use Automattic\WooCommerce\Blocks\Package;
|
||
use Automattic\WooCommerce\Enums\OrderStatus;
|
||
use Automattic\WooCommerce\Enums\ProductTaxStatus;
|
||
use Automattic\Jetpack\Constants;
|
||
|
||
add_action('plugins_loaded', 'woocommerce_payanyway', 0);
|
||
add_action('plugins_loaded', 'start_session', 1);
|
||
|
||
function woocommerce_payanyway() {
|
||
if (!class_exists('WC_Payment_Gateway'))
|
||
return; // if the WC payment gateway class is not available, do nothing
|
||
if (class_exists('WC_Payanyway'))
|
||
return;
|
||
|
||
class WC_Payanyway extends WC_Payment_Gateway {
|
||
const MODULE_VERSION = '1.5.0';
|
||
const MODULE_NAME = 'payanyway';
|
||
|
||
const PROD_ENDPOINT = 'https://www.payanyway.ru/assistant.htm';
|
||
const DEMO_ENDPOINT = 'https://demo.moneta.ru/assistant.htm';
|
||
|
||
const PROD_IFRAME_ENDPOINT = 'https://www.payanyway.ru/assistant.widget';
|
||
const DEMO_IFRAME_ENDPOINT = 'https://demo.moneta.ru/assistant.widget';
|
||
|
||
const FAIL_RESPONSE = 'FAIL';
|
||
const XML_CONTENT_TYPE = 'application/xml';
|
||
const JSON_CONTENT_TYPE = 'application/json';
|
||
const TEXT_CONTENT_TYPE = 'text/plain; charset=UTF-8';
|
||
const TRANSACTION_ID_STRING_DELIMITER = '|';
|
||
const DESCRIPTION_MAX_LENGTH = 500;
|
||
|
||
const CLIENT_NAME_MAX_LENGTH = 256;
|
||
const ITEM_NAME_MAX_LENGTH = 128;
|
||
const SHIPPING_PAYMENT_OBJECT = 'service';
|
||
const DEFAULT_SHIPPING_NAME = 'Доставка';
|
||
const DEFAULT_MEASURE = 'unit';
|
||
|
||
public $id;
|
||
public $has_fields;
|
||
public $MNT_ID;
|
||
public $MNT_DATAINTEGRITY_CODE;
|
||
public $MNT_TEST_MODE;
|
||
public $title;
|
||
public $iniframe;
|
||
public $debug;
|
||
public $description;
|
||
public $instructions;
|
||
public $demo_mode;
|
||
|
||
private $logger = null;
|
||
|
||
public function __construct() {
|
||
$this->id = 'payanyway';
|
||
$this->method_title = 'Монета | PayAnyWay';
|
||
$this->method_description = 'PayAnyWay - универсальный платежный агрегатор для интернет-магазинов и сервисов';
|
||
$this->icon = plugin_dir_url(dirname(__FILE__)) . 'payanyway/assets/images/payanyway.png';
|
||
$this->has_fields = false;
|
||
|
||
// Load the settings
|
||
$this->init_form_fields();
|
||
$this->init_settings();
|
||
|
||
// Define user set variables
|
||
$this->MNT_ID = (int)$this->get_option('MNT_ID');
|
||
$this->MNT_DATAINTEGRITY_CODE = $this->get_option('MNT_DATAINTEGRITY_CODE');
|
||
$this->MNT_TEST_MODE = 0;
|
||
$this->demo_mode = $this->get_option('demo_mode');
|
||
$this->title = $this->get_option('title');
|
||
$this->iniframe = $this->get_option('iniframe');
|
||
$this->debug = $this->get_option('debug');
|
||
$this->description = $this->get_option('description');
|
||
$this->instructions = $this->get_option('instructions');
|
||
|
||
// Logs
|
||
if ($this->debug === 'yes') {
|
||
$this->logger = wc_get_logger();
|
||
}
|
||
|
||
// Actions
|
||
add_action('woocommerce_receipt_payanyway', array($this, 'receipt_page'));
|
||
|
||
// Save options
|
||
add_action('woocommerce_update_options_payment_gateways_payanyway', array($this, 'process_admin_options'));
|
||
|
||
// Payment listener/API hook
|
||
add_action('woocommerce_api_wc_payanyway', array($this, 'check_assistant_response'));
|
||
|
||
if (!$this->is_valid_for_use()) {
|
||
$this->enabled = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check if this gateway is enabled and available in the user's country
|
||
*/
|
||
function is_valid_for_use() {
|
||
if (!in_array(get_option('woocommerce_currency'), array('RUB'))) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Admin Panel Options
|
||
* - Options for bits like 'title' and availability on a country-by-country basis
|
||
*
|
||
* @since 0.1
|
||
**/
|
||
public function admin_options() {
|
||
if ($this->is_valid_for_use()) {
|
||
$this->generate_settings_html();
|
||
} else {
|
||
?>
|
||
<div class="inline error">
|
||
<p>
|
||
<strong>
|
||
<?php _e('Шлюз отключен', 'woocommerce_gateway_payanyway'); ?>
|
||
</strong>:
|
||
<?php _e('PayAnyWay не поддерживает валюты вашего магазина.', 'woocommerce_gateway_payanyway'); ?>
|
||
</p>
|
||
</div>
|
||
<?php
|
||
}
|
||
} // End admin_options()
|
||
|
||
/**
|
||
* Initialise Gateway Settings Form Fields
|
||
*
|
||
* @access public
|
||
* @return void
|
||
*/
|
||
function init_form_fields() {
|
||
$this->form_fields = include 'includes/settings/settings-paw.php';
|
||
}
|
||
|
||
/**
|
||
* Дополнительная информация в форме выбора способа оплаты
|
||
**/
|
||
function payment_fields() {
|
||
echo $this->get_description();
|
||
if (isset($_GET['pay_for_order']) && !empty($_GET['key'])) {
|
||
$order = wc_get_order(wc_get_order_id_by_order_key(wc_clean($_GET['key'])));
|
||
$this->receipt_page($order->get_id());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Process the payment and return the result
|
||
**/
|
||
function process_payment($order_id) {
|
||
/** @var WC_Order $order */
|
||
$order = new WC_Order($order_id);
|
||
return array(
|
||
'result' => 'success',
|
||
'redirect' => add_query_arg(
|
||
'order-pay',
|
||
$order->get_id(),
|
||
add_query_arg(
|
||
'key',
|
||
$order->get_order_key(),
|
||
$order->get_checkout_payment_url()
|
||
)
|
||
),
|
||
);
|
||
}
|
||
|
||
private function getPaymentObject($productName) {
|
||
if (false !== mb_stripos($productName, 'акциз')) {
|
||
$itemPaymentObject = 'excise';
|
||
} elseif (false !== mb_stripos($productName, 'работ')) {
|
||
$itemPaymentObject = 'job';
|
||
} elseif (false !== mb_stripos($productName, 'услуг')) {
|
||
$itemPaymentObject = 'service';
|
||
} elseif (false !== mb_stripos($productName, 'ставк')) {
|
||
$itemPaymentObject = 'gambling_bet';
|
||
} elseif (false !== mb_stripos($productName, 'выигрыш')) {
|
||
$itemPaymentObject = 'gambling_prize';
|
||
} elseif (false !== mb_stripos($productName, 'лотере')) {
|
||
$itemPaymentObject = 'lottery';
|
||
} elseif (false !== mb_stripos($productName, 'приз')) {
|
||
$itemPaymentObject = 'lottery_prize';
|
||
} elseif (false !== mb_stripos($productName, 'интеллект')) {
|
||
$itemPaymentObject = 'intellectual_activity';
|
||
} elseif (false !== mb_stripos($productName, 'взнос')) {
|
||
$itemPaymentObject = 'payment';
|
||
} elseif (false !== mb_stripos($productName, 'агент')) {
|
||
$itemPaymentObject = 'agent_commission';
|
||
} else {
|
||
$itemPaymentObject = 'commodity';
|
||
}
|
||
|
||
return $itemPaymentObject;
|
||
}
|
||
|
||
/**
|
||
* @param string $data
|
||
* @return string
|
||
*/
|
||
private function getVatTag($data)
|
||
{
|
||
$vatList = array(
|
||
'none' => '1105',
|
||
'vat0' => '1104',
|
||
'vat5' => '1108',
|
||
'vat105' => '1110',
|
||
'vat7' => '1109',
|
||
'vat107' => '1111',
|
||
'vat10' => '1103',
|
||
'vat110' => '1107',
|
||
'vat20' => '1102',
|
||
'vat120' => '1106',
|
||
'vat22' => '1113',
|
||
'vat122' => '1114',
|
||
);
|
||
|
||
return isset($vatList[$data]) ? $vatList[$data] : '1105';
|
||
}
|
||
|
||
/**
|
||
* @param int|null $rate
|
||
* @return string
|
||
*/
|
||
private function convertVatIdByRate($rate)
|
||
{
|
||
if ($rate === null) {
|
||
return 'none';
|
||
}
|
||
|
||
$vatMap = array(
|
||
0 => 'vat0',
|
||
5 => 'vat5',
|
||
7 => 'vat7',
|
||
10 => 'vat10',
|
||
20 => 'vat20',
|
||
22 => 'vat22',
|
||
);
|
||
|
||
return isset($vatMap[$rate]) ? $vatMap[$rate] : 'vat' . $rate;
|
||
}
|
||
|
||
private function getPaymentMethod($productName) {
|
||
if (false !== mb_stripos($productName, 'аванс')) {
|
||
$itemPaymentMethod = 'advance';
|
||
} elseif (false !== mb_stripos($productName, 'кредит')) {
|
||
$itemPaymentMethod = 'credit';
|
||
} else {
|
||
$itemPaymentMethod = 'full_prepayment';
|
||
}
|
||
|
||
return $itemPaymentMethod;
|
||
}
|
||
|
||
/**
|
||
* Форма оплаты
|
||
**/
|
||
function receipt_page($order_id) {
|
||
$order = new WC_Order($order_id);
|
||
|
||
$amount = $this->formatPrice($order->get_total());
|
||
$test_mode = $this->MNT_TEST_MODE;
|
||
$in_iframe = ($this->iniframe === 'yes') ? 1 : 0;
|
||
$currency = get_woocommerce_currency();
|
||
if ($currency === 'RUR') $currency = 'RUB';
|
||
|
||
$transactionId = $this->createTransactionId($order_id);
|
||
$subscriberId = $this->getSubscriberId($order);
|
||
|
||
$signature = md5(
|
||
$this->MNT_ID .
|
||
$transactionId .
|
||
$amount .
|
||
$currency .
|
||
$subscriberId .
|
||
$test_mode .
|
||
$this->MNT_DATAINTEGRITY_CODE
|
||
);
|
||
|
||
$wcKey = (isset($_GET['key'])) ? $_GET['key'] : '';
|
||
|
||
$args = array(
|
||
'MNT_ID' => $this->MNT_ID,
|
||
'MNT_TRANSACTION_ID' => $transactionId,
|
||
'MNT_AMOUNT' => $amount,
|
||
'MNT_CURRENCY_CODE' => $currency,
|
||
'MNT_TEST_MODE' => $test_mode,
|
||
'MNT_DESCRIPTION' => $this->getDescription($order),
|
||
'MNT_SUBSCRIBER_ID' => $subscriberId,
|
||
'MNT_SIGNATURE' => $signature,
|
||
'MNT_SUCCESS_URL' => get_site_url() . "/?wc-api=wc_payanyway&payanyway=success&key={$wcKey}&order_id={$order_id}",
|
||
'MNT_FAIL_URL' => get_site_url() . "/?wc-api=wc_payanyway&payanyway=fail",
|
||
'MNT_RETURN_URL' => get_site_url() . '/checkout/',
|
||
'MNT_CMS' => $this->getCmsModuleVersion(),
|
||
);
|
||
|
||
$form_fields = array();
|
||
foreach ($args as $key => $value) {
|
||
$form_fields[] = '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr($value) . '" />';
|
||
}
|
||
|
||
wp_enqueue_style('paw_css_main', plugin_dir_url(__FILE__) . 'assets/css/paw-main.css', '', '');
|
||
|
||
$demoMode = ($this->demo_mode === 'yes') ? 1 : 0;
|
||
|
||
$this->logger && $this->logger->info('Create payment processed', array(
|
||
'source' => self::MODULE_NAME,
|
||
'form_data' => $args,
|
||
));
|
||
|
||
if ($in_iframe) {
|
||
$assistantEndpoint = $demoMode ? self::DEMO_IFRAME_ENDPOINT : self::PROD_IFRAME_ENDPOINT;
|
||
$form_html = '<br><iframe src="' . $assistantEndpoint . '?' . http_build_query($args, '', '&') . '" id="pawrepliframe" frameborder="0" style="margin-top: 15px; background-color: #f7f8f9; border-radius: 10px; width: 100%; min-width: 320px; height: available; min-height: 550px;></iframe>';
|
||
echo $form_html;
|
||
} else {
|
||
$assistantEndpoint = $demoMode ? self::DEMO_ENDPOINT : self::PROD_ENDPOINT;
|
||
$paymentUrl = $assistantEndpoint . '?' . http_build_query($args);
|
||
|
||
header('Location: ' . $paymentUrl);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check Response
|
||
**/
|
||
function check_assistant_response() {
|
||
$_REQUEST = stripslashes_deep($_REQUEST);
|
||
$orderId = $this->getOrderIdFromTransactionId($_REQUEST['MNT_TRANSACTION_ID']);
|
||
$order = new WC_Order($orderId);
|
||
|
||
$payanyway = isset($_REQUEST['payanyway']) ? $_REQUEST['payanyway'] : null;
|
||
switch ($payanyway) {
|
||
case 'callback':
|
||
@ob_clean();
|
||
|
||
try {
|
||
$callback = $this->getCallbackData($_REQUEST);
|
||
} catch (\Exception $e) {
|
||
$this->logger && $this->logger->error('Exception', array(
|
||
'source' => self::MODULE_NAME,
|
||
'exception_message' => $e,
|
||
));
|
||
|
||
$this->sendResponse(self::FAIL_RESPONSE);
|
||
}
|
||
|
||
$this->validateCallback($callback);
|
||
(isset($callback['MNT_COMMAND'])) && ('CHECK' === $callback['MNT_COMMAND'])
|
||
? $this->handleCheckCallback($order, $callback)
|
||
: $this->handlePayCallback($order, $callback);
|
||
break;
|
||
case 'success':
|
||
echo'nen';
|
||
$this->handleSuccess($order);
|
||
break;
|
||
case 'fail':
|
||
$this->handleFail($order);
|
||
break;
|
||
default:
|
||
$this->sendResponse(self::FAIL_RESPONSE);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @param array<string, mixed> $callback
|
||
* @return void
|
||
* @throws \Exception
|
||
*/
|
||
private function handleCheckCallback($order, array $callback)
|
||
{
|
||
if (!$this->checkSignature($callback, $callback['MNT_SIGNATURE'])) {
|
||
$this->logger && $this->logger->info('Check callback error', array(
|
||
'source' => self::MODULE_NAME,
|
||
'error_message' => 'Неверная подпись',
|
||
));
|
||
|
||
$this->sendResponse(self::FAIL_RESPONSE);
|
||
}
|
||
|
||
list($resultCode, $description) = $this->determineCheckResult($order, $callback);
|
||
$responseData = $this->buildBaseResponseData($order, $callback, $resultCode, $description);
|
||
|
||
$itemsInfo = $this->getItemsInfo($order);
|
||
|
||
$responseData['client'] = $this->getClientInfo($order);
|
||
$responseData['items'] = $this->getCheckItems($itemsInfo);
|
||
|
||
$jsonResponse = $this->buildJsonResponse($responseData);
|
||
|
||
$this->logger && $this->logger->info('Check callback processed', array(
|
||
'source' => self::MODULE_NAME,
|
||
'result' => json_decode($jsonResponse['data'], false),
|
||
));
|
||
|
||
$this->sendResponse($jsonResponse);
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @param array<string, mixed> $callback
|
||
* @return void
|
||
* @throws \Exception
|
||
*/
|
||
private function handlePayCallback($order, array $callback)
|
||
{
|
||
$orderTotal = $order->get_total();
|
||
$currencyCode = $order->get_currency();
|
||
if ($currencyCode === 'RUR') {
|
||
$currencyCode = 'RUB';
|
||
}
|
||
|
||
$shopData = array(
|
||
'MNT_ID' => $this->MNT_ID,
|
||
'MNT_TRANSACTION_ID' => $callback['MNT_TRANSACTION_ID'],
|
||
'MNT_OPERATION_ID' => $callback['MNT_OPERATION_ID'],
|
||
'MNT_AMOUNT' => $this->formatPrice($orderTotal),
|
||
'MNT_CURRENCY_CODE' => $currencyCode,
|
||
'MNT_SUBSCRIBER_ID' => $callback['MNT_SUBSCRIBER_ID'],
|
||
'MNT_TEST_MODE' => $callback['MNT_TEST_MODE'],
|
||
);
|
||
|
||
if (!$this->checkSignature($shopData, $callback['MNT_SIGNATURE'])) {
|
||
$this->logger && $this->logger->info('Pay callback error', array(
|
||
'source' => self::MODULE_NAME,
|
||
'error_message' => 'Неверная подпись',
|
||
));
|
||
|
||
$this->sendResponse(self::FAIL_RESPONSE);
|
||
}
|
||
|
||
$description = $this->isPayedOrder($order) ? 'Order already paid' : 'Order success paid';
|
||
if (!$this->isPayedOrder($order)) {
|
||
$order->add_order_note(__('Платеж успешно завершен', 'woocommerce_gateway_payanyway'));
|
||
$order->update_status(OrderStatus::COMPLETED, __('Платеж успешно оплачен', 'woocommerce_gateway_payanyway'));
|
||
$order->payment_complete();
|
||
}
|
||
|
||
$resultCode = 200;
|
||
$responseData = $this->buildBaseResponseData($order, $callback, $resultCode, $description);
|
||
|
||
$itemsInfo = $this->getItemsInfo($order);
|
||
|
||
$payInventoryJson = $this->getPayInventoryJson($itemsInfo);
|
||
$responseData['inventory'] = $payInventoryJson ?: null;
|
||
$clientInfo = $this->getClientInfo($order);
|
||
$responseData['client'] = $clientInfo
|
||
? json_encode([$clientInfo], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||
: null;
|
||
|
||
$this->logger && $this->logger->info('Pay callback processed', array(
|
||
'source' => self::MODULE_NAME,
|
||
'result' => $responseData,
|
||
));
|
||
|
||
$this->sendResponse($this->buildXmlResponse($responseData));
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return void
|
||
*/
|
||
private function handleSuccess($order)
|
||
{
|
||
global $woocommerce;
|
||
|
||
if (!$this->isPayedOrder($order)) {
|
||
$order->update_status(OrderStatus::PROCESSING, __('Обработка платежа', 'woocommerce_gateway_payanyway'));
|
||
}
|
||
|
||
$woocommerce->cart->empty_cart();
|
||
wp_redirect($this->get_return_url($order));
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return void
|
||
*/
|
||
private function handleFail($order)
|
||
{
|
||
if (!$this->isPayedOrder($order)) {
|
||
$order->update_status(OrderStatus::FAILED, __('Платеж не оплачен', 'woocommerce_gateway_payanyway'));
|
||
wp_redirect($order->get_cancel_order_url());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return bool
|
||
*/
|
||
private function isPayedOrder($order)
|
||
{
|
||
return ($order->get_status() === OrderStatus::COMPLETED);
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $data
|
||
* @param string $callbackSignature
|
||
* @return bool
|
||
*/
|
||
private function checkSignature(array $data, $callbackSignature)
|
||
{
|
||
$baseFields = array(
|
||
'MNT_ID',
|
||
'MNT_TRANSACTION_ID',
|
||
'MNT_OPERATION_ID',
|
||
'MNT_AMOUNT',
|
||
'MNT_CURRENCY_CODE',
|
||
'MNT_SUBSCRIBER_ID',
|
||
'MNT_TEST_MODE',
|
||
);
|
||
|
||
$fields = isset($data['MNT_COMMAND']) && ('CHECK' === $data['MNT_COMMAND'])
|
||
? array_merge(['MNT_COMMAND'], $baseFields)
|
||
: $baseFields;
|
||
|
||
$signatureString = array_reduce($fields, function ($carry, $field) use ($data) {
|
||
return $carry . $data[$field];
|
||
}, '') . $this->MNT_DATAINTEGRITY_CODE;
|
||
|
||
return hash_equals(md5($signatureString), $callbackSignature);
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @param array<string, mixed> $callback
|
||
* @return array<int|string>
|
||
* @throws \Exception
|
||
*/
|
||
private function determineCheckResult($order, array $callback)
|
||
{
|
||
$orderStatus = $order->get_status();
|
||
if (empty($callback['MNT_AMOUNT'])) {
|
||
return array(100, "Order status is {$orderStatus}");
|
||
}
|
||
|
||
if ($orderStatus === OrderStatus::COMPLETED) {
|
||
return array(200, 'Order Paid');
|
||
}
|
||
|
||
if (in_array($orderStatus, array(OrderStatus::CANCELLED, OrderStatus::FAILED, OrderStatus::REFUNDED), true)) {
|
||
return array(500, "Order status is {$orderStatus}");
|
||
}
|
||
|
||
return array(402, 'Order created, but not paid');
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @param array $callbackData
|
||
* @param int $resultCode
|
||
* @param string $description
|
||
* @return array<string, mixed>
|
||
* @throws \Exception
|
||
*/
|
||
private function buildBaseResponseData($order, array $callbackData, $resultCode, $description)
|
||
{
|
||
$orderTotal = $order->get_total();
|
||
|
||
$data = array(
|
||
'MNT_ID' => $this->MNT_ID,
|
||
'MNT_TRANSACTION_ID' => $callbackData['MNT_TRANSACTION_ID'],
|
||
'MNT_RESULT_CODE' => $resultCode,
|
||
'MNT_DESCRIPTION' => $this->validateString($description, self::DESCRIPTION_MAX_LENGTH),
|
||
'MNT_AMOUNT' => $this->formatPrice($orderTotal),
|
||
'MNT_CURRENCY_CODE' => $callbackData['MNT_CURRENCY_CODE'],
|
||
);
|
||
|
||
$signatureSource = $resultCode . $this->MNT_ID . $callbackData['MNT_TRANSACTION_ID'] . $this->MNT_DATAINTEGRITY_CODE;
|
||
$data['MNT_SIGNATURE'] = md5($signatureSource);
|
||
$data['MNT_CMS'] = $this->getCmsModuleVersion();
|
||
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return array|null
|
||
*/
|
||
private function getItemsInfo($order)
|
||
{
|
||
$items = $order->get_items();
|
||
if (empty($items)) {
|
||
return null;
|
||
}
|
||
|
||
$itemsInfo = array();
|
||
foreach ($items as $item) {
|
||
/** @var WC_Product $product */
|
||
$product = $item->get_product();
|
||
|
||
$name = (isset($item['name'])) ? $item['name'] : $item->get_name();
|
||
$price = wc_get_price_including_tax($product);
|
||
$quantity = (isset($item['item_meta']['_qty'][0])) ? $item['item_meta']['_qty'][0] : $item->get_quantity();
|
||
$rate = $product->is_taxable() ? $this->getItemTaxRate($item) : null;
|
||
|
||
$itemsInfo[] = array(
|
||
'name' => $name,
|
||
'price' => $price,
|
||
'quantity' => $quantity,
|
||
'rate' => $rate,
|
||
);
|
||
}
|
||
|
||
$deliveryInfo = $this->getDeliveryInfo($order);
|
||
if (null !== $deliveryInfo) {
|
||
$itemsInfo[] = $deliveryInfo;
|
||
}
|
||
|
||
return $itemsInfo;
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order_Item $item
|
||
* @return int|null
|
||
*/
|
||
public function getItemTaxRate($item)
|
||
{
|
||
$taxClass = $item->get_product()->get_tax_class();
|
||
if (empty($taxClass)) {
|
||
$taxClass = '';
|
||
}
|
||
|
||
$taxRates = WC_Tax::get_rates($taxClass);
|
||
if (empty($taxRates)) {
|
||
return null;
|
||
}
|
||
|
||
$firstRate = reset($taxRates);
|
||
|
||
return (int)$firstRate['rate'];
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return array|null
|
||
*/
|
||
private function getDeliveryInfo($order)
|
||
{
|
||
$shipping = $order->get_items('shipping');
|
||
if (empty($shipping)) {
|
||
return null;
|
||
}
|
||
|
||
$shippingMethod = method_exists($order, 'get_shipping_method') ? $order->get_shipping_method() : self::DEFAULT_SHIPPING_NAME;
|
||
|
||
$shippingTotal = (float)$order->get_shipping_total();
|
||
$shippingTax = (float)$order->get_shipping_tax();
|
||
$shippingPrice = $shippingTotal + $shippingTax;
|
||
|
||
$shippingRate = (0.0 !== $shippingTotal) ? ($shippingTax / $shippingTotal) * 100 : null;
|
||
|
||
return array(
|
||
'name' => $shippingMethod,
|
||
'price' => $shippingPrice,
|
||
'quantity' => 1,
|
||
'rate' => $shippingRate,
|
||
'isShipping' => true,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return array
|
||
*/
|
||
private function getClientInfo($order)
|
||
{
|
||
$clientInfo = array();
|
||
$name = $this->getClientName($order);
|
||
if (null !== $name) {
|
||
$clientInfo['name'] = $name;
|
||
}
|
||
$email = $this->getClientEmail($order);
|
||
if (null !== $email) {
|
||
$clientInfo['email'] = $email;
|
||
}
|
||
$phoneNumber = $this->getClientPhone($order);
|
||
if (null !== $phoneNumber) {
|
||
$clientInfo['phone'] = $phoneNumber;
|
||
}
|
||
|
||
return $clientInfo;
|
||
}
|
||
|
||
/**
|
||
* @param array|null $itemsInfo
|
||
* @return non-empty-array
|
||
*/
|
||
private function getCheckItems($itemsInfo)
|
||
{
|
||
$checkItems = array();
|
||
foreach ($itemsInfo as $itemInfo) {
|
||
$data = $this->extractItemData($itemInfo);
|
||
|
||
$checkItems[] = array(
|
||
'name' => $data['name'],
|
||
'price' => $this->formatPrice($data['price']),
|
||
'quantity' => $data['quantity'],
|
||
'vat' => $data['rate'] ? $this->convertVatIdByRate($data['rate']) : 'none',
|
||
'measure' => self::DEFAULT_MEASURE,
|
||
'paymentMethod' => $this->getPaymentMethod($data['name']),
|
||
'paymentObject' => $data['isShipping'] ? self::SHIPPING_PAYMENT_OBJECT : $this->getPaymentObject($data['name']),
|
||
);
|
||
}
|
||
|
||
return $checkItems;
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed>|null $itemsInfo
|
||
* @return false|string
|
||
*/
|
||
private function getPayInventoryJson($itemsInfo) {
|
||
if (empty($itemsInfo)) {
|
||
return false;
|
||
}
|
||
|
||
$inventory = array();
|
||
foreach ($itemsInfo as $itemInfo) {
|
||
$data = $this->extractItemData($itemInfo);
|
||
$vat = $this->convertVatIdByRate($data['rate']);
|
||
|
||
$inventory[] = array(
|
||
'name' => $data['name'],
|
||
'price' => $this->formatPrice($data['price']),
|
||
'quantity' => $this->formatQuantity($data['quantity']),
|
||
'vatTag' => $this->getVatTag($vat),
|
||
'pm' => $this->getPaymentMethod($data['name']),
|
||
'po' => $data['isShipping'] ? self::SHIPPING_PAYMENT_OBJECT : $this->getPaymentObject($data['name']),
|
||
'measure' => self::DEFAULT_MEASURE,
|
||
);
|
||
}
|
||
|
||
return json_encode($inventory, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $itemInfo
|
||
* @return array{
|
||
* name: string,
|
||
* price: float|string,
|
||
* quantity: float,
|
||
* rate: int|null,
|
||
* taxIncluded: bool,
|
||
* isShipping: bool
|
||
* }
|
||
*/
|
||
private function extractItemData(array $itemInfo)
|
||
{
|
||
return array(
|
||
'name' => isset($itemInfo['name'])
|
||
? $this->validateString($itemInfo['name'], self::ITEM_NAME_MAX_LENGTH)
|
||
: '',
|
||
'price' => isset($itemInfo['price']) ? (float)$itemInfo['price'] : 0,
|
||
'quantity' => isset($itemInfo['quantity']) ? (float)$itemInfo['quantity'] : 0,
|
||
'rate' => isset($itemInfo['rate']) ? (int)$itemInfo['rate'] : null,
|
||
'isShipping' => isset($itemInfo['isShipping']) && (bool)$itemInfo['isShipping'],
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param array<non-empty-string, mixed> $data
|
||
* @return array
|
||
* @throws \JsonException
|
||
*/
|
||
public function buildJsonResponse(array $data)
|
||
{
|
||
$jsonData = array(
|
||
'id' => $data['MNT_ID'],
|
||
'transactionId' => $data['MNT_TRANSACTION_ID'],
|
||
'amount' => $data['MNT_AMOUNT'],
|
||
'signature' => $data['MNT_SIGNATURE'],
|
||
'resultCode' => $data['MNT_RESULT_CODE'],
|
||
'timestamp' => (new DateTime('now', new DateTimeZone('Europe/Moscow')))->format('c'),
|
||
'description' => $data['MNT_DESCRIPTION'],
|
||
'externalId' => $data['MNT_TRANSACTION_ID'],
|
||
'cms' => $data['MNT_CMS'],
|
||
'receipt' => array(
|
||
'client' => $data['client'],
|
||
'items' => $data['items'],
|
||
),
|
||
);
|
||
|
||
$json = json_encode($jsonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
|
||
return array(
|
||
'data' => $json ?: '',
|
||
'contentType' => self::JSON_CONTENT_TYPE,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $data
|
||
* @return array
|
||
* @throws \Exception
|
||
*/
|
||
private function buildXmlResponse(array $data)
|
||
{
|
||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||
$dom->formatOutput = false;
|
||
|
||
$root = $dom->createElement('MNT_RESPONSE');
|
||
$dom->appendChild($root);
|
||
|
||
$requiredFields = array(
|
||
'MNT_ID',
|
||
'MNT_TRANSACTION_ID',
|
||
'MNT_RESULT_CODE',
|
||
'MNT_DESCRIPTION',
|
||
'MNT_AMOUNT',
|
||
'MNT_CURRENCY_CODE',
|
||
'MNT_SIGNATURE',
|
||
'MNT_CMS',
|
||
);
|
||
|
||
foreach ($requiredFields as $field) {
|
||
if (isset($data[$field]) && ($data[$field] !== '')) {
|
||
$root->appendChild($dom->createElement($field, (string)$data[$field]));
|
||
}
|
||
}
|
||
|
||
$attributes = $dom->createElement('MNT_ATTRIBUTES');
|
||
$root->appendChild($attributes);
|
||
|
||
$attributeMap = array(
|
||
'INVENTORY' => isset($data['inventory']) ? $data['inventory'] : null,
|
||
'CLIENT' => isset($data['client']) ? $data['client'] : null,
|
||
'SNO' => isset($data['sno']) ? $data['sno'] : null,
|
||
'DELIVERY' => isset($data['delivery']) ? $data['delivery'] : null,
|
||
);
|
||
|
||
foreach ($attributeMap as $key => $value) {
|
||
if (!empty($value)) {
|
||
$attrElement = $dom->createElement('ATTRIBUTE');
|
||
$attrElement->appendChild($dom->createElement('KEY', $key));
|
||
$attrElement->appendChild($dom->createElement('VALUE', (string)$value));
|
||
$attributes->appendChild($attrElement);
|
||
}
|
||
}
|
||
|
||
$xml = $dom->saveXML();
|
||
|
||
return array(
|
||
'data' => (false !== $xml) ? $xml : self::FAIL_RESPONSE,
|
||
'contentType' => (false !== $xml) ? self::XML_CONTENT_TYPE : self::TEXT_CONTENT_TYPE,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param float $price
|
||
* @return string
|
||
*/
|
||
private function formatPrice($price)
|
||
{
|
||
return number_format($price, 2, '.', '');
|
||
}
|
||
|
||
/**
|
||
* @param float $quantity
|
||
* @return string
|
||
*/
|
||
private function formatQuantity($quantity)
|
||
{
|
||
$quantityString = number_format($quantity, 3, '.', '');
|
||
return rtrim(rtrim($quantityString, '0'), '.');
|
||
}
|
||
|
||
/**
|
||
* @param string $value
|
||
* @param int $maxLength
|
||
* @return string
|
||
*/
|
||
private function validateString($value, $maxLength)
|
||
{
|
||
$maxLength = max(0, $maxLength);
|
||
$value = $this->sanitizeString($value);
|
||
|
||
if (mb_strlen($value, 'UTF-8') <= $maxLength) {
|
||
return $value;
|
||
}
|
||
|
||
$trimLength = max(0, $maxLength - 3);
|
||
return mb_substr($value, 0, $trimLength, 'UTF-8') . ($maxLength > 3 ? '...' : '');
|
||
}
|
||
|
||
/**
|
||
* @param string $value
|
||
* @return string
|
||
*/
|
||
private function sanitizeString($value)
|
||
{
|
||
$decoded = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||
$cleaned = preg_replace('/[^\p{L}\p{N}\s:.,()_№+-]/u', '', $decoded);
|
||
$cleaned = str_replace(['&', '/', '\\', ';', '%', '#', '"', "'"], '', $cleaned);
|
||
|
||
return trim(preg_replace('/\s+/', ' ', $cleaned));
|
||
}
|
||
|
||
/**
|
||
* @param int $orderId
|
||
* @return string
|
||
*/
|
||
private function createTransactionId($orderId)
|
||
{
|
||
$date = date('YmdHis');
|
||
return $orderId . self::TRANSACTION_ID_STRING_DELIMITER . $date;
|
||
}
|
||
|
||
/**
|
||
* @param string $transactionId
|
||
* @return int|null
|
||
*/
|
||
private function getOrderIdFromTransactionId($transactionId)
|
||
{
|
||
$transactionIdData = explode(self::TRANSACTION_ID_STRING_DELIMITER, $transactionId, 2);
|
||
|
||
return isset($transactionIdData[0]) ? (int)$transactionIdData[0] : null;
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return string
|
||
*/
|
||
private function getDescription($order)
|
||
{
|
||
$description = "Оплата заказа №{$order->get_id()}";
|
||
|
||
$clientName = $this->getClientName($order);
|
||
if (null !== $clientName) {
|
||
$description .= " от {$clientName}";
|
||
}
|
||
|
||
$comment = $order->get_customer_note();
|
||
if ('' !== $comment) {
|
||
$description .= '. Примечание к заказу: ' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
return $this->validateString($description, self::DESCRIPTION_MAX_LENGTH);
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return string
|
||
*/
|
||
private function getSubscriberId($order) {
|
||
$email = $this->getClientEmail($order);
|
||
if (null !== $email) {
|
||
return $email;
|
||
}
|
||
|
||
$phone = $this->getClientPhone($order);
|
||
|
||
return (null !== $phone) ? $phone : '';
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return non-empty-string|null
|
||
*/
|
||
private function getClientName($order)
|
||
{
|
||
$firstname = $order->get_billing_first_name();
|
||
$lastname = $order->get_billing_last_name();
|
||
|
||
$name = trim($firstname . ' ' . $lastname);
|
||
|
||
return ('' !== $name) ? $this->validateString($name, self::CLIENT_NAME_MAX_LENGTH) : null;
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return non-empty-string|null
|
||
*/
|
||
private function getClientEmail($order)
|
||
{
|
||
$email = method_exists($order, 'get_billing_email') ? $order->get_billing_email() : $order->billing_email;
|
||
$email = trim((string)$email);
|
||
|
||
return ('' !== $email) ? filter_var($email, FILTER_SANITIZE_EMAIL) : null;
|
||
}
|
||
|
||
/**
|
||
* @param WC_Order $order
|
||
* @return non-empty-string|null
|
||
*/
|
||
private function getClientPhone($order)
|
||
{
|
||
$phone = method_exists($order, 'get_billing_phone') ? $order->get_billing_phone() : $order->billing_phone;
|
||
$digits = preg_replace('/\D/', '', trim((string)$phone));
|
||
|
||
return ('' !== $digits) ? '+' . $digits : null;
|
||
}
|
||
|
||
/** @return string */
|
||
private function getCmsModuleVersion()
|
||
{
|
||
return sprintf(
|
||
'WooCommerce v%s (WordPress v%s)|PHP %s|%s v%s',
|
||
$this->getWooCommerceVersion(),
|
||
$this->getWordPressVersion(),
|
||
PHP_VERSION,
|
||
self::MODULE_NAME,
|
||
self::MODULE_VERSION
|
||
);
|
||
}
|
||
|
||
/** @return string */
|
||
private function getWooCommerceVersion()
|
||
{
|
||
$version = '?';
|
||
if (defined('WC_VERSION')) {
|
||
$version = Constants::get_constant('WC_VERSION');
|
||
} elseif (defined('WOOCOMMERCE_VERSION')) {
|
||
$version = Constants::get_constant('WOOCOMMERCE_VERSION');
|
||
}
|
||
|
||
return $version;
|
||
}
|
||
|
||
/** @return string */
|
||
private function getWordPressVersion()
|
||
{
|
||
global $wp_version;
|
||
|
||
$version = isset($wp_version) ? $wp_version : null;
|
||
if (null !== $version) {
|
||
return $version;
|
||
}
|
||
|
||
return isset($GLOBALS['wp_version']) ? $GLOBALS['wp_version'] : '?';
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $requestData
|
||
* @return array<string, mixed>
|
||
*/
|
||
private function getCallbackData(array $requestData) {
|
||
return array(
|
||
'MNT_COMMAND' => $this->getOptionalStringWithDefault($requestData, 'MNT_COMMAND'),
|
||
'MNT_ID' => $this->getRequiredString($requestData, 'MNT_ID'),
|
||
'MNT_TRANSACTION_ID' => $this->getRequiredString($requestData, 'MNT_TRANSACTION_ID'),
|
||
'MNT_OPERATION_ID' => $this->getOptionalStringWithDefault($requestData, 'MNT_OPERATION_ID'),
|
||
'MNT_AMOUNT' => $this->getOptionalStringWithDefault($requestData, 'MNT_AMOUNT'),
|
||
'MNT_CURRENCY_CODE' => $this->getRequiredString($requestData, 'MNT_CURRENCY_CODE'),
|
||
'MNT_SUBSCRIBER_ID' => $this->getOptionalStringWithDefault($requestData, 'MNT_SUBSCRIBER_ID'),
|
||
'MNT_TEST_MODE' => $this->getRequiredString($requestData, 'MNT_TEST_MODE'),
|
||
'MNT_SIGNATURE' => $this->getRequiredString($requestData, 'MNT_SIGNATURE'),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param array $callback
|
||
* @return void
|
||
*/
|
||
private function validateCallback(array $callback)
|
||
{
|
||
if (null === $this->getOrderIdFromTransactionId($callback['MNT_TRANSACTION_ID'])) {
|
||
$this->logger && $this->logger->info('Error', array(
|
||
'source' => self::MODULE_NAME,
|
||
'error_message' => 'callback: Не удалось получить номер заказа из transactionId',
|
||
));
|
||
|
||
$this->sendResponse(self::FAIL_RESPONSE);
|
||
}
|
||
|
||
if (empty($this->MNT_ID) || empty($this->MNT_DATAINTEGRITY_CODE)) {
|
||
$this->logger && $this->logger->info('Error', array(
|
||
'source' => self::MODULE_NAME,
|
||
'error_message' => "callback: Не настроен платежный модуль ('Номер расширенного счёта' или 'Код проверки целостности данных', указанные в настройках расширенного счёта в системе MONETA.RU)",
|
||
));
|
||
|
||
$this->sendResponse(self::FAIL_RESPONSE);
|
||
}
|
||
|
||
if ($this->MNT_ID !== (int)$callback['MNT_ID']) {
|
||
$this->logger && $this->logger->info('Error', array(
|
||
'source' => self::MODULE_NAME,
|
||
'error_message' => "callback: Неверный 'Номер расширенного счёта' в callback",
|
||
));
|
||
|
||
$this->sendResponse(self::FAIL_RESPONSE);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $params
|
||
* @param string $key
|
||
* @param string $default
|
||
* @return string
|
||
*/
|
||
private function getOptionalStringWithDefault(array $params, $key, $default = '')
|
||
{
|
||
$optionalString = $this->getOptionalString($params, $key);
|
||
if (null !== $optionalString) {
|
||
return $optionalString;
|
||
}
|
||
|
||
return $default;
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $params
|
||
* @param string $key
|
||
* @return string|null
|
||
*/
|
||
private function getOptionalString(array $params, $key)
|
||
{
|
||
$value = isset($params[$key]) ? $params[$key] : null;
|
||
if (!is_string($value) || trim($value) === '') {
|
||
return null;
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $params
|
||
* @param string $key
|
||
* @return string
|
||
* @throws \InvalidArgumentException
|
||
*/
|
||
private function getRequiredString(array $params, $key)
|
||
{
|
||
if (!isset($params[$key])) {
|
||
throw new \InvalidArgumentException('Missing required field: ' . $key);
|
||
}
|
||
|
||
$value = $params[$key];
|
||
if (!is_string($value)) {
|
||
throw new \InvalidArgumentException(
|
||
sprintf('Field "%s" must be a string, %s given', $key, gettype($value))
|
||
);
|
||
}
|
||
|
||
if ('' === $value) {
|
||
throw new \InvalidArgumentException(sprintf('Field "%s" must be a non-empty string', $key));
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* @param array|string $response
|
||
* @param int $statusCode
|
||
* @return void
|
||
*/
|
||
private function sendResponse($response, $statusCode = 200)
|
||
{
|
||
http_response_code($statusCode);
|
||
if (is_string($response)) {
|
||
header('Content-Type: ' . self::TEXT_CONTENT_TYPE);
|
||
echo $response;
|
||
} else {
|
||
header('Content-Type: ' . $response['contentType']);
|
||
echo $response['data'];
|
||
}
|
||
|
||
exit();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Add the gateway to WooCommerce
|
||
**/
|
||
function add_payanyway_gateway($methods) {
|
||
$methods[] = 'WC_Payanyway';
|
||
|
||
return $methods;
|
||
}
|
||
|
||
function start_session() {
|
||
if (!session_id()) session_start();
|
||
}
|
||
|
||
function end_session() {
|
||
session_destroy();
|
||
}
|
||
|
||
add_action('wp_logout', 'end_session');
|
||
add_action('wp_login', 'end_session');
|
||
add_action('end_session_action', 'end_session');
|
||
|
||
add_filter('woocommerce_payment_gateways', 'add_payanyway_gateway');
|
||
|
||
add_filter('plugin_action_links', function ($links, $file) {
|
||
|
||
if ($file != plugin_basename(__FILE__)) {
|
||
return $links;
|
||
}
|
||
|
||
$settings_link = sprintf('<a href="%s">%s</a>', admin_url('admin.php?page=wc-settings&tab=checkout§ion=payanyway'), 'Настройки');
|
||
|
||
array_unshift($links, $settings_link);
|
||
return $links;
|
||
}, 10, 2);
|
||
|
||
/**
|
||
* Woocommerce blocks support
|
||
*/
|
||
add_action('before_woocommerce_init', 'payanyway_declare_cart_checkout_blocks_compatibility');
|
||
add_action('woocommerce_blocks_loaded', 'payanyway_woocommerce_block_support');
|
||
|
||
function payanyway_declare_cart_checkout_blocks_compatibility() {
|
||
if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) {
|
||
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('cart_checkout_blocks', __FILE__, true);
|
||
}
|
||
}
|
||
|
||
function payanyway_woocommerce_block_support() {
|
||
if (
|
||
class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType') &&
|
||
class_exists(PaymentMethodRegistry::class) &&
|
||
class_exists(Package::class)
|
||
) {
|
||
require_once dirname(__FILE__) . '/block-support.php';
|
||
|
||
add_action(
|
||
'woocommerce_blocks_payment_method_type_registration',
|
||
function (PaymentMethodRegistry $payment_method_registry) {
|
||
$container = Automattic\WooCommerce\Blocks\Package::container();
|
||
$container->register(
|
||
WC_Payanyway_Blocks::class,
|
||
function () {
|
||
return new WC_Payanyway_Blocks();
|
||
}
|
||
);
|
||
$payment_method_registry->register($container->get(WC_Payanyway_Blocks::class));
|
||
},
|
||
5
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
?>
|