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
- Ao abrir o caixa, consulte o aparelho. Chame
ConsultarSATe mostre na tela, de forma visível, se o SAT respondeu. Descobrir que ele está desligado com a venda já registrada é pior. - Monte o XML da venda com os itens, pagamentos e a assinatura do aplicativo comercial, vinculada ao CNPJ da software house e do contribuinte.
- Gere o número de sessão e grave a tentativa no banco com status
enviando, antes de chamar a DLL. - Chame
EnviarDadosVendacom o número de sessão, o código de ativação do aparelho e o XML. - Se a resposta vier, separe os campos, grave código, mensagem, chave e o XML decodificado, e só então imprima o extrato.
- Se a resposta não vier (exceção, travamento, aparelho desconectado), marque a tentativa como
incerta. Na próxima ação, chameConsultarNumeroSessaocom o número gravado. Ela devolve o resultado daquela chamada, se o aparelho chegou a processá-la. - 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
incertae são resolvidas comConsultarNumeroSessao. - 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?