Método de pagamento customizado no Magento 2 com a Payment Gateway API
Há duas maneiras de escrever um método de pagamento no Magento 2. A antiga estende Magento\Payment\Model\Method\AbstractMethod, que está deprecated há anos mas ainda aparece em muitos tutoriais e extensões. A atual é a Payment Gateway API, em que o método é um adapter configurado e o trabalho de verdade acontece em pequenas classes ligadas no di.xml.
Este artigo usa a Payment Gateway API no Magento Open Source e no Adobe Commerce 2.4.8. Começa com um método offline, parecido com o Check / Money Order, e depois adiciona um command authorize que chama um gateway externo. A mesma estrutura escala de um simples método "pagar na fatura" até uma integração completa de cartão.
As peças de um método de pagamento com gateway
Antes do código, um mapa do que cada parte faz:
- Facade. Um virtual type de
Magento\Payment\Model\Method\Adapter. É o objeto que o Magento enxerga como o método de pagamento. - Value handler pool. Responde perguntas de config como "este método está ativo" ou "ele pode fazer capture", normalmente lendo a config
payment/<code>/*. - Validator pool. Checagens opcionais, por exemplo países ou moedas permitidos.
- Command pool. Mapeia operações como
authorize,capture,voiderefundpara objetos de command. - Gateway command. Para cada operação: um request builder, uma transfer factory, um HTTP client, um response validator e um response handler.
- Renderer de frontend. Um componente JavaScript e um template Knockout que mostram o método no checkout do Luma.
Parecem muitas classes para um método de pagamento. A vantagem é que cada peça é pequena, testável e substituível por DI, e é assim que as integrações do Braintree e do PayPal no core são escritas.
Configuração: config.xml
Métodos de pagamento ficam em payment/<code>. O nó model aponta para o virtual type da facade, não para uma classe PHP.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<payment>
<vendorpay>
<active>0</active>
<model>VendorPayFacade</model>
<title>Pay by Bank Transfer</title>
<order_status>pending</order_status>
<payment_action>authorize</payment_action>
<can_authorize>1</can_authorize>
<can_use_checkout>1</can_use_checkout>
<can_use_internal>1</can_use_internal>
<is_gateway>1</is_gateway>
<allowspecific>0</allowspecific>
<sort_order>50</sort_order>
</vendorpay>
</payment>
</default>
</config>
As flags can_* são lidas pelo value handler pool. Se o can_capture estiver ausente, o admin não oferece capture online nas invoices. Se o can_use_internal estiver desligado, o método não aparece ao criar pedidos no admin. Adicione um grupo correspondente em etc/adminhtml/system.xml na seção payment para os lojistas poderem mudar título, status e países.
Ligando tudo no di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<virtualType name="VendorPayFacade" type="Magento\Payment\Model\Method\Adapter">
<arguments>
<argument name="code" xsi:type="const">Vendor\Payment\Model\Ui\ConfigProvider::CODE</argument>
<argument name="formBlockType" xsi:type="string">Magento\Payment\Block\Form</argument>
<argument name="infoBlockType" xsi:type="string">Magento\Payment\Block\Info</argument>
<argument name="valueHandlerPool" xsi:type="object">VendorPayValueHandlerPool</argument>
<argument name="commandPool" xsi:type="object">VendorPayCommandPool</argument>
</arguments>
</virtualType>
<virtualType name="VendorPayConfig" type="Magento\Payment\Gateway\Config\Config">
<arguments>
<argument name="methodCode" xsi:type="const">Vendor\Payment\Model\Ui\ConfigProvider::CODE</argument>
</arguments>
</virtualType>
<virtualType name="VendorPayConfigValueHandler" type="Magento\Payment\Gateway\Config\ConfigValueHandler">
<arguments>
<argument name="configInterface" xsi:type="object">VendorPayConfig</argument>
</arguments>
</virtualType>
<virtualType name="VendorPayValueHandlerPool" type="Magento\Payment\Gateway\Config\ValueHandlerPool">
<arguments>
<argument name="handlers" xsi:type="array">
<item name="default" xsi:type="string">VendorPayConfigValueHandler</item>
</argument>
</arguments>
</virtualType>
<virtualType name="VendorPayCommandPool" type="Magento\Payment\Gateway\Command\CommandPool">
<arguments>
<argument name="commands" xsi:type="array">
<item name="authorize" xsi:type="string">VendorPayAuthorizeCommand</item>
</argument>
</arguments>
</virtualType>
<virtualType name="VendorPayAuthorizeCommand" type="Magento\Payment\Gateway\Command\GatewayCommand">
<arguments>
<argument name="requestBuilder" xsi:type="object">Vendor\Payment\Gateway\Request\AuthorizeRequest</argument>
<argument name="transferFactory" xsi:type="object">Vendor\Payment\Gateway\Http\TransferFactory</argument>
<argument name="client" xsi:type="object">Vendor\Payment\Gateway\Http\Client</argument>
<argument name="handler" xsi:type="object">Vendor\Payment\Gateway\Response\TransactionHandler</argument>
<argument name="validator" xsi:type="object">Vendor\Payment\Gateway\Validator\ResponseValidator</argument>
</arguments>
</virtualType>
</config>
Para um método puramente offline você pode eliminar o command pool por completo e deixar payment_action vazio. O pedido é feito e fica no status configurado até alguém faturá-lo à mão. O resto deste artigo assume que você quer a chamada authorize.
Se virtual types são novidade para você, dependency injection e virtual types explica como esse arquivo é lido.
O request builder
Um builder recebe um array "subject" e devolve um array que vira o corpo da request. Use o Magento\Payment\Gateway\Helper\SubjectReader para lê-lo em vez de indexar o array você mesmo.
<?php
declare(strict_types=1);
namespace Vendor\Payment\Gateway\Request;
use Magento\Payment\Gateway\Helper\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
class AuthorizeRequest implements BuilderInterface
{
public function build(array $buildSubject): array
{
$paymentDO = SubjectReader::readPayment($buildSubject);
$amount = SubjectReader::readAmount($buildSubject);
$order = $paymentDO->getOrder();
return [
'reference' => $order->getOrderIncrementId(),
'amount' => number_format((float) $amount, 2, '.', ''),
'currency' => $order->getCurrencyCode(),
'email' => $order->getBillingAddress()?->getEmail(),
];
}
}
O $paymentDO->getOrder() retorna um OrderAdapterInterface, um wrapper fino somente leitura. Você também pode chamar $paymentDO->getPayment() para chegar à instância de payment info e às suas additional information. Quando a request tem muitas partes, divida em vários builders e combine com o Magento\Payment\Gateway\Request\BuilderComposite.
Transfer, client, validator, handler
A transfer factory transforma o array em um objeto TransferInterface usando o Magento\Payment\Gateway\Http\TransferBuilder, onde você define corpo, headers, URI e método. O client implementa Magento\Payment\Gateway\Http\ClientInterface com um único método placeRequest(TransferInterface $transferObject) que envia a request e devolve a resposta decodificada como array. Mantenha o logging no client, usando o Magento\Payment\Model\Method\Logger, que mascara os valores que você marcar como sensíveis.
O validator verifica a resposta e devolve um resultado criado pelo Magento\Payment\Gateway\Validator\ResultInterfaceFactory. Se falhar, o command lança uma exceção e o pedido não é feito. O cliente vê um erro genérico, então registre o motivo real no log.
O handler guarda no payment o que voltou:
<?php
declare(strict_types=1);
namespace Vendor\Payment\Gateway\Response;
use Magento\Payment\Gateway\Helper\SubjectReader;
use Magento\Payment\Gateway\Response\HandlerInterface;
use Magento\Sales\Model\Order\Payment;
class TransactionHandler implements HandlerInterface
{
public function handle(array $handlingSubject, array $response): void
{
$paymentDO = SubjectReader::readPayment($handlingSubject);
$payment = $paymentDO->getPayment();
if ($payment instanceof Payment) {
$payment->setTransactionId((string) $response['transaction_id']);
$payment->setIsTransactionClosed(false);
$payment->setAdditionalInformation('gateway_status', (string) $response['status']);
}
}
}
Mostrando o método no checkout
A parte de backend deixa o método disponível para a API. O checkout do Luma ainda precisa de um renderer. Registre-o em view/frontend/layout/checkout_index_index.xml:
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="checkout.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="steps" xsi:type="array">
<item name="children" xsi:type="array">
<item name="billing-step" xsi:type="array">
<item name="children" xsi:type="array">
<item name="payment" xsi:type="array">
<item name="children" xsi:type="array">
<item name="renders" xsi:type="array">
<item name="children" xsi:type="array">
<item name="vendorpay" xsi:type="array">
<item name="component" xsi:type="string">Vendor_Payment/js/view/payment/vendorpay</item>
<item name="methods" xsi:type="array">
<item name="vendorpay" xsi:type="array">
<item name="isBillingAddressRequired" xsi:type="boolean">true</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
O componente registrado adiciona o renderer à lista:
define([
'uiComponent',
'Magento_Checkout/js/model/payment/renderer-list'
], function (Component, rendererList) {
'use strict';
rendererList.push({
type: 'vendorpay',
component: 'Vendor_Payment/js/view/payment/method-renderer/vendorpay'
});
return Component.extend({});
});
E o renderer estende o padrão, que já sabe selecionar o método, mostrar o endereço de cobrança e fazer o pedido:
define([
'Magento_Checkout/js/view/payment/default'
], function (Component) {
'use strict';
return Component.extend({
defaults: {
template: 'Vendor_Payment/payment/vendorpay'
}
});
});
Para o template em view/frontend/web/template/payment/vendorpay.html, copie o do Magento_OfflinePayments para o Check / Money Order e adapte. Ele contém o radio input, a região do endereço de cobrança, os agreements e o botão de fazer pedido com os bindings certos. Escrever esse template de memória é um bom jeito de perder o bloco de termos e condições.
Se o renderer precisa de dados do servidor, como uma chave pública ou um texto de instruções, implemente Magento\Checkout\Model\ConfigProviderInterface e adicione sua classe ao argumento configProviders do Magento\Checkout\Model\CompositeConfigProvider em etc/frontend/di.xml. Os dados aparecem em window.checkoutConfig.payment.
O Hyvä Checkout e frontends headless não usam esse JavaScript. Com GraphQL, o método aparece em available_payment_methods assim que o backend diz que está disponível, e o frontend envia o code dele em setPaymentMethodOnCart. Veja mutations de GraphQL para carrinho e checkout.
Capture, void e refund
Cada operação extra é mais uma entrada no command pool e mais um virtual type de GatewayCommand. Os builders mudam, mas a transfer factory e o client normalmente são compartilhados. Depois ligue as flags correspondentes no config.xml: can_capture, can_void, can_refund e, se o gateway suportar, can_capture_partial e can_refund_partial_per_invoice. Sem as flags os botões do admin não aparecem, mesmo que os commands existam.
O builder de capture precisa do transaction id da autorização. Leia-o do payment com getParentTransactionId() ou das additional information que você salvou no handler. Mantenha os valores como o gateway espera, normalmente na moeda do pedido, e tome cuidado com lojas multimoeda em que a moeda base e a do pedido são diferentes.
Teste cada operação no sandbox do gateway e escreva testes unitários para builders e handlers. São classes simples com entrada e saída em array, o que as torna parte do código mais fácil de testar no Magento. Testes unitários com PHPUnit cobre a configuração.
Quando o método não aparece
O Magento executa uma lista de checagens de Magento\Payment\Model\Checks antes de mostrar um método. Na prática, veja estas em ordem:
- O método está ativo no scope de website da store que você está testando?
- O
can_use_checkoutestá em 1? - Os países permitidos incluem o país de cobrança?
- As configurações de total mínimo e máximo do pedido, se você as adicionou, excluem o carrinho?
- O total do pedido é zero? Nesse caso só o método Zero Subtotal Checkout é oferecido.
- O renderer JS está carregado? Veja no console do navegador se há um erro do RequireJS no path do seu componente.
FAQ
Ainda devo estender AbstractMethod em um método de pagamento novo?
Não. O AbstractMethod está deprecated. Use a Payment Gateway API com Magento\Payment\Model\Method\Adapter como facade. Extensões antigas ainda funcionam, mas código novo deve seguir a estrutura de gateway.
Preciso de um command pool para um método de pagamento offline?
Não. Um método que só registra o pedido, como transferência bancária, funciona só com a facade e o value handler pool. Commands são necessários quando você chama um gateway externo.
Como passo dados extras do checkout para o pagamento?
Sobrescreva o getData no renderer JS para adicionar campos em additional_data. Depois copie-os para as additional information do payment em um observer que estenda Magento\Payment\Observer\AbstractDataAssignObserver, escutando o evento payment_method_assign_data_vendorpay.
Por que meu método de pagamento não aparece no formulário de pedido do admin?
Confira se can_use_internal está em 1 para o método. O formulário de pedido do admin usa essa flag no lugar do can_use_checkout.
Atendo como freelancer em Magento e Adobe Commerce, incluindo integrações com gateways de pagamento. Se você precisa de ajuda com isso, fale comigo.
Artigos relacionados
Shipping method customizado no Magento 2: crie um módulo de carrier
Como criar um shipping method customizado no Magento 2.4: config.xml, system.xml, um carrier model com collectRates, regras de frete grátis e APIs externas de tarifa.
Taxa customizada no Magento 2: um totals collector que funciona
Adicione uma taxa customizada no Magento 2 com um quote totals collector: sales.xml, collect e fetch, exibição no carrinho e checkout, cópia para pedidos, invoices e PDFs.
Guest checkout e contas de cliente no Magento 2 explicados
Como o Magento 2 trata guest checkout e contas de cliente: configuração, criação de conta depois do pedido, vínculo seguro de pedidos de visitante e checagem de e-mail.
Magento 2 Cart Price Rules: como criar uma condição customizada
Crie uma condição customizada nas cart price rules do Magento 2: o evento combine, o model da condition com validate, atributos de promo rule e armadilhas de performance.