Skip to content

Commit

Permalink
Merge pull request #1239 from BearGroup/release/5.17.1
Browse files Browse the repository at this point in the history
Release/5.17.1
  • Loading branch information
akshitaWaldia authored Jun 18, 2024
2 parents ccf7589 + 8a18fb1 commit 50ec143
Show file tree
Hide file tree
Showing 13 changed files with 456 additions and 30 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Change Log

## 5.17.1
* Changed php allowed versions to include 8.3
* Fixed issue where orders could be processing but not capture payment
* Fixed issue with amazon-product-add.js 404 not found (thanks @tim-breitenstein-it!)
* Fixed issue where a variable could be undefined (thanks @dimitriBouteiile!)
* Fixed issue where incorrect message "can't create invoice" could be displayed

## 5.17.0
* Changed sequence of placing Magento order/processing Amazon payment to reduce likelihood of
transactions with no associated order IDs in Seller Central
Expand Down
7 changes: 6 additions & 1 deletion Controller/Checkout/PlaceOrder.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@ public function execute()
$amazonCheckoutSessionId = $this->request->getParam('amazonCheckoutSessionId');

$result = $this->amazonCheckoutSessionManagement->placeOrder($amazonCheckoutSessionId);
if (!$result['success']) {

if ($result['success']) {
// for orders placed before payment authorization (Express checkout)
$this->amazonCheckoutSessionManagement->setOrderPendingPaymentReview($result['order_id'] ?? null);
} else {
$this->messageManager->addErrorMessage($result['message']);
}

} catch (\Exception $e) {
$this->exceptionLogger->logException($e);
$this->messageManager->addErrorMessage($e->getMessage());
Expand Down
182 changes: 182 additions & 0 deletions Cron/CleanUpIncompleteSessions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?php
/**
* Copyright © Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

namespace Amazon\Pay\Cron;

use Amazon\Pay\Helper\Transaction as TransactionHelper;
use Amazon\Pay\Model\Adapter\AmazonPayAdapter;
use Amazon\Pay\Model\AsyncManagement\Charge;
use Amazon\Pay\Model\CheckoutSessionManagement;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Psr\Log\LoggerInterface;
use Amazon\Pay\Model\AsyncManagement\Charge as AsyncCharge;

class CleanUpIncompleteSessions
{
public const SESSION_STATUS_STATE_CANCELED = 'Canceled';
public const SESSION_STATUS_STATE_OPEN = 'Open';
public const SESSION_STATUS_STATE_COMPLETED = 'Completed';

protected const LOG_PREFIX = 'AmazonCleanUpIncompleteSesssions: ';

/**
* @var TransactionHelper
*/
protected $transactionHelper;

/**
* @var LoggerInterface
*/
protected $logger;

/**
* @var AmazonPayAdapter
*/
protected $amazonPayAdapter;

/**
* @var CheckoutSessionManagement
*/
protected $checkoutSessionManagement;

/**
* @var OrderRepositoryInterface
*/
protected $orderRepository;

/**
* @var AsyncCharge
*/
protected $asyncCharge;

/**
* @param TransactionHelper $transactionHelper
* @param LoggerInterface $logger
* @param AmazonPayAdapter $amazonPayAdapter
* @param CheckoutSessionManagement $checkoutSessionManagement
* @param OrderRepositoryInterface $orderRepository
* @param AsyncCharge $asyncCharge
*/
public function __construct(
TransactionHelper $transactionHelper,
LoggerInterface $logger,
AmazonPayAdapter $amazonPayAdapter,
CheckoutSessionManagement $checkoutSessionManagement,
OrderRepositoryInterface $orderRepository,
AsyncCharge $asyncCharge
) {
$this->transactionHelper = $transactionHelper;
$this->logger = $logger;
$this->amazonPayAdapter = $amazonPayAdapter;
$this->checkoutSessionManagement = $checkoutSessionManagement;
$this->orderRepository = $orderRepository;
$this->asyncCharge = $asyncCharge;
}

/**
* Execute cleanup
*
* @return void
*/
public function execute()
{
// Get transactions
$incompleteTransactionList = $this->transactionHelper->getIncomplete();

// Process each transaction
foreach ($incompleteTransactionList as $transactionData) {
$this->processTransaction($transactionData);
}
}

/**
* Process a single transaction
*
* @param array $transactionData
* @return void
*/
protected function processTransaction(array $transactionData)
{
$checkoutSessionId = $transactionData['checkout_session_id'];
$orderId = $transactionData['order_id'];

$this->logger->info(self::LOG_PREFIX . 'Cleaning up checkout session id: ' . $checkoutSessionId);

try {

// Check current state of Amazon checkout session
$amazonSession = $this->amazonPayAdapter->getCheckoutSession(null, $checkoutSessionId);
$state = $amazonSession['statusDetails']['state'] ?? false;
switch ($state) {
case self::SESSION_STATUS_STATE_CANCELED:
$logMessage = 'Checkout session Canceled, cancelling order and closing transaction: ';
$logMessage .= $checkoutSessionId;
$this->logger->info(self::LOG_PREFIX . $logMessage);
$this->cancelOrder($orderId);
$this->transactionHelper->closeTransaction($transactionData['transaction_id']);
break;
case self::SESSION_STATUS_STATE_OPEN:
$logMessage = 'Checkout session Open, completing: ';
$logMessage .= $checkoutSessionId;
$this->logger->info(self::LOG_PREFIX . $logMessage);
$this->checkoutSessionManagement->completeCheckoutSession($checkoutSessionId, null, $orderId);
break;
case self::SESSION_STATUS_STATE_COMPLETED:
$logMessage = 'Checkout session Completed, nothing more needed: ';
$logMessage .= $checkoutSessionId;
$this->logger->info(self::LOG_PREFIX . $logMessage);
break;
}
} catch (\Exception $e) {
$errorMessage = 'Unable to process checkoutSessionId: ' . $checkoutSessionId;
$this->logger->error(self::LOG_PREFIX . $errorMessage . '. ' . $e->getMessage());
}
}

/**
* Cancel the order
*
* @param int $orderId
* @return void
*/
protected function cancelOrder($orderId)
{
$order = $this->loadOrder($orderId);

if ($order) {
$this->checkoutSessionManagement->cancelOrder($order);
} else {
$this->logger->error(self::LOG_PREFIX . 'Order not found for ID: ' . $orderId);
}
}

/**
* Load order by ID
*
* @param int $orderId
* @return OrderInterface
*/
protected function loadOrder($orderId)
{
try {
return $this->orderRepository->get($orderId);
} catch (\Exception $e) {
$this->logger->error(self::LOG_PREFIX . 'Error loading order: ' . $e->getMessage());
return null;
}
}
}
150 changes: 150 additions & 0 deletions Helper/Transaction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php
/**
* Copyright © Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

namespace Amazon\Pay\Helper;

use Amazon\Pay\Gateway\Config\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Sales\Api\Data\TransactionInterface;
use Magento\Sales\Api\TransactionRepositoryInterface;
use Magento\Sales\Model\Order;

class Transaction
{

// Length of time in minutes we wait before cleaning up the transaction
protected const MIN_ORDER_AGE_MINUTES = 30;

/**
* @var int
*/
private $limit;

/**
* @var ResourceConnection
*/
private ResourceConnection $resourceConnection;

/**
* @var TransactionRepositoryInterface
*/
private TransactionRepositoryInterface $transactionRepository;

/**
* @param ResourceConnection $resourceConnection
* @param TransactionRepositoryInterface $transactionRepository
* @param int $limit
*/
public function __construct(
ResourceConnection $resourceConnection,
TransactionRepositoryInterface $transactionRepository,
int $limit = 100
) {
$this->limit = $limit;
$this->resourceConnection = $resourceConnection;
$this->transactionRepository = $transactionRepository;
}

/**
* Query for possible incomplete transactions
*
* @return array
*/
public function getIncomplete()
{
// Do not process recent orders, synchronous ones need time to be
// resolved in payment gateway on auth decline
// todo confirm timeout length in gateway
$maxOrderPlacedTime = $this->getMaxOrderPlacedTime();

$connection = $this->resourceConnection->getConnection();

// tables used to determine stalled order status
$salesOrderTable = $connection->getTableName('sales_order');
$salesOrderPaymentTable = $connection->getTableName('sales_order_payment');
$salesPaymentTransaction = $connection->getTableName('sales_payment_transaction');
$amazonPayAsyncTable = $connection->getTableName('amazon_payv2_async');

// specifying(limiting) columns is unnecessary, but helpful for debugging
// pending actions:
// captures for "charge when order is placed" payment action
// authorizations for "charge when shipped" payment action
$tableFields = [
'sales_order' => ['order_id' => 'entity_id', 'store_id', 'increment_id', 'created_at', 'state'],
'sales_order_payment' => ['method'],
'sales_payment_transaction' => ['transaction_id', 'checkout_session_id' => 'txn_id', 'is_closed'],
'amazon_payv2_async' => ['pending_action', 'is_pending']
];

$select = $connection->select()
->from(['so' => $salesOrderTable], $tableFields['sales_order'])
->joinLeft(
['sop' => $salesOrderPaymentTable],
'so.entity_id = sop.parent_id',
$tableFields['sales_order_payment']
)
->joinLeft(
['spt' => $salesPaymentTransaction],
'sop.entity_id = spt.payment_id',
$tableFields['sales_payment_transaction']
)
->joinLeft(
['apa' => $amazonPayAsyncTable],
'spt.txn_id = apa.pending_id',
$tableFields['amazon_payv2_async']
)
// No async record pending
->where('apa.pending_action IS NULL')
// Order awaiting payment
->where("so.status = ?", Order::STATE_PAYMENT_REVIEW)
// A transaction is not complete
->where('spt.is_closed <> ?', 1)
// Delay processing new orders
->where('so.created_at <= ?', $maxOrderPlacedTime)
// Amazon Pay orders only
->where('sop.method = ?', Config::CODE)
// Meter to reduce load
->limit($this->limit);

// Return stalled orders
return $connection->fetchAll($select);
}

/**
* Close transaction and save
*
* @param mixed $transactionId
* @return void
*/
public function closeTransaction(mixed $transactionId)
{
$transaction = $this->transactionRepository->get($transactionId);
$transaction->setIsClosed(true);
$this->transactionRepository->save($transaction);
}

/**
* Use db time to reduce likelihood of server/db time mismatch
*
* @return string
*/
private function getMaxOrderPlacedTime()
{
// phpcs:ignore Magento2.SQL.RawQuery
$query = 'SELECT NOW() - INTERVAL ' . self::MIN_ORDER_AGE_MINUTES . ' MINUTE';
return $this->resourceConnection->getConnection()->fetchOne($query);
}
}
16 changes: 15 additions & 1 deletion Model/AsyncManagement/Charge.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Magento\Sales\Api\Data\InvoiceInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Invoice;
use Magento\Framework\Event\ManagerInterface;

class Charge extends AbstractOperation
Expand Down Expand Up @@ -309,13 +310,26 @@ public function authorize($order, $chargeId)
*/
public function capture($order, $chargeId, $chargeAmount)
{
// Try to load invoice based on charge ID or from the order
$invoice = $this->loadInvoice($chargeId, $order);

// Create invoice if we didn't find one for the chargeId but can invoice
if (!$invoice && $order->canInvoice()) {
$invoice = $this->invoiceService->prepareInvoice($order);
$invoice->register();
}

if ($invoice && ($invoice->canCapture() || $invoice->getOrder()->getStatus() == Order::STATE_PAYMENT_REVIEW)) {
// Finally, load from the order if all else fails
if (!$invoice) {
$invoice = $order->getInvoiceCollection()->getFirstItem();
}

if ($invoice
&& (
$invoice->canCapture()
|| $invoice->getOrder()->getStatus() == Order::STATE_PAYMENT_REVIEW
|| $invoice->getState() == Invoice::STATE_OPEN
)) {
$order = $invoice->getOrder();
$this->setProcessing($order);
$payment = $order->getPayment();
Expand Down
Loading

0 comments on commit 50ec143

Please sign in to comment.