A República Tcheca está trazendo de volta o registro eletrônico de vendas. No EET 2.0 (Elektronická evidence tržeb), um ponto de venda informa cada venda à autoridade fiscal no momento em que ela acontece, e a autoridade fiscal responde com um código de confirmação, o pok, que é a prova de que a venda foi informada. O envio das vendas começa em 1º de janeiro de 2027, e o playground onde os sistemas de caixa são desenvolvidos e testados já está aberto.
O sgcSign tem um novo componente para isso, o TsgcEETClient. Ele valida a venda, monta a mensagem, assina com o certificado do contribuinte, envia, confere a assinatura da confirmação e devolve o resultado. Este post explica o que o EET 2.0 exige, como o componente faz o ciclo completo e o código Delphi para uma primeira venda, uma fila offline e uma confirmação verificada.
Uma venda registrada na demo Delphi, do modo de verificação até um pok real. Também no YouTube.
O EET 2.0 é um novo protocolo, não uma atualização
Se você desenvolveu um sistema de caixa para o primeiro regime do EET, comece do zero. A versão 4.1 da interface de dados não é compatível com a antiga versão 3.1, e é mais simples: não há código de segurança PKP ou BKP para calcular, não há discriminação do IVA e não há certificado TLS de cliente. Uma venda tem dez atributos de dados. O que resta é um serviço web SOAP 1.1 padrão:
- Uma mensagem por venda, um elemento
Trzbano namespacehttp://fs.gov.cz/eet/schema/v4, enviada por HTTPS com TLS 1.2 ou superior. - Uma assinatura WS-Security sobre o corpo SOAP, com canonicalização exclusiva, SHA-256 e RSA, e o certificado do contribuinte levado em um
wsse:BinarySecurityToken. - O certificado do contribuinte é um arquivo PKCS#12, e o seu nome comum é o identificador do contribuinte.
- O envelope inteiro não pode passar de 12 kB, ou seja, 12.288 bytes.
- A resposta traz o pok, o horário de recebimento e eventuais avisos, ou então um código de erro.
O cadastro como contribuinte, a obtenção do certificado pelo portal MOJE daně e a atribuição de um número de unidade de registro acontecem antes de qualquer código ser executado. Dessa papelada, a biblioteca precisa de um arquivo PKCS#12 e de dois números, o identificador do contribuinte e o identificador da unidade.
Como o TsgcEETClient faz o ciclo completo
Uma chamada a Send executa todas as etapas, nesta ordem:
- Valida cada campo do registro
TsgcEETSaleconforme as regras do schema, de modo que uma venda inválida é recusada localmente com um motivo legível e nunca chega ao serviço. - Monta o elemento
Trzbae o envolve em um envelope SOAP 1.1, com um novouuid_zpravypara a mensagem. - Assina o corpo SOAP com WS-Security usando a chave de qualquer provedor de chave do sgcSign: um arquivo PFX, o repositório de certificados do Windows, um token PKCS#11 ou smart card, ou um serviço de chaves na nuvem.
- Mede o envelope pronto em relação ao limite de 12 kB antes de enviar qualquer coisa.
- Envia o envelope à autoridade fiscal. O playground é o endpoint padrão, então um componente colocado em um formulário não consegue registrar uma venda real por acidente.
- Interpreta a resposta: o pok, o horário de recebimento, o sinalizador de teste, os avisos e o código de erro.
- Verifica a assinatura da confirmação. As respostas de erro não são assinadas por definição, então uma rejeição nunca se transforma em falha de assinatura.
Sua primeira venda em Delphi
A especificação orienta os contribuintes a começar pelo modo de verificação. A mensagem é conferida por completo, exatamente como uma real, e depois descartada, então nada é registrado. Se ela passar, o certificado, a assinatura, a conexão TLS e todos os campos da venda estão corretos. O código abaixo executa essa verificação primeiro e depois registra a venda de verdade.
var
oProvider: TsgcPFXKeyProvider;
oClient: TsgcEETClient;
oSale: TsgcEETSale;
oResponse: TsgcEETResponse;
begin
oProvider := TsgcPFXKeyProvider.Create(nil);
oClient := TsgcEETClient.Create(nil);
try
oProvider.FileName := 'CZ00000019.p12';
oProvider.Password := '...';
// Without LoadFromFile the certificate is empty and the message would
// carry no token for the tax authority to verify the signature with.
oProvider.LoadFromFile;
oClient.KeyProvider := oProvider as IsgcKeyProvider;
oClient.Environment := eetPlayground;
sgcEETInitSale(oSale);
oSale.SendDateTime := Now;
oSale.SaleDateTime := Now;
oSale.FirstSending := True;
// The common name of an EET certificate IS the taxpayer identifier.
oSale.TaxpayerEIC := oProvider.Certificate.SubjectCN;
oSale.UnitID := 11;
oSale.PosID := '1';
oSale.ReceiptNumber := '0/6460/ZQ42';
oSale.TotalAmount := 349;
// Verification mode first. Nothing is filed.
oClient.VerificationMode := True;
oResponse := oClient.Send(oSale);
if sgcEETResponseOutcome(oResponse) <> eoVerified then
raise Exception.CreateFmt('Verification failed, code %d: %s',
[oResponse.ErrorCode, oResponse.ErrorText]);
// Now for real. Only eoAcknowledged reports a sale.
oClient.VerificationMode := False;
oResponse := oClient.Send(oSale);
if sgcEETResponseOutcome(oResponse) = eoAcknowledged then
PrintReceipt(oResponse.POK, oResponse.Test) // your own routine
else
// Not filed. Store the sale and replay it later with Resend.
QueueSale(oSale); // your own routine
finally
oClient.Free;
oProvider.Free;
end;
end;
Alguns detalhes decidem se essa primeira execução funciona:
- Chame
LoadFromFile. DefinirFileNameePasswordapenas guarda os valores, eBuildMessagese recusa a montar uma mensagem sem certificado. TaxpayerEICdeve ser igual ao nome comum do certificado de assinatura, caso contrário a resposta traz o aviso 1. LerCertificate.SubjectCNgarante isso.- Um número de unidade de registro realmente atribuído tem pelo menos dois dígitos e termina em 1, 2, 3 ou 4. Um valor inventado como 1 passa na validação, mas gera o aviso 6.
- Leia um valor digitado por um operador de caixa com
sgcEETParseAmount, e não comStrToCurr, que segue o separador decimal da máquina.
Para o playground, a administração tributária publica certificados de teste compartilhados, entre eles o CZ00000019, em eet.gov.cz. Uma confirmação do playground traz test="true" e um pok terminado em ff, o que não prova nada sobre uma venda real.
Lendo a resposta
Esta é a parte do protocolo com mais chances de surpreender você. Todo resultado chega como HTTP 200, inclusive uma rejeição, então o status HTTP não diz nada. E um sucesso no modo de verificação chega dentro de um elemento de erro com o código 0, então TsgcEETResponse.IsError é True em uma execução de verificação perfeitamente correta.
O sgcEETResponseOutcome aplica as duas regras e retorna uma de três respostas:
| Resultado | Significado |
|---|---|
eoAcknowledged | A venda foi informada e o pok está em TsgcEETResponse.POK. Este é o único resultado que informa uma venda. |
eoVerified | O sucesso do modo de verificação. Nada foi registrado. |
eoRejected | Todo o resto. A venda não foi informada e ainda precisa ser informada à autoridade fiscal. |
Os avisos não são críticos. Até dez deles podem acompanhar uma confirmação válida, e OnWarning é disparado uma vez para cada um, enquanto OnError é disparado para um erro cujo código não é 0. Após cada mensagem, registre em log o LastTransactionId, o header de resposta X-Global-Transaction-Id, porque é a primeira coisa que o suporte do EET pede. LastRequestXML e LastResponseXML guardam as duas mensagens exatamente como trafegaram.
Quando a conexão cai: uma fila offline
Um caixa precisa continuar vendendo quando a conexão cai. O TsgcEETClient divide o ciclo completo para que um ponto de venda possa enfileirar mensagens e enviá-las depois:
BuildMessagevalida, monta, assina e mede o envelope sem enviá-lo. Ouuid_zpravyusado fica emLastMessageUUID.SendRawenvia um envelope armazenado sem alterações, incluindo ouuid_zpravye oprvni_zaslanioriginais, o que é o correto para uma mensagem que nunca saiu da máquina.Resendrepete uma venda que foi enviada mas nunca confirmada, com um novouuid_zpravyeprvni_zaslanidefinido como false, que é a nova tentativa descrita pela especificação.
// The line is down: sign the message now and keep it
sEnvelope := oClient.BuildMessage(oSale);
StoreInQueue(oClient.LastMessageUUID, sEnvelope); // your own storage
// The line is back: post the stored envelope exactly as it was built
oResponse := oClient.SendRaw(sEnvelope);
// Sent earlier but no answer arrived: replay the sale as a repeat
oResponse := oClient.Resend(oSale);
Vale conhecer uma armadilha antes de montar a fila. O horário da venda é gravado com um deslocamento de fuso horário e, a menos que o registro da venda traga o seu próprio deslocamento, a biblioteca usa o deslocamento da máquina no momento em que a mensagem é montada. Uma venda de julho repetida com Resend em dezembro seria carimbada com o deslocamento de dezembro. Guarde o deslocamento junto com a venda e defina SaleOffsetMinutes e HasSaleOffsetMinutes ao repeti-la.
Verificando a confirmação
VerifyResponseSignature é True por padrão, então a assinatura de cada confirmação já é conferida sem nenhuma configuração. Conferir também a cadeia de certificados exige as âncoras de confiança certas, e elas não são as óbvias. A confirmação é assinada por um certificado comercial da I.CA, e não pelos certificados EET que acompanham o material de teste do playground, e nenhum dos dois emissores da I.CA está no repositório raiz do Windows. Baixe I.CA Root CA/RSA 05/2022 e I.CA Public CA/RSA 06/2022 em ica.cz e informe-os como âncoras:
oClient.TrustedCertificates.Add('ica-root-ca-rsa-05-2022.cer');
oClient.TrustedCertificates.Add('ica-public-ca-rsa-06-2022.cer');
oClient.RequireTrustedChain := True;
Se uma verificação falhar, LastVerificationDetails informa a etapa que falhou. Quando uma confirmação chega, mas a sua assinatura não confere, Send gera uma exceção, e LastResponse ainda contém a resposta interpretada com o seu pok, de modo que uma venda já registrada nunca é enviada duas vezes por engano.
O limite de 12 kB
O serviço recusa uma mensagem maior que 12 kB com o código de erro 7, e BuildMessage verifica o tamanho antes de enviar qualquer coisa. Cada campo da venda tem um limite definido pelo schema, então a única parte do envelope cujo tamanho realmente varia é o certificado de assinatura no wsse:BinarySecurityToken. É também por isso que o envelope traz exatamente um header SOAP, e por isso o componente não oferece nenhuma forma de adicionar outro.
C++Builder, .NET, o servidor e a linha de comando
- O C++Builder usa o mesmo componente, e
Demos\CBuilder\EETespelha a demo Delphi. - O .NET tem o
TsgcEETClientcom a mesma API e uma demo WinForms emdemos\EET. - O sgcSign Server adiciona
POST /api/v1/sign/eet, que monta e assina uma venda com uma chave mantida pelo servidor, e pode enviá-la e retornar a resposta. Assim, uma rede de caixas pode compartilhar um único certificado do contribuinte guardado no servidor, em vez de cada caixa manter uma cópia. - A ferramenta de linha de comando
sgcsigntem o verboeetcorrespondente. Ela lê a venda de um arquivo JSON, e o seu código de saída informa a um script de caixa o que aconteceu: 6 significa que a autoridade fiscal rejeitou a venda, 5 significa que o servidor não conseguiu assiná-la ou não conseguiu alcançar a autoridade fiscal, e 4 significa que o próprio servidor sgcSign não pôde ser alcançado.
set SGCSIGN_SERVER=https://sign.shop.local:8443
set SGCSIGN_APIKEY=sgcsk_...
sgcsign eet --provider eet-taxpayer --submit sale.json
Experimente
A demo Delphi em Demos\Delphi\EET percorre todo o ciclo contra o playground em um único formulário. Carregue um certificado de teste, envie em modo de verificação, depois desmarque essa opção e envie uma venda real para obter uma confirmação com o seu pok. O botão Build Message (no send) mostra o envelope assinado que uma fila offline armazenaria, e Resend Stored Sale repete a última venda como um reenvio. Os certificados de teste não acompanham a demo, porque o documento que os distribui é restrito, então baixe-os em eet.gov.cz.
Todas as propriedades, métodos e eventos estão documentados na ajuda online do sgcSign, e a seção EET 2.0 da página de perfis por país do sgcSign resume o componente.
Disponibilidade
O TsgcEETClient está disponível no sgcSign 2026.10 para Delphi, C++Builder e .NET, junto com a rota do sgcSign Server e o verbo eet da ferramenta de linha de comando.
Dúvidas, ou um caixa que precisa estar pronto para janeiro? Entre em contato. Se algo não se comportar como você espera, envie o XML da requisição e da resposta junto com o X-Global-Transaction-Id, e você receberá uma resposta das pessoas que escreveram o código.
