Skip to content

Commit

Permalink
[ECP-8692] Implement usage of pending_payment status for action requi…
Browse files Browse the repository at this point in the history
…red payment attempts (#2381)

* [ECP-8692] Create an observer to update the order status after checkout

* [ECP-8692] Update the order on payment details

* [ECP-8692] Implement status update logic after paymentsDetails call

* [ECP-8692] Abstract paymentsDetails call and related response handler, refactor Return controller

* [ECP-8692] Fix Order unit tests

* [ECP-8692] Remove remains of remote tracking

* [ECP-8692] Fix unit test

* [ECP-8692] Write unit tests for AdyenPaymentsDetails class

* [ECP-8692] Write unit tests for GuestAdyenPaymentsDetails class

* [ECP-8692] Clean-up code duplications

* [ECP-8692] Remove redundant method implementation

* [ECP-8692] Write unit tests for Return/Index class

* [ECP-8692] Remove redundant property

* [ECP-8692] Remove redundant property

* [ECP-8692] Fix condition with no redirectResponse

* [ECP-8692] Write unit tests for PaymentResponseHandler class

* [ECP-8692] Replace the constant for resultCode

* [ECP-8692] Write unit tests for PaymentResponseHandler class

* [ECP-8692] Finalise TODO

* [ECP-8692] Fix unreachable statement

* [ECP-8692] Increase the coverage of PaymentResponseHandler class

* [ECP-8692] Write unit tests for SetOrderStateAfterPaymentObserver class

* [ECP-8692] Increase test coverage

* [ECP-8692] Write unit test for Order class

* [ECP-8692] Write unit test for PaymentDetails class

* [ECP-8692] Fix CS issues

---------

Co-authored-by: hossam-adyen <132500300+hossam-adyen@users.noreply.github.com>
  • Loading branch information
candemiralp and hossam-adyen authored Feb 21, 2024
1 parent f20b4f6 commit c8ba5d0
Show file tree
Hide file tree
Showing 15 changed files with 1,565 additions and 488 deletions.
394 changes: 96 additions & 298 deletions Controller/Return/Index.php

Large diffs are not rendered by default.

28 changes: 27 additions & 1 deletion Helper/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class Order extends AbstractHelper
/** @var AdyenCreditmemoHelper */
private $adyenCreditmemoHelper;

private MagentoOrder\StatusResolver $statusResolver;

public function __construct(
Context $context,
Builder $transactionBuilder,
Expand All @@ -100,7 +102,8 @@ public function __construct(
OrderPaymentCollectionFactory $adyenOrderPaymentCollectionFactory,
PaymentMethods $paymentMethodsHelper,
AdyenCreditMemoResourceModel $adyenCreditmemoResourceModel,
AdyenCreditmemoHelper $adyenCreditmemoHelper
AdyenCreditmemoHelper $adyenCreditmemoHelper,
MagentoOrder\StatusResolver $statusResolver
) {
parent::__construct($context);
$this->transactionBuilder = $transactionBuilder;
Expand All @@ -119,6 +122,7 @@ public function __construct(
$this->paymentMethodsHelper = $paymentMethodsHelper;
$this->adyenCreditmemoResourceModel = $adyenCreditmemoResourceModel;
$this->adyenCreditmemoHelper = $adyenCreditmemoHelper;
$this->statusResolver = $statusResolver;
}

/**
Expand Down Expand Up @@ -370,6 +374,28 @@ public function setPrePaymentAuthorized(MagentoOrder $order): MagentoOrder
return $order;
}

public function setStatusOrderCreation(OrderInterface $order): OrderInterface
{
$paymentMethod = $order->getPayment()->getMethod();

// Fetch the default order status for order creation from the configuration.
$status = $this->configHelper->getConfigData(
'order_status',
$paymentMethod,
$order->getStoreId()
);

if (is_null($status)) {
// If the configuration doesn't exist, use the default status.
$status = $this->statusResolver->getOrderStatusByState($order, MagentoOrder::STATE_NEW);
}

$order->setStatus($status);
$order->setState(MagentoOrder::STATE_NEW);

return $order;
}

/**
* @param MagentoOrder $order
* @param $ignoreHasInvoice
Expand Down
249 changes: 158 additions & 91 deletions Helper/PaymentResponseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
use Adyen\Payment\Logger\AdyenLogger;
use Exception;
use Magento\Framework\Exception\AlreadyExistsException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Model\Order\Payment;
use Magento\Sales\Model\Order\Status\HistoryFactory;
use Magento\Sales\Model\OrderRepository;
use Magento\Sales\Model\ResourceModel\Order;

class PaymentResponseHandler
Expand All @@ -35,7 +37,7 @@ class PaymentResponseHandler
const VAULT = 'Magento Vault';
const POS_SUCCESS = 'Success';

public const ACTION_REQUIRED_STATUSES = [
const ACTION_REQUIRED_STATUSES = [
self::REDIRECT_SHOPPER,
self::IDENTIFY_SHOPPER,
self::CHALLENGE_SHOPPER,
Expand All @@ -46,46 +48,35 @@ class PaymentResponseHandler
* @var AdyenLogger
*/
private AdyenLogger $adyenLogger;

/**
* @var Vault
*/
private Vault $vaultHelper;

/**
* @var Order
*/
private Order $orderResourceModel;

/**
* @var Data
*/
private Data $dataHelper;

/**
* @var Quote
*/
private Quote $quoteHelper;
private \Adyen\Payment\Helper\Order $orderHelper;
private OrderRepository $orderRepository;
private HistoryFactory $orderHistoryFactory;
private StateData $stateDataHelper;

/**
* @param AdyenLogger $adyenLogger
* @param Vault $vaultHelper
* @param Order $orderResourceModel
* @param Data $dataHelper
* @param Quote $quoteHelper
*/
public function __construct(
AdyenLogger $adyenLogger,
Vault $vaultHelper,
Order $orderResourceModel,
Data $dataHelper,
Quote $quoteHelper
Quote $quoteHelper,
\Adyen\Payment\Helper\Order $orderHelper,
OrderRepository $orderRepository,
HistoryFactory $orderHistoryFactory,
StateData $stateDataHelper
) {
$this->adyenLogger = $adyenLogger;
$this->vaultHelper = $vaultHelper;
$this->orderResourceModel = $orderResourceModel;
$this->dataHelper = $dataHelper;
$this->quoteHelper = $quoteHelper;
$this->orderHelper = $orderHelper;
$this->orderRepository = $orderRepository;
$this->orderHistoryFactory = $orderHistoryFactory;
$this->stateDataHelper = $stateDataHelper;
}

public function formatPaymentResponse(
Expand Down Expand Up @@ -132,95 +123,156 @@ public function formatPaymentResponse(
}

/**
* @param array $paymentsResponse
* @param Payment $payment
* @param OrderInterface|null $order
* @param array $paymentsDetailsResponse
* @param OrderInterface $order
* @return bool
* @throws LocalizedException
* @throws AlreadyExistsException
* @throws InputException
* @throws NoSuchEntityException
*/
public function handlePaymentResponse(
array $paymentsResponse,
Payment $payment,
OrderInterface $order = null
):bool {
if (empty($paymentsResponse)) {
public function handlePaymentsDetailsResponse(
array $paymentsDetailsResponse,
OrderInterface $order
): bool {
if (empty($paymentsDetailsResponse)) {
$this->adyenLogger->error("Payment details call failed, paymentsResponse is empty");
return false;
}

if (!empty($paymentsResponse['resultCode'])) {
$payment->setAdditionalInformation('resultCode', $paymentsResponse['resultCode']);
$this->adyenLogger->addAdyenResult('Updating the order');
$payment = $order->getPayment();

$authResult = $paymentsDetailsResponse['authResult'] ?? $paymentsDetailsResponse['resultCode'] ?? null;
if (is_null($authResult)) {
// In case the result is unknown we log the request and don't update the history
$this->adyenLogger->error(
"Unexpected result query parameter. Response: " . json_encode($paymentsDetailsResponse)
);

return false;
}

if (!empty($paymentsResponse['action'])) {
$payment->setAdditionalInformation('action', $paymentsResponse['action']);
$paymentMethod = $paymentsDetailsResponse['paymentMethod']['brand'] ??
$paymentsDetailsResponse['paymentMethod']['type'] ??
'';

$pspReference = isset($paymentsDetailsResponse['pspReference']) ?
trim((string) $paymentsDetailsResponse['pspReference']) :
'';

$type = 'Adyen paymentsDetails response:';
$comment = __(
'%1 <br /> authResult: %2 <br /> pspReference: %3 <br /> paymentMethod: %4',
$type,
$authResult,
$pspReference,
$paymentMethod
);

if (!empty($paymentsDetailsResponse['resultCode'])) {
$payment->setAdditionalInformation('resultCode', $paymentsDetailsResponse['resultCode']);
}

if (!empty($paymentsResponse['additionalData'])) {
$payment->setAdditionalInformation('additionalData', $paymentsResponse['additionalData']);
if (!empty($paymentsDetailsResponse['action'])) {
$payment->setAdditionalInformation('action', $paymentsDetailsResponse['action']);
}

if (!empty($paymentsResponse['pspReference'])) {
$payment->setAdditionalInformation('pspReference', $paymentsResponse['pspReference']);
if (!empty($paymentsDetailsResponse['additionalData'])) {
$payment->setAdditionalInformation('additionalData', $paymentsDetailsResponse['additionalData']);
}

if (!empty($paymentsResponse['details'])) {
$payment->setAdditionalInformation('details', $paymentsResponse['details']);
if (!empty($paymentsDetailsResponse['pspReference'])) {
$payment->setAdditionalInformation('pspReference', $paymentsDetailsResponse['pspReference']);
}

switch ($paymentsResponse['resultCode']) {
case self::PRESENT_TO_SHOPPER:
case self::PENDING:
case self::RECEIVED:
case self::IDENTIFY_SHOPPER:
case self::CHALLENGE_SHOPPER:
break;
//We don't need to handle these resultCodes
case self::REDIRECT_SHOPPER:
$this->adyenLogger->addAdyenResult("Customer was redirected.");
if ($order) {
$order->addStatusHistoryComment(
__(
'Customer was redirected to an external payment page. (In case of card payments the shopper is redirected to the bank for 3D-secure validation.) Once the shopper is authenticated,
the order status will be updated accordingly.
<br />Make sure that your notifications are being processed!
<br />If the order is stuck on this status, the shopper abandoned the session.
The payment can be seen as unsuccessful.
<br />The order can be automatically cancelled based on the OFFER_CLOSED notification.
Please contact Adyen Support to enable this.'
),
$order->getStatus()
)->save();
}
break;
if (!empty($paymentsDetailsResponse['details'])) {
$payment->setAdditionalInformation('details', $paymentsDetailsResponse['details']);
}

if (!empty($paymentsDetailsResponse['donationToken'])) {
$payment->setAdditionalInformation('donationToken', $paymentsDetailsResponse['donationToken']);
}

// Handle recurring details
$this->vaultHelper->handlePaymentResponseRecurringDetails($payment, $paymentsDetailsResponse);

// If the response is valid, update the order status.
if (!in_array($paymentsDetailsResponse['resultCode'], PaymentResponseHandler::ACTION_REQUIRED_STATUSES)) {
/*
* Change order state from pending_payment to new and expect authorisation webhook
* if no additional action is required according to /paymentsDetails response.
* Otherwise keep the order state as pending_payment.
*/
$order = $this->orderHelper->setStatusOrderCreation($order);
$this->orderRepository->save($order);
}

// Cleanup state data if exists.
try {
$this->stateDataHelper->cleanQuoteStateData($order->getQuoteId(), $authResult);
} catch (Exception $exception) {
$this->adyenLogger->error(__('Error cleaning the payment state data: %s', $exception->getMessage()));
}

switch ($paymentsDetailsResponse['resultCode']) {
case self::AUTHORISED:
if (!empty($paymentsResponse['pspReference'])) {
if (!empty($paymentsDetailsResponse['pspReference'])) {
// set pspReference as transactionId
$payment->setCcTransId($paymentsResponse['pspReference']);
$payment->setLastTransId($paymentsResponse['pspReference']);
$payment->setCcTransId($paymentsDetailsResponse['pspReference']);
$payment->setLastTransId($paymentsDetailsResponse['pspReference']);

// set transaction
$payment->setTransactionId($paymentsResponse['pspReference']);
}

// Handle recurring details
$this->vaultHelper->handlePaymentResponseRecurringDetails($payment, $paymentsResponse);

if (!empty($paymentsResponse['donationToken'])) {
$payment->setAdditionalInformation('donationToken', $paymentsResponse['donationToken']);
$payment->setTransactionId($paymentsDetailsResponse['pspReference']);
}

$this->orderResourceModel->save($order);
try {
$this->quoteHelper->disableQuote($order->getQuoteId());
} catch (Exception $e) {
$this->adyenLogger->error('Failed to disable quote: ' . $e->getMessage(), [
'quoteId' => $order->getQuoteId()
]);
}

$result = true;
break;
case self::PENDING:
/* Change order state from pending_payment to new and expect authorisation webhook
* if no additional action is required according to /paymentDetails response. */
$order = $this->orderHelper->setStatusOrderCreation($order);
$this->orderRepository->save($order);

// do nothing wait for the notification
if (strpos((string) $paymentMethod, "bankTransfer") !== false) {
$comment .= "<br /><br />Waiting for the customer to transfer the money.";
} elseif ($paymentMethod == "sepadirectdebit") {
$comment .= "<br /><br />This request will be send to the bank at the end of the day.";
} else {
$comment .= "<br /><br />The payment result is not confirmed (yet).
<br />Once the payment is authorised, the order status will be updated accordingly.
<br />If the order is stuck on this status, the payment can be seen as unsuccessful.
<br />The order can be automatically cancelled based on the OFFER_CLOSED notification.
Please contact Adyen Support to enable this.";
}
$this->adyenLogger->addAdyenResult('Do nothing wait for the notification');

$result = true;
break;
case self::PRESENT_TO_SHOPPER:
case self::IDENTIFY_SHOPPER:
case self::CHALLENGE_SHOPPER:
case self::REDIRECT_SHOPPER:
$this->adyenLogger->addAdyenResult("Additional action is required for the payment.");
$result = true;
break;
case self::RECEIVED:
$result = true;
if (str_contains((string)$paymentMethod, "alipay_hk")) {
$result = false;
}
$this->adyenLogger->addAdyenResult('Do nothing wait for the notification');
break;
case self::REFUSED:
case self::CANCELLED:
// Cancel order in case result is refused
if (null !== $order) {
// Set order to new so it can be cancelled
Expand All @@ -229,17 +281,32 @@ public function handlePaymentResponse(
$order->setActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_CANCEL, true);
$this->dataHelper->cancelOrder($order);
}
return false;
case self::ERROR:
$result = false;
break;
default:
$this->adyenLogger->error(
sprintf("Payment details call failed for action, resultCode is %s Raw API responds: %s",
$paymentsResponse['resultCode'],
json_encode($paymentsResponse)
sprintf("Payment details call failed for action, resultCode is %s Raw API responds: %s.
Cancel or Hold the order on OFFER_CLOSED notification.",
$paymentsDetailsResponse['resultCode'],
json_encode($paymentsDetailsResponse)
));

return false;
$result = false;
break;
}
return true;

$history = $this->orderHistoryFactory->create()
->setStatus($order->getStatus())
->setComment($comment)
->setEntityName('order')
->setOrder($order);

$history->save();

// needed because then we need to save $order objects
$order->setAdyenResulturlEventCode($authResult);
$this->orderResourceModel->save($order);

return $result;
}
}
Loading

0 comments on commit c8ba5d0

Please sign in to comment.