Files
esun3d/app/code/Esun/Oceanpayment/Model/Callback/Processor.php
T
2026-09-15 10:35:56 +08:00

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,
};
}
}