Skip to content

Commit

Permalink
Merge pull request #14 from Dinwy/feature/restoreQuote
Browse files Browse the repository at this point in the history
Restore Quote when clicking browser back button
  • Loading branch information
Dinwy authored Jun 5, 2024
2 parents fd48abe + 772ddf4 commit b30ad93
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 69 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
name: lint

on:
- push
push:
branches:
- master
pull_request:
branches:
- master

jobs:
php-cs-fixer:
Expand Down
63 changes: 63 additions & 0 deletions docs/events_and_observers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Magento Events and Observers

## Introduction to Magento Events

Magento utilizes an event-driven architecture to allow for extending and customizing the behavior of the core system and third-party components without modifying the original source code. This architecture revolves around the concept of events and observers:

- Events: These are specific points in the Magento codebase where Magento announces that something has occurred, such as saving an item, loading a page, or completing a transaction.
- Observers: These are custom modules or scripts that 'listen' for specific events and execute code in response to them. This allows developers to hook into the application lifecycle to add or modify functionality dynamically.

## How Events Work in Magento

When an event is triggered in Magento, any observers that are configured to listen to that event are executed. This mechanism is managed by Magento's event dispatch system, which handles the mapping of events to their respective observers. The process can be summarized as follows:

- Event Triggering: An event is triggered by calling a method like $this->_eventManager->dispatch('event_name', ['data' => $data]) from within Magento's core code or a custom module.
- Observer Execution: Once an event is dispatched, Magento checks for observers registered for that event and executes their execute method, passing an Observer object that contains any relevant data about the event.

## Configuration of Observers

Observers are configured in a module's events.xml file, which is located in the module's etc directory. The file specifies which events an observer is listening to and the class that contains the logic to be executed when the event is triggered.

Example of an events.xml configuration:

```xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="controller_action_postdispatch_checkout_index_index">
<observer name="restore_quote_from_session" instance="Komoju\Payments\Observer\RestoreQuoteFromSession" />
</event>
</config>
```

## Restore Quote Session Observer
### What is the Restore Quote Session?

The `RestoreQuoteFromSession` observer is designed to enhance the user experience during the checkout process. It specifically addresses scenarios where a customer may not complete their payment, leaving their order in a pending_payment state.

### Why This Functionality Is Important

Restoring a quote to the session in cases where the payment was not completed is crucial for the following reasons:

- User Convenience: It provides a seamless experience for users who may have navigated away from the checkout page or experienced an interruption during the payment process.
- Increased Conversion Rates: By allowing users to easily return to their last state in the checkout process, it potentially increases the likelihood of completing the sale.
- Reduced Frustration: Minimizes customer frustration by eliminating the need to re-add products to the cart and re-enter information.

```php
public function execute(Observer $observer)
{
$quote = $this->checkoutSession->getQuote();

if ($quote) {
$order = $this->checkoutSession->getLastRealOrder();

if ($order) {
$orderStatus = $order->getStatus();

if ($orderStatus == ORDER::STATE_PENDING_PAYMENT) {
$this->checkoutSession->restoreQuote();
}
}
}
}
```
Explanation:
- State Check and Action: Only if the order's status matches Order::STATE_PENDING_PAYMENT then perform the action to restore the quote.
27 changes: 11 additions & 16 deletions src/app/code/Komoju/Payments/Api/KomojuApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,18 @@

use Komoju\Payments\Exception\KomojuExceptionBadServer;
use Komoju\Payments\Exception\InvalidJsonException;
use Komoju\Payments\Gateway\Config\Config;
use Magento\Framework\HTTP\Client\Curl;
use Exception;

class KomojuApi
{
/**
* @var \Komoju\Payments\Gateway\Config\Config
*/
private $config;

/**
* @var \Magento\Framework\HTTP\Client\Curl
*/
private $curl;

public function __construct(
\Komoju\Payments\Gateway\Config\Config $config,
\Magento\Framework\HTTP\Client\Curl $curl
) {
private Config $config;
private Curl $curl;
private string $endpoint;

public function __construct(Config $config, Curl $curl)
{
$this->endpoint = 'https://komoju.com';
$this->config = $config;
$this->curl = $curl;
Expand Down Expand Up @@ -69,7 +64,7 @@ private function get($uri)

$decoded = json_decode($body);

if (! empty(json_last_error())) {
if (!empty(json_last_error())) {
$errorMsg = (__("KOMOJU Payments JSON Decoding Failure. Error: %1", json_last_error_msg()));
throw new InvalidJsonException($errorMsg);
}
Expand Down Expand Up @@ -108,7 +103,7 @@ private function post($uri, $payload)

$decoded = json_decode($body);

if (! empty(json_last_error())) {
if (!empty(json_last_error())) {
$errorMsg = (__("KOMOJU Payments JSON Decoding Failure. Error: %1", json_last_error_msg()));
throw new InvalidJsonException($errorMsg);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
use Magento\Framework\App\ObjectManager;
use Magento\Sales\Model\Order;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\App\Action\Action;
use Magento\Sales\Api\OrderRepositoryInterface;
use Komoju\Payments\Gateway\Config\Config;
use Psr\Log\LoggerInterface;
use Magento\Checkout\Model\Session;
use Komoju\Payments\Api\KomojuApi;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Event\ManagerInterface;

/**
* The PostSessionRedirect endpoint serves as the return URL param for Komoju
Expand All @@ -17,51 +25,24 @@
* the PostSessionRedirect URL, which is being used to ensure that the request hasn't been
* tampered with.
*/
class PostSessionRedirect extends \Magento\Framework\App\Action\Action
class PostSessionRedirect extends Action
{
/**
* @var \Magento\Framework\Controller\ResultFactory
*/
protected $_resultFactory;

/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;

/**
* @var \Magento\Checkout\Model\Session
*/
protected $_checkoutSession;

/**
* @var \Komoju\Payments\Gateway\Config\Config
*/
private $config;

/**
* @var \Magento\Sales\Model\Order|false
*/
private $order = false;

/**
* @var \Magento\Sales\Api\OrderRepositoryInterface
*/
private $orderRepository;

/**
* @var \Komoju\Payments\Api\KomojuApi
*/
private $komojuApi;
protected ResultFactory $_resultFactory;
private LoggerInterface $logger;
protected Session $_checkoutSession;
private Config $config;
private OrderRepositoryInterface $orderRepository;
private KomojuApi $komojuApi;
private ManagerInterface $eventManager;

public function __construct(
\Magento\Sales\Api\OrderRepositoryInterface $orderRepository,
\Magento\Framework\Controller\ResultFactory $resultFactory,
\Magento\Framework\App\Action\Context $context,
\Komoju\Payments\Gateway\Config\Config $config,
\Psr\Log\LoggerInterface $logger = null,
\Magento\Checkout\Model\Session $checkoutSession,
\Komoju\Payments\Api\KomojuApi $komojuApi
OrderRepositoryInterface $orderRepository,
ResultFactory $resultFactory,
Context $context,
Config $config,
LoggerInterface $logger = null,
Session $checkoutSession,
KomojuApi $komojuApi
) {
$this->logger = $logger ?: ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class);
$this->_resultFactory = $resultFactory;
Expand All @@ -87,7 +68,7 @@ public function execute()
$resultRedirect = $this->_resultFactory->create(ResultFactory::TYPE_REDIRECT);

if ($this->isSessionCompleted()) {
$successUrl = $this->_url->getUrl('checkout/onepage/success');
$successUrl = $this->processSuccessOrder();
$resultRedirect->setUrl($successUrl);
} else {
$redirectUrl = $this->processFailedOrder();
Expand All @@ -96,6 +77,11 @@ public function execute()
return $resultRedirect;
}

private function processSuccessOrder()
{
return $this->_url->getUrl('checkout/onepage/success');
}

/**
* If an order can be cancelled, cancel the order and restore the items to cart
* and return the checkout url. Otherwise return the home page url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class Redirect extends \Magento\Framework\App\Action\Action
* @var \Magento\Directory\Model\CountryFactory
*/
private $_countryFactory;
private $storeManager;
private $orderRepository;

public function __construct(
\Magento\Framework\App\Action\Context $context,
Expand Down
11 changes: 7 additions & 4 deletions src/app/code/Komoju/Payments/Gateway/Config/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
namespace Komoju\Payments\Gateway\Config;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\UrlInterface;
use Magento\Framework\Locale\Resolver as LocaleResolver;
use Magento\Payment\Gateway\Config\Config as PaymentConfig;

/**
* This class provides a programmatic interface between the rest of the module
* and the options set for the module in the admin panel.
*/
class Config extends \Magento\Payment\Gateway\Config\Config
class Config extends PaymentConfig
{

private $urlInterface;
private UrlInterface $urlInterface;
private LocaleResolver $locale;

/**
* Komoju config constructor
Expand Down Expand Up @@ -40,7 +43,7 @@ public function __construct(
* @param int|null $storeId
* @return bool
*/
public function isActive($storeId = null)
public function isActive($storeId = null): bool
{
return (bool) $this->getValue('active', $storeId);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/code/Komoju/Payments/Model/ExternalPayment.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protected function _construct()
{
$this->_init(\Komoju\Payments\Model\ResourceModel\ExternalPayment::class);
}

/**
* Creates an external payment object. The external payment id is created using
* PHP's uniqid value, combined with the order id. This creates a very low chance
Expand Down
2 changes: 2 additions & 0 deletions src/app/code/Komoju/Payments/Model/Ui/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class ConfigProvider implements ConfigProviderInterface
*/
private $komojuApi;

private $scopeConfig;

/**
* Constructor
*
Expand Down
6 changes: 3 additions & 3 deletions src/app/code/Komoju/Payments/Model/WebhookEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ public function __construct($requestBody)

if (! empty(json_last_error())) {
$errorMsg = (__("KOMOJU Payments JSON Decoding Failure. Error: %1", json_last_error_msg()));

throw new Komoju\Payments\Exception\InvalidJsonException($errorMsg);
}
}
Expand Down Expand Up @@ -58,11 +57,12 @@ public function status()

/**
* A getter to retrieve the external_order_num from the webhook event
* @return string
* @return string|null
*/
public function externalOrderNum()
{
return $this->data()['external_order_num'];
$data = $this->data();
return isset($data['external_order_num']) ? $data['external_order_num'] : null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\App\ObjectManager;
use Psr\Log\LoggerInterface;

/**
* The PaymentMethodAvailable observer is a class that gets executed when the
Expand All @@ -29,6 +30,8 @@ class PaymentMethodAvailable implements ObserverInterface
/** @var string */
private $methodCode;

private LoggerInterface $logger;

/**
* Class constructor. The methodCode and allowableCurrencyCodes are passed in
* through dependency injection, and are configured in etc/di/xml.
Expand Down
55 changes: 55 additions & 0 deletions src/app/code/Komoju/Payments/Observer/RestoreQuoteFromSession.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Komoju\Payments\Observer;

use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Magento\Framework\Session\SessionManager;
use Magento\Sales\Model\Order;
use Magento\Quote\Api\CartRepositoryInterface;
use Psr\Log\LoggerInterface;

/**
* Observer for restoring a customer's quote during the checkout,
* if the last order state is pending payment.
*
* Configured to listen to `controller_action_postdispatch_checkout_index_index` event.
* It verifies if the last real order within the session is still pending payment.
* If so, it restores the quote to the session.
* This allows customers to resume their cart and complete the transaction without re-entering info.
* For more information, please check the events_and_observers.md file in docs folder.
*/
class RestoreQuoteFromSession implements ObserverInterface
{
protected CheckoutSession $checkoutSession;
protected SessionManager $sessionManager;
protected CartRepositoryInterface $quoteRepository;
private LoggerInterface $logger;

public function __construct(
CheckoutSession $checkoutSession,
SessionManager $sessionManager,
CartRepositoryInterface $quoteRepository,
LoggerInterface $logger = null
) {
$this->checkoutSession = $checkoutSession;
$this->sessionManager = $sessionManager;
$this->quoteRepository = $quoteRepository;
$this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class);
}

public function execute(Observer $observer)
{
$quote = $this->checkoutSession->getQuote();

if ($quote) {
$order = $this->checkoutSession->getLastRealOrder();

if ($order && $order->getStatus() == Order::STATE_PENDING_PAYMENT) {
$this->checkoutSession->restoreQuote();
}
}
}
}
5 changes: 4 additions & 1 deletion src/app/code/Komoju/Payments/etc/events.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
<event name="payment_method_is_active">
<observer name="disable_payment" instance="Komoju\Payments\Observer\PaymentMethodAvailable" />
</event>
</config>
<event name="controller_action_postdispatch_checkout_index_index">
<observer name="restore_quote_from_session" instance="Komoju\Payments\Observer\RestoreQuoteFromSession" />
</event>
</config>

0 comments on commit b30ad93

Please sign in to comment.