228 lines
9.0 KiB
PHP
228 lines
9.0 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Esun\Oceanpayment\Model\Callback;
|
|
|
|
use Esun\Oceanpayment\Model\Config;
|
|
use Esun\Oceanpayment\Model\OrderStatusManager;
|
|
use Esun\Oceanpayment\Model\Signature;
|
|
use Magento\Framework\Exception\LocalizedException;
|
|
use Magento\Sales\Api\OrderRepositoryInterface;
|
|
use Magento\Sales\Model\Order;
|
|
use Magento\Sales\Model\OrderFactory;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class Processor
|
|
{
|
|
public const SUCCESS = 'success';
|
|
public const FAILURE = 'failure';
|
|
public const PENDING = 'pending';
|
|
|
|
public function __construct(
|
|
private readonly Config $config,
|
|
private readonly Signature $signature,
|
|
private readonly OrderFactory $orderFactory,
|
|
private readonly OrderRepositoryInterface $orderRepository,
|
|
private readonly OrderStatusManager $statusManager,
|
|
private readonly LoggerInterface $logger
|
|
) {
|
|
}
|
|
|
|
public function process(array $data, bool $isAsync): string
|
|
{
|
|
$orderNumber = (string)($data['order_number'] ?? '');
|
|
if ($orderNumber === '' || (string)($data['payment_id'] ?? '') === '') {
|
|
throw new LocalizedException(__('Oceanpayment callback is missing required fields.'));
|
|
}
|
|
|
|
/** @var Order $order */
|
|
$order = $this->orderFactory->create()->loadByIncrementId($orderNumber);
|
|
if (!$order->getEntityId()
|
|
|| !Config::isSupportedMethod((string)$order->getPayment()->getMethod())
|
|
) {
|
|
throw new LocalizedException(__('The Oceanpayment order could not be found.'));
|
|
}
|
|
|
|
$this->validate($order, $data);
|
|
$payment = $order->getPayment();
|
|
$paymentId = (string)$data['payment_id'];
|
|
$status = (string)$data['payment_status'];
|
|
$method = (string)$payment->getMethod();
|
|
|
|
if ((string)$payment->getAdditionalInformation('qianhai_processed_payment_id') === $paymentId
|
|
&& (string)$payment->getAdditionalInformation('qianhai_processed_status') === $status
|
|
) {
|
|
return $this->mapStatus($status);
|
|
}
|
|
|
|
$payment->setAdditionalInformation('qianhai_payment_id', $paymentId);
|
|
$payment->setAdditionalInformation('qianhai_processed_payment_id', $paymentId);
|
|
$payment->setAdditionalInformation('qianhai_processed_status', $status);
|
|
$payment->setAdditionalInformation('qianhai_payment_details', (string)$data['payment_details']);
|
|
$payment->setAdditionalInformation('qianhai_payment_method', (string)$data['methods']);
|
|
$payment->setTransactionId($paymentId);
|
|
$payment->setLastTransId($paymentId);
|
|
|
|
$result = $this->mapStatus($status);
|
|
if ($result === self::SUCCESS) {
|
|
$this->processSuccess($order, $data);
|
|
} elseif ($result === self::PENDING) {
|
|
$this->processPending($order, $data);
|
|
} else {
|
|
$this->processFailure($order, $data, $isAsync);
|
|
}
|
|
|
|
$this->orderRepository->save($order);
|
|
|
|
if ($this->config->isDebug((int)$order->getStoreId(), $method)) {
|
|
$this->logger->debug('Oceanpayment callback processed', [
|
|
'order_number' => $orderNumber,
|
|
'payment_id' => $paymentId,
|
|
'payment_status' => $status,
|
|
'async' => $isAsync,
|
|
]);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function validate(Order $order, array $data): void
|
|
{
|
|
$storeId = (int)$order->getStoreId();
|
|
$method = (string)$order->getPayment()->getMethod();
|
|
if (!hash_equals($this->config->getSecret('account', $storeId, $method), (string)$data['account'])
|
|
|| !hash_equals($this->config->getSecret('terminal', $storeId, $method), (string)$data['terminal'])
|
|
) {
|
|
throw new LocalizedException(__('Oceanpayment callback account verification failed.'));
|
|
}
|
|
|
|
if (!hash_equals(
|
|
strtoupper((string)$order->getOrderCurrencyCode()),
|
|
strtoupper((string)$data['order_currency'])
|
|
) || abs((float)$order->getGrandTotal() - (float)$data['order_amount']) >= 0.005) {
|
|
throw new LocalizedException(__('Oceanpayment callback amount verification failed.'));
|
|
}
|
|
|
|
$secureCode = $this->config->getSecret('secure_code', $storeId, $method);
|
|
if (!$this->signature->verifyCallback($data, $secureCode)) {
|
|
if ($this->config->isDebug($storeId, $method)) {
|
|
$this->logger->warning(
|
|
'Oceanpayment callback signature verification failed',
|
|
$this->getSignatureDiagnostics($data, $secureCode)
|
|
);
|
|
}
|
|
throw new LocalizedException(__('Oceanpayment callback signature verification failed.'));
|
|
}
|
|
|
|
$expectedPaymentId = (string)$order->getPayment()
|
|
->getAdditionalInformation('qianhai_payment_id');
|
|
if ($expectedPaymentId !== ''
|
|
&& !hash_equals($expectedPaymentId, (string)$data['payment_id'])
|
|
) {
|
|
throw new LocalizedException(__('Oceanpayment callback payment ID verification failed.'));
|
|
}
|
|
|
|
if (!in_array((string)$data['payment_status'], ['-1', '0', '1'], true)) {
|
|
throw new LocalizedException(__('Oceanpayment returned an unknown payment status.'));
|
|
}
|
|
}
|
|
|
|
private function getSignatureDiagnostics(array $data, string $secureCode): array
|
|
{
|
|
$provided = strtolower(trim((string)($data['signValue'] ?? '')));
|
|
$expected = $this->signature->createCallback($data, $secureCode);
|
|
|
|
return [
|
|
'account' => $this->mask((string)($data['account'] ?? '')),
|
|
'terminal' => $this->mask((string)($data['terminal'] ?? '')),
|
|
'order_number' => (string)($data['order_number'] ?? ''),
|
|
'order_currency' => (string)($data['order_currency'] ?? ''),
|
|
'order_amount' => (string)($data['order_amount'] ?? ''),
|
|
'order_notes_length' => strlen((string)($data['order_notes'] ?? '')),
|
|
'card_number' => $this->mask((string)($data['card_number'] ?? '')),
|
|
'payment_id' => (string)($data['payment_id'] ?? ''),
|
|
'payment_auth_type' => (string)($data['payment_authType'] ?? ''),
|
|
'payment_status' => (string)($data['payment_status'] ?? ''),
|
|
'payment_details' => mb_substr((string)($data['payment_details'] ?? ''), 0, 200),
|
|
'payment_risk' => mb_substr((string)($data['payment_risk'] ?? ''), 0, 200),
|
|
'provided_sign_prefix' => substr($provided, 0, 12),
|
|
'provided_sign_length' => strlen($provided),
|
|
'expected_sign_prefix' => substr($expected, 0, 12),
|
|
];
|
|
}
|
|
|
|
private function mask(string $value): string
|
|
{
|
|
$length = strlen($value);
|
|
if ($length <= 4) {
|
|
return str_repeat('*', $length);
|
|
}
|
|
|
|
return str_repeat('*', $length - 4) . substr($value, -4);
|
|
}
|
|
|
|
private function processSuccess(Order $order, array $data): void
|
|
{
|
|
$payment = $order->getPayment();
|
|
$method = (string)$payment->getMethod();
|
|
$payment->setIsTransactionClosed(true);
|
|
|
|
if ($order->canInvoice() && (float)$order->getTotalDue() > 0.0001) {
|
|
$payment->registerCaptureNotification((float)$data['order_amount'], true);
|
|
}
|
|
|
|
$this->statusManager->apply(
|
|
$order,
|
|
$this->config->getValue('success_order_status', (int)$order->getStoreId(), $method),
|
|
Order::STATE_PROCESSING
|
|
);
|
|
$order->addCommentToStatusHistory(
|
|
__('Oceanpayment payment approved. Payment ID: %1', $data['payment_id'])
|
|
);
|
|
}
|
|
|
|
private function processPending(Order $order, array $data): void
|
|
{
|
|
$method = (string)$order->getPayment()->getMethod();
|
|
$order->getPayment()->setIsTransactionClosed(false)->setIsTransactionPending(true);
|
|
$this->statusManager->apply(
|
|
$order,
|
|
$this->config->getValue('pending_order_status', (int)$order->getStoreId(), $method),
|
|
Order::STATE_PAYMENT_REVIEW
|
|
);
|
|
$order->addCommentToStatusHistory(
|
|
__('Oceanpayment payment is pending. Payment ID: %1', $data['payment_id'])
|
|
);
|
|
}
|
|
|
|
private function processFailure(Order $order, array $data, bool $isAsync): void
|
|
{
|
|
$method = (string)$order->getPayment()->getMethod();
|
|
$order->getPayment()->setIsTransactionClosed(true);
|
|
if ($isAsync && $order->canCancel()) {
|
|
$order->cancel();
|
|
$this->statusManager->apply(
|
|
$order,
|
|
$this->config->getValue('failure_order_status', (int)$order->getStoreId(), $method),
|
|
Order::STATE_CANCELED
|
|
);
|
|
}
|
|
$order->addCommentToStatusHistory(
|
|
__(
|
|
'Oceanpayment payment failed. Payment ID: %1. Details: %2',
|
|
$data['payment_id'],
|
|
$data['payment_details']
|
|
)
|
|
);
|
|
}
|
|
|
|
private function mapStatus(string $status): string
|
|
{
|
|
return match ($status) {
|
|
'1' => self::SUCCESS,
|
|
'-1' => self::PENDING,
|
|
default => self::FAILURE,
|
|
};
|
|
}
|
|
}
|