feat: paygw_moneta 1.0.0, MONETA.Assistant payment gateway for Moodle
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user