Files
Moodle/classes/local/callback/CallbackHandler.php
T

175 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
namespace paygw_moneta\local\callback;
use core\lock\lock_config;
use core_payment\helper;
use paygw_moneta\local\BuyerNotifier;
use paygw_moneta\local\CmsInfo;
use paygw_moneta\local\ErrorLogLogger;
use paygw_moneta\local\GatewayConfig;
use paygw_moneta\local\GatewayConfigRepository;
use paygw_moneta\local\Logger;
use paygw_moneta\local\Money;
use paygw_moneta\local\order\CurrentPrice;
use paygw_moneta\local\order\Order;
use paygw_moneta\local\order\RejectionReason;
use paygw_moneta\local\order\TransactionRepository;
use paygw_moneta\local\protocol\AssistantTransactionId;
use paygw_moneta\local\protocol\CallbackNotification;
use paygw_moneta\local\protocol\CallbackResponse;
use paygw_moneta\local\protocol\CheckResponse;
use paygw_moneta\local\protocol\ResultCode;
/**
* @package paygw_moneta
* @copyright 2026 Moneta Labs {@link https://moneta.ru/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class CallbackHandler
{
private const LOCK_TIMEOUT = 10;
private readonly NotificationHandler $check;
private readonly NotificationHandler $pay;
/**
* @param \Closure(string, string, int, int, int): bool|null $deliverOrder
*/
public function __construct(
private readonly TransactionRepository $repository = new TransactionRepository(),
?\Closure $deliverOrder = null,
BuyerNotifier $notifier = new BuyerNotifier(),
private readonly Logger $logger = new ErrorLogLogger(),
private readonly GatewayConfigRepository $configs = new GatewayConfigRepository(),
CurrentPrice $currentPrice = new CurrentPrice(),
) {
$this->check = new CheckHandler($repository);
$this->pay = new PayHandler(
repository: $repository,
deliverOrder: $deliverOrder ?? helper::deliver_order(...),
notifier: $notifier,
currentPrice: $currentPrice,
logger: $logger,
);
}
/**
* @param array<string, mixed> $fields
*/
public function handle(array $fields): CallbackResponse
{
try {
$notification = new CallbackNotification($fields);
} catch (\InvalidArgumentException) {
return CallbackResponse::fail();
}
$merchantOrderId = AssistantTransactionId::parse($notification->getTransactionId());
if ($merchantOrderId === null) {
return CallbackResponse::fail();
}
$order = $this->repository->findByMerchantOrderId($merchantOrderId);
if ($order === null) {
return $this->unknownOrder($notification);
}
$config = $this->configs->forAccount($order->accountId);
if ($config === null || !$config->isComplete()) {
return CallbackResponse::fail();
}
if (
$notification->getAccountId() !== $config->accountNumber
|| $notification->getAccountId() !== $order->snapshot->accountNumber
|| !$notification->isSignedBy($config->signature())
) {
$this->logger->error('Уведомление отклонено: подпись или номер счёта не совпадают.');
return CallbackResponse::fail();
}
$lock = lock_config::get_lock_factory('paygw_moneta')->get_lock($order->merchantOrderId, self::LOCK_TIMEOUT);
if (!$lock) {
return CallbackResponse::fail();
}
try {
$order = $this->repository->findById($order->id) ?? $order;
return $this->handleSigned($notification, $order, $config);
} finally {
$lock->release();
}
}
private function handleSigned(CallbackNotification $notification, Order $order, GatewayConfig $config): CallbackResponse
{
$error = $this->validate($notification, $order);
if ($error !== null) {
$this->repository->noteError($order, $error);
return CallbackResponse::fail();
}
$handler = $notification->isCheck() ? $this->check : $this->pay;
return $handler->handle($notification, $order, $config);
}
private function validate(CallbackNotification $notification, Order $order): ?RejectionReason
{
if ($notification->getCurrency() !== $order->currency) {
return RejectionReason::Currency;
}
if ($notification->isTestMode() !== $order->snapshot->testMode) {
return RejectionReason::TestMode;
}
if ($notification->getSubscriberId() !== '' && $notification->getSubscriberId() !== $order->subscriberId()) {
return RejectionReason::Subscriber;
}
$amount = $notification->getAmount();
if ($amount !== null && !$amount->equals($order->amount)) {
return RejectionReason::Amount;
}
return null;
}
private function unknownOrder(CallbackNotification $notification): CallbackResponse
{
$config = $this->configs->findByAccountNumber($notification->getAccountId());
if ($config === null || !$notification->isSignedBy($config->signature())) {
return CallbackResponse::fail();
}
if (!$notification->isCheck()) {
$this->logger->error(
sprintf(
'Оплачено уведомление по неизвестному заказу %s (операция %s).',
$notification->getTransactionId(),
$notification->getOperationId(),
),
);
return CallbackResponse::fail();
}
$response = new CheckResponse(
accountId: $config->accountNumber,
transactionId: $notification->getTransactionId(),
amount: $notification->getAmount() ?? Money::fromMinorUnits(0),
resultCode: ResultCode::Rejected,
signature: $config->signature(),
cms: CmsInfo::getCmsModuleVersion(),
);
return $response->toXml();
}
}