Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECP-9593] Implement an abstract handler for order status history #2906

Open
wants to merge 10 commits into
base: develop-10
Choose a base branch
from
55 changes: 55 additions & 0 deletions Gateway/Response/AbstractOrderStatusHistoryHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Gateway\Response;

use Adyen\AdyenException;
use Adyen\Payment\Helper\OrderStatusHistory;
use Magento\Payment\Gateway\Helper\SubjectReader;
use Magento\Payment\Gateway\Response\HandlerInterface;

class AbstractOrderStatusHistoryHandler implements HandlerInterface
{
/**
* @param string $eventType Indicates the API call event type (authorization, capture etc.)
* @param OrderStatusHistory $orderStatusHistoryHelper
*/
public function __construct(
protected readonly string $eventType,
protected readonly OrderStatusHistory $orderStatusHistoryHelper
) { }

/**
* @throws AdyenException
*/
public function handle(array $handlingSubject, array $responseCollection): void
{
if (empty($this->eventType)) {
throw new AdyenException(
__('Order status history can not be handled due to missing event type!')
);
}

$readPayment = SubjectReader::readPayment($handlingSubject);
$payment = $readPayment->getPayment();
$order = $payment->getOrder();

// Temporary workaround to clean-up `hasOnlyGiftCards` key. It needs to be handled separately.
if (isset($responseCollection['hasOnlyGiftCards'])) {
unset($responseCollection['hasOnlyGiftCards']);
}

foreach ($responseCollection as $response) {
$comment = $this->orderStatusHistoryHelper->buildApiResponseComment($response, $this->eventType);
$order->addCommentToStatusHistory($comment, $order->getStatus());
}
}
}
47 changes: 0 additions & 47 deletions Gateway/Response/CheckoutPaymentCommentHistoryHandler.php

This file was deleted.

4 changes: 4 additions & 0 deletions Gateway/Response/CheckoutPaymentsDetailsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public function handle(array $handlingSubject, array $responseCollection)
$payment->setTransactionId($response['pspReference']);
}

if (isset($response['resultCode'])) {
$payment->getOrder()->setAdyenResulturlEventCode($response['resultCode']);
}

// do not close transaction, so you can do a cancel() and void
$payment->setIsTransactionClosed(false);
$payment->setShouldCloseParentTransaction(false);
Expand Down
14 changes: 7 additions & 7 deletions Helper/Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -561,8 +561,9 @@ public function getLiveEndpointPrefix($storeId = null)
* Cancels the order
*
* @param $order
* @param null $reason
*/
public function cancelOrder($order)
public function cancelOrder($order, $reason = null)
{
$orderStatus = $this->configHelper->getAdyenAbstractConfigData('payment_cancelled');
$order->setActionFlag($orderStatus, true);
Expand All @@ -577,12 +578,11 @@ public function cancelOrder($order)
if ($order->canCancel()) {
if ($this->orderManagement->cancel($order->getEntityId())) { //new canceling process
try {
$orderStatusHistory = $this->orderStatusHistoryFactory->create()
->setParentId($order->getEntityId())
->setEntityName('order')
->setStatus(Order::STATE_CANCELED)
->setComment(__('Order has been cancelled by "%1" payment response.', $order->getPayment()->getMethod()));
$this->orderManagement->addComment($order->getEntityId(), $orderStatusHistory);
$comment = __('Order has been cancelled.', $order->getPayment()->getMethod());
if ($reason) {
$comment .= '<br />' . __("Reason: %1", $reason) . '<br />';
}
$order->addCommentToStatusHistory($comment, $order->getStatus());
} catch (Exception $e) {
$this->adyenLogger->addAdyenDebug(
__('Order cancel history comment error: %1', $e->getMessage()),
Expand Down
17 changes: 12 additions & 5 deletions Helper/Invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,11 @@ public function createAdyenInvoice(
*
* @param Order $order
* @param Notification $notification
* @return AdyenInvoice
* @return array
* @throws AdyenWebhookException
* @throws AlreadyExistsException
* @throws Exception
*/
public function handleCaptureWebhook(Order $order, Notification $notification): AdyenInvoice
public function handleCaptureWebhook(Order $order, Notification $notification): array
{
$invoiceFactory = $this->adyenInvoiceFactory->create();
$adyenInvoice = $this->adyenInvoiceResourceModel->getAdyenInvoiceByCaptureWebhook($order, $notification);
Expand Down Expand Up @@ -280,12 +280,19 @@ public function handleCaptureWebhook(Order $order, Notification $notification):
$magentoInvoice = $this->magentoInvoiceFactory->create()->load($adyenInvoiceObject->getInvoiceId());

if ($this->isFullInvoiceAmountManuallyCaptured($magentoInvoice)) {
/*
* Magento Invoice updates the Order object while paying the invoice. This creates two divergent
* Order objects. In the downstream, some information might be missing due to setting them on the
* wrong order object as the Order might be already updated in the upstream without persisting
* it to the database. Setting the order again on the Invoice makes sure we are dealing
* with the same order object always.
*/
$magentoInvoice->setOrder($order);
$magentoInvoice->pay();
$this->invoiceRepository->save($magentoInvoice);
$this->magentoOrderResourceModel->save($magentoInvoice->getOrder());
}

return $adyenInvoiceObject;
return [$adyenInvoiceObject, $magentoInvoice, $order];
}

/**
Expand Down
140 changes: 140 additions & 0 deletions Helper/OrderStatusHistory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Helper;

use Adyen\Payment\Api\Data\NotificationInterface;
use Adyen\Payment\Model\Notification;
use Magento\Sales\Api\Data\OrderInterface;

class OrderStatusHistory
{
/**
* @param ChargedCurrency $chargedCurrencyHelper
* @param Data $adyenHelper
*/
public function __construct(
private readonly ChargedCurrency $chargedCurrencyHelper,
private readonly Data $adyenHelper
) { }

/**
* Builds the order status history comment based on the Checkout API response
*
* @param array $response
* @param string $actionName
* @return string
*/
public function buildApiResponseComment(array $response, string $actionName): string
{
$comment = '<strong>' . __("Adyen %1 response", $actionName) . '</strong><br />';

if (isset($response['resultCode'])) {
$comment .= __("Result code: %1", $response['resultCode']) . '<br />';
}

// Modification responses contain `status` but not `resultCode`.
if (isset($response['status'])) {
$comment .= __("Status: %1", $response['status']) . '<br />';
}

if (isset($response['pspReference'])) {
$comment .= __("PSP reference: %1", $response['pspReference']) . '<br />';
}

if ($paymentMethod = $response['paymentMethod']['brand'] ?? $response['paymentMethod']['type'] ?? null) {
$comment .= __("Payment method: %1", $paymentMethod) . '<br />';
}

if (isset($response['refusalReason'])) {
$comment .= __("Refusal reason: %1", $response['refusalReason']) . '<br />';
}

if (isset($response['errorCode'])) {
$comment .= __("Error code: %1", $response['errorCode']) . '<br />';
}

return $comment;
}

/**
* Builds the order status history comment based on the webhook
*
* @param OrderInterface $order
* @param NotificationInterface $notification
* @param string|null $klarnaReservationNumber
* @return string
*/
public function buildWebhookComment(
OrderInterface $order,
NotificationInterface $notification,
?string $klarnaReservationNumber = null,
): string {
$comment = '<strong>' . __(
"Adyen %1%2 webhook",
$this->getIsWebhookForPartialPayment($order, $notification) ? 'partial ': '',
$notification->getEventCode()
) . '</strong><br />';

if (!empty($notification->getPspreference())) {
$comment .= __("PSP reference: %1", $notification->getPspreference()) . '<br />';
}

if (!empty($notification->getPaymentMethod())) {
$comment .= __("Payment method: %1", $notification->getPaymentMethod()) . '<br />';
}

if (!empty($notification->getSuccess())) {
$status = $notification->isSuccessful() ? 'Successful' : 'Failed';
$comment .= __("Event status: %1", $status) . '<br />';
}

if (!empty($notification->getReason())) {
$comment .= __("Reason: %1", $notification->getReason()) . '<br />';
}

if (isset($klarnaReservationNumber)) {
$comment .= __("Reservation number: %1", $klarnaReservationNumber) . '<br />';
}

return $comment;
}

/**
* Identifies whether if the notification belongs to a partial modification or not
*
* @param OrderInterface $order
* @param NotificationInterface $notification
* @return bool
*/
private function getIsWebhookForPartialPayment(
OrderInterface $order,
NotificationInterface $notification
): bool {
$isPartial = false;

if (in_array($notification->getEventCode(), [Notification::REFUND, Notification::CAPTURE])) {
// check if it is a full or partial refund
$amount = $notification->getAmountValue();
$orderAmountCurrency = $this->chargedCurrencyHelper->getOrderAmountCurrency($order, false);
$formattedOrderAmount = $this->adyenHelper->formatAmount(
$orderAmountCurrency->getAmount(),
$orderAmountCurrency->getCurrencyCode()
);

if ($amount !== $formattedOrderAmount) {
$isPartial = true;
}
}

return $isPartial;
}
}
Loading
Loading