175 lines
5.9 KiB
PHP
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();
|
|
}
|
|
}
|