| # | O que o seu plugin faz | Gancho ou metadado | Plano · desde |
|---|---|---|---|
| 1 | Detecta o Correios Automático e registra os ganchos | plugins_loaded + constante INFIXS_CORREIOS_AUTOMATICO_PLUGIN_VERSION | Grátis |
| 2 | Entrega a NF-e do pedido | filtro infixs_correios_automatico_order_invoice e/ou os metas _infixs_correios_automatico_invoice_number + _infixs_correios_automatico_invoice_key | Grátis 1.11.0 |
| 3 | Faz a pré-postagem automática esperar a nota | filtro infixs_correios_automatico_auto_prepost_wait_invoice | PRO 1.3.0 |
| 4 | Avisa que a nota ficou pronta ou falhou | ação infixs_correios_automatico_order_invoice_ready / infixs_correios_automatico_order_invoice_failed (o seu plugin dispara) | PRO 1.3.0 ouve |
| 5 | No cancelamento, tira a nota dos metas | apagar os 2 metas se ainda guardam aquela chave | Grátis |
| 6 | Entrega os dados da DANFE simplificada | filtro infixs_correios_automatico_order_danfe | Grátis 1.12.0 |
| 7 | Mostra o status da nota na lista de pedidos | filtro infixs_correios_automatico_order_invoice_status | Grátis 1.12.0 |
| 8 | Ajusta o documento da logística reversa | filtro infixs_correios_automatico_reverse_prepost_fiscal_document | Grátis 1.14.0 |
| 9 | Entrega o XML ao armazém Correios LOG+ | ação infixs_correios_automatico_pro_logplus_order_created + filtro infixs_correios_automatico_pro_logplus_invoice_xml | PRO 1.6.0 |
| 10 | Lê o objeto e o rastreio | ação infixs_correios_automatico_prepost_created e infixs_correios_automatico_after_add_tracking_code | Grátis |
Visão do fluxo
- O pedido é pago e entra em Processando. Com o PRO, a pré-postagem automática roda nesse momento (no
woocommerce_order_status_processing, prioridade 100) e procura a NF-e de cada envio. - O seu plugin responde pelo filtro
infixs_correios_automatico_order_invoiceou pelos metas da nota. Com a nota autorizada, a pré-postagem sai com ela na hora, sem DC-e. - Se a nota ainda vai ser emitida, o seu plugin devolve
trueeminfixs_correios_automatico_auto_prepost_wait_invoicePRO. O envio fica aguardando e o pedido aparece em Pendências → NF-e no painel. - A SEFAZ autoriza: o seu plugin grava a nota, salva o pedido e dispara
infixs_correios_automatico_order_invoice_ready. O PRO relê o pedido e cria a pré-postagem com a NF-e. - Se a emissão falhar, o seu plugin dispara
infixs_correios_automatico_order_invoice_failedcom o motivo. O lojista vê a mensagem no pedido e o envio segue aguardando a nota corrigida. - Na impressão,
infixs_correios_automatico_order_danfeentrega os dados da DANFE simplificada, que sai junto com a etiqueta. - Na lista de pedidos,
infixs_correios_automatico_order_invoice_statusmostra o estado da nota com links para o seu plugin. - Na devolução,
infixs_correios_automatico_reverse_prepost_fiscal_documentajusta o documento da reversa, por exemplo com uma NF-e reemitida. - No armazém Correios LOG+ PRO, o seu plugin recebe o id do pedido nos Correios, emite a NF-e com ele em
xPede entrega o XML porinfixs_correios_automatico_pro_logplus_invoice_xml. - Por fim,
infixs_correios_automatico_prepost_createdeinfixs_correios_automatico_after_add_tracking_codedevolvem ao seu plugin o objeto e o código de rastreio.
No plugin grátis, quem pré-posta é o lojista (botões do pedido, formulário do painel ou pré-postagem em massa), e essas pré-postagens já usam a NF-e do seu plugin. A pré-postagem automática no pagamento é do PRO.
NF-e ou DC-e: como a pré-postagem decide
Em toda pré-postagem de ida, o Correios Automático escolhe o documento fiscal nesta ordem:
- Documento informado por quem pediu a pré-postagem. Se a chamada já traz
invoice_key,invoice_numberouemiteDCe, ele vale como veio e nenhum filtro roda. - Filtro
infixs_correios_automatico_prepost_use_order_invoice. Devolverfalsefaz a pré-postagem seguir com DC-e. - NF-e do pedido: o filtro
infixs_correios_automatico_order_invoicee, sem resposta válida dele, os metas_infixs_correios_automatico_invoice_numbere_infixs_correios_automatico_invoice_key. - Resultado: com chave de NF-e (modelo 55), a pré-postagem vai aos Correios com
chaveNFeenumeroNotaFiscal, sem DC-e. Sem NF-e, os Correios emitem a DC-e (emiteDCe: "S").
| Onde a pré-postagem nasce | O que o Correios Automático recebe | Documento |
|---|---|---|
| Pedido, botão “Criar Pré-Postagem c/ Nota Fiscal” | número e chave dos campos, já preenchidos com a NF-e resolvida | NF-e |
| Pedido, botão “Criar Pré-Postagem c/ Declaração Eletrônica (DCe)” | withDce: true | DC-e |
| Painel, formulário da pré-postagem | número e chave da nota, ou a opção DC-e | o escolhido |
| Painel, pré-postagem em massa | nenhum documento | o filtro da nota roda: NF-e se houver, senão DC-e |
| PRO Pré-postagem automática | a NF-e encontrada, ou nada se ninguém espera a nota | NF-e, ou DC-e sem espera |
PRO PUT /wp-json/wc/v3/orders/{id} com os dois metas | número e chave do corpo da requisição | NF-e na hora |
POST /wp-json/infixs-correios-automatico/v1/preposts | o que vier no corpo (detalhes) | conforme o corpo |
Bom saber
Depois de cada pré-postagem, o Correios Automático grava a nota usada nos metas _infixs_correios_automatico_invoice_number e _infixs_correios_automatico_invoice_key, seja qual for a origem. Assim o número e a chave ficam visíveis no pedido e na caixa de pré-postagem. Por isso o cancelamento da nota passa pelos metas (veja como).
1. Informar a NF-e do pedido
Dois caminhos levam a nota à pré-postagem, e eles se completam:
- Filtro
infixs_correios_automatico_order_invoice: responde com a nota a partir dos dados do seu plugin. Uma resposta válida tem prioridade sobre os metas. - Metas do pedido
_infixs_correios_automatico_invoice_numbere_infixs_correios_automatico_invoice_key: gravados quando a nota é autorizada. Valem como reserva e aparecem para o lojista na caixa de pré-postagem.
O padrão recomendado usa os dois: grave os metas na autorização, sem sobrescrever uma nota que o lojista digitou, e responda o filtro enquanto os metas estiverem vazios ou com a sua chave.
// Como o Correios Automático pergunta a nota:
/**
* @param array|null $invoice Resposta dos emissores anteriores; começa em null.
* @param WC_Order $order Pedido.
* @param array $context for ('prepost' | 'display'), shipping_item_id (0 = pedido inteiro), vendor_id (0 = a loja).
* @return array|null
*/
$invoice = apply_filters( 'infixs_correios_automatico_order_invoice', null, $order, $context );| Contexto | Valor |
|---|---|
for | 'prepost' quando a nota vai aos Correios (pré-postagem em massa, automática do PRO, reversa); 'display' quando só é exibida (lista de pedidos, etiqueta, caixa do pedido). |
shipping_item_id | linha de frete (volume) do envio; 0 = pedido inteiro. |
vendor_id | vendedor do marketplace que posta; 0 = a loja. |
| Chave da resposta | Obrigatória | Regra |
|---|---|---|
invoice_key | sim | 44 dígitos com modelo 55 nas posições 21 e 22. Na leitura, tudo o que não é dígito sai (espaços, pontos, o prefixo NFe). |
invoice_number | não | Sem ele, o número é lido da chave (posições 26 a 34, sem zeros à esquerda). |
source | não | Identificador da sua integração; passa por sanitize_key(). Vazio vira unknown. |
source_label | não | Nome mostrado ao lojista. Vazio vira o source. |
Devolva null, ou o valor recebido, quando o pedido não for seu: o próximo emissor responde e, por fim, os metas. Quando a nota vem dos metas, o plugin mostra a origem como “informada no pedido” (source = order_meta).
add_filter( 'infixs_correios_automatico_order_invoice', 'meu_plugin_nfe_do_pedido', 10, 3 );
function meu_plugin_nfe_do_pedido( $invoice, $order, $context ) {
if ( null !== $invoice || ! $order instanceof WC_Order ) {
return $invoice; // Outro emissor já respondeu.
}
if ( ! empty( $context['vendor_id'] ) ) {
return $invoice; // No marketplace, o vendedor emite a nota dele.
}
$nfe = meu_plugin_nfe_autorizada( $order ); // Leitura local (meta ou tabela), sem chamar API.
if ( ! $nfe || 'production' !== $nfe['ambiente'] ) {
return $invoice; // Só nota autorizada de produção viaja com o objeto.
}
$informada = preg_replace( '/\D+/', '', (string) $order->get_meta( '_infixs_correios_automatico_invoice_key', true ) );
if ( '' !== $informada && $informada !== $nfe['chave'] ) {
return $invoice; // O lojista informou outra NF-e: os metas respondem por ela.
}
return [
'invoice_key' => $nfe['chave'], // '35261112345678000195550010000012341123456781'
'invoice_number' => $nfe['numero'], // '1234' (opcional: sai da chave)
'source' => 'meu-plugin-nfe',
'source_label' => 'Meu Plugin de NF-e',
];
}E a publicação nos metas, com o aviso de nota pronta logo depois:
// Quando a SEFAZ autorizar a NF-e do pedido:
function meu_plugin_publicar_nfe( WC_Order $order, string $numero, string $chave ) {
$atual = preg_replace( '/\D+/', '', (string) $order->get_meta( '_infixs_correios_automatico_invoice_key', true ) );
if ( '' !== $atual && $atual !== $chave ) {
return; // O lojista (ou outro emissor) já informou outra nota: ela prevalece.
}
$order->update_meta_data( '_infixs_correios_automatico_invoice_number', $numero );
$order->update_meta_data( '_infixs_correios_automatico_invoice_key', $chave ); // string de 44 dígitos
$order->save();
do_action( 'infixs_correios_automatico_order_invoice_ready', $order->get_id() );
}Dicas para a sua integração
- O filtro roda em cada linha da lista de pedidos do painel, em cada etiqueta e na caixa do pedido (sempre com
for = display). Responda do que já está salvo no pedido ou na sua tabela, sem chamar API. - Na pré-postagem, o Correios Automático confere o tamanho e o modelo da chave. O dígito verificador, o ambiente (produção ou homologação) e o emitente ficam com o seu plugin: entregue só nota autorizada de produção e valide a chave antes (
\Infixs\CorreiosAutomatico\Utils\AccessKey::hasValidCheckDigit()). - Uma resposta fora do formato é ignorada, e a leitura segue para os metas.
- Encadeamento: devolva o
$invoicerecebido quando ele não fornull, e também quando$context['vendor_id']não for zero (o vendedor emite a nota dele).
2. Avisar que a nota ficou pronta
Assim que a nota estiver legível (metas salvos ou filtro pronto para responder), dispare a ação:
// Depois de salvar a nota (metas gravados ou filtro pronto para responder):
do_action( 'infixs_correios_automatico_order_invoice_ready', (int) $order_id );- Quem dispara é o seu plugin. Quem ouve é o PRO: ele relê o pedido, busca a nota de novo e cria a pré-postagem dos envios que aguardavam. No Correios LOG+ (PRO 1.6.2+), o mesmo aviso adianta o envio do XML ao armazém.
- Pode disparar sempre que autorizar uma nota. Sem envio aguardando, a ação não faz nada; com o grátis sozinho, a nota já aparece na próxima pré-postagem do lojista.
- O PRO trava o pedido enquanto retoma os envios (a trava sai no fim e, se sobrar, expira sozinha em 60 segundos) e tira cada envio da espera depois da tentativa. Assim, avisos simultâneos ou seguidos (webhook e cron, por exemplo) geram uma pré-postagem só.
- Um pedido em Aguardando (
on-hold) não é postado no aviso. Ele continua aguardando e sai quando o pedido voltar a Processando. - O PRO dispara as mesmas ações para as notas da integração nativa com o Bling; um ouvinte seu também as recebe.
3. Fazer a pré-postagem automática esperar a nota
PRO Sem nota no momento do pagamento, a pré-postagem automática sai na hora com DC-e. Para ela esperar a NF-e do seu plugin, responda true neste filtro:
// Como o PRO pergunta se deve esperar:
/**
* @param bool $wait false por padrão.
* @param WC_Order $order Pedido que vai ser pré-postado.
* @param int $shipping_item_id Linha de frete do envio, 0 para o pedido inteiro.
* @return bool
*/
$wait = apply_filters( 'infixs_correios_automatico_auto_prepost_wait_invoice', false, $order, $shipping_item_id );- É perguntado só quando o envio ainda não tem NF-e, e só se
infixs_correios_automatico_prepost_use_order_invoicenão devolveufalse. - A pergunta chega quando o pedido entra em Processando, antes de
woocommerce_payment_complete. Responda pelo que o plugin já sabe (configuração, regra de emissão) e não por um agendamento criado no pagamento. - Ela se repete a cada aviso de nota pronta e de hora em hora, na varredura dos pedidos que aguardam. Continue respondendo
trueenquanto a nota estiver a caminho, inclusive depois de uma rejeição que o lojista vai corrigir. - Quando a nota não vai sair (emissão desligada, pedido dispensado, ambiente de homologação), responda
false: o envio sai com DC-e no próximo aviso ou na varredura. $shipping_item_idé0para o pedido inteiro ou o id da linha de frete de cada volume. Uma NF-e cobre todos os volumes do pedido.- Se outro emissor já pediu a espera (
$waitigual atrue), devolvatrue.
add_filter( 'infixs_correios_automatico_auto_prepost_wait_invoice', 'meu_plugin_esperar_nfe', 10, 3 );
function meu_plugin_esperar_nfe( $wait, $order, $shipping_item_id ) {
if ( $wait || ! $order instanceof WC_Order ) {
return (bool) $wait; // Outro emissor já pediu a espera.
}
if ( 'yes' !== get_option( 'meu_plugin_emitir_automatico', 'yes' ) ) {
return false; // O seu plugin não vai emitir: a pré-postagem segue com DC-e.
}
// Decida pelo que já se sabe do pedido: configuração, regra de emissão, situação da nota.
// Uma nota rejeitada continua esperando: o lojista corrige e emite de novo.
return ! in_array( meu_plugin_situacao_nfe( $order ), [ 'autorizada', 'cancelada', 'dispensada' ], true );
}| Evento | O que acontece com o envio que aguarda |
|---|---|
| Aviso de nota pronta e a nota resolve | Pré-postagem com a NF-e, sem DC-e. |
| Aviso de nota pronta, sem nota, e o filtro ainda pede espera | Continua aguardando. |
Aviso de nota pronta, sem nota, e o filtro passou a dizer false | Pré-postagem com DC-e. |
| Varredura de hora em hora | O mesmo que o aviso, para cada pedido que aguarda. |
| Pedido cancelado, reembolsado ou com falha | A espera é encerrada. |
Pedido em Aguardando (on-hold) | Não posta agora; posta quando o pedido volta a Processando. |
| Lojista pré-posta à mão (com NF-e ou DC-e) | A espera daquele envio é encerrada. |
| Pedido enviado ao armazém Correios LOG+ | A espera é encerrada: o armazém posta. |
| Método com atraso de pré-postagem configurado | O atraso conta desde o pagamento; a pré-postagem sai quando ele termina. |
Atalho
Se o seu plugin autoriza a nota de forma síncrona no próprio woocommerce_order_status_processing, com prioridade menor que 100, e salva o pedido, a pré-postagem automática já encontra a nota e sai com ela, sem passar pela espera.
Para acompanhar a espera: o meta _infixs_correios_automatico_auto_prepost_waiting_invoice (somente leitura) guarda shipping_item_id => timestamp de cada envio que aguarda; no painel, o pedido mostra “Pré-postagem automática aguardando a NF-e” e aparece em Pendências → NF-e; na primeira espera, o pedido ganha a nota privada “Pré-postagem automática aguardando a nota fiscal (NF-e).”
4. Quando a emissão falha
// A SEFAZ rejeitou a nota:
do_action( 'infixs_correios_automatico_order_invoice_failed', (int) $order_id, 'Rejeição 778: NCM inexistente' );- O PRO registra, em cada envio que aguarda, o erro de pré-postagem
invoice_unavailable: “A pré-postagem automática aguarda a nota fiscal (NF-e), que não pôde ser emitida: {mensagem}”. O lojista vê o erro no pedido e no painel. - O envio continua aguardando. A nota reemitida, seguida do aviso de nota pronta, cria a pré-postagem e limpa o erro. Nesse meio-tempo o lojista pode pré-postar à mão com DC-e.
- A mensagem passa por
sanitize_text_field(). Use o motivo da SEFAZ, que é o que o lojista precisa para corrigir. - O aviso vale para envios que já aguardam. Para uma rejeição anterior ao pagamento, mostre o motivo também no status da nota.
Cancelamento da NF-e
Ao cancelar uma NF-e, apague os dois metas se eles ainda guardam aquela chave, e preserve uma nota que o lojista tenha digitado. Assim a próxima pré-postagem, a etiqueta e a DANFE passam a usar a nota certa.
// Ao cancelar a NF-e no seu plugin:
function meu_plugin_nfe_cancelada( WC_Order $order, string $chave_cancelada ) {
$atual = preg_replace( '/\D+/', '', (string) $order->get_meta( '_infixs_correios_automatico_invoice_key', true ) );
if ( $atual !== $chave_cancelada ) {
return; // Os metas guardam outra nota (digitada pelo lojista ou de outro emissor).
}
$order->delete_meta_data( '_infixs_correios_automatico_invoice_number' );
$order->delete_meta_data( '_infixs_correios_automatico_invoice_key' );
$order->save();
}Uma pré-postagem criada com a nota cancelada continua valendo nos Correios. Avise o lojista, por exemplo com uma nota no pedido, para cancelar e refazer a pré-postagem (o cancelamento de pré-postagem é um recurso do PRO). A página de impressão também avisa quando a chave da pré-postagem é diferente da chave da DANFE.
DANFE simplificada na etiqueta
O filtro infixs_correios_automatico_order_danfe entrega os dados da DANFE simplificada. A página de impressão, o PDF e a impressão automática do PRO colocam a DANFE junto com a etiqueta, separada ou embutida conforme o perfil de impressão (danfe.enabled e danfe.mode separate ou embedded).
// Como o Correios Automático pede a DANFE:
/**
* @param array|null $danfe Resposta dos emissores anteriores; começa em null.
* @param WC_Order $order Pedido.
* @param array $context for ('label' | 'display'), shipping_item_id, vendor_id e invoice:
* [ 'invoice_number' => '1234', 'invoice_key' => '3526...', 'model' => '55', 'source' => '...', 'source_label' => '...' ].
* @return array|null
*/
$danfe = apply_filters( 'infixs_correios_automatico_order_danfe', null, $order, $context );- Roda quando o pedido tem NF-e resolvida, e
$context['invoice']traz essa nota. Responda só por ela. - Respostas:
null(sem dados),[ 'schema' => 1, 'pending' => true ](dados em preparação) ou o bloco completo, com os valores crus do XML autorizado. - Registrar o filtro liga as opções da DANFE no painel; registre-o quando o seu plugin tiver esses dados.
foré'label'na impressão e'display'no painel.
| Campo | Origem no XML | Regra de aceite |
|---|---|---|
schema | — | Exatamente 1 (OrderDanfeResolver::SCHEMA). |
source / source_label | — | Identificador e nome do seu plugin. |
environment | ide/tpAmb | production ou homologation. |
key | chNFe | 44 dígitos, modelo 55, dígito verificador válido e igual a $context['invoice']['invoice_key']. |
number | ide/nNF | 1 a 9 dígitos, igual ao número da chave. |
series | ide/serie | 1 a 3 dígitos, igual à série da chave. |
operation | ide/tpNF | out (1, saída) ou in (0, entrada). |
issued_at | ide/dhEmi | Data e hora ISO-8601 (AAAA-MM-DDTHH:MM:SS, com o fuso do XML). |
protocol.number | protNFe/infProt/nProt | 15 dígitos. |
protocol.authorized_at | protNFe/infProt/dhRecbto | Data e hora ISO-8601. |
emitter.name / .document / .ie / .uf | emit | Nome obrigatório (a DANFE usa até 60 caracteres); documento igual ao CNPJ/CPF da chave; UF com 2 letras; IE opcional. |
recipient.name / .document / .ie / .uf | dest | Nome e UF obrigatórios (EX no exterior); documento e IE opcionais. |
total | total/ICMSTot/vNF | Em centavos (inteiro). Opcional: um valor fora do formato só fica fora da DANFE. |
add_filter( 'infixs_correios_automatico_order_danfe', 'meu_plugin_danfe', 10, 3 );
function meu_plugin_danfe( $danfe, $order, $context ) {
if ( null !== $danfe || ! $order instanceof WC_Order || ! empty( $context['vendor_id'] ) ) {
return $danfe;
}
$nfe = meu_plugin_nfe_autorizada( $order );
if ( ! $nfe || $nfe['chave'] !== $context['invoice']['invoice_key'] ) {
return $danfe; // Responda só pela NF-e que o Correios Automático resolveu para o pedido.
}
if ( empty( $nfe['protocolo'] ) ) {
return [ 'schema' => 1, 'pending' => true ]; // Dados ainda em preparação.
}
return [
'schema' => 1,
'source' => 'meu-plugin-nfe',
'source_label' => 'Meu Plugin de NF-e',
'environment' => 'production',
'key' => '35261112345678000195550010000012341123456781',
'number' => '1234',
'series' => '1',
'operation' => 'out',
'issued_at' => '2026-11-04T10:15:00-03:00',
'protocol' => [ 'number' => '135260000000001', 'authorized_at' => '2026-11-04T10:15:30-03:00' ],
'emitter' => [ 'name' => 'Loja Exemplo LTDA', 'document' => '12345678000195', 'ie' => '123456789', 'uf' => 'SP' ],
'recipient' => [ 'name' => 'Cliente Exemplo', 'document' => '000.000.000-00', 'ie' => '', 'uf' => 'RJ' ],
'total' => 15990, // R$ 159,90 em centavos
];
}Um bloco recusado é registrado no log do Correios Automático com o motivo (reason: schema, environment, key_invalid, key_mismatch, number, operation, issued_at, protocol, emitter ou recipient), e a etiqueta sai sem a DANFE. Para imprimir só a DANFE de pedidos, \Infixs\CorreiosAutomatico\Core\Admin\PrintUrl::orders( [ $order_id ], PrintUrl::VIEW_DANFE ) devolve a URL da página de impressão no admin (exige manage_woocommerce).
Status da nota na lista de pedidos
Cada pedido do painel mostra o estado da NF-e. O padrão vem pronto (“NF-e nº 1234”, “Pré-postagem automática aguardando a NF-e” ou “Sem NF-e”), e o filtro deixa o seu plugin contar mais: nota em emissão, rejeitada com o motivo, cancelada e links para ela.
// Como o Correios Automático monta o status:
/**
* @param array $status Estado padrão (ou o de emissores anteriores).
* @param WC_Order $order Pedido.
* @param array $context vendor_id (int), invoice (array|null, a NF-e resolvida) e waiting (bool).
* @return array
*/
$status = apply_filters( 'infixs_correios_automatico_order_invoice_status', $status, $order, $context );| Chave | Valores aceitos |
|---|---|
state | none, pending, authorized, error ou cancelled. |
tone | muted, warning, success ou danger. Sem ele, vale o tom do estado. |
label | Texto curto, ex.: NF-e nº 1234. |
detail | Texto longo, ex.: o motivo da rejeição. |
source / source_label | Identificador e nome do seu plugin. |
danfe | ready, pending, unavailable ou none. A prontidão calculada pelo plugin chega em $status['danfe']. |
actions | Até 4 links { id, label, url, target }. A url precisa estar no mesmo host do home_url() ou do admin_url(); target aceita _blank. |
Os textos passam por sanitize_text_field(), e um item fora do formato é descartado. Se $context['invoice'] traz a chave de outra nota, devolva $status como veio.
add_filter( 'infixs_correios_automatico_order_invoice_status', 'meu_plugin_status_nfe', 10, 3 );
function meu_plugin_status_nfe( $status, $order, $context ) {
if ( ! is_array( $status ) || ! $order instanceof WC_Order || ! empty( $context['vendor_id'] ) ) {
return $status;
}
$nfe = meu_plugin_nfe( $order ); // Qualquer situação: em emissão, autorizada, rejeitada...
if ( ! $nfe ) {
return $status; // Mantém o padrão do Correios Automático.
}
$resolvida = $context['invoice']['invoice_key'] ?? '';
if ( '' !== $resolvida && $resolvida !== $nfe['chave'] ) {
return $status; // O pedido viaja com a nota de outro emissor ou digitada à mão.
}
if ( 'rejeitada' === $nfe['situacao'] ) {
return [
'state' => 'error',
'tone' => 'danger',
'label' => 'NF-e rejeitada',
'detail' => $nfe['motivo'],
'source' => 'meu-plugin-nfe',
'source_label' => 'Meu Plugin de NF-e',
'danfe' => 'none',
'actions' => [
[
'id' => 'abrir',
'label' => 'Abrir no Meu Plugin de NF-e',
'url' => admin_url( 'admin.php?page=meu-plugin-nfe&order=' . $order->get_id() ),
'target' => '_blank',
],
],
];
}
return $status;
}Pendências próprias na lista de pedidos
Dois filtros acrescentam uma pendência sua ao filtro de pendências da rota GET /orders do painel, ao lado das nativas tracking, prepost e invoice. A chave precisa casar com [a-z_]+, e a condição é um trecho SQL já preparado que usa o marcador {order_id} no lugar da coluna do pedido (o plugin troca pelo nome certo, com posts ou HPOS).
add_filter( 'infixs_correios_automatico_order_pending_kinds', function ( $kinds ) {
$kinds = is_array( $kinds ) ? $kinds : [];
$kinds[] = 'meu_plugin_nfe';
return array_values( array_unique( $kinds ) );
} );
add_filter( 'infixs_correios_automatico_order_pending_condition', function ( $condition, $kind ) {
if ( null !== $condition || 'meu_plugin_nfe' !== $kind ) {
return $condition;
}
global $wpdb;
$token = \Infixs\CorreiosAutomatico\Utils\OrderQuery::ORDER_ID_TOKEN; // '{order_id}'
if ( \Infixs\CorreiosAutomatico\Utils\OrderQuery::usesOrdersTable() ) {
$table = \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::get_meta_table_name();
$column = 'order_id';
} else {
$table = $wpdb->postmeta;
$column = 'post_id';
}
return $wpdb->prepare(
"EXISTS ( SELECT 1 FROM {$table} AS meu_nfe WHERE meu_nfe.{$column} = {$token} AND meu_nfe.meta_key = %s AND meu_nfe.meta_value = %s )",
'_meu_plugin_nfe_situacao',
'rejeitada'
);
}, 10, 2 );A pendência própria fica disponível em GET /orders?pending=meu_plugin_nfe e na preferência salva do operador. O menu Pendências do painel mostra as pendências nativas e as do Correios LOG+.
Logística reversa
Na devolução, o Correios Automático já reaproveita o documento fiscal da ida. O filtro permite ajustar essa escolha, por exemplo trocando uma NF-e cancelada e reemitida pela nova.
// Como o Correios Automático decide o documento da devolução:
/**
* @param array $document emiteDCe, source e, com nota, invoice_number e invoice_key.
* @param WC_Order $order Pedido devolvido.
* @param int $shipping_item_id Linha de frete devolvida, 0 para o pedido inteiro.
* @param Prepost|null $outbound Pré-postagem da ida (\Infixs\CorreiosAutomatico\Models\Prepost), ou null.
* @return array
*/
$document = apply_filters( 'infixs_correios_automatico_reverse_prepost_fiscal_document', $document, $order, $shipping_item_id, $outbound );source recebido | Quando | Documento |
|---|---|---|
outbound_invoice | A ida saiu com NF-e emitida pela loja (mesma raiz de CNPJ do remetente). | A mesma NF-e, com emiteDCe: 'N'. |
outbound_dce | A ida saiu com DC-e. | A mesma DC-e (chave modelo 99). Se os Correios não acharem a chave, pede uma DC-e nova (emiteDCe: 'S'). |
order_invoice | A ida não saiu com NF-e da loja nem com DC-e: o plugin não pré-postou o envio (ou a pré-postagem foi cancelada ou expirou), a pré-postagem é anterior à DC-e ou foi com a NF-e de outro emitente. | A NF-e do pedido, se emitida pela loja. |
declaration | Nenhum dos casos acima. | Só a declaração de conteúdo. |
- O filtro roda na criação da reversa e na prévia (
GET /preposts/reverse/document). Quando quem pediu a reversa já mandou o documento (source=caller), ele vai como veio. - Na resposta,
emiteDCesó pede DC-e com a string'S'; a chave precisa ser de modelo 55 ou 99;invoice_numberfica só com dígitos e, vazio, sai da chave. - Se os Correios recusarem o documento (400 ou 422), o plugin refaz a reversa sem documento e deixa uma nota no pedido. Essa proteção vale para os
sourceacima: mantenha osourcerecebido para conservá-la. - A reversa é do Contrato Próprio; no Contrato pré-pago, a rota responde 422
prepaid_reverse_not_supported.
add_filter( 'infixs_correios_automatico_reverse_prepost_fiscal_document', 'meu_plugin_documento_da_devolucao', 10, 4 );
function meu_plugin_documento_da_devolucao( $document, $order, $shipping_item_id, $outbound ) {
if ( ! is_array( $document ) || ! $order instanceof WC_Order || empty( $document['invoice_key'] ) ) {
return $document; // Mantém a decisão do Correios Automático.
}
$nova = meu_plugin_nfe_que_substituiu( $order, $document['invoice_key'] ); // null se a nota da ida segue valendo
if ( ! $nova ) {
return $document;
}
return [
'invoice_key' => $nova['chave'],
'invoice_number' => $nova['numero'],
'emiteDCe' => 'N',
'source' => $document['source'], // Um source conhecido mantém a nova tentativa sem documento.
];
}Correios LOG+ e o xPed
PRO No Correios LOG+, o armazém dos Correios separa, embala e posta. O pedido vai ao armazém, a NF-e é emitida com o id do pedido nos Correios em <compra><xPed> e o XML é entregue ao armazém. Os pedidos do LOG+ não passam pela pré-postagem automática.
- Ouça
infixs_correios_automatico_pro_logplus_order_created( $order_id, $correios_id, $order ). O$correios_id, também gravado no meta_infixs_logplus_id, é o valor doxPede é diferente do número do pedido da loja. - Emita a NF-e depois desse evento, com
<compra><xPed>igual a$correios_id. A nota do pedido orienta também a transportadora: Correios, CNPJ 34.028.316/0001-03. - Entregue o XML autorizado (o
nfeProccompleto) pelo filtroinfixs_correios_automatico_pro_logplus_invoice_xml. - Dispare
infixs_correios_automatico_order_invoice_ready: o envio do XML é adiantado para agora (PRO 1.6.2+; na 1.6.0 e na 1.6.1, o XML espera o envio agendado ou a rodada de 30 minutos).
// 1) O pedido entrou no armazém: guarde o id dos Correios, que a NF-e leva em <compra><xPed>.
add_action( 'infixs_correios_automatico_pro_logplus_order_created', function ( $order_id, $correios_id, $order ) {
meu_plugin_agendar_emissao( (int) $order_id, [ 'xPed' => (string) $correios_id ] );
}, 10, 3 );
// 2) Entregue o XML autorizado (nfeProc completo).
add_filter( 'infixs_correios_automatico_pro_logplus_invoice_xml', function ( $xml, $order, $correios_id ) {
if ( ( is_string( $xml ) && '' !== trim( $xml ) ) || ! $order instanceof WC_Order ) {
return $xml; // Outro plugin já entregou.
}
$autorizado = meu_plugin_xml_autorizado( $order ); // emitido com xPed = $correios_id
return $autorizado ? $autorizado : $xml;
}, 10, 3 );
// 3) Explique ao lojista onde corrigir quando o XML sai sem o xPed.
add_filter( 'infixs_correios_automatico_pro_logplus_invoice_xml_error', function ( $message, $error, $xml, $order, $correios_id ) {
$data = $error instanceof WP_Error ? $error->get_error_data() : null;
if ( is_array( $data ) && 'xped' === ( $data['reason'] ?? '' ) && meu_plugin_e_meu_xml( $order, $xml ) ) {
return sprintf( 'Emita a NF-e de novo no Meu Plugin de NF-e com o campo "Pedido de compra" = %s.', $correios_id );
}
return $message;
}, 10, 5 );
// 4) Assim que a NF-e autorizar, adiante o envio do XML (PRO 1.6.2+; sem o aviso, o envio sai na rodada seguinte).
do_action( 'infixs_correios_automatico_order_invoice_ready', $order_id );| Evento | O que acontece |
|---|---|
| Pedido criado no armazém | O primeiro envio do XML é agendado para 30 minutos depois. |
| Aviso de nota pronta (PRO 1.6.2+) | O envio do XML é adiantado para agora (e um XML recusado na conferência é reaberto, se ainda restam tentativas). |
| Rodada a cada 30 minutos | Procura o XML dos pedidos que ainda aguardam; sem XML, apenas espera, sem contar tentativa. |
| XML reprovado na conferência local | O pedido ganha uma nota com o motivo, uma vez por motivo. Um novo aviso de nota pronta tenta de novo. |
| Armazém recusa o XML | As 7 primeiras tentativas seguem o manual dos Correios (30 minutos e depois de 10 em 10). Se o armazém responde que o pedido ainda não está liberado para faturamento, ou os Correios não respondem, o XML segue indo sozinho: de hora em hora até 3 dias do pedido e a cada 3 horas até 10 dias (filtro infixs_correios_automatico_pro_logplus_invoice_wait_days), com uma nota no pedido. Outras recusas param na 7ª, e o lojista reenvia pelo quadro Correios LOG+ do pedido. O botão Atualizar do pedido também envia na hora o XML que ele espera. Desde PRO 1.6.4. |
| Armazém aceita | Nota no pedido: “XML da NF-e entregue ao armazém Correios LOG+.” |
- O XML pode ter até 1 MB. A conferência procura
compra/xPedsem depender do namespace e remove BOM e espaços das pontas. - O XML que o lojista envia à mão (no quadro Correios LOG+ do pedido ou pela REST) tem prioridade, e enquanto ele existir o filtro não é consultado (desde a 1.6.2).
- Registre o filtro na prioridade padrão (10) e devolva o
$xmlrecebido quando ele não for seu: assim o seu plugin convive com outras integrações de nota. - O filtro de mensagem
infixs_correios_automatico_pro_logplus_invoice_xml_errorvale para a conferência local; quando falta oxPedou ele está diferente,$error->get_error_data()['reason']éxped. - O Correios LOG+ usa o Contrato Próprio, a licença PRO ativa e a integração ligada nas configurações.
Ler a pré-postagem e o rastreio
infixs_correios_automatico_prepost_created( $order_id, $prepost )dispara depois de toda pré-postagem criada, de ida e de reversa, por qualquer caminho. Ela chega antes de o código de rastreio ser gravado.- O
$preposttrazid,order_id,shipping_item_id,vendor_id,object_code(o código do objeto, que é o rastreio),service_code,status,status_label,expire_at,invoice_keyeinvoice_numbercomo os Correios responderam, edce. Leia os campos comisset(). - Para saber se é devolução, compare
object_codecom o meta_infixs_correios_automatico_reverse_prepost_code(já gravado nesse momento), useDeliveryServiceCode::isReverseService( $prepost->service_code )ou ouçainfixs_correios_automatico_reverse_prepost_created. infixs_correios_automatico_after_add_tracking_code( $code, $order_id )recebe o código primeiro e o pedido depois. Ele dispara para todo código gravado, inclusive de outras transportadoras e da devolução;Container::trackingService()->getShipmentCodes( $order_id )separa os objetos de ida.- Na REST do WooCommerce,
GET /wp-json/wc/v3/orders/{id}traztracking_codesecorreios_tracking_code.
// Pré-postagem criada (ida e reversa, por qualquer caminho).
add_action( 'infixs_correios_automatico_prepost_created', function ( $order_id, $prepost ) {
$order = wc_get_order( $order_id );
if ( ! $order || ! is_object( $prepost ) || empty( $prepost->object_code ) ) {
return;
}
if ( $prepost->object_code === $order->get_meta( '_infixs_correios_automatico_reverse_prepost_code', true ) ) {
return; // Devolução: volta para a loja.
}
$com_dce = ! empty( $prepost->dce );
$chave = $com_dce ? '' : (string) $order->get_meta( '_infixs_correios_automatico_invoice_key', true );
meu_plugin_registrar_objeto( (int) $order_id, (string) $prepost->object_code, $chave );
}, 10, 2 );
// Código de rastreio gravado. Atenção à ordem: código primeiro, pedido depois.
add_action( 'infixs_correios_automatico_after_add_tracking_code', function ( $code, $order_id ) {
$ida = wp_list_pluck(
\Infixs\CorreiosAutomatico\Container::trackingService()->getShipmentCodes( (int) $order_id ),
'code'
);
if ( in_array( (string) $code, array_map( 'strval', $ida ), true ) ) {
meu_plugin_enviar_rastreio( (int) $order_id, (string) $code );
}
}, 10, 2 );ERP fora do WordPress
Um ERP que emite a nota fora da loja integra pela REST, com uma senha de aplicação de um usuário com manage_woocommerce:
- Pré-postar com a NF-e:
POST /wp-json/infixs-correios-automatico/v1/prepostscomorder_id,shipping_item_id,invoice_numbereinvoice_key. A rota pré-posta, grava a nota no pedido e adiciona o código de rastreio. Veja o exemplo. - PRO Gravar a nota pelo WooCommerce:
PUT /wp-json/wc/v3/orders/{id}com os dois metas emmeta_datapré-posta na hora cada volume ainda não pré-postado, com a NF-e e o código de rastreio. Veja REST do WooCommerce. - PRO XML do Correios LOG+:
POST /wp-json/infixs-correios-automatico/v1/logplus/order/{id}/xmlcom{ "xml": "…" }. - Acompanhar:
GET /wp-json/infixs-correios-automatico/v1/orders/{id}(nota, pré-postagens, erros e rastreios) eGET /wp-json/infixs-correios-automatico/v1/orders?pending=invoice&status=(pedidos aguardando a nota).
Pré-postar pelo seu código
Para a maioria das integrações, entregar a nota e disparar o aviso de nota pronta basta: o PRO e o lojista continuam no controle. Se o seu plugin precisar criar a pré-postagem ele mesmo, Container::prepostService()->createPrepost( $order_id, $data ) aceita invoice_key, invoice_number, emiteDCe (S ou N), shipping_item_id, vendor_id, receiptNotice, receiptNoticeElectronic, ownHands e dangerousProduct. Complete o que a rota REST faz por você: confira o contrato, valide a chave, evite duplicar o envio e grave o código de rastreio. No marketplace, deixe de fora as remessas dos vendedores, que saem com a nota deles.
function meu_plugin_prepostar_com_nfe( int $order_id, string $chave, string $numero ) {
$c = '\Infixs\CorreiosAutomatico\Container';
if ( ! class_exists( $c ) || ! method_exists( $c, 'contractMode' ) ) {
return new WP_Error( 'ca_missing', 'Correios Automático 1.14.0 ou superior não está ativo.' );
}
$erro = $c::contractMode()->prepostError();
if ( $erro ) {
return $erro; // Contrato Próprio desligado, ou Contrato pré-pago desconectado ou com a conexão expirada.
}
$key_class = '\Infixs\CorreiosAutomatico\Utils\AccessKey';
$chave = (string) $key_class::normalize( $chave );
if ( ! $key_class::isInvoice( $chave ) || ! $key_class::hasValidCheckDigit( $chave ) ) {
return new WP_Error( 'invalid_key', 'Chave de NF-e inválida.' );
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
return new WP_Error( 'invalid_order', 'Pedido não encontrado.' );
}
$entidade = '\Infixs\CorreiosAutomatico\Entities\Order';
foreach ( $c::shipmentContextResolver()->resolveShipmentContexts( $order_id, [] ) as $ctx ) {
// O vendedor da remessa, resolvido como a pré-postagem o resolve.
$escopo = $entidade::resolveScope( new $entidade( $order ), [ 'for' => 'prepost' ] + $ctx );
if ( ! empty( $escopo['vendor_id'] ) ) {
continue; // No marketplace, o vendedor emite a nota dele.
}
$item_id = isset( $ctx['shipping_item_id'] ) ? (int) $ctx['shipping_item_id'] : 0;
$item = $item_id ? $order->get_item( $item_id ) : null;
$feito = $item
? $item->get_meta( '_infixs_correios_automatico_prepost_id' )
: $order->get_meta( '_infixs_correios_automatico_prepost_created' );
if ( $feito ) {
continue; // Já pré-postado.
}
$prepost = $c::prepostService()->createPrepost( $order_id, $ctx + [
'invoice_key' => $chave,
'invoice_number' => $numero,
'emiteDCe' => 'N',
] );
if ( is_wp_error( $prepost ) ) {
continue; // O erro já ficou registrado no pedido.
}
$c::trackingService()->add( $order_id, $prepost->object_code, false, [
'shipping_item_id' => (int) $prepost->shipping_item_id,
] );
}
return true;
}Exemplo completo
O mini-plugin “Meu Plugin de NF-e” (PHP 7.4) faz a integração inteira só com ganchos públicos e metas do pedido: entrega a nota, espera no PRO, mostra o status, imprime a DANFE, cuida da reversa, do Correios LOG+, do cancelamento e da leitura do rastreio. Troque a classe My_NFe_Record pela sua tabela ou serviço de notas.
| Parte | O que faz | Ganchos |
|---|---|---|
my_nfe_boot_correios_integration() | No plugins_loaded 20, registra a integração quando o grátis 1.11.0+ e o WooCommerce estão ativos. | — |
My_NFe_Access_Key | Confere 44 dígitos, modelo 55 e dígito verificador (módulo 11). | — |
orderInvoice() | Responde só nota autorizada de produção; respeita outro emissor, o vendedor do marketplace e a nota digitada à mão. | order_invoice (10, 3) |
waitInvoice() | Espera enquanto a nota vai sair (rejeitada continua esperando); pedido marcado sem nota não espera. | auto_prepost_wait_invoice (10, 3) |
invoiceStatus() | Estado próprio (em emissão, autorizada, rejeitada, cancelada, sem nota) e 2 links do admin. | order_invoice_status (10, 3) |
orderDanfe() | Bloco cru do XML; pending enquanto faltam dados. | order_danfe (10, 3) |
reverseDocument() | Troca a NF-e cancelada da ida pela nova, mantendo o source. | reverse_prepost_fiscal_document (10, 4) |
onLogPlusOrderCreated(), logPlusInvoiceXml(), logPlusRefusal() | Guarda o xPed, entrega o XML e explica a recusa por xPed. | LOG+ (10, 3 · 10, 3 · 10, 5) |
onPrepostCreated(), onTrackingCodeAdded() | Lembra a chave usada em cada objeto de ida e guarda só o rastreio de ida. | prepost_created e after_add_tracking_code (10, 2) |
onAuthorized(), onFailed(), onSkipped(), onCancelled() | Publica os metas e dispara o aviso; repassa a rejeição; avisa que o pedido não vai ter nota (o envio sai com a DC-e); no cancelamento, limpa os metas e lista os objetos que saíram com a nota cancelada. | order_invoice_ready, order_invoice_failed |
No seu plugin, ligue os momentos da emissão aos eventos do exemplo. Marque também o pedido que não vai ter nota: assim o envio que esperava por ela sai com a DC-e.
$records = new My_NFe_Record();
// Opcional, ao enviar a nota à SEFAZ (o painel mostra "NF-e em emissão"):
$records->saveProcessing( $order );
// NF-e autorizada pela SEFAZ:
$records->saveAuthorized( $order, [
'key' => $chave, // 44 dígitos
'number' => $nNF,
'series' => $serie,
'environment' => 'production', // ou 'homologation'
'operation' => 'out',
'issued_at' => $dhEmi, // ISO-8601, como no XML
'protocol' => $nProt, // 15 dígitos
'authorized_at' => $dhRecbto,
'emitter' => [ 'name' => $emit_nome, 'document' => $emit_cnpj, 'ie' => $emit_ie, 'uf' => $emit_uf ],
'recipient' => [ 'name' => $dest_nome, 'document' => $dest_doc, 'ie' => $dest_ie, 'uf' => $dest_uf ],
'total_cents' => $vNF_em_centavos,
'xml' => $xml_nfeProc,
] );
do_action( 'my_nfe_authorized', $order->get_id() );
// NF-e rejeitada:
$records->saveRejected( $order, $xMotivo );
do_action( 'my_nfe_failed', $order->get_id(), $xMotivo );
// NF-e cancelada:
$records->saveCancelled( $order );
do_action( 'my_nfe_cancelled', $order->get_id(), $chave );
// Pedido que não vai ter NF-e (o envio que esperava a nota sai com a DC-e):
$records->saveSkipped( $order );
do_action( 'my_nfe_skipped', $order->get_id() );Ver o código completo do exemplo (meu-plugin-nfe.php)
<?php
/**
* Plugin Name: Meu Plugin de NF-e (integração com o Correios Automático)
* Description: Exemplo de como um plugin de NF-e entrega a nota à pré-postagem do Correios Automático (grátis 1.11.0+ e PRO 1.3.0+): NF-e no lugar da DC-e, espera da pré-postagem automática, DANFE simplificada, status no painel, logística reversa, XML do armazém Correios LOG+ e leitura do rastreio.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Meu Plugin
* Text Domain: meu-plugin-nfe
*
* O Correios Automático é opcional: sem ele, nada deste arquivo é registrado.
* O exemplo fala com ele só por ganchos públicos e metas do pedido.
*
* Onde ligar no SEU plugin (o resto é a integração pronta):
* 1. Ao autorizar a NF-e: grave a nota com My_NFe_Record::saveAuthorized() e dispare do_action( 'my_nfe_authorized', $order_id ).
* 2. Ao ser rejeitada: My_NFe_Record::saveRejected() e do_action( 'my_nfe_failed', $order_id, $mensagem ).
* 3. Ao cancelar: My_NFe_Record::saveCancelled() e do_action( 'my_nfe_cancelled', $order_id, $chave ).
* 4. Pedido que não vai ter NF-e: My_NFe_Record::saveSkipped() e do_action( 'my_nfe_skipped', $order_id ).
* Opcional: ao enviar a nota à SEFAZ, My_NFe_Record::saveProcessing() mostra "NF-e em emissão" no painel.
* Troque My_NFe_Record pela sua tabela/serviço de notas: a integração só usa os métodos públicos dela.
*/
defined( 'ABSPATH' ) || exit;
/**
* Regras da chave de acesso de 44 dígitos que o Correios Automático confere.
*
* UF(2) AAMM(4) CNPJ(14) modelo(2) série(3) número(9) tpEmis(1) código(8) DV(1).
*/
final class My_NFe_Access_Key {
/**
* Só os dígitos, quando são 44; vazio em qualquer outro caso.
*
* @param mixed $key Chave como foi lida ou guardada.
*/
public static function normalize( $key ): string {
if ( ! is_string( $key ) && ! is_int( $key ) ) {
return '';
}
$digits = (string) preg_replace( '/\D+/', '', (string) $key );
return 44 === strlen( $digits ) ? $digits : '';
}
/**
* Chave inteira de NF-e (modelo 55) com o dígito verificador certo.
*
* @param mixed $key Chave.
*/
public static function isValidInvoice( $key ): bool {
$digits = self::normalize( $key );
if ( '' === $digits || '55' !== substr( $digits, 20, 2 ) ) {
return false;
}
return self::checkDigit( substr( $digits, 0, 43 ) ) === (int) $digits[43];
}
/**
* Número da nota gravado na chave (posições 26 a 34), sem os zeros à esquerda.
*
* @param string $key Chave normalizada.
*/
public static function number( string $key ): string {
$number = ltrim( substr( $key, 25, 9 ), '0' );
return '' === $number ? '0' : $number;
}
/**
* Módulo 11 com pesos de 2 a 9 da direita para a esquerda; resto 0 ou 1 dá 0.
*
* @param string $body Os 43 primeiros dígitos.
*/
public static function checkDigit( string $body ): int {
$sum = 0;
$weight = 2;
for ( $index = strlen( $body ) - 1; $index >= 0; $index-- ) {
$sum += (int) $body[ $index ] * $weight;
$weight = 9 === $weight ? 2 : $weight + 1;
}
$rest = $sum % 11;
return $rest < 2 ? 0 : 11 - $rest;
}
}
/**
* Configurações do seu plugin que a integração consulta.
*/
final class My_NFe_Settings {
/**
* Se o plugin emite a NF-e sozinho quando o pedido é pago.
*/
public function issuesAutomatically(): bool {
return 'yes' === get_option( 'my_nfe_auto_issue', 'yes' );
}
/**
* Se o lojista quer que a pré-postagem automática do PRO espere a NF-e.
*/
public function waitsForCorreios(): bool {
return 'yes' === get_option( 'my_nfe_correios_wait', 'yes' );
}
/**
* Ambiente em que o plugin emite: `production` ou `homologation`.
*/
public function environment(): string {
return 'homologation' === get_option( 'my_nfe_environment', 'production' ) ? 'homologation' : 'production';
}
}
/**
* Onde o seu plugin guarda a NF-e do pedido. Aqui é um meta do próprio pedido;
* no seu plugin, troque pela sua tabela ou serviço.
*/
final class My_NFe_Record {
const META = '_my_nfe';
const STATUS_PROCESSING = 'processing';
const STATUS_AUTHORIZED = 'authorized';
const STATUS_REJECTED = 'rejected';
const STATUS_CANCELLED = 'cancelled';
const STATUS_SKIPPED = 'skipped';
/** Campos do pedido que sobrevivem à troca da nota. */
const HISTORY_FIELDS = [ 'previous_keys', 'prepost_keys', 'xped', 'tracking_codes' ];
/**
* A nota do pedido, vazia quando o plugin ainda não tem nenhuma.
*/
public function get( WC_Order $order ): array {
$record = $order->get_meta( self::META, true );
return is_array( $record ) ? $record : [];
}
public function status( WC_Order $order ): string {
$record = $this->get( $order );
return isset( $record['status'] ) ? (string) $record['status'] : '';
}
/**
* A NF-e autorizada do pedido, em qualquer ambiente, ou null.
*/
public function authorizedInvoice( WC_Order $order ): ?array {
$record = $this->get( $order );
if ( self::STATUS_AUTHORIZED !== ( $record['status'] ?? '' ) || ! My_NFe_Access_Key::isValidInvoice( $record['key'] ?? '' ) ) {
return null;
}
$record['key'] = My_NFe_Access_Key::normalize( $record['key'] );
return $record;
}
/**
* A NF-e que pode viajar com o objeto: autorizada e de produção.
*
* A chave não diz o ambiente, e o Correios Automático não confere isso:
* uma nota de homologação iria aos Correios como se valesse.
*/
public function usableInvoice( WC_Order $order ): ?array {
$record = $this->authorizedInvoice( $order );
return $record && 'production' === ( $record['environment'] ?? '' ) ? $record : null;
}
/**
* Chaves que o plugin já pôs no pedido e que foram canceladas.
*
* @return string[]
*/
public function previousKeys( WC_Order $order ): array {
$record = $this->get( $order );
return isset( $record['previous_keys'] ) && is_array( $record['previous_keys'] ) ? array_map( 'strval', $record['previous_keys'] ) : [];
}
/**
* Grava a NF-e autorizada. O seu fluxo de emissão chama isto e depois
* dispara `my_nfe_authorized`.
*
* @param array $nfe key, number, series, environment, operation (`out`/`in`), issued_at, protocol,
* authorized_at, emitter e recipient (name, document, ie, uf), total_cents e xml.
*/
public function saveAuthorized( WC_Order $order, array $nfe ): void {
$this->replace( $order, array_merge( $nfe, [
'status' => self::STATUS_AUTHORIZED,
'key' => My_NFe_Access_Key::normalize( $nfe['key'] ?? '' ),
'message' => '',
] ) );
}
public function saveRejected( WC_Order $order, string $message ): void {
$this->replace( $order, [ 'status' => self::STATUS_REJECTED, 'message' => $message ] );
}
/**
* A nota foi enviada à SEFAZ e aguarda a resposta. Opcional: o painel passa a
* mostrar "NF-e em emissão", e a pré-postagem automática continua esperando.
*/
public function saveProcessing( WC_Order $order ): void {
$this->replace( $order, [ 'status' => self::STATUS_PROCESSING, 'message' => '' ] );
}
/**
* O pedido não vai ter NF-e (por exemplo, a loja não emite para ele). O seu
* fluxo chama isto e depois dispara `my_nfe_skipped`: o envio que esperava a
* nota deixa de esperar e sai com a DC-e.
*/
public function saveSkipped( WC_Order $order ): void {
$this->replace( $order, [ 'status' => self::STATUS_SKIPPED, 'message' => '' ] );
}
public function saveCancelled( WC_Order $order ): void {
$record = $this->get( $order );
$previous = $this->previousKeys( $order );
if ( ! empty( $record['key'] ) ) {
$previous[] = (string) $record['key'];
}
$this->put( $order, [ 'status' => self::STATUS_CANCELLED, 'previous_keys' => array_values( array_unique( $previous ) ) ] );
}
/**
* Lembra com que NF-e (chave vazia: DC-e) cada objeto de ida foi pré-postado.
* Um pedido em volumes tem um objeto por volume.
*/
public function rememberPrepost( WC_Order $order, string $object_code, string $key ): void {
$keys = $this->prepostKeys( $order );
$keys[ $object_code ] = $key;
$this->put( $order, [ 'prepost_keys' => $keys ] );
}
/**
* Objetos de ida do pedido e a chave com que cada um foi pré-postado.
*
* @return array<string, string> código do objeto => chave (vazia: DC-e).
*/
public function prepostKeys( WC_Order $order ): array {
$record = $this->get( $order );
return isset( $record['prepost_keys'] ) && is_array( $record['prepost_keys'] ) ? array_map( 'strval', $record['prepost_keys'] ) : [];
}
/**
* Guarda o id do pedido no armazém Correios LOG+, que a NF-e leva em `<compra><xPed>`.
*/
public function rememberPurchaseOrder( WC_Order $order, string $correios_id ): void {
$this->put( $order, [ 'xped' => $correios_id ] );
}
public function addTrackingCode( WC_Order $order, string $code ): void {
$record = $this->get( $order );
$codes = isset( $record['tracking_codes'] ) && is_array( $record['tracking_codes'] ) ? $record['tracking_codes'] : [];
if ( in_array( $code, $codes, true ) ) {
return;
}
$codes[] = $code;
$this->put( $order, [ 'tracking_codes' => $codes ] );
}
/**
* Troca a nota inteira, mantendo só o histórico do pedido: os campos de uma
* NF-e anterior nunca se misturam com os da nova.
*/
private function replace( WC_Order $order, array $fields ): void {
$history = array_intersect_key( $this->get( $order ), array_flip( self::HISTORY_FIELDS ) );
$this->write( $order, array_merge( $history, $fields ) );
}
/**
* Mescla os campos na nota.
*/
private function put( WC_Order $order, array $fields ): void {
$this->write( $order, array_merge( $this->get( $order ), $fields ) );
}
/**
* Salva só os metas: estes métodos também rodam dentro de ações do Correios
* Automático, que salvam o pedido logo depois.
*/
private function write( WC_Order $order, array $record ): void {
$order->update_meta_data( self::META, $record );
$order->save_meta_data();
}
}
/**
* Toda a conversa com o Correios Automático (grátis e PRO).
*/
final class My_NFe_Correios_Bridge {
const SOURCE = 'meu-plugin-nfe';
const SOURCE_LABEL = 'Meu Plugin de NF-e';
/** Metas do pedido que o Correios Automático lê. */
const NUMBER_META = '_infixs_correios_automatico_invoice_number';
const KEY_META = '_infixs_correios_automatico_invoice_key';
/** Último objeto de logística reversa do pedido, gravado antes de a ação de criação disparar. */
const REVERSE_CODE_META = '_infixs_correios_automatico_reverse_prepost_code';
/** Versão do bloco da DANFE simplificada (OrderDanfeResolver::SCHEMA). */
const DANFE_SCHEMA = 1;
const DANFE_RESOLVER_CLASS = '\Infixs\CorreiosAutomatico\Services\OrderDanfeResolver';
const CONTAINER_CLASS = '\Infixs\CorreiosAutomatico\Container';
/** @var My_NFe_Record */
private $records;
/** @var My_NFe_Settings */
private $settings;
public function __construct( My_NFe_Record $records, My_NFe_Settings $settings ) {
$this->records = $records;
$this->settings = $settings;
}
public function register(): void {
// 1. Entregar a NF-e à pré-postagem (grátis 1.11.0+ e PRO).
add_filter( 'infixs_correios_automatico_order_invoice', [ $this, 'orderInvoice' ], 10, 3 );
// 2. Fazer a pré-postagem automática do PRO esperar a nota (PRO 1.3.0+; sem o PRO, ninguém pergunta).
add_filter( 'infixs_correios_automatico_auto_prepost_wait_invoice', [ $this, 'waitInvoice' ], 10, 3 );
// 3. Status da nota na lista de Pedidos do Correios Automático (grátis 1.12.0+).
add_filter( 'infixs_correios_automatico_order_invoice_status', [ $this, 'invoiceStatus' ], 10, 3 );
// 4. DANFE simplificada na etiqueta. Registrar este filtro liga as opções da DANFE no painel,
// então registre só quando o Correios Automático a imprime e o seu plugin tem os dados.
if ( method_exists( self::DANFE_RESOLVER_CLASS, 'resolve' ) ) {
add_filter( 'infixs_correios_automatico_order_danfe', [ $this, 'orderDanfe' ], 10, 3 );
}
// 5. Logística reversa (grátis 1.14.0+).
if ( version_compare( INFIXS_CORREIOS_AUTOMATICO_PLUGIN_VERSION, '1.14.0', '>=' ) ) {
add_filter( 'infixs_correios_automatico_reverse_prepost_fiscal_document', [ $this, 'reverseDocument' ], 10, 4 );
}
// 6. Armazém Correios LOG+ (PRO 1.6.0+; a mensagem de recusa é da 1.6.2+).
add_action( 'infixs_correios_automatico_pro_logplus_order_created', [ $this, 'onLogPlusOrderCreated' ], 10, 3 );
add_filter( 'infixs_correios_automatico_pro_logplus_invoice_xml', [ $this, 'logPlusInvoiceXml' ], 10, 3 );
add_filter( 'infixs_correios_automatico_pro_logplus_invoice_xml_error', [ $this, 'logPlusRefusal' ], 10, 5 );
// 7. Ler o resultado: pré-postagem criada e código de rastreio.
add_action( 'infixs_correios_automatico_prepost_created', [ $this, 'onPrepostCreated' ], 10, 2 );
add_action( 'infixs_correios_automatico_after_add_tracking_code', [ $this, 'onTrackingCodeAdded' ], 10, 2 );
// 8. Eventos do SEU plugin, que viram os avisos do contrato.
add_action( 'my_nfe_authorized', [ $this, 'onAuthorized' ] );
add_action( 'my_nfe_failed', [ $this, 'onFailed' ], 10, 2 );
add_action( 'my_nfe_skipped', [ $this, 'onSkipped' ] );
add_action( 'my_nfe_cancelled', [ $this, 'onCancelled' ], 10, 2 );
}
/**
* NF-e do pedido para a pré-postagem, a etiqueta e o painel.
*
* Roda para cada pedido da lista do painel e para cada etiqueta: responda
* do que já está salvo, sem chamar API.
*
* @param array|null $invoice Resposta dos emissores anteriores.
* @param mixed $order Pedido.
* @param mixed $context for (`prepost`/`display`), shipping_item_id, vendor_id.
*
* @return array|null
*/
public function orderInvoice( $invoice, $order, $context = [] ) {
if ( null !== $invoice || ! $order instanceof WC_Order ) {
return $invoice; // Outro emissor já respondeu.
}
if ( is_array( $context ) && ! empty( $context['vendor_id'] ) ) {
return $invoice; // No marketplace, o vendedor emite a nota dele.
}
$nfe = $this->records->usableInvoice( $order );
if ( ! $nfe ) {
return $invoice;
}
$informed = My_NFe_Access_Key::normalize( $order->get_meta( self::KEY_META, true ) );
if ( '' !== $informed && $informed !== $nfe['key'] && ! in_array( $informed, $this->records->previousKeys( $order ), true ) ) {
return $invoice; // O lojista informou outra NF-e no pedido: os metas respondem por ela.
}
return [
'invoice_key' => $nfe['key'],
'invoice_number' => (string) ( $nfe['number'] ?? '' ),
'source' => self::SOURCE,
'source_label' => self::SOURCE_LABEL,
];
}
/**
* Se a pré-postagem automática do PRO espera a NF-e deste envio.
*
* Só é perguntado quando o pedido ainda não tem nota. Roda quando o pedido
* entra em "Processando", antes de `woocommerce_payment_complete`: responda
* pelo que o plugin sabe do pedido e da configuração, não por um agendamento
* feito no pagamento. Também roda a cada hora enquanto o envio espera.
*
* @param mixed $wait Resposta dos emissores anteriores.
* @param mixed $order Pedido.
* @param int $shipping_item_id Linha de frete do envio, zero para o pedido inteiro.
*/
public function waitInvoice( $wait, $order, $shipping_item_id = 0 ): bool {
if ( $wait || ! $order instanceof WC_Order ) {
return (bool) $wait; // Outro emissor já pediu a espera.
}
if ( ! $this->settings->issuesAutomatically() || ! $this->settings->waitsForCorreios() || 'production' !== $this->settings->environment() ) {
return false; // Nota de homologação nunca vai aos Correios: esperar por ela travaria o envio.
}
$status = $this->records->status( $order );
// Rejeitada continua esperando: o lojista corrige e emite de novo. Responder false
// aqui faria a varredura de hora em hora do PRO postar o envio com DC-e.
return ! in_array( $status, [ My_NFe_Record::STATUS_SKIPPED, My_NFe_Record::STATUS_CANCELLED, My_NFe_Record::STATUS_AUTHORIZED ], true );
}
/**
* Estado da nota na lista de Pedidos do Correios Automático.
*
* @param mixed $status Estado padrão (ou dos emissores anteriores).
* @param mixed $order Pedido.
* @param mixed $context vendor_id, invoice (a NF-e que o Correios Automático resolveu) e waiting.
*
* @return mixed
*/
public function invoiceStatus( $status, $order, $context = [] ) {
if ( ! is_array( $status ) || ! $order instanceof WC_Order ) {
return $status;
}
$context = is_array( $context ) ? $context : [];
if ( ! empty( $context['vendor_id'] ) ) {
return $status;
}
$record = $this->records->get( $order );
if ( empty( $record['status'] ) ) {
return $status;
}
$resolved = My_NFe_Access_Key::normalize( $context['invoice']['invoice_key'] ?? '' );
if ( '' !== $resolved && $resolved !== My_NFe_Access_Key::normalize( $record['key'] ?? '' ) ) {
return $status; // O pedido viaja com a NF-e de outro emissor ou informada à mão.
}
list( $state, $tone, $label, $detail ) = $this->panelState( $record );
return [
'state' => $state,
'tone' => $tone,
'label' => $label,
'detail' => $detail,
'source' => self::SOURCE,
'source_label' => self::SOURCE_LABEL,
'danfe' => isset( $status['danfe'] ) ? $status['danfe'] : 'none', // A prontidão da DANFE só vem aqui.
'actions' => $this->panelActions( $order, $record ),
];
}
/**
* Dados crus do XML autorizado para a DANFE simplificada.
*
* @param array|null $danfe Resposta dos emissores anteriores.
* @param mixed $order Pedido.
* @param mixed $context for (`label`/`display`), shipping_item_id, vendor_id e invoice.
*
* @return array|null
*/
public function orderDanfe( $danfe, $order, $context = [] ) {
if ( null !== $danfe || ! $order instanceof WC_Order || ! is_array( $context ) || ! empty( $context['vendor_id'] ) ) {
return $danfe;
}
$nfe = $this->records->authorizedInvoice( $order );
$resolved = My_NFe_Access_Key::normalize( $context['invoice']['invoice_key'] ?? '' );
if ( ! $nfe || $resolved !== $nfe['key'] ) {
return $danfe; // Só responda pela NF-e que o Correios Automático resolveu para o pedido.
}
if ( ! $this->hasDanfeData( $nfe ) ) {
return [ 'schema' => self::DANFE_SCHEMA, 'pending' => true ];
}
return [
'schema' => self::DANFE_SCHEMA,
'source' => self::SOURCE,
'source_label' => self::SOURCE_LABEL,
'environment' => $nfe['environment'],
'key' => $nfe['key'],
'number' => (string) $nfe['number'],
'series' => (string) $nfe['series'],
'operation' => 'in' === ( $nfe['operation'] ?? 'out' ) ? 'in' : 'out',
'issued_at' => (string) $nfe['issued_at'],
'protocol' => [
'number' => (string) $nfe['protocol'],
'authorized_at' => (string) $nfe['authorized_at'],
],
'emitter' => $this->party( $nfe['emitter'] ),
'recipient' => $this->party( $nfe['recipient'] ),
'total' => isset( $nfe['total_cents'] ) ? (int) $nfe['total_cents'] : null,
];
}
/**
* Documento com que a devolução viaja.
*
* O padrão do Correios Automático já é o documento da ida. Aqui só se troca
* uma NF-e da ida que o seu plugin cancelou pela nota que a substituiu.
*
* @param mixed $document emiteDCe, invoice_number, invoice_key e source.
* @param mixed $order Pedido devolvido.
* @param int $shipping_item_id Linha de frete devolvida, zero para o pedido inteiro.
* @param mixed $outbound Pré-postagem da ida, ou null.
*
* @return mixed
*/
public function reverseDocument( $document, $order, $shipping_item_id = 0, $outbound = null ) {
if ( ! is_array( $document ) || ! $order instanceof WC_Order || empty( $document['invoice_key'] ) ) {
return $document;
}
$sent = My_NFe_Access_Key::normalize( $document['invoice_key'] );
$current = $this->records->usableInvoice( $order );
if ( ! $current || $sent === $current['key'] || ! in_array( $sent, $this->records->previousKeys( $order ), true ) ) {
return $document;
}
return [
'invoice_key' => $current['key'],
'invoice_number' => (string) ( $current['number'] ?? '' ),
'emiteDCe' => 'N',
'source' => isset( $document['source'] ) ? $document['source'] : 'outbound_invoice', // Mantém a nova tentativa sem documento.
];
}
/**
* O pedido entrou no armazém Correios LOG+: a NF-e dele precisa levar este id em `<compra><xPed>`.
*
* @param int $order_id Pedido da loja.
* @param string $correios_id Id do pedido nos Correios.
* @param mixed $order Pedido da loja.
*/
public function onLogPlusOrderCreated( $order_id, $correios_id, $order = null ): void {
$order = $order instanceof WC_Order ? $order : wc_get_order( $order_id );
if ( ! $order instanceof WC_Order || ! is_scalar( $correios_id ) || '' === (string) $correios_id ) {
return;
}
$this->records->rememberPurchaseOrder( $order, (string) $correios_id );
// No seu plugin: emita a NF-e deste pedido a partir de agora, com <compra><xPed> igual a $correios_id.
}
/**
* XML autorizado da NF-e para o armazém LOG+. Só é perguntado enquanto o lojista não enviou um XML à mão.
*
* @param mixed $xml XML de outro emissor, vazio quando não há.
* @param mixed $order Pedido.
* @param string $correios_id Id do pedido nos Correios.
*
* @return mixed
*/
public function logPlusInvoiceXml( $xml, $order, $correios_id = '' ) {
if ( ( is_string( $xml ) && '' !== trim( $xml ) ) || ! $order instanceof WC_Order ) {
return $xml;
}
$nfe = $this->records->usableInvoice( $order );
return $nfe && ! empty( $nfe['xml'] ) ? (string) $nfe['xml'] : $xml;
}
/**
* Explica ao lojista onde corrigir quando o armazém recusa a sua NF-e por faltar o xPed.
*
* @param mixed $message Mensagem do Correios Automático.
* @param mixed $error WP_Error da conferência; `reason` = `xped` quando falta o id.
* @param mixed $xml XML recusado.
* @param mixed $order Pedido.
* @param string $correios_id Id do pedido nos Correios.
*
* @return mixed
*/
public function logPlusRefusal( $message, $error, $xml, $order, $correios_id = '' ) {
if ( ! $error instanceof WP_Error || ! $order instanceof WC_Order ) {
return $message;
}
$data = $error->get_error_data();
$nfe = $this->records->usableInvoice( $order );
if ( ! is_array( $data ) || 'xped' !== ( $data['reason'] ?? '' ) || ! $nfe || ! is_string( $xml ) || false === strpos( $xml, $nfe['key'] ) ) {
return $message; // Só a recusa do XML da NF-e deste plugin (o XML traz a chave em chNFe).
}
return sprintf(
/* translators: %s: id do pedido nos Correios */
__( 'A NF-e do Meu Plugin de NF-e saiu sem o pedido do armazém (xPed = %s). Cancele e emita de novo pelo Meu Plugin de NF-e com esse número.', 'meu-plugin-nfe' ),
(string) $correios_id
);
}
/**
* Lembra com que nota cada objeto de ida foi pré-postado e avisa quando foi com uma NF-e já cancelada.
*
* @param int $order_id Pedido.
* @param mixed $prepost Pré-postagem salva (Infixs\CorreiosAutomatico\Models\Prepost).
*/
public function onPrepostCreated( $order_id, $prepost = null ): void {
$order = wc_get_order( $order_id );
if ( ! $order instanceof WC_Order || ! is_object( $prepost ) ) {
return;
}
$object_code = (string) self::prop( $prepost, 'object_code' );
if ( '' === $object_code || $object_code === (string) $order->get_meta( self::REVERSE_CODE_META, true ) ) {
return; // A devolução volta para a loja: não diz nada da nota da ida.
}
$key = $this->prepostKey( $order, $prepost );
$this->records->rememberPrepost( $order, $object_code, $key );
if ( '' !== $key && in_array( $key, $this->records->previousKeys( $order ), true ) ) {
$order->add_order_note( sprintf(
/* translators: %s: código do objeto */
__( 'Meu Plugin de NF-e: a pré-postagem %s saiu com uma NF-e já cancelada. Cancele a pré-postagem e refaça com a nota atual.', 'meu-plugin-nfe' ),
$object_code
) );
}
}
/**
* Guarda o rastreio dos objetos que vão ao cliente (para o seu plugin levar à nota ou ao ERP).
*
* Atenção à ordem dos argumentos: código primeiro, pedido depois.
*
* @param mixed $code Código de rastreio.
* @param mixed $order_id Pedido.
*/
public function onTrackingCodeAdded( $code, $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! is_string( $code ) || '' === $code || ! $order instanceof WC_Order || ! $this->isOutboundCode( $order, $code ) ) {
return;
}
$this->records->addTrackingCode( $order, $code );
}
/**
* A NF-e foi autorizada: põe a nota nos metas do pedido e avisa quem espera.
*
* @param int $order_id Pedido.
*/
public function onAuthorized( $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order instanceof WC_Order ) {
return;
}
$nfe = $this->records->usableInvoice( $order );
if ( $nfe ) {
$this->publishMetas( $order, $nfe );
}
// Avise depois de salvar: o PRO relê o pedido, pergunta o filtro de NF-e e posta.
// Sem nota utilizável (homologação), o filtro de espera diz não e o PRO posta com DC-e.
do_action( 'infixs_correios_automatico_order_invoice_ready', (int) $order_id );
}
/**
* A NF-e foi rejeitada: o PRO mostra a mensagem como erro de pré-postagem e o envio segue esperando.
*
* @param int $order_id Pedido.
* @param mixed $message Motivo, para o lojista.
*/
public function onFailed( $order_id, $message = '' ): void {
do_action( 'infixs_correios_automatico_order_invoice_failed', (int) $order_id, is_scalar( $message ) ? (string) $message : '' );
}
/**
* O pedido não vai ter NF-e: waitInvoice() já responde false, e o aviso faz o PRO
* retomar agora o envio que esperava, que sai com a DC-e. Sem o aviso, a retomada
* vem na varredura de hora em hora do PRO.
*
* @param int $order_id Pedido.
*/
public function onSkipped( $order_id ): void {
do_action( 'infixs_correios_automatico_order_invoice_ready', (int) $order_id );
}
/**
* A NF-e foi cancelada: tira a nota dos metas, se ainda for ela, para não viajar na próxima pré-postagem,
* e avisa quais objetos de ida já saíram com ela.
*
* @param int $order_id Pedido.
* @param mixed $key Chave cancelada.
*/
public function onCancelled( $order_id, $key = '' ): void {
$order = wc_get_order( $order_id );
$key = My_NFe_Access_Key::normalize( $key );
if ( ! $order instanceof WC_Order || '' === $key ) {
return;
}
if ( My_NFe_Access_Key::normalize( $order->get_meta( self::KEY_META, true ) ) === $key ) {
$order->delete_meta_data( self::NUMBER_META );
$order->delete_meta_data( self::KEY_META );
$order->save();
}
$objects = array_keys( $this->records->prepostKeys( $order ), $key, true );
if ( $objects ) {
$order->add_order_note( sprintf(
/* translators: %s: códigos dos objetos */
_n(
'Meu Plugin de NF-e: a NF-e cancelada viaja na pré-postagem %s. Cancele a pré-postagem e refaça com a nova nota.',
'Meu Plugin de NF-e: a NF-e cancelada viaja nas pré-postagens %s. Cancele as pré-postagens e refaça com a nova nota.',
count( $objects ),
'meu-plugin-nfe'
),
implode( ', ', $objects )
) );
}
}
/**
* Escreve a nota nos dois metas sem tomar o lugar de uma NF-e que o lojista
* (ou outro emissor) informou: só um meta vazio, a mesma chave ou uma chave
* antiga deste plugin cedem lugar.
*/
private function publishMetas( WC_Order $order, array $nfe ): void {
$current = My_NFe_Access_Key::normalize( $order->get_meta( self::KEY_META, true ) );
if ( '' !== $current && $current !== $nfe['key'] && ! in_array( $current, $this->records->previousKeys( $order ), true ) ) {
return;
}
$order->update_meta_data( self::NUMBER_META, (string) ( $nfe['number'] ?? '' ) );
$order->update_meta_data( self::KEY_META, $nfe['key'] ); // Sempre string: 44 dígitos não cabem num inteiro.
$order->save();
}
/**
* Chave da NF-e com que a pré-postagem saiu; vazia quando saiu com DC-e.
*/
private function prepostKey( WC_Order $order, $prepost ): string {
if ( ! empty( self::prop( $prepost, 'dce' ) ) ) {
return '';
}
$answered = My_NFe_Access_Key::normalize( self::prop( $prepost, 'invoice_key' ) );
if ( '' !== $answered ) {
return '55' === substr( $answered, 20, 2 ) ? $answered : '';
}
// O Correios Automático grava nos metas a nota usada antes de disparar a ação.
return My_NFe_Access_Key::normalize( $order->get_meta( self::KEY_META, true ) );
}
/**
* Se o código é de um objeto que vai ao cliente (não de uma devolução).
*/
private function isOutboundCode( WC_Order $order, string $code ): bool {
if ( $code === (string) $order->get_meta( self::REVERSE_CODE_META, true ) ) {
return false;
}
if ( ! method_exists( self::CONTAINER_CLASS, 'trackingService' ) ) {
return true;
}
$service = call_user_func( [ self::CONTAINER_CLASS, 'trackingService' ] );
if ( ! is_object( $service ) || ! method_exists( $service, 'getShipmentCodes' ) ) {
return true; // Antes da 1.11.0 não há como separar ida e volta.
}
return in_array( $code, array_map( 'strval', wp_list_pluck( (array) $service->getShipmentCodes( $order->get_id() ), 'code' ) ), true );
}
/**
* @return array{0: string, 1: string, 2: string, 3: string} state, tone, label, detail.
*/
private function panelState( array $record ): array {
$number = (string) ( $record['number'] ?? '' );
$message = (string) ( $record['message'] ?? '' );
/* translators: %s: número da NF-e */
$numbered = sprintf( __( 'NF-e nº %s', 'meu-plugin-nfe' ), $number );
switch ( $record['status'] ) {
case My_NFe_Record::STATUS_AUTHORIZED:
return 'production' === ( $record['environment'] ?? '' )
? [ 'authorized', 'success', $numbered, '' ]
: [ 'authorized', 'muted', $numbered, __( 'Homologação, sem valor fiscal', 'meu-plugin-nfe' ) ];
case My_NFe_Record::STATUS_REJECTED:
return [ 'error', 'danger', __( 'NF-e rejeitada', 'meu-plugin-nfe' ), $message ];
case My_NFe_Record::STATUS_CANCELLED:
return [ 'cancelled', 'muted', $numbered, __( 'Cancelada', 'meu-plugin-nfe' ) ];
case My_NFe_Record::STATUS_SKIPPED:
return [ 'none', 'muted', __( 'NF-e não será emitida', 'meu-plugin-nfe' ), '' ];
}
return [ 'pending', 'warning', __( 'NF-e em emissão', 'meu-plugin-nfe' ), '' ];
}
/**
* Links do painel: só endereços deste site (o Correios Automático descarta os outros), no máximo quatro.
*/
private function panelActions( WC_Order $order, array $record ): array {
if ( My_NFe_Record::STATUS_AUTHORIZED !== $record['status'] || ! current_user_can( 'edit_shop_orders' ) ) {
return [];
}
$url = add_query_arg( [ 'page' => 'meu-plugin-nfe', 'order' => $order->get_id() ], admin_url( 'admin.php' ) );
return [
[ 'id' => 'danfe_pdf', 'label' => __( 'DANFE completa (PDF)', 'meu-plugin-nfe' ), 'url' => add_query_arg( 'file', 'danfe', $url ), 'target' => '_blank' ],
[ 'id' => 'xml', 'label' => __( 'XML', 'meu-plugin-nfe' ), 'url' => add_query_arg( 'file', 'xml', $url ) ],
];
}
private function hasDanfeData( array $nfe ): bool {
return preg_match( '/^\d{15}$/', (string) ( $nfe['protocol'] ?? '' ) )
&& ! empty( $nfe['issued_at'] )
&& ! empty( $nfe['authorized_at'] )
&& ! empty( $nfe['number'] )
&& isset( $nfe['series'] ) && '' !== (string) $nfe['series']
&& ! empty( $nfe['emitter']['name'] ) && ! empty( $nfe['emitter']['uf'] )
&& ! empty( $nfe['recipient']['name'] ) && ! empty( $nfe['recipient']['uf'] );
}
/**
* @param mixed $party name, document, ie e uf, como saem do XML.
*/
private function party( $party ): array {
$party = is_array( $party ) ? $party : [];
return [
'name' => (string) ( $party['name'] ?? '' ),
'document' => (string) ( $party['document'] ?? '' ),
'ie' => (string) ( $party['ie'] ?? '' ),
'uf' => (string) ( $party['uf'] ?? '' ),
];
}
/**
* Lê um campo do modelo da pré-postagem sem aviso de propriedade inexistente.
*
* @param object $model Modelo.
*
* @return mixed
*/
private static function prop( $model, string $name ) {
return isset( $model->{$name} ) ? $model->{$name} : null;
}
}
/**
* Liga a integração depois que o Correios Automático (grátis no include, PRO
* no `plugins_loaded` 11) carregou.
*/
function my_nfe_boot_correios_integration(): void {
if ( ! defined( 'INFIXS_CORREIOS_AUTOMATICO_PLUGIN_VERSION' ) || ! function_exists( 'wc_get_order' ) ) {
return; // Correios Automático ou WooCommerce inativo.
}
if ( version_compare( INFIXS_CORREIOS_AUTOMATICO_PLUGIN_VERSION, '1.11.0', '<' ) ) {
return; // O filtro da NF-e do pedido chegou na 1.11.0.
}
( new My_NFe_Correios_Bridge( new My_NFe_Record(), new My_NFe_Settings() ) )->register();
}
add_action( 'plugins_loaded', 'my_nfe_boot_correios_integration', 20 );Lista de verificação de testes
Prepare o grátis 1.14.0 com o Contrato Próprio ativo e um método do Correios Automático na zona de entrega; para os itens do PRO, o PRO 1.6.2 com licença ativa e a pré-postagem automática ligada no método. Ligue o log em Correios Automático → Configurações → Depuração (nível Debug Log) e acompanhe em WooCommerce → Status → Logs.
Plugin grátis
- Sem o Correios Automático ativo, o seu plugin carrega normalmente e não registra os ganchos.
- Pedido com nota autorizada: a caixa “Pré-Postagem” do pedido mostra a NF-e com o nome do seu plugin e os campos vêm preenchidos.
- Pré-postagem em massa com um pedido com nota e outro sem: o log mostra
chaveNFeenumeroNotaFiscalno primeiro eemiteDCe: "S"no segundo. - Depois da pré-postagem, o pedido traz os metas
_infixs_correios_automatico_invoice_numbere_invoice_keycom a nota usada. - Nota de homologação: o seu filtro não responde e a pré-postagem sai com DC-e.
- Chave com 43 dígitos ou modelo 99: o Correios Automático ignora a resposta. Chave com dígito verificador errado: o seu plugin não a entrega.
- Nota digitada pelo lojista no pedido: a próxima resolução usa a nota digitada (
source=order_meta). - Botão “c/ Declaração Eletrônica (DCe)”: sai com DC-e mesmo com a nota pronta.
- Lista de pedidos com 50 pedidos: o tempo de resposta continua o mesmo com o seu plugin ativo.
- Status no painel: “em emissão”, “rejeitada” com o motivo e “NF-e nº …”; os links abrem no admin do mesmo site.
- DANFE: ligada no perfil de impressão, sai com a etiqueta; com protocolo faltando, aparece “em preparação”; com o CNPJ do emitente diferente do da chave, o log traz
reason = emitter. - Cancelar a nota: os dois metas somem (se eram dela) e a etiqueta deixa de sair com a DANFE.
- Reversa de um pedido com a NF-e da ida reemitida: o log “Documento fiscal da pré-postagem reversa definido.” mostra
nfe: truee o envio leva a chave nova comemiteDCe: "N". - Rastreio: o ouvinte recebe
($code, $order_id)nessa ordem; o código da devolução não entra como rastreio de ida; num pedido em volumes, cada volume grava o seu código. - Marketplace: numa remessa de vendedor,
vendor_idé diferente de zero e o seu plugin não responde.
PRO
- Pagamento sem nota: o pedido ganha a nota “Pré-postagem automática aguardando a nota fiscal (NF-e).”, o meta de espera aparece e o pedido entra em Pendências → NF-e.
- Nota autorizada + aviso de nota pronta: a pré-postagem sai com NF-e, a espera some e o código de rastreio é gravado.
- Nota autorizada de forma síncrona antes da prioridade 100 do
woocommerce_order_status_processing: pré-postagem direta, sem espera. - Rejeição +
order_invoice_failed: o painel mostra a mensagem e o envio continua aguardando; reemitir e avisar cria a pré-postagem e limpa o erro. - Espera desligada no seu plugin: na varredura de hora em hora (ou no próximo aviso), o envio sai com DC-e.
- Pedido marcado sem nota (
saveSkipped()+my_nfe_skippedno exemplo): o envio que aguardava sai com DC-e, sem esperar a varredura. - Pedido em Aguardando (
on-hold) com aviso de nota pronta: não posta; ao voltar para Processando, posta. - Pedido cancelado: a espera some.
- Pedido em 2 volumes: cada linha espera e posta separada, com a mesma NF-e.
- Dois avisos seguidos (webhook + cron): uma pré-postagem por envio.
- Correios LOG+: o pedido criado no armazém dispara
infixs_correios_automatico_pro_logplus_order_created; a nota emitida comxPed, seguida do aviso, gera “XML da NF-e entregue ao armazém Correios LOG+.”; uma nota semxPedmostra a mensagem do seu filtro de erro. - REST do WooCommerce:
PUT /wp-json/wc/v3/orders/{id}com os dois metas emmeta_datapré-posta com NF-e.
Perguntas frequentes
Preciso do PRO para a pré-postagem sair com a NF-e?
Não. No grátis, as pré-postagens feitas pelo lojista (botões do pedido, formulário do painel e pré-postagem em massa) já usam a NF-e que o seu plugin entrega. O PRO acrescenta a pré-postagem automática no pagamento, que espera a nota, e o armazém Correios LOG+.
Uso o filtro ou os metadados?
Os dois se completam. Os metadados guardam a nota no pedido, e o lojista vê e pode corrigir. O filtro responde a partir dos dados do seu plugin e deixa você decidir na hora (por exemplo, só notas de produção). Com os metadados a integração já funciona; com o filtro ela fica mais precisa.
E se a nota demorar para sair?
A pré-postagem automática do PRO espera enquanto o seu filtro de espera responder true, sem prazo. Para liberar o envio com DC-e, passe a responder false e dispare o aviso de nota pronta. O lojista também pode pré-postar à mão a qualquer momento.
Posso entregar uma nota de homologação?
Entregue à pré-postagem só nota autorizada em produção. A chave de acesso não informa o ambiente, e o Correios Automático envia aos Correios a chave que receber. Em homologação, devolva null no filtro da nota e false no filtro de espera.
Como funciona com pedido dividido em volumes?
Cada volume é uma linha de frete e é pré-postado separadamente; os filtros recebem o shipping_item_id de cada um. A mesma NF-e vale para todos os volumes do pedido.
E no marketplace (Dokan, WCFM)?
Nas remessas de vendedor, o contexto traz vendor_id diferente de zero. Devolva o valor recebido e deixe o vendedor emitir a nota dele.
O Correios Automático confere a chave?
Na pré-postagem ele confere os 44 dígitos e o modelo 55. Na DANFE simplificada confere também o dígito verificador, o número, a série e o emitente. Valide a chave no seu plugin antes de entregar, por exemplo com AccessKey::hasValidCheckDigit().
E uma nota de emitente com CNPJ alfanumérico?
Na 1.14.0, o filtro e os metadados leem chaves só com dígitos. Para essa nota, informe número e chave na pré-postagem manual (botão “c/ Nota Fiscal” do pedido ou POST /preposts), que aceita letras na chave e a envia como informada.
O lojista digitou outra nota no pedido. Qual vale?
Uma resposta válida do filtro vem primeiro. Para respeitar a nota digitada, faça o filtro responder só quando os metadados estão vazios ou guardam a sua chave, como faz o exemplo completo.
Como vejo o que foi enviado aos Correios?
Em Correios Automático → Configurações → Depuração, ligue “Ativar Logs” e o nível Debug Log. Em WooCommerce → Status → Logs, a mensagem “Enviando prepostagem para os correios.” traz o envio com chaveNFe e numeroNotaFiscal (ou emiteDCe, quando vai com DC-e).