Programação Por Equipe DevBras

Capa do artigo: SAT fiscal no PDV: integrar o aparelho do caixa sem duplicar cupom

SAT fiscal no PDV: integrar o aparelho do caixa sem duplicar cupom

Como o PDV conversa com o SAT pela DLL do fabricante, por que o número de sessão é a sua rede de segurança e o que a tela do caixa precisa mostrar quando o aparelho falha.

Sábado, 11h, fila no caixa 02 de uma mercearia fictícia em Campinas. O operador finaliza a venda, a tela congela por dez segundos e volta com "erro de comunicação". Ele aperta "finalizar" de novo, o cupom sai, e no fim do mês aparecem dois CF-e para a mesma compra. A resposta curta: antes de chamar o SAT, grave no banco o número de sessão daquela chamada; se a resposta não vier, use esse número para perguntar ao aparelho o que aconteceu antes de tentar de novo. A especificação do SAT tem uma função exatamente para isso.

O SAT (Sistema Autenticador e Transmissor de Cupons Fiscais Eletrônicos) é o equipamento que o estado de São Paulo adotou para o varejo emitir o CF-e-SAT, de modelo 59. Ele fica ao lado do caixa, ligado ao computador, e é diferente da NF-e de um e-commerce em quase tudo que importa para o código.

Por que o SAT não é "a NF-e com outro nome"

Na NF-e, o seu sistema assina o XML e fala com o web service da Sefaz pela internet. No SAT, o PDV entrega os dados da venda a um aparelho local. O próprio SAT valida, assina com o certificado que tem dentro dele, devolve o cupom autorizado e se encarrega de transmitir os cupons à Sefaz depois. Por isso a loja consegue vender mesmo com a internet instável: quem precisa estar presente e funcionando é o aparelho, não o link.

O PDV não fala com o SAT por HTTP. Cada fabricante entrega uma biblioteca (no Windows, uma DLL) que expõe as funções definidas na especificação técnica do SAT, como ConsultarSAT, EnviarDadosVenda, CancelarUltimaVenda e ConsultarNumeroSessao. Os nomes das funções são padronizados. O nome do arquivo da DLL, a convenção de chamada e alguns detalhes de codificação de texto variam por fabricante e estão no manual do aparelho. O XML de venda também é outro leiaute, como detalha o post sobre o XML do SAT.

Toda função recebe um número de sessão: um inteiro de seis dígitos gerado pelo PDV, que não deve se repetir entre chamadas recentes. A resposta volta como texto, com campos separados por barra vertical (|), começando pelo número de sessão e pelo código de retorno. Em uma venda bem-sucedida, o retorno inclui o XML do CF-e em Base64 e a chave de consulta.

O fluxo da venda no caixa, passo a passo

  1. Ao abrir o caixa, consulte o aparelho. Chame ConsultarSAT e mostre na tela, de forma visível, se o SAT respondeu. Descobrir que ele está desligado com a venda já registrada é pior.
  2. Monte o XML da venda com os itens, pagamentos e a assinatura do aplicativo comercial, vinculada ao CNPJ da software house e do contribuinte.
  3. Gere o número de sessão e grave a tentativa no banco com status enviando, antes de chamar a DLL.
  4. Chame EnviarDadosVenda com o número de sessão, o código de ativação do aparelho e o XML.
  5. Se a resposta vier, separe os campos, grave código, mensagem, chave e o XML decodificado, e só então imprima o extrato.
  6. Se a resposta não vier (exceção, travamento, aparelho desconectado), marque a tentativa como incerta. Na próxima ação, chame ConsultarNumeroSessao com o número gravado. Ela devolve o resultado daquela chamada, se o aparelho chegou a processá-la.
  7. Só reenvie a venda, com sessão nova, se a consulta mostrar que o SAT não emitiu.

Chamando a DLL em C#

Este exemplo usa .NET e P/Invoke. O texto vai como bytes UTF-8 terminados em zero e o retorno é lido como UTF-8. Confira no manual do seu fabricante o nome da DLL, a convenção de chamada (Cdecl ou StdCall) e a codificação esperada; errar a convenção costuma derrubar o processo sem mensagem clara, tema do post sobre chamar DLL sem adivinhar.

using System;
using System.Runtime.InteropServices;
using System.Text;

static class Sat
{
    // Nome do arquivo e convenção de chamada mudam por fabricante: confira no manual.
    const string Dll = "SAT.dll";

    [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
    static extern IntPtr ConsultarSAT(int numeroSessao);

    [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
    static extern IntPtr EnviarDadosVenda(int numeroSessao, byte[] codigoDeAtivacao, byte[] dadosVenda);

    [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
    static extern IntPtr ConsultarNumeroSessao(int numeroSessao, byte[] codigoDeAtivacao, int cNumeroDeSessao);

    public static string[] Consultar(int sessao) =>
        Separar(ConsultarSAT(sessao));

    public static string[] Enviar(int sessao, string codigoAtivacao, string xmlVenda) =>
        Separar(EnviarDadosVenda(sessao, Texto(codigoAtivacao), Texto(xmlVenda)));

    public static string[] Recuperar(int novaSessao, string codigoAtivacao, int sessaoPerdida) =>
        Separar(ConsultarNumeroSessao(novaSessao, Texto(codigoAtivacao), sessaoPerdida));

    static byte[] Texto(string valor) => Encoding.UTF8.GetBytes(valor + "\0");

    static string[] Separar(IntPtr retorno) =>
        (Marshal.PtrToStringUTF8(retorno) ?? string.Empty).Split('|');
}

O "\0" acrescenta o terminador nulo que a função em C espera. Repare que a classe não decide nada: ela só traduz chamadas. A decisão de reenviar, consultar ou bloquear fica no serviço de venda, que tem acesso ao banco.

A tabela que impede o cupom duplicado

CREATE TABLE sat_tentativa (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  venda_id BIGINT NOT NULL,
  numero_sessao INT NOT NULL,
  status VARCHAR(12) NOT NULL,
  codigo_retorno VARCHAR(10) NULL,
  mensagem VARCHAR(255) NULL,
  chave_cfe VARCHAR(60) NULL,
  xml_cfe MEDIUMTEXT NULL,
  criada_em DATETIME NOT NULL,
  KEY ix_venda (venda_id)
);

A regra no serviço é curta: se a venda tem uma tentativa incerta, nenhum botão de "finalizar" chama EnviarDadosVenda antes de resolver essa tentativa com ConsultarNumeroSessao. O operador vê "verificando o último cupom no SAT" em vez de um botão que convida a apertar de novo.

O que a tela do caixa precisa mostrar

  • Estado do SAT antes de abrir a venda: respondendo ou não.
  • Motivo da recusa em português, com a ação esperada ("aparelho sem comunicação: confira o cabo USB e a energia"), e o código cru guardado no log.
  • Cupom e arquivo de retorno gravados juntos, para reimpressão sem nova emissão.
  • Uma decisão de contingência combinada antes, para o sábado em que o aparelho queima. Contingência é regra de negócio definida com o contador, não um modo escondido que alguém liga no desespero.

Cancelamento também tem regra própria: CancelarUltimaVenda só cancela a última venda emitida pelo aparelho e dentro de um prazo curto. Se o cliente volta uma hora depois, o caminho é outro procedimento, que o contador define. Integração com TEF tem o mesmo risco de duplicidade na outra ponta, tratado em TEF no caixa sem duplicar.

Legislação muda: confirme antes de escrever código

O SAT é uma solução paulista. Outros estados adotaram modelos diferentes, como a NFC-e, e o Ceará usa um equipamento próprio, o MFE. Além disso, a Sefaz de São Paulo vem conduzindo a migração do varejo do SAT para a NFC-e, com cronograma definido em norma. Antes de investir na integração, confirme com o contador e no portal oficial da Sefaz qual documento a loja deve emitir hoje e até quando. O post SAT de um estado não se copia para outro trata dessa diferença. Este texto descreve a integração técnica e não substitui a orientação de um profissional de contabilidade.

Erros comuns

  • Chamar a DLL sem gravar o número de sessão antes. Se a resposta se perde, não há como perguntar ao aparelho o que ele fez.
  • Reenviar a venda depois de um erro de comunicação. O primeiro envio pode ter sido emitido, e o segundo vira cupom duplicado.
  • Reutilizar números de sessão ou usar um contador que reinicia. O aparelho pode recusar a chamada ou devolver a resposta de outra.
  • Copiar a convenção de chamada de outro fabricante. O processo do PDV cai de forma intermitente e difícil de diagnosticar.
  • Imprimir o extrato antes de gravar o retorno. Se o PDV fecha nesse meio-tempo, o cliente tem papel e o banco não tem cupom.
  • Tratar SAT e NF-e com o mesmo módulo de "gerar XML". Um depende de hardware na loja, o outro de web service; as falhas e as respostas são outras.

Checklist

  • O PDV consulta o SAT na abertura do caixa e mostra o estado na tela.
  • Toda chamada à DLL tem número de sessão gravado antes de acontecer.
  • Tentativas sem resposta ficam como incerta e são resolvidas com ConsultarNumeroSessao.
  • O retorno é gravado (código, mensagem, chave, XML) antes da impressão.
  • Nome da DLL, convenção de chamada e codificação estão conferidos no manual do fabricante.
  • Mensagens de recusa aparecem em português, com a ação esperada.
  • O procedimento de contingência está escrito e foi combinado com o contador.

Desligue o cabo do SAT de teste no meio de uma venda de homologação. Quando você religar, o seu PDV sabe dizer se aquele cupom foi emitido, ou a única saída é apertar "finalizar" de novo e torcer?

Avalie este artigo

Referência: https://social.msdn.microsoft.com/Forums/pt-BR/adaea637-348f-4432-b8a8-0dc8218deba3/gerar-xml-para-o-sat-fiscal