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

1247 lines
44 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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