체코가 매출 전자 등록 제도를 다시 도입합니다. 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 웹 서비스입니다.
- 매출당 하나의 메시지로, 네임스페이스
http://fs.gov.cz/eet/schema/v4의Trzba요소를 TLS 1.2 이상의 HTTPS로 전송합니다. - SOAP 본문에 대한 WS-Security 서명은 배타적 정규화, SHA-256, RSA를 사용하며, 납세자 인증서는
wsse:BinarySecurityToken에 담깁니다. - 납세자 인증서는 PKCS#12 파일이며, 인증서의 일반 이름(CN)이 곧 납세자 식별자입니다.
- 전체 엔벨로프는 12 kB, 즉 12,288바이트를 넘으면 안 됩니다.
- 응답에는 pok, 수신 시각, 경고가 담기거나, 오류 코드가 담깁니다.
납세자 등록, MOJE daně 포털을 통한 인증서 발급, 등록 단위 번호 할당은 모두 코드를 실행하기 전에 이루어집니다. 이 행정 절차를 거쳐 라이브러리에 필요한 것은 PKCS#12 파일 하나와 두 개의 번호, 즉 납세자 식별자와 단위 식별자입니다.
TsgcEETClient가 왕복 과정을 처리하는 방식
Send를 한 번 호출하면 모든 단계가 다음 순서로 실행됩니다.
TsgcEETSale레코드의 모든 필드를 스키마 규칙에 따라 검증합니다. 따라서 잘못된 매출은 읽기 쉬운 사유와 함께 로컬에서 거부되며 서비스에 도달하지 않습니다.Trzba요소를 생성하고, 메시지용 새uuid_zpravy와 함께 SOAP 1.1 엔벨로프로 감쌉니다.- sgcSign 키 공급자의 키로 WS-Security에 따라 SOAP 본문에 서명합니다. PFX 파일, Windows 인증서 저장소, PKCS#11 토큰이나 스마트 카드, 클라우드 키 서비스 등 어떤 키 공급자든 사용할 수 있습니다.
- 전송하기 전에 완성된 엔벨로프의 크기를 12 kB 상한과 비교합니다.
- 세무 당국에 전송합니다. 기본 엔드포인트는 플레이그라운드이므로, 폼에 올려놓은 컴포넌트가 실수로 실제 매출을 제출하는 일은 없습니다.
- 응답에서 pok, 수신 시각, 테스트 플래그, 경고, 오류 코드를 파싱합니다.
- 확인 응답의 서명을 검증합니다. 오류 응답은 설계상 서명되지 않으므로, 거부가 서명 실패로 바뀌는 일은 없습니다.
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;
첫 실행의 성공 여부는 몇 가지 세부 사항에 달려 있습니다.
LoadFromFile을 호출하십시오.FileName과Password를 설정하는 것만으로는 값이 기록될 뿐이며,BuildMessage는 인증서 없이는 메시지 생성을 거부합니다.TaxpayerEIC는 서명 인증서의 일반 이름과 같아야 합니다. 그렇지 않으면 응답에 경고 1이 포함됩니다.Certificate.SubjectCN을 읽으면 이 조건이 보장됩니다.- 실제로 할당된 등록 단위 번호는 두 자리 이상이며 1, 2, 3, 4 중 하나로 끝납니다. 1 같은 임의의 값은 검증을 통과하지만 경고 6을 받습니다.
- 계산원이 입력한 금액은
sgcEETParseAmount로 읽으십시오. 머신의 소수점 구분 기호를 따르는StrToCurr는 사용하지 마십시오.
플레이그라운드용으로 세무 당국은 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 지원팀이 가장 먼저 요청하는 값이기 때문입니다. LastRequestXML과 LastResponseXML은 두 메시지를 전송된 그대로 보관합니다.
회선이 끊겼을 때: 오프라인 대기열
계산대는 연결이 끊겨도 계속 판매할 수 있어야 합니다. TsgcEETClient는 왕복 과정을 나누어, POS 시스템이 메시지를 대기열에 넣었다가 나중에 보낼 수 있게 합니다.
BuildMessage는 엔벨로프를 전송하지 않고 검증, 생성, 서명, 크기 측정까지 수행합니다. 사용된uuid_zpravy는LastMessageUUID에 있습니다.SendRaw는 저장된 엔벨로프를 원래의uuid_zpravy와prvni_zaslani를 포함해 변경 없이 전송합니다. 머신을 한 번도 떠나지 않은 메시지에는 이 방식이 적합합니다.Resend는 전송되었지만 확인 응답을 받지 못한 매출을 새uuid_zpravy와 false로 설정한prvni_zaslani로 다시 보냅니다. 이것이 명세가 설명하는 재시도 방식입니다.
// 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월의 오프셋이 찍힙니다. 오프셋을 매출과 함께 저장하고, 다시 보낼 때 SaleOffsetMinutes와 HasSaleOffsetMinutes를 설정하십시오.
확인 응답 검증하기
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, 서버, 명령줄
- C++Builder는 동일한 컴포넌트를 사용하며,
Demos\CBuilder\EET는 Delphi 데모와 같은 구성입니다. - .NET에는 동일한 API를 갖춘
TsgcEETClient와demos\EET의 WinForms 데모가 있습니다. - sgcSign Server는
POST /api/v1/sign/eet경로를 추가합니다. 이 경로는 서버가 보유한 키로 매출을 생성하고 서명하며, 제출한 뒤 응답을 반환할 수도 있습니다. 따라서 체인점의 계산대들이 각자 사본을 보관하는 대신 서버에 보관된 납세자 인증서 하나를 공유할 수 있습니다. sgcsign명령줄 도구에는 이에 대응하는eet동사가 있습니다. JSON 파일에서 매출을 읽으며, 종료 코드로 계산대 스크립트에 결과를 알려 줍니다. 6은 세무 당국이 매출을 거부했음을, 5는 서버가 서명하지 못했거나 세무 당국에 연결하지 못했음을, 4는 sgcSign 서버 자체에 연결할 수 없었음을 뜻합니다.
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와 함께 보내 주십시오. 코드를 작성한 사람들로부터 직접 답변을 받으실 수 있습니다.
