As defined in app/code/core/Mage/Authorizenet/Model/Directpost.php, this exception is thrown if any portion of the below evaluates as false:
if (!$this->getConfigData('trans_md5') || !$this->getConfigData('login') ||
!$response->isValidHash($this->getConfigData('trans_md5'), $this->getConfigData('login'))
) {
Mage::throwException(
Mage::helper('authorizenet')->__('Response hash validation failed. Transaction declined.')
);
}
So, it's one of three things:
The method isValidHash
returns false because there is an issue with either the generated hash (e.g. your API Login/Key are wrong, generate a new one and re-enter) or the amount of the transaction somehow differs between Authorize and your system.
The latter is less likely, but because generateHash
in Mage_Authorizenet_Model_Directpost_Response
uses the transaction amount as part of the hash, it's a potential factor here.
The fact that this endpoint defines result of the call as single int instead of at least object with order id and extension attribute is awful and probably won't change. We will see what the Magento team will come up for 2.3 when they will introduce their own PWA and hopefully create new endpoint with better support for 3rd party extensions. Right now however there are 2 possibilities to do:
Assuming you are making a request to /V1/guest-carts/:cartId/payment-information
or /V1/carts/mine/payment-information
you can:
If your payment gateway requires GET
redirect simply hook after Magento\Checkout\Api\PaymentInformationManagementInterface::savePaymentInformationAndPlaceOrder()
and set redirect url according to your needs. Magento should recognize redirect header.
If your redirect need to be actually a POST
request you need to override definition of the endpoint and provide your own interface which will declare more reasonable result which you will be able to process in the browser. This however will require to make sure native Magento classes are also covered. If you are working on the project for single client this might work.
Create your own module, ie. Vendor_CheckoutExt
as separate composer module or in app/code/Vendor/CheckoutExt
. Make sure to add Magento_Checkout
to sequence
tag in module.xml
so your definition will be read after magento one.
In etc/webapi.xml
put a definition like:
<route url="/V1/carts/mine/payment-information" method="POST">
<service class="Vendor\CheckoutExt\Api\PaymentInformationManagementInterface" method="savePaymentInformationAndPlaceOrder"/>
<resources>
<resource ref="self" />
</resources>
<data>
<parameter name="cartId" force="true">%cart_id%</parameter>
</data>
</route>
Create an interface Vendor\CheckoutExt\Api\PaymentInformationManagementInterface
to look like
namespace Vendor\CheckoutExt\Api;
use Magento\Checkout\Api\PaymentInformationManagementInterface as MagentoPaymentInformationManagementInterface;
interface PaymentInformationManagementInterface extends MagentoPaymentInformationManagementInterface
{
/**
* Set payment information and place order for a specified cart.
*
* @param int $cartId
* @param \Magento\Quote\Api\Data\PaymentInterface $paymentMethod
* @param \Magento\Quote\Api\Data\AddressInterface|null $billingAddress
* @throws \Magento\Framework\Exception\CouldNotSaveException
* @return \Vendor\CheckoutExt\Api\Data\ResultInterface place order result data.
*/
public function savePaymentInformationAndPlaceOrder(
$cartId,
\Magento\Quote\Api\Data\PaymentInterface $paymentMethod,
\Magento\Quote\Api\Data\AddressInterface $billingAddress = null
);
}
Now create interface for the response
namespace Vendor\CheckoutExt\Api\Data;
interface ResultInterface
{
/**
* @param string $orderId
* @return Vendor\CheckoutExt\Api\Data\ResultInterface
*/
public function setOrderId($orderId);
/**
* @return string
*/
public function getOrderId();
/**
* Retrieve existing extension attributes object or create a new one.
*
* @return Vendor\CheckoutExt\Api\Data\ResultInterface
*/
public function getExtensionAttributes();
/**
* Set an extension attributes object.
*
* @param Vendor\CheckoutExt\Api\Data\ResultExtensionInterface $extensionAttributes
* @return $this
*/
public function setExtensionAttributes(
Vendor\CheckoutExt\Api\Data\ResultExtensionInterface $extensionAttributes
);
}
We have interfaces ready so rest api module will be able to use it to prepare output. Now we need to implement them somehow to actually make the request work. Here there are 2 possibilities, you can either simply make a preference for the original one and prepare output using plugin or implement it itself. Let's go with the first option, in etc/di.xml
or better in etc/webapi/di.xml
define preference
<preference for="Vendor\CheckoutExt\Api\PaymentInformationManagementInterface" type="\Magento\Checkout\Model\PaymentInformationManagement" />
This will work because our interface extends native Magento one and we didn't change the function definition, only what should be returned from the function. But Magento class returns simple integer and just because we defined other output Magento won't generate it. We need to to this. So first let's implement class that we will use in the response
<preference for="Vendor\CheckoutExt\Api\Data\ResultInterface" type="Vendor\CheckoutExt\Model\Data\OrderResponse" />
namespace Vendor\CheckoutExt\Model\Data;
use Vendor\CheckoutExt\Api\Data\ResultInterface;
use Vendor\CheckoutExt\Api\Data\ResultExtensionInterface;
use Magento\Framework\Model\AbstractExtensibleModel;
class Result extends AbstractExtensibleModel implements ResultInterface
{
/**
* @param string $orderId
* @return \Vendor\CheckoutExt\Api\Data\ResultInterface
*/
public function setOrderId($orderId)
{
return $this->setData(self::ORDER_ID, $orderId);
}
/**
* @return string
*/
public function getOrderId()
{
return $this->_getData(self::ORDER_ID);
}
/**
* @param string $incrementId
* @return \Vendor\CheckoutExt\Api\Data\ResultInterface
*/
public function setOrderRealId($incrementId)
{
return $this->setData(self::ORDER_REAL_ID, $incrementId);
}
/**
* @return string
*/
public function getOrderRealId()
{
return $this->_getData(self::ORDER_REAL_ID);
}
/**
* @return \Vendor\CheckoutExt\Api\Data\ResultExtensionInterface
*/
public function getExtensionAttributes()
{
$extensionAttributes = $this->_getExtensionAttributes();
if (!$extensionAttributes) {
/** @var ResultExtensionInterface $extensionAttributes */
$extensionAttributes = $this->extensionAttributesFactory->create(ResultInterface::class);
}
return $extensionAttributes;
}
/**
* @param \Vendor\CheckoutExt\Api\Data\ResultExtensionInterface $extensionAttributes
* @return \Vendor\CheckoutExt\Api\Data\ResultInterface
*/
public function setExtensionAttributes(ResultExtensionInterface $extensionAttributes)
{
return $this->_setExtensionAttributes($extensionAttributes);
}
}
And last piece of the puzzle is the plugin which will hook after original place order method is called to change returned integer into object we need. Again in di.xml
define
<type name="Magento\Checkout\Api\PaymentInformationManagementInterface">
<plugin name="vendorCheckoutExtSaveOrderResultPlugin" type="Vendor\CheckoutExt\Plugin\Checkout\PaymentInformationManagement" sortOrder="1" />
</type>
And the plugin code
namespace Vendor\CheckoutExt\Plugin\Checkout;
use Vendor\CheckoutExt\Api\Data\ResultInterface;
use Vendor\CheckoutExt\Api\Data\ResultInterfaceFactory;
use Magento\Checkout\Api\PaymentInformationManagementInterface;
class PaymentInformationManagement
{
/** @var ResultFactory */
private $resultFactory;
/**
* @param ResultInterfaceFactory $resultFactory
*/
public function __construct(ResultInterfaceFactory $resultFactory)
{
$this->resultFactory = $resultFactory;
}
/**
* @param PaymentInformationManagementInterface $subject
* @param int $orderId
* @return ResultInterface
*/
public function afterSavePaymentInformationAndPlaceOrder(
PaymentInformationManagementInterface $subject,
$orderId
) {
/** @var ResultInterface $obj */
$obj = $this->resultFactory->create();
$obj->setOrderId($orderId);
return $obj;
}
}
So now the result is the object that implements extension_attributes. In your custom payment api module you can define similar plugin, just make sure sortOrder there is higher than the above one so you already get Vendor\CheckoutExt\Api\Data\ResultInterface
object as an argument.
In payment module create file etc/extension_attributes.xml
with
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
<extension_attributes for="Vendor\ChecoutExt\Api\Data\ResultInterface">
<attribute code="my_payment_gateway_data" type="Vendor\CustomPayment\Api\Data\RedirectInterface" />
</extension_attributes>
</config>
Create interface and its implementation according to your needs and in plugin
namespace Vendor\CustomPayment\Plugin\Checkout;
use Vendor\CustomPayment\Api\Data\RedirectInterface;
use Vendor\CustomPayment\Api\Data\RedirectInterfaceFactory;
use Vendor\CheckoutExt\Api\Data\ResultInterface;
use Magento\Checkout\Api\PaymentInformationManagementInterface;
class PaymentInformationManagement
{
/** @var RedirectInterfaceFactory */
private $redirectFactory;
/**
* @param RedirectInterfaceFactory $redirectFactory
*/
public function __construct(RedirectInterfaceFactory $redirectFactory)
{
$this->redirectFactory = $redirectFactory;
}
/**
* @param PaymentInformationManagementInterface $subject
* @param ResultInterface $orderId
* @return ResultInterface
*/
public function afterSavePaymentInformationAndPlaceOrder(
PaymentInformationManagementInterface $subject,
$orderId
) {
/** @var ResultInterface $obj */
$redirect = $this->redirectFactory->create();
$extensionAttributes = $orderId->getExtensionAttributes();
$extensionAttributes->setMyPaymentGatewayData($redirect);
$orderId->setExtensionAttributes($extensionAttributes);
return $orderId;
}
}
Now the response is proper json with extension_attributes in it which you can check and hook into. There might be some other elements to cover as magento by default redirect to success page, so you need to make sure you disable it when preparing your redirect. Also similar overwrites need to be done for classes handling guest checkout.
Best Answer
I've hit a similar brick wall once.
After digging in the code I found that you cannot use payment methods that required a redirect to the payment gateway in multishipping checkout. I may be wrong here, but here is the reasons of my conclusion.
In the onepage checkout, when saving the order
Mage_Checkout_OnepageController::saveOrderAction
there is this code:and in
opcheckout.js
in thereview
class there is this:So if there is a redirect url specified you will get redirected to that url.
There is something similar in the
savePaymentAction
.I found no trace of something like that in the
MultishippingController
. take a look at whatoverviewPostAction
does. (that's where the payment is processed).After saving a few details about the credit card type (if applicable) this is called.
The main attraction in the
createOrders
method isSo nothing about any redirect.
An other thing that made me reach this conclusion is that most of the online payment methods that Magento has by default are not allowed in the multishipping checkout.
Look in the code for the variable
$_canUseForMultishipping
. It is set tofalse
for most of the online payment methods.I guess the safest solution would be to disable your method for multishipping.
You can do that by adding
to the model that handles the payment method.
I ended up doing the same in my case.