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

154 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
namespace paygw_moneta\local\callback;
use core_payment\helper;
use paygw_moneta\local\BuyerNotifier;
use paygw_moneta\local\CmsInfo;
use paygw_moneta\local\GatewayConfig;
use paygw_moneta\local\Logger;
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\order\TransactionStatus;
use paygw_moneta\local\protocol\CallbackNotification;
use paygw_moneta\local\protocol\CallbackResponse;
use paygw_moneta\local\protocol\PayResponse;
use paygw_moneta\local\protocol\ResultCode;
use paygw_moneta\local\receipt\Receipt;
/**
* @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 PayHandler implements NotificationHandler
{
/**
* @param \Closure(string, string, int, int, int): bool $deliverOrder
*/
public function __construct(
private readonly TransactionRepository $repository,
private readonly \Closure $deliverOrder,
private readonly BuyerNotifier $notifier,
private readonly CurrentPrice $currentPrice,
private readonly Logger $logger,
) {}
public function handle(CallbackNotification $notification, Order $order, GatewayConfig $config): CallbackResponse
{
if ($order->status === TransactionStatus::Paid) {
// Повторная доставка того же уведомления — тот же ответ, без второго зачисления.
if ($order->providerTransactionId === $notification->getOperationId()) {
return $this->paid($notification, $order, $config, ResultCode::Paid, $order->snapshot->receipt);
}
// Вторая операция по оплаченному заказу: деньги списаны дважды — нужен человек.
$this->repository->noteError($order, RejectionReason::DuplicateOperation);
return CallbackResponse::fail();
}
if ($order->status === TransactionStatus::Canceled) {
// Оплата отменённого заказа: повторы бессмысленны, но след должен остаться.
return $this->paid($notification, $order, $config, ResultCode::Rejected, null);
}
// Снимок защищает легитимный заказ от смены настроек, но не должен позволять
// дожать сохранённую форму после того, как администратор выключил тестовый
// режим или поднял цену: сверяемся ещё и с текущим состоянием.
if ($notification->isTestMode() !== $config->testMode) {
$this->repository->noteError($order, RejectionReason::TestMode);
return CallbackResponse::fail();
}
if (!$config->enabled) {
$this->repository->noteError($order, RejectionReason::GatewayDisabled);
return CallbackResponse::fail();
}
if (!$this->currentPrice->matches($order)) {
$this->repository->noteError($order, RejectionReason::AmountChanged);
return CallbackResponse::fail();
}
$paid = $this->deliver($notification, $order);
if ($paid === null) {
return CallbackResponse::fail();
}
$this->notifier->notifyPaid($paid);
return $this->paid($notification, $paid, $config, ResultCode::Paid, $paid->snapshot->receipt);
}
/**
* Платёж ядра, доставка заказа и статус — одной транзакцией БД. При сбое
* заказ помечается `Failed`, чтобы повтор уведомления попробовал ещё раз.
*/
private function deliver(CallbackNotification $notification, Order $order): ?Order
{
global $DB;
$transaction = $DB->start_delegated_transaction();
try {
$paymentId = helper::save_payment(
$order->accountId,
$order->component,
$order->paymentArea,
$order->itemId,
$order->userId,
(float) $order->amount->toDecimal(),
$order->currency,
GatewayConfig::GATEWAY,
);
if (!($this->deliverOrder)(
$order->component,
$order->paymentArea,
$order->itemId,
$paymentId,
$order->userId,
)) {
throw new \RuntimeException('Доставка заказа вернула false.');
}
$paid = $this->repository->markPaid($order, $notification->getOperationId(), $paymentId);
$transaction->allow_commit();
return $paid;
} catch (\Throwable $exception) {
try {
// rollback() перебрасывает исключение — гасим его здесь, причина уходит в lasterror.
$transaction->rollback($exception);
} catch (\Throwable) {
// Откат выполнен, исключение уже обработано.
}
$this->repository->markFailed($order, RejectionReason::Delivery);
$this->logger->error('Доставка заказа не удалась: ' . get_class($exception));
}
return null;
}
private function paid(
CallbackNotification $notification,
Order $order,
GatewayConfig $config,
ResultCode $code,
?Receipt $receipt,
): CallbackResponse {
$payResponse = new PayResponse(
accountId: $config->accountNumber,
transactionId: $notification->getTransactionId(),
amount: $order->amount,
resultCode: $code,
signature: $config->signature(),
cms: CmsInfo::getCmsModuleVersion(),
receipt: $receipt,
);
return $payResponse->toXml();
}
}