EET 2.0 tcheco em Delphi: registre vendas com TsgcEETClient

· Componentes
EET 2.0 tcheco em Delphi: registre vendas com TsgcEETClient

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:

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:

  1. Valida cada campo do registro TsgcEETSale conforme as regras do schema, de modo que uma venda inválida é recusada localmente com um motivo legível e nunca chega ao serviço.
  2. Monta o elemento Trzba e o envolve em um envelope SOAP 1.1, com um novo uuid_zpravy para a mensagem.
  3. 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.
  4. Mede o envelope pronto em relação ao limite de 12 kB antes de enviar qualquer coisa.
  5. 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.
  6. Interpreta a resposta: o pok, o horário de recebimento, o sinalizador de teste, os avisos e o código de erro.
  7. 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:

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:

ResultadoSignificado
eoAcknowledgedA venda foi informada e o pok está em TsgcEETResponse.POK. Este é o único resultado que informa uma venda.
eoVerifiedO sucesso do modo de verificação. Nada foi registrado.
eoRejectedTodo 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:

// 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

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.