Campo customizado no checkout do Magento 2 e salvar no pedido
"Dá para adicionar um campo no checkout?" parece uma tarefa de dez minutos. No checkout Luma ela mexe no layout dos UI components, em um mixin JavaScript, em um payload da web API, em extension attributes, em duas tabelas do banco e em um observer. Pule um deles e o valor se perde em algum ponto entre o navegador e o pedido.
Este artigo acompanha um único campo, uma observação de entrega, da etapa de shipping até o pedido no Magento Open Source e no Adobe Commerce 2.4.8. O mesmo caminho serve para um número de PO, uma flag de mensagem de presente ou uma data de entrega preferida.
Decida primeiro a quem o dado pertence
Antes de escrever código, decida o que o campo descreve:
- O endereço do cliente, como um código de acesso ao prédio que deve ser reaproveitado na próxima vez. Isso é um customer address attribute e pertence aos formulários de endereço e ao address book.
- Este pedido, como uma observação de entrega ou uma referência de pedido de compra. Isso é dado do pedido e deve ser guardado na quote e no pedido.
Misturar os dois causa a maioria dos bugs estranhos que vejo nessa área, como valores que aparecem no pedido seguinte ou somem quando o cliente escolhe um endereço salvo. O resto deste artigo trata do segundo caso.
O caminho do dado
- Um campo UI component é renderizado na etapa de shipping e grava o valor no data provider do checkout.
- Quando o cliente clica em Next, o Magento faz um post de shipping information para
/V1/carts/mine/shipping-informationou o equivalente de guest. Um mixin adiciona o nosso valor a esse payload como extension attribute. - Um plugin em
Magento\Checkout\Model\ShippingInformationManagementlê o extension attribute e guarda na quote. - Um observer copia o valor da quote para o pedido quando o pedido é feito.
Passo 1: colunas na quote e na sales_order
Em etc/db_schema.xml, adicione a coluna nas duas tabelas. Lembre de gerar a whitelist com bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_DeliveryNote. Mais sobre isso em declarative schema e data patches.
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="quote">
<column xsi:type="varchar" name="delivery_note" nullable="true" length="255" comment="Delivery Note"/>
</table>
<table name="sales_order">
<column xsi:type="varchar" name="delivery_note" nullable="true" length="255" comment="Delivery Note"/>
</table>
</schema>
Passo 2: o campo na etapa de shipping
Dá para adicionar o campo em checkout_index_index.xml, mas o aninhamento é profundo e fácil de quebrar. Um plugin em Magento\Checkout\Block\Checkout\LayoutProcessor é mais curto e permite usar valores de config, por exemplo para mostrar o campo só em algumas lojas.
<?php
declare(strict_types=1);
namespace Vendor\DeliveryNote\Plugin\Checkout;
use Magento\Checkout\Block\Checkout\LayoutProcessor;
class AddDeliveryNoteField
{
public function afterProcess(LayoutProcessor $subject, array $jsLayout): array
{
$jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']
['children']['shippingAddress']['children']['shippingAdditional']['children']['delivery_note'] = [
'component' => 'Magento_Ui/js/form/element/textarea',
'config' => [
'template' => 'ui/form/field',
'elementTmpl' => 'ui/form/element/textarea',
'rows' => 3,
],
'provider' => 'checkoutProvider',
'dataScope' => 'deliveryNote.delivery_note',
'label' => __('Delivery note'),
'validation' => ['max_text_length' => 255],
'sortOrder' => 10,
];
return $jsLayout;
}
}
Registre em etc/frontend/di.xml com uma declaração de plugin <type name="Magento\Checkout\Block\Checkout\LayoutProcessor">. A região shippingAdditional é renderizada abaixo dos métodos de envio. Colocar o campo ali o mantém fora do fieldset de endereço, então o Magento nunca tenta salvá-lo como atributo de endereço.
O dataScope coloca o valor no provider do checkout em deliveryNote.delivery_note, de onde o nosso mixin consegue lê-lo.
Passo 3: declare o extension attribute
O payload de shipping information é tipado como Magento\Checkout\Api\Data\ShippingInformationInterface. Chaves desconhecidas são rejeitadas pela web API, então declare a nova em etc/extension_attributes.xml:
<?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="Magento\Checkout\Api\Data\ShippingInformationInterface">
<attribute code="delivery_note" type="string"/>
</extension_attributes>
</config>
Depois do setup:di:compile, a extension interface gerada tem getDeliveryNote() e setDeliveryNote(). Se extension attributes são novidade para você, leia extension attributes do Magento 2.
Passo 4: adicione o valor ao payload
O Magento monta o payload de shipping information em Magento_Checkout/js/model/shipping-save-processor/default e o passa por Magento_Checkout/js/model/shipping-save-processor/payload-extender. Esse extender existe justamente para ser envolvido por módulos como o nosso, o que é mais limpo do que sobrescrever o save processor inteiro. Declare o mixin em view/frontend/requirejs-config.js:
var config = {
config: {
mixins: {
'Magento_Checkout/js/model/shipping-save-processor/payload-extender': {
'Vendor_DeliveryNote/js/payload-extender-mixin': true
}
}
}
};
E o mixin em si:
define([
'mage/utils/wrapper',
'uiRegistry'
], function (wrapper, registry) {
'use strict';
return function (payloadExtender) {
return wrapper.wrap(payloadExtender, function (originalFn, payload) {
var provider = registry.get('checkoutProvider'),
note = provider ? provider.get('deliveryNote.delivery_note') : '';
payload = originalFn(payload);
payload.addressInformation.extension_attributes =
payload.addressInformation.extension_attributes || {};
payload.addressInformation.extension_attributes.delivery_note = note || '';
return payload;
});
};
});
Abra a aba de rede do navegador, clique em Next na etapa de shipping e confira o corpo da request. Se delivery_note não estiver em addressInformation.extension_attributes, nada no servidor vai enxergá-lo. O artigo sobre mixins do RequireJS explica como confirmar que um mixin foi carregado.
Passo 5: guarde na quote
Um plugin before em saveAddressInformation() cobre clientes logados e guests, porque a classe de management de guest resolve o masked cart id e depois chama o mesmo método.
<?php
declare(strict_types=1);
namespace Vendor\DeliveryNote\Plugin\Checkout;
use Magento\Checkout\Api\Data\ShippingInformationInterface;
use Magento\Checkout\Model\ShippingInformationManagement;
use Magento\Quote\Api\CartRepositoryInterface;
class SaveDeliveryNoteToQuote
{
public function __construct(
private readonly CartRepositoryInterface $cartRepository
) {
}
public function beforeSaveAddressInformation(
ShippingInformationManagement $subject,
$cartId,
ShippingInformationInterface $addressInformation
): void {
$note = $addressInformation->getExtensionAttributes()?->getDeliveryNote();
$note = trim(strip_tags((string) $note));
$quote = $this->cartRepository->getActive((int) $cartId);
$quote->setData('delivery_note', mb_substr($note, 0, 255) ?: null);
}
}
Declare esse plugin no etc/di.xml global, não no de frontend, porque a chamada chega pela área webapi_rest. O quote repository mantém as quotes carregadas em memória, então o método original salva a mesma instância logo depois e o valor é gravado na tabela quote. Limpe o input aqui. Tudo que for digitado em um campo do checkout acaba em telas do admin e em e-mails.
Passo 6: copie para o pedido
Um observer em sales_model_service_quote_submit_before recebe a quote e o pedido antes de o pedido ser salvo.
<?php
declare(strict_types=1);
namespace Vendor\DeliveryNote\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
class CopyDeliveryNoteToOrder implements ObserverInterface
{
public function execute(Observer $observer): void
{
$quote = $observer->getEvent()->getQuote();
$order = $observer->getEvent()->getOrder();
if ($quote->getData('delivery_note')) {
$order->setData('delivery_note', $quote->getData('delivery_note'));
}
}
}
Registre no etc/events.xml global para que ele rode também em pedidos feitos pela API.
Mostrando no admin e nos e-mails
Quando o valor está em sales_order, exiba-o na visualização do pedido com um pequeno block adicionado por sales_order_view.xml e use-o nos templates de e-mail pelos dados do pedido que você passa. Para sistemas externos, exponha como extension attribute em Magento\Sales\Api\Data\OrderInterface com um plugin no order repository, para que ele apareça nas respostas de /V1/orders.
Tornando o campo obrigatório
Adicionar 'required-entry' => true ao array validation mostra o erro de costume embaixo do campo, mas não impede a etapa de shipping sozinho, porque o componente de shipping só valida o formulário de endereço e a seleção do método. Para bloquear o botão Next, adicione um mixin em Magento_Checkout/js/view/shipping que envolva validateShippingInformation, dispare a validação do seu campo pelo provider e retorne false quando falhar.
Depois imponha a regra também no servidor. No plugin da quote, lance uma Magento\Framework\Exception\InputException quando o valor estiver vazio. O checkout mostra a mensagem, e pedidos feitos pela API seguem a mesma regra.
Testando sem o navegador
Você pode testar a metade de backend antes de o JavaScript existir. Crie um guest cart pelo REST, adicione um item e faça um post em /V1/guest-carts/{cartId}/shipping-information com delivery_note em addressInformation.extension_attributes. Consulte a tabela quote e você deve ver o valor. Isso divide o problema em dois e poupa tempo quando algo não funciona.
Erros comuns
- Colocar o campo dentro do fieldset de endereço com data scope
shippingAddress.custom_attributesquando ele não é um atributo de endereço. - Registrar o plugin da quote só em
etc/frontend/di.xml. A chamada REST nunca o vê. - Esquecer o fluxo de guest. Teste um guest e um cliente logado.
- Confiar no valor. Valide o tamanho e remova tags no servidor, não só no UI component.
- Supor que o Hyvä Checkout funciona do mesmo jeito. Ele não usa esses componentes, então veja a visão geral do Hyvä Checkout se for a sua stack.
FAQ
Por que meu campo customizado do checkout está vazio no pedido?
Siga o valor passo a passo. Confira a request de rede para o extension attribute, depois a tabela quote, depois o observer. Na maioria das vezes o plugin está registrado na área frontend em vez de global, ou o valor nunca chegou ao payload.
Posso adicionar o campo na etapa de pagamento?
Pode. Nesse caso envie o valor na request de payment information e declare o extension attribute em Magento\Quote\Api\Data\PaymentInterface, que é o tipo do objeto paymentMethod nesse payload.
Preciso de uma coluna na quote se só quero o valor no pedido?
Na prática sim. O pedido é criado depois, em outra request, então a quote é o lugar onde guardar o valor até lá.
Um customer address attribute é uma opção melhor?
Só quando o valor pertence ao endereço e deve ser salvo no address book. Para dados específicos do pedido, guarde na quote e no pedido como mostrado aqui.
Atendo como freelancer em Magento e Adobe Commerce, incluindo mudanças de checkout como esta. Se precisar de ajuda com isso, fale comigo.
Artigos relacionados
UI components e Knockout.js no Magento 2: o básico
Introdução prática a UI components e Knockout.js no Magento 2: uiComponent, templates, observables, imports e exports, jsLayout e o uiRegistry.
Como adicionar uma etapa customizada no checkout do Magento 2
Adicione uma nova etapa ao checkout Luma do Magento 2 com step-navigator, layout XML e template Knockout, e veja onde validar e guardar os dados que ela coleta.
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.
Método de pagamento customizado no Magento 2 com a Payment Gateway API
Crie um método de pagamento customizado no Magento 2.4 com a Payment Gateway API: virtual type de facade, config, commands e o renderer JS do checkout, passo a passo.