Files
WordPress-WooCommerce/payanyway/wc-payanyway.php
T
2026-07-16 14:32:49 +03:00

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, '', '&amp;') . '" 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&section=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
);
}
}
}
?>