3
0
2026-05-15 17:13:17 +03:00

381 lines
11 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace Opencart\Catalog\Controller\Extension\PayAnyWay\Payment;
require_once DIR_EXTENSION . '/payanyway/catalog/library/payanyway/traits/payanyway.php';
/**
* @property \Opencart\System\Engine\Config $config
*/
class Callback extends \Opencart\System\Engine\Controller
{
use \Opencart\Catalog\Library\Extension\PayAnyWay\Traits\PayAnyWay;
private const INVENTORY_ITEM_NAME_MAX_LENGTH = 128;
private const INVENTORY_ITEM_DEFAULT_VAT_TAG = '1105';
private const INVENTORY_ITEM_DEFAULT_PAYMENT_METHOD = 'full_payment';
private const INVENTORY_ITEM_DEFAULT_PAYMENT_OBJECT = 'commodity';
private const INVENTORY_ITEM_DEFAULT_MEASURE = 'unit';
private const CANCEL_ORDER_STATUS_IDS = [7, 9, 16];
private int $mntId;
private string $integrityCode;
private string $server;
private string $vatRate;
private string $currency;
private int $pendingOrderStatusId;
private int $successOrderStatusId;
private int $errorOrderStatusId;
private int $geoZoneId;
private int $sortOrder;
private string $cmsModuleVersion;
private array $callbackData = [];
private ?int $orderId;
private array $orderInfo = [];
public function __construct(\Opencart\System\Engine\Registry $registry)
{
parent::__construct($registry);
if (!$this->validateOrder()) {
$this->sendResponse('FAIL');
}
$this->initializeModule();
$this->initializeCallbackData();
}
/** @throws \Exception */
public function index(): void
{
($this->callbackData['MNT_COMMAND'] === 'CHECK')
? $this->handleCheckCallback()
: $this->handlePayCallback();
}
private function initializeCallbackData(): void
{
$requestData = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET;
$this->callbackData = [
'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'),
];
}
private function sendResponse(string $response, int $statusCode = 200): void
{
http_response_code($statusCode);
$isXml = str_starts_with($response, '<?xml') || str_contains($response, '<MNT_RESPONSE');
header('Content-Type: ' . ($isXml ? 'application/xml' : 'text/plain; charset=UTF-8'));
echo $response;
exit;
}
/** @throws \Exception */
private function handleCheckCallback(): void
{
if (!$this->checkSignature($this->callbackData)) {
$this->sendResponse('FAIL');
}
$this->load->model('account/order');
$products = $this->model_account_order->getProducts($this->orderId);
$deliveryPrice = $this->getDeliveryPrice();
$xmlData = [
'MNT_ID' => $this->mntId,
'MNT_TRANSACTION_ID' => $this->callbackData['MNT_TRANSACTION_ID'],
'MNT_AMOUNT' => $this->formatPrice((float)($this->orderInfo['total'] ?? 0)),
'MNT_CURRENCY_CODE' => $this->callbackData['MNT_CURRENCY_CODE'],
'inventory' => $this->getInventoryJson($products) ?: null,
'client' => $this->callbackData['MNT_SUBSCRIBER_ID'],
'sno' => null,
'delivery' => (null !== $deliveryPrice) ? $this->formatPrice($deliveryPrice) : null,
];
[$xmlData['MNT_RESULT_CODE'], $xmlData['MNT_DESCRIPTION']] = $this->determineCheckResult();
$xmlData['MNT_SIGNATURE'] = md5(
$xmlData['MNT_RESULT_CODE'] .
$xmlData['MNT_ID'] .
$xmlData['MNT_TRANSACTION_ID'] .
$this->integrityCode,
);
$this->sendResponse($this->buildXMLResponse($xmlData));
}
/** @throws \Exception */
private function determineCheckResult(): array
{
$orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null;
if (null === $orderStatusId) {
return [500, "Order status not set"];
}
$orderStatus = $this->getOrderStatusById($orderStatusId);
if (empty($this->callbackData['MNT_AMOUNT'])) {
return [100, "Order status is '{$orderStatus}'"];
}
if ($this->successOrderStatusId === $orderStatusId) {
return [200, 'Order Paid'];
}
if (in_array($orderStatusId, self::CANCEL_ORDER_STATUS_IDS, true)) {
return [500, "Order status is '{$orderStatus}'"];
}
return [402, 'Order created, but not paid'];
}
/** @throws \Exception */
private function handlePayCallback(): void
{
$shopData = [
'MNT_ID' => $this->mntId,
'MNT_TRANSACTION_ID' => $this->callbackData['MNT_TRANSACTION_ID'],
'MNT_OPERATION_ID' => $this->callbackData['MNT_OPERATION_ID'],
'MNT_AMOUNT' => $this->formatPrice((float)$this->orderInfo['total']),
'MNT_CURRENCY_CODE' => $this->orderInfo['currency_code'],
'MNT_SUBSCRIBER_ID' => $this->callbackData['MNT_SUBSCRIBER_ID'],
'MNT_TEST_MODE' => $this->callbackData['MNT_TEST_MODE'],
];
if (!$this->checkSignature($shopData)) {
$this->sendResponse('FAIL');
}
$isPayedOrder = $this->isOrderPaid();
$resultCode = 200;
$this->load->model('account/order');
$products = $this->model_account_order->getProducts($this->orderId);
$deliveryPrice = $this->getDeliveryPrice();
$xmlData = [
'MNT_ID' => $this->mntId,
'MNT_TRANSACTION_ID' => $this->callbackData['MNT_TRANSACTION_ID'],
'MNT_RESULT_CODE' => $resultCode,
'MNT_SIGNATURE' => md5($resultCode . $this->mntId . $this->callbackData['MNT_TRANSACTION_ID'] . $this->integrityCode),
'MNT_AMOUNT' => $this->callbackData['MNT_AMOUNT'],
'MNT_CURRENCY_CODE' => $this->callbackData['MNT_CURRENCY_CODE'],
'MNT_DESCRIPTION' => $isPayedOrder ? 'Order already paid' : 'Order success paid',
'inventory' => $this->getInventoryJson($products),
'client' => $this->callbackData['MNT_SUBSCRIBER_ID'],
'sno' => null,
'delivery' => $deliveryPrice !== null ? $this->formatPrice($deliveryPrice) : null,
];
if (!$isPayedOrder) {
$this->load->language('extension/payanyway/payment/payanyway');
$this->addOrderHistory($this->orderId, $this->successOrderStatusId, $this->language->get('text_payment_completed'),);
}
$this->sendResponse($this->buildXMLResponse($xmlData));
}
private function isOrderPaid(): bool
{
return $this->successOrderStatusId === (int)$this->orderInfo['order_status_id'];
}
/** @param array<string, mixed> $data */
private function checkSignature(array $data): bool
{
$baseFields = [
'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, static function ($carry, $field) use ($data) {
return $carry . $data[$field];
}, '') . $this->integrityCode;
return hash_equals(md5($signatureString), $this->callbackData['MNT_SIGNATURE']);
}
private function getDeliveryPrice(): ?float
{
$this->load->model('account/order');
foreach ($this->model_account_order->getTotals($this->orderId) as $orderTotal) {
if (($orderTotal['code'] ?? '') === 'shipping') {
$deliveryPrice = (float)($orderTotal['value'] ?? 0);
return $deliveryPrice !== 0.0 ? $deliveryPrice : null;
}
}
return null;
}
/**
* @param array<string, mixed> $products
* @return false|string
*/
private function getInventoryJson(array $products): false|string
{
$inventory = [];
foreach ($products as $product) {
$inventory[] = [
'name' => $this->validateString($product['name'] ?? '', self::INVENTORY_ITEM_NAME_MAX_LENGTH),
'price' => $this->formatPrice((float)($product['price'] ?? 0)),
'quantity' => $this->formatQuantity((string)($product['quantity'] ?? '0')),
'vatTag' => $this->vatRate,
'pm' => self::INVENTORY_ITEM_DEFAULT_PAYMENT_METHOD,
'po' => self::INVENTORY_ITEM_DEFAULT_PAYMENT_OBJECT,
'measure' => self::INVENTORY_ITEM_DEFAULT_MEASURE,
];
}
return json_encode($inventory, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
/**
* @param int $orderStatusId
* @return string
* @throws \Exception
*/
public function getOrderStatusById(int $orderStatusId): string
{
$this->load->model('localisation/order_status');
/**@var array<int, array{order_status_id: string, name: string}> $orderStatuses */
$orderStatuses = $this->model_localisation_order_status->getOrderStatuses();
$orderStatus = 'unknown';
foreach ($orderStatuses as $orderStatus) {
$statusId = isset($orderStatus['order_status_id']) ? (int)$orderStatus['order_status_id'] : null;
if ($statusId === $orderStatusId) {
$orderStatus = $orderStatus['name'];
break;
}
}
return $orderStatus;
}
private function formatQuantity(string $quantity): string
{
$quantity = str_replace(',', '.', $quantity);
if (!str_contains($quantity, '.') && ctype_digit($quantity)) {
return (string)(int)$quantity;
}
return number_format((float)$quantity, 3, '.', '');
}
/**
* @param array<string, mixed> $data
* @throws \Exception
*/
private function buildXMLResponse(array $data): false|string
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = false;
$root = $dom->createElement('MNT_RESPONSE');
$dom->appendChild($root);
$requiredFields = [
'MNT_ID',
'MNT_TRANSACTION_ID',
'MNT_RESULT_CODE',
'MNT_DESCRIPTION',
'MNT_AMOUNT',
'MNT_CURRENCY_CODE',
'MNT_SIGNATURE',
];
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 = [
'INVENTORY' => $data['inventory'] ?? null,
'CLIENT' => $data['client'] ?? null,
'SNO' => $data['sno'] ?? null,
'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);
}
}
return $dom->saveXML();
}
/** @param array<string, mixed> $params */
private function getOptionalStringWithDefault(array $params, string $key, string $default = ''): string
{
return $this->getOptionalString($params, $key) ?? $default;
}
/** @param array<string, mixed> $params */
private function getOptionalString(array $params, string $key): ?string
{
$value = $params[$key] ?? null;
if (!is_string($value) || trim($value) === '') {
return null;
}
return $value;
}
/**
* @param array<string, mixed> $params
* @throws \InvalidArgumentException
*/
private function getRequiredString(array $params, string $key): string
{
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;
}
}