Overview
If you have tried to implement a payment integration in Magento, you might have had a trouble with the sale (authorize & capture) payment operation. This post describes how the Place an Order flow is implemented in Magento, the “bridge” between Magento and payment service providers, and how to implement the sale operation when using custom payment methods without breaking the Place an Order flow.
At first, let’s define a small glossary:
Let’s start with the Place an Order flow in Magento, how Magento interacts with PSPs such as PayPal, Braintree, and Authorize.net, etc., and why we cannot use a standard flow.
Magento payment integrations support payment operations to be performed in one of two ways:
The following diagram describes the Magento Place an Order flow based on an Authorize payment operation:
The steps in the flow are as follows.
Also, the capture transaction can be performed on the PSP's side (the merchant can manually create a capture transaction via the PSP interface).
The following diagram describes the “place order” flow based on the Authorize & Capture (Sale) payment operation:
An issue
The sale payment operation flow is similar to an authorize payment operation, but in this case, the authorization and capture transactions are combined into one transaction. It will be submitted for settlement automatically by the PSP, so the store's administrator does not need to create an invoice manually.
According to sale restrictions in the Sales Management module:
<?php
protected function processAction($action, Order $order) { $totalDue = $order->getTotalDue(); $baseTotalDue = $order->getBaseTotalDue();
switch ($action) { case \Magento\Payment\Model\Method\AbstractMethod::ACTION_ORDER: $this->_order($baseTotalDue);
break; case \Magento\Payment\Model\Method\AbstractMethod::ACTION_AUTHORIZE: $this->authorize(true, $baseTotalDue); // base amount will be set inside $this->setAmountAuthorized($totalDue); break; case \Magento\Payment\Model\Method\AbstractMethod::ACTION_AUTHORIZE_CAPTURE: $this->setAmountAuthorized($totalDue); $this->setBaseAmountAuthorized($baseTotalDue); $this->capture(null); break; default: break; } }
The capture operation will be called for both the sale and capture payment operations. In that case, payment integrations do not have an ability to process both the authorize & capture operations.
For example, if a customer wants to buy some items, and the merchant wants to charge the customer balance immediately, the payment integration can’t send the correct type of transaction to the Payment Service Provider. Each payment integration would have to solve this issue again and again.
The flow described in this post assumes that the payment integration is based on the Magento Payment Gateway.
As previously mentioned, we only have the capture action for the sale payment operation. To avoid breaking the Place an Order flow, we will use it as it is. To solve this issue, we need to answer three questions:
In most cases, the sale operation is similar to authorization, and it also processes the capture operation, answering the first question.
If our payment provides transaction capturing, we have part of the capture operation (which includes customer details, billing information, order amount, etc.), the answer to the second question.
The third answer is based on two previous points. Our payment method should perform payment operations similar to capture. We do not have an authorization transaction, so we can use some strategy to decide what kind of payment operation (sale or capture) our payment integration should perform when Magento Sales Management calls the capture action.
Luckily, OOP has a suitable pattern, the Strategy pattern. In our case, this pattern allows us to switch between authorization and sale payment operations, and vice versa.
Let's assume we have a payment integration based on a Magento Payment Provider Gateway such as Braintree.
If we open DI configuration, we will see the command pool with a list of available commands:
<virtualType name="BraintreeCommandPool" type="Magento\Payment\Gateway\Command\CommandPool"> <arguments> <argument name="commands" xsi:type="array"> <item name="authorize" xsi:type="string">BraintreeAuthorizeCommand</item> <item name="sale" xsi:type="string">BraintreeSaleCommand</item> <item name="capture" xsi:type="string">BraintreeCaptureStrategyCommand</item> <item name="settlement" xsi:type="string">BraintreeCaptureCommand</item> </argument> </arguments> </virtualType>
The three interesting operations for us are:
Solution
To solve our issue with the sale operation and make payment integration clearer (as previously mentioned, the sales order payment calls the capture operation for both sale and capture actions), the capture command is specified as the strategy. This approach allows us to recognize what operation to process.
One possible way to implement our strategy is to check transactions. If an authorization transaction already exists, then we have a capture payment operation. Otherwise, it is a sale operation. Our capture algorithm is as follows:
So, the capture is our strategy and that is how it can be implemented (based on the Braintree payment method):
<?php class CaptureStrategyCommand implements CommandInterface { const SALE = 'sale'; const CAPTURE = 'settlement';
public function execute(array $commandSubject) { /** @var \Magento\Payment\Gateway\Data\PaymentDataObjectInterface $paymentDO */ $paymentDO = $this->subjectReader->readPayment($commandSubject);
/** @var \Magento\Sales\Api\Data\OrderPaymentInterface $paymentInfo */ $paymentInfo = $paymentDO->getPayment(); ContextHelper::assertOrderPayment($paymentInfo); $command = $this->getCommand($paymentInfo); $this->commandPool->get($command)->execute($commandSubject); } private function getCommand(OrderPaymentInterface $payment) { // if auth transaction does not exist execute authorize&capture command $existsCapture = $this->isExistsCaptureTransaction($payment);
if (!$payment->getAuthorizationTransaction() && !$existsCapture) { return self::SALE; } // do capture for authorization transaction if (!$existsCapture && !$this->isExpiredAuthorization($payment)) { return self::CAPTURE; } ... } }
I omitted code not important to our example. The full class listing can be found in the Magento Github repository.
This logic is pretty simple. We just check, and if there are no authorization or capture transactions, then it's a sale operation. On the other hand, if the authorization has not expired and a capturing transaction does not exist then it’s a capture operation to charge customer's funds.
We considered Braintree capture strategy checks capturing transaction, but we are not considering it in this topic because it is related to Braintree partial invoicing and is not interesting for us. Then we process the capture operation.
Depending on the payment integration, the strategy to decide which payment action to execute can be more complicated than described above. For example, a Payment Gateway can provide API entry points to retrieve additional transaction details, and your code might choose a needed payment operation according to the Payment Gateway API response.
Conclusion
In this post, we’ve described how to implement the sale payment operation for a custom payment method based on the Magento Payment Provider Gateway. Using this knowledge, you can more easily extend your payment integration and add the sale payment operation, if the Payment Gateway supports authorize & capture transactions.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.