Rewards Points Unreturned - Incomplete Order Payment DeclineThis article was co-authored by Seth Daugherty, a full-stack web developer here at Human-Element.


We recently found a Magento 2 workflow bug which caused the Reward Points module to decrement points from a customer’s account, even in the event of a failed checkout. In this article, we will discuss this problem and present its surprisingly simple solution. Seth discussed the issue here on Stack Overflow, but we are going to get a little more into the nitty gritty here.


TL;DR: If you just want to grab the fixes in a module format, download this zip file and copy it into ‘[webroot]/app/code/’. You can skip to The last step in this article to finish the module installation.
Magento 2 Enterprise Extension: Download the Authorize.net DirectPost and Rewards Fix Here.
Magento 2 Community Extension: Download the Authorize.net DirectPost Fix Here.


Issue Synopsis: Returning Rewards Points When for Incomplete Order

A customer is given 100 reward points to redeem during checkout. The customer begins checkout with an item totalling 150.00. After the 100 points are subtracted from the total, there is still 50.00 left. The customer pays the remaining 50.00 using Authorize.net, but the payment is declined. In the background, Magento 2 creates the order and then cancels it, but the 100 reward points are never returned to the customer account. In the end, the customer loses their reward points, but never actually completes checkout.

Understanding the Cause

The reason for this behavior is that in the core ‘Magento\Sales\Model\Order’ there is a “cancel” and “registerCancellation” function. The standard “cancel” function calls “registerCancellation” which does the heavy lifting. However, “cancel” also fires an event called “order_cancel_after” which a few other core modules use to apply their order cancellation business logic. The event “order_cancel_after” not getting fired is the real issue. By skipping the “cancel” method we miss out on all of the custom business logic that is attached to this event.

Implementing the Fix to Authorize.net DirectPost

In the Magento 2 core Authorize.net module, we found that the method that handled declining payments was skipping the call to “cancel” and going directly to “registerCancellation”. See the “declineOrder” function located at “\Magento\Authorizenet\Model\Directpost”.

There does not seem to be a case where you would want to skip calling the “cancel” function, so we created a preference in our HumanElement module that simply changes the call from “registerCancellation” to “cancel”.

Here is the updated code:

<\?php namespace HumanElement\Authorizenet\Model; /** * Authorize.net DirectPost payment method model. * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class Directpost extends \Magento\Authorizenet\Model\Directpost { /** * Register order cancellation. Return money to customer if needed. * * @param \Magento\Sales\Model\Order $order * @param string $message * @param bool $voidPayment * @return void */ protected function declineOrder(\Magento\Sales\Model\Order $order, $message = '', $voidPayment = true) { try { $response = $this->getResponse();
            if (
                $voidPayment && $response->getXTransId() && strtoupper($response->getXType())
                == self::REQUEST_TYPE_AUTH_ONLY
            ) {
                $order->getPayment()->setTransactionId(null)->setParentTransactionId($response->getXTransId())->void();
            }

            // Call cancel instead of registerCancellation
            $order->cancel();

            // Preserve comment being added to order
            if (!empty($message)) {
                $order->addStatusHistoryComment($message, false);
            }

            // Save order
            $order->save();
        } catch (\Exception $e) {
            //quiet decline
            $this->getPsrLogger()->critical($e);
        }
    }
}

Implementing the Fix to Magento 2 EE Reward

After we found that the “declineOrder” method in Authorize.net was not firing the “order_cancel_after” it was easy to understand why points were not getting re-applied to the order. However, even after applying the fix above, we found that the business logic in the Rewards module was not being run. This turned out to be an issue with where the event was placed. In Magento 2 there are 3 scopes for events; global, adminhtml, and frontend. The event in the Rewards module was only looking in the adminhtml scope.

The solution for this particular problem ended up being relatively simple, in that we just added an event observer in the frontend scope to listen for “order_cancel_after”. After this, our Rewards business logic was able to run and reapply points to the customer account.

<\config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <\event name="order_cancel_after">
        <\observer name="magento_reward" instance="Magento\Reward\Observer\ReturnRewardPoints" />
    <\/event>
<\/config>

Apply the Module and Let Magento Know

Copy the following packages into ‘[webroot]/app/code/’.

Magento 2 Enterprise Extension: Download the Authorize.net DirectPost and Rewards Fix Here.
Magento 2 Community Extension: Download the Authorize.net DirectPost Fix Here.
Now that we have these fixes in place, all we need to do is let Magento know by running the following:

cd [webroot]
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento cache:flush

That’s it! So easy, right?

If you found that applying these fixes helped you out, please let us know. If they didn’t help, let us know that, too! We welcome the feedback. Please leave a comment and we will be happy to help as much as we can.