6

I am creating a Magento 2 store that sells virtual products, this site have its checkout directly at the product page. The customers will never buy more than 1 product at a time.

This checkout uses ajax calls aimed at the REST API's for the steps in the purchase.

So far I can create new cart, add product, set billing address, set payment method, collect totals and place order. BUT! One of the payment methods we use is an external credit card gateway. I have created a custom payment method module for this and it is settable through the APIs. However, the response that i get from "placeOrder()" API call contains only the order entity_id.

My intuition tells me that depending on payment method such an API call would add something to the response to tell the frontend that a redirect should take place.

I have probably missed something, maybe an API call that should be made before placeorder to process payment and then in case of redirect do placeorder at the callback url?

Does anyone have an idea of how this is supposed to be done?

  • The response of placeOrder will only contain order ID, nothing dependent on payment method. – Alex Paliarush Jan 28 '16 at 15:39
  • 1
    I know, but where in the flow would be the logical place to load payment method credentials and send them to the javascript client? So that the proper post request to the payment method can be done. After placeorder or before? Do magento have an api call specifically for a thin like this or should i write my own? – nenne Jan 28 '16 at 17:10
2

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:

  1. 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.

  2. 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.

0

I have answered a similar question here: https://magento.stackexchange.com/a/136782/29688

In summary, you have to manually call the external service.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.