Pedido duplicado quase nunca é bug do Magento. É timeout. O middleware manda o pedido, o Magento leva 40 segundos gravando, a conexão estoura antes da resposta, e a fila do middleware faz o que foi programada para fazer: tenta de novo. O primeiro pedido entrou. O segundo também.
A correção não é aumentar o timeout. É dar ao Magento uma forma de reconhecer que aquele pedido já chegou. E existe campo nativo para isso desde sempre.
O campo que já está lá
Abra o db_schema.xml do Magento_Sales e você acha isto na tabela sales_order:
<column xsi:type="varchar" name="ext_order_id" nullable="true" length="32"/>E, algumas linhas abaixo, um índice dedicado, o SALES_ORDER_EXT_ORDER_ID, do tipo btree. Ou seja: consultar por esse campo em tabela com meio milhão de pedidos é rápido, não é varredura.
Na API ele aparece como ext_order_id, com getExtOrderId() e setExtOrderId() declarados na OrderInterface. Não é campo de extensão, não é atributo customizado: é parte do contrato oficial do pedido.
Uma limitação para já anotar: são 32 caracteres. UUID com hífen tem 36 e não cabe. Use o número do pedido do ERP, ou o UUID sem hífen, ou um hash MD5 da chave que o seu sistema considera única.
Consulte antes de criar
Essa é a parte que faz o trabalho. Antes de montar qualquer payload, o middleware pergunta se aquele pedido já existe:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://loja.com.br/rest/V1/orders?searchCriteria[filter_groups][0][filters][0][field]=ext_order_id&searchCriteria[filter_groups][0][filters][0][value]=ERP-000123&searchCriteria[filter_groups][0][filters][0][condition_type]=eq"A resposta traz total_count. Se vier 0, o pedido nunca chegou e pode criar. Se vier 1, o retry acabou de virar uma consulta barata, e o items[0].increment_id é o número que o ERP precisava receber na primeira tentativa.
Dá para enxugar a resposta com &fields=total_count,items[entity_id,increment_id], o que ajuda bastante quando o pedido tem cinquenta itens e você só quer saber se existe.
Grave o ID na hora de criar
Se a sua integração cria o pedido pelo POST /V1/orders, que é o caminho de quem recebe pedido fechado de fora (hub de marketplace, televendas, PDV), o campo entra junto com o resto da entidade:
{
"entity": {
"ext_order_id": "ERP-000123",
"customer_email": "[email protected]",
"items": []
}
}Se o pedido nasce pelo fluxo de carrinho — POST /V1/carts e depois o PUT que fecha o pedido —, o ext_order_id não faz parte desse payload. Aí a gravação precisa de um observer curto no evento sales_order_place_after, copiando o identificador que você carregou no carrinho. É a única parte da história que pede código.
Em qualquer um dos dois casos, grave o ID na mesma transação em que o pedido é criado. Gravar depois, num segundo request, reabre exatamente a janela que você está tentando fechar.
Onde isso falha
Nenhuma solução de idempotência é de graça, e essa tem três limites que vale conhecer antes de confiar cegamente.
O índice não é único. É um btree comum, criado para busca, não uma constraint. Ele deixa gravar dois pedidos com o mesmo ext_order_id sem reclamar. Quem impede a duplicata é a sua consulta, não o banco.
Consulta e criação não são atômicas. Se dois retries saem ao mesmo tempo, os dois consultam, os dois recebem zero e os dois criam. Se o seu middleware tem paralelismo, resolva com uma trava do lado dele — chave no Redis, lock na tabela de fila, o que já existir aí.
O admin não mostra esse campo. A tabela sales_order_grid não tem a coluna ext_order_id, então não adianta procurar no filtro do grid de pedidos. Ou você consulta pela API, ou por SQL, ou adiciona a coluna ao grid num módulo. Como montar a integração inteira sem se afogar em retry está no guia de integração com ERP e NF-e.
Perguntas rápidas
Posso usar o increment_id do Magento como chave de idempotência?
Não serve para esse caso. O increment_id é gerado pelo Magento na criação, então o middleware só descobre o valor depois que o pedido já existe. A chave precisa ser conhecida antes do primeiro envio, e por isso ela vem do lado do ERP.
E se o ERP mandar o mesmo pedido com valores diferentes?
Aí não é retry, é atualização, e a resposta certa não é criar nem ignorar. Consulte pelo ext_order_id, encontre o pedido e trate a diferença como cancelamento e novo pedido, ou como edição, conforme a regra do negócio.
Dá para transformar o índice em UNIQUE?
Tecnicamente sim, com um db_schema.xml de módulo próprio. Só entenda o efeito: qualquer pedido criado sem preencher o campo grava NULL, e no MySQL vários NULL convivem num índice único, então a proteção só vale para quem preenche.
Pra conferir na fonte
- OrderInterface.php — constante EXT_ORDER_ID — GitHub
- Magento_Sales db_schema.xml — coluna e índice ext_order_id — GitHub
- Search using REST endpoints — Adobe Developer
- Magento_Sales webapi.xml — rotas de pedido — GitHub