Delphi로 구현하는 체코 EET 2.0: TsgcEETClient로 매출 등록하기

· 컴포넌트
Delphi로 구현하는 체코 EET 2.0: TsgcEETClient로 매출 등록하기 | eSeGeCe 블로그

체코가 매출 전자 등록 제도를 다시 도입합니다. EET 2.0(Elektronická evidence tržeb)에서는 POS 시스템이 매출이 발생할 때마다 이를 세무 당국에 보고하고, 세무 당국은 매출이 보고되었다는 증거인 확인 코드, 즉 pok으로 응답합니다. 보고는 2027년 1월 1일부터 시작되며, 계산대 시스템을 구축하고 테스트하는 플레이그라운드는 이미 열려 있습니다.

sgcSign은 이를 위한 새 컴포넌트 TsgcEETClient를 제공합니다. 이 컴포넌트는 매출을 검증하고, 메시지를 생성하고, 납세자 인증서로 서명하여 전송한 뒤, 확인 응답의 서명을 점검하고 결과를 돌려줍니다. 이 글에서는 EET 2.0이 요구하는 사항, 컴포넌트가 왕복 과정을 처리하는 방식, 그리고 첫 매출, 오프라인 대기열, 검증된 확인 응답을 위한 Delphi 코드를 설명합니다.

Delphi 데모에서 검증 모드부터 실제 pok까지 진행한 매출 등록 과정입니다. YouTube에서도 보기.

EET 2.0은 업데이트가 아니라 새로운 프로토콜입니다

첫 번째 EET 제도에 맞춘 계산대 시스템을 만들었다면, 처음부터 새로 시작하십시오. 데이터 인터페이스 버전 4.1은 이전 버전 3.1과 호환되지 않으며, 더 단순합니다. 계산해야 할 PKP나 BKP 보안 코드가 없고, VAT 내역도, TLS 클라이언트 인증서도 없습니다. 매출 하나는 열 개의 데이터 속성으로 이루어집니다. 남는 것은 표준 SOAP 1.1 웹 서비스입니다.

납세자 등록, MOJE daně 포털을 통한 인증서 발급, 등록 단위 번호 할당은 모두 코드를 실행하기 전에 이루어집니다. 이 행정 절차를 거쳐 라이브러리에 필요한 것은 PKCS#12 파일 하나와 두 개의 번호, 즉 납세자 식별자와 단위 식별자입니다.

TsgcEETClient가 왕복 과정을 처리하는 방식

Send를 한 번 호출하면 모든 단계가 다음 순서로 실행됩니다.

  1. TsgcEETSale 레코드의 모든 필드를 스키마 규칙에 따라 검증합니다. 따라서 잘못된 매출은 읽기 쉬운 사유와 함께 로컬에서 거부되며 서비스에 도달하지 않습니다.
  2. Trzba 요소를 생성하고, 메시지용 새 uuid_zpravy와 함께 SOAP 1.1 엔벨로프로 감쌉니다.
  3. sgcSign 키 공급자의 키로 WS-Security에 따라 SOAP 본문에 서명합니다. PFX 파일, Windows 인증서 저장소, PKCS#11 토큰이나 스마트 카드, 클라우드 키 서비스 등 어떤 키 공급자든 사용할 수 있습니다.
  4. 전송하기 전에 완성된 엔벨로프의 크기를 12 kB 상한과 비교합니다.
  5. 세무 당국에 전송합니다. 기본 엔드포인트는 플레이그라운드이므로, 폼에 올려놓은 컴포넌트가 실수로 실제 매출을 제출하는 일은 없습니다.
  6. 응답에서 pok, 수신 시각, 테스트 플래그, 경고, 오류 코드를 파싱합니다.
  7. 확인 응답의 서명을 검증합니다. 오류 응답은 설계상 서명되지 않으므로, 거부가 서명 실패로 바뀌는 일은 없습니다.

Delphi에서 첫 매출 등록하기

명세는 납세자에게 검증 모드로 시작하라고 안내합니다. 메시지는 실제 메시지와 똑같이 완전히 점검된 뒤 폐기되므로, 아무것도 제출되지 않습니다. 점검을 통과하면 인증서, 서명, TLS 연결, 매출의 모든 필드가 올바르다는 뜻입니다. 아래 코드는 먼저 이 점검을 실행한 다음 매출을 실제로 제출합니다.

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;

첫 실행의 성공 여부는 몇 가지 세부 사항에 달려 있습니다.

플레이그라운드용으로 세무 당국은 CZ00000019를 포함한 공용 테스트 인증서를 eet.gov.cz에 공개합니다. 플레이그라운드의 확인 응답에는 test="true"ff로 끝나는 pok이 담기며, 이는 실제 매출에 대해 아무것도 증명하지 않습니다.

응답 읽기

프로토콜에서 가장 당황하기 쉬운 부분입니다. 거부를 포함한 모든 결과가 HTTP 200으로 도착하므로, HTTP 상태로는 아무것도 알 수 없습니다. 또한 검증 모드의 성공은 코드 0을 담은 오류 요소 안에 도착하므로, 완전히 정상적인 검증 실행에서도 TsgcEETResponse.IsError가 True입니다.

sgcEETResponseOutcome은 두 규칙을 모두 적용하여 세 가지 결과 중 하나를 반환합니다.

결과의미
eoAcknowledged매출이 보고되었으며 pok은 TsgcEETResponse.POK에 있습니다. 매출을 보고하는 유일한 결과입니다.
eoVerified검증 모드의 성공입니다. 아무것도 제출되지 않았습니다.
eoRejected그 밖의 모든 경우입니다. 매출이 보고되지 않았으며 여전히 세무 당국에 보고해야 합니다.

경고는 치명적이지 않습니다. 유효한 확인 응답에 최대 10개까지 함께 담길 수 있으며, OnWarning은 경고마다 한 번씩 발생하고, OnError는 코드가 0이 아닌 오류에 대해 발생합니다. 메시지를 보낼 때마다 LastTransactionId, 즉 X-Global-Transaction-Id 응답 헤더를 기록하십시오. EET 지원팀이 가장 먼저 요청하는 값이기 때문입니다. LastRequestXMLLastResponseXML은 두 메시지를 전송된 그대로 보관합니다.

회선이 끊겼을 때: 오프라인 대기열

계산대는 연결이 끊겨도 계속 판매할 수 있어야 합니다. TsgcEETClient는 왕복 과정을 나누어, POS 시스템이 메시지를 대기열에 넣었다가 나중에 보낼 수 있게 합니다.

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

대기열을 만들기 전에 알아 둘 함정이 하나 있습니다. 매출 시각은 시간대 오프셋과 함께 기록되며, 매출 레코드에 자체 오프셋이 없으면 라이브러리는 메시지를 생성하는 시점의 머신 오프셋을 사용합니다. 7월의 매출을 12월에 Resend로 다시 보내면 12월의 오프셋이 찍힙니다. 오프셋을 매출과 함께 저장하고, 다시 보낼 때 SaleOffsetMinutesHasSaleOffsetMinutes를 설정하십시오.

확인 응답 검증하기

VerifyResponseSignature는 기본값이 True이므로, 별도 설정 없이 모든 확인 응답의 서명이 점검됩니다. 인증서 체인까지 점검하려면 올바른 신뢰 앵커가 필요한데, 이는 짐작하기 쉬운 인증서가 아닙니다. 확인 응답은 플레이그라운드 테스트 자료에 포함된 EET 인증서가 아니라 상용 I.CA 인증서로 서명되며, 두 I.CA 발급자 모두 Windows 루트 저장소에 없습니다. I.CA Root CA/RSA 05/2022와 I.CA Public CA/RSA 06/2022를 ica.cz에서 다운로드하여 앵커로 지정하십시오.

oClient.TrustedCertificates.Add('ica-root-ca-rsa-05-2022.cer');
oClient.TrustedCertificates.Add('ica-public-ca-rsa-06-2022.cer');
oClient.RequireTrustedChain := True;

점검이 실패하면 LastVerificationDetails가 실패한 단계를 알려 줍니다. 확인 응답은 도착했지만 서명이 검증되지 않으면 Send는 예외를 발생시킵니다. 그래도 LastResponse에는 pok을 포함한 파싱된 응답이 그대로 남아 있으므로, 이미 등록된 매출이 실수로 두 번 전송되는 일은 없습니다.

12 kB 상한

서비스는 12 kB보다 큰 메시지를 오류 코드 7로 거부하며, BuildMessage는 전송하기 전에 크기를 점검합니다. 매출 필드는 모두 스키마로 길이가 제한되므로, 엔벨로프에서 크기가 실제로 달라지는 부분은 wsse:BinarySecurityToken에 담긴 서명 인증서뿐입니다. 엔벨로프에 SOAP 헤더가 정확히 하나만 있고, 컴포넌트가 헤더를 추가하는 방법을 제공하지 않는 것도 이 때문입니다.

C++Builder, .NET, 서버, 명령줄

set SGCSIGN_SERVER=https://sign.shop.local:8443
set SGCSIGN_APIKEY=sgcsk_...

sgcsign eet --provider eet-taxpayer --submit sale.json

직접 사용해 보기

Demos\Delphi\EET의 Delphi 데모는 하나의 폼에서 플레이그라운드를 대상으로 전체 왕복 과정을 보여 줍니다. 테스트 인증서를 로드하고, 검증 모드로 전송한 다음, 체크를 해제하고 실제 매출을 전송하면 pok이 담긴 확인 응답을 받을 수 있습니다. Build Message (no send)는 오프라인 대기열에 저장될 서명된 엔벨로프를 보여 주고, Resend Stored Sale은 마지막 매출을 반복 제출로 다시 보냅니다. 테스트 인증서를 배포하는 문서는 공개가 제한되어 있어 데모에 포함되지 않으므로, eet.gov.cz에서 다운로드하십시오.

모든 속성, 메서드, 이벤트는 sgcSign 온라인 도움말에 문서화되어 있으며, sgcSign 국가 프로파일 페이지의 EET 2.0 섹션에서 컴포넌트의 요약을 볼 수 있습니다.

제공 안내

TsgcEETClient는 Delphi, C++Builder, .NET용 sgcSign 2026.10에 포함되며, sgcSign Server 경로와 명령줄 도구의 eet 동사도 함께 제공됩니다.

질문이 있거나 1월까지 준비해야 할 계산대가 있으신가요? 문의하기. 예상대로 동작하지 않는 부분이 있다면 요청 및 응답 XML을 X-Global-Transaction-Id와 함께 보내 주십시오. 코드를 작성한 사람들로부터 직접 답변을 받으실 수 있습니다.